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

Qwen3.8 Max’s P25 review, graded

Qwen3.8 Max 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 spent five hours reading before it wrote a word. I checked its claims against the code, the manual, the bundled DSD 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
63 / 100 weighted

Best case yet for the microphone. Never looks at the CPU.

Qwen3.8 Max writes the clearest microphone argument of the seven audits. The monitor’s own 0xE0=0xC9 sets the bit HRC6000SetMic() uses, undoing the stock Turn off Microphone input, and the stream it reads is the buffer the stock firmware fills from the microphone. It makes settling that question step one, with a stop rule: if the capture tracks the microphone, stop working on the codec path and tap the discriminator instead. It is also the only audit to check the decoder against a reference itself: the IMBE interleave, the C4FM sync and the trellis transition matrix all match DSD byte for byte.

Then it clears the wrong thing. The Phase-1 bit-level decoder is not the problem covers IMBE synthesis, which needs 11–16 times the 72 MHz CPU; the review never looks at CPU at all. It calls the 24 kHz capture far too low for C4FM, though that is five samples per symbol and the firmware resamples to 38.4 kHz with no frames lost, and it trusts a capture tool that exports the wrong halfwords. 21 of 27 claims hold. It finds two of the four decode-critical issues and reports nothing about the instruments, which puts it at D.

Claims that check out
21 / 27
4 overstated or doubtful · 2 wrong
Decode-critical issues found
2 of 4
misses the 8 kHz rule; clears the decoder
CPU the vocoder needs
11–16×
of 72 MHz; it calls the decode chain correct
Frames lost to the resampler
0 of 180
the 24 kHz rate it calls far too low

The run, not graded: 5 h 10 min · 71 agent steps · 111 tool calls · 598K output tokens (583K reasoning) · 12.0M tokens re-read · Alibaba Cloud Token Plan, interactive

Headline findings

Six things to know about this review

Holds up

The clearest microphone case of the seven

The monitor writes 0xE0=0xC9, which sets the bit HRC6000SetMic() toggles, immediately after the stock FM setup wrote 0x89, commented “Turn off Microphone input”. The buffer it reads is the one the stock firmware fills from the microphone, labelled “only use the Left Channel of the Mic Audio”. The manual frames I2S as external-codec and vocoder plumbing and ends FM receive at the speaker, and the stock-firmware analysis found no discriminator route either. The review credits both.

p25mon.c:443, 648, 733 · HR-C6000.c:2849, 2964 · sound.c:444 · manual 1109–1138, 2794–2806

Holds up

It checked the decoder against DSD itself

Most audits take the test suite’s word for the decoder. This one compared the tables: the IMBE interleave iW/iX/iY/iZ, the C4FM sync and its inverse, and the trellis dibit-transition matrix all match the bundled DSD sources byte for byte. I reran those comparisons and they hold, including the trellis table the project’s own test re-implements rather than checks. Its build and host-suite results match the baseline too.

p25_tables.h · p25_fec.c:178 · ref-sources/dsd-fme/src/p25_12.c · ref-sources/dsd-fme/include/p25p1_const.h

Strong plan shape

It answers the decisive question first, and says when to give up

Step one is the RF-versus-microphone capture contrast with a written branch: if the stream tracks the microphone, stop working on the codec I2S path and go to the discriminator tap. No other audit states the abandon condition. It also proposes the chip’s one-layer mode as a symbol source, the documented hrc6000Task suspension, and a real C4FM capture kept as a replay fixture so the decoder is finally tested against something other than its own generator.

STAGE3-BRINGUP.md:134, 359–363 · manual 2552–2638 · CAPABILITY-REPORT.md

Critical error

It clears the decoder that cannot keep up

“The Phase-1 bit-level decoder is not the problem … the sync/NID/framing/FEC/IMBE path is correct and well tested.” The bit-level parts are correct, and it proved several of them. IMBE synthesis is in that same path, and emulated it needs 11–16 times the radio’s 72 MHz CPU: 134–458 ms for one voiced 20 ms frame. The review never measures CPU, never mentions the 72 MHz clock, and its plan never reaches the vocoder.

QWEN38MAX_REVIEW.md verdict · mbelib.c:306 · Core/Src/main.c:269–274

Critical error

It chases a sample rate that is already fine

