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

DeepSeek V4.1 Flash’s P25 review, graded

DeepSeek V4.1 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 real code: the host suite, a firmware build, its own FEC harness and filter experiments. I checked its claims against the code, the manual and the repository’s notes, reran its FEC harness, and put its filter claims through the firmware’s demodulator.

Overall grade
C−
66 / 100 weighted

Finds the microphone and the filters. Waves de-emphasis through.

DeepSeek V4.1 Flash does careful work on the radio side. It makes the microphone the default explanation for the I2S stream and designs a half-day test to settle it, it names the AT1846S voice filters as destructive, and it builds a codec-clock model from the manual that reproduces all four measured sample rates and predicts that one untested clock setting is 2.4% off its assumed rate. Its protocol checks hold up too: exact status-symbol phase against DSD, a clean FEC harness and a reproducible 810,252-byte build.

It stops short in three places. It calls HR-C6000 de-emphasis “by itself, benign”, yet that one stage leaves zero of 180 voice frames in my model, and its own high-pass table doesn’t reproduce. It never looks at CPU, where mbelib needs 11–16 times the 72 MHz core, and it misses the manual’s 8 kHz frame-clock rule and the capture bug its first experiment depends on. 28 of 35 claims hold. That lands it at C−, four points behind Grok 4.6 on the same clean-room protocol.

Claims that check out
28 / 35
4 miscited, overstated or doubtful · 3 wrong
Decode-critical issues found
2 of 4
misses the CPU wall and the 8 kHz rule
Voice frames with de-emphasis on
0 / 180
the stage it calls benign, in my model
Frames through a 30 Hz high-pass
180 / 180
its table says 0; first-order, in my model
Headline findings

Six things to know about this review

Holds up

It sets up the right microphone test

The monitor reads the I2S slot OpenGD77 labels “Mic Audio”, and writes 0xE0=0xC9, the value whose bit 6 feeds the microphone into LineIn1. DeepSeek calls that the default explanation and designs the test: four 0xE0 values, each against a keyed carrier and a mic tap, with a stated decision rule and a half-day time box. It also names the AT1846S FM register set, 0x58=0xBC85 with its high- and low-pass filters, as destructive, and puts the unfiltered DMR-style setting in its fix.

p25mon.c:443, 648, 733 · sound.c:444 · HR-C6000.c:2962–2965 · AT1846S.c:191–217

New idea

Its clock model explains the measured rates

From the manual’s divider formulas and a 12.288 MHz codec clock, it models the sample rate as the lower of LRCK and BCLK/64. That reproduces all four rates measured on the radio, including the no-op config, and explains that config by the manual’s I2S_CK_M > 34 × I2S_FS_M rule. It predicts config 3 runs at 12,000 Hz, not the 12,288 Hz the code assumes: 2.4% off, inside the ±10% gate but outside what the demodulator tracks. It also finds the clock writes bypass the verified SPI writer, and that OpenGD77’s one-layer interrupt handler is an empty stub.

manual 1126–1133, 1216–1221 · p25mon.c:80–87, 550–552 · STAGE3-BRINGUP.md:376–382 · HR-C6000.c:1265–1267

Critical error

It calls the stage that erases C4FM benign

“FM de-emphasis is a low-pass and, by itself, benign.” The inherited 0x34=0x3C switches it on, −6 dB per octave from 300 Hz, and in my model that one stage leaves 0 of 180 voice frames. Its plan does turn de-emphasis off, but for the wrong reason. Its measured high-pass table doesn’t reproduce either: a first-order 30 Hz high-pass decodes 180/180 through the same demodulator, where it reports none. Voice high-passes still kill C4FM, from about 100 Hz.

DEEPSEEK41_CLEAN_REVIEW.md B2, §2 · HR-C6000.c:2851 · manual 2806 · rerun: firstorder_check.py

Critical miss

It never looks at CPU

