From 4300f7f4a8212e117bd58362c47cf57580f0808d Mon Sep 17 00:00:00 2001 From: ScottW514 Date: Wed, 19 Aug 2026 21:21:30 -0400 Subject: [PATCH] Follow the pulse ring to 32 MiB The ring and the device-tree pool that backs it are now the size the factory firmware runs. Docs follow, and the hardware facts bank gains the reserved-memory map: 511 MiB usable, 96 MiB reserved for DMA, ~454 MiB left to Linux. image.health reads the pool size and ring_mb back and fails if they disagree, so a pool the parameter does not use cannot ship unnoticed, and checks that free never exceeds the ring less its gap. --- docs/BRINGUP.md | 30 +++++++++++++++++++-------- docs/MOTION.md | 8 ++++---- forgetest/forgetest/suite/image.py | 33 ++++++++++++++++++++++++++++-- kas/README.md | 6 +++--- 4 files changed, 60 insertions(+), 17 deletions(-) diff --git a/docs/BRINGUP.md b/docs/BRINGUP.md index 80a97ff..c7357f6 100644 --- a/docs/BRINGUP.md +++ b/docs/BRINGUP.md @@ -26,7 +26,7 @@ Read together with: hardware-validated.** - **Platform bring-up: complete and hardware-verified.** SDMA + EPIT pulse - playback out of a 16 MiB reserved pool, live-fed during a run; laser PWM at + playback out of a reserved DMA pool, live-fed during a run; laser PWM at 39.98 kHz; `CONFIG_PREEMPT=y`; both OV5648 cameras on the mainline imx-media pipeline with VPU JPEG encode; A/B slot install, signed `.fw` releases and factory restore. @@ -657,12 +657,22 @@ not a release. `sdio write failed (-84)` and costs ~1 s of Wi-Fi (wlcore firmware recovery) — see "Wi-Fi SDIO CRC watch" under Next work. - **SDMA pulse engine**: ring size = the `ring_mb` module parameter (default - 16 MiB; power of two, must fit the 16 MiB `cnc-pulsebuf` no-map DT pool). - Free = size − 32 KiB gap. Bench-verified at 16 MiB: 20 MB streamed at 100 kHz - through the wrapping ring, 0 ENOMEM, 0.4 ms max write latency, starve → - `underrun` per protocol. The ring caps legacy cloud-mode job length - (whole-file preload: ~1 MiB per 100 s of 10 kHz stream, so ~28 min); the - grblHAL live feed keeps only a few KB in flight. The playback script is + 32 MiB, the factory ring size; power of two, must fit the 32 MiB + `cnc-pulsebuf` no-map DT pool). Free = size − 32 KiB gap, so 33,521,664 bytes. + Bench-verified on the 16 MiB ring the earlier images shipped, and the + mechanism is size-independent: 20 MB streamed at 100 kHz through the wrapping + ring, 0 ENOMEM, 0.4 ms max write latency, starve → `underrun` per protocol. + The ring caps legacy cloud-mode job length (whole-file preload: ~1 MiB per + 100 s of 10 kHz stream, so ~56 min); the grblHAL live feed keeps only a few + KB in flight. +- **Reserved memory**: 511 MiB usable DRAM (`0x10000000`–`0x2fefffff`), of which + 96 MiB is reserved for DMA: the 32 MiB `cnc-pulsebuf` no-map pool (dynamically + placed, `alignment = size`, so it lands at `0x2c000000`) plus 64 MiB of + reusable CMA for camera/IPU/VPU buffers. no-map means the pulse pool is gone + from the kernel's map whether a job uses it or not, which is what makes + `dma_alloc_coherent()` deterministic for a late-probing out-of-tree module. + MemTotal ~454 MiB; measured idle use in GRBL mode with the daemon and + controller up is ~100 MiB. The playback script is relocated to SDMA channel 26 at `<26 0xF00>` (halfword 7680) with a pre-run integrity guard; the probe lines to look for are `EPIT clock 66000000 Hz` and `SDMA channel 26 reserved for pulse playback (script at halfword 7680)`. @@ -1010,7 +1020,11 @@ Open items only. Anything closed is in `CAMPAIGN-LOG.md`. image built with the `-pin.inc` layout: that image is a platform change against everything recorded so far, unavoidably, and only from then on does a component pin bump re-require just the tests covering that - component. `20260817124714` was built after the pin files landed and the + component. It now also carries the 32 MiB pulse ring (DT pool plus the + module default, batched into that flash), which is a platform change in its + own right and the first bench sighting of the larger ring: `image.health` + reads the pool and `ring_mb` back, and a preloaded cloud job past the old + 28-minute cap is the capability proof. `20260817124714` was built after the pin files landed and the parity tests were driven on it, but no full-campaign export is recorded for it — confirm on the bench and write the record here. Also still owed: exercising the ported bench tools from the page (they are registered and diff --git a/docs/MOTION.md b/docs/MOTION.md index 8911229..8ccc7f6 100644 --- a/docs/MOTION.md +++ b/docs/MOTION.md @@ -60,7 +60,7 @@ Two consequences worth knowing: ### The ring, and two ways to fill it -Pulse bytes go into a 16 MiB ring buffer in reserved memory. There are two ways +Pulse bytes go into a 32 MiB ring buffer in reserved memory, the same size the factory firmware uses. There are two ways to use it, and the mode you run decides which: - **Live streaming (GRBL mode).** The controller keeps only a small window of @@ -70,8 +70,8 @@ to use it, and the mode you run decides which: falls behind far enough to empty the ring, the machine enters an **underrun** state: motion stops instantly, and position is no longer trusted. - **Preloading (cloud mode).** The whole job is written into the ring before it - starts. Nothing can starve, but the ring size caps job length — roughly - 1 MiB per 100 seconds at the cloud's 10 kHz tick, so about 28 minutes. A job + starts. Nothing can starve, but the ring size caps job length: roughly + 1 MiB per 100 seconds at the cloud's 10 kHz tick, so about 56 minutes. A job larger than the ring is rejected cleanly before it runs. ### Stopping and resuming at the hardware level @@ -398,7 +398,7 @@ switch to cloud mode and back — re-home after switching. | Input | G-code over TCP:23 | a downloaded pulse file | | Ring use | live-streamed, small window | whole job preloaded | | Machine tick | 28160 Hz default | 10 kHz (from the job header) | -| Job length limit | none | ~28 minutes (ring size) | +| Job length limit | none | ~56 minutes (ring size) | | Needs internet | no | yes | | Laser arming | button press per job | button press per job | | Button mid-job | feed hold / cycle start | pause with backtrack / resume with lead | diff --git a/forgetest/forgetest/suite/image.py b/forgetest/forgetest/suite/image.py index 1970d91..6f4b9ee 100644 --- a/forgetest/forgetest/suite/image.py +++ b/forgetest/forgetest/suite/image.py @@ -41,6 +41,16 @@ def kernel_config(): return cfg +def _dt_u32(path): + """A device-tree cell as an int (big-endian), or None.""" + try: + with open(path, "rb") as f: + raw = f.read(4) + except OSError: + return None + return int.from_bytes(raw, "big") if len(raw) == 4 else None + + def fds_of(pid): out = [] try: @@ -61,8 +71,8 @@ def fds_of(pid): ("grblhal-glowforge", "CMakeLists.txt"), ("kernel-module-glowforge", "**"), ("linux-fslc", "**")], description="The image that is running is the image the manifest describes, with the " - "kernel options, the module, the daemon ownership, the init ordering, and the " - "file modes the release depends on.") + "kernel options, the module and the pulse ring it maps, the daemon ownership, " + "the init ordering, and the file modes the release depends on.") def image_health(ctx): ev = ctx.evidence manifest = ctx.runner.manifest @@ -98,6 +108,25 @@ def image_health(ctx): ev["cnc_free"] = free ctx.check(free is not None and free > 0, "cnc/free unreadable or zero (ring not mapped?)") ctx.log("cnc/free: %s bytes", free) + + # The ring the platform ships: the DT pool is the promise, ring_mb is what + # the module took. They must agree - a pool the parameter does not use is + # no-map RAM burned for nothing, and a parameter the pool cannot back would + # have failed the probe. free can only ever be size less the 32 KiB gap. + pool = _dt_u32("/proc/device-tree/reserved-memory/cnc-pulsebuf/size") + try: + ring = int((_read("/sys/module/glowforge/parameters/ring_mb", "") or "").strip()) << 20 + except ValueError: + ring = None + ev["cnc_pool_bytes"] = pool + ev["cnc_ring_bytes"] = ring + ctx.log("cnc-pulsebuf pool: %s bytes, ring_mb: %s bytes", pool, ring) + ctx.check(pool is not None, "no cnc-pulsebuf reserved-memory node (the ring fell back to CMA?)") + ctx.check(ring is not None, "/sys/module/glowforge/parameters/ring_mb unreadable") + ctx.check(pool is None or ring is None or pool == ring, + "DT pool %s and ring_mb %s disagree", pool, ring) + ctx.check(ring is None or free is None or free <= ring - 32 * 1024, + "cnc/free %s exceeds the ring less its 32 KiB gap (%s)", free, ring) ctx.check(hw.sysfs_read("cnc/interlock_circuit") is not None, "cnc/interlock_circuit unreadable") # 4. forgectrl holds /dev/glowforge and supervises the controller diff --git a/kas/README.md b/kas/README.md index 81ad79b..d235ea7 100644 --- a/kas/README.md +++ b/kas/README.md @@ -255,9 +255,9 @@ Scarthgap, but the legacy (Dunfell/Gatesgarth) layers won't build clean until: rides out worst-case scheduling latency with orders of magnitude to spare (measured: 0.2 ms worst write latency under full CPU + I/O load; the underrun bench ran 100 kHz for 120 s with zero underruns). The ring - itself is 16 MiB (the `ring_mb` module parameter, backed by the 16 MiB - reserved pool): ~84 s of stream at 200 kHz, ~28 min at the 10 kHz - cloud-mode tick — a capacity that matters for the whole-job preload of + itself is 32 MiB (the `ring_mb` module parameter, backed by the 32 MiB + reserved pool, matching the factory ring): ~168 s of stream at 200 kHz, + ~56 min at the 10 kHz cloud-mode tick — a capacity that matters for the whole-job preload of cloud mode, not for latency. Bounded queue depth + `SCHED_FIFO` for the feeder is the design; revisit RT only if the underrun bench ever contradicts this arithmetic.