Booting Raspberry Pi 3
This is a tutorial on bare-metal OS development on the AArch64 architecture. This article oriented to the Raspberry Pi 3 (RPi 3), but is aimed to be as device agnostic as possible. Therefore, some sections will be specific to the RPi 3 and will be marked to represent that.
This is the author's very first ARM system and wiki page. I learn as I write and will continue to develop this page once I feel comfortable enough at the subjet to share.
Preamble
This article assumes that you have all of the necessary materials needed to begin developing on your respective hardware. Other assumptions are that you are comfortable with low-level programming using C/C++, GNU Make buildsystem, and that you are comfortable enough about OS development and hardware to not be confused.. Such details are not in the scope of this article and must be done before proceeding to the next steps.
This article is also intended for beginner users who want a minimal solution for creating/starting an operating system, although some insight can be derived for the advanced audience.
Preperations
Cross Compiler
A bare-metal AArch64 toolchain is available on the AUR for installation. The links are provided here as well as a download link from the publisher for Windows users.
https://www.linaro.org/downloads/
https://aur.archlinux.org/packages/aarch64-elf-gcc-linaro-bin/
https://aur.archlinux.org/packages/aarch64-elf-newlib-linaro-bin/
RPi 3: Firmware
Firmware is required for the RPi 3 to boot properly. The firmware needed can be found at these link:
https://github.com/raspberrypi/firmware/blob/master/boot/start.elf
https://github.com/raspberrypi/firmware/blob/master/boot/bootcode.bin
There is also another file required to boot properly. A config.txt file must be supplied to provide configuration details for the device and the OS. Here are the only entries you need:
boot_delay=1 force_turbo=1 enable_uart=1
More details about booting and configuration can be found at thse links.
https://elinux.org/RPi_Software#Overview
https://raspberrypi.stackexchange.com/a/10595
https://www.raspberrypi.org/documentation/configuration/config-txt/
https://wiki.beyondlogic.org/index.php?title=Understanding_RaspberryPi_Boot_Process (although a bit old, is still useful)
When loading the OS onto an SD card, these file must be included in the boot partition.
Overview
By now you should have your cross compiler set up. The compiler binaries have the same name as they usually would with "aarch64-elf-" prefixing them (e.x. aarch64-elf-gcc). For this example, three files are used:
- linker.ld - Linker script
- start.S - Setup the environment and call the kernel
- kernel.c - Kernel entry and use
This article is written so that segments of the code will be presented with an explaination, with the full code in the Conclusion section.
Linking the Kernel
Linker script allows us to segment, organize, and align our kernel image. More information about linker scripts can be found osdev's Linker Scripts.
The start address and alignment are for the RPi3. Use whatever is applicable to you.
SECTIONS { . = 0x8000; .text : { KEEP(*(.boot)); *(.text*) } .rodata ALIGN(16) : { *(.rodata*) } .data ALIGN(16) : { *(.data*) } .bss (NOLOAD) : { . = ALIGN(16); __bss_start = .; *(.bss*) . = ALIGN(16); __bss_nlwords = (. - __bss_start); } }
Booting the Kernel
After the start.elf finishes, the CPU and SRAM have been enabled and control is given to the kernel image. But there are a few things we must do in order to set up a basic C environment. This is known as a bootstrap stage which initializes our OS on startup and hands control to the kernel. Keep in mind that this is for a minimalistic development environment on a single core.
When the CPU starts running the kernel, all cores will be executing the same code. This can be tricky for multiple reasons, which will be described later. For now, all we want is just one core running the kernel. To do that, we need get the core identification number and set all but one core to run an infinite loop.
.section .boot .global _start _start: mrs x0, mpidr_el1 and x0, x0, #3 cbz x0, _init 0: wfe b 0b
The mrs instruction loads data from specialty registers into a standard register. This special register in question is called the MPIDR or the Multiprocessor Affinity Register. Caring only about the first two bits only we use a bitwise and to weed out any core ID's that aren't zero. Then we apply the cbz instruction, which is a shorthand instruction for comparing the x0 register with 0 and branching if they are equal, i.e. core 0.
If the core ID is 0, we branch to another section that will initialize the environment and will hand control over to the kernel. For now, the C function for our kernel will simply by kern_main.
_init: b kern_main
Writing the Kernel
Conclusion
Here is the code for all of the files.
start.S
.section .boot .global _start _start: mrs x0, mpidr_el1 and x0, x0, #3 cbz x0, _init 0: wfe b 0b _init: b kern_main