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

Gemini 3.8 Flash’s P25 review, graded

Gemini 3.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 finished in ten minutes. I checked its claims against the code, the manual and the repository’s notes, compiled the parts it questioned, and used the firmware-in-the-loop model and CPU emulation from the earlier clean-room audits.

Overall grade
D
64 / 100 weighted

A buildable plan, on an invented number.

This is the review to hand an engineer. It gets both hardware causes: the I2S stream is the microphone, and the analog chain that feeds it is filtered for speech. It is the only audit to prescribe the fix as one register the firmware already writes — 0x58 = 0x9CDD, the DMR filter set — and it follows through with a tap, an anti-alias network, a timer-triggered ADC and DMA design, and the PLL configuration to go with it. Then it decides how much CPU the vocoder needs by writing the number down.

15–18 ms of CPU time per 20 ms voice frame appears nowhere in the repository. The project’s own measurement is 4.36M instructions per frame, and the decoder emulates at 11–16× the 72 MHz budget, or 220–320 ms. On the invented figure, 168 MHz leaves over 65% of CPU time available; on the real one it leaves the vocoder at five to seven times over. It also certifies the instruments it needed to distrust. 23 of 35 claims hold, which puts it at D.

Claims that check out
23 / 35
8 overstated, mislabelled or miscounted · 4 wrong
Decode-critical issues found
2 of 4
both halves of the analog chain, with the register fix
The vocoder cost it used
15–18 ms
per 20 ms frame; measured, it is 220–320
The Phase 2 vocoder it says is missing
in the tree
mbelib’s AMBE+2 3600×2450, twice

The run, not graded: 10 min · 75 agent steps · 77 tool calls · 40K output tokens (26K reasoning) · 7.3M tokens re-read · OpenRouter API, one-shot

Headline findings

Six things to know about this review

Holds up

Both halves of the analog chain, and the register that fixes them

It is the only audit to name the fix as a register the firmware already writes. Analog FM sets the AT1846S filters with 0x58 = 0xBC85; P25 should use the DMR set, {0x58, 0x9C, 0xDD}, which is what AT1846S.c writes for DMR today. It names the HR-C6000 de-emphasis at 0x34 = 0x3C alongside it, and explains both effects correctly: baseline wander from the 300 Hz high-pass, inter-symbol interference from the de-emphasis. One slip: it says the AT1846S applies de-emphasis too. The firmware’s own comment says that is turned off here because the C6000 does it.

AT1846S.c:99, 196, 215 · HR-C6000.c:2851

Holds up

The sample source, argued from three directions

0xE0 = 0xC9 sets bit 6, and the driver’s own line says that bit enables the Mic Input to Line in 1. The manual gives I2S to external codecs and vocoders and never routes the IF ADC to it. The bring-up log camped on −80 dBm carriers without a single sync detection. Its conclusion — the demodulator is being fed room audio — is the right one, and it draws the right consequence: stop working on the I2S path.

HR-C6000.c:2964 · manual §4.5, §4.6, §6 · STAGE3-BRINGUP.md

Critical error

The vocoder cost is invented, and the plan rests on it

Floating-point mbelib IMBE decoding consumes approximately 15–18 ms of CPU time per 20 ms voice frame. No such measurement exists in the repository. What does exist is STAGE4-TX-FEASIBILITY.md:36: 4.36M instructions per 20 ms frame, against the 1.44M a 72 MHz core can issue. Emulated, the decoder needs 11–16× that budget. So the 2.33× from 168 MHz does not leave over 65% of CPU time available; it leaves the vocoder at five to seven times over. The one real blocker in software is written out of the review.

STAGE4-TX-FEASIBILITY.md:36 · mbelib.c:306 · Core/Src/main.c:269–274

Critical error

It rules out a vocoder that is in the tree twice

Neither the stock firmware’s DMR vocoder blob nor open-source mbelib supports P25 Phase 2 AMBE+2 vocoding. The blob half is right. The mbelib half is not: ambe3600x2450.c is the enhanced half-rate AMBE+2 decoder, it ships in both copies of mbelib in this tree, and the bundled DSD-FME decodes Phase 2 voice with it. It also gives the Phase 2 vocoder rate as 4900 bps. It is 3600: 2450 bps of voice plus FEC — the number in the filename it missed.