“Every arrow except the first is implemented and unit-tested,” and “the vocoder question is settled.” Implemented, yes, but 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 even a perfect front end produces no real-time audio. It also quotes the manual paragraph that holds the I2S frame-clock rule, “must be 8KHz”, for its divider formula and never mentions the rule.

DEEPSEEK41_CLEAN_REVIEW.md §3, §7 · mbelib.c:306 · manual 1217–1219

Major gap

Its first experiment depends on a broken instrument

Step 0 settles the microphone question with the existing c capture. That capture stages the first 80 interleaved halfwords of each DMA row, while the decoder reads every other one: 158 of 160 positions differ from what the demodulator sees. The review never mentions it. It also misses the parser that certifies incomplete captures, the pcm_starve counter that never counts, and both muting policies.

p25mon.c:443, 460–466 · tools/p25_capture.py · p25rx.c:159–181

Overstated

It pushes rate discipline too far

The ±10% gate against a ±1% clamp is a real mismatch. But “tolerance ≈ ±0.1%” is too tight: its own sweep decodes 89 of 90 at 0.5%, and my knee rerun decodes every frame within ±0.4%. The default 24 kHz clock is an exact crystal divide. Its fix would hurt: the 1 Hz rate report is quantised to about ±0.67%, not 0.1%, so a ±0.5% gate would flap and reset the stream, and steering the resampler from that report adds error to a rate that is already exact. Correcting config 3 to 12,000 is right.

p25mon.c:574–577, 1151–1156 · p25_4fsk.c:240–241 · knee rerun

Scorecard

How the 66 breaks down

Same rubric, weights and scale as every P25 audit on bench, so scores compare directly. DeepSeek V4.1 Flash matches Grok 4.6 on originality and comes close on accuracy, but finds one fewer decode-critical issue and gives most of the difference back on coverage.

Dimension scores

Hover or focus a bar for the reasoning. Weighted total 66, grade C−.

0255075100Score out of 100Accuracy & evidenceweight 30%73Coverage of decode problemsweight 25%49Root cause & prioritisationweight 15%66Fix plan & acceptance gatesweight 15%70Originality & attributionweight 10%74Clarity & calibrationweight 5%74
Rubric table
DimensionWeightScorePointsWhy
Accuracy & evidence30%7321.928 of 35 claims hold, with exact line citations, a reproducible build and FEC harness, and a clock model that matches every measured rate. Three are wrong: de-emphasis “benign” (0/180 frames), a high-pass table that doesn’t reproduce, and a 0.1% rate resolution that is really ±0.67%.
Coverage of decode problems25%4912.2Finds the microphone route and the AT1846S filter set, the one-layer mode, the M17 tap, the gate/clamp mismatch and RAM exhaustion, plus new clock-path findings. It misses the manual’s 8 kHz rule, the vocoder CPU wall, the capture staging bug its own first experiment depends on, and both muting policies.
Root cause & prioritisation15%669.9Puts the microphone question and the filter set first, both right. Then promotes a low-impact rate gate to a third blocker, clears de-emphasis and never considers CPU.
Fix plan & acceptance gates15%7010.5A time-boxed microphone matrix with a decision rule, a well-specified M17 ADC tap and a cheap one-layer probe are decisive. But its first experiment uses a capture that stages the wrong samples, a ±0.5% gate would flap on a ±0.67% rate report, and it says not to fix the test oracle.
Originality & attribution10%747.4Adds work no repository document has: a codec-clock model that predicts config 3’s real rate, unverified clock writes, the empty physical-layer interrupt stub, and one-layer mode as a symbol tap. It credits the stock report, bring-up notes and capability report, with two small miscitations.
Clarity & calibration5%743.7Well organised, with severity labels, decision rules and reproduction notes. Undercut by confident overreach: “the vocoder question is settled”, “can never decode”, de-emphasis “by itself, benign”.
Weighted total100%66Grade 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