Its second problem is that 24 kHz is “far too low for C4FM”, “the bare minimum that clears Nyquist”, with “almost no margin for the ±3600 Hz C4FM spectrum”. C4FM at 4800 symbols per second occupies under 2.88 kHz of baseband at ±1.8 kHz deviation, 24 kHz is five samples per symbol, and the firmware resamples to 38.4 kHz before the matched filter. In the model that resampled path decodes exactly the frames the direct path does. The rate is not the blocker; the de-emphasis it names next is.

p25mon.c:9, 575–577 · p25_resample.c · resampler_check.py

Major gap

It trusts the instruments and never audits them

Its first step depends on the c capture and tools/p25_capture.py, which it calls “already validated”. The capture exports the first 80 interleaved halfwords of each row instead of the decoder’s left-channel samples, so 158 of 160 positions differ, and the parser passes streams missing their first or last chunk. It is also the only audit to report nothing about the tooling at all: no starvation counter, no memory budget, no capture epochs, no ring-and-tick arithmetic.

p25mon.c:123, 443, 460–466, 517 · tools/p25_capture.py:104–190 · MDUV380_FW.map

Scorecard

How the 63 breaks down

Same rubric, weights and scale as every P25 audit on bench, so scores compare directly. Qwen3.8 Max scores like the stronger audits on root cause, the plan and clarity, and last on coverage.

Dimension scores

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

0255075100Score out of 100Accuracy & evidenceweight 30%70Coverage of decode problemsweight 25%39Root cause & prioritisationweight 15%71Fix plan & acceptance gatesweight 15%71Originality & attributionweight 10%66Clarity & calibrationweight 5%78
Rubric table
DimensionWeightScorePointsWhy
Accuracy & evidence30%7021.021 of 27 claims hold, and its own cross-checks against DSD hold up: the IMBE interleave, the sync and the trellis matrix all match. Two are wrong and both steer the work: the decode chain “is not the problem”, and 24 kHz is “far too low for C4FM”.
Coverage of decode problems25%399.8Finds the microphone evidence and the de-emphasis that sits in front of the capture, keeps the one-layer symbol path and the discriminator tap as real options, and is accurate on Phase 2. It reports nothing about the instruments — the capture, the parser, starvation, memory, epochs and the tick budget — and misses the 8 kHz rule and the vocoder wall.
Root cause & prioritisation15%7110.7Names the sample source as the root cause, puts the experiment that settles it first and writes the branch to abandon the codec path if it answers microphone. But it clears the decoder without measuring it and puts the sample rate second, where the filters belong.
Fix plan & acceptance gates15%7110.7Ordered around the decisive question, with a stop rule, the exact de-emphasis change, the one-layer probe, hrc6000Task suspension and a replay fixture from real capture. It rests step one on a capture tool that exports the wrong samples, and never reaches the vocoder.
Originality & attribution10%666.6Its own reference cross-checks and the stop-rule branch are real additions, and it credits the bring-up notes and stock report it leans on. Most of the rest restates those documents, and the microphone case was made first by Grok 4.6.
Clarity & calibration5%783.9Short, ordered and readable, with a summary table and an honest scope line, and it separates what it executed from what it inferred. Two of its confident statements are wrong.
Weighted total100%63Grade 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 Max’s findings sit at the front of the chain: the microphone bit, the inherited de-emphasis, the codec contention and the decoder tables it verified. The red tags are the vocoder it cleared, the sample rate it misread, and the whole diagnostic branch it never looked at.

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 tapACVEN1N2N3N4N5N6N7N8
A raised by Qwen3.8 MaxN1 missed or misreadDashed edge: no documented connection
A0xE0=0xC9 turns the microphone on; I2S is vocoder plumbing
N1mbelib needs 11–16× the 72 MHz CPU (“not the problem”)
CInherited 0x34=0x3C: de-emphasis and a voice low-pass
N2Calls 24 kHz too low; the firmware resamples to 38.4 kHz
VDecoder tables verified against DSD
N3The capture stages the wrong halfwords
EEvery passing test is synthetic
N4The parser certifies incomplete captures
N5pcm_starve never counts
N6Ring and tick real-time budget
N7Manual: the I2S frame clock must be 8 kHz
N8AT1846S voice filters and low-frequency bit
The front end, checked