src/p25/mbelib/ambe3600x2450.c · ref-sources/dsd-fme/src/dsd_mbe2.c:67

Major gap

It certifies the instruments instead of checking them

Its verdict on the software is that it is technically sound, highly compliant, and thoroughly tested because the host tests pass. The tests pass because they never compile the monitor callback that stages capture samples: the export ships the contiguous first 80 halfwords of each row while the decoder reads [j][i*2], so 158 of 160 positions differ. The parser certifies incomplete captures, pcm_starve never increments, and the test oracle shapes its waveform with the receiver’s own filter. It also certifies the muting logic, where a group call with a non-standard manufacturer ID is never opened.

p25mon.c:123, 443, 460–466 · tests/p25/run.py:264–270 · p25rx.c:159–163

Gap in the plan

It solders before it measures

The review is certain the capture is microphone audio, and the repository is not: the stock analysis says the question remains open. The plan goes straight to a hardware modification without the carrier-keyed listening test that would settle it in an afternoon, and without fixing the capture export that any such test would run through. It is also the second audit to rule the chip out without trying the one documented mode that emits 4FSK symbols at P25’s own rate.

analysis/stock-p25/REPORT.md:175–176 · manual 2553–2635

Scorecard

How the 64 breaks down

Same rubric, weights and scale as every P25 audit on bench, so scores compare directly. Gemini 3.8 Flash scores high on clarity and sits mid-table on root cause, held down by coverage.

Dimension scores

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

0255075100Score out of 100Accuracy & evidenceweight 30%65Coverage of decode problemsweight 25%46Root cause & prioritisationweight 15%74Fix plan & acceptance gatesweight 15%70Originality & attributionweight 10%70Clarity & calibrationweight 5%84
Rubric table
DimensionWeightScorePointsWhy
Accuracy & evidence30%6519.523 of 35 claims hold, and every register value, map symbol and clock configuration it quotes is exact. Four are wrong, and two of those — the vocoder’s CPU cost and the missing Phase 2 codec — are the ones its conclusions rest on.
Coverage of decode problems25%4611.5The analog chain and the sample source, both complete, plus the clock gating, the RAM ceiling and one of the strongest Phase 2 sections on this task. It finds no tooling defect at all, and certifies the capture path, the test suite and the muting logic as correct.
Root cause & prioritisation15%7411.1The causal chain is the clearest any audit has written: no route from the discriminator to I2S, so the samples are microphone audio, and the front end is filtered for speech anyway. It then removes the CPU from the causal chain on a number it made up.
Fix plan & acceptance gates15%7010.5Concrete enough to build from: tap, filter network, ADC channel, DMA design, filter register, playback routing. But it has no acceptance gate before an irreversible hardware modification, and its CPU step is a clock change that doesn’t close the gap.
Originality & attribution10%707.0No new defect, but more implementation engineering than any audit before it: the sampling chain, the PLL and wait-state configuration, the microphone-mute register, and a fixed-point vocoder proposal from outside the repository.
Clarity & calibration5%844.2The best-presented review of the audits up to it: a failure-mode taxonomy, architecture diagrams, a comparison matrix, and a checklist a person could work through. Its calibration is the weak part — the wrong claims are stated as flatly as the verified ones.
Weighted total100%64Grade 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

Gemini 3.8 Flash covers the front of the chain completely — the source, the filters, the clock — and the RAM ceiling at the back. The red tags are the vocoder it discounted and the instruments it certified.

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 tapSF1F2KN1N2N3N4N5N6
S raised by Gemini 3.8 FlashN1 missed or certified as correctDashed edge: no documented connection
SThe I2S stream is microphone audio; take the discriminator tap
F1AT1846S speech filters; write the DMR set 0x58=0x9CDD
F2HR-C6000 de-emphasis at 0x34=0x3C
KSlot engine and sync hunter gate the I2S clock
N1mbelib needs 11–16× the CPU (put at 15–18 ms per frame)
N2The capture stages the wrong halfwords
N3The parser certifies incomplete captures
N4pcm_starve never counts
N5The test oracle shares the receiver’s RRC filter
N6The 8 kHz frame-clock rule in the manual
The fix it prescribes

What the DMR filter set buys

Gemini 3.8 Flash is right that the front end is configured for speech, and right about which register changes it. This is what that register is worth, modelled through the firmware’s own demodulator.

Register state for a P25 session

