diff options
| author | Gabriel Schneider <[email protected]> | 2026-08-25 12:40:53 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-25 12:46:51 -0300 |
| commit | f5f8068fac59b4f16046c2022c2fc7c7e447ef4c (patch) | |
| tree | 2731a3ed4e51cae09e184e25778eded5fc37d1f5 /captures | |
| download | esp32p4-f5f8068fac59b4f16046c2022c2fc7c7e447ef4c.tar.gz esp32p4-f5f8068fac59b4f16046c2022c2fc7c7e447ef4c.zip | |
zig-p4: pure-Zig ESP32-P4 toolchain
build.zig generates the linker script and drives Zig's own LLD; tools/image.zig
turns the ELF into a flashable image and tools/{rom,serial}.zig speak the mask
ROM loader over the UART. No CMake, ninja, idf.py, esptool, or external linker.
src/soc.zig is a comptime register model over ESP-IDF's own *_reg.h headers;
src/hal/ adds peripheral sequences; src/io/ implements std.Io for the chip;
src/oracle/ diffs this HAL against ESP-IDF's on the die.
Diffstat (limited to 'captures')
| -rw-r--r-- | captures/RF-REPRODUCTION.md | 98 | ||||
| -rw-r--r-- | captures/idf-recheck-chop.json | 67 | ||||
| -rw-r--r-- | captures/zig-tone-chop-chop.json | 67 |
3 files changed, 232 insertions, 0 deletions
diff --git a/captures/RF-REPRODUCTION.md b/captures/RF-REPRODUCTION.md new file mode 100644 index 0000000..dd58204 --- /dev/null +++ b/captures/RF-REPRODUCTION.md @@ -0,0 +1,98 @@ +# Reproducing the report's RF experiment from this toolchain + +`04-report` section 5 is the only real SDR measurement in that document: a 25 MHz carrier on GPIO20, +OOK-keyed with `0x4200`, detected at **+12.96 dB ± 0.16 dB** and decoded back to `0x4200` from raw IQ. +The emitter was ESP-IDF firmware (`02-esp32p4-m3-radio`). This is the same experiment driven by this +project's own HAL, with the report's analysis tools used unmodified so the comparison is like for +like. + +`examples/rf.zig` is the firmware. `zig build -Dapp=examples/rf.zig flash`, then the report's own +`tools/rfprobe.py` and `tools/decode_tone.py`. + +## What reproduced + +**The emitter, exactly.** LEDC on GPIO20, 1-bit duty resolution, duty 1, hpoint 0 — the +configuration from `02-esp32p4-m3-radio/main/main.c:77-94`. The report derives the carrier from the +divider: 1-bit resolution off the 80 MHz clock can only synthesise `80e6 × 256 / (410 × 2)` = +24,975,609 Hz, never a round 25 MHz. This HAL computes divider **410** and reports +**24,975,610 Hz** — the same number, from IDF's own Q10.8 arithmetic reproduced in Zig. + +**The source clock, measured rather than assumed.** 80,104,000 Hz, timed as 100 LEDC output periods +against the systimer's fixed 16 MHz. Worth checking: this image runs at whatever the bootloader left +(90.0 MHz CPU), not the 360 MHz the ESP-IDF firmware configures, so its APB being 80 MHz was an +assumption until measured. + +**Register-level equivalence to ESP-IDF, on the die.** The differential harness gained a case that +performs the whole 25 MHz / 1-bit bring-up both ways — IDF's `ledc_ll_*` on one side, this HAL on the +other, one image, one boot — and compares the LEDC block: + +``` +MARK DIFF ok ledc.full_rf_config_25MHz_1bit(25) 96 words identical +``` + +**Behavioural equivalence at every rate the instrument can measure.** Same image, same pad, same edge +counter: + +| resolution | frequency | ESP-IDF LL | this HAL | +|---|---|---|---| +| 2-bit | 5 MHz | 112,498 edges | 112,500 | +| 1-bit | 10 MHz | 75,000 | 75,000 | +| 1-bit | 25 MHz | 0 | 0 | + +## What did not reproduce, and why it is not this toolchain's fault + +**The +12.96 dB detection is gone — from the original firmware too.** ESP-IDF's own binary was +reflashed from `02-esp32p4-m3-radio/build/` and driven by the report's own `rfprobe.py chop` with the +report's parameters (25 MHz, gain 19.7 dB, 4 cycles, ±2 kHz): + +| firmware | ON segments (dB) | OFF segments (dB) | ON − OFF | +|---|---|---|---| +| report, section 5.2 | 19.206, 19.169, 19.206, 19.146 | 6.360, 6.252, 6.153, 6.104 | **+12.96 dB** | +| ESP-IDF, re-run today | 6.396, 6.670, 6.401, 6.403 | 6.457, 6.659, 6.523, 6.482 | **−0.063 dB** | +| this toolchain, today | 5.046, 5.298, 5.146, 4.999 | 5.120, 5.128, 5.014, 5.027 | **+0.05 dB** | + +The receive chain is not at fault: the report's own instrument validation was repeated first and an +FM broadcast carrier sits **31.8 dB** above the span median at 97.500625 MHz, against the report's +30.9 dB at 97.497 MHz. The dongle, the antenna and the analysis path all work. + +So the report's detection was conditional on a physical coupling that no longer exists — the report +itself says the emission reached the dongle through whatever wire happened to be on JP1 pin 17. Two +firmwares now give the same null through the same tool. That is a correction to the report, not a +difference between toolchains: **a measurement whose apparatus is "incidental coupling" is not +reproducible, and this one is not.** + +## What is out of scope by construction + +Wi-Fi, BLE and 802.15.4 — sections 4 and 7 of the report. The P4 has no radio; those went out over an +ESP32-C6 across SDIO under `esp_hosted` 2.12.12 + `esp_wifi_remote` 1.6.3, a 19,000-line host stack +plus a prebuilt coprocessor binary. None of that is low-level hardware and none of it exists here. +Reproducing it would mean porting `esp_hosted`, not porting a HAL. + +## The one thing this HAL cannot yet measure, and the fix + +Neither implementation shows edges at 25 MHz, and that is the instrument rather than the pad. The +counter reads `GPIO_IN` in a loop, and both the loop's cadence and the LEDC output descend from the +same clock: 75,000 edges in 300,000 reads at 10 MHz is exactly 300,000/4, i.e. perfectly +phase-locked. At 25 MHz the lock lands on one phase and the count is zero. Adding jitter to the loop +did not break it. + +This is exactly why the report used the ADC (§5.1) and read its **min and max** rather than its mean: +"the ADC cannot track 25 MHz, so its sampling phase is uncorrelated with the pad". Running that same +witness against the ESP-IDF firmware today still shows both rails while the carrier runs +(`min=0 max=3228` against `driven_high min=3362`), so the pad does toggle — under IDF, measured with +IDF's instrument. + +To close it from this side, the HAL needs an instrument whose sampling is uncorrelated with the +signal: a minimal ADC one-shot read, or PCNT. ADC is the harder of the two — a prior review found it +among the peripherals whose LL needs `regi2c_ctrl.c` and the analog I²C master — so PCNT is the +cheaper route to a trustworthy pulse count. Until one of them exists, "the pad toggles at 25 MHz" is +a claim this toolchain can make about its registers and not about its output. + +## Incidental finding + +ESP-IDF's LEDC **LL** silently programs an out-of-range divider. A 1-bit 25 MHz target from the +40 MHz XTAL needs divider 205, below the hardware minimum of 256 (`LEDC_IS_DIV_INVALID`, +`ledc.c:114`). `ledc_ll_set_clock_divider` writes it anyway, because the range check lives one layer +up in `ledc.c` rather than in the LL. This HAL returns `DividerOutOfRange`. The first version of the +differential case above hit exactly this and reported four differing registers; the divergence is +real, and this HAL is on the right side of it. diff --git a/captures/idf-recheck-chop.json b/captures/idf-recheck-chop.json new file mode 100644 index 0000000..bbf3eb6 --- /dev/null +++ b/captures/idf-recheck-chop.json @@ -0,0 +1,67 @@ +{ + "tag": "idf-recheck", + "mode": "chop", + "center_hz": 25000000.0, + "rate_hz": 2048000.0, + "gain": 19.7, + "cmd_on": "T", + "cmd_off": "o", + "chop_secs": 4.0, + "cycles": 4, + "guard_s": 0.8, + "integration_halfband_hz": 2000.0, + "integration_offset_hz": 0.0, + "on_segment_db": [ + 6.396, + 6.67, + 6.401, + 6.403 + ], + "off_segment_db": [ + 6.457, + 6.659, + 6.523, + 6.482 + ], + "on_minus_off_db": -0.063, + "pooled_sd_db": 0.141, + "verdict": "no detection above 3 sd", + "marks": [ + { + "t": 242717.41154511, + "line": "STATUS p4_mac=80:f1:b2:d3:9a:7f wifi_started=0 ap_up=0 tone_gpio=20 tone_hz=25000000 t=7614532us" + }, + { + "t": 242721.321932845, + "line": "MARK TONE_CONTINUOUS_ON 25MHz square wave on gpio20 t=11527889us" + }, + { + "t": 242725.318133708, + "line": "MARK TONE_OFF - t=15527901us" + }, + { + "t": 242729.327587696, + "line": "MARK TONE_CONTINUOUS_ON 25MHz square wave on gpio20 t=19528418us" + }, + { + "t": 242733.319070332, + "line": "MARK TONE_OFF - t=23528684us" + }, + { + "t": 242737.328479421, + "line": "MARK TONE_CONTINUOUS_ON 25MHz square wave on gpio20 t=27529126us" + }, + { + "t": 242741.320336123, + "line": "MARK TONE_OFF - t=31530804us" + }, + { + "t": 242745.329732972, + "line": "MARK TONE_CONTINUOUS_ON 25MHz square wave on gpio20 t=35531248us" + }, + { + "t": 242749.321956982, + "line": "MARK TONE_OFF - t=39531493us" + } + ] +}
\ No newline at end of file diff --git a/captures/zig-tone-chop-chop.json b/captures/zig-tone-chop-chop.json new file mode 100644 index 0000000..8049e78 --- /dev/null +++ b/captures/zig-tone-chop-chop.json @@ -0,0 +1,67 @@ +{ + "tag": "zig-tone-chop", + "mode": "chop", + "center_hz": 25000000.0, + "rate_hz": 2048000.0, + "gain": 19.7, + "cmd_on": "T", + "cmd_off": "o", + "chop_secs": 4.0, + "cycles": 4, + "guard_s": 0.8, + "integration_halfband_hz": 2000.0, + "integration_offset_hz": 0.0, + "on_segment_db": [ + 5.046, + 5.298, + 5.146, + 4.999 + ], + "off_segment_db": [ + 5.12, + 5.128, + 5.014, + 5.027 + ], + "on_minus_off_db": 0.05, + "pooled_sd_db": 0.126, + "verdict": "no detection above 3 sd", + "marks": [ + { + "t": 242100.648010967, + "line": "MARK STATUS carrier=0 divider=410(Q10.8) freq=24975610Hz src=80000000Hz pin=20 cpu_mhz_x1000=89999 t=98510620us" + }, + { + "t": 242104.536940576, + "line": "MARK TONE_CONTINUOUS_ON gpio20 24975609Hz square t=102406216us" + }, + { + "t": 242108.539328207, + "line": "MARK TONE_OFF gpio20 released to blink t=106406606us" + }, + { + "t": 242112.538504666, + "line": "MARK TONE_CONTINUOUS_ON gpio20 24975609Hz square t=110406998us" + }, + { + "t": 242116.539028346, + "line": "MARK TONE_OFF gpio20 released to blink t=114407349us" + }, + { + "t": 242120.537367672, + "line": "MARK TONE_CONTINUOUS_ON gpio20 24975609Hz square t=118407744us" + }, + { + "t": 242124.538288634, + "line": "MARK TONE_OFF gpio20 released to blink t=122408156us" + }, + { + "t": 242128.539139364, + "line": "MARK TONE_CONTINUOUS_ON gpio20 24975609Hz square t=126408570us" + }, + { + "t": 242132.539313173, + "line": "MARK TONE_OFF gpio20 released to blink t=130408947us" + } + ] +}
\ No newline at end of file |
