Files
MontaukOS/docs/raspberry-pi.md
T

14 KiB

Raspberry Pi 4 / 400

MontaukOS runs on the Raspberry Pi 4 and 400 through the Pi Firmware Task Force's UEFI firmware (pftf/RPi4, an EDK2 build) in ACPI mode, booting Limine like any other UEFI machine.

Building and writing the SD card

./toolchain/build-montauk-toolchain.sh aarch64   # once
make ARCH=aarch64 rpi4-sd                        # -> montauk-rpi4.img
sudo dd if=montauk-rpi4.img of=/dev/sdX bs=4M conv=fsync

Or use the imager, which also updates a card in place:

make rpi4-imager                                 # = ./scripts/montauk-imager.py

The imager

scripts/montauk-imager.py is a Tk window (python3-tk) that lists the SD cards and USB drives attached (never the host's own disks) and what is on them, and either writes a fresh image or updates the MontaukOS on a card. It runs as the user and does the writing by starting itself again through pkexec (it asks for the password each time). The same operations work from a terminal: list, and as root flash, update and expand (--help for the options). Images can be raw or .gz, .xz or .zip compressed; Build runs make ARCH=aarch64 rpi4-sd.

  • Write a fresh image erases the card, writes the image, clears the last MiB of the card (a backup GPT from an earlier OS) and reads the card back to verify it. Expand grows the root to fill the card right away instead of leaving it to firstboot.elf.
  • Update leaves the partition table alone and finds both partitions by label, so a root that has grown to fill the card (or was resized by hand) stays as it is. On the ESP every file is written except RPI_EFI.fd, which holds the firmware's own settings (Update the UEFI firmware replaces it); a changed config.txt is kept as config.txt.bak. On the root, os/, apps/, sdk/, boot/ get the image's new and changed files; config/ and www/ only get files the card lacks, users/ and state/ are not touched. init.toml becomes the image's, keeping each service's enabled from the card and appending services only the card has; capabilities.toml becomes the image's plus grants only the card has (old copies: .old). os/firstboot.elf is only written if the card still has it, so a grown card does not grow again. Remove system files the new image no longer ships prunes os/, apps/, sdk/ and boot/ (keeping os/btkeys.bin, the Bluetooth bonds). Unchanged files are skipped and each file is replaced by a rename; the volume is checked with e2fsck before and after, and its features against what Fs/Ext2.cpp mounts.
  • Expand (a host-side first boot) grows the root partition to the end of the card, at most 1 TiB, and the volume with resize2fs; firstboot.elf then finds nothing to do and deletes itself.

While it works, a runtime udev rule keeps the desktop from automounting the card's partitions (Linux writes to an ext2 volume as soon as it mounts it).

scripts/mk-rpi4-sd.sh downloads the firmware release once into rpi4-firmware/ (PFTF_VERSION=v1.53 by default) and builds an MBR image:

Partition Type Contents
1 0xEF, FAT32 the firmware files, Limine (EFI/BOOT/BOOTAA64.EFI), boot/kernel
2 0x83, ext2, label MONTAUKROOT the system tree (the contents of ramdisk.tar)

Limine loads only the kernel; the limine.conf on the card has no ramdisk module. Finding no ramdisk, the kernel mounts the partition labelled MONTAUKROOT as 0:/ (Fs/Boot.cpp), read through the SD driver (Drivers/Storage/Sdhci), and the ESP after it. Files load as they are used instead of the firmware reading the whole 80 MB tree before boot. The card can also boot from a USB card reader (or the image from a USB stick): the root then comes through Drivers/USB/MassStorage.cpp. A USB root can register after the boot scan (a SuperSpeed link still training, a reader still bringing its card up), so a boot that finds no MONTAUKROOT waits up to 10 s for one, servicing USB hot-plug meanwhile. Unlike the ramdisk, the root is persistent: changes to 0:/ survive a reboot, so shut down from the desktop rather than pulling power. The image is sized to its contents (ROOT_FREE_MIB, default 64, sets the free space on the root) and grows to fill the card on first boot.

First boot: growing the root

Only the SD image ships 0:/os/firstboot.elf (programs/src/firstboot, built into programs/bin-aarch64-rpi/, which the script lays over the system tree), and the script adds its init service, ahead of all others, and its capability grant (storage_admin, system_image) to the image's init.toml and capabilities.toml. On first boot it shows a status card in the login screen's style while it calls SYS_FS_GROW on drive 0 in a loop:

  1. The kernel grows the MBR entry of the root partition as far as the filesystem can use (to the end of the card, or 1 TiB), on a 1 MiB boundary (Gpt::ResizeMbrPartition).
  2. The ext2 driver grows the mounted volume (Fs/Ext2.cpp, "Online grow"): each call writes the bitmaps and zeroed inode tables of a few new block groups past the old end, then the last call commits the bitmaps, descriptor table and superblock. The descriptor table grows into the blocks mke2fs -O resize_inode -E resize=2^28 reserved; the leftover reservation is freed and resize_inode cleared, so the volume grows once.

The new groups' inode tables are the bulk of the writes (0.1% of the card, one inode per 256 KiB), written as long multi-block runs. Once the volume has grown, firstboot.elf deletes itself and init skips its (optional) service from then on. If it fails it stays and the next boot tries again; an interrupted grow is safe, since the new groups are only referenced once the superblock is written, and a partition larger than its filesystem is valid.

make ARCH=aarch64 rpi4-sd ROOTFS=ramdisk builds the old layout instead: one FAT32 partition with boot/ramdisk.tar, which Limine loads through the firmware. It boots slowly but does not depend on the kernel's SD driver.

make ARCH=aarch64 run-rpi4-sd boots the ext2 image on QEMU with the card on sdhci-pci, so the root comes through the same driver. QEMU's UEFI has no SD driver, so the card's ESP is also attached on its own over virtio for the firmware to load Limine from. The card is padded to RPI4_QEMU_CARD_MIB (default 4096), and every run starts from a fresh copy, so every run is a first boot.

The firmware defaults are what MontaukOS needs; leave them:

  • System Table Mode: ACPI. The kernel reads ACPI only.
  • Limit RAM to 3 GB: enabled. The BCM2711's PCIe can only reach the first 3 GB (its tables say so in _DMA). The kernel warns at boot when RAM extends past a device's _DMA range.

Serial console

The kernel console (the whole boot log, including what was printed before the port was found) is mirrored to the UART the firmware's SPCR table names. The firmware's config.txt applies the miniuart-bt overlay, which puts the PL011 on GPIO 14/15: header pins 8 (TX) and 10 (RX), ground on pin 6, 115200 8N1, 3.3 V levels. On the Pi 400 the header is on the back.

How the hardware is reached

Part Source Driver
4x Cortex-A72 MADT, PSCI (TF-A, SMC) Arch/aarch64 SMP via Limine MP
GIC-400 (GICv2) MADT Arch/aarch64/Interrupts.cpp
Generic timer GTDT Arch/aarch64/Timer.cpp
Display UEFI GOP (VideoCore) Limine framebuffer
Keyboard (built in), USB ports VL805 xHCI as ACPI \_SB.SCB0.XHC0 (PNP0D10) Drivers/USB/Xhci.cpp platform probe
USB 2 devices (built-in keyboard too) VIA hub on the VL805's USB 2 root port Drivers/USB/Hub.cpp
Reset, power-off PSCI Arch::Reboot, Arch::PowerOff
Bluetooth (CYW43455 on the 4B, CYW43456 on the 400) BCM2EA6 (\_SB.GDV0.BTH0) on the mini UART (BCM2836) Drivers/Bluetooth/Broadcom.cpp, Drivers/Serial/BtUart.cpp
VideoCore firmware mailbox (radio power, clocks, board MAC) BCM2849 (RPIQ) Drivers/Platform/RpiFirmware.cpp
GPIO pin functions BCM2845 (GPI0) Drivers/Platform/RpiGpio.cpp
Ethernet (GENET v5 MAC, BCM54213PE PHY) BCM6E4E (\_SB.ETH0) Drivers/Net/Genet.cpp

The VL805 sits behind the BCM2711's PCIe, whose config space is not ECAM (the root bridge's _DSD says linux-ecam-quirk-id = "bcm2711"), so the kernel does not enumerate PCI there. The firmware has already brought up the link and the VL805 and describes the controller as a platform device: a register block at 0x600000000, GSI 175, and _CCA = 0, i.e. its DMA does not snoop the CPU caches. XHC0._INI (run by the AML interpreter) enables the controller's PCI memory decoding and bus mastering. The xHCI driver then keeps its rings and contexts in uncached memory, bounces control-transfer data through an uncached page, and cleans/invalidates the caller buffers of bulk transfers (Memory/Dma.hpp). It also watches the event ring from the timer tick, so input keeps working if the INTx line never fires.

