Course 1: Stage 1 – Basic Preparation (Week 1)
Topic: Bare-metal program development, PL011 UART communication, ARMv8 boot process
1.1 Basics of Bare-metal Programming
Core Concepts:
-
Bare-metal Program: Runs directly on hardware without operating system support.
-
Boot Process:
-
The CPU starts executing instructions from the reset address (usually
<span>0x0</span>
or<span>0x8000</span>
). -
Hardware (such as memory and peripherals) needs to be manually initialized.
PL011 UART:
-
ARM’s serial controller for input and output.
-
Base address of registers:
<span>0x9000000</span>
(default address for QEMU<span>virt</span>
machine).
1.2 Development Environment Setup
Toolchain Installation:
# Install cross-compiler and QEMU (Linux example)
sudo apt install gcc-aarch64-linux-gnu qemu-system-arm
Verify Toolchain:
aarch64-linux-gnu-gcc --version
qemu-system-aarch64 --version
1.3 Writing a Bare-metal Program
Code Example (<span>uart_hello.s</span>
):
// Define UART register addresses
.equ UART0_BASE, 0x9000000 // Base address of PL011 UART
.equ UARTFR, 0x18 // Offset for flag register
.equ UARTFR_TXFF, (1 << 5) // Transmit FIFO full flag
.equ UARTDR, 0x0 // Offset for data register
.section .text
.global _start
_start:
// Initialize stack pointer (optional, not used in this simple example)
ldr x0, =stack_top
mov sp, x0
// Print string
ldr x1, =message // Load string address
loop:
ldrb w2, [x1], #1 // Read one byte into w2, increment address x1 by 1
cbz w2, halt // If read 0 (end of string), jump to halt
bl uart_putc // Call UART send function
b loop // Continue loop
// UART send single character function
uart_putc:
ldr x3, =UART0_BASE // Load UART base address into x3
tx_wait:
ldr w4, [x3, UARTFR] // Read UARTFR register
tst w4, UARTFR_TXFF // Check if transmit FIFO is full
b.ne tx_wait // If full, wait
str w2, [x3, UARTDR] // Write character to UARTDR register
ret // Return
halt:
b halt // Infinite loop (halt)
.section .data
message:
.asciz "Hello, ARMv8 Bare-metal!\n" // Define stack space (1KB)
.align 12
stack_bottom:
.space 1024
stack_top:
1.4 Compile and Run
Steps:
-
Compile Assembly File:
aarch64-linux-gnu-as -o uart_hello.o uart_hello.s
-
Link to Generate ELF File (specifying entry point and memory layout):
aarch64-linux-gnu-ld -nostdlib -o uart_hello.elf uart_hello.o -Ttext=0x80000
-
Run with QEMU:
qemu-system-aarch64 -M virt -cpu cortex-a53 -nographic -kernel uart_hello.elf
-
<span>-M virt</span>
: Select QEMU’s<span>virt</span>
machine (default includes PL011 UART). -
<span>-nographic</span>
: Disable graphical interface, output directly to terminal. -
<span>-kernel</span>
: Specify the ELF file to load.
Expected Output:
Hello, ARMv8 Bare-metal!
1.5 Key Code Analysis
-
UART Sending Logic:
-
Check Transmit FIFO: Ensure the buffer is not full by reading the
<span>UARTFR</span>
register’s<span>TXFF</span>
bit. -
Write Character: Write the character to the
<span>UARTDR</span>
register, hardware automatically sends it.
Boot Process:
-
The program starts executing from
<span>_start</span>
, initializing the stack pointer (this is just an example, actual complex programs need this). -
Characters are sent one by one in a loop until the terminator
<span>0</span>
is encountered.
1.6 Hands-on Experiment
-
Modify String Content: Change the text in
<span>message</span>
to something else, recompile and run. -
Debug the Program: Try enabling GDB debugging in QEMU:
qemu-system-aarch64 -M virt -cpu cortex-a53 -nographic -kernel uart_hello.elf -S -s
Connect GDB in another terminal:
gdb-multiarch -ex "target remote localhost:1234" -ex "file uart_hello.elf"
Practical results show: