port/raspberrypi: add a bare metal BCM2711 port for the Raspberry Pi 4B - #1025
Closed
zmscode wants to merge 2 commits into
Closed
port/raspberrypi: add a bare metal BCM2711 port for the Raspberry Pi 4B#1025zmscode wants to merge 2 commits into
zmscode wants to merge 2 commits into
Conversation
Adds an aarch64 target to microzig. The BCM2711 is an application processor rather than a microcontroller, so this is a bring up port: it boots, drives gpio, talks over the serial console and keeps time. The boot model is different from every other port. There is no flash to execute from and no reset vector table to place: the VideoCore reads kernel8.img off the card to 0x80000 and the armstub enters it at EL2 with cores 1 to 3 parked. A firmware here is therefore a plain ram image whose first byte is the entry point. The cpu module parks anything that is not core 0 so the image also survives being entered by all four, points sp at the top of ram, clears .bss and installs an exception vector table that reports the syndrome and the faulting address instead of locking up silently. It stays at EL2 and leaves the mmu and the caches off, which keeps the bring up small at the cost of speed. _start has to be naked: a normal function pushes a frame onto whatever stack the firmware left and then keeps addressing its locals through it after sp has moved. Broadcom publishes no svd for this part, so the registers the port uses are written out by hand from the peripherals datasheet, in the low peripheral view the cores see at 0xFE000000. The gpio hal drives the pull resistors through GPIO_PUP_PDN_CNTRL_REG, which replaces the clocked GPPUD sequence of the older chips. The uart hal drives the PL011 and routes it onto GPIO14 and GPIO15, taking the header back from the mini uart, whose baud rate would otherwise move with the VideoCore core clock. Timekeeping reads the ARM generic timer, which needs no peripheral setup. The linker script places .got explicitly. Left as an orphan it lands past the heap at the end of the ram region, and the flat image then spans all of it. Not here yet: the GIC-400, the mmu, the firmware mailbox (and with it the ACT led, which hangs off the VideoCore gpio expander), cores 1 to 3, and i2c, spi, pwm, sd and usb.
Running with the mmu off does not work past the simplest register poking. Every access is then treated as Device-nGnRnE, and device memory does not allow unaligned accesses at all, so compiled code faults as soon as it touches an unaligned field. The first log line was enough: booting blinky under QEMU faulted inside std.fmt with esr=96000021, an alignment fault on a data abort. So identity map the low four gigabytes with one 1 GiB block descriptor each, the first three as Normal memory and the fourth, which holds the peripheral window, as Device, and turn the caches on with it. The exception path also needed a re-entry guard. Reporting a fault runs through the formatter and the uart, either of which can fault in turn, and the console filled with half printed messages instead of one. With both fixed, blinky runs under `qemu-system-aarch64 -M raspi4b`: reports EL2, reads the generic timer rate off CNTFRQ_EL0 and toggles at the interval it asked for. uart_echo echoes. Both READMEs now document the QEMU invocation.
Contributor
|
I don't see why application processors shouldn't be in MicroZig. Are you planning on buying the hardware so you can test this patch? |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds an aarch64 target to microzig: a bare metal port for the Broadcom BCM2711, the SoC of the Raspberry Pi 4 Model B.
This is a different shape of port from the rest of the tree, so I would rather have the approach argued with than merged quietly. It is a bring up port: it boots, drives gpio, talks over the serial console and keeps time. Opening it as a draft partly because it is untested on hardware (see below) and partly to ask whether an application processor belongs in microzig at all.
Why it fits
Less had to change than I expected. Ports already supply their own cpu module (the esp port does), the core's cpu contract is small (
export_startup_logicplus five interrupt functions, withInterruptOptionsandCPU_Optionsoptional),register_definitionaccepts a hand written zig file, and nothing in the build system gates on architecture. So this adds no core changes at all beyond registering the port.Boot model
There is no flash to execute from and no reset vector table to place. The VideoCore, not the ARM cores, starts first: it reads
config.txt, loadskernel8.imgfrom the boot partition to0x80000, and runsarmstub8.bin, which drops to EL2, parks cores 1 to 3 on a spin table and enters the image at its first byte on core 0. A firmware here is therefore a plain ram image whose first byte is the entry point, which is what.ram_image = trueandld/kernel8_sections.ldexpress.The cpu module parks anything that is not core 0 (so the image also survives being entered by all four), points sp at the top of ram, clears
.bssand installs an exception vector table that reports the syndrome and the faulting address rather than locking up silently. It stays at EL2 rather than dropping to EL1, and brings up a flat identity mapping with the caches on (see Testing for why that is not optional)._starthas to be naked. As an ordinary function it emitted a frame push before the stack pointer was set, and then kept addressing its locals through a frame pointer into the stack it had just abandoned:Peripherals
Broadcom publishes no SVD for this part, so
src/chips/BCM2711.zigwrites out the registers this port uses by hand from the peripherals datasheet, in the low peripheral view the cores see at0xFE000000.hal.gpiocovers the 58 pins. Note the BCM2711 sets the pull resistors directly throughGPIO_PUP_PDN_CNTRL_REG; the clockedGPPUDsequence of the BCM2835 and BCM2837 does not carry over.hal.uartdrives the PL011 and routes it onto GPIO14 and GPIO15, taking the header back from the mini uart and leaving bluetooth without a uart. That is the right trade here: unlike the mini uart, the PL011 baud rate does not move with the VideoCore core clock. It does assume the firmware default 48 MHz reference clock.hal.timereads the ARM generic timer throughCNTPCT_EL0, so it needs no peripheral setup and is not shared with the VideoCore.A linker script note that may interest #857
The first flat image this produced was 134 MB. An orphan
.gothad been placed past the heap at the end of the ram region, so the binary spanned all of it; placing.gotexplicitly in the script fixed it.That is not the same root cause as #857, but while checking I did reproduce that one. On
blinky_ch32v003.elfthe.heapgets its own program header:A zero
fileszsegment 512 MB up is what the flat binary is spanning. If that is worth fixing centrally rather than working around with GNU objcopy,tools/generate_linker_script.zigemitting an explicitPHDRSblock, so the NOBITS heap does not get aPT_LOADof its own, would do it for every port at once. Happy to open that separately if it sounds right.Testing
Runs under QEMU, not yet on silicon — I do not have a Pi 4B, which is why this is a draft.
Unlike most targets in this tree, this one can be exercised without hardware, which turned out to matter:
uart_echoechoes what it is given. Also verified: entry is0x80000with_startfirst, readingMPIDR_EL1, parking the spare cores and pointing sp at the top of the declared ram region; the vector table is 2048 aligned with its sixteen entries 128 bytes apart.That 62.5 MHz is QEMU's generic timer rate rather than the 54 MHz of real silicon, which is a good argument for
hal.timereadingCNTFRQ_EL0rather than assuming.The first QEMU run is also what caught the biggest bug in this branch. The port originally left the mmu off to keep the bring up small, and faulted inside
std.fmton the very first log line withesr=96000021— EC 0x25, DFSC 0x21, an alignment fault. With the mmu off every access is Device-nGnRnE, and device memory does not permit unaligned accesses at all, so ordinary compiled code cannot run. It now identity maps the low four gigabytes with 1 GiB blocks, three of Normal memory and one of Device for the peripheral window, and turns the caches on with it. The exception path also needed a re-entry guard, since reporting a fault runs through the formatter and the uart and either can fault in turn.What QEMU cannot tell us is anything about the real board's clocking. The most likely remaining first-boot problem is the PL011 reference clock, which the port assumes is the firmware default 48 MHz but which
init_uart_clockinconfig.txtoverrides.Not here yet
The GIC-400 (so interrupts only mask globally), the mmu and caches, the firmware mailbox, cores 1 to 3, and i2c, spi, pwm, sd and usb. The mailbox is the highest leverage of those: it unlocks the framebuffer, the board revision, real clock rates instead of assumed ones, and the green ACT led, which on a Pi 4B hangs off the VideoCore gpio expander rather than a BCM gpio and so cannot be driven today.