summaryrefslogtreecommitdiff
path: root/captures/RF-REPRODUCTION.md
blob: dd5820410f4dcec0d809158b8060ab4aafbee54c (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
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.