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

Qwen3.8 Flash’s P25 review, graded

Qwen3.8 Flash 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, and it ran the host tests and built the firmware. I checked its claims against the code, the manual, the bundled DSD-FME sources and the repository’s notes, and used the firmware-in-the-loop model and CPU emulation from the earlier clean-room audits.

Overall grade
D
59 / 100 weighted

Right about the filters. Wrong about the CPU.

Qwen3.8 Flash reads the receive path well. It finds that the monitor inherits HR-C6000 de-emphasis and the AT1846S voice filters, and proposes exactly the right first change, 0x34=0x1C, which clears de-emphasis and nothing else. In the firmware-in-the-loop model de-emphasis alone leaves 0 of 180 frames. It also argues from the driver’s own bit meanings that the monitor’s 0xE0=0xC9 switches the microphone on, and it finds the capture bug along with the reason the tests missed it.

Its architecture claims are weaker. It calls the CPU-side chain “not the problem”, though mbelib needs 11–16 times the 72 MHz CPU. It puts DMR at “9600 Bd”, twice the real rate, and uses that to rule out the chip’s one-layer mode, and it builds Phase 2 from DMR’s BPTC code, Motorola’s X2-TDMA and IMBE voice. 21 of 33 claims hold and 6 are wrong, the most of any clean-room audit. That puts it at D.

Claims that check out
21 / 33
6 overstated, mislabelled or doubtful · 6 wrong
Decode-critical issues found
2 of 4
misses the 8 kHz rule; clears the vocoder
Frames left by de-emphasis
0 of 180
the stage its 0x34=0x1C would switch off
CPU the vocoder needs
11–16×
of 72 MHz; it calls the chain “not the problem”

The run, not graded: 57 min · 92 agent steps · 97 tool calls · 147K output tokens (126K reasoning) · 13.4M tokens re-read · OpenRouter API, one-shot · second attempt; the first stopped on a provider rate limit

Headline findings

Six things to know about this review

Holds up

It finds the front-end fix, down to the bit

The monitor never writes HR-C6000 register 0x34, so it inherits 0x3C from the FM setup: de-emphasis on, −6 dB per octave from 300 Hz. Qwen3.8 Flash explains why that breaks the 4-level eye and proposes 0x34=0x1C, which clears de-emphasis and keeps the 3 kHz filter, plus the AT1846S DMR filter setting as the first probe. In the firmware-in-the-loop model, de-emphasis alone leaves 0 of 180 frames, and the 3 kHz filter it keeps costs none.

HR-C6000.c:2851 · AT1846S.c:191–217 · manual 2798–2824 · QWEN38FLASH_REVIEW.md P1-3

Holds up

It makes the microphone the default and designs the test

HRC6000SetMic() toggles bit 6 of 0xE0, and the monitor pins 0xE0=0xC9 with that bit set, while the stock FM setup writes 0x89, “Turn off Microphone input”. It calls the microphone hypothesis well founded and says it must be retired with data first. Its one scripted hardware session crosses 0xE0 against 0x34 with read-back checks, and it falls back to the M17 discriminator tap if RF isn’t there.

HR-C6000.c:2849, 2964 · p25mon.c:648, 733 · STAGE3-BRINGUP.md RF survey

New insight

It explains why the capture bug passed the tests

The decoder reads the left channel, [j][i*2], but the capture exports the first 80 interleaved words of each row, so half the left samples never leave the radio. The capture tests feed p25monCaptureSamples() clean mono arrays, so they check the module and never the adapter in p25mon.c that has the bug. It also finds that PTT isn’t masked during monitoring and that APRS writes the microphone bit behind the monitor’s back.

p25mon.c:443, 462–466 · tests/p25/capture_isr_tests.py · applicationMain.c:607 · aprs.c:530

Critical error

It clears the decoder that can’t keep up

“The CPU-side P25 Phase 1 decode chain … is not the problem”, and the capture path is “the only thing that actually blocks Phase 1 voice”. Emulated instruction counts put mbelib’s IMBE synthesis at 11–16 times the radio’s 72 MHz CPU: one voiced 20 ms frame takes 134–458 ms, so the decoder falls behind on every voiced frame, whatever the front end does. The review never looks at CPU time or mentions the 72 MHz clock.