DeepSeek V4.1 Flash’s findings sit in the radio front end and on the clock path: the microphone input, the AT1846S filters, the sample-rate gate and the one-layer route all carry amber tags. The red tags show what it missed or got wrong: de-emphasis on the HR-C6000, the 8 kHz rule, the capture staging bug and everything that happens once samples become voice.

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-bitHR-C6000 4FSK demodulatorone-layer test mode → RX RAM 0x30RFaudio?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 amplifierSPI RX-RAM reader36 B per 30 ms = 4,800 sym/sproposed, not builtCapture export ‘c’ISR stages [j][0..79]→ USB → p25_capture.pydiagnostic tapdemodulated bits over SPIdibits straight to p25rxB1M4B2B3B5M3N1N2N3N4N5N6N7
B1 raised by DeepSeek V4.1 FlashN1 missed or wrongpath its proposed one-layer routeGrey dashed edge: no documented connection
B1I2S most likely carries the microphone; a four-value 0xE0 test decides it
N1HR-C6000 de-emphasis: it calls the stage benign; 0/180 frames
B2AT1846S FM filter set (0x58=0xBC85) destroys the symbol waveform
N2Manual: the I2S frame clock must be 8 kHz
B3±10% gate versus ±1% clamp, a codec-clock model, config 3 at 12,000 Hz
N3mbelib needs 11–16× the 72 MHz CPU
B5One-layer 4FSK test mode as a symbol tap; its interrupt is an empty stub
N4The capture its first experiment uses stages the wrong halfwords
M3Capture header hard-codes measured=0
N5The test waveform shares the receiver’s RRC; it says not to fix it
M4The monitor’s 0xE0=0xC9 selects the microphone
N6Fail-closed and non-standard MFID muting
N7Two unverified SPI writes per decoded frame; pcm_starve never counts
The front end, checked

Right about the filters, wrong about de-emphasis

DeepSeek V4.1 Flash reads the AT1846S register sets correctly and draws the right conclusion from them. On the HR-C6000 side it lists de-emphasis among the FM stages but calls it harmless, and its measured high-pass limits are far tighter than the firmware’s demodulator actually is.

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 as the manual describes, is fatal on its own. DeepSeek V4.1 Flash calls that stage “by itself, benign”.
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

Its high-pass table, rerun

Standard C4FM through one-pole filters, then the firmware’s live 768/512 demodulator and framer, 20 LDUs (180 IMBE frames). Its own harness wasn’t saved, so this reruns the claim, not the script.

  • Its reported table
  • Firmware model
0%25%50%75%100%Frames decoded (share of the stream)High-pass 10 Hz30100High-pass 30 Hz0100High-pass 100 Hz010High-pass 300 Hz00Low-pass 2 kHz100100
The direction holds: voice-style high-passes destroy C4FM. The numbers don’t: a first-order high-pass costs nothing until about 100 Hz, where its table already reports total loss at 10–30 Hz. Its “benign” de-emphasis, a one-pole low-pass at 300 Hz, decodes nothing (last table row).
Data table
First-order filterIts tableModel: frames · NIDsModel: symbol errorsModel at 20 dB
High-pass 10 Hz27/90180/180 · 20/200.00%180/180
High-pass 30 Hz0/90180/180 · 20/200.03%180/180
High-pass 100 Hz0/9018/180 · 2/207.07%27/180
High-pass 300 Hz0/900/180 · 0/2029.90%0/180
Low-pass 2 kHz90/90180/180 · 20/200.00%180/180
Low-pass 300 Hz (de-emphasis)“benign”0/180 · 0/2051.32%0/180
The clock path

Its strongest new work

No repository document derives the monitor’s sample rates from the manual. DeepSeek V4.1 Flash does, and its model survives checking against every rate the bring-up notes measured on the radio.

Its codec-clock model against the radio

Codec clock 12.288 MHz. BCLK = clock / (2 × (0x2F + 1)); LRCK = clock / (2 × ({0x32, 0x33} + 1)); modelled rate = the lower of LRCK and BCLK / 64. Measured rates from the bring-up notes.

