QEMUのシステムエミュレーションを使ってRaspberry Pi 3のUARTをベアメタル動作させてみました。
Ubuntu 18.04で試してました。
準備
Raspberry Pi 3のシステムエミュレーションはQEMUが新しくないと対応していないので、QEMU 2.12をコンパイルして使いました。
% sudo apt build-dep qemu
% sudo apt install gcc-aarch64-linux-gnu
$ git clone git://git.qemu.org/qemu.git
$ cd qemu
$ git checkout -b v2.12.0-release refs/tags/v2.12.0
$ git submodule update --init
$ ./configure --prefix=`pwd`/build --target-list=arm-softmmu,aarch64-softmmu --audio-drv-list=alsa
$ make
$ make install
動作例
% cd src
% make
aarch64-linux-gnu-gcc -mcpu=cortex-a53 -fpic -ffreestanding -c boot.S -o boot.o
aarch64-linux-gnu-gcc -mcpu=cortex-a53 -fpic -ffreestanding -std=gnu99 -O2 -Wall -Wextra -c kernel.c -o kernel.o
aarch64-linux-gnu-gcc -T linker.ld -o kernel.elf -ffreestanding -O2 -nostdlib boot.o kernel.o
% file kernel.elf
kernel.elf: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), dynamically linked,
interpreter /lib/ld-linux-aarch64.so.1, BuildID[sha1]=cbfa706293919f9c941e3d63c748e339643c4cc9, not stripped
% cd ..
% qemu/build/bin/qemu-system-aarch64 -m 128 -M raspi3 -nographic -kernel src/kernel.elf
Hello, kernel World!
Hello, kernel World!
Hello, kernel World!
Hello, kernel World!
QEMU: Terminated
ソースコードでは1回しか出力してないのに、なぜか4回出力されます。なんでだろうと思ったけど、4コアで実行されているから4回出力されているっぽいです。
QEMUを終了するときは、Ctrl-a xを入力します。
QEMUのraspi3は、PL011のUARTがデフォルトで使われれていました。 miniuartを標準入出力で使いたい場合は-serial を2回指示すると、使えました。1個目でPL011のUARTを無視し、2個目でminiuartを標準入出力に設定します。
qemu-system-aarch64 -m 128 -M raspi3 -nographic -kernel kernel.elf -serial null -serial mon:stdio
ソースコード
ブート
boot.S
// To keep this in the first portion of the binary.
.section ".text.boot"
// Make _start global.
.globl _start
// Entry point for the kernel.
// r15 -> should begin execution at 0x0080000.
// r0 -> 0x00000000
// r1 -> 0x00000C42
// r2 -> 0x00000100 - start of ATAGS
// preserve these registers as argument for kernel_main
_start:
// Setup the stack.
mov sp, #0x0080000
// Call kernel_main
bl kernel_main
b .
UART処理
kernel.c
#include <stdint.h>
// Memory-Mapped I/O output
static inline void mmio_write(uint32_t reg, uint32_t data)
{
*(volatile uint32_t*)reg = data;
}
// Memory-Mapped I/O input
static inline uint32_t mmio_read(uint32_t reg)
{
return *(volatile uint32_t*)reg;
}
enum
{
// The base address for UART.
UART0_BASE = 0x3F201000, // for raspi2 & 3, 0x20201000 for raspi1
// The offsets for reach register for the UART.
UART0_DR = (UART0_BASE + 0x00),
UART0_FR = (UART0_BASE + 0x18),
};
void uart_putc(unsigned char c)
{
// Wait for UART to become ready to transmit.
while ( mmio_read(UART0_FR) & (1 << 5) ) { }
mmio_write(UART0_DR, c);
}
unsigned char uart_getc()
{
// Wait for UART to have received something.
while ( mmio_read(UART0_FR) & (1 << 4) ) { }
return mmio_read(UART0_DR);
}
void uart_puts(const char* str)
{
int i;
for (i = 0; str[i] != '\0'; i ++)
uart_putc((unsigned char)str[i]);
}
#if defined(__cplusplus)
extern "C" /* Use C linkage for kernel_main. */
#endif
void kernel_main(uint32_t r0, uint32_t r1, uint32_t atags)
{
// Declare as unused
(void) r0;
(void) r1;
(void) atags;
uart_puts("Hello, kernel World!\n");
while (1)
uart_putc(uart_getc());
}
UARTは初期化しないでも動いたので初期化処理は省略しました。
リンカスクリプト
linker.ld
ENTRY(_start)
SECTIONS
{
/* Starts at LOADER_ADDR. */
. = 0x80000;
__start = .;
__text_start = .;
.text :
{
KEEP(*(.text.boot))
*(.text)
}
. = ALIGN(4096); /* align to page size */
__text_end = .;
__rodata_start = .;
.rodata :
{
*(.rodata)
}
. = ALIGN(4096); /* align to page size */
__rodata_end = .;
__data_start = .;
.data :
{
*(.data)
}
. = ALIGN(4096); /* align to page size */
__data_end = .;
__bss_start = .;
.bss :
{
bss = .;
*(.bss)
}
. = ALIGN(4096); /* align to page size */
__bss_end = .;
__end = .;
}
Makefile
Makefile
CC = aarch64-linux-gnu-gcc
CFLAGS = -mcpu=cortex-a53 -fpic -ffreestanding -std=gnu99 -O2 -Wall -Wextra
ASM_FLAGS = -mcpu=cortex-a53 -fpic -ffreestanding
OBJ = boot.o kernel.o
kernel.elf: ${OBJ}
${CC} -T linker.ld -o $@ -ffreestanding -O2 -nostdlib ${OBJ}
boot.o: boot.S
${CC} ${ASM_FLAGS} -c $< -o $@
kernel.o: kernel.c
${CC} ${CFLAGS} -c $< -o $@
clean:
rm -f *.o *.elf
.PHONY: clean