diff options
Diffstat (limited to 'README.md')
| -rw-r--r-- | README.md | 77 |
1 files changed, 74 insertions, 3 deletions
@@ -5,6 +5,8 @@ zig build # compile, link, and emit a flashable image zig build flash # ...then write it to the chip and run it zig build run # flash, then print the console (ordered; `flash monitor` is not) zig build monitor # reset the board and print its console +zig build console # attach an interactive terminal: keystrokes in, screen out (Ctrl-] detaches) +zig build interact # flash, then attach that terminal (ordered, like `run`) zig build reset # just pulse the reset line zig build size # where every byte of the image went zig build test # host tests: image builder, and the register layer's field arithmetic @@ -65,11 +67,14 @@ Everything is a `b.option`, so `zig build -h` lists them all. | `-Dflash-size=<enum>` | `16MB` | fitted flash, written into the image header | | `-Dmin-rev`/`-Dmax-rev` | `100`/`199` | silicon revision window. The pre-v3 P4 needs 100..199 | | `-Ddescriptor=<enum>` | `minimal` | 184-byte descriptor, or `full` for the 256-byte one `esptool image-info` can parse | -| `-Dstack=<u32>` | `8192` | stack size; the generated linker script follows | +| `-Dstack=<u32>` | `8192` | stack size; the generated linker script follows. `32768` under `-Dpardes` | | `-Dverify=<bool>` | `true` | ask the ROM for an MD5 of what it stored and compare | | `-Delf=<bool>` | `false` | also install the ELF | | `-Doptimize=<mode>` | `ReleaseSmall` | firmware default, not Debug (Debug costs ~780 B here) | | `-Dseconds=<u32>` | `5` | how long `monitor` listens | +| `-Dconsole-baud=<enum>` | `b115200` | the interactive console's rate: what the bootloader leaves UART0 at. Distinct from `-Dbaud`, which the ROM loader auto-detects | +| `-Dpardes=<bool>` | `false` | build the pardes editor as the application. Needs the object below | +| `-Dpardes-obj=<path>` | `../02-pardes-code/zig-out/pardes-p4.o` | the editor, compiled freestanding by its own build and linked here | ## Layout @@ -79,16 +84,82 @@ tools/image.zig ELF -> ESP image. Header, segments, congruence filler, ch tools/image_test.zig 8 host tests, one per rule the ROM bootloader enforces tools/rom.zig SLIP framing + the ROM loader protocol. No software stub tools/serial.zig termios2 raw mode, arbitrary baud, DTR/RTS reset dance +tools/console.zig the interactive bridge: raw stdin <-> UART, and the window-size handshake src/soc.zig comptime register model: GPIO, IOMUX, mask-ROM entry points, cycle counter src/appdesc.zig esp_app_desc_t, linked as its own object so it cannot be optimised away src/main.zig demo: prints what it can prove, then blinks +src/pardes/ the pardes editor as firmware: entry, heap, UART, and the C ABI it links to examples/minimal.zig the floor: 432 B, blinks and nothing else +examples/echo.zig UART0 duplex echo: the proof that receive works on the die +examples/memprobe.zig what RAM this board actually has, measured rather than assumed +examples/heapcheck.zig the allocator under an editor's workload, on the die ``` +## What RAM this board has + +Measured by `examples/memprobe.zig`, on the die, because it cannot be read off ESP-IDF's linker +fragments. The relevant one (`esp_system/ld/esp32p4/memory.ld.in:18-33`) is parameterised on +`CONFIG_CACHE_L2_CACHE_SIZE`, and that Kconfig's own help text says the size is set "on application +startup" — by an application this is not. + +``` +0x4FF03000..0x4FF3F000 240 KiB RAM .data/.bss/.stack live at the bottom of this +0x4FF3F000..0x4FF40000 4 KiB ROM the mask ROM's .data/.bss; ets_printf needs it +0x4FF40000..0x4FFA0000 384 KiB RAM handed over whole as __heap_start..__heap_end +0x4FFA0000..0x4FFC0000 128 KiB cache NOT memory: the L2 cache lives here +0x48000000 PSRAM 32 MB fitted, untrained; touching it hangs the core +``` + +That last RAM line cost a bug worth repeating, because the first version of the probe reported the +whole upper 512 KiB as usable. It wrote a pattern to a page and read it straight back, one page at a +time — and a store followed immediately by a load of the *same* address returns the stored value +whether the backing store is real memory, an address mirror, or merely a dirty cache line. Writing +every page before reading any page separates the three, and the top 128 KiB then failed. ESP-IDF's +own arithmetic agrees exactly: `SRAM_HIGH_SIZE = 0x80000 - CONFIG_CACHE_L2_CACHE_SIZE`, with the +128 KiB default from `esp_system/port/soc/esp32p4/Kconfig.cache:5,19`. The wrong number had already +been committed to the linker script, where it handed 128 KiB of live L2 cache to an allocator. + +PSRAM stays untrained deliberately. ESP-IDF's ESP32-P4 implementation runs past a thousand lines — +MPLL, MSPI clocking, pin drive and DQS, CS timing, mode registers, a connectivity check, and a +whole timing-calibration subsystem — and the mask ROM exposes only MMU mapping +(`Cache_PSRAM_MMU_Init`, `Cache_PSRAM_MMU_Set`), no device init. The probe reads `0x48000000` on +purpose and hangs there, which is why it prints its cursor before every access rather than after. + +**The RTC watchdog is armed when the bootloader hands over**, and it expects the application to take +it over. Nothing here did, so every example in this repo had been resetting on a ten-second cycle, +invisibly, for as long as no run lasted eight seconds. `examples/heapcheck.zig`'s 20,000 allocations +is the first run that did. `hal.rwdt.disable()` is the fix and `hal.rwdt.armed()` is worth printing +at startup. + ## What adversarial review found -Five reviewers went at this in two rounds; sixteen findings were applied. The ones worth knowing -about, because each is a trap the next person will hit too: +Five reviewers went at this in two rounds; sixteen findings were applied. Two more rounds went at +the memory map and the editor port and found the three below first. The ones worth knowing about, +because each is a trap the next person will hit too: + +* **A linker symbol declared as an object gives the optimiser a zero-sized object, and it will drop + your stores.** `extern const __heap_start: anyopaque` plus `@intFromPtr`/`@ptrFromInt` looks like + the obvious way to reach a region the linker script defines. The pointer it produces carries + provenance for zero bytes, so the ordinary (non-volatile) store the allocator makes through it is + dead code the backend may remove — and did. The first block header read back as + `size=2988759312 next=0x14284684` instead of `{393216, 0xFFFFFFFF}`, the free-list walk followed + garbage, and with asserts compiled out in `ReleaseSmall` that is a silent hang with no console + output after `MARK HEAP_INIT`. `@extern([*]u8, .{ .name = "__heap_start" })` has no size to lose. + Note that `examples/memprobe.zig` could not have caught this: it writes through a `volatile` + pointer, which the optimiser must leave alone. The two files disagreed about whether the same + address worked, which is what made it findable. + +* **A one-page memory probe cannot tell RAM from a mirror or a cache line.** See the section above: + writing and reading the same address back-to-back succeeds in all three cases, and the pattern + being address-derived does not help because the alias is written *and* read through the alias. + This one had already shipped a wrong 512 KiB into the linker script. + +* **Ignoring `POLL.HUP`/`ERR`/`NVAL` is a hot spin, not a no-op.** `tools/console.zig` polls stdin + and the port and acted only on `POLL.IN`. Unplug the CH340 mid-session and `revents` carries + `HUP|ERR|NVAL` forever: `poll` returns immediately with a non-zero count, neither branch matches, + and the loop burns a core with the terminal still in raw mode and `ISIG` off — so Ctrl-C cannot + even end it. `std.posix.poll` cannot report it as an error either, because a dead descriptor is + delivered in `revents` rather than errno (`std/posix.zig:1007-1017` maps `INVAL` to `unreachable`). * **The ROM's status byte is at `data[len-4]`, not `data[len-2]`.** The four-byte trailer is a ROM-versus-stub difference (esptool `loader.py:653-655`). Reading the wrong byte made *every* ROM |
