mirror of
https://github.com/openglow-org/forgefirm.git
synced 2026-09-27 08:41:13 -07:00
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.
This commit is contained in:
+22
-8
@@ -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 `<recipe>-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
|
||||
|
||||
+4
-4
@@ -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 |
|
||||
|
||||
@@ -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
|
||||
|
||||
+3
-3
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user