Config0x2F0x32:0x33BCLKLRCK (Hz)Model (Hz)Code assumes (Hz)Measured (Hz)
00x0B02FF512 kHz8,0008,0008,0008,000 (stock)
10x0B00FF512 kHz24,0008,0008,0008,000 (no-op)
20x0300FF1,536 kHz24,00024,00024,00024,000
30x0700FF768 kHz24,00012,00012,288never measured
40x0302FF1,536 kHz8,0008,0008,0008,000
The model matches every measured rate. Configs 1 and 3 also break the manual’s rule that BCLK exceed 34 × LRCK (21× and 32×), which is why config 1 changes nothing. Config 3 was never measured, so 12,000 Hz is a prediction, but a well-founded one. Sources: manual 1126–1133 and 1216–1221, p25mon.c:80–87, STAGE3-BRINGUP.md:376–382.
The CPU budget

The wall it never looked for

The review treats everything after the input as solved. The emulated budget shows where one second of decoding goes, and why voice can’t play in real time on this CPU as the code stands.

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

35 claims, checked one by one

Twenty-eight hold, three of them with gaps that matter. Four are miscited, overstated or doubtful. Three are wrong, and two of those would steer a fix: calling de-emphasis benign, and a rate resolution that makes its ±0.5% gate look safe.

