From 0611528a6f2a0fabaae545ca36e741acb874ec0f Mon Sep 17 00:00:00 2001 From: Dmitry Ilyin <6576495+widgetii@users.noreply.github.com> Date: Sun, 3 May 2026 11:44:36 +0300 Subject: [PATCH] Emit AE/AGC callback skeleton from runtime phase + sc2315e monitor docs Two changes that share a theme: making the AE/AGC layer of an extracted sensor driver navigable. The first (write-side) finishes a deferred item from #145; the second (read-side) documents an existing-but-undocumented ipctool feature. Write-side: trace_to_driver.py now emits a third function `_ae_step` that writes each runtime hot register (top 8, above 25% of the max count) with its last-seen trace value, in trace order, each tagged `/* TODO: derive */`. Header documents that the values are placeholders and points at the vendor's cmos_inttime_update / cmos_gains_update equivalents for the math. Cross-checked against widgetii/smart_sc2315e on the SC2315E + Majestic capture from #145: skeleton emits exactly 0x3314, 0x5781, 0x5785 with values 0x14, 0x60, 0x30 - matching the else-branches of the reference's two AE callbacks. The trace was captured under steady ambient light, so only the low-gain / short-inttime branch values appear. Read-side: `ipctool sensor monitor` already supported SC2315E (added some time ago); the feature was never documented anywhere. It reads the same hot register set in a loop while the sensor runs, giving you value time-series under varying lighting - the natural complement to the static trace-extraction. New "Stage 4 - Live-reading the AE state" section in docs/sensor-driver-extraction.md walks through pairing the two: extract `_ae_step` for the register set, then `monitor` while varying lighting to capture the value distribution that derives the threshold conditionals. Renamed sc2315e's R3812 to HOLD (it is the group-hold trigger from cmos_gains_update) so the live-read output is self-explanatory. test_pipeline.sh asserts the new function is emitted and the file still passes gcc -Wall -Wextra. Docs updated to describe the third function and the inherent limitation (math not in trace). Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/sensor-driver-extraction.md | 98 +++++++++++++++++++++++++++++--- src/snstool.c | 5 +- tools/test_pipeline.sh | 2 + tools/trace_to_driver.py | 72 +++++++++++++++++++---- 4 files changed, 157 insertions(+), 20 deletions(-) diff --git a/docs/sensor-driver-extraction.md b/docs/sensor-driver-extraction.md index 20bbd3f..1a7c7a0 100644 --- a/docs/sensor-driver-extraction.md +++ b/docs/sensor-driver-extraction.md @@ -307,10 +307,29 @@ python3 tools/trace_segment.py tools/dumps/cap.log ### `trace_to_driver.py` -Emits HiSilicon-SDK-shaped C from the segmented JSON. Two functions per -sensor: `_linear_init` and `_post_init_exposure_prime`, -plus a comment block summarising the most-frequently-written runtime -registers (the AE/AGC hot list). +Emits HiSilicon-SDK-shaped C from the segmented JSON. Three functions +per sensor: + +- `_linear_init` — the cold-init register table. +- `_post_init_exposure_prime` — the AE/exposure values applied + once when the AE loop first kicks in. +- `_ae_step` — a per-frame AE callback skeleton listing the + registers the AE loop hits in steady state, with their last-seen + values as placeholders. The math driving those values (gain LUTs, + exposure scaling, threshold branches) is not in the trace; the + skeleton points the reverse-engineer at the vendor's + `cmos_inttime_update` / `cmos_gains_update` equivalents to fill in. + +For SC2315E captured under steady ambient light, the `_ae_step` body +matches the **else-branches** of the reference's `cmos_inttime_update` +(`0x3314=0x14`) and `cmos_gains_update` (`0x5781=0x60`, `0x5785=0x30`) +— exactly the registers and values the running AE loop wrote each +frame. A capture under varying lighting would surface the if-branches +too; the skeleton would then need a conditional that you'd derive by +hand from the value distribution. + +For value-distribution data without a fresh trace, use `ipctool sensor +monitor` (see "Live-reading the AE state" below). The output is **standalone-buildable** — it includes a small "SDK stubs" block (typedef for `VI_PIPE`, a no-op `sensor_write_register`) so that @@ -337,9 +356,11 @@ The output is a scaffold, not a finished driver: vendor's `sensor_init` callback. - The `post_init_exposure_prime` function hints at what the SDK's `cmos_inittime_update` sets up. -- The runtime hot-register comment block tells you which registers - belong inside the AE/AGC callback. The values driving them are - **not** in the trace — only the regs touched and how often. +- The `_ae_step` function is the runtime AE/AGC callback skeleton: + the registers the AE loop touches every frame, with their last-seen + trace values as `/* TODO: derive */` placeholders. The math driving + those values is **not** in the trace — see "Live-reading the AE + state" below for how to capture value distributions. ### `trace_diff.py` @@ -363,6 +384,69 @@ python3 tools/trace_diff.py \ --ref-scope sc2315e_linear_1080P30_init ``` +## Stage 4 — Live-reading the AE state with `ipctool sensor monitor` + +`ipctool sensor` is a built-in subcommand (separate from `trace`) that +reads a fixed list of AE/exposure registers from the running sensor +over I2C/SPI in a loop, decoded as labelled fields. Same idea as +`_ae_step` but read-side: instead of inferring AE registers from a +captured trace, you can poll the actual sensor while it's running. + +```console +$ ipctool sensor monitor +EXP 100 AGAIN 310 DGAIN 80 VMAX 546 R3301 f R3314 14 R3632 8 HOLD 0 R5781 60 R5785 30 +EXP 100 AGAIN 310 DGAIN 80 VMAX 546 R3301 f R3314 14 R3632 8 HOLD 0 R5781 60 R5785 30 +EXP ff AGAIN 330 DGAIN 80 VMAX 546 R3301 f R3314 14 R3632 8 HOLD 0 R5781 60 R5785 30 + ^^^^^^^^^^^^^^^^^^^^^^^^^ + same hot regs trace_to_driver picked up +``` + +The reg list per supported sensor lives in `src/snstool.c` (a small +table, ~10 entries). For SC2315E that table mirrors what `_ae_step` +emits — both are the registers the running AE loop writes per frame. + +### Pairing `sensor monitor` with `_ae_step` + +The trace-and-extract workflow gives you the **register set** of the +AE loop. `sensor monitor` gives you **value time-series** under +whatever lighting conditions you can produce. Pair them: + +1. Extract `_ae_step` via `trace`. Note the placeholder values + (e.g., `0x5781=0x60`, `0x3314=0x14` on SC2315E). +2. Cover/uncover the lens, point at varying scenes, switch the IR cut + etc. while running `ipctool sensor monitor` and recording the + output (`tee monitor.log`). +3. Plot or grep the resulting log for value transitions on the + `_ae_step` registers. +4. Derive the conditional / LUT that maps trace inputs (often gain + index, integration time) to those values. This is the manual step + that no register-trace tool can automate. + +For SC2315E, the reference's `cmos_gains_update` writes +`0x5781=1, 0x5785=2` once gain ≥ a fixed threshold; the rest of the +time it writes the `0x60, 0x30` we captured. A varying-light +`monitor` session that pushes the AE into high-gain regime will show +that transition directly. + +### Adding a new sensor to `monitor` + +Edit `src/snstool.c`: + +1. Add a `Reg[]` table for the sensor: name, address, byte length. + Cover the AE-modified registers (exposure, gain, vmax) plus any + tuning registers that change per-frame in `cmos_gains_update`. +2. Add an entry to `sns_regs[]`: sensor ID (uppercase, matching + `ctx->sensor_id` from `src/sensors.c`), pointer to the reg + table, and `.be = 1` if the sensor is big-endian on multi-byte + registers. +3. The dispatch in `monitor()` is automatic — no other glue needed. + +When `_ae_step` emits a register set you didn't have in `monitor`, +that's a good cue to extend the `monitor` table to match. A nice +property of the workflow: the trace tells you which registers the +running streamer cares about, the monitor lets you watch them change +in real time. + ## Reading the diff For SC2315E captured from Majestic without scoping, the unscoped diff diff --git a/src/snstool.c b/src/snstool.c index 2671a6c..fa5f3a7 100644 --- a/src/snstool.c +++ b/src/snstool.c @@ -18,6 +18,9 @@ typedef struct { uint8_t len; } Reg; +/* Mirrors the runtime hot set ipctool trace's _ae_step skeleton picks up + * for SC2315E. Values readable while the streamer is paused; HOLD toggles + * 0x00 -> 0x30 around grouped writes by cmos_gains_update. */ const Reg sc2315e_regs[] = { {"EXP", 0x3e00, 3}, {"AGAIN", 0x3e08, 2}, @@ -26,7 +29,7 @@ const Reg sc2315e_regs[] = { {"R3301", 0x3301, 1}, {"R3314", 0x3314, 1}, {"R3632", 0x3632, 1}, - {"R3812", 0x3812, 1}, + {"HOLD", 0x3812, 1}, {"R5781", 0x5781, 1}, {"R5785", 0x5785, 1}, {NULL}, diff --git a/tools/test_pipeline.sh b/tools/test_pipeline.sh index 0951504..0d92fe4 100755 --- a/tools/test_pipeline.sh +++ b/tools/test_pipeline.sh @@ -52,6 +52,8 @@ python3 tools/trace_to_driver.py "$tmp/segments.json" \ test -s "$tmp/driver.c" || { echo "driver.c empty"; exit 1; } grep -q '^void testsensor_linear_init' "$tmp/driver.c" \ || { echo "linear_init function not emitted"; exit 1; } +grep -q '^void testsensor_ae_step' "$tmp/driver.c" \ + || { echo "ae_step skeleton not emitted"; exit 1; } echo "== gcc -fsyntax-only ==" gcc -Wall -Wextra -fsyntax-only "$tmp/driver.c" diff --git a/tools/trace_to_driver.py b/tools/trace_to_driver.py index 7408353..aef70e4 100644 --- a/tools/trace_to_driver.py +++ b/tools/trace_to_driver.py @@ -93,17 +93,70 @@ def emit_phase(events, indent=" "): def runtime_summary(events): - """Per-register write count and last value seen.""" + """Per-register write count, last value, and first-seen index.""" from collections import Counter counts = Counter() last_val = {} - for ev in events: + first_seen = {} # trace-order, used to emit hot regs in original sequence + for i, ev in enumerate(events): if ev["kind"] == "write": counts[ev["reg"]] += 1 last_val[ev["reg"]] = ev["val"] + first_seen.setdefault(ev["reg"], i) items = sorted(counts.items(), key=lambda kv: -kv[1]) - return [(reg, n, last_val[reg]) for reg, n in items] + return [(reg, n, last_val[reg], first_seen[reg]) for reg, n in items] + + +def runtime_ae_skeleton(events, sensor, top_k=8, hot_ratio=0.25): + """Emit a callback skeleton from the runtime phase. + + Selects the most-frequently-written runtime registers (top_k by count, + each above hot_ratio * max_count) and emits a void function that + writes the last-seen value to each. Order matches the trace. + + The values are placeholders; the math driving them - exposure scaling, + gain LUTs, threshold-branched tuning - is not in the register trace. + Reverse-engineer via the vendor's cmos_inttime_update / + cmos_gains_update equivalents. + """ + summary = runtime_summary(events) + if not summary: + return "" + + max_count = summary[0][1] + threshold = max(1, int(max_count * hot_ratio)) + hot = [(r, n, v, idx) for r, n, v, idx in summary[:top_k] if n >= threshold] + if not hot: + return "" + + # Emit in trace-order so the writes look plausible to the AE loop. + ordered = sorted(hot, key=lambda t: t[3]) + + lines = [] + lines.append("/*") + lines.append(f" * AE/AGC steady-state callback skeleton ({sensor}).") + lines.append(" *") + lines.append(" * Per-frame writes captured during steady state. Values shown are the") + lines.append(" * last-seen values from the trace; the AE math driving them (gain LUTs,") + lines.append(" * exposure scaling, threshold branches) is NOT derivable from a register") + lines.append(" * trace alone. Cross-reference the vendor's cmos_inttime_update /") + lines.append(" * cmos_gains_update equivalents to fill in the math.") + lines.append(" *") + lines.append(" * Hot register frequency:") + lines.append(" * reg count last value") + for reg, count, val, _ in hot: + lines.append(f" * 0x{reg:04X} {count:5d} 0x{val:02X}") + lines.append(" */") + lines.append(f"void {sensor}_ae_step(VI_PIPE ViPipe)") + lines.append("{") + lines.append(" (void)ViPipe;") + for reg, _, val, _ in ordered: + lines.append( + f" sensor_write_register(0x{reg:04X}, 0x{val:02X}); /* TODO: derive */" + ) + lines.append("}") + return "\n".join(lines) + "\n" def main(): @@ -142,15 +195,10 @@ def main(): runtime = phases.get("runtime", []) if runtime: - parts.append("\n/*\n") - parts.append(" * RUNTIME AE/AGC HOT REGISTERS\n") - parts.append(" * (per-frame writes during steady state - the math\n") - parts.append(" * that drives these values is NOT in the trace)\n") - parts.append(" *\n") - parts.append(" * reg count last value\n") - for reg, n, v in runtime_summary(runtime)[:32]: - parts.append(f" * 0x{reg:04X} {n:5d} 0x{v:02X}\n") - parts.append(" */\n") + skeleton = runtime_ae_skeleton(runtime, args.sensor) + if skeleton: + parts.append("\n") + parts.append(skeleton) src = "".join(parts) out_path = args.out or args.segments + ".c"