QWEN38FLASH_REVIEW.md bottom line · mbelib.c:306 · Core/Src/main.c:269–274

Critical error

It rules out the chip’s modem at the wrong baud rate

“DMR is 9600 Bd 4FSK (19.2 kbps)”, so, it argues, the HR-C6000’s modem samples P25 at a 2:1 mismatch and can serve neither phase. DMR is 4FSK at 4,800 symbols per second, 9.6 kbps, the same symbol rate as P25 Phase 1, and the manual lists 9.6 kbps. The one-layer test mode it dismisses stores 36 bytes every 30 ms, exactly 4,800 symbols per second. Whether it needs DMR sync is an open question worth a test, not a closed door.

QWEN38FLASH_REVIEW.md §1 · manual 36, 2553–2635

Wrong

Its Phase 2 stack comes from other protocols

It gets the scope right: 6,000 symbols per second, the 20-symbol sync, 30 ms slots, and not enough memory. But it builds the stack from DMR’s BPTC(196,96) code, Motorola’s X2-TDMA decoder (x2tdma_voice.c) and IMBE voice. Phase 2 voice is AMBE+2 half-rate, and the DSD-FME Phase 2 sources use no BPTC. It also calls P25 group IDs 24-bit (they are 16), counts the linker’s 1,536-byte heap and stack reservation as headroom, and misses the manual’s 8 kHz frame-clock rule in the very sentence it quotes.

ref-sources/dsd-fme/src/x2tdma_voice.c, p25p2_frame.c, p25p2_vpdu.c · manual 1216–1221 · MDUV380_FW.map

Scorecard

How the 59 breaks down

Same rubric, weights and scale as every P25 audit on bench, so scores compare directly. Qwen3.8 Flash sits mid-table on root cause and the plan, and has the lowest accuracy of the six clean-room audits.

Dimension scores

Hover or focus a bar for the reasoning. Weighted total 59, grade D.

0255075100Score out of 100Accuracy & evidenceweight 30%62Coverage of decode problemsweight 25%40Root cause & prioritisationweight 15%68Fix plan & acceptance gatesweight 15%70Originality & attributionweight 10%64Clarity & calibrationweight 5%70
Rubric table
DimensionWeightScorePointsWhy
Accuracy & evidence30%6218.621 of 33 claims hold, including every receive-path register and line it cites. Six are wrong and two steer the plan: the CPU-side chain “is not the problem”, and DMR runs at “9600 Bd”, which rules out the chip’s one-layer mode; its Phase 2 stack borrows DMR and X2-TDMA parts.
Coverage of decode problems25%4010.0Finds both halves of the filter chain with the exact fix, the microphone evidence, the capture bug, the dead starvation counter and the discriminator tap. It misses the 8 kHz rule, the vocoder CPU wall, 72 MHz, the oracle, the parser, capture epochs and the gate/clamp mismatch, and dismisses one-layer mode on a wrong baud rate.
Root cause & prioritisation15%6810.2Puts the unverified source, the voice filters and the microphone bit first and designs one session that separates them, but calls the CPU-side chain “not the problem” and the capture path “the only thing” blocking voice.
Fix plan & acceptance gates15%7010.5Small, ordered, test-first steps with one decisive hardware session (a 0xE0 × 0x34 matrix that includes the right 0x1C), a tap fallback and codec ownership. No vocoder work or independent oracle, and it would keep fail-closed muting and add a 48 kHz clock the manual rules out.
Originality & attribution10%646.4Adds why the capture bug survived testing (the tests drive the module, not the p25mon.c adapter), the exact one-bit de-emphasis change, and unmasked PTT and APRS microphone writes, and credits the repository’s notes. Much of the rest restates the stock report and bring-up log.
Clarity & calibration5%703.5A clear bottom line, severity-labelled findings with resolutions and an honest list of what it didn’t verify, undercut by confident errors: “neither P25 phase can be demodulated by the C6000”, “not the problem”, and a Phase 2 stack stated as fact.
Weighted total100%59Grade scale: A ≥ 90 · B 80–89 · C+ 75–79 · C 70–74 · C− 65–69 · D 50–64 · F < 50
Receive chain