The VL805 has one USB 2 root port (port 1) and four USB 3 ones. An on-board VIA hub (2109:3431) sits on the USB 2 port, and the USB 2 lines of every socket, and the Pi 400's internal keyboard (04d9:0007), hang off it: a keyboard or mouse is always a low/full-speed device behind a high-speed hub, reached through that hub's transaction translator.

Bluetooth

The radio's Bluetooth half speaks HCI over a UART (H4) on GPIO 30-33 (CTS, RTS, TXD, RXD); its power switch, BT_ON, is line 0 of the VideoCore firmware's GPIO expander, reached through the mailbox. The firmware tables describe it as BTH0, whose UARTSerialBus names the UART it uses: with the miniuart-bt overlay the PL011 is the console, so Bluetooth has the mini UART. The kernel refuses to take a UART that is the SPCR console.

At boot the driver probe routes the pins (ALT5 for the mini UART, ALT3 for the PL011), starts the UART (interrupt driven, 16 KiB receive and transmit rings) and the H4 transport under the shared HCI layer. Once the root is mounted the idle loop finishes, as for USB adapters:

  1. power-cycle the controller through BT_ON, HCI Reset at 115200 baud;
  2. Read Local Version: the ROM's LMP subversion picks the patch file, 0:/os/firmware/brcm/BCM4345C0.hcd (0x6119, 4B) or BCM4345C5.hcd (0x6606, 400), shipped unmodified from Raspberry Pi OS's bluez-firmware;
  3. raise both ends to the operating speed (vendor 0xFC18);
  4. Download Minidriver (0xFC2E), then every record of the .hcd file (Write RAM, then Launch RAM), as Linux's btbcm does;
  5. the patched firmware restarts at 115200: reset, raise the speed again.