ItemWhat DeepSeek V4.1 Flash claimsWhereResultNotes
§2Host suite: python3 tests/p25/run.py exits 0 with selftest=PASS tests=111111tests/p25/run.pyHoldsMatches the baseline run.
§2Firmware cross-build exits 0 with no warnings; image 810,252 bytesMDUV380_firmware buildHoldsSame size as the reference relink.
§2SRAM .data 17,000 + .bss 112,532 + heap/stack 1,540 = 131,072 bytes (100%); CCM 64,376 of 65,536; FreeRTOS heap 20,480MDUV380_FW.elf; FreeRTOSConfig.h:71Holds
M6_estack sits at 0x2001FFFC, leaving about 1.5 KB of main stack for all nested interruptsSTM32F405VGTX_FLASH.ld:52–55; MDUV380_FW.elfHoldsThe firmware links with STM32F405VGTX_FLASH.ld, where _estack is RAM end − 4 = 0x2001FFFC (confirmed in the ELF). The 1,532 bytes above .bss hold a 512-byte heap reservation and the 1 KiB stack, so interrupts get 1–1.5 KB depending on heap use.
§2FEC round trips: Golay and Hamming 0 failures; RS(24,8), (24,12), (36,16) 0 encode/decode failures and 0/3600 correction failuresanalysis/scratch/fec_rt.c, fec_rt2.cHoldsRerun from its scratch files: identical. Its first RS harness reported 600 failures from a comparison bug; the corrected fec_rt2 is what it reports.
§2Symbol-rate sweep: 0, ±200, 1,000 ppm decode 90/90; 5,000 ppm 89 LDUs; 12,000 ppm 9 frames; ±23,400 ppm nonep25_channel_generate + live demodulatorHoldsIts harness wasn’t saved, but the numbers agree with the knee rerun: every frame within ±0.4%, collapse by ±1%.
B3Measured tolerance is about ±0.1%; a config 2–10% off passes the ±10% gate and can never decode, a silent blockerp25mon.c:574–577, 1066–1067OverstatedIts own sweep decodes 89 of 90 LDUs at 0.5%, and the knee rerun decodes every frame within ±0.4%. The gate/clamp mismatch is real, but the default 24 kHz clock is an exact crystal divide measured at 24,000 Hz.
§2A first-order high-pass at 30, 100 or 300 Hz leaves 0 frames and one at 10 Hz leaves 27; a 2–4 kHz low-pass is harmlessPython harness (not saved)WrongDoesn’t reproduce: standard C4FM through a first-order high-pass and the same demodulator decodes 180/180 at 10 Hz and at 30 Hz (0.03% symbol errors), and 18/180 at 100 Hz. The 2 kHz low-pass result holds, and so does the conclusion that voice high-passes kill C4FM.
§2RRC and raised-cosine pulses at α 0.2 and 0.35 all decode 90/90 with no payload errors at 20 dBlive demodulatorHoldsConsistent with the model at 20 dB.
§6LDU status-symbol phase matches DSD’s status_count rule exactly: NID status at wire dibit 35, then every 35 data symbols, 784 payload dibitsp25rx.c; dsd-master/src/p25p1_ldu.c:80–99HoldsA verified non-issue, traced symbol by symbol.
B1The monitor reads i2s_Rx_Buffer[j][i*2]; the only other reader, sound.c:444, labels that slot “Mic Audio”p25mon.c:443–444; sound.c:444Holds
B10xE0 bit 6 is the mic-into-LineIn1 enable in HRC6000SetMic, and the monitor writes 0xC9 at start and on refreshHR-C6000.c:2962–2965; p25mon.c:648, 733HoldsStock receive also writes 0xC9 (stock report §5), so the bit’s meaning isn’t settled; its probe matrix allows for that.
B1The manual gives I2S to an external codec, its Built-in Codec and external Codec sections are OMISSIS, and the FM diagram ends at Audio_outmanual 1110–1119, 2638–2676Holds
B1analysis/stock-p25/REPORT.md §4 concludes “no evidence found” and “treat the mic as the default hypothesis”analysis/stock-p25/REPORT.mdMiscited“No evidence found” is in REPORT.md §6 (line 214); “default hypothesis” is STAGE3-BRINGUP.md:174–175. The substance holds.
Step 0Settle RF versus mic with a 0xE0 matrix (0xC9, 0x89, 0x8B, 0xC1) against a keyed carrier and a mic tap, using the existing c capturep25mon.c; tools/p25_capture.pyIncompleteThe right experiment, but the c capture stages the first 80 interleaved halfwords rather than the decoder’s [j][i*2] samples (p25mon.c:460–466), so it needs fixing first; the review never mentions that.
B2Analog mode loads AT1846FMSettings (0x58=0xBC85, high- and low-pass filters on); the DMR set uses 0x58=0x9CDD, all filters offp25mon.c:724; AT1846S.c:191–217Holds
B2The DMR set runs 100% AF gain “because the DMR signal directly modulates…”AT1846S.c:204–215MiscitedThat phrase is the 0x3A line’s comment about transmit. The 0x44 comment says full gain is needed to decode 4FSK from some hotspots, and that the DM-1701 needs more AF gain.
B2The C6000 stays in its FM chain (de-emphasis, compression, 0x34[7] band-pass); de-emphasis is a low-pass and by itself benign, the killers are high-pass stages and compandingHR-C6000.c:2851; manual 2798–2824WrongDe-emphasis, −6 dB/oct from 300 Hz, leaves 0 of 180 frames on its own (model and rerun). The inherited 0x34=0x3C has de-emphasis on and the compressor and band-pass off.
B3The resampler ratio and playback use guessHz, the decode gate accepts ±10%, and the symbol-period clamp is ±1%p25mon.c:398, 574–577, 1066–1067; p25_4fsk.c:240–241Holds
B3p25monMeasuredRate is available once a second with about 0.1% resolutionp25mon.c:1151–1156WrongIt counts 160-sample DMA blocks over about a second, so at 24 kHz it is quantised to about ±0.67%. That is why its ±0.5% gate would flap (trap X14).
B3With a 12.288 MHz codec clock, the manual’s formulas and rate = min(BCLK/64, LRCK) reproduce every measured rate; config 3’s 12,288 should be 12,000, and configs 1 and 3 break the manual’s I2S_CK_M > 34 × I2S_FS_M rule, which explains config 1’s no-opmanual 1126–1133, 1216–1221; p25mon.c:80–87; STAGE3-BRINGUP.md:376–382HoldsNew: the model matches all four measured rates (8,000, 8,000, 24,000, 8,000 Hz). Config 3 was never measured, so 12,000 is a prediction.
B4Phase 2 is two-slot TDMA at 6,000 sym/s (H-DQPSK, with C4FM disc-tap handling in DSD-FME) carrying AMBE+2 voicedsd-fme dsd_main.c:1479, dsd_symbol.c:404, p25p2_frame.c:491HoldsThe DSD-FME citations are exact.
B4mbelib exports the AMBE+2 2450 decoder, nothing calls it, and Phase 2 voice should use itmbelib.h:63–68DoubtfulThe exports and the absent callers check out, but mbelib’s AMBE+2 path shares the synthesis routine that puts IMBE at 11–16× the CPU, and the radio’s DVSI decoder already runs AMBE+2 in real time (trap X20).
B5Table 5.6 one-layer recipe (0x01, 0x07–0x09 IF word, 0x10=0x02, 0x40=0x40, 0x41=0x41); 36 bytes per 30 ms is 9,600 bit/s, P25’s channel rate; it may need DMR syncmanual 2552–2637HoldsExact values and the right caveat.
B5OpenGD77’s physical-layer interrupt handler is an empty stubHR-C6000.c:1265–1267, 1438–1444HoldsNew: the doorbell for one-layer mode exists and does nothing.
M1p25monApplyClockCfg writes 0x2F, 0x32 and 0x33 with raw HRC6000SetPageRegByte, bypassing the verified writerp25mon.c:541–552, 622–623HoldsNew, and real: those are the writes the 24 kHz latch depends on.
M2p25monLastHealMs is both the self-heal limiter and the 5 s refresh timerp25mon.c:1089, 1116–1118HoldsThe code’s own comment says “reused as the refresh timer”.
M3The capture header hard-codes measured=0p25mon_capture.c:222–223Holds
M5README.md:157 and CAPABILITY-REPORT.md:4 cite P25-HARDWARE-REVIEW.md, which isn’t in the directoryREADME.md:157; CAPABILITY-REPORT.md:4HoldsTrue of the clean-room copy, from which earlier reviews were removed.
M6The capture’s gap queue was cut from 8 to 2 entries to make the link fitp25mon_capture.c:85; analysis/capture-milestone-REPORT.md:109Holds
M7hi2s3 runs as I2S slave receive, so the C6000’s codec clock owns the sample rateCore/Src/main.c:463–464Holds
M8Two RX audio mux definitions disagree: PORTC pin 5 in gpio.h versus PD9 in main.hgpio.h:105–113; main.h:177–178HoldsReal, but unrelated to decoding.
§6The NID BCH test checks all 65,536 data words plus 1–11-bit corrections against an independently built generatortests/p25/run.py:36–56, 117, 125Holds
§6The channel model reusing the demodulator’s pulse table is not a blocker; record that so nobody “fixes” it§2 pulse-shape experimentIncompleteNot a blocker at 20 dB, but the shared RRC hides about 2 dB against standard C4FM, which is a raised cosine times inverse sinc, not the plain raised cosine it tested. The test oracle still needs fixing.
§1“Every arrow except the first is implemented and unit-tested”; “the vocoder question is settled”§3, §7IncompleteImplemented and tested, yes, but mbelib IMBE synthesis needs 11–16× the 72 MHz CPU (emulated), so voice can’t play in real time even with a perfect front end. The review never looks at CPU.
Provenance

