I wrote a trivial bare-metal app for Qemu (raspi3b):
loader.s
.global _start
_start:
BL run
BL .
stdio.h
#ifndef __STDIO_H__
#define __STDIO_H__
#define AUXENB 0x7e215004
#define AUX_MU_CNTL_REG 0x7e215060
#define AUX_MU_IER_REG 0x7e215044
#define AUX_MU_LCR_REG 0x7e21504c
#define AUX_MU_MCR_REG 0x7e215050
#define AUX_MU_BAUD 0x7e215068
#define AUX_MU_IIR_REG 0x7e215048
#define AUX_MU_CNTL_REG 0x7e215060
#define AUX_MU_IO_REG 0x7e215040
#endif //__STDIO_H__
kernel.c
#include "stdio.h"
void enable_mini_uart(void) {
// Set AUXENB register to enable mini UART. Then mini UART register can be accessed.
unsigned long * auxenb = (unsigned long *)AUXENB;
*auxenb |= 0x1;
// Set AUX_MU_CNTL_REG to 0. Disable transmitter and receiver during configuration.
unsigned long * aux_mu_cntl_reg = (unsigned long *)AUX_MU_CNTL_REG;
*aux_mu_cntl_reg &= 0xfffffffe;
// Set AUX_MU_IER_REG to 0. Disable interrupt because currently you don’t need interrupt.
unsigned long * aux_mu_ier_reg = (unsigned long *)AUX_MU_IER_REG;
*aux_mu_ier_reg &= 0xfffffffe;
// Set AUX_MU_LCR_REG to 3. Set the data size to 8 bit.
unsigned long * aux_mu_lcr_reg = (unsigned long *)AUX_MU_LCR_REG;
*aux_mu_lcr_reg |= 0x3;
// Set AUX_MU_MCR_REG to 0. Don’t need auto flow control.
unsigned long * aux_mu_mcr_reg = (unsigned long *)AUX_MU_MCR_REG;
*aux_mu_mcr_reg &= 0xfffffffd;
// Set AUX_MU_BAUD to 270. Set baud rate to 115200
unsigned long * aux_mu_baud = (unsigned long *)AUX_MU_BAUD;
*aux_mu_baud = 0x10e;
// Set AUX_MU_IIR_REG to 6. No FIFO.
unsigned long * aux_mu_iir_reg = (unsigned long *)AUX_MU_IIR_REG;
*aux_mu_iir_reg |= 0x6;
// Set AUX_MU_CNTL_REG to 3. Enable the transmitter and receiver.
*aux_mu_cntl_reg |= 0x3;
}
void run(void) {
enable_mini_uart();
unsigned long * aux_mu_io_reg = (unsigned long *)AUX_MU_IO_REG;
const char * str = "Hello!";
for (int i = 0; i < 7; ++i) {
*aux_mu_io_reg = *(str + i);
}
}
linker.ld
ENTRY(_start)
SECTIONS {
. = 0x80000;
.text : { *(.text) }
}
Makefile
# Build directory path
BUILD_DIR=build
# Toolchain path
BINTOOLS_PATH=/opt/homebrew/Cellar/aarch64-elf-binutils/2.41/bin
# LLVM path
LLVM_PATH=/opt/homebrew/opt/llvm/bin
# Qemu path
QEMU_PATH=/opt/homebrew/Cellar/qemu/8.1.1
# Tools aliases
OBJCOPY=$(LLVM_PATH)/llvm-objcopy
OBJDUMP=$(LLVM_PATH)/llvm-objdump
HEXDUMP=hexdump
CC=$(LLVM_PATH)/clang
AS=$(BINTOOLS_PATH)/aarch64-elf-as
LD=$(BINTOOLS_PATH)/aarch64-elf-ld
# Headers and sources paths
HEADER_PATH=include
SRC_PATH=src
# C sources
C_SRCS := $(wildcard $(SRC_PATH)/*.c)
# ASM sources
ASM_SRCS := $(wildcard $(SRC_PATH)/*.s)
# C objects
C_OBJS := $(C_SRCS:$(SRC_PATH)/%.c=$(BUILD_DIR)/%.o)
# ASM objects
ASM_OBJS := $(ASM_SRCS:$(SRC_PATH)/%.s=$(BUILD_DIR)/%.o)
# C targets
C_TARGETS := $(C_SRCS:$(SRC_PATH)/%.c=%)
# ASM targets
ASM_TARGETS := $(ASM_SRCS:$(SRC_PATH)/%.s=%)
# Default target
all: mkdir kernel_img
# Target to build a binary kernel
kernel_img: kernel_elf
$(OBJCOPY) $(BUILD_DIR)/kernel.elf -O binary $(BUILD_DIR)/kernel.img
# Target to build an ELF kernel
kernel_elf: $(ASM_TARGETS) $(C_TARGETS)
$(LD) -m aarch64elf -nostdlib -T linker.ld $(ASM_OBJS) $(C_OBJS) -o $(BUILD_DIR)/kernel.elf
# Target to compile loader.s
$(ASM_TARGETS): %: $(SRC_PATH)/%.s
$(AS) $< -o $(BUILD_DIR)/$@.o
# Targets to compile C sources
$(C_TARGETS): %: $(SRC_PATH)/%.c
$(CC) -c \
--target=aarch64-none-linux \
-Wall \
-O2 \
-fomit-frame-pointer \
-fno-exceptions \
-Wno-incompatible-library-redeclaration \
-fno-asynchronous-unwind-tables \
-fno-unwind-tables \
-I$(HEADER_PATH) \
-o $(BUILD_DIR)/$@.o $<
# Target to ensure if the build directory was created
mkdir:
mkdir -p $(BUILD_DIR)
# Target to run Qemu
run:
qemu-system-aarch64 -M raspi3b \
-display none \
-serial null \
-serial stdio \
-kernel $(BUILD_DIR)/kernel.img
However, when I launch it in Qemu:
qemu-system-aarch64 -M raspi3b \
-display none \
-serial null \
-serial stdio \
-kernel $(BUILD_DIR)/kernel.img
my app prints a nothing although registers were changed and Qemu has executed the code.
I setup mini UART port by manual that I've found in the Internet, so it can be a not working code. However, I don't still know how it must be then.
P.S. My host machine is MacBook Pro (macOS, M1 Pro) if it's important.
Finally, I found problems...
Addresses 0x7exxxxxx
are video core's addresses, MMU maps them to 0x3fxxxxxx
in the physical memory so stdio.h
will look as below:
#ifndef __STDIO_H__
#define __STDIO_H__
#define AUXENB 0x3f215004
#define AUX_MU_CNTL_REG 0x3f215060
#define AUX_MU_IER_REG 0x3f215044
#define AUX_MU_LCR_REG 0x3f21504c
#define AUX_MU_MCR_REG 0x3f215050
#define AUX_MU_BAUD 0x3f215068
#define AUX_MU_IIR_REG 0x3f215048
#define AUX_MU_CNTL_REG 0x3f215060
#define AUX_MU_IO_REG 0x3f215040
#endif //__STDIO_H__
Clang as any compiler can reorder instructions to optimise the execution speed. As a rule it works correctly. So, clang doesn't know that addresses above belong to the special memory and some bits were set after the output. There are few ways to fix such problems, in my case I need to use asm volatile("": : :"memory");
:
typedef unsigned long long ulong;
void enable_mini_uart(void) {
// Set AUXENB register to enable mini UART. Then mini UART register can be accessed.
ulong * auxenb = (ulong *)AUXENB;
*auxenb |= 0x1;
// Set AUX_MU_CNTL_REG to 0. Disable transmitter and receiver during configuration.
ulong * aux_mu_cntl_reg = (ulong *)AUX_MU_CNTL_REG;
*aux_mu_cntl_reg &= 0xfffffffe;
// Set AUX_MU_IER_REG to 0. Disable interrupt because currently you don’t need interrupt.
ulong * aux_mu_ier_reg = (ulong *)AUX_MU_IER_REG;
*aux_mu_ier_reg &= 0xfffffffe;
// Set AUX_MU_LCR_REG to 3. Set the data size to 8 bit.
ulong * aux_mu_lcr_reg = (ulong *)AUX_MU_LCR_REG;
*aux_mu_lcr_reg |= 0x3;
// Set AUX_MU_MCR_REG to 0. Don’t need auto flow control.
ulong * aux_mu_mcr_reg = (ulong *)AUX_MU_MCR_REG;
*aux_mu_mcr_reg &= 0xfffffffd;
// Set AUX_MU_BAUD to 270. Set baud rate to 115200
ulong * aux_mu_baud = (ulong *)AUX_MU_BAUD;
*aux_mu_baud = 0x10e;
// Set AUX_MU_IIR_REG to 6. No FIFO.
ulong * aux_mu_iir_reg = (ulong *)AUX_MU_IIR_REG;
*aux_mu_iir_reg |= 0x6;
// Set AUX_MU_CNTL_REG to 3. Enable the transmitter and receiver.
*aux_mu_cntl_reg |= 0x3;
}
void run(void) {
enable_mini_uart();
asm volatile("": : :"memory");
ulong * aux_mu_io_reg = (ulong *)AUX_MU_IO_REG;
*aux_mu_io_reg = 'V';
}
Now I can see the output but there is still yet one bug, unfortunately, the code above prints VVVV
instead of V
.