Review audit · firmware & RF
17 September 2026
DM-1701 · HR-C6000 · AT1846S · STM32F405 · P25

Claude Fable 5.1’s P25 review, graded

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

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.

sound.c:436–465 · HR-C6000.c:1742, 2849, 2964 · p25mon.c:648

Validated

It reads the map at object granularity

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,812 cosf 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.

analysis/mbelib-cost/cost.c · mbelib.c:306 · STAGE4-TX-FEASIBILITY.md:36

New

Three demodulator defects nobody else reached

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+.

0255075100Score out of 100Accuracy & evidenceweight 30%85Coverage of decode problemsweight 25%62Root cause & prioritisationweight 15%84Fix plan & acceptance gatesweight 15%86Originality & attributionweight 10%84Clarity & calibrationweight 5%88
Rubric table
DimensionWeightScorePointsWhy
Accuracy & evidence30%8525.515 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 problems25%6215.5Both 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 & prioritisation15%8412.6It 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 gates15%8612.9Six 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 & attribution10%848.4The 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 & calibration5%884.4A 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 total100%79Grade 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.

RADIO · ANALOG FRONT ENDSTM32F405 · 72 MHz · -OsMicrophoneLineIn1 · enabled by E0 bit 6P25 C4FM carrier12.5 kHz channel±1.8 kHz peak deviationAT1846S receiveranalog FM register setHPF + LPF on · LF audio off · 25 kHzHR-C6000 codec ADCE0 = 0xC9 · 0x34 de-emphasis onno documented RF input to I2SI2S to the MCULRCK 24 kHz (manual: 8 kHz)left slot, 16-bitRFaudio?ADCDMA half-buffer ISRp25monI2sHalfBuffer160 frames per callbackSample ring512 samples ≈ 21 ms≤128 drained per tickResampler ×1.624 → 38.4 kHz, nominalhealth gate ±10 %RRC FIR + timing81 taps · 768/512 windowperiod clamp ±1 %p25rx framingsync · NID · LDU · FECfail-closed mutembelib IMBE≈16 M instructions/framecosf slow pathPCM ring → I2S TXradioSetAudioPath() per frame→ speaker amplifierCapture export ‘c’ISR stages [j][0..79]framed hex over USBUSB CDC consoleshared with 1 Hz reportp25_capture.pyparser on the hostdiagnostic tapF1F2F3F4F5F6N1N2N3N4
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.

RegisterMonitor today (analog FM)OpenGD77 DMR modeEffect on C4FM
AT1846S 0x400x0030, 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 0x580xBC85, “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 0x440x06CC, AF gain 80%0x07FF, 100%; the DM-1701 “needs higher AF gain” to decode 4FSKLower discriminator level into the next stage
AT1846S bandwidth25 kHz12.5 kHz, forced for digitalWider noise bandwidth and deviation scaling set for ±5 kHz FM
HR-C6000 0x340x3C, “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.

04590135180IMBE frames decoded, noise-free (of 180)no audio filtering180 / 1803 kHz low-pass only180 / 180de-emphasis only0 / 180de-emphasis + 3 kHz low-pass (0x34=0x3C)0 / 180
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 stageNoise-free frames · NIDsSymbol errors20 dB frames · NIDs
no audio filtering180/180 · 20/200.0%180/180 · 20/20
3 kHz low-pass only180/180 · 20/200.0%180/180 · 20/20
de-emphasis only0/180 · 0/2051.3%0/180 · 0/20
de-emphasis + 3 kHz low-pass (0x34=0x3C)0/180 · 0/2049.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.

0%200%400%600%800%1000%1200%Share of one CPU second at 72 MHz (-Os, 1.0 cycles per instruction)Resampler ×1.61.4 M instructions/s1.9%RRC FIR, timing and level fit28.0 M instructions/s39%Framing, NID and FEC0.7 M instructions/s1.0%mbelib IMBE decode and synthesis781.9 M instructions/s1,086%whole CPU
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/sShare at 1.0 · 1.5 c/i-O2, M instr/s
Resampler ×1.61.401.9% · 2.9%1.32
RRC FIR, timing and level fit28.0038.9% · 58.3%27.70
Framing, NID and FEC0.741.0% · 1.5%0.73
mbelib IMBE decode and synthesis781.901,086.0% · 1,629.0%781.60
Total812.11,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.