What was already known, and what it added

Both clean-room audits could see only the repository: its notes, code comments and the HR-C6000 manual, summarised in the first column. Grok 4.6 and DeepSeek V4.1 Flash overlap on the microphone route, the AT1846S filters, one-layer mode and the M17 tap. DeepSeek adds the clock path, RAM exhaustion and the gate mismatch; Grok adds de-emphasis, the 8 kHz rule and the capture bug.

IssueProject docs
in the repository
Grok 4.6
Sep 17
DeepSeek V4.1 Flash
Sep 17
Capture export stages the wrong I2S halfwordsabsentraised
§1, first code change
absent
its first experiment uses that capture
Capture parser certifies an incomplete streamabsentabsentabsent
pcm_starve never incrementsabsent
documented as working
absentabsent
No static RAM marginpartly
margins still to measure
absentraised
M6, 0 bytes free
The I2S stream is most likely microphone audiopartly
open, leaning sceptical
raised
§1, 0x89 versus 0xC9
raised
B1, four-value test
HR-C6000 de-emphasis on the capture path missedabsentraised
§2, bit 5 of 0x34
absent
calls it benign
AT1846S FM filters, low-frequency bit, 25 kHzpartly
“require characterization”
raised
§2, register level
raised
B2, filter register
0x10=0x6E hybrid state; 0x36 dual rolepartly
bring-up clock rules
partly
misses 0x6E and the 0x36 clock gate
partly
“undocumented hybrid state”
Manual: I2S frame clock “must be 8KHz” missedabsentraised
§3
absent
quotes the paragraph, not the rule
One-layer 4FSK test mode as a P25 tappartly
stock BER-test block only
raised
Gate D
raised
B5, exact recipe
±10% health gate versus ±1% timing clampabsentabsentraised
B3, impact overstated
Fail-closed muting at LDU cadenceabsentabsent
late-entry mute only
absent
Non-standard MFID mutes clear callsabsentabsentabsent
Test waveform shares the receiver’s RRC filterpartly
“synthetic RRC/AWGN” caveat
absentpartly
tested it, says not to fix
MCU runs at 72 MHzraisedraised
in passing
absent
Vocoder needs 11–16× the 72 MHz CPU missedpartly
decode timing unmeasured
absent
“fine on a 1 ms tick”
absent
“vocoder question settled”
Direct discriminator tap (M17 mod)raisedraised
uncredited
raised
pins, timer ADC, 48 kS/s
Phase 2 architecture and scopepartly
not implemented
partly
misplaces the AMBE+2 decoder
partly
voice via mbelib AMBE+2
Two unverified SPI writes per decoded 20 ms frameabsentabsentabsent
Capture sessions lack epochsabsentabsentpartly
measured=0 only
Ring and tick real-time budgetpartly
deadlines unproven
partly
calls it fine
absent
Clock config 3 assumes 12,288 Hz; the codec formula gives 12,000absentabsentraised
B3, clock model
Clock-config writes bypass the verified SPI writerpartly
SPI retry note
absentraised
M1