What trxSetModeAndBandwidth(RADIO_MODE_ANALOG, true) leaves in place, next to OpenGD77’s own settings for 4800-baud 4FSK.

RegisterMonitor today (analog FM)OpenGD77 DMR modeEffect on C4FM
AT1846S 0x400x0030, low-frequency bit cleared: “so it should be cleared to receive FM”0x0031, “allows low freq audio”Removes the near-DC content that runs of equal symbols depend on
AT1846S 0x580xBC85, “enable some filters for FM e.g. high and low pass filters”0x9CDD, “disable all filters in DMR mode”High- and low-pass shaping of the symbol stream
AT1846S 0x440x06CC, AF gain 80%0x07FF, 100%; the DM-1701 “needs higher AF gain” to decode 4FSKLower discriminator level into the next stage
AT1846S bandwidth25 kHz12.5 kHz, forced for digitalWider noise bandwidth and deviation scaling set for ±5 kHz FM
HR-C6000 0x340x3C, “compressor off, de-emph on, 3 kHz audio filter”not written; the FM value persists−6 dB/octave from 300 Hz and a 3 kHz low-pass on the FM receive path (manual §6.2)
Sources: AT1846S.c:191–217, HR-C6000.c:2846–2854 and 2858–2875, trx.c:228–279, p25mon.c:724. Register comments are OpenGD77’s; the manual translation documents 0x34.

What the inherited 0x34=0x3C does to C4FM

Standard C4FM through each HR-C6000 receive stage alone, then the firmware’s live demodulator and framer. 20 LDUs: 180 IMBE frames possible.

04590135180IMBE frames decoded, noise-free (of 180)no audio filtering180 / 1803 kHz low-pass only180 / 180de-emphasis only0 / 180de-emphasis + 3 kHz low-pass (0x34=0x3C)0 / 180
De-emphasis alone, −6 dB per octave from 300 Hz, leaves no frames at all. Its 0x58 = 0x9CDD clears the AT1846S half; the 0x34 bit it names is the other half, and its plan does not write it.
Data table
Audio stageNoise-free frames · NIDsSymbol errors20 dB frames · NIDs
no audio filtering180/180 · 20/200.0%180/180 · 20/20
3 kHz low-pass only180/180 · 20/200.0%180/180 · 20/20
de-emphasis only0/180 · 0/2051.3%0/180 · 0/20
de-emphasis + 3 kHz low-pass (0x34=0x3C)0/180 · 0/2049.8%0/180 · 0/20
The CPU budget

The number it wrote, and the number the repository measured

Its case for 168 MHz depends entirely on the vocoder costing 15–18 ms per frame. A 72 MHz core issues about 1.44M instructions in 20 ms; the project’s own figure for the encoder is 4.36M, and the decoder emulates worse.

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-three hold, including every register value, map symbol and clock configuration. Eight are overstated, mislabelled or miscounted. Four are wrong, and the CPU figure and the Phase 2 vocoder are load-bearing.

