Claude Fable 5.1 got the same brief as every P25 audit on bench: find why this firmware struggles to decode P25 Phase 1 and Phase 2 voice on the radio’s HR-C6000, and what to do about it.
It worked in a clean room, with a fresh copy of the repository, no earlier reviews and no network — but through the Claude Code CLI rather than the harness the ranked audits shared, which is why this page is not ranked with them. I checked its claims against the code and the manual, and reran the measurements it reports.
Overall grade
C+
79 / 100 weighted
FDCBA
Counts the calls, misprices them.
On the question that matters it is exhaustive. It does not just show that the monitor enables the microphone bit; it shows that
soundReceiveRefillData is the only stock consumer of that buffer, that the DMR transmit path is what arms it, and that DMR receive audio
never touches I2S at all. Then it reads the map at object granularity and names the three buffers a P25 build could reclaim —
uiNotification.o at 38,400 bytes, and two smaller ones — all three exact.
It also measured the vocoder, wrapping libm to count 11,812 cosf calls per frame, which is close to what the grading emulator
counts. Then it priced them at 100–200 cycles each and concluded the synthesis needs “60–120% of a 72 MHz core”.
With newlib’s range reduction the true cost is nearer a thousand cycles a call: 655% on the same build another audit profiled, and 1,100–1,600%
on the live one. The finding is right and the margin is out by about six times — which is what makes its “168 MHz and a cheaper oscillator” plan look sufficient
when it is not.
Claims that check out
15 / 20
4 overstated or unverified · 1 wrong
Decode-critical issues found
2 of 4
plus a vocoder it measured and under-costed
Trig calls per frame
11,812
counted correctly, priced at a sixth
Ranking
Unranked
different harness, no step cap
The run, not graded: 12 min · 71 agent steps · 44 tool calls · 249K output tokens (168K reasoning) · 8.7M tokens re-read · Claude Code CLI, subscription, one-shot
Headline findings
Six things to know about this review
Validated
The most complete case on the board for the microphone
Other audits show that the monitor sets the microphone bit. This one shows what that buffer is for: soundReceiveRefillData is the only stock consumer of i2s_Rx_Buffer, it is armed by soundReceiveData() from the DMR transmit path, and its comment reads “only use the Left Channel of the Mic Audio”. DMR receive audio never touches I2S receive at all — the C6000 hands AMBE frames over SPI and PCM goes out over I2S TX.
Then the registers: HRC6000SetMic() toggles bit 0x40 of 0xE0, and HRC6000SetFMRx() writes 0xE0 = 0x89, “Turn off Microphone input”. The monitor writes 0xC9.
Every audit that reached the memory ceiling reported the same totals. This one goes further and names what to reclaim: uiNotification.o holds 38,400 bytes of CCM, SPI_Flash.o 4,096 and voicePrompts.o 3,788. I checked all three against the map — 0x9600, 0x1000, 0xecc, exact.
It then sizes what a new design would need against that: an ADC DMA double buffer at 24 kHz is under a kilobyte, so the overlay pays for itself several times over.
MDUV380_FW.map · p25_4fsk.c
Wrong
It counts the trig calls, then prices them at a sixth
It wrapped libm and counted: 11,812cosf calls per 20 ms frame, 14,865 at worst, with uvquality already at its cheapest. That matches the grading emulator’s 13,072 on a different frame set, so the measurement is sound.
Then it assumes 100–200 cycles per call and concludes the synthesis needs “roughly 60–120% of a 72 MHz core”. Newlib’s cosf does full range reduction — the emulator counts a matching ~13,000 __ieee754_rem_pio2f calls per frame — and mbelib totals 9.4M instructions per frame on the demo build, 655% of the core, or 11–16× on the live build. Its own conclusion survives; its margin does not, and the plan it builds on that margin would not close the gap.
A real ±1.1 kHz carrier offset at 450 MHz is comparable to the 1.2 kHz level spacing, so fitting gain and DC over an unbalanced 64-symbol window can converge on the wrong labelling — calibrate from the 24 known sync symbols instead. Sync detection compares hard dibits with ≤2 mismatches, so a +3↔+1 slip counts as a full error where soft correlation would be more sensitive and calibrate the levels for free.
And after a lost frame the parser stays in RX_LDU for the full 784 payload dibits, so the next frame’s sync is swallowed too: one corrupted frame costs two.
p25_4fsk.c:186–210 · p25rx.c:409–433
Overstated
It rules out the chip mode the other run planned around
It says the one-layer mode is still DMR-framed — 264 or 288 bit data after DMR sync and slot timing, with the interrupt “typically tested in bit error rate” — so there is no continuous dibit stream and no route without a hardware modification.
Those lines are real. But the same section describes one-layer mode as solving “the channel filtering of the baseband or low-IF signal and the modulation and demodulation process”, leaving “the channel codec and the processing of all communication protocol” to the user. Whether the chip emits frames without DMR sync is untested, and the Opus 5 run built a falsifiable acceptance test for exactly that question.
manual 1346–1360, 2384–2386 · manual 2553–2632
Overstated
A real gap, attributed to the wrong cause
It found something no other audit did: in the project’s own suite, the direct 38.4 kHz path recovers 7 of 9 clean frames at 8 dB while the 24 kHz-plus-resampler path recovers 2 of 9. I reran it — exact.
But the two tests differ in more than the resampler: test 4 generates at 24 kHz, so samples per symbol and noise bandwidth change too. Isolating the resampler alone — band-limit, resample, decode — gives equal or fewer symbol errors at every SNR from 16 dB down to 8. The gap deserves investigating; blaming linear interpolation for it is not established.
tests/p25/run.py test 3, test 4 · p25_resample.c
Scorecard
How the 79 breaks down
The same rubric, weights and answer key as every P25 audit on bench — but a different harness, so the scores are shown for reading rather than for ranking.
Dimension scores
Hover or focus a bar for the reasoning. Weighted total 79, grade C+.
Rubric table
Dimension
Weight
Score
Points
Why
Accuracy & evidence
30%
85
25.5
15 of 20 claims hold, six of them validated by rerunning what it ran. Its register, map and test citations are exact to the byte. One is wrong — the cycle cost of the trig it counted — and three are overstated, including a denial of the chip mode the other run planned around.
Coverage of decode problems
25%
62
15.5
Both hardware causes, the two SPI writes per voice frame, the memory ceiling with named objects, and three demodulator defects nobody else reached. It never opens the capture export, the starvation counter, the squelch path, the health gate or the identity gating.
Root cause & prioritisation
15%
84
12.6
It separates “architectural, not bugs” cleanly and draws the right conclusion for the bring-up work: the clock machinery solved a real problem on the wrong signal. Its CPU mis-pricing is what keeps this from being the strongest causal account.
Fix plan & acceptance gates
15%
86
12.9
Six steps from a fifteen-minute source test to a validation ladder that starts by injecting known baseband from a sound card — the most practical first bench step any audit proposed. The register and profile changes are specific and correct.
Originality & attribution
10%
84
8.4
The provenance chain for the I2S buffer, object-level memory reclamation, the libm call-counting harness, the in-frame resync defect and the exact control-channel opcode that carries the Phase 2 descrambler seed are all its own.
Clarity & calibration
5%
88
4.4
A verified-checks table before any argument, evidence blocks under each claim, and a Phase 2 table that states component, status and source file. It is the easiest of the sixteen to audit, which is how its one wrong number was found.
Weighted total
100%
79
Grade scale: A ≥ 90 · B 80–89 · C+ 75–79 · C 70–74 · C− 65–69 · D 50–64 · F < 50
Why this page is not ranked
Same rubric, different harness
Everything below is graded exactly as the ranked audits were. What differs is how the model was run.
The thirteen ranked audits all ran through one harness: a Hermes profile with no skills, memory or delegation, its tools inside a container with no network and only the repository mounted, and a 150-step ceiling. This run used the Claude Code CLI on a subscription, stripped as far as that client allows — no skills, no plugins, no MCP servers, no memory files, no web tools, and no subagent tool, so one model did all the work.
The differences that remain are the same three as on the other Claude Code page: the system prompt and tool implementations are Claude Code’s; the tools ran on the host rather than in a no-network container; and there was no step cap, though at 71 steps this run never approached one.
So the grade is real and the evidence is checkable, and the number is not comparable with the thirteen. It sits here the way the UNIONALPHA audit does: published, in the comparison tables, and outside the ranking.
Receive chain
Where its findings sit
Fable covers the source, the conditioning, the vocoder and the demodulator’s own loops, and reaches the per-frame SPI writes that only two other audits found. The red tags are the diagnostics and the audio-policy defects it never examines.
F1 raised by Claude Fable 5.1N1 never examinedDashed edge: no documented connection
F1The I2S buffer exists to capture the microphone, traced to its only consumer
F2AT1846S FM profile versus the DMR profile a P25 receiver needs
F3C6000 de-emphasis, 2.55/3 kHz low-pass and 300–3400 Hz bandpass
F4The vocoder blocks real time — 11,812 trig calls per frame
F5DC and gain fitting, hard-dibit sync, and the swallowed re-sync
F6Two SPI writes per voice frame on a bus that fails silently
N1The capture exports the wrong halfwords
N2The parser certifies incomplete captures
N3pcm_starve never counts
N4The manual’s 8 kHz frame-clock rule
The front end
The profile it names, and the fix it prescribes
It is one of the few audits to state the fix as a profile swap the firmware already performs for DMR, and the only one to insist that the tap must be DC-coupled because the sync runs 24 symbols at constant deviation.
Register state for a P25 session
What trxSetModeAndBandwidth(RADIO_MODE_ANALOG, true) leaves in place, next to OpenGD77’s own settings for 4800-baud 4FSK.
Register
Monitor today (analog FM)
OpenGD77 DMR mode
Effect on C4FM
AT1846S 0x40
0x0030, low-frequency bit cleared: “so it should be cleared to receive FM”
0x0031, “allows low freq audio”
Removes the near-DC content that runs of equal symbols depend on
AT1846S 0x58
0xBC85, “enable some filters for FM e.g. high and low pass filters”
0x9CDD, “disable all filters in DMR mode”
High- and low-pass shaping of the symbol stream
AT1846S 0x44
0x06CC, AF gain 80%
0x07FF, 100%; the DM-1701 “needs higher AF gain” to decode 4FSK
Lower discriminator level into the next stage
AT1846S bandwidth
25 kHz
12.5 kHz, forced for digital
Wider noise bandwidth and deviation scaling set for ±5 kHz FM
HR-C6000 0x34
0x3C, “compressor off, de-emph on, 3 kHz audio filter”
not written; the FM value persists
−6 dB/octave from 300 Hz and a 3 kHz low-pass on the FM receive path (manual §6.2)
Sources: AT1846S.c:191–217, HR-C6000.c:2846–2854 and 2858–2875, trx.c:228–279, p25mon.c:724. Register comments are OpenGD77’s; the manual translation documents 0x34.
What the inherited 0x34=0x3C does to C4FM
Standard C4FM through each HR-C6000 receive stage alone, then the firmware’s live demodulator and framer. 20 LDUs: 180 IMBE frames possible.
De-emphasis alone leaves no frames decodable in the grading model, which is why “keep the AT1846S in its DMR receive profile while monitoring” is the right instruction.Data table
Audio stage
Noise-free frames · NIDs
Symbol errors
20 dB frames · NIDs
no audio filtering
180/180 · 20/20
0.0%
180/180 · 20/20
3 kHz low-pass only
180/180 · 20/20
0.0%
180/180 · 20/20
de-emphasis only
0/180 · 0/20
51.3%
0/180 · 0/20
de-emphasis + 3 kHz low-pass (0x34=0x3C)
0/180 · 0/20
49.8%
0/180 · 0/20
The CPU budget
Right count, wrong price
Its measurement of the trig calls is sound and close to the grading emulator’s. What it does with the number is where it goes wrong.
Where one second of decoding goes
The firmware’s streaming loop, compiled with its own GCC flags and newlib and run in an emulated Cortex-M4F over 2.2 s of input (12 LDUs, 108 IMBE frames, all decoded). Instruction counts become time by assuming one clock cycle per instruction, the optimistic end.
The demodulator fits: about 40% of the core, 60% at 1.5 cycles per instruction. The vocoder needs roughly 11 times the whole CPU, 16 times at 1.5. Building with -O2 changes these by under 1%, because the time is spent inside newlib’s prebuilt libm.Per-stage table
Stage
-Os, M instr/s
Share at 1.0 · 1.5 c/i
-O2, M instr/s
Resampler ×1.6
1.40
1.9% · 2.9%
1.32
RRC FIR, timing and level fit
28.00
38.9% · 58.3%
27.70
Framing, NID and FEC
0.74
1.0% · 1.5%
0.73
mbelib IMBE decode and synthesis
781.90
1,086.0% · 1,629.0%
781.60
Total
812.1
1,128% · 1,692%
811.4
CPU time for each 20 ms of speech
One call to mbelib’s IMBE decoder per frame, uvquality 1, FEC-valid codewords with chosen pitch and voicing, mean over 100 consecutive frames.
Every frame type misses its 20 ms deadline, from about 2× for the simplest frames to more than 20× for loud voiced ones. The cause is mbelib’s per-harmonic phase, which accumulates without wrapping (mbelib.c:306). Once a call has run for about half a second, nearly every voiced-band cosf argument exceeds about 201 radians, and newlib’s cosf takes its __kernel_rem_pio2f path at roughly 1,700 instructions per call instead of 40 to 100. The real firmware links that same path.Per-frame table
Frame type
Mean, M instr
Worst, M instr
Mean ms at 1.0 · 1.5 c/i
L=20, all bands unvoiced
3.00
5.02
42 · 62
L=20, typical (15 of 20 voiced)
9.68
10.23
134 · 202
L=30, typical (21 of 30 voiced)
13.97
14.76
194 · 291
Random pitch and voicing
15.86
26.17
220 · 330
L=56, all bands voiced
31.25
32.97
434 · 651
Two runs, one harness
Where Opus 5 and Fable 5.1 disagree
The two Claude Code runs took the same brief through the same client on the same evening. They agree on the diagnosis and split on two questions that decide the project’s next year of work.
On the chip’s one-layer mode they reach opposite conclusions from the same manual. Fable reads the 264/288-bit quantum and the bit-error-rate framing and rules the route out; Opus reads the passage where the chip does the demodulation and the user does the protocol, and turns it into a plan with a test that would fail cleanly. The answer key’s position is that the question is untested — so Fable is too certain, and Opus is honest about the risk while betting the other way. Anyone with the radio on a bench can settle it in an afternoon, and that is the most valuable thing these two pages jointly produce.
On the vocoder they both measured, differently, and only one of them converted correctly. Fable counted cosf calls and multiplied by an assumed cycle cost; Opus counted instructions inside an emulator. The call counts agree within 10%. The conclusions differ by six times, and the difference is entirely in what a newlib trig call costs on a Cortex-M4 once range reduction is included.
Where Fable is clearly ahead is the evidence chain for the microphone and the object-level memory map. Where Opus is ahead is the filter sweep, the instruction counts and the mechanism that ties the CPU cost to the parser reset.
Claim check
20 claims, checked one by one
Fifteen of twenty hold, six of them validated by rerunning what it ran. Three are overstated, one is unverified, and one — the cycle cost of the calls it counted — is wrong.
Item
What Claude Fable 5.1 claims
Where
Result
Notes
§3
OpenGD77 only ever reads the I2S RX buffer to capture the microphone: soundReceiveRefillData is its sole consumer, armed from the DMR transmit path, and its comment says “only use the Left Channel of the Mic Audio”
sound.c:436–465; HR-C6000.c:1742
Validated
Exact, and the provenance chain — who arms that buffer and from which path — is evidence no other audit assembled. DMR receive audio reaches the MCU over SPI, not I2S.
§3
Bit 0x40 of 0xE0 is the microphone enable: HRC6000SetMic toggles exactly that bit, and HRC6000SetFMRx writes 0xE0=0x89 to “Turn off Microphone input”, so the monitor’s 0xC9 turns it on
HR-C6000.c:2849, 2964; p25mon.c:648, 733
Validated
Verified at each line. Found independently of the other Claude Code run, which reached the same conclusion from the same two registers.
§3
The manual documents no discriminator-to-I2S route: §4.5 gives I2S to an external codec or vocoder, and §6.2 ends the FM path at the codec DAC
manual 1110–1136, 496–530, 2794–2798
Holds
All three passages are where it says and say what it says.
§3
In DMR mode the AT1846S is configured for exactly what a P25 receiver needs — filters off (0x58=0x9CDD) and the undocumented low-frequency bit set (0x40=0x0031) — while the monitor selects the filtered 25 kHz FM profile
AT1846S.c:196–199, 205, 215; p25mon.c:724
Validated
Both register values and the “MAGIC REGISTER WHICH ALLOWS LOW FREQ AUDIO” comment check out. The clearest statement of C1’s fix on the AT1846S side.
§3
The C6000’s one-layer mode is still DMR-framed — 264 or 288 bit data after DMR sync and slot timing, with the interrupt “typically tested in bit error rate” — so there is no continuous dibit or baseband stream
manual 1346–1360, 2384–2386, 1455–1459
Overstated
The lines it cites are real and the 264/288-bit quantum is DMR burst geometry. But the manual also describes one-layer mode as solving “the channel filtering of the baseband or low-IF signal and the modulation and demodulation process” while “the user needs to solve the channel codec and the processing of all communication protocol” (1348–1352), which is the opposite reading. Whether it emits frames without DMR sync is untested — the other Claude Code run built a falsifiable plan around it.
§2
The host suite passes with 77 PASS lines and selftest 111111, on both trees
tests/p25/run.py
Validated
Rerun here: exactly 77 lines begin with PASS, and a 78th line contains PASS as part of “selftest=PASS”. Naming the selftest separately makes the count unambiguous — and corrects the grading note on an earlier audit that used 77.
§2
The build artefact is 810,252 bytes, CCM is 64,376 of 65,536 used, and .bss ends at 0x2001F9FC with 1.5 KB to _estack
MDUV380_FW.map
Holds
Every figure matches the map.
§2
README.md and CAPABILITY-REPORT.md link to P25-HARDWARE-REVIEW.md, which does not exist in the tree
README.md:157
Holds
Correct, and worth flagging: an earlier audit cited that file as though it had read it.
§4.1
The self-test shows the resampler’s cost: at 8 dB the direct 38.4 kHz path recovers 7 of 9 clean frames, the 24 kHz-plus-resampler path only 2 of 9
tests/p25/run.py test 4
Overstated
The numbers are exact — rerun here, test 4 gives 2 of 9 clean frames at 8 dB against the direct path’s 7 of 9 — and no other audit found them. But the two tests differ in more than the resampler: test 4 generates at 24 kHz, so sample rate and noise bandwidth change too. Isolating the resampler (band-limit a 38.4 kHz signal, resample, decode) gives equal or fewer symbol errors at every SNR from 16 dB down to 8 dB. The gap is real and worth investigating; attributing it to linear interpolation is not established.
§4.2
The demodulator estimates DC as the window mean and fits gain over 64 symbols, so with a real ±1.1 kHz carrier offset at 450 MHz — comparable to the 1.2 kHz level spacing — an unbalanced window can converge on the wrong labelling
p25_4fsk.c:186–210
Holds
The mechanism is right and the arithmetic is the right order. Its fix — calibrate from the 24 known sync symbols, then track slowly — is what the reference implementations do.
§4.2
Sync detection compares hard dibits with ≤2 mismatches, so a +3↔+1 slip counts as a full error; soft correlation would be more sensitive and would calibrate the levels for free
p25rx.c:409–433
Holds
Accurate reading of the sync hunt, and new: no other audit examined it.
§4.2
After a lost frame the parser stays in RX_LDU for the full 784 payload dibits, so the next frame’s sync is swallowed too
p25rx.c
Holds
Correct, and the consequence — one corrupted frame costs two — is new to the answer key.
§4.3
p25monAudioSink calls radioSetAudioPath(false) on every 20 ms voice frame, which is two SPI0 writes (0x36 and 0x10=0x6E) per frame on a bus documented as failing silently
p25mon.c:490; HR-C6000.c:2967–2977
Holds
Exactly the answer key’s two-unverified-writes defect, and the only audit to name both register values.
§4.4
Measured on the host: mbelib synthesis makes 11,812 cosf calls per frame on average, 14,865 at worst, with uvquality at its minimum
analysis/mbelib-cost/cost.c
Holds
Consistent with the grading emulator, which counts 13,072 cosf calls per IMBE frame on a different frame set. The measurement method — wrapping libm to count calls — is sound.
§4.4
At 100–200 cycles per cosf the synthesis needs roughly 60–120% of a 72 MHz core by itself
—
Wrong
The call count is right; the per-call cost is not. Newlib’s cosf with full range reduction is far more than 200 cycles — the emulator counts ~13,000 cosf plus ~13,000 __ieee754_rem_pio2f and ~8,700 __kernel_rem_pio2f calls per frame, and mbelib totals 9.4–16M instructions per frame depending on the build. The real figure is 655–1600% of the core, not 60–120%. Its conclusion that nothing is left for the FIR, USB and UI survives; its margin does not, and 168 MHz plus a 20× cheaper oscillator would look sufficient on these numbers when it is not.
§4.5
The largest reclaimable CCM users are uiNotification.o (38,400 bytes), SPI_Flash.o (4,096) and voicePrompts.o (3,788)
MDUV380_FW.map
Validated
All three sizes are exact in the map (0x9600, 0x1000, 0xecc). The only audit to name reclaimable memory at object granularity.
§5
The M17 mod targets MCU pin 18, which on the LQFP100 STM32F405 is PC3 = ADC123_IN13; verify against the board before soldering
analysis/stock-p25/REPORT.md:176
Unverified
The repository only ever says “MCU pin 18” and never maps it to a port. Two audits now disagree — this one says PC3/IN13, two others say PC2/IN12 — which is itself worth knowing before anyone solders. Both hedge correctly and tell the reader to check the schematic.
§5
DC coupling is mandatory for the tap, because P25 sync runs 24 symbols at ±1.8 kHz deviation and any AC coupling above a few hertz produces baseline wander the slicer cannot follow
p25_4fsk.c
Holds
Right, and it matches the filter sweep the other Claude Code run measured: a 50 Hz high-pass already costs 5.6% symbol errors.
§6
Phase 2 needs LFSR descrambling keyed by WACN, System ID and NAC, which come from NET_STS_BCST (opcode 0x3B) on the Phase 1 control channel — and the trunk code does not parse it
p25_trunk.c; ref-sources/dsd-fme
Holds
The most specific Phase 2 dependency any audit has named: not just “a descrambler seed” but which control-channel message carries it and that the follower ignores it today.
§5
There is no route without a hardware modification: the only unexplored possibility is an undocumented 0xE0 or auxiliary-table bit, and even then the audio would sit behind the C6000’s filters and de-emphasis
—
Overstated
The reasoning about the codec path is sound, but it rests on the one-layer denial above. With that route unsettled, “not one that decodes P25” is stronger than the evidence supports.
Provenance
What was already known, and what it added
Every clean-room audit could see only the repository: its notes, code comments and the HR-C6000 manual, summarised in the first column. Fable raises both hardware causes, the per-frame SPI writes and the demodulator’s loops, and none of the instrument rows.
Issue
Project docs in the repository
Grok 4.6 Sep 17
DeepSeek V4.1 Flash Sep 17
Muse Spark 1.3 Contributor Sep 17
GLM 5.3 Flash Sep 17
UNIONALPHA Sep 17
Qwen3.8 Flash Sep 17
Qwen3.8 Max Sep 17
GLM 5.3 Sep 17
DeepSeek V4 Pro Sep 17
Gemini 3.8 Flash Sep 17
HY4 Preview Sep 17
GPT-5.6 Sol Sep 17
GPT-5.6 Luna Sep 17
GPT-6 Astra Sep 17
Claude Opus 5 Sep 17
Claude Fable 5.1 Sep 17
Capture export stages the wrong I2S halfwords
absent
raised §1, first code change
absent its first experiment uses that capture
absent trusts the capture
absent relies on the capture
raised F3, fixed and verified
raised P1-2, tests miss the adapter
absent calls the capture validated
raised §2, proven with a reproducer
absent calls the capture module sound
absent certifies the capture path
absent its plan depends on it
raised derived from the stride
absent its first step relies on it
raised F4, reproduced on the host
absent
absent
Capture parser certifies an incomplete stream
absent
absent
absent
absent trusts the parser
absent relies on the parser
absent
absent
absent relies on the parser
absent
absent
absent
absent
partly module tests miss the stride
absent
partly asks for tests at the callback
absent
absent
pcm_starve never increments
absent documented as working
absent
absent
absent relies on it
absent
raised F7
raised P1-7
absent
raised §2
absent
absent named only as a predicted symptom
absent
absent
raised F7, first to find it
raised F9, reproduced
absent
absent
No static RAM margin
partly margins still to measure
absent
raised M6, 0 bytes free
partly “nearly full”, from the docs
raised F5, byte-exact map audit
raised F6, byte-exact
partly calls the reservation headroom
absent
partly “~zero headroom”
partly from the docs, not the map
raised §3.5, exact map symbols
absent
raised exact map figures
absent could not read the map
raised F8, from its own link map
raised with the overlay buffers named
raised with the objects to reclaim named
The I2S stream is most likely microphone audio
partly open, leaning sceptical
raised §1, 0x89 versus 0xC9
raised B1, four-value test
raised P1-1
partly open, leans towards RF
partly blocking, but never says microphone
raised P1-4, 0xE0 mic bit
raised A, the clearest case yet
raised §1, three sources
raised §1–§2, with the tap as the fix
raised §1 and §3.1
partly listed unverified, tested first
raised Blocker 1, source unproven
raised F1, as the safe default
raised F2, as far as the evidence goes
raised proved from 0xE0=0x89 and sound.c
raised traced to the buffer’s only consumer
HR-C6000 de-emphasis on the capture path
absent
raised §2, bit 5 of 0x34
absent calls it benign
raised P1-2, 0x34=0x3C
raised F2, closes the eye
partly cited, called unmeasured
raised P1-3, 0x34=0x1C
raised C, with the 0x34 bit 5 fix
raised §1, tilts the eye
absent
raised §3.3, 0x34=0x3C
raised P1-1, and the boot table too
raised 0x34=0x3C, never replaced
raised F2, without the fix
raised F2, 0x34=0x3C
raised measured at 48.9% SER
raised with the 300–3400 Hz bandpass
AT1846S FM filters, low-frequency bit, 25 kHz
partly “require characterization”
raised §2, register level
raised B2, filter register
raised P1-2, 0x58 filters
partly “voice filtering”; wrong bandwidth premise
partly cited, called unmeasured
raised P1-3, DMR 0x58 probe
absent
absent
absent
raised §3.3, with the DMR fix
absent
raised 25 kHz for a 12.5 kHz channel
absent
raised F2, the FM settings table
raised all three, with the DMR fix
raised all three, FM against DMR
0x10=0x6E hybrid state; 0x36 dual role
partly bring-up clock rules
partly misses 0x6E and the 0x36 clock gate
partly “undocumented hybrid state”
partly quiet-chip registers
raised F4, 0x10=0x80 kills the clock
partly F1, 0x6E against 0x80
partly “hybrid I2S state”
partly slot engine and eco only
partly §4.3, 0x10=0x80 hazard
partly “inconsistent hybrid state”
partly 0x36 and 0xE0, not 0x10
partly 0x36, 0xE0, 0x26; not 0x10
partly FM mode writes, not 0x10
partly 0x36, 0xE0, 0x26 listed
partly 0x36 and 0x10 via the squelch path
partly 0x36 and 0xE0, not 0x10
raised both, as the per-frame writes
Manual: I2S frame clock “must be 8KHz” missed or ruled out
absent
raised §3
absent quotes the paragraph, not the rule
absent
absent
raised F4
absent quotes the formulas, not the rule
absent cites the section, not the rule
absent
absent
absent
raised P1-2, with a test for it
absent
absent
raised F7, with the divisor arithmetic
raised H2
absent
One-layer 4FSK test mode as a P25 tap missed or ruled out
partly stock BER-test block only
raised Gate D
raised B5, exact recipe
absent dismissed
absent
partly worth a bounded test
absent ruled out at “9600 Bd”
raised step 2, a symbol source
absent “not a P25 symbol source”
absent “no raw modem mode”
absent “zero internal silicon capability”
partly cited, then dismissed
absent
absent
partly points at the layer architecture
raised Route A, with an acceptance test
absent ruled out on DMR framing
±10% health gate versus ±1% timing clamp
absent
absent
raised B3, impact overstated
raised P1-3
raised F3
absent
absent
absent
absent
absent
absent
absent
raised with the resampler consequence
raised F4, with the resampling consequence
raised F7
absent
absent
Fail-closed muting at LDU cadence
absent
absent late-entry mute only
absent
absent calls it an asset
absent
absent
partly “keep it”
absent
absent calls it tested
absent
absent certifies it as correct
absent
absent certifies it as correct
raised F8, weighed as a trade
raised F6, the late-entry half
partly via the MFID path
partly late entry mutes seven frames
Non-standard MFID mutes clear calls
absent
absent
absent
absent
absent
absent
absent
absent
absent
absent
absent certifies it as correct
absent
absent certifies it as correct
absent
absent
raised M1, with the Motorola case
absent
Test waveform shares the receiver’s RRC filter
partly “synthetic RRC/AWGN” caveat
absent
partly tested it, says not to fix
partly synthetic only, wants recordings
absent would extend that model
raised F5, unquantified
absent would extend that model
partly synthetic only, not the circularity
partly synthetic only
partly “ideal RRC-shaped signal”
absent
partly P1-5, circularity without the filter
raised names the shared table
partly synthetic, not circular
raised F10, with why the tests missed F1/F4
raised tested, called harmless
partly asks for a recorded waveform
MCU runs at 72 MHz
raised
raised in passing
absent
raised P1-5
absent
raised F7
absent
absent
raised §5
raised §7, with the OpenRTX precedent
raised §3.5, with the PLL settings
raised P1-4, with the PLL lines
partly named as the target, never costed
raised F5, as the real-time risk
raised F9, the target for measurement
raised and measured against it
raised with the PLL arithmetic
Vocoder needs 11–16× the 72 MHz CPU missed or ruled out
partly decode timing unmeasured
absent “fine on a 1 ms tick”
absent “vocoder question settled”
partly unmeasured; fix order backwards
absent “in good shape”
partly deadlines “unproven”
absent “not the problem”
absent “not the problem”
partly inline, unmeasured
partly cites 4.36M, calls it 60%
absent puts it at 15–18 ms per frame
partly 87% measured, budget unresolved
absent left as a later measurement
partly named, never sized
partly counted the filter, not the vocoder
raised measured 6.5× on the demo build
partly 11,812 trig calls, priced at 60–120%
Direct discriminator tap (M17 mod)
raised
raised uncredited
raised pins, timer ADC, 48 kS/s
raised fallback, pin 9
raised fallback
raised fallback
raised fallback
raised step 5, the likely answer
raised §1 pivot
raised its central recommendation
raised §5.1, with the ADC and DMA design
raised the fallback if Gate 0 fails
raised ADC with bias and anti-alias
raised F1, the fallback route
raised F2, ADC or another interface
raised Route B, with the ADC channels checked
raised with DC coupling and the ADC plan
Phase 2 architecture and scope
partly not implemented
partly misplaces the AMBE+2 decoder
partly voice via mbelib AMBE+2
raised with RF band limits
partly says mbelib has no AMBE+2
raised most accurate section
partly DMR and X2-TDMA parts
raised AMBE+2 present but uncalled
raised with a reference map
partly no symbol rate or sync
raised accurate on rate and slots
partly right conclusion, H-CPM mislabelled
raised plus the linked-symbol check
raised careful about the vocoder
raised the most complete of the fourteen
raised including the descrambler seed
raised including the NET_STS_BCST seed
Two unverified SPI writes per decoded 20 ms frame
absent
absent
absent
absent
absent treats them as protection
absent
absent
absent
absent
absent
partly the SPI0inUse mechanism
partly silent SPI0 failures
raised radioSetAudioPath per frame
absent
partly the sink is cited, not the writes
absent
raised 0x36 and 0x10=0x6E
Capture sessions lack epochs
absent
absent
partly measured=0 only
absent
absent
raised F9, 8 kHz under a 24 kHz header
absent
absent
absent
absent
absent
absent
partly asks for source metadata
absent
raised F4, segment on retune and clock change
absent
absent
Ring and tick real-time budget
partly deadlines unproven
partly calls it fine
absent
partly overruns look like weak RF
absent
raised F7, 1 ms is a minimum
partly register stalls against the ring
absent
partly 1 ms tick as a constraint
absent
absent
absent
raised the 2 s refresh stall
partly buffer inventory only
raised F9, all four figures exact
raised the 21 ms ring against a 55–190 ms stall
partly per-window MAC cost only
Clock config 3 assumes 12,288 Hz; the codec formula gives 12,000
absent
absent
raised B3, clock model
absent
partly “guessed semantics”
absent
raised P1-6, for a different reason
absent
partly “unvalidated on hardware”
absent
absent
absent
absent
absent
partly computes config 2 instead
absent
absent
Clock-config writes bypass the verified SPI writer
partly SPI retry note
absent
raised M1
absent
absent
raised F9
absent
absent
raised §4.2
absent
absent
absent
raised traced through four files
raised F6, both call sites
partly asks that writes be verified
partly calls the heal machinery fragile
absent calls the machinery retirable
Stock squelch re-arms FM audio (0x10=0x80) during monitoring
absent assumes it can’t re-arm
absent
absent
absent
partly names squelch logic as a risk
raised F1, new
absent “fixed” by forcing squelch open
partly names the squelch path as a writer
partly hazard flagged, “unlikely”
absent
absent
absent
raised same call chain, independently
absent ownership named in general
raised F1, reproduced on the host
absent
absent
Stale clear-call state releases a new call’s first frames
absent
absent
absent
absent
absent
raised F8, probe
absent
absent
absent
absent
absent
absent
absent
absent
partly via identity, not the 1,120 samples
absent
absent
No frequency tracking; ad-hoc timing loop gains
partly a code comment calls the DC estimate biased
absent
absent
absent
absent
absent
absent
absent
absent
raised §4, new
absent reads the loop as sound
absent
partly the ±1% clamp only
partly asks for a timing/AFC loop
absent
raised DC fit once per window, with the fix
raised with the carrier-offset arithmetic
Unknown talkgroup opens audio (fail-open gating)
absent
absent
absent
absent
absent
absent
absent
absent
absent
absent
absent
absent
absent
absent
raised F6, new and reproduced
absent
absent
Project docs: README.md, CAPABILITY-REPORT.md, STAGE3-BRINGUP.md, STAGE4-TX-FEASIBILITY.md, analysis/stock-p25/REPORT.md, analysis/capture-milestone-REPORT.md, dm1701-p25-demo/README.md and source comments. The last two columns are from runs in a different harness; they are here for comparison, not for ranking.
Its plan
What to keep, change and add
Six steps, opening with a fifteen-minute test to close the source question and closing with a validation ladder that does not need RF at all for its first rung.
Keep
Worth doing as written
Step 0: capture with the radio quiet and the microphone tapped, then with a keyed carrier, and afterwards retire the clock latching, self-heal and eco machinery from the P25 path.
Inject known baseband from a PC sound card straight into the ADC pin as the first validation rung — the host generator already produces valid LDU streams, so the whole chain is proven with no RF at all.
Keep the AT1846S in its DMR profile with the low-frequency bit set and 12.5 kHz bandwidth, and DC-couple the tap.
Move the audio-path SPI writes to call start instead of every 20 ms frame.
Calibrate levels from the sync symbols and add the in-frame re-sync so one lost frame does not cost two.
Change
Would cause new problems
“60–120% of a 72 MHz core”: the trig calls it counted cost about six times that, so 168 MHz plus a cheaper oscillator does not close the gap on its own.
Ruling out the chip’s one-layer mode: the manual supports both readings and nobody has tested it.
Add
Missing from its plan
The instrument defects: the capture export its own Step 0 depends on, the parser’s completeness check and the starvation counter.
The manual’s 8 kHz frame-clock rule, which is why the 24 kHz work was fragile in the first place.
The one-layer path, and what it would take
Manual Table 5.6 (verified line by line), with the questions that decide whether it can carry P25.
Register
Value
Manual description
0x01
xxxx 0000
IF receive mode
0x07 0x08 0x09
0x0B 0xD9 0x54
24-bit IF word, 455 kHz by default (OpenGD77 boots at 450 kHz)
0x10
0x02
One-layer mode, continuous receive; bit 5 for slot receive
0x40
0x40
Receive enable, test mode
0x41
0x41
Receive test enable
Output: 36 demodulated bytes per frame in RX RAM from 0x30, frame type in 0x51, system interrupt bit 0 when ready.
Rate fits exactly: 36 bytes every 30 ms is 9,600 bit/s, which is 4,800 symbols per second. Contiguous frames would lose nothing.
Open: Figure 5.15 is DMR burst geometry, so the mode may only emit frames after DMR sync. That’s the first thing to test.
Also needed: the AT1846S DMR register set, since the demodulator is fed by the AT1846S. And the vocoder still has to run in real time.
Phase 2
The most specific dependency list
Its Phase 2 table gives component, status and the source file that already implements it in the bundled reference: the 6,000-baud demodulator, superframe and ISCH synchronisation, SACCH and FACCH MAC PDUs with their line counts in DSD-FME, the 4V/2V voice bursts, and the AMBE+2 decoder vendored in the tree and unused.
One entry is sharper than anything else written on Phase 2 here: the descrambler is keyed by WACN, System ID and NAC, those values arrive in NET_STS_BCST, opcode 0x3B, on the Phase 1 control channel — and the trunk follower does not parse that message today. It also notes that Phase 2 mostly lives on 700/800 MHz, outside this radio’s RF hardware, which makes the whole exercise moot for most systems even if the software existed.
Against the clean-room audits
The best evidence chain, and one number that undoes its plan
Sixteen models have now reviewed this repository. Fable builds the most thorough case for the microphone finding and the only object-level memory plan, and it reaches the per-frame SPI writes that only two other audits found. Its sibling run on the same client found all four decode-critical issues; this one finds two and a half, and prices the vocoder at a sixth of its cost.
Same rubric and weights. Hover or focus a bar for the score.
Claude Opus 5
GPT-6 Astra
GPT-5.6 Sol
Claude Fable 5.1
Method and limits
How this was checked
Disclosure: this review was written by a model from the same maker as the auditor that graded it. The rubric, the weights and the answer key were fixed before this run and are the same ones used for every audit on this task; every claim was checked at its cited line, and the measurements it reports were rerun during grading. The evidence is listed so a reader can check it rather than take the grade on trust.
I read CLAUDEFABLE51_REVIEW.md in full and verified its citations at source, including the three CCM object sizes and the buffer-provenance chain. I reran the host suite to check its PASS count and its 8 dB figures, and extended the grading model’s resampler sweep down to 8 dB to test its attribution. The vocoder comparison uses the emulator built for the earlier clean-room audits.
The run itself: one model, 71 steps, no subagents — the subagent tool was not in its toolset — no skills, plugins or MCP servers, no web tools, and nothing read outside its copy of the repository. It ran through the Claude Code CLI rather than the bench profile, which is why the page is not ranked.
Filter responses are modelled, not measured on the radio.
CPU figures are emulator instruction counts, which are a lower bound on cycles.
Its ADC pin mapping is unverified: the repository never maps “MCU pin 18” to a port, and two audits disagree about it.
No firmware was flashed, and no radio was touched.
python3 tests/p25/run.py | grep -c '^PASS' # 77; a 78th line contains "selftest=PASS"
# test 3 at 8 dB: clean 7/9 · test 4 (24 kHz + resampler) at 8 dB: clean 2/9
DM1701_REPO=… python3 resampler_check.py # resampler isolated, 16 dB down to 8 dB
# equal or fewer symbol errors through p25_resample at every SNR