The mini UART's FIFO is 8 bytes deep. Its hardware RTS is tried first; if the controller answers with it on, the link runs at 921600 baud, otherwise at 460800 without flow control (what Raspberry Pi OS uses), and at 115200 if the UART has no interrupt. The baud rate derives from the VPU core clock, so mk-rpi4-sd.sh pins it (core_freq=500, core_freq_min=500 in config.txt), as the overlay's documentation requires. A failed fast bring-up is retried entirely at 115200.

The CYW43456 reports a chip-wide default address (43:45:C5:00:1F:AC); the driver replaces it with the board's Ethernet MAC from the firmware. 0:/config/bluetooth.toml still overrides both (Broadcom 0xFC01).

Ethernet

The BCM2711's own Gigabit MAC, GENET v5 (a UniMAC behind a DMA engine), talks RGMII to a BCM54213PE PHY at MDIO address 1. The firmware describes it as BCM6E4E: a 64 KiB register block, two interrupts (the driver uses the first, which carries the default queue), _CCA = 0, and a _DSD with phy-mode (rgmii-rxid: the PHY delays the receive clock, the MAC the transmit clock) and brcm,max-dma-burst-size.

The driver uses only the default queue (ring 16), with all 256 descriptors of each direction. The descriptors live in the MAC's own SRAM, so only the 2 KiB packet buffers need cache maintenance. Received frames start two bytes into their buffer (RBUF_ALIGN_2B); neither direction uses the 64-byte status blocks, so there is no checksum offload. The UniMAC accepts broadcast and its own address only (the MDF filter).

The interrupt only acknowledges and masks; frames are dispatched from the idle bottom half, which also checks the ring itself, so receive works if the interrupt never arrives. The PHY is polled once a second (its interrupt pin is not described); a link change reprograms the RGMII block and the UniMAC's speed and duplex. A link the firmware already negotiated is kept, otherwise the PHY is reset and advertises 10/100 and 1000 full duplex.

The MAC address is the board's, read from OTP through the firmware mailbox (the one the Bluetooth driver uses too); the address the firmware's own driver programmed is the fallback. dhcp.elf waits up to 10 s for a wired link, since autonegotiation takes about 3 s after the PHY reset.

Not supported yet

  • SD high-speed mode: the driver transfers by polled PIO at 25 MHz on a 4-bit bus (multi-block CMD18/CMD25 with Auto CMD12), at most 12.5 MB/s; switching the card to 50 MHz (CMD6) would double that.
  • Wi-Fi (the CYW4345x's SDIO half, on the Arasan controller).
  • Audio (HDMI via VideoCore, the 3.5 mm jack via PWM).

Status

Bluetooth works on a Pi 400 (CYW43456), including A2DP music to headphones. Ethernet has not run yet: QEMU has no GENET model. It is untested on a Pi 4B (CYW43455), and QEMU has no model of the radio.

Everything else above is verified on QEMU only, including the Pi-specific path: the firmware's own XHC0 table, retargeted at QEMU's xHCI and loaded with the bcm2711 quirk on QEMU's root bridge, boots to the desktop with non-coherent DMA. Real hardware has not been tried yet. The ext2 root on the SD card boots on QEMU (run-rpi4-sd); the first-boot grow has been tested only against the kernel's ext2 code in a host harness (images grown, then checked with e2fsck).