0 ms100 ms200 ms300 ms400 ms500 msCPU time per decoded 20 ms frame at 72 MHz (mean, 1.0 cycles per instruction)L=20, all bands unvoiced3.0 M instructions42 msL=20, typical (15 of 20 voiced)9.7 M instructions134 msL=30, typical (21 of 30 voiced)14.0 M instructions194 msRandom pitch and voicing15.9 M instructions220 msL=56, all bands voiced31.2 M instructions434 ms20 ms deadline
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 typeMean, M instrWorst, M instrMean ms at 1.0 · 1.5 c/i
L=20, all bands unvoiced3.005.0242 · 62
L=20, typical (15 of 20 voiced)9.6810.23134 · 202
L=30, typical (21 of 30 voiced)13.9714.76194 · 291
Random pitch and voicing15.8626.17220 · 330
L=56, all bands voiced31.2532.97434 · 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.

ItemWhat Claude Fable 5.1 claimsWhereResultNotes
§3OpenGD77 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:1742ValidatedExact, 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.
§3Bit 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 onHR-C6000.c:2849, 2964; p25mon.c:648, 733ValidatedVerified at each line. Found independently of the other Claude Code run, which reached the same conclusion from the same two registers.
§3The 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 DACmanual 1110–1136, 496–530, 2794–2798HoldsAll three passages are where it says and say what it says.
§3In 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 profileAT1846S.c:196–199, 205, 215; p25mon.c:724ValidatedBoth 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.
§3The 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 streammanual 1346–1360, 2384–2386, 1455–1459OverstatedThe 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.
§2The host suite passes with 77 PASS lines and selftest 111111, on both treestests/p25/run.pyValidatedRerun 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.
§2The build artefact is 810,252 bytes, CCM is 64,376 of 65,536 used, and .bss ends at 0x2001F9FC with 1.5 KB to _estackMDUV380_FW.mapHoldsEvery figure matches the map.
§2README.md and CAPABILITY-REPORT.md link to P25-HARDWARE-REVIEW.md, which does not exist in the treeREADME.md:157HoldsCorrect, and worth flagging: an earlier audit cited that file as though it had read it.
§4.1The 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 9tests/p25/run.py test 4OverstatedThe 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.2The 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 labellingp25_4fsk.c:186–210HoldsThe 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.2Sync 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 freep25rx.c:409–433HoldsAccurate reading of the sync hunt, and new: no other audit examined it.
§4.2After a lost frame the parser stays in RX_LDU for the full 784 payload dibits, so the next frame’s sync is swallowed toop25rx.cHoldsCorrect, and the consequence — one corrupted frame costs two — is new to the answer key.
§4.3p25monAudioSink 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 silentlyp25mon.c:490; HR-C6000.c:2967–2977HoldsExactly the answer key’s two-unverified-writes defect, and the only audit to name both register values.
§4.4Measured on the host: mbelib synthesis makes 11,812 cosf calls per frame on average, 14,865 at worst, with uvquality at its minimumanalysis/mbelib-cost/cost.cHoldsConsistent 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.4At 100–200 cycles per cosf the synthesis needs roughly 60–120% of a 72 MHz core by itselfWrongThe 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.5The largest reclaimable CCM users are uiNotification.o (38,400 bytes), SPI_Flash.o (4,096) and voicePrompts.o (3,788)MDUV380_FW.mapValidatedAll three sizes are exact in the map (0x9600, 0x1000, 0xecc). The only audit to name reclaimable memory at object granularity.
§5The M17 mod targets MCU pin 18, which on the LQFP100 STM32F405 is PC3 = ADC123_IN13; verify against the board before solderinganalysis/stock-p25/REPORT.md:176UnverifiedThe 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.
§5DC 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 followp25_4fsk.cHoldsRight, and it matches the filter sweep the other Claude Code run measured: a 50 Hz high-pass already costs 5.6% symbol errors.
§6Phase 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 itp25_trunk.c; ref-sources/dsd-fmeHoldsThe 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.
§5There 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-emphasisOverstatedThe 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.