Project docs: README.md, CAPABILITY-REPORT.md, STAGE3-BRINGUP.md, STAGE4-TX-FEASIBILITY.md, analysis/stock-p25/REPORT.md, analysis/capture-milestone-REPORT.md, dm1701-p25-demo/README.md and source comments. The last two rows are new issues first raised here.

Its plan

What to keep, change and add

DeepSeek V4.1 Flash orders its plan well: answer the input question first, fix the front end or replace it with a direct ADC tap, then probe the chip’s own 4FSK mode. Most of that survives checking. Its rate-discipline step would cause new problems, and the plan never reaches the vocoder.

Keep

Worth doing as written

  • Step 0, the microphone test: four 0xE0 values against a keyed carrier and a mic tap, a decision rule and a half-day limit.
  • The unfiltered front end: the AT1846S DMR-style filter setting and the HR-C6000 out of its FM voice chain, verified on a recording.
  • The direct ADC tap if Step 0 fails: AT1846S pin 9 to MCU pin 18, timer-triggered ADC with DMA at 48 kS/s, DC-coupled.
  • The one-layer probe in parallel, and the small fixes: config 3 to 12,000 Hz, clock writes through the verified writer, measured= in the capture header.
Change

Would cause new problems

  • The ±0.5% gate: the rate report is quantised to about ±0.67%, so the gate would flap and reset the stream. Keep the exact nominal ratio for config 2 and report the deviation.
  • A resampler steered by the measured rate: the crystal-derived 24 kHz rate is already exact; a quantised estimate only adds error.
  • “Record that the shared RRC is fine”: it hides about 2 dB. Test with standard C4FM (raised cosine × inverse sinc) instead.
  • Phase 2 voice via mbelib AMBE+2: same synthesis routine as the 11–16× CPU wall. Assess the DVSI decoder the radio already runs.
