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
FDCBA
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.
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.
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.
“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.
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.
Rubric table
Dimension
Weight
Score
Points
Why
Accuracy & evidence
30%
62
18.6
21 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 problems
25%
40
10.0
Finds 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 & prioritisation
15%
68
10.2
Puts 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 gates
15%
70
10.5
Small, 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 & attribution
10%
64
6.4
Adds 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 & calibration
5%
70
3.5
A 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 total
100%
59
Grade 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.
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.
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.
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 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
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.
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
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.
Item
What Qwen3.8 Flash claims
Where
Result
Notes
Checks
The host suite exits 0 with 78 PASS lines, and the full cross-build gives an 810,252-byte image
tests/p25/run.py; make all PLATFORM=RT84_DM1701 VARIANT=DM1701
Holds
Matches the baseline run and build.
BLUF
The 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 line
Wrong
Emulated 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.
§1
The 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; §3
Wrong
DMR 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.
§1
The 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–2635
Overstated
The 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.
§1
The 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 one
The 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 first
STAGE3-BRINGUP.md RF survey
Holds
The right reading: silence says nothing about the sample path.
P1-2
The 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 exported
p25mon.c:443, 462–466
Holds
158 of 160 staged positions differ from what the decoder consumed. Its layout simulation wasn’t saved, but the arithmetic matches the code.
P1-2
The 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 faithful
The bug survived because the capture tests drive p25monCaptureSamples() directly with mono arrays: they validate the module, not the adapter in p25mon.c
Right: run.py compiles p25mon_capture.c but never p25mon.c’s callback.
P1-3
HRC6000SetFMRx 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 0x34
HR-C6000.c:2851; manual 2798–2824
Holds
P1-3
De-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 content
p25_4fsk.c; P1-3
Holds
In 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-3
The AT1846S analog profile enables filters (0x58=0xBC85) while the DMR profile disables them all (0x9CDD), and the monitor runs the analog set
AT1846S.c:191–217; p25mon.c:724
Holds
P1-3 fix
Write 0x34=0x1C (de-emphasis off, band-pass off, 3 kHz LPF) and try the AT1846S DMR 0x58 value, as candidates to measure
P1-3; R4
Holds
Exactly the safe change: clear bit 5 only, keeping away from the 2.55 kHz filter that 0x00 might select.
P1-4
HRC6000SetMic 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, 733
Holds
The driver’s comment on 0x89 is “Turn off Microphone input”.
P1-5
Squelch-open events rewriting 0x36/0x10 were “fixed by forcing squelch open once at start”
P1-5; STAGE3-BRINGUP.md:296–297
Wrong
The 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-5
p25monWriteReg can stall about 200 ms per contended register, and about ten per refresh, against a 21 ms ring whose overrun resets the whole stream
p25mon.c:624–656, 578–580
Holds
P1-5
Suspending hrc6000Task, the bring-up notes’ next step, is still not implemented, and APRS calls HRC6000SetMic(true) behind the monitor’s back
p25/ (no vTaskSuspend); aprs.c:530; STAGE3-BRINGUP.md:359–363
Holds
The APRS call is compiled for STM32 platforms.
P1-6
Clock 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 mismatch
p25mon.c:80–87; manual 1216–1221
Doubtful
The 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-6
48 kHz needs {0x2F=0x02, LRCK=0x007F}, the only pair meeting the manual’s CK>34·FS and clock≥6·CK constraints, exactly at the boundary
manual 1216–1221; Appendix A.4
Holds
The arithmetic checks out. The same manual sentence says the frame clock “must be 8KHz”, which the review never mentions.
P1-7
p25monAudioUnderruns is declared and printed but never incremented
p25mon.c:123, 517
Holds
P1-8
decode_link_control keeps talkgroups to 16 bits, so “24-bit P2 group IDs alias/truncate”
p25rx.h:56
Wrong
P25 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-8
Sync tolerates up to two dibit errors in 24, and camping needs at least two BCH-valid NIDs
p25rx.c:10, 324
Holds
§5
Golay, Hamming, RS, trellis and CRC16/32 are all cross-checked against independent reference encoders
tests/p25/signalling.py
Overstated
Golay, 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-8
Fail-closed muting means a noisy call stays muted rather than partially decoding, “the safe one — keep it”
p25rx.c
Overstated
One 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 2
Phase 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 machinery
Phase 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
x2tdma_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 2
Software 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
§3
Doubtful
The 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.
§3
Memory: 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 unused
MDUV380_FW.map; Appendix A.5
Mislabelled
The 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.
§4
README.md, CAPABILITY-REPORT.md and the demo README cite P25-HARDWARE-REVIEW.md, which doesn’t exist and was never committed
PTT isn’t masked while the monitor is active: applicationMain.c:607 honours only the global TX-inhibit setting
applicationMain.c:607, 1046–1051
Holds
Real, 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.
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
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
Capture parser certifies an incomplete stream
absent
absent
absent
absent trusts the parser
absent relies on the parser
absent
absent
pcm_starve never increments
absent documented as working
absent
absent
absent relies on it
absent
raised F7
raised P1-7
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
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
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
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
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”
Manual: I2S frame clock “must be 8KHz” missed
absent
raised §3
absent quotes the paragraph, not the rule
absent
absent
raised F4
absent quotes the formulas, not the rule
One-layer 4FSK test mode as a P25 tap
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”
±10% health gate versus ±1% timing clamp
absent
absent
raised B3, impact overstated
raised P1-3
raised F3
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”
Non-standard MFID mutes clear calls
absent
absent
absent
absent
absent
absent
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
MCU runs at 72 MHz
raised
raised in passing
absent
raised P1-5
absent
raised F7
absent
Vocoder needs 11–16× the 72 MHz CPU missed
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”
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
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
Two unverified SPI writes per decoded 20 ms frame
absent
absent
absent
absent
absent treats them as protection
absent
absent
Capture sessions lack epochs
absent
absent
partly measured=0 only
absent
absent
raised F9, 8 kHz under a 24 kHz header
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
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
Clock-config writes bypass the verified SPI writer
partly SPI retry note
absent
raised M1
absent
absent
raised F9
absent
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
Stale clear-call state releases a new call’s first frames
absent
absent
absent
absent
absent
raised 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.
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
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.
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
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