Where its findings sit

Qwen3.8 Flash’s findings sit on the front end: the AT1846S filters, HR-C6000 de-emphasis, the microphone bit, the capture export and the clock table. The red tags sit on the MCU side, where the vocoder, the test oracle, the parser and two decoder policies go unexamined or cleared.

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 tapP3P3P4P2P7P6N1N2N3N4N5N6
P3 raised by Qwen3.8 FlashN1 missed or misreadDashed edge: no documented connection
P3De-emphasis and AT1846S voice filters, with the 0x34=0x1C fix
N1Manual: the I2S frame clock must be 8 kHz
P40xE0=0xC9 sets the microphone bit
N2mbelib needs 11–16× the 72 MHz CPU (“not the problem”)
P2The capture stages the wrong halfwords; tests never reach the adapter
N3The test waveform shares the receiver’s RRC
P7pcm_starve never counts
N4The parser certifies incomplete captures
P6Clock config 3’s 12,288 Hz label is wrong
N5Fail-closed muting at LDU cadence (“keep it”)
N6±10% decode gate versus ±1% timing clamp
The front end, checked

Right about the stages, and about the fix

Qwen3.8 Flash’s reading of the analog chain matches the registers and the model. The one change it proposes first, clearing 0x34 bit 5, removes the stage that does the damage without touching the filter that doesn’t.

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
The 3 kHz low-pass is harmless because C4FM sits below 2.88 kHz; de-emphasis, −6 dB per octave from 300 Hz, leaves no frames on its own. 0x34=0x1C clears exactly that bit, as the review proposes.
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

The chain it called “not the problem”

The review clears the CPU-side decoder without measuring it. The emulated budget shows where one second of decoding goes.

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
Claim check

33 claims, checked one by one

Twenty-one hold, including every receive-path register and line it cites. Six are overstated, mislabelled or doubtful. Six are wrong, the most of any clean-room audit, and three of those shape the architecture: the CPU, the chip’s baud rate and the Phase 2 stack.