IssueProject 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 halfwordsabsentraised
§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
absentabsent
Capture parser certifies an incomplete streamabsentabsentabsentabsent
trusts the parser
absent
relies on the parser
absentabsentabsent
relies on the parser
absentabsentabsentabsentpartly
module tests miss the stride
absentpartly
asks for tests at the callback
absentabsent
pcm_starve never incrementsabsent
documented as working
absentabsentabsent
relies on it
absentraised
F7
raised
P1-7
absentraised
§2
absentabsent
named only as a predicted symptom
absentabsentraised
F7, first to find it
raised
F9, reproduced
absentabsent
No static RAM marginpartly
margins still to measure
absentraised
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
absentpartly
“~zero headroom”
partly
from the docs, not the map
raised
§3.5, exact map symbols
absentraised
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 audiopartly
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 pathabsentraised
§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
absentraised
§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 kHzpartly
“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
absentabsentabsentraised
§3.3, with the DMR fix
absentraised
25 kHz for a 12.5 kHz channel
absentraised
F2, the FM settings table
raised
all three, with the DMR fix
raised
all three, FM against DMR
0x10=0x6E hybrid state; 0x36 dual rolepartly
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 outabsentraised
§3
absent
quotes the paragraph, not the rule
absentabsentraised
F4
absent
quotes the formulas, not the rule
absent
cites the section, not the rule
absentabsentabsentraised
P1-2, with a test for it
absentabsentraised
F7, with the divisor arithmetic
raised
H2
absent
One-layer 4FSK test mode as a P25 tap missed or ruled outpartly
stock BER-test block only
raised
Gate D
raised
B5, exact recipe
absent
dismissed
absentpartly
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
absentabsentpartly
points at the layer architecture
raised
Route A, with an acceptance test
absent
ruled out on DMR framing
±10% health gate versus ±1% timing clampabsentabsentraised
B3, impact overstated
raised
P1-3
raised
F3
absentabsentabsentabsentabsentabsentabsentraised
with the resampler consequence
raised
F4, with the resampling consequence
raised
F7
absentabsent
Fail-closed muting at LDU cadenceabsentabsent
late-entry mute only
absentabsent
calls it an asset
absentabsentpartly
“keep it”
absentabsent
calls it tested
absentabsent
certifies it as correct
absentabsent
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 callsabsentabsentabsentabsentabsentabsentabsentabsentabsentabsentabsent
certifies it as correct
absentabsent
certifies it as correct
absentabsentraised
M1, with the Motorola case
absent
Test waveform shares the receiver’s RRC filterpartly
“synthetic RRC/AWGN” caveat
absentpartly
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”
absentpartly
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 MHzraisedraised
in passing
absentraised
P1-5
absentraised
F7
absentabsentraised
§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 outpartly
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)raisedraised
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 scopepartly
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 frameabsentabsentabsentabsentabsent
treats them as protection
absentabsentabsentabsentabsentpartly
the SPI0inUse mechanism
partly
silent SPI0 failures
raised
radioSetAudioPath per frame
absentpartly
the sink is cited, not the writes
absentraised
0x36 and 0x10=0x6E
Capture sessions lack epochsabsentabsentpartly
measured=0 only
absentabsentraised
F9, 8 kHz under a 24 kHz header
absentabsentabsentabsentabsentabsentpartly
asks for source metadata
absentraised
F4, segment on retune and clock change
absentabsent
Ring and tick real-time budgetpartly
deadlines unproven
partly
calls it fine
absentpartly
overruns look like weak RF
absentraised
F7, 1 ms is a minimum
partly
register stalls against the ring
absentpartly
1 ms tick as a constraint
absentabsentabsentraised
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,000absentabsentraised
B3, clock model
absentpartly
“guessed semantics”
absentraised
P1-6, for a different reason
absentpartly
“unvalidated on hardware”
absentabsentabsentabsentabsentpartly
computes config 2 instead
absentabsent
Clock-config writes bypass the verified SPI writerpartly
SPI retry note
absentraised
M1
absentabsentraised
F9
absentabsentraised
§4.2
absentabsentabsentraised
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 monitoringabsent
assumes it can’t re-arm
absentabsentabsentpartly
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”
absentabsentabsentraised
same call chain, independently
absent
ownership named in general
raised
F1, reproduced on the host
absentabsent
Stale clear-call state releases a new call’s first framesabsentabsentabsentabsentabsentraised
F8, probe
absentabsentabsentabsentabsentabsentabsentabsentpartly
via identity, not the 1,120 samples
absentabsent
No frequency tracking; ad-hoc timing loop gainspartly
a code comment calls the DC estimate biased
absentabsentabsentabsentabsentabsentabsentabsentraised
§4, new
absent
reads the loop as sound
absentpartly
the ±1% clamp only
partly
asks for a timing/AFC loop
absentraised
DC fit once per window, with the fix
raised
with the carrier-offset arithmetic
Unknown talkgroup opens audio (fail-open gating)absentabsentabsentabsentabsentabsentabsentabsentabsentabsentabsentabsentabsentabsentraised
F6, new and reproduced
absentabsent

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.

RegisterValueManual description
0x01xxxx 0000IF receive mode
0x07 0x08 0x090x0B 0xD9 0x5424-bit IF word, 455 kHz by default (OpenGD77 boots at 450 kHz)
0x100x02One-layer mode, continuous receive; bit 5 for slot receive
0x400x40Receive enable, test mode
0x410x41Receive 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.

Claude Opus 5 clean-room review, graded A (92), also unranked →

Scores by dimension

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
0255075100Score out of 100Accuracy & evidence94929185Coverage of decode problems86847062Root cause & prioritisation96888484Fix plan & acceptance gates93929086Originality & attribution95908484Clarity & calibration92868688Weighted total92898479
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

Scripts: resampler_check.py, deemph_check.py, c4fm_sim.py and metrics.json. They need numpy and gcc.