ItemWhat Gemini 3.8 Flash claimsWhereResultNotes
§1The HR-C6000 is a fixed-function DMR ASIC whose hardware modem is hard-coded to 4800 baud 4FSK with DMR sync words, and it cannot decode P25 in siliconmanual §5.4; HR-C6000.c:131HoldsTrue for P25 framing and FEC. It does not mention that the documented one-layer mode (manual 2553–2635) demodulates 4FSK into 36-byte frames at exactly 4,800 sym/s, which is the untried bit-level tap (M5).
§10xE0=0xC9 sets bit 6, which connects the codec ADC to LineIn1, the microphone capsule; the I2S stream is room audio, not RFHR-C6000.c:2964HoldsIts one line-numbered citation, and it is exact: SPI0ClearPageRegByteWithMask(0x04, 0xE0, 0xBF, isOn ? 0x40 : 0x00) “Enable or disable the Mic Input to Line in 1”. 0xC9 has bit 6 set.
§1The manual defines I2S only as external-codec or vocoder PCM plumbing, and Figure 6.1 never routes the IF ADC or discriminator to the I2S transmit registersmanual §4.5, §4.6, §6HoldsMatches the manual and the stock analysis, which reaches the same conclusion from V02.03’s register use.
§1This “fails completely at the hardware level”, as “established by” the manual and the disassemblyanalysis/stock-p25/REPORT.md:175–176OverstatedThe repository’s own wording is that the question “remains open and now has stronger grounds for skepticism”. The reading is very likely right, but it is an inference from absence, not an established routing fact, and the review’s own acceptance test is missing because of it.
§10x36=0x02 enables analog FM feedthrough to the speaker amplifier; the monitor’s 0x36=0x00 mutes itp25mon.c:647; HR-C6000.c:2969–2972Holds
§10x26=0xFD turns the IF ADC onp25mon.c:649Holds
§1The DMR AMBE+2 blob lives at 0x0807537C in STM32 flash and cannot decode IMBEdmr_codec/codec.h:38; STM32F405VGTX_FLASH.ld:156HoldsThe address is exact and appears in the linker script, the codec header and the stock analysis.
§2P25 Phase 1 IMBE is 7200 bps total (4400 voice + 2800 FEC), 88 bits of voice parameters in 144 bits per 20 ms framembelib/imbe7200x4400.cHolds
§2Neither the flash blob nor mbelib supports the P25 Phase 2 half-rate AMBE+2 vocodermbelib/ambe3600x2450.cWrongmbelib’s AMBE+2 3600×2450 is the Phase 2 vocoder, and it is in the tree twice (dm1701-p25-demo and the monitor). The bundled DSD-FME reference decodes Phase 2 voice with mbe_processAmbe3600x2450Framef (dsd_mbe2.c:67). The blob half of the claim holds; the mbelib half is backwards, and it repeats in §3.4, §5.2 and the comparison table.
§2Phase 2 half-rate AMBE+2 is “4900 bps (2450 voice + 2000 FEC + 450 signalling)”ref-sources/mbelib-masterWrongThe enhanced half-rate vocoder is 3600 bps: 2450 bps of voice plus 1150 bps of FEC. The file it should have found is named ambe3600x2450.c.
§3.2The stock clock is 8 kHz (0x2F=0x0B, 0x32:0x33=0x02FF); 24 kHz needs 0x2F=0x03 with 0x32:0x33=0x00FFp25mon.c:82–85HoldsBoth configurations are quoted exactly, including that LRCK alone is a no-op.
§3.2The 24 kHz write must happen live on a settled codec; at startup it leaves the clock at an unrecoverable 4.5 kHz or 13 kHzSTAGE3-BRINGUP.md:208–232OverstatedThe behaviour is exactly what the bring-up log reports, but the log attributes the 4.5 kHz reading to ~56% receive-window duty rather than a corrupted PLL divider (line 250). Right symptom, invented mechanism.
§3.2The DMR slot engine (0x40=0x43, 0x41=0x40) gates the I2S clock to about 54% duty, and the sync hunter at 0x5F re-locks onto any 4800-baud 4FSK energyp25mon.c:650–652; HR-C6000.c:131, 416HoldsAll four register values and the duty figure match the code and its comments, including that P25 C4FM is itself 4,800 baud.
§3.2OpenGD77’s power saving drops the C6000 PWD pin during idle; SetState(INACTIVE) only grants a 10-second reprieve, and only rxPowerSavingSetLevel(0) suppresses itrxPowerSaving.c:106; p25mon.c:665–685MislabelledThe API behaviour is right (the reprieve is 12 − 2×level seconds, so 10 s at level 1), but this is listed as a live failure mode when the monitor already saves the eco level and calls rxPowerSavingSetLevel(0) at start, restoring it at stop.
§3.2SPI0 register writes fail silently when the C6000 ISR holds SPI0inUse, so writes are dropped and clock state is mismatchedp25mon.c:622–623HoldsThe same mechanism as M2, though it does not reach the two unverified writes the audio sink makes per decoded frame.
§3.3Analog FM sets the AT1846S 300 Hz high-pass filter with 0x58=0xBC85; C4FM through that filter causes baseline wander and eye closureAT1846S.c:196HoldsThe register value is exact, and the modelled effect is the same direction as the firmware-in-the-loop result.
§3.3The AT1846S and the HR-C6000 (0x34=0x3C) both apply de-emphasisAT1846S.c:196; HR-C6000.c:2851MislabelledThe C6000 half is right and is the decode blocker. The AT1846S half is not: the FM register set’s own comment says “De-emphasis turned off as this is done by the HRC6000 on the MDUV380”. Its fix still removes the right filters.
§3.3The AT1846S limiter strips the envelope, “preventing CQPSK demodulation” of simulcast systemsAT1846S.cOverstatedA discriminator receives CQPSK/LSM with degradation rather than not at all; simulcast distortion, not the limiter, is what usually defeats it. Not decision-relevant here.
§3.4Phase 2 is 6000 sym/s H-DQPSK outbound and H-CPM inbound in 30 ms two-slot TDMA, which the 4800-baud DMR modem cannot track§3.4HoldsThe most accurate Phase 2 statement of the audits up to it; it avoids the 4800-baud and BPTC traps others fell into.
§3.4p25_trunk.c rejects TDMA identifier updates: “if (op==0x33) { t->valid&=~(1U<<id); return; } /* TDMA unsupported */”p25_trunk.c:26HoldsQuoted verbatim, at the line given.
§3.5Floating-point mbelib IMBE decoding takes about 15–18 ms of CPU per 20 ms voice frame at 72 MHz, so utilisation approaches 100%p25mon.c; mbelibWrongNo such measurement exists in the tree; the repository’s own figure is 4.36M instructions per 20 ms frame for the encoder (STAGE4-TX-FEASIBILITY.md:36), which is 303% of a 72 MHz core’s 1.44M. Emulated, the mbelib decoder needs 11–16× the CPU, i.e. 220–320 ms per frame, not 15–18.
§3.5Static SRAM ends at _ebss = 0x2001F9FC, consuming 129,532 bytes and leaving 1,540; CCM ends at 0x1000FB78 with 1,160 bytes leftMDUV380_FW.map:31416, 36961, 38286HoldsEvery number checks against the map. The 1,540 bytes are the FreeRTOS heap and stack reservation, so the practical headroom is zero, which is how the review uses it.
§3.5p25monRx is about 3.7 KB of CCMp25mon.c:112MiscountedIt repeats the source comment; sizeof(p25rx_t) compiles to 4,136 bytes.
§4.1The demodulator is a 65-tap Q12 RRC matched filter with α = 0.2p25_4fsk.c:13, 50MiscountedRRC_TAPS is 81, and both the float and Q12 tables have 81 entries. The 65 appears to come from a comment about phase wraps emitting 63/65 symbols.
§4.1Timing recovery is a damped loop across overlapping windows holding −1000 to +1000 ppm, with a 3-iteration decision-directed regression for DC offset and symbol scalep25_4fsk.c:190–210, 236–241; tests/p25/run.py:247HoldsThe regression, the iteration count and the ppm range are all real. It reads the loop as sound where it is ad hoc (gains 0.5 and 0.001, no lock test, no carrier AFC), which is M14.
§4.1The NID decoder is an allocation-free BCH(63,16,23) over GF(2⁶) with p(x)=x⁶+x+1 that corrects up to 11 changed bits and is validated across all 65,536 data combinationsp25_nid.c:58–70; tests/p25/run.py:117Holds
§4.1FEC: Golay(24,12,8) syndrome table to 3 errors, Hamming(10,6,3), RS(36,20,17) to 8 symbols in the HDU and RS(24,12,13) to 6 in the LDUp25_fec.c; tests/p25/signalling.pyIncompleteEach code and bound it names is right, but it omits RS(24,16,9), the LDU1 code that corrects 4.
§4.1The framer parses HDU/LDU1/LDU2/TDU/TDULC/TSBK with the 20-dibit link-control spacing, and “correctly mutes encrypted traffic and unknown calls, opening audio only upon validated clear-call signalling”p25rx.c:12, 159–163, 190IncompleteThe geometry and the 0x80 clear test are right, but the muting it certifies is the defect: a group link control with a non-standard manufacturer ID never establishes a clear call (M9), and a single failed signalling word mutes at LDU cadence (M8).
§4The pure software stack is “technically sound, highly compliant, and thoroughly tested”, passing all host teststests/p25/run.pyIncompleteThe suite does pass, but it never compiles the monitor callback that stages capture samples: the export ships the contiguous first 80 halfwords of each row while the decoder reads [j][i*2], so 158 of 160 positions differ (T1), the parser certifies incomplete captures (T2), pcm_starve never increments (T3) and the test oracle shapes with the receiver’s own RRC filter (M1). Certifying the instruments is what leaves the four defects in place.
§5.1Tap AT1846S pin 9 through a 1 µF DC block and a 1 kΩ/10 nF low-pass into STM32F405 pin 18 (PC2, ADC123_IN12), sampled by TIM2-triggered ADC1 DMA at 38.4 kHzanalysis/stock-p25/REPORT.md:175–176HoldsThe repository names the M17 mod and pin 18; the anti-alias values, the ADC channel mapping and the DMA design are the review’s own and are correct for this part.
§5.1In P25 mode write {0x58, 0x9C, 0xDD}, the DMR filter configuration, to bypass the 300 Hz high-pass and de-emphasisAT1846S.c:99, 215HoldsExactly the register set the firmware already uses for DMR, and the clearest single-line fix of any audit for the front end.
§5.1Raising the clock to 168 MHz gives 2.33× throughput, cutting IMBE synthesis from ~16 ms to ~6.8 ms and leaving over 65% of the CPU freeCore/Src/main.c:269–274WrongIt rests on the 15–18 ms figure. Against the measured 11–16×, 2.33× leaves the vocoder at 4.7–6.9× the budget, so the headroom it promises does not exist. The clock change is worth making, after the vocoder is fixed.
§5.1Replace floating-point mbelib with Pavel Yazev’s fixed-point IMBE from OP25, using SMLALD/SMUAD, in about 5 KB of RAM and 24 KB of flashexternalUnverifiedThe right instinct and the strongest vocoder proposal on this task, but nothing in the tree supports the footprint figures, and even a 4× fixed-point win with the clock change only reaches parity with the 11–16× gap.
§5.1Keep the C6000 for playback: 8 kHz I2S transmit and 0xE0=0x89 to disable the microphone inputHR-C6000.c:2778, 2964Holds0xC9 with bit 6 cleared is 0x89, which is what the mask at 2964 does.
§5.2Phase 2 is infeasible on this hardware: no linear I/Q path, no 6000-baud modem, no RAM headroom§5.2HoldsThe conclusion is right for the receive path, though the vocoder row of its table is the wrong claim above.
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. Gemini 3.8 Flash is the first audit to raise both halves of the analog chain and the sample source together, and one of two to raise no tooling defect 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
GLM 5.3
Sep 17
DeepSeek V4 Pro
Sep 17
Gemini 3.8 Flash
Sep 17
Capture export stages the wrong I2S halfwordsabsentraised
§1, first code change
absent
its first experiment uses that capture
absent
trusts the capture
absent
relies on the capture
raised
F3, fixed and verified
raised
P1-2, tests miss the adapter
absent
calls the capture validated
raised
§2, proven with a reproducer
absent
calls the capture module sound
absent
certifies the capture path
Capture parser certifies an incomplete streamabsentabsentabsentabsent
trusts the parser
absent
relies on the parser
absentabsentabsent
relies on the parser
absentabsentabsent
pcm_starve never incrementsabsent
documented as working
absentabsentabsent
relies on it
absentraised
F7
raised
P1-7
absentraised
§2
absentabsent
named only as a predicted symptom
No static RAM marginpartly
margins still to measure
absentraised
M6, 0 bytes free
partly
“nearly full”, from the docs
raised
F5, byte-exact map audit
raised
F6, byte-exact
partly
calls the reservation headroom
absentpartly
“~zero headroom”
partly
from the docs, not the map
raised
§3.5, exact map symbols
The I2S stream is most likely microphone audiopartly
open, leaning sceptical
raised
§1, 0x89 versus 0xC9
raised
B1, four-value test
raised
P1-1
partly
open, leans towards RF
partly
blocking, but never says microphone
raised
P1-4, 0xE0 mic bit
raised
A, the clearest case yet
raised
§1, three sources
raised
§1–§2, with the tap as the fix
raised
§1 and §3.1
HR-C6000 de-emphasis on the capture pathabsentraised
§2, bit 5 of 0x34
absent
calls it benign
raised
P1-2, 0x34=0x3C
raised
F2, closes the eye
partly
cited, called unmeasured
raised
P1-3, 0x34=0x1C
raised
C, with the 0x34 bit 5 fix
raised
§1, tilts the eye
absentraised
§3.3, 0x34=0x3C
AT1846S FM filters, low-frequency bit, 25 kHzpartly
“require characterization”
raised
§2, register level
raised
B2, filter register
raised
P1-2, 0x58 filters
partly
“voice filtering”; wrong bandwidth premise
partly
cited, called unmeasured
raised
P1-3, DMR 0x58 probe
absentabsentabsentraised
§3.3, with the DMR fix
0x10=0x6E hybrid state; 0x36 dual rolepartly
bring-up clock rules
partly
misses 0x6E and the 0x36 clock gate
partly
“undocumented hybrid state”
partly
quiet-chip registers
raised
F4, 0x10=0x80 kills the clock
partly
F1, 0x6E against 0x80
partly
“hybrid I2S state”
partly
slot engine and eco only
partly
§4.3, 0x10=0x80 hazard
partly
“inconsistent hybrid state”
partly
0x36 and 0xE0, not 0x10
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
absentabsentabsent
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
absent
“not a P25 symbol source”
absent
“no raw modem mode”
absent
“zero internal silicon capability”
±10% health gate versus ±1% timing clampabsentabsentraised
B3, impact overstated
raised
P1-3
raised
F3
absentabsentabsentabsentabsentabsent
Fail-closed muting at LDU cadenceabsentabsent
late-entry mute only
absentabsent
calls it an asset
absentabsentpartly
“keep it”
absentabsent
calls it tested
absentabsent
certifies it as correct
Non-standard MFID mutes clear callsabsentabsentabsentabsentabsentabsentabsentabsentabsentabsentabsent
certifies it as correct
Test waveform shares the receiver’s RRC filterpartly
“synthetic RRC/AWGN” caveat
absentpartly
tested it, says not to fix
partly
synthetic only, wants recordings
absent
would extend that model
raised
F5, unquantified
absent
would extend that model
partly
synthetic only, not the circularity
partly
synthetic only
partly
“ideal RRC-shaped signal”
absent
MCU runs at 72 MHzraisedraised
in passing
absentraised
P1-5
absentraised
F7
absentabsentraised
§5
raised
§7, with the OpenRTX precedent
raised
§3.5, with the PLL settings
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”
partly
inline, unmeasured
partly
cites 4.36M, calls it 60%
absent
puts it at 15–18 ms per frame
Direct discriminator tap (M17 mod)raisedraised
uncredited
raised
pins, timer ADC, 48 kS/s
raised
fallback, pin 9
raised
fallback
raised
fallback
raised
fallback
raised
step 5, the likely answer
raised
§1 pivot
raised
its central recommendation
raised
§5.1, with the ADC and DMA design
Phase 2 architecture and scopepartly
not implemented
partly
misplaces the AMBE+2 decoder
partly
voice via mbelib AMBE+2
raised
with RF band limits
partly
says mbelib has no AMBE+2
raised
most accurate section
partly
DMR and X2-TDMA parts
raised
AMBE+2 present but uncalled
raised
with a reference map
partly
no symbol rate or sync
raised
accurate on rate and slots
Two unverified SPI writes per decoded 20 ms frameabsentabsentabsentabsentabsent
treats them as protection
absentabsentabsentabsentabsentpartly
the SPI0inUse mechanism
Capture sessions lack epochsabsentabsentpartly
measured=0 only
absentabsentraised
F9, 8 kHz under a 24 kHz header
absentabsentabsentabsentabsent
Ring and tick real-time budgetpartly
deadlines unproven
partly
calls it fine
absentpartly
overruns look like weak RF
absentraised
F7, 1 ms is a minimum
partly
register stalls against the ring
absentpartly
1 ms tick as a constraint
absentabsent
Clock config 3 assumes 12,288 Hz; the codec formula gives 12,000absentabsentraised
B3, clock model
absentpartly
“guessed semantics”
absentraised
P1-6, for a different reason
absentpartly
“unvalidated on hardware”
absentabsent
Clock-config writes bypass the verified SPI writerpartly
SPI retry note
absentraised
M1
absentabsentraised
F9
absentabsentraised
§4.2
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
partly
hazard flagged, “unlikely”
absentabsent
Stale clear-call state releases a new call’s first framesabsentabsentabsentabsentabsentraised
F8, probe
absentabsentabsentabsentabsent
No frequency tracking; ad-hoc timing loop gainspartly
a code comment calls the DC estimate biased
absentabsentabsentabsentabsentabsentabsentabsentraised
§4, new
absent
reads the loop as sound
Unknown talkgroup opens audio (fail-open gating)absentabsentabsentabsentabsentabsentabsentabsentabsentabsentabsent

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 squelch and call-state rows by UNIONALPHA, and the control-loop row by DeepSeek V4 Pro.