ItemWhat Qwen3.8 Flash claimsWhereResultNotes
ChecksThe host suite exits 0 with 78 PASS lines, and the full cross-build gives an 810,252-byte imagetests/p25/run.py; make all PLATFORM=RT84_DM1701 VARIANT=DM1701HoldsMatches the baseline run and build.
BLUFThe CPU-side Phase 1 chain (framing, NID/BCH, FEC, IMBE) “is not the problem”; the analog capture path is “the only thing that actually blocks Phase 1 voice”QWEN38FLASH_REVIEW.md bottom lineWrongEmulated instruction counts put mbelib’s IMBE synthesis at 11–16× the radio’s 72 MHz CPU: one voiced 20 ms frame takes 134–458 ms. The review never looks at CPU or mentions 72 MHz.
§1The C6000 modem runs a fixed DMR symbol clock of “9600 Bd 4FSK (19.2 kbps)”, so it samples P25 Phase 1 at a 2:1 mismatch and neither phase can be demodulated by the chip§1; §3WrongDMR is 4FSK at 4,800 symbols per second (9.6 kbps), the same symbol rate as P25 Phase 1 C4FM, and the manual lists 9.6 kbps. One-layer mode’s 36 bytes per 30 ms is exactly 4,800 symbols per second, so the premise it uses to rule the chip out is false.
§1The one-layer BER mode (Table 5.6: 0x10=0x02, 0x40=0x40, 0x41=0x41, RX RAM 1.2K at 0x30 over SPI) “still frames to DMR slot structure”manual 2553–2635OverstatedThe register values and RX RAM address are right (it leaves out 0x01 and the 0x07–0x09 IF word). The frame format is DMR-shaped, but whether the mode emits frames without DMR sync is untested, not settled.
§1The manual documents I2S only as external-codec and vocoder PCM plumbing (“voice data from CodecADC”); no discriminator route is documented, and stock V02.03 was searched without finding onemanual §4.5.2, §4.6; analysis/stock-p25/REPORT.md §4HoldsCredited to the stock report.
§1The monitor has never produced a P25 sync from real RF across two surveys while measuring live analog carriers; absence of evidence is not proof, but the microphone hypothesis is well founded and must be retired with data firstSTAGE3-BRINGUP.md RF surveyHoldsThe right reading: silence says nothing about the sample path.
P1-2The decoder takes even words (p25mon.c:443) but the capture stages the first 80 words of each row (462–466): L0, R0 … L39, R39, so words 80–159, the other 40 left samples, are never exportedp25mon.c:443, 462–466Holds158 of 160 staged positions differ from what the decoder consumed. Its layout simulation wasn’t saved, but the arithmetic matches the code.
P1-2The header still declares mono int16, the tool derives duration from count over nominal rate, and comments at p25mon_capture.c:70 and p25mon.c:456–459 describe the staging as faithfulp25mon_capture.c:70, 224; tools/p25_capture.py:89; p25mon.c:456–459HoldsExact lines.
P1-2The bug survived because the capture tests drive p25monCaptureSamples() directly with mono arrays: they validate the module, not the adapter in p25mon.ctests/p25/capture_isr_tests.py; tests/p25/run.py:264–270HoldsRight: run.py compiles p25mon_capture.c but never p25mon.c’s callback.
P1-3HRC6000SetFMRx writes 0x34=0x3C, which per manual §6.2 enables de-emphasis (bit 5, −6 dB/oct over 300–3000 Hz) with an LPF choice, and the P25 monitor never writes 0x34HR-C6000.c:2851; manual 2798–2824Holds
P1-3De-emphasis destroys the flat spectrum the RRC matched filter and the ±1/±3 level fit assume, and a single scalar gain and DC fit can’t equalise the tilt; a 2.55 kHz LPF sits below C4FM’s contentp25_4fsk.c; P1-3HoldsIn the firmware-in-the-loop model de-emphasis alone leaves 0 of 180 frames, and the 2.55 kHz filter costs symbols while the 3 kHz one doesn’t.
P1-3The AT1846S analog profile enables filters (0x58=0xBC85) while the DMR profile disables them all (0x9CDD), and the monitor runs the analog setAT1846S.c:191–217; p25mon.c:724Holds
P1-3 fixWrite 0x34=0x1C (de-emphasis off, band-pass off, 3 kHz LPF) and try the AT1846S DMR 0x58 value, as candidates to measureP1-3; R4HoldsExactly the safe change: clear bit 5 only, keeping away from the 2.55 kHz filter that 0x00 might select.
P1-4HRC6000SetMic toggles 0xE0 bit 0x40, the mic-into-Line-in-1 enable; the monitor pins 0xE0=0xC9 (mic on) at p25mon.c:648 and 733, while stock SetFMRx writes 0x89 (mic off)HR-C6000.c:2849, 2964; p25mon.c:648, 733HoldsThe driver’s comment on 0x89 is “Turn off Microphone input”.
P1-5Squelch-open events rewriting 0x36/0x10 were “fixed by forcing squelch open once at start”P1-5; STAGE3-BRINGUP.md:296–297WrongThe repository says so, but trx.c:517 re-arms whenever the RF amplifier is off, and the monitor turns it off at start, on call changes and after 300 ms idle, so the stock path still writes 0x10=0x80. Its own step R6, gating radioSetAudioPath while the monitor is active, would close it.
P1-5p25monWriteReg can stall about 200 ms per contended register, and about ten per refresh, against a 21 ms ring whose overrun resets the whole streamp25mon.c:624–656, 578–580Holds
P1-5Suspending hrc6000Task, the bring-up notes’ next step, is still not implemented, and APRS calls HRC6000SetMic(true) behind the monitor’s backp25/ (no vTaskSuspend); aprs.c:530; STAGE3-BRINGUP.md:359–363HoldsThe APRS call is compiled for STM32 platforms.
P1-6Clock config 3 {0x07, 0x00FF} can’t be 12,288 Hz: under the divider model it “would be 24 kHz gated by the CK>34×FS rule”; derive guessHz from 12.288 MHz/2·(LRCK+1), and the ±10% gate would catch a mismatchp25mon.c:80–87; manual 1216–1221DoubtfulThe 12,288 label is wrong, but not for its reason. Rate = min(BCLK/64, LRCK) reproduces all four measured configs and puts config 3 at 12,000 Hz. That 2.4% error passes the ±10% gate and defeats the ±1% timing clamp, and deriving guessHz from LRCK alone would call it 24 kHz.
P1-648 kHz needs {0x2F=0x02, LRCK=0x007F}, the only pair meeting the manual’s CK>34·FS and clock≥6·CK constraints, exactly at the boundarymanual 1216–1221; Appendix A.4HoldsThe arithmetic checks out. The same manual sentence says the frame clock “must be 8KHz”, which the review never mentions.
P1-7p25monAudioUnderruns is declared and printed but never incrementedp25mon.c:123, 517Holds
P1-8decode_link_control keeps talkgroups to 16 bits, so “24-bit P2 group IDs alias/truncate”p25rx.h:56WrongP25 talkgroups are 16-bit in both phases; DSD-FME’s Phase 2 MAC decoder reads 16-bit groups (p25p2_vpdu.c:110, 209, 309). Unit IDs are the 24-bit ones.
P1-8Sync tolerates up to two dibit errors in 24, and camping needs at least two BCH-valid NIDsp25rx.c:10, 324Holds
§5Golay, Hamming, RS, trellis and CRC16/32 are all cross-checked against independent reference encoderstests/p25/signalling.pyOverstatedGolay, Hamming and RS use DSD’s reference classes, but the trellis test re-implements the production transition table (signalling.py:84) and the trunk tests live in trunk.py.
P1-8Fail-closed muting means a noisy call stays muted rather than partially decoding, “the safe one — keep it”p25rx.cOverstatedOne failed signalling or NID decode demotes a clear call and mutes up to nine frames (p25rx.c:177–181). Latching an established clear call through a single RS failure keeps the safety without the dropouts.
Phase 2Phase 2 is out of scope per CAPABILITY-REPORT.md; the demodulator is fixed at 4800 Bd and 8 samples per symbol, with no 20-symbol P25P2_SYNC search and no TDMA machineryCAPABILITY-REPORT.md:31, 77; p25_4fsk.h; ref-sources/dsd-fme/include/dsd.h:1027HoldsThe 20-symbol sync string is right.
Phase 2Phase 2 framing needs BPTC(196,1/3,96) FEC and per-call IMBE re-interleave; it is DSD-FME’s “X2-TDMA/P25p2” path (x2tdma_voice.c, bptc.c) on the same mbelib IMBE back end, and hotspot.c’s DMR BPTC codec mirrors what’s needed§3; ref-sources/dsd-fme/src/x2tdma_voice.c, p25p2_frame.cWrongx2tdma_voice.c is Motorola’s DMR-derived X2-TDMA, not P25 Phase 2; the p25p2 sources have no BPTC; and Phase 2 voice is AMBE+2 half-rate (p25p2_frame.c stores ambe frames), not IMBE. BPTC(196,96) is DMR’s code.
Phase 2Software Phase 2 needs at least 48 kS/s (8 samples per symbol) of flat capture, and the chip modem’s rate error is 1.6:1§3DoubtfulThe 1.6:1 figure rests on the 9,600-baud premise; against the real 4,800 it is 1.25:1. And 24 kS/s gives four samples per 6,000-baud symbol, which resampling can work with; 48 kS/s is a preference, not an established requirement.
§3Memory: RAM static end 0x2001FA00 with “1536 B of heap+min-stack left” (headroom), CCM 1,160 bytes free, a 20 KB FreeRTOS heap and about 189 KB of flash unusedMDUV380_FW.map; Appendix A.5MislabelledThe numbers are right, but those 1,536 bytes are the linker’s 512-byte heap and 1 KiB main-stack reservations, not headroom: SRAM has 0 bytes free for new static data.
§4README.md, CAPABILITY-REPORT.md and the demo README cite P25-HARDWARE-REVIEW.md, which doesn’t exist and was never committedREADME.md:157; CAPABILITY-REPORT.md:4; dm1701-p25-demo/README.md:84HoldsTrue of this clean-room copy, whose git history has no such file.
§4The demo README’s “patent-encumbered” IMBE note is outdated, as STAGE4 establisheddm1701-p25-demo/README.md; STAGE4-TX-FEASIBILITY.md:41–43WrongThe demo README has no patent note any more; the remark in STAGE4 is itself out of date.
§4Shared P25 files are byte-identical between the two trees, enforced by tests/p25/run.py:255tests/p25/run.py:253–255Holds
A.6DSD-FME’s P25P1_SYNC string matches the firmware’s p25_sync_plus tableref-sources/dsd-fme/include/dsd.h:1025; p25_tables.h:75–77Holds
A.6PTT isn’t masked while the monitor is active: applicationMain.c:607 honours only the global TX-inhibit settingapplicationMain.c:607, 1046–1051HoldsReal, though unrelated to decoding.
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. Qwen3.8 Flash joins Grok 4.6 and Muse Spark 1.3 Contributor on the filter chain and the microphone, and like every audit before it misses the vocoder CPU wall.

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
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
Capture parser certifies an incomplete streamabsentabsentabsentabsent
trusts the parser
absent
relies on the parser
absentabsent
pcm_starve never incrementsabsent
documented as working
absentabsentabsent
relies on it
absentraised
F7
raised
P1-7
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
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
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
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
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”
Manual: I2S frame clock “must be 8KHz” missedabsentraised
§3
absent
quotes the paragraph, not the rule
absentabsentraised
F4
absent
quotes the formulas, not the rule
One-layer 4FSK test mode as a P25 tappartly
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”
±10% health gate versus ±1% timing clampabsentabsentraised
B3, impact overstated
raised
P1-3
raised
F3
absentabsent
Fail-closed muting at LDU cadenceabsentabsent
late-entry mute only
absentabsent
calls it an asset
absentabsentpartly
“keep it”
Non-standard MFID mutes clear callsabsentabsentabsentabsentabsentabsentabsent
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
MCU runs at 72 MHzraisedraised
in passing
absentraised
P1-5
absentraised
F7
absent
Vocoder needs 11–16× the 72 MHz CPU missedpartly
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”
Direct discriminator tap (M17 mod)raisedraised
uncredited
raised
pins, timer ADC, 48 kS/s
raised
fallback, pin 9
raised
fallback
raised
fallback
raised
fallback
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
Two unverified SPI writes per decoded 20 ms frameabsentabsentabsentabsentabsent
treats them as protection
absentabsent
Capture sessions lack epochsabsentabsentpartly
measured=0 only
absentabsentraised
F9, 8 kHz under a 24 kHz header
absent
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
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
Clock-config writes bypass the verified SPI writerpartly
SPI retry note
absentraised
M1
absentabsentraised
F9
absent
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
Stale clear-call state releases a new call’s first framesabsentabsentabsentabsentabsentraised
F8, probe
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 clock-config rows were first raised by DeepSeek V4.1 Flash, the last two by UNIONALPHA.

