summaryrefslogtreecommitdiff
path: root/README.md
diff options
context:
space:
mode:
Diffstat (limited to 'README.md')
-rw-r--r--README.md77
1 files changed, 74 insertions, 3 deletions
diff --git a/README.md b/README.md
index 6e15730..abb94d7 100644
--- a/README.md
+++ b/README.md
@@ -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