Right about de-emphasis, silent on the AT1846S

Qwen3.8 Max names the inherited 0x34 and proposes clearing bit 5, which is the safe change. It never looks at the AT1846S register set the monitor also loads, where the voice filters and the low-frequency bit sit.

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, −6 dB per octave from 300 Hz, leaves no frames on its own, which is what the review’s 0x34 fix removes. The 3 kHz low-pass it also blames costs nothing; the 2.55 kHz option is the one that bites.
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 rate it chased

Twenty-four kilohertz is not the problem

The review puts the sample rate second among its Phase 1 problems and would rather have 48 kHz or symbols straight from the chip. The firmware already resamples to 38.4 kHz, and the model shows what that costs.

What 24 kHz actually costs

Standard C4FM into the firmware’s demodulator and framer, once directly at 38.4 kHz and once band-limited to the codec’s 24 kHz, then upsampled by the firmware’s own p25_resample() in 32-sample chunks, as the monitor does.

  • Direct at 38.4 kHz
  • 24 kHz through p25_resample
0100200300400500Symbol errors per 100,000 symbolsNoise-free0016 dB SNR4413 dB SNR996811 dB SNR448372
Both paths decode the same frames in every trial, and the resampled path never has more symbol errors. Five samples per symbol at 24 kHz is enough, and the rate the review calls “far too low” costs nothing.
Data table
NoiseDirect: frames per trialDirect: symbol errorsVia 24 kHz: frames per trialVia 24 kHz: symbol errors
Noise-free1800 / 17,2001800 / 17,200
16 dB SNR180, 180, 1802 / 51,600180, 180, 1802 / 51,600
13 dB SNR180, 180, 18051 / 51,600180, 180, 18035 / 51,600
11 dB SNR180, 180, 171231 / 51,600180, 180, 171192 / 51,600

180 IMBE frames possible per trial. Three noise seeds per SNR; one noise-free trial.

The CPU budget

The path it called correct

The review checked the decoder’s tables and declared the chain sound. The emulated budget shows where a second of decoding actually 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

27 claims, checked one by one

Twenty-one hold, including every register, line and reference table it cites and the cross-checks it ran itself. Four are overstated or doubtful. Two are wrong, and both would steer the work: the decoder it clears and the sample rate it chases.