Its plan

What to keep, change and add

Qwen3.8 Flash’s eight steps are small, ordered and mostly host-testable, and its one hardware session tests the sample source and the filter setting together. What it leaves out is everything on the CPU side.

Keep

Worth doing as written

  • Fix the capture export test-first, with a failing test on the stereo-row adapter before the stride fix, and count pcm_starve.
  • One scripted hardware session: a keyed carrier and a known tone across 0xE0 ∈ {C9, 89, 8B} × 0x34 ∈ {3C, 1C}, read back, exported and analysed offline, with a decision for each outcome.
  • The M17 discriminator tap if the codec path carries no RF.
  • Codec ownership: suspend hrc6000Task, and gate radioSetAudioPath and HRC6000SetMic while the monitor runs. That also shuts the squelch path it believes is already fixed.
Change

Would cause new problems

  • “Keep” fail-closed muting: latch an established clear call through a single RS failure, or one bad block mutes up to nine frames.
  • guessHz from LRCK alone: the measured rates follow min(BCLK/64, LRCK), which puts config 3 at 12,000 Hz, not 24,000.
  • A 48 kHz clock candidate for Phase 2: the manual says the I2S frame clock must be 8 kHz; 24 kHz already runs outside that.
  • A channel-model variant of p25_channel_generate(): it shapes with the receiver’s own RRC, so test the filters with standard C4FM instead.
