Raspberry Pi: Difference between revisions

From OSDev.wiki
Jump to navigation Jump to search
[unchecked revision][unchecked revision]
Content added Content deleted
No edit summary
 
(63 intermediate revisions by 23 users not shown)
Line 1: Line 1:
{{In Progress}}
{{In Progress}}
{{FirstPerson}}

{{You}}
==Intro==
{{Sole Editor}}
This is a tutorial on bare-metal [OS] development on the Raspberry Pi. This tutorial is written specifically for the Raspberry Pi Model B Rev 2 because the author has no other hardware to test on. But so far the models are basically identical for the purpose of this tutorial (Rev 1 has 256MB ram, Model A has no ethernet).
This is a tutorial on bare-metal [OS] development on the Raspberry Pi. This tutorial is written specifically for the Raspberry Pi Model B Rev 2 because the author has no other hardware to test on. But so far the models are basically identical for the purpose of this tutorial (Rev 1 has 256MB ram, Model A has no ethernet).


This is the authors very first ARM system and we learn as we write without any prior knowledge about arm. Experience in Linux/Unix ('''very''' important) and C/C++ language ('''incredibly''' important, including how to use inline asm) is assumed and required. This is not a tutorial about how to build a kernel but a simple intro in how to get started on the RPi.
This is the authors very first ARM system and we learn as we write without any prior knowledge about ARM. Experience in Linux/Unix ('''very''' important) and C/C++ language ('''incredibly''' important, including how to use inline assembler) is assumed and required. This is not a tutorial about how to build a kernel but a simple intro in how to get started on the RPi.

== Preparations ==


=== Materials ===
=== Materials ===
Line 10: Line 13:
You will need a:
You will need a:
* '''Raspberry Pi''', RPi in short.
* '''Raspberry Pi''', RPi in short.
* SD Card to boot from.
* [[SD Card]] to boot from.
* A SD Card reader so you can write to the SD Card from your developement system.
* A SD Card reader so you can write to the SD Card from your developement system.
* A serial adaptor for the RPi.
* A serial adaptor for the RPi.
* Power from an external power supply, usb or the serial adaptor.
* Power from an external Power Supply, USB or the Serial Adaptor.


=== Serial adaptor ===
=== Serial adaptor ===