Its plan

What to keep, change and add

Seven checklist items, most of them buildable as written. The gap is at both ends: nothing verifies the premise before the soldering iron comes out, and nothing fixes the instruments that would do the verifying.

Keep

Worth doing as written

  • The discriminator tap with its analog front end: AT1846S pin 9 through a DC block and an RC anti-alias filter into PC2.
  • The sampling chain: TIM2 at 38.4 kHz triggering ADC1 with circular DMA and a ping-pong buffer, straight into the demodulator.
  • 0x58 = 0x9CDD for P25 receive, the firmware’s own DMR filter set.
  • 0xE0 = 0x89 for playback, which is 0xC9 with the microphone bit cleared. The arithmetic checks out.
Change

Would cause new problems

  • 168 MHz as the CPU answer: 2.3× against an 11–16× gap. Worth doing, after the vocoder.
  • Deprecating Phase 2 on the vocoder argument: the receive path is the real obstacle; the half-rate decoder is already in the tree.
  • “The software stack is sound”: the capture export, the parser and the starvation counter are all broken.
Add

Missing from its plan

  • A carrier-keyed acceptance test before the modification, so the hypothesis is measured rather than assumed.
  • The 0x34 write its own analysis calls for, which the checklist leaves out.
  • A real-time vocoder beyond the clock change: wrapped per-harmonic phase and a cosine table, or the fixed-point port it suggests.

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

