Repository navigation
EPIC-12: RAM layout and budgets - #13
Merged
Merged
Conversation
Every http_server handler, and most of emul.c, gemdrive.c, runner.c, term.c and select.c, was marked __not_in_flash_func, so 47 KB of code and data sat in RAM and the release heap was 35,472 bytes. RAM placement only matters for code that runs while flash is unavailable: the settings write itself, the per-tick command path, and there are no application interrupt handlers (C-09). The handlers run from flash now. The five functions of the download data path (srv_recv_cb, srv_sent_cb, stream_download_drive, stream_send_chunk, send_buffered) stay in RAM: without them a 256 KB download slowed from 0.65 s to 0.72 s, and keeping them costs 4.8 KB. Release: code and data in RAM 47,496 -> 20,824 bytes, heap 35,472 -> 60,048 (+24,576). Debug heap 23,132 -> about 55,900 before the download path was put back. Measured on hardware, medians of three runs of a 256 KB round trip: upload 3.03 s before and 3.04 s after (SD-bound), download 0.65 s and 0.64 s, listing 43-48 ms both. flash.sh keys its build folder on the source path too, so building two checkouts that are both called rp/src no longer reuses one CMake cache. The stale comment in emul.c that credited the RAM move is corrected.
DirNavigation was 24,774 bytes of permanent BSS for a browser that is open for a few seconds: 256 names of 96 bytes. Upstream allocates it on entry and frees it on exit, which is what this does now. A failed allocation says "Not enough memory to open the folder picker." on the menu instead of following a NULL, and a read error closes the picker with a message too. Release heap 60,048 -> 84,820 bytes, debug 55,900 -> 80,672. Verified on hardware: opening the picker with [o] took the heap from 71,352 to 46,568 bytes (24,784), the listing appeared, and ESC returned it to exactly 71,352. With the heap held at 22,184 bytes free the picker refused with the message on screen and the menu intact.
Pressing [X] left the Atari ST showing this app's farewell screen and Booster never appeared. The jump itself ran: the debug probe showed Booster's stack pointer loaded and VTOR pointing at 0x10120100. But the core was in lockup, PC 0xfffffffe. The jump writes VTOR and branches without stopping anything of ours, so from that instruction on, our interrupts (the buffered console's UART transmit, USB, alarms) vector through Booster's table into handlers it has not installed yet; the fault handler is not installed either, so the core locks up. Masking and clearing every NVIC interrupt first fixes it: after the change the probe reads PC inside Booster's code with its own RAM vector table installed, and Diego confirmed the ST now boots into Booster. The jump sequence itself is unchanged and still matches the template.
… (EPIC-12) Copying 4 MB from the ST to the drive failed with a TOS error and left the last file truncated. The ST waits for the RP's answer to each 1 KB chunk, and an SD write that allocates a cluster and updates the FAT blocks for tens of milliseconds: 70 ms measured on this card, with the console instrumented to report writes over 20 ms. Its budget was nowhere near that. This project had COMMAND_TIMEOUT $0000FFFF and 3 retries, where md-drives-emulator ships $0006FFFF (about 800 ms) and 5 retries for the same protocol, with a comment saying the value exists to cover FatFs SD operations. Both values are restored, in gemdrive.s and runner.s. Three more faults on the same path: - The RP slept up to SLEEP_LOOP_MS before draining the command ring, so every chunk paid that latency while the ST was already waiting. chandler_loop() now runs before the wait as well as after. - A failed write published 0 bytes written, which the ST's loop treats as progress: it would re-issue the same chunk for ever. It now bails out with EIO. - f_write ran without feeding the watchdog, so a card that stalls for 8 s would reboot the RP mid-copy. Every GEMDRIVE function also runs from RAM again (Diego's rule). STORY-01 had moved the command callback to flash, where a first execution waits on the XIP cache while the ST is blocked; only that callback was ever marked before, so the handlers themselves ran from flash even upstream. Costs about 16 KB: debug heap 80,672 -> 64,288 bytes, still +41,156 on the epic's start. term.c no longer traces every command (two lines per 1 KB chunk broke the transfer by itself; EPIC-10 STORY-04 left that trimming open), and debug builds report only writes slower than GEMDRIVE_SLOW_WRITE_US. Verified on hardware: eight 512 KB files copied from the ST, all exactly 524,288 bytes, on release and on debug.
md-devops forked the microfirmware template before its commit d7ec84e, "fix(sync): prevent false positive token detection", so its synchronous wait accepted a bare token match. A stale token from an earlier command could therefore be taken as the answer to the current one, which desynchronises the handshake: one chunk appears to succeed early and the next waits for a token that never arrives. The template's version requires the seed to have advanced as well, and is now copied verbatim into both wait loops; the file is identical to the template again. Found while chasing TOS write errors on multi-megabyte copies from the ST. It moved the first failure from the first file to the last, so it was one of several causes, not the only one.
Writes from the ST ran at 4.6 to 25 KB/s and the total time tracked the ST's command timeout, although no retry was ever visible: the counters showed 0 repeated tokens, the RP busy 0.5 ms per command, and a 1 KB frame apparently taking 45 ms to arrive, spread over 13 main-loop passes. The parser explains all of it. tprotocol_parse restarts on PROTOCOL_READ_RESTART_MICROSECONDS (10 ms) of silence, but it refreshed its timestamp only while hunting for a header, so the window was a deadline for the whole frame. Samples are timestamped when they are parsed, in batches drained from the DMA ring, and this app's main loop sleeps up to SLEEP_LOOP_MS between drains: any 1 KB payload that did not land in a single drain was thrown away mid-frame. The ST then waited its full timeout and resent with a fresh token, which is why the time scaled with the timeout and no repeat was ever counted. md-drives-emulator has the same parser and the same constant, but its runtime loop never sleeps, so a frame never spans a gap. The timestamp is now refreshed on every sample, which is what "restart after silence" means, and the window is 50 ms to cover this loop's own gap between two drains. Measured on hardware, 4 MB copied from the ST with the timeout at $0006FFFF: 13-14 minutes before, 47 seconds after (87 KB/s), every file byte-exact, no checksum errors, no repeats. A 1 KB frame now arrives in 0.91 ms across 1.24 passes instead of 45 ms across 13. chandler.c keeps the counters this was found with (handled, dropped, repeated, checksum errors, busy/gap/quiet microseconds, passes per frame). They cost a few microseconds per command and are read over SWD by symbol, so a release build can be measured.
The deepest static paths were 5,696 bytes through the terminal, 5,120 through GEMDRIVE and 3,784 through the HTTP server, against a 2 KB reserved stack. Four frames accounted for most of it: - term_loop copied the published protocol slot into a 4,184-byte local. It reads the slot in place now; the double buffer already keeps the producer off it and both run on core 0. - handleReadBuffCall read into a 4 KB local and then copied it into the shared window. It reads straight into the window and swaps there. - handleWriteBuffCall copied the chunk into a 1 KB local to byte-swap it. It swaps it where the parser already put it. - pathUp held 256 pointers (1 KB) for a path that cannot contain more than half its length in segments. FATFS fsys moved out of emul_start's frame into .bss, and the path buffers of the nine largest HTTP handlers are static: 6.2 KB of RAM for 1,672-byte frames, safe because the server is single-threaded and its spin-waits pump chandler_loop only, never lwIP. Deepest static path now: release 2,976 bytes, debug 2,952, MinSizeRel debug 2,928 -- all under the story's 3 KB. On hardware the measured high-water fell from 5,772 to 2,564 bytes and the smoke script passes. measure_builds.sh also lost its way to the repo root when the tools moved into tools/dev.
FF_FS_TINY 0 gives every open FIL its own 512-byte sector buffer, 6 KB across the slots this app opens. Measured both ways on hardware: eight 512 KB files copied from the ST, which interleaves a read and a write through the shared buffer and is the case the setting can hurt, took 47 s at 0 and 48 s at 1; the 256 KB HTTP round trips stayed inside their run-to-run spread. Release heap 61,556 -> 67,700 bytes.
The SDK default put core 0's stack at the end of the 4 KB SCRATCH_Y bank with PICO_STACK_SIZE reserving 2 KB of it. This firmware's measured peak was 2,564 bytes before this epic's work and up to 7,700 during a Runner session, so it was already running through the rest of SCRATCH_Y and into core 1's SCRATCH_X, silently. The stack now occupies the top of main RAM and the heap is capped at its bottom, porting Booster's memmap_booster.ld block and its reasoning. Size follows D-02: peak x 2 + 1 KB, rounded to 4 KB, never below 16 KB; 2,564 x 2 + 1,024 rounds to 8 KB, so the 16 KB floor decides. Set in the linker script only, never through PICO_STACK_SIZE, which would move core 1's stack as well. EPIC-09's painting and high-water scan follow the stack: one plain region between __StackBottom and __StackTop, with no code interleaved as there was in the scratch banks. Release heap 67,700 -> 51,316 bytes (35,472 at the start of this epic), debug 47,164. Measured on hardware: reserved 16,384, peak 2,580 during the smoke script on release and 2,008 on debug, no overflow, and both build types pass the smoke script.
With the stack at the top of RAM, an overflow would write down through the heap and, past that, whatever else it reached. PICO_USE_STACK_GUARDS puts an MPU region over the lowest 32 bytes of core 0's stack, so an overflow faults there and reboots with a breadcrumb (D-03). Enabled in both build types, not just debug: in a shipping build the alternative is silent corruption that surfaces much later as a mystery. It costs one of the eight MPU regions, which nothing else here uses, the lowest 32 bytes of the stack, and a few instructions per core at boot. EPIC-09's stack painting had to move above the guard: it painted from __StackBottom and faulted on the guarded bytes at every boot, which put the RP in a crash loop tight enough that neither picotool nor the probe could attach -- recovery needed BOOTSEL. Painted stack is now 16,352 of 16,384 bytes. POST /api/v1/debug/test/stack-overflow (debug builds) recurses until it hits the guard. Verified on hardware: HARDFAULT with pc inside the recursion and sp 0x2002bff8, eight bytes below the 0x2002c000 stack bottom, then a reboot with reason hardfault and a clean recovery. Two earlier versions of that test were optimised into a loop by GCC and never grew the stack; the frame has to escape through a volatile pointer to stop it. Release: smoke passes, stack peak 1,936 of 16,384, heap unchanged.
Three asserts in memmap_rp.ld, so a change that eats RAM stops at the build instead of on a desk: the heap may not fall below __MinHeapSize (40K), the stack may not overlap the heap, and the core-0 stack may not drop below D-02's 16K floor. 40K is what this epic's measurements support: 51,316 bytes of heap on release and 47,164 on debug, about 15 KB ever claimed and 31 KB free at the worst moment of a smoke run. Every link now prints the same size report the CI job summary uses, so local and CI builds report identical numbers. Two of its rows were wrong since the stack moved: it read the stack from .stack_dummy, still 2 KB after STORY-04 put the real 16 KB stack at the top of RAM, and the heap from __bss_end__ rather than __end__. Checked by raising the floor to 52K: the link fails with "heap is below __MinHeapSize: something new is eating RAM, see EPIC-12 STORY-06".
The developer tools in tools/dev leave .pyc files behind when they import each other.
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.
Fifth epic of v1.1.0. The core-0 stack had 2 KB reserved at the end of the 4 KB
SCRATCH_Ybank while the firmware's real peak was 5,772 bytes, and up to 7,700 during a Runner session: it had been running through the rest of that bank and into core 1'sSCRATCH_X, unnoticed, on every build that shipped. Meanwhile 47 KB of code sat in RAM that never needed to be there.What changes
http_serverhandler, and most ofemul.c,gemdrive.c,runner.c,term.candselect.c, was marked__not_in_flash_func. RAM placement only matters for code that runs while flash is unavailable. The five functions of the download data path stay in RAM, because without them a 256 KB download slowed from 0.65 s to 0.72 s, and all of GEMDRIVE stays in RAM, which is Diego's rule for the ST-facing path.DirNavigationwas 24,774 bytes of permanent.bssfor a browser that is open for seconds; a failed allocation now says so on the menu instead of following a NULL.term_loopcopied a whole protocol slot (4,184 bytes) and now reads it in place;handleReadBuffCallread into a 4 KB local and copied it to the window, and now reads straight into the window;handleWriteBuffCallswaps the chunk where the parser already put it;pathUpheld 256 pointers for a path that cannot have more than half its length in segments;FATFS fsysleftemul_start's frame; the nine largest HTTP handlers keep their path buffers in.bss. Deepest static path 5,696 -> 2,976 bytes.FF_FS_TINY 1shares one FatFs sector buffer, decided by measurement: the ST copy that interleaves a read and a write took 47 s with per-file buffers and 48 s with the shared one.PICO_USE_STACK_GUARDSin both build types puts an MPU region over the lowest 32 bytes of the stack; a forced overflow givesHARDFAULT pc=0x10003af6 sp=0x2002bff8, eight bytes below the stack bottom, and a clean reboot.POST /api/v1/debug/test/stack-overflowtriggers it in debug builds..stack_dummyand whose heap row used the wrong symbol.Fixes found while verifying this epic
Measuring GEMDRIVE throughput exercised paths the smoke script never had.
$0000FFFFwith 3 retries where md-drives-emulator ships$0006FFFFand 5 for the same protocol, while an SD write here blocks up to 70 ms; the RP slept up to 10 ms before draining the command ring; and the protocol parser treated its 10 ms resync window as a deadline for a whole frame, so any 1 KB payload that did not arrive in a single drain was discarded and the ST resent it after a full timeout. The parser now refreshes its timestamp per sample, which is what "restart after silence" means.f_writeran without feeding the watchdog, so a card that stalls for 8 s rebooted the RP mid-copy.[X]never reached Booster. The jump ran, but our interrupts vectored through Booster's table before it installed handlers and the core locked up (PC 0xfffffffe), leaving the ST on this app's farewell screen. Masking and clearing the NVIC first fixes it.term.cno longer traces every command: two lines per 1 KB chunk broke transfers ondebugby itself.Verified on hardware
release,--manual --entries 500debug, automated[X]BoosterKnown and filed, not fixed here