ItemWhat Qwen3.8 Max claimsWhereResultNotes
VerifiedBoth trees build with the bundled toolchain: the demo (ELF text 93 KB) and the firmware at 810,252 bytesmake; build/OpenGD77_RT84_DM1701_DM1701.binHoldsText is 93,108 bytes and the image matches the reference size.
Verifiedpython3 tests/p25/run.py exits 0 with “PASS all native P25 regression tests” and selftest 111111 for both treestests/p25/run.pyHoldsMatches the baseline run.
VerifiedThe IMBE interleave tables iW/iX/iY/iZ match DSD byte for bytep25_tables.h; ref-sources/dsd-fme/include/p25p1_const.hHoldsAll four 72-entry tables are identical.
VerifiedThe C4FM sync 111113113311333313133333 and its inverse match DSDp25_tables.h:75–77; dsd-fme/include/dsd.h:1024–1025Holds
VerifiedThe trellis de-interleave and dibit-transition matrix match dsd-fme/src/p25_12.c exactlyp25_fec.c:178–186; dsd-fme/src/p25_12.cHoldsThe transition table equals DSD’s p25_dtm byte for byte, and the stride-eight de-interleave reproduces DSD’s p25_interleave. The only audit to check the trellis against a reference rather than the suite.
VerifiedBCH-NID, Golay, Hamming, RS and CRC16/32 are validated against independent DSD encoders in tests/p25tests/p25/signalling.py; fec_reference.cppHoldsRight, and it leaves the trellis out of that list, which is the one the suite re-implements.
Verdict“The Phase-1 bit-level decoder is not the problem”: the sync, NID, framing, FEC and IMBE path is correct and well testedQWEN38MAX_REVIEW.md verdictWrongThe bit-level parts hold up, but IMBE synthesis is inside that path: emulated, mbelib needs 11–16× the radio’s 72 MHz CPU, 134–458 ms per voiced 20 ms frame. The review never looks at CPU time or mentions 72 MHz.
AThe monitor reads i2s_Rx_Buffer, the buffer the stock path fills from the microphone (“only use the Left Channel of the Mic Audio”), through the same I2S callbackssound.c:444; p25mon.c:443; i2s.c:59–92Holds
AThe monitor writes 0xE0=0xC9, which sets the mic-into-LineIn1 bit HRC6000SetMic toggles, while the stock FM setup writes 0x89, “Turn off Microphone input”p25mon.c:648, 733; HR-C6000.c:2849, 2964HoldsThe clearest statement of the microphone case in any audit of this tree.
AThe manual frames I2S as external-codec and vocoder PCM and shows FM receive ending at the speaker DAC, with no documented discriminator route to I2S; the stock analysis agrees and calls the 0xE0 bits community reverse-engineeringmanual 1109–1138, 480–540, 2794–2806; analysis/stock-p25/REPORT.mdHoldsCredited to the stock report.
AThe microphone hypothesis is “exactly why” the RF survey sees carriers but zero P25 syncsSTAGE3-BRINGUP.md RF surveyOverstatedThe survey never confirmed P25 traffic on those frequencies, so zero syncs is not evidence either way. The rest of its microphone case stands on its own.
BThe I2S sample rate is “far too low for C4FM”: 24 kHz is “the bare minimum that clears Nyquist” and leaves almost no margin for the “±3600 Hz C4FM spectrum”QWEN38MAX_REVIEW.md §BWrong24 kHz is five samples per symbol, and the firmware resamples to 38.4 kHz, which costs no frames in the model. C4FM occupies under 2.88 kHz of baseband at ±1.8 kHz deviation. The rate is not a decode blocker, and its own text says the DSP resamples.
BNative codec rate is 8 kHz (config 0); the monitor forces 24 kHz with 0x2F=0x03 and LRCK 0x00FF; decoding is gated off below 12 kHz; the DSP resamples to 38.4 kHzp25mon.c:82, 84, 575–577Holds
BHolding 24 kHz is fragile: the slot engine must stay off, eco power saving disabled, squelch forced open, and SPI writes verified and retried because they fail silentlyp25mon.c:624–656, 711–716; STAGE3-BRINGUP.mdHolds
CHRC6000SetFMRx writes 0x34=0x3C, leaving de-emphasis (bit 5) and the voice low-pass in front of whatever is captured, and the monitor never changes 0x34HR-C6000.c:2851; manual 2794–2824Holds
CA 3 kHz low-pass plus de-emphasis collapses the four-level eye, because the C4FM symbol spectrum “extends past 4 kHz”QWEN38MAX_REVIEW.md §COverstatedDe-emphasis alone leaves 0 of 180 frames, so the conclusion is right about that stage. But C4FM sits below 2.88 kHz and the 3 kHz filter alone costs no frames in the model; the 2.55 kHz option is the one that bites.
DEco power saving power-cycles the C6000 and averages the I2S rate to about 54% of nominal; the fix is applied, but hrc6000Task and the squelch path still write codec registers while the monitor runsSTAGE3-BRINGUP.md:250, 274; p25mon.c:660–676, 704–716HoldsIt names the squelch path as a live writer, which is the direction of the defect UNIONALPHA proved: with squelch forced open, the stock path re-arms whenever the amp is off and rewrites 0x10=0x80.
DThe self-heal loop thrashes (“heal every ~5 s without reaching a stable streak”), and the documented next step, suspending hrc6000Task, is not implementedSTAGE3-BRINGUP.md:280, 359–363; p25mon.c:1089–1113HoldsQuoted exactly; no vTaskSuspend anywhere in the P25 sources.
EEvery passing test is synthetic: p25_channel_generate builds the baseband in software, the suite’s own header says synthetic payload bits test transport, and the docs admit RF reception is unverifiedp25_4fsk.c:295; tests/p25/run.py:8; CAPABILITY-REPORT.md; README.mdHoldsIt stops short of the circularity: the generator shapes with the receiver’s own RRC filter, which hides about 2 dB against standard C4FM.
Phase 2Nothing of Phase 2 exists: no CQPSK demodulator, no TDMA framing, p25rx.c calls only the Phase 1 IMBE path, the trunk follower rejects TDMA identifiers, and the AMBE+2 decoder is compiled but never calledp25rx.c:105; p25_trunk.c:26; mbelib/ambe3600x2450.cHoldsRight, including that the AMBE+2 decoder is present. Only GLM 5.3 Flash got that backwards.
Step 1Settle the routing question with the c capture and tools/p25_capture.py, which “were built exactly for this and are already validated”p25mon.c; tools/p25_capture.py; STAGE3-BRINGUP.md:134IncompleteThe right first experiment with a clear stop rule, but the capture stages the first 80 interleaved halfwords instead of the decoder’s [j][i*2] samples, and the parser certifies streams missing their first or last chunk. Both need fixing or the contrast proves nothing.
Step 2Program 0x34 with de-emphasis off (bit 5 = 0) before trusting any captureHR-C6000.c:2851; manual 2798–2824HoldsThe safe register change, and the one the model supports.
Step 2One-layer mode delivers “raw demodulated 4FSK” to receive RAM, 36 bytes per frame at 0x30, with 0x40=0x40 and 0x41=0x41, and is “the documented digital path for C4FM symbols”manual 2552–2638, Table 5.6OverstatedThe values and the RAM address are right and the idea is a real opening, but the manual documents 36-byte frames typed by DMR sync class in DMR burst geometry, and the recipe omits 0x10=0x02 and the 0x07–0x09 IF word. Whether it emits frames without DMR sync is untested.
Step 3Suspend hrc6000Task while the monitor is active and feed its AliveCount from p25monTick, so no other writer touches codec registersSTAGE3-BRINGUP.md:359–363HoldsThe bring-up notes’ own next step, credited.
Step 4Capture a known C4FM source, measure deviation scale, DC offset, polarity and filter response, and add the capture to tests/p25 as a replay fixtureStep 4HoldsThe independent oracle the suite lacks, though it doesn’t say the current generator shares the receiver’s filter.
Step 5If no discriminator route exists, follow the M17 precedent: tap AT1846S pin 9 into an STM32 ADC and leave the C6000 for speaker audioCAPABILITY-REPORT.md; analysis/stock-p25/REPORT.md:175–176HoldsCredited to the repository’s own notes.
Step 6Phase 2 later, using the already-present mbe_processAmbe3600x2450Frame for voicembelib/ambe3600x2450.cDoubtfulThat decoder exists, but it shares the synthesis routine that puts IMBE at 11–16× the CPU, and the radio already runs a licensed DVSI AMBE+2 decoder for DMR in real time.
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 Max makes the microphone case most firmly and keeps the one-layer path and the discriminator tap open, but it is the only audit with no tooling findings at all.

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
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
Capture parser certifies an incomplete streamabsentabsentabsentabsent
trusts the parser
absent
relies on the parser
absentabsentabsent
relies on the parser
pcm_starve never incrementsabsent
documented as working
absentabsentabsent
relies on it
absentraised
F7
raised
P1-7
absent
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
absent
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
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
AT1846S FM filters, low-frequency bit, 25 kHz missedpartly
“require characterization”
raised
§2, register level
raised
B2, filter register
raised
P1-2, 0x58 filters
partly
“voice filtering”; wrong bandwidth premise
partly
cited, called unmeasured
raised
P1-3, DMR 0x58 probe
absent
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
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
absent
cites the section, 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”
raised
step 2, a symbol source
±10% health gate versus ±1% timing clampabsentabsentraised
B3, impact overstated
raised
P1-3
raised
F3
absentabsentabsent
Fail-closed muting at LDU cadenceabsentabsent
late-entry mute only
absentabsent
calls it an asset
absentabsentpartly
“keep it”
absent
Non-standard MFID mutes clear callsabsentabsentabsentabsentabsentabsentabsentabsent
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
MCU runs at 72 MHzraisedraised
in passing
absentraised
P1-5
absentraised
F7
absentabsent
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”
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
raised
step 5, the likely answer
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
Two unverified SPI writes per decoded 20 ms frameabsentabsentabsentabsentabsent
treats them as protection
absentabsentabsent
Capture sessions lack epochsabsentabsentpartly
measured=0 only
absentabsentraised
F9, 8 kHz under a 24 kHz header
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
absent
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
absent
Clock-config writes bypass the verified SPI writerpartly
SPI retry note
absentraised
M1
absentabsentraised
F9
absentabsent
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
Stale clear-call state releases a new call’s first framesabsentabsentabsentabsentabsentraised
F8, probe
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 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 Max’s six steps are ordered around one question and say what to do with either answer. What they rest on, and what they leave out, is where they lose.