Add

Missing from its plan

  • A real-time vocoder: a wrapped per-harmonic phase and a cosine table in mbelib. Without it no voice plays, whatever the front end does.
  • The one-layer probe it ruled out: 36 bytes per 30 ms is 4,800 symbols per second, P25 Phase 1’s own rate.
  • A strict capture parser that rejects streams missing their first or last chunk.

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

Right scope, wrong parts

Qwen3.8 Flash gets the shape of Phase 2 right: nothing is implemented, the demodulator is fixed at 4,800 symbols per second, there is no search for the 20-symbol sync (P25P2_SYNC), slots are 30 ms and 180 symbols, and the memory to add any of it isn’t there.

The stack it describes is not P25’s. BPTC(196,96) is DMR’s code, and the Phase 2 sources in DSD-FME use none; x2tdma_voice.c decodes Motorola’s DMR-derived X2-TDMA, not Phase 2; and Phase 2 voice is AMBE+2 half-rate, not IMBE. Its “1.6:1” modem mismatch rests on the 9,600-baud premise, and P25 group IDs are 16-bit in both phases.

The useful facts it leaves out: mbelib already contains the AMBE+2 half-rate decoder, but it shares the synthesis routine that is too slow for IMBE, while the radio’s licensed DVSI decoder already runs AMBE+2 in real time for DMR.