The RPi has 2 serials (UARTs). This tutorial only concerns itself with UART0, called simply UART or serial port. UART1 is ignored from now on. The basic UART onboard uses a 3.3V TTL and is connected to some of the GPIO pins labeled "P1" on the board. x86 PCs and MACs do use 5V TTL so you need some adaptor to convert the TTL. I recommend a '''USB to TTL Serial Cable - Debug / Console Cable for Raspberry Pi''' with seperate connectors per lead, like [http://www.adafruit.com/products/954 commercial RPi serial adaptor]. Which is then connected to the RPi [[Media:ARM_RaspberryPi_serial.jpg|like this]].
The RPi has 2 serial ports ([[UART|UARTs]]). This tutorial only concerns itself with UART0, called simply UART or serial port. UART1 is ignored from now on. The basic UART onboard uses a 3.3V TTL and is connected to some of the GPIO pins labeled "P1" on the board. x86 PCs and MACs do use 5V TTL so you need some adaptor to convert the TTL. A '''USB to TTL Serial Cable - Debug/Console Cable for Raspberry Pi''' with separate connectors per lead, like [http://www.adafruit.com/products/954 commercial RPi serial adaptor], is recommended.. Which is then connected to the RPi [[Media:ARM_RaspberryPi_serial.jpg|like this]]. The slightly cheaper [http://www.ebay.com/itm/USB-To-RS232-TTL-PL2303HX-Auto-Converter-Module-Converter-Adapter-5V-3-3V-Output-/350568364250 PL2303HX adapter] was found usable, but seems to be unreliable if connected to an USB port with an extension cable (a USB 2.0 hub might remedy this).


Note: The serial adaptor I use provides both a 0V and 5V lead (black and red) which provide power to the RPi. No extra power supply is needed besides this.
Note: The serial adaptor I use provides both a 0V and 5V lead (black and red) which provide power to the RPi. No extra power supply is needed besides this.


Alternatively, you can use the FTDI chip on an Arduino (or clone thereof). Connect RX on the Arduino to RX on the Pi, TX to TX and GND to GND, then connect the Arduino's reset pin to ground to prevent code in flash from interfering. Due to reset being held low, the Arduino itself is entirely bypassed. If you have a clone that supports 3.3V operation (such as a Seeeduino) then you can enable it to be safe, but this approach works fine with 5V.
==Preparations==


===Testing your hardware/serial port===
=== Testing your hardware/serial port ===


First things first, you're going to want to make sure all your hardware works. Connect your serial adaptor to the RPi and boot up the official Raspian image. The boot process will output to both the serial and the HDMI and will start a getty on the serial. Set up your serial port, however yours works, and open up minicom. Make sure you have flow control turned off.
First things first, you're going to want to make sure all your hardware works. Connect your serial adaptor to the RPi and boot up the official Raspian image. The boot process will output to both the serial and the HDMI and will start a getty on the serial. Set up your serial port, however yours works, and open up minicom. Make sure you have flow control turned off.
Line 29: Line 32:


If you get 'Permission Denied' '''do NOT become root!''' This is unnecessary. Instead do:
If you get 'Permission Denied' '''do NOT become root!''' This is unnecessary. Instead do:
<source lang="asm">
<syntaxhighlight lang="bash">
sudo adduser <user> dialout
sudo adduser <user> dialout
</syntaxhighlight>
</source>
This will let your user use serial ports without needing root.
This will let your user use serial ports without needing root.


Line 38: Line 41:
If you started minicom only after the RPi has booted then simply press '''return''' in minicom so the getty will output a fresh login prompt. Otherwise wait for the boot messages to appear. If you don't get any output then connect the RPi to a monitor to check that it actually boots, check your connections and minicom settings.
If you started minicom only after the RPi has booted then simply press '''return''' in minicom so the getty will output a fresh login prompt. Otherwise wait for the boot messages to appear. If you don't get any output then connect the RPi to a monitor to check that it actually boots, check your connections and minicom settings.


===Building a cross compiler===
=== Building a cross compiler ===


Like me you are probably using a x86 PC as main machine and want to edit and compile the source on that and the RPi is an ARM cpu so you absoluetly need a cross compiler. But even if you are developing on an ARM system it is still a good idea to build a cross compiler to avoid accidentally mixing stuff from your developement system with your own kernel. Follow the steps from [[GCC_Cross-Compiler]] to build your own cross compiler but use:
Like me you are probably using a x86 PC as main machine and want to edit and compile the source on that and the RPi is an ARM CPU so you absolutely need a cross compiler. But even if you are developing on an ARM system it is still a good idea to build a cross compiler to avoid accidentally mixing stuff from your developement system with your own kernel. Follow the steps from [[GCC Cross-Compiler]] to build your own cross compiler but use:


<source lang="bash">
<syntaxhighlight lang="bash">
export TARGET=arm-none-eabi
export TARGET=arm-none-eabi
</syntaxhighlight>
</source>


Now we are ready to start.
Now you are ready to start.


== Tutorials and examples ==
==Bare minimum kernel==
# [https://www.cl.cam.ac.uk/projects/raspberrypi/tutorials/os/ Tutorial in assembler (University of Cambridge)]
# [[Raspberry Pi Bare Bones|Tutorial in C]]
# [[Raspberry Pi Bare Bones Rust|Tutorial in Rust]]
# [https://github.com/dwelch67/raspberrypi Collection of examples and bootloader by dwelch67]
# [https://github.com/bztsrc/raspi3-tutorial Tutorials for AArch64 in C by bzt]
# [https://github.com/s-matyukevich/raspberry-pi-os OS tutorial for the Raspberry Pi by s-matyukevich]
# [[Detecting Raspberry Pi Board]]


== Booting the kernel ==
Lets start with a minimum of 4 files. The kernel is going to use a subset of C++, meaning C++ without exceptions and without runtime types. The main function will be in main.cc. Before the main function can be called though some things have to be set up using assembly. This will be placed in boot.S. On top of that we also need a linker script and a Makefile to build the kernel and need to create an include directory for later use.


Do you still have the SD card with the original Raspian image on it from when you where testing the hardware above? Great. So you already have a SD card with a boot partition and the required files. If not then download one of the original Raspberry boot images and copy them to the SD card.
===main.cc===


Now mount the first partition from the SD card and look at it:
<source lang="c">
/* main.cc - the entry point for the kernel */


<syntaxhighlight lang="text">
extern "C" {
bootcode.bin fixup.dat kernel.img start.elf
// kernel_main gets called from boot.S. Declaring it extern "C" avoid
cmdline.txt fixup_cd.dat kernel_cutdown.img start_cd.elf
// having to deal with the C++ name mangling.
config.txt issue.txt kernel_emergency.img
void kernel_main(uint32_t r0, uint32_t r1, uint32_t atags);
</syntaxhighlight>
}


When the RPi powers up the ARM CPU is halted and the GPU runs. The GPU loads the bootloader from rom and executes it. That then finds the SD card and loads the bootcode.bin. The bootcode handles the config.txt and cmdline.txt (or does start.elf read that?) and then runs start.elf. start.elf loads the kernel.img at 0x00008000, puts a few opcodes at 0x00000000 and the ATAGS at 0x00000100 and at last the ARM CPU is started. The CPU starts executing at 0x00000000, where it will initialize r0, r1 and r2 and jump to 0x00008000 where the kernel image starts.
#define UNUSED(x) (void)(x)


So to boot your own kernel simply replace kernel.img with our own, umount, sync, stick the SD card into RPi and turn the power on.
// kernel main function, it all begins here
void kernel_main(uint32_t r0, uint32_t r1, uint32_t atags) {
UNUSED(r0);
UNUSED(r1);
UNUSED(atags);
}
</source>


Note: The GPU also initialized the video ouput, detecting the right resolution from the monitor (if hdmi) or from the config.txt and creates a 2x2 pixel framebuffer (red, yellow, blue and cyan pixels) that the hardware scales to fullscreen with color interpolation. So you get rectangle with a nice color fading.
This simply declares an empty kernel_main function that simply returns. The GPU bootloader passes arguments to the kernel via r0-r2 and the boot.S makes sure to preserve those 3 registers. They are the first 3 arguments in a C function call. I'm not sure what R0 and R1 are for but r2 contains the address of the ATAGs (more about them later). For now the UNUSED() makro makes sure the compiler doesn't complain about unused variables.


== Boot from serial ==
===boot.S===
<source lang=text>
/* boot.S - assembly startup code */


The RPi boots the kernel directly from an SD card and only from an SD card. There is no other option. While developing this becomes tiresome since one has to constantly swap the SD card from the RPi to a SD card reader and back. Writing the kernel to the SD card over and over also wears out the card. Plus the SD card slot is somewhat fragile; several people have reported that they broke it accidentally. Overall not an ideal solution. So what can we do about that?
// To keep this in the first portion of the binary.
.section ".text.boot"


I've written a small bootloader named [https://github.com/mrvn/raspbootin Raspbootin] based on the Tutorial in C above that loads the real kernel from the serial port. Raspbootin is acompanied by Raspbootcom ([https://github.com/mrvn/raspbootin same repository]) that acts as a boot server and terminal program. Using the two I only need to reboot my RPi to get it to boot the latest kernel. This makes testing both faster and safer for the hardware.
// Make Start global.
.globl Start


Raspbootin is completely transparent for your kernel. It preserves the r0, r1 and r2 registers and ATAGs placed into memory by the GPU for your kernel. So whether you boot your kernel directly from an SD card or with Raspbootin via serial port makes no difference to your code.
// Entry point for the kernel.
// r15 -> should begin execution at 0x8000.
// r0 -> 0x00000000
// r1 -> 0x00000C42
// r2 -> 0x00000100 - start of ATAGS
// preserve these registers as argument for kernel_main
Start:
// Setup the stack.
mov sp, #0x8000


=== Raspbootin serial protocol ===
// Clear out bss.
ldr r4, =_bss_start
ldr r9, =_bss_end
mov r5, #0
mov r6, #0
mov r7, #0
mov r8, #0
1:
// store multiple at r4.
stmia r4!, {r5-r8}


You don't have to care about this unless you want to write your own boot server.
// If we're still below bss_end, loop.
cmp r4, r9
blo 1b


The boot protocol for Raspbootin is rather simple. Raspbootin first sends 3 breaks (<code>\x03</code>) over the serial line to signal that it is ready to receive a kernel. It then expects the size of the kernel as <code>uint32_t</code> in little endian byte order. After the size it replies with "<code>OK</code>" if the size is acceptable or "<code>SE</code>" if it is too large for it to handle. After "<code>OK</code>" it expects <code>size</code> many bytes representing the kernel. That's it.
// Call kernel_main
ldr r3, =kernel_main
blx r3


== Parsing ATAGs ==
// halt
halt:
wfe
b halt
</source>


A good documentation for ATAGs can be found [http://www.simtec.co.uk/products/SWLINUX/files/booting_article.html#appendix_tag_reference here].
The section ".text.boot" will be used in the linker script to place the boot.S as the verry first thing in out kernel image. The code initializes a minimum C environment, which means having a stack and zeroing the BSS segment, before calling the kernel_main function. Note that the code avoids using r0-r2 so the remain valid for the kernel_main call.


[http://web.archive.org/web/20120605001004/http://www.simtec.co.uk/products/SWLINUX/files/booting_article.html cached version from the Wayback Machine]
===link-arm-eabi.ld===
<source lang=text>
* link-arm-eabi.ld - linker script for arm eabi */
ENTRY(Start)


Note that later Raspberry Pis will pass you a device tree blob instead in R2. ATAGs still can be found at 0x100, if you disable device tree (r2 contains 0x0 in this case) - identifiable because they always start with an ATAG_CORE (0x54410001). In comparison, Device Tree is probably far more useful, but is more complex. A device tree starts with the uint32_t 0xd00dfeed '''(big-endian).''' Note the big endian - this applies to all values. ARM defaults to little endian, so you'll probably want to write some endian routines early!
SECTIONS
[https://www.devicetree.org/ Device Tree Specification]
{
/* Starts at LOADER_ADDR. */
.text 0x8000 :
_text_start = .;
_start = .;
{
KEEP(*(.text.boot))
*(.text)
}
. = ALIGN(4096); /* align to page size */
_text_end = .;
.rodata:
_rodata_start = .;
{
*(.rodata)
}
. = ALIGN(4096); /* align to page size */
_rodata_end = .;
.data :
_data_start = .;
{
*(.data)
}
. = ALIGN(4096); /* align to page size */
_data_end = .;
.bss :
_bss_start = .;
{
bss = .;
*(.bss)
}
. = ALIGN(4096); /* align to page size */
_bss_end = .;
_end = .;
}
</source>


== Framebuffer support ==
There is a lot of text here but don't despair. The script is rather simple if you look at it bit by bit.

ENTRY(Start) declares the entry point for the kernel image. That symbol was declared in the boot.S file. Since we are actually booting a binary image I think the entry is completly irelevant. But it has to be there in the elf file we build as intermediate file. Declaring it makes the linker happy.

SECTIONS declares, well, sections. It decides where the bits and pieces of our code and data go and also sets a few symbols that help us track the size of each section.

<source lang=text>
. = 0x8000;
_start = .;
</source>

The "." denotes the current address so the first line tells the linker to set the current address to 0x8000, where the kernel starts. The current address is automatically incremented when the linker adds data. The second line then creates a symbol "_start" and sets it to the current address.

After that sections are defined for text (code), read-only data, read-write data and BSS (0 initialized memory). Other than the name the sections are identical so lets just look at one of them:

<source lang=text>
_text_start = .;
.text : {
KEEP(*(.text.boot))
*(.text)
}
. = ALIGN(4096); /* align to page size */
_text_end = .;
</source>

The first line creates a _text_start symbol for the section. The second line opens a .text section for the output file which gets closed in the fifth line. Lines 3 and 4 declare what sections from the input files will be placed inside the output .text section. In our case ".text.boot" is to be placed first followed by the more general ".text". ".text.boot" is only used in boot.S and ensures that it ends up at the beginning of the kernel image. ".text" then contains all the remaining code.
Any data added by the linker automatically increments the current addrress ("."). In line 6 we explicitly increment it so that it is aligned to a 4096 byte boundary (which is the page size for the RPi). And last line 7 creates a _text_end symbol so we know where the section ends.

What are the _text_start and _text_end for and why use page alignment? The 2 symbols can be used in the kernel source and the linker will then place the correct addresses into the binary. As an example the _bss_start and _bss_end are used in boot.S. But you can also use the symbols from C by declaring them extern first. While not required I made all sections aligned to page size. This later allows mapping them in the page tables with executable, read-only and read-write permissions without having to handle overlaps (2 sections in one page).

<source lang=text>
_end = .;
</source>

After all sections are declared the _end symbol is created. If you ever want to know how large your kernel is at runtime you can use _start and _end to find out.

===Makefile===
<source lang=make>
# Makefile - build script */

# build environment
PREFIX ?= /usr/local/cross
ARMGNU ?= $(PREFIX)/bin/arm-none-eabi

# source files
SOURCES_ASM := $(wildcard *.S)
SOURCES_CC := $(wildcard *.cc)

# object files
OBJS := $(patsubst %.S,%.o,$(SOURCES_ASM))
OBJS += $(patsubst %.cc,%.o,$(SOURCES_CC))

# Build flags
DEPENDFLAGS := -MD -MP
INCLUDES := -I include
BASEFLAGS := -O2 -fpic -pedantic -pedantic-errors -nostdlib
BASEFLAGS += -nostartfiles -ffreestanding -nodefaultlibs
BASEFLAGS += -fno-builtin -fomit-frame-pointer -mcpu=arm1176jzf-s
WARNFLAGS := -Wall -Wextra -Wshadow -Wcast-align -Wwrite-strings
WARNFLAGS += -Wredundant-decls -Winline
WARNFLAGS += -Wno-attributes -Wno-deprecated-declarations
WARNFLAGS += -Wno-div-by-zero -Wno-endif-labels -Wfloat-equal
WARNFLAGS += -Wformat=2 -Wno-format-extra-args -Winit-self
WARNFLAGS += -Winvalid-pch -Wmissing-format-attribute
WARNFLAGS += -Wmissing-include-dirs -Wno-multichar
WARNFLAGS += -Wredundant-decls -Wshadow
WARNFLAGS += -Wno-sign-compare -Wswitch -Wsystem-headers -Wundef
WARNFLAGS += -Wno-pragmas -Wno-unused-but-set-parameter
WARNFLAGS += -Wno-unused-but-set-variable -Wno-unused-result
WARNFLAGS += -Wwrite-strings -Wdisabled-optimization -Wpointer-arith
WARNFLAGS += -Werror
ASFLAGS := $(INCLUDES) $(DEPENDFLAGS) -D__ASSEMBLY__
CXXFLAGS := $(INCLUDES) $(DEPENDFLAGS) $(BASEFLAGS) $(WARNFLAGS)
CXXFLAGS += -fno-exceptions -std=c++0x

# build rules
all: kernel.img

include $(wildcard *.d)

kernel.elf: $(OBJS) link-arm-eabi.ld
$(ARMGNU)-ld $(OBJS) -Tlink-arm-eabi.ld -o $@

kernel.img: kernel.elf
$(ARMGNU)-objcopy kernel.elf -O binary kernel.img

clean:
$(RM) -f $(OBJS) kernel.elf kernel.img

dist-clean: clean
$(RM) -f *.d

# C++.
%.o: %.cc Makefile
$(ARMGNU)-g++ $(CXXFLAGS) -c $< -o $@

# AS.
%.o: %.S Makefile
$(ARMGNU)-g++ $(ASFLAGS) -c $< -o $@
</source>

And there you go. Try building it. A minimum kernel that does absolutely nothing.

==Hello World kernel==

Lets make the kernel do something. Lets say hello to the world using the serial port.

===main.cc===

<source lang=c>
/* main.cc - the entry point for the kernel */

#include <uart.h>

extern "C" {
// kernel_main gets called from boot.S. Declaring it extern "C" avoid
// having to deal with the C++ name mangling.
void kernel_main()uint32_t r0, uint32_t r1, uint32_t atags;
}

#define UNUSED(x) (void)(x)

const char hello[] = "\r\nHello World\r\n";
const char halting[] = "\r\n*** system halting ***";

// kernel main function, it all begins here
void kernel_main(uint32_t r0, uint32_t r1, uint32_t atags) {
UNUSED(r0);
UNUSED(r1);
UNUSED(atags);

UART::init();
UART::puts(hello);

// Wait a bit
for(volatile int i = 0; i < 10000000; ++i) { }

UART::puts(halting);
}
</source>

===include/mmio.h===

<source lang=c>
/* mmio.h - access to MMIO registers */

#ifndef MMIO_H
#define MMIO_H

#include <stdint.h>

namespace MMIO {
// write to MMIO register
static inline void write(uint32_t reg, uint32_t data) {
uint32_t *ptr = (uint32_t*)reg;
asm volatile("str %[data], [%[reg]]"
: : [reg]"r"(ptr), [data]"r"(data));
}

// read from MMIO register
static inline uint32_t read(uint32_t reg) {
uint32_t *ptr = (uint32_t*)reg;
uint32_t data;
asm volatile("ldr %[data], [%[reg]]"
: [data]"=r"(data) : [reg]"r"(ptr));
return data;
}
}

#endif // #ifndef MMIO_H
</source>


===include/uart.h===

<source lang=c>
/* uart.h - UART initialization & communication */

#ifndef UART_H
#define UART_H

#include <stdint.h>

namespace UART {
/*
* Transmit a byte via UART0.
* uint8_t Byte: byte to send.
*/
void putc(uint8_t byte);

/*
* print a string to the UART one character at a time
* const char *str: 0-terminated string
*/
void puts(const char *str);
}

#endif // #ifndef UART_H
</source>

===uart.cc===

<source lang=c>
/* uart.cc - UART initialization & communication */
/* Reference material:
* http://www.raspberrypi.org/wp-content/uploads/2012/02/BCM2835-ARM-Peripherals.pdf
* Chapter 13: UART
*/

#include <stdint.h>
#include <mmio.h>
#include <uart.h>

namespace UART {
enum {
// The GPIO registers base address.
GPIO_BASE = 0x20200000,

// The offsets for reach register.

// Controls actuation of pull up/down to ALL GPIO pins.
GPPUD = (GPIO_BASE + 0x94),

// Controls actuation of pull up/down for specific GPIO pin.
GPPUDCLK0 = (GPIO_BASE + 0x98),

// The base address for UART.
UART0_BASE = 0x20201000,

// The offsets for reach register for the UART.
UART0_DR = (UART0_BASE + 0x00),
UART0_RSRECR = (UART0_BASE + 0x04),
UART0_FR = (UART0_BASE + 0x18),
UART0_ILPR = (UART0_BASE + 0x20),
UART0_IBRD = (UART0_BASE + 0x24),
UART0_FBRD = (UART0_BASE + 0x28),
UART0_LCRH = (UART0_BASE + 0x2C),
UART0_CR = (UART0_BASE + 0x30),
UART0_IFLS = (UART0_BASE + 0x34),
UART0_IMSC = (UART0_BASE + 0x38),
UART0_RIS = (UART0_BASE + 0x3C),
UART0_MIS = (UART0_BASE + 0x40),
UART0_ICR = (UART0_BASE + 0x44),
UART0_DMACR = (UART0_BASE + 0x48),
UART0_ITCR = (UART0_BASE + 0x80),
UART0_ITIP = (UART0_BASE + 0x84),
UART0_ITOP = (UART0_BASE + 0x88),
UART0_TDR = (UART0_BASE + 0x8C),
};

/*
* delay function
* int32_t delay: number of cycles to delay
*
* This just loops <delay> times in a way that the compiler
* wont optimize away.
*/
void delay(int32_t count) {
asm volatile("1: subs %[count], %[count], #1; bne 1b"
: : [count]"r"(count));
}
/*
* Initialize UART0.
*/
void init() {
// Disable UART0.
MMIO::write(UART0_CR, 0x00000000);
// Setup the GPIO pin 14 && 15.
// Disable pull up/down for all GPIO pins & delay for 150 cycles.
MMIO::write(GPPUD, 0x00000000);
delay(150);

// Disable pull up/down for pin 14,15 & delay for 150 cycles.
MMIO::write(GPPUDCLK0, (1 << 14) | (1 << 15));
delay(150);

// Write 0 to GPPUDCLK0 to make it take effect.
MMIO::write(GPPUDCLK0, 0x00000000);
// Clear pending interrupts.
MMIO::write(UART0_ICR, 0x7FF);

// Set integer & fractional part of baud rate.
// Divider = UART_CLOCK/(16 * Baud)
// Fraction part register = (Fractional part * 64) + 0.5
// UART_CLOCK = 3000000; Baud = 115200.

// Divider = 3000000/(16 * 115200) = 1.627 = ~1.
// Fractional part register = (.627 * 64) + 0.5 = 40.6 = ~40.
MMIO::write(UART0_IBRD, 1);
MMIO::write(UART0_FBRD, 40);

// Enable FIFO & 8 bit data transmissio (1 stop bit, no parity).
MMIO::write(UART0_LCRH, (1 << 4) | (1 << 5) | (1 << 6));

// Mask all interrupts.
MMIO::write(UART0_IMSC, (1 << 1) | (1 << 4) | (1 << 5) |
(1 << 6) | (1 << 7) | (1 << 8) |
(1 << 9) | (1 << 10));

// Enable UART0, receive & transfer part of UART.
MMIO::write(UART0_CR, (1 << 0) | (1 << 8) | (1 << 9));
}

/*
* Transmit a byte via UART0.
* uint8_t Byte: byte to send.
*/
void putc(uint8_t byte) {
// wait for UART to become ready to transmit
while(true) {
if (!(MMIO::read(UART0_FR) & (1 << 5))) {
break;
}
}
MMIO::write(UART0_DR, byte);
}

/*
* print a string to the UART one character at a time
* const char *str: 0-terminated string
*/
void puts(const char *str) {
while(*str) {
UART::putc(*str++);
}
}
}
</source>

===Booting the kernel===

Do you still have the SD card with the original raspian image on it from when you where testing the hardware above? Great. So you already have a SD card with a boot partition and the required files. If not then download one of the original raspberry boot images and copy them to the SD card.

Now mount the first partition from the SD card and look at it:

<source lang=text>
bootcode.bin fixup.dat kernel.img start.elf
cmdline.txt fixup_cd.dat kernel_cutdown.img start_cd.elf
config.txt issue.txt kernel_emergency.img
</source>


On boot the RPi configures a display with a virtual resolution of 2x2 pixel scaled to full screen. Each pixel has a different color and the hardware scaling interpolates the colors to show a nice color fade. So before you do anything first connect a monitor and see to it that you get some output.
Simplified when the RPi powers up the ARM cpu is halted and the GPU runs. The GPU loads the bootloader from rom and executes it. That then finds the SD card and loads the bootcode.bin. The bootcode handles the config.txt and cmdline.txt (or does start.elf read that?) and then runs start.elf. start.elf loads the kernel.img and at last the ARM cpu is started running that kernel image.


For a framebuffer, you need to learn how to
So now we replace the original kernel.img with out own, umount, sync, stick the SD card into RPi and turn the power on.
[https://github.com/raspberrypi/firmware/wiki/Accessing-mailboxes access mailboxes]. And then you have to send the GPU some [https://github.com/raspberrypi/firmware/wiki/Mailbox-property-interface mail]. Or, you could read about the framebuffer [http://elinux.org/RPi_Framebuffer here],
Your minicom should then show the following:


Start with a single simple querry at first and then build up more complex mails. If you get it working then I recommend just altering the virtual size and color depth and leaving the physical resolution as is. The RPi seems to do a fine job of detecting the monitor and the user can also configure a resolution in the boot config files on the SD card. Best to honor his wishes.
<source lang=text>


== Interrupts and exceptions ==
Hello World
*** system halting ***
</source>


By default the exception vector table on ARM starts at 0x0. You can use that but there are better ways. You can set a flag to use a high vector at 0xffff0000 or set the exception vector base address register to point to your own table anywhere (32 byte aligned) you like.
==Echo kernel==


Note: Interrupts are level triggered so you have to clear the source of an interrupt or mask it before returning from interrupt. The ARM CPU in the RPi also supports some extra instructions for storing registers on the stack of a different mode, switching modes and returning from interrupt. Those extensions are nicely described in the ARM arm.
The hello world kernel shows how to do output to the UART. Next lets do some input. And to see that the input works we simply echo the input as output. A simple echo kernel.


Note: The return address in LR during an interrupt will be 0-8 byte, depending on the type of exception, offset to what it should be and needs to be adjusted before returning. Again look into the ARM arm for which offset applies to which exception.
In include/uart.h add the following:


Implementing and testing the software interrupt first is a good idea since you can trigger it in a controlled way.
<source lang=c>
/*
* Receive a byte via UART0.
*
* Returns:
* uint8_t: byte received.
*/
uint8_t getc();
</source>


When configuring some peripheral to send an interrupt it is a useful thing to have interrupts disabled in the CPSR, enable the interrupt you are interested in (or all) in the 3 interrupt enable registers and then poll the 3 pending registers in a tight loop and output changes. This allows you to see if the peripheral raises an interrupt and (if in doubt) which one. After that the real interrupt handler can be configured and tested. Gives you a nice half way point to test what you have so far.
In uart.c add the following:


== Floating point support ==
<source lang=c>
/*
* Receive a byte via UART0.
*
* Returns:
* uint8_t: byte received.
*/
uint8_t getc() {
// wait for UART to have recieved something
while(true) {
if (!(MMIO::read(UART0_FR) & (1 << 4))) {
break;
}
}
return MMIO::read(UART0_DR);
}
</source>


To be able to use any floating point operations, such as storing or loading floating point numbers, you need to enable the FPU before using it. To do this, you have to enable access to the coprocessor to whoever should be able to use it, and you have to enable the FPU itself.
And last in main.cc put the following:


<source lang=c>
<syntaxhighlight lang="asm">
# enable FPU in coprocessor enable register - this gives everybody access to both locations of coprocessor.
const char hello[] = "\r\nHello World, feel the echo\r\n";
ldr r0, =(0xF << 20)
b
mcr p15, 0, r0, c1, c0, 2
// kernel main function, it all begins here
</syntaxhighlight>
void kernel_main() {
UART::init();


And then enable the FPU itself:
UART::puts(hello);


<syntaxhighlight lang="asm">
while(true) {
# enable FPU in FP exception register
UART::putc(UART::getc());
MOV r3, #0x40000000
}
# VMSR FPEXC, r3 # assembler bug
}
.long 0xeee83a10
</source>
</syntaxhighlight>


The third line is the actual instruction that you'd want to use, but due to a bug in Binutils 2.23 it does not assemble. The line below it is what it should assemble to, and replaces the opcode. After doing these two, it's possible to use the FPU.
==Boot-from-serial kernel==


== USB ==
The RPi boots the kernel directly form SD card and only from SD card. There is no other option. While devloping this becomes tiresome since one has to constantly swap the SD card from the RPi to a SD card reader and back. Writing the kernel to the SD card over and over also wears out the card.


A standalone BSD-licenced USB driver with support for keyboard and mouse is available here: https://github.com/Chadderz121/csud . This driver can be kept stand-alone, by editing the <code>/source/platform.c</code> file to interface the driver with your implementation of <code>malloc()</code> and similar functions, or you can integrate the driver more closely with your operating system.
But above we have seen how to get into C/C++ code at boot and how to read from and write to the serial port. We can use that to download code over the serial port and then execute that. We will call that Raspbootin (pronounced Rasputin). Before you start editing files make a copy of the echo-kernel you have so far. We will later boot that from the serial console to test Raspbootin.


==External references==
== External references ==
# [https://www.scss.tcd.ie/~waldroj/3d1/arm_arm.pdf arm_arm.pdf] - general ARM Architecture Reference Manual v6
# [https://www.scss.tcd.ie/~waldroj/3d1/arm_arm.pdf arm_arm.pdf] - General ARM Architecture Reference Manual v6
# [http://infocenter.arm.com/help/topic/com.arm.doc.ddi0301h/DDI0301H_arm1176jzfs_r0p7_trm.pdf DDI0301H_arm1176jzfs_r0p7_trm.pdf] - More specific ARM for the RPi
# [http://infocenter.arm.com/help/topic/com.arm.doc.ddi0301h/DDI0301H_arm1176jzfs_r0p7_trm.pdf DDI0301H_arm1176jzfs_r0p7_trm.pdf] - More specific ARM for the RPi
# [https://github.com/dwelch67/raspberrypi] - basic toolchain + UART stuff
# [https://github.com/dwelch67/raspberrypi dwelch67 examples] - Basic toolchain + UART stuff
# [http://elinux.org/RPi_Hardware RPi_Hardware] - list of datasheets (and one manual about peripherals on the broadcom chip)
# [http://elinux.org/RPi_Hardware RPi_Hardware] - List of datasheets (and one manual about peripherals on the Broadcom chip)
# [https://github.com/raspberrypi/firmware/wiki] - for mailboxes and video stuff
# [https://github.com/raspberrypi/firmware/wiki GitHub Raspberry Pi firmware wiki] - For mailboxes and video stuff
# [http://www.raspberrypi.org/wp-content/uploads/2012/02/BCM2835-ARM-Peripherals.pdf BCM2835-ARM-Peripherals.pdf] - Datasheep for RPi peripherals
# [http://www.raspberrypi.org/wp-content/uploads/2012/02/BCM2835-ARM-Peripherals.pdf BCM2835-ARM-Peripherals.pdf] - Datasheet for RPi peripherals
# [http://www.simtec.co.uk/products/SWLINUX/files/booting_article.html Booting ARM Linux] - Describes the generic bootloader interface to the ARM port of Linux which the RPi bootloader emulates
# [http://sourceforge.net/projects/rpiqemuwindows/ RPi Emulator] - A preconfigured QEMU RPi emulation environment for Windows.


[[Category:ARM]]
[[Category:ARM]]
[[Category:Raspberry Pi]]

Latest revision as of 08:26, 17 June 2024

This page is a work in progress.
This page may thus be incomplete. Its content may be changed in the near future.
This article refers to its readers or editors using I, my, we or us.
It should be edited to be in an encyclopedic tone.
This article refers to its readers using you in an unencyclopedic manner.
It should be edited to be in an encyclopedic tone.
This article was written like there is only one author.
This is a wiki, not a personal site. You can help the wiki by editing this article to remove mentions of authors.

This is a tutorial on bare-metal [OS] development on the Raspberry Pi. This tutorial is written specifically for the Raspberry Pi Model B Rev 2 because the author has no other hardware to test on. But so far the models are basically identical for the purpose of this tutorial (Rev 1 has 256MB ram, Model A has no ethernet).

This is the authors very first ARM system and we learn as we write without any prior knowledge about ARM. Experience in Linux/Unix (very important) and C/C++ language (incredibly important, including how to use inline assembler) is assumed and required. This is not a tutorial about how to build a kernel but a simple intro in how to get started on the RPi.

Preparations

Materials

You will need a:

  • Raspberry Pi, RPi in short.
  • SD Card to boot from.
  • A SD Card reader so you can write to the SD Card from your developement system.
  • A serial adaptor for the RPi.
  • Power from an external Power Supply, USB or the Serial Adaptor.

Serial adaptor

The RPi has 2 serial ports (UARTs). This tutorial only concerns itself with UART0, called simply UART or serial port. UART1 is ignored from now on. The basic UART onboard uses a 3.3V TTL and is connected to some of the GPIO pins labeled "P1" on the board. x86 PCs and MACs do use 5V TTL so you need some adaptor to convert the TTL. A USB to TTL Serial Cable - Debug/Console Cable for Raspberry Pi with separate connectors per lead, like commercial RPi serial adaptor, is recommended.. Which is then connected to the RPi like this. The slightly cheaper PL2303HX adapter was found usable, but seems to be unreliable if connected to an USB port with an extension cable (a USB 2.0 hub might remedy this).

Note: The serial adaptor I use provides both a 0V and 5V lead (black and red) which provide power to the RPi. No extra power supply is needed besides this.

Alternatively, you can use the FTDI chip on an Arduino (or clone thereof). Connect RX on the Arduino to RX on the Pi, TX to TX and GND to GND, then connect the Arduino's reset pin to ground to prevent code in flash from interfering. Due to reset being held low, the Arduino itself is entirely bypassed. If you have a clone that supports 3.3V operation (such as a Seeeduino) then you can enable it to be safe, but this approach works fine with 5V.

Testing your hardware/serial port

First things first, you're going to want to make sure all your hardware works. Connect your serial adaptor to the RPi and boot up the official Raspian image. The boot process will output to both the serial and the HDMI and will start a getty on the serial. Set up your serial port, however yours works, and open up minicom. Make sure you have flow control turned off. Ensure you can run at 115200 baud, 8N1, which is what the RPi uses.

If you get 'Permission Denied' do NOT become root! This is unnecessary. Instead do:

sudo adduser <user> dialout

This will let your user use serial ports without needing root.

Or do ls -l /dev/ttyS* to find out the group that own the device, then add you into that group under /etc/group (normally the group is uucp)

If you started minicom only after the RPi has booted then simply press return in minicom so the getty will output a fresh login prompt. Otherwise wait for the boot messages to appear. If you don't get any output then connect the RPi to a monitor to check that it actually boots, check your connections and minicom settings.

Building a cross compiler

Like me you are probably using a x86 PC as main machine and want to edit and compile the source on that and the RPi is an ARM CPU so you absolutely need a cross compiler. But even if you are developing on an ARM system it is still a good idea to build a cross compiler to avoid accidentally mixing stuff from your developement system with your own kernel. Follow the steps from GCC Cross-Compiler to build your own cross compiler but use:

export TARGET=arm-none-eabi

Now you are ready to start.

Tutorials and examples

  1. Tutorial in assembler (University of Cambridge)
  2. Tutorial in C
  3. Tutorial in Rust
  4. Collection of examples and bootloader by dwelch67
  5. Tutorials for AArch64 in C by bzt
  6. OS tutorial for the Raspberry Pi by s-matyukevich
  7. Detecting Raspberry Pi Board

Booting the kernel

Do you still have the SD card with the original Raspian image on it from when you where testing the hardware above? Great. So you already have a SD card with a boot partition and the required files. If not then download one of the original Raspberry boot images and copy them to the SD card.

Now mount the first partition from the SD card and look at it:

bootcode.bin  fixup.dat     kernel.img            start.elf
cmdline.txt   fixup_cd.dat  kernel_cutdown.img    start_cd.elf
config.txt    issue.txt     kernel_emergency.img

When the RPi powers up the ARM CPU is halted and the GPU runs. The GPU loads the bootloader from rom and executes it. That then finds the SD card and loads the bootcode.bin. The bootcode handles the config.txt and cmdline.txt (or does start.elf read that?) and then runs start.elf. start.elf loads the kernel.img at 0x00008000, puts a few opcodes at 0x00000000 and the ATAGS at 0x00000100 and at last the ARM CPU is started. The CPU starts executing at 0x00000000, where it will initialize r0, r1 and r2 and jump to 0x00008000 where the kernel image starts.

So to boot your own kernel simply replace kernel.img with our own, umount, sync, stick the SD card into RPi and turn the power on.

Note: The GPU also initialized the video ouput, detecting the right resolution from the monitor (if hdmi) or from the config.txt and creates a 2x2 pixel framebuffer (red, yellow, blue and cyan pixels) that the hardware scales to fullscreen with color interpolation. So you get rectangle with a nice color fading.

Boot from serial

The RPi boots the kernel directly from an SD card and only from an SD card. There is no other option. While developing this becomes tiresome since one has to constantly swap the SD card from the RPi to a SD card reader and back. Writing the kernel to the SD card over and over also wears out the card. Plus the SD card slot is somewhat fragile; several people have reported that they broke it accidentally. Overall not an ideal solution. So what can we do about that?

I've written a small bootloader named Raspbootin based on the Tutorial in C above that loads the real kernel from the serial port. Raspbootin is acompanied by Raspbootcom (same repository) that acts as a boot server and terminal program. Using the two I only need to reboot my RPi to get it to boot the latest kernel. This makes testing both faster and safer for the hardware.

Raspbootin is completely transparent for your kernel. It preserves the r0, r1 and r2 registers and ATAGs placed into memory by the GPU for your kernel. So whether you boot your kernel directly from an SD card or with Raspbootin via serial port makes no difference to your code.

Raspbootin serial protocol

You don't have to care about this unless you want to write your own boot server.

The boot protocol for Raspbootin is rather simple. Raspbootin first sends 3 breaks (\x03) over the serial line to signal that it is ready to receive a kernel. It then expects the size of the kernel as uint32_t in little endian byte order. After the size it replies with "OK" if the size is acceptable or "SE" if it is too large for it to handle. After "OK" it expects size many bytes representing the kernel. That's it.

Parsing ATAGs

A good documentation for ATAGs can be found here.

cached version from the Wayback Machine

Note that later Raspberry Pis will pass you a device tree blob instead in R2. ATAGs still can be found at 0x100, if you disable device tree (r2 contains 0x0 in this case) - identifiable because they always start with an ATAG_CORE (0x54410001). In comparison, Device Tree is probably far more useful, but is more complex. A device tree starts with the uint32_t 0xd00dfeed (big-endian). Note the big endian - this applies to all values. ARM defaults to little endian, so you'll probably want to write some endian routines early! Device Tree Specification

Framebuffer support

On boot the RPi configures a display with a virtual resolution of 2x2 pixel scaled to full screen. Each pixel has a different color and the hardware scaling interpolates the colors to show a nice color fade. So before you do anything first connect a monitor and see to it that you get some output.

For a framebuffer, you need to learn how to access mailboxes. And then you have to send the GPU some mail. Or, you could read about the framebuffer here,

Start with a single simple querry at first and then build up more complex mails. If you get it working then I recommend just altering the virtual size and color depth and leaving the physical resolution as is. The RPi seems to do a fine job of detecting the monitor and the user can also configure a resolution in the boot config files on the SD card. Best to honor his wishes.

Interrupts and exceptions

By default the exception vector table on ARM starts at 0x0. You can use that but there are better ways. You can set a flag to use a high vector at 0xffff0000 or set the exception vector base address register to point to your own table anywhere (32 byte aligned) you like.

Note: Interrupts are level triggered so you have to clear the source of an interrupt or mask it before returning from interrupt. The ARM CPU in the RPi also supports some extra instructions for storing registers on the stack of a different mode, switching modes and returning from interrupt. Those extensions are nicely described in the ARM arm.

Note: The return address in LR during an interrupt will be 0-8 byte, depending on the type of exception, offset to what it should be and needs to be adjusted before returning. Again look into the ARM arm for which offset applies to which exception.

Implementing and testing the software interrupt first is a good idea since you can trigger it in a controlled way.

When configuring some peripheral to send an interrupt it is a useful thing to have interrupts disabled in the CPSR, enable the interrupt you are interested in (or all) in the 3 interrupt enable registers and then poll the 3 pending registers in a tight loop and output changes. This allows you to see if the peripheral raises an interrupt and (if in doubt) which one. After that the real interrupt handler can be configured and tested. Gives you a nice half way point to test what you have so far.

Floating point support

To be able to use any floating point operations, such as storing or loading floating point numbers, you need to enable the FPU before using it. To do this, you have to enable access to the coprocessor to whoever should be able to use it, and you have to enable the FPU itself.

# enable FPU in coprocessor enable register - this gives everybody access to both locations of coprocessor.
ldr r0, =(0xF << 20)
mcr p15, 0, r0, c1, c0, 2

And then enable the FPU itself:

# enable FPU in FP exception register
MOV r3, #0x40000000
#  VMSR FPEXC, r3    # assembler bug
.long 0xeee83a10

The third line is the actual instruction that you'd want to use, but due to a bug in Binutils 2.23 it does not assemble. The line below it is what it should assemble to, and replaces the opcode. After doing these two, it's possible to use the FPU.

USB

A standalone BSD-licenced USB driver with support for keyboard and mouse is available here: https://github.com/Chadderz121/csud . This driver can be kept stand-alone, by editing the /source/platform.c file to interface the driver with your implementation of malloc() and similar functions, or you can integrate the driver more closely with your operating system.

External references

  1. arm_arm.pdf - General ARM Architecture Reference Manual v6
  2. DDI0301H_arm1176jzfs_r0p7_trm.pdf - More specific ARM for the RPi
  3. dwelch67 examples - Basic toolchain + UART stuff
  4. RPi_Hardware - List of datasheets (and one manual about peripherals on the Broadcom chip)
  5. GitHub Raspberry Pi firmware wiki - For mailboxes and video stuff
  6. BCM2835-ARM-Peripherals.pdf - Datasheet for RPi peripherals
  7. Booting ARM Linux - Describes the generic bootloader interface to the ARM port of Linux which the RPi bootloader emulates
  8. RPi Emulator - A preconfigured QEMU RPi emulation environment for Windows.