Keep

Worth doing as written

  • The RF-versus-microphone contrast first, with its branch: if the stream tracks the microphone, stop working on the codec path.
  • Clear 0x34 bit 5 before trusting any capture: de-emphasis is the stage that erases the eye.
  • The one-layer symbol probe and the M17 discriminator tap as the two routes that don’t depend on the codec’s audio path.
  • Suspend hrc6000Task while the monitor owns the codec, and keep a real C4FM capture as a replay fixture.
Change

Would cause new problems

  • Step one’s instruments: fix the capture export and the parser first, or the contrast it rests on proves nothing.
  • “Raise and stabilise the sample rate”: 24 kHz is five samples per symbol and the resampler costs no frames. Spend that effort on the AT1846S filter set it never names.
  • Phase 2 voice through mbelib’s AMBE+2: that decoder shares the synthesis routine that is 11–16× too slow, and the radio already runs a licensed AMBE+2 decoder for DMR.
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 capture carries.
  • The AT1846S DMR register set (0x58=0x9CDD, 0x40=0x0031, 12.5 kHz) alongside the 0x34 change.
  • The tooling it skipped: counted starvation, capture epochs, a strict parser, and the memory budget that decides what can be added.

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

Accurate, and rightly brief

Its Phase 2 section is short and correct: no CQPSK demodulator, no TDMA framing, p25rx.c calls only the Phase 1 IMBE path, and the trunk follower rejects TDMA identifiers. It is also the only audit besides UNIONALPHA to get the vocoder inventory right, noting that mbelib’s AMBE+2 decoder is compiled in but never called.