Against the other clean-room audits

Right on the front end, wrong on the architecture

Qwen3.8 Flash finds both halves of the filter chain and the microphone evidence, as Grok 4.6 and Muse Spark 1.3 Contributor do, and names the exact register fix. It also makes six wrong claims, more than any other clean-room audit, and like all of them misses the vocoder CPU wall.

UNIONALPHA clean-room review, graded C (74) →

Scores by dimension

Same rubric and weights. Hover or focus a bar for the score.

  • Muse Spark 1.3 Contributor
  • DeepSeek V4.1 Flash
  • GLM 5.3 Flash
  • Qwen3.8 Flash
0255075100Score out of 100Accuracy & evidence74736462Coverage of decode problems54493440Root cause & prioritisation72666268Fix plan & acceptance gates71706270Originality & attribution70745864Clarity & calibration80746870Weighted total68665659
Method and limits

How this was checked

I read QWEN38FLASH_REVIEW.md and checked each claim against the firmware sources, the OpenGD77 radio drivers, the translated HR-C6000 manual, the bundled DSD-FME sources and the repository’s notes. It saved no scripts, so its capture-layout simulation and clock arithmetic were checked by hand against the code and the manual.

Before grading I audited the run: one model for all 92 calls, no network, no skills or memory, and nothing read outside its copy of the repository. Its first attempt stopped after eight calls on a provider rate limit; this run gave the model extra retries, which changes how long the harness waits, not what the model sees. The de-emphasis and CPU results come from the model and emulator used for the Grok 4.6 audit.

  • Filter responses are modelled, not measured.
  • CPU figures come from instruction counts in an emulator, not timing on the radio.
  • No firmware was flashed, and no radio was touched.
grep -n "processX2TDMAvoice" ref-sources/dsd-fme/src/x2tdma_voice.c
# Motorola X2-TDMA, not P25 Phase 2

grep -n "Kbps" HR_C6000_user_manual.txt | head -1
# 4.8 and 9.6 kbps: DMR is 4,800 symbols per second

python3 -c "print(36*8/0.030/2)"
# one-layer mode: 4800.0 symbols per second

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