Add

Missing from its plan

  • Fix the capture first: stage the decoder’s own [j][i*2] samples, or Step 0 measures the wrong stream.
  • De-emphasis off on purpose: clear 0x34 bit 5, and treat it as a decode killer, not a detail.
  • A real-time vocoder: a per-harmonic phase accumulator with wrapping and a cosine table. Without it, no audio.
  • The manual’s 8 kHz rule: 24 kHz runs, but outside the documented operating point; keep that in the Step 0 evidence.

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 on the waveform, wrong on the voice route

Its Phase 2 description holds: two-slot TDMA at 6,000 symbols per second, H-DQPSK, AMBE+2 voice, and none of it in the tree. Its DSD-FME citations are exact, and starting Phase 2 with a RAM budget is sensible advice on a radio whose SRAM is full and whose CCM has 1,160 bytes free.

Its voice route is not. The unused AMBE+2 entry points it found in mbelib share the synthesis routine that puts IMBE at 11–16 times this CPU, while the radio already runs DVSI’s AMBE+2 decoder for DMR in real time. That decoder’s entry points and licensing are the real open question.

Against Grok 4.6

Same protocol, a different half of the chain

Both clean-room audits find the microphone route and the AT1846S filters, both propose one-layer mode and the M17 tap, and both miss the vocoder CPU wall. Grok 4.6 also finds de-emphasis, the 8 kHz rule and the capture bug. DeepSeek V4.1 Flash runs more code, derives the clock path from the manual and makes fewer errors per claim, but clears de-emphasis. An earlier DeepSeek V4.1 Flash run on the original task delegated work to Muse Spark 1.3 subagents, so its grade does not represent this model; this run replaces it.

Grok 4.6 clean-room review, graded C (70) →

Scores by dimension

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

  • Grok 4.6
  • DeepSeek V4.1 Flash
0255075100Score out of 100Accuracy & evidence7473Coverage of decode problems5949Root cause & prioritisation7066Fix plan & acceptance gates7570Originality & attribution7274Clarity & calibration7874Weighted total7066
Method and limits

How this was checked

I read DEEPSEEK41_CLEAN_REVIEW.md and its scratch folder, then checked each claim against the firmware sources, the OpenGD77 radio drivers, the translated HR-C6000 manual, DSD and DSD-FME, and the repository’s notes. Before grading I audited the run: one model for all 88 calls, no network, no skills or memory, and nothing read outside its copy of the repository.

I rebuilt and reran its FEC harness unchanged, and put its filter claims through the same firmware-in-the-loop model used for the Grok 4.6 audit, which also supplies the de-emphasis and CPU results.

  • Filter responses are modelled, not measured, and its own filter harness was not saved, so the rerun tests its claim rather than its script.
  • Config 3’s 12,000 Hz rate is a model prediction; nobody has measured it.
  • CPU figures come from instruction counts in an emulator, not timing on the radio. No firmware was flashed and no radio was touched.
gcc -O1 -I <p25 dir> -o fec_rt2 fec_rt2.c <p25 dir>/p25_fec.c && ./fec_rt2
# rs encode/decode failures=0  correction failures=0/3600

DM1701_REPO=/path/to/DM-1701 python3 firstorder_check.py
# high-pass 10 Hz 180/180 · 30 Hz 180/180 · 100 Hz 18/180 · de-emphasis 0/180

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

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