A simple 2nd-stage bootloader for the GBA, intended to be loaded via Multiboot/Xboo Burst Boot Backdoor (recommended), or directly embedded into homebrew (less-recommended, but works in a pinch).
Flow:
Each transaction is 32 bits, with the Master and Slave exchanging their words simultaneously.
(SPI Mode 3, MSB First)
Master - PC/Other
Slave - GBA
Master -> "RDY?"
Slave -> "NOOT" Note: Master will typically repeat sending "RDY?" if a "NOOT" was not yet received, this could be detected similarly to the Xboo Burst Boot Backdoor and alternatively branched into, instead of downloading nootloader via the Xboo Backdoor.
Master -> length of incoming payload in bytes
Slave -> "LEN?" (to indicate expecting length)
- First payload word sent
Master -> first_word
Slave -> "LOK!" (to indicate length was accepted, i.e. 4 <= len <= 0x40000)
- Subsequent payload words sent
Master -> payload_word
Slave -> EWRAM Address for payload_word (the first expected would've been 0x2000000 if not for "LOK!", so the first expected should be 0x2000004)
- Final payload word sent
Master -> last_payload_word
Slave -> "CRC?" (to indicate expecting CRC as next and final word)
- CRC exchange and compare
Master -> crc
Slave -> crc
The CRC is *very* simply calculated: sum of all payload words XOR'd by payload length (u32)
If the CRC matches, the slave will boot the downloaded payload, otherwise it will wait for another "RDY?" to restart the process.
Upon verifying the crude CRC, nootloader will clear itself from IWRAM while simultaneously resetting all registers and peripherals.
This is achieved by copying a teeny subroutine into the (unused by nootloader) user stack area that overlaps with the final 0x200 bytes in IWRAM that don't get cleared by RegisterRamReset even when set to clear IWRAM.
The subroutine calls RegisterRamReset with all flags but EWRAM, followed by SoftReset, which is configured to branch to 0x2000000 after it resets the final 0x200 bytes within IWRAM.
Regarding payload entry points and nootloader's (optional yet frequent) payload patching requirement:
- When launching a binary off a cartridge, the GBA branches to (header-relative) address
0x0. - When launching a binary downloaded via Normal Mode Multiboot, the GBA branches to (header-relative) address
0xC0.
This allows developers to craft a single binary that can behave differently depending on how it was loaded, but poses an interesting problem for nootloader, as SoftReset branches to 0x2000000, whereas Multiboot would typically branch to 0x20000C0.
As a result, it is a strong recommendation that payloads sent to nootloader have their root entry points either contain branches to the same points in your code, or have the payloads patched by the sending application to header-relative 0xC0 (or something custom if you're modifying my stuff~).
Otherwise, the binary may assume it's running on a cartridge and try to read extra data from the potentially-empty slot.
This patching is already performed (with safety checks) by gbasend (and the desktop crate using it, gbasend-cli), so no extra work should be required from users, but it should be noted in case you use SoftReset yourself and potentially rely on that strange branching behavior, or if there is a payload that performs a self-CRC check, that would be violated by the aforementioned patching. There is an option to skip the behavior within my crates, but if there's a payload that expects the initial branch to 0xC0 + SoftResets to 0x0 and/or self-CRCs, then it would be deemed incompatible. I am unaware if any such payload exists, given the niche setups required, but I'd love to know!
To be clear, this is an entirely self-imposed problem, but the idea of having a rather deterministic reset state from the combination of RegisterRamReset and SoftReset combined was an appealing one. If there's no such benefit, then perhaps I'll reverse course. We'll see~
Also within the src/ folder is an implementation of the Xboo Burst Boot Backdoor!
Original Xboo Documentation by Martin Korth in no$gba/GBATEK: https://problemkaputt.de/gbatek-aux-xboo-pc-to-gba-multiboot-cable.htm
While you can embed a 2nd-stage bootloader like nootloader (~588 bytes) directly into your homebrew apps or games, it comes with a few downsides, such as locking your binary to that 2nd-stage loader, which may get bugfixes or potentially deprecated, or being unable to use a different loader with it's own (potentially faster) transfer method.
A simple but effective approach to this problem is to arm your homebrew with a slim loader that, upon receiving the request to do so, will take over the GBA to load a 2nd-stage bootloader into IWRAM, which will in turn prepare to load whichever larger new payload into EWRAM.
The assembly file contains that simple, slimmer loader (~320 bytes) that can be entered via C/++ or ASM, and will listen for Xboo Burst Boot requests.
Flow:
Each transaction is 32 bits, with the Master and Slave exchanging their words simultaneously.
(SPI Mode 3, MSB First)
Master - PC/Other
Slave - GBA
Master -> "BRST"
Slave -> "BOOT" Note: Master will typically repeat sending "BRST" if a "BOOT" was not yet received.
Master -> length of incoming payload in bytes
Slave -> "OKAY" (to indicate expecting length)
- All payload words sent sequentially
Master -> payload_word
Slave -> Reponse doesn't matter, this implementation will typically return number of bytes remaining.
- CRC exchange and compare
Master -> crc
Slave -> crc
The CRC is ***very*** simply calculated: sum of all payload words.
If the CRC matches, the slave will boot the downloaded payload, otherwise it will wait for another "BRST" to restart the process.