A strong section, with one wrong row

Its Phase 2 analysis is the most accurate any audit has produced: 6,000 symbols per second, H-DQPSK outbound and H-CPM inbound, 30 ms two-slot TDMA with its own sync and control channels, a limiter that removes the envelope a linear format needs, and the trunking code’s 0x33 rejection quoted at its line. Its advice — do not spend engineering effort here, use an SDR — is the honest answer.

The row that fails is the vocoder. It says neither the licensed blob nor mbelib can decode Phase 2 voice, at 4900 bps. The rate is 3600 bps, and mbelib’s ambe3600x2450.c is the decoder for it, sitting in both copies of mbelib in this repository and used by the bundled DSD-FME reference. The conclusion survives — the receive path, not the vocoder, is what makes Phase 2 infeasible here — but it reaches it through the one part that is wrong.

Against the other clean-room audits

The best-written review, and the thinnest evidence base

Gemini 3.8 Flash reads like a design document, and it is the only audit to name the register that changes both halves of the analog chain, alongside the register that changes them. It is also the only one to raise no tooling defect at all, and the only one to state a CPU measurement that does not exist. Ten minutes and 75 steps, against five hours for Qwen3.8 Max.

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 Pro
  • GLM 5.3
  • Gemini 3.8 Flash
0255075100Score out of 100Accuracy & evidence74606865Coverage of decode problems59335246Root cause & prioritisation70647474Fix plan & acceptance gates75606870Originality & attribution72627070Clarity & calibration78487284Weighted total70536564
Method and limits

How this was checked

I read GEMINI38FLASH_REVIEW.md in full and checked each claim against the firmware sources, the OpenGD77 drivers, the translated HR-C6000 manual and the repository’s notes. Every register value, map symbol and clock configuration it quotes was compared with the file it came from, and the figures it gave without a citation were re-derived: the CPU budget from the project’s own measurement, the Phase 2 vocoder from the code in the tree.

Before grading I audited the run: one model for all 75 steps, no network, no skills or memory, and nothing read outside its copy of the repository. It ran one-shot through OpenRouter with the same prompt, tools and sandbox as every other run. The de-emphasis, filter 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.
  • Its fixed-point vocoder footprint figures come from outside the repository and were not verified.
  • No firmware was flashed, and no radio was touched.
python3 -c "print(72e6*0.02)"
# 1,440,000 instructions per 20 ms at 72 MHz — the repo measured 4.36M for the encoder

ls src/p25/mbelib/ambe3600x2450.c ref-sources/mbelib-master/ambe3600x2450.c
# the Phase 2 vocoder it says is absent, in the tree twice

DM1701_REPO=/path/to/DM-1701 python3 deemph_check.py
# de-emphasis alone: 0 of 180 frames decode

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