The one thing to change is what it would do with that decoder. mbelib’s AMBE+2 path shares the synthesis routine that puts IMBE at 11–16× the CPU, while the radio already runs a licensed DVSI AMBE+2 decoder for DMR in real time. The vocoder is not the hard part of Phase 2; the demodulator, framing and descrambling are.

Against the other clean-room audits

Deepest on the source, thinnest on the tools

Qwen3.8 Max and Grok 4.6 make the same microphone argument, and Qwen3.8 Max makes it in more detail and adds the stop rule. It also spent five hours and 598K output tokens to do it, and returned nothing about the capture, the parser, starvation or memory, where UNIONALPHA and DeepSeek V4.1 Flash found real defects.

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

Scores by dimension

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

  • Qwen3.8 Flash
  • Grok 4.6
  • UNIONALPHA
  • Qwen3.8 Max
0255075100Score out of 100Accuracy & evidence62749070Coverage of decode problems40596539Root cause & prioritisation68706071Fix plan & acceptance gates70757071Originality & attribution64727666Clarity & calibration70788078Weighted total59707463
Method and limits

How this was checked

I read QWEN38MAX_REVIEW.md and checked each claim at its cited lines in the firmware sources, the OpenGD77 drivers, the translated HR-C6000 manual, the bundled DSD and DSD-FME sources and the repository’s notes. Its three reference cross-checks were rerun: the IMBE interleave tables, the C4FM sync and the trellis transition matrix all match DSD, as it says.

Before grading I audited the run: one model for all 71 steps, no network, no skills or memory, and nothing read outside its copy of the repository. It ran as an interactive session on the Alibaba Token Plan, whose terms allow interactive use only; the prompt, tools and sandbox were the same as every other run. The de-emphasis, resampler 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.
  • Interactive runs write no usage file; its run facts come from the session history.
  • No firmware was flashed, and no radio was touched.
python3 -c "import re;..."   # compare p25_fec.c transition[] with dsd-fme p25_dtm[]
# {2,12,1,15,14,0,13,3,9,7,10,4,5,11,6,8} in both

DM1701_REPO=/path/to/DM-1701 python3 resampler_check.py
# 24 kHz through p25_resample decodes the same frames as 38.4 kHz direct

DM1701_REPO=/path/to/DM-1701 python3 deemph_check.py
# de-emphasis alone 0/180 frames · 3 kHz low-pass 180/180

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