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

GLM 5.3’s P25 review, graded

GLM 5.3 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 wrote defect reproducers against the real sources. I reran both of them, checked its claims against the code, the manual and the repository’s notes, and used the firmware-in-the-loop model and CPU emulation from the earlier clean-room audits.

Overall grade
C−
65 / 100 weighted

Proves the real capture bug. Invents a second one.

GLM 5.3 does the thing this project needs: it tests its own claims. It proves that the c capture exports L/R-interleaved samples rather than the left-channel stream the decoder reads, shows the header and host parser then read that as mono, and explains exactly why three rounds of capture tests missed it — they drive the module directly and never the call site. It also keeps the sample source as the top blocker, with the microphone evidence laid out from the manual, the stock disassembly and the driver comments, and it finds two smaller defects nobody else reported.

Its second “PROVEN BUG” is not one. It reads p25_golay_decode’s failure path as returning check bits, and reports a reproduction. Compiling the real file shows the opposite: the wire word carries the received data in its top 12 bits, so the fallback returns exactly what the code’s comment claims, and the fix it proposes would corrupt a correct path. With the vocoder left as an open measurement rather than the 11–16× CPU wall it is, 25 of 31 claims hold and it lands at C−.

Claims that check out
25 / 31
4 overstated or doubtful · 2 wrong
Decode-critical issues found
2 of 4
CPU left open; misses the 8 kHz rule
Golay fallback, rechecked
0 bugs
its second “proven bug” doesn’t reproduce
CPU the vocoder needs
11–16×
of 72 MHz; it calls this unmeasured

The run, not graded: 2 h 57 min · 143 agent steps · 169 tool calls · 512K output tokens (485K reasoning) · 19.5M tokens re-read · Alibaba Cloud Token Plan, interactive

Headline findings

Six things to know about this review

Holds up

It proves the capture bug, and why the tests missed it

The capture stages the raw interleaved row while the decoder reads the left channel at [i*2], so the export is L0, R0, L1, R1… GLM 5.3 reproduces it against the real staging code, then points out that the header and tools/p25_capture.py read that as mono at the nominal rate, so the RF-versus-microphone contrast runs on the wrong signal. Its explanation of why three rounds of capture tests passed is exactly right: they feed the module contiguous arrays and never exercise the call site in p25mon.c.

p25mon.c:443, 460–467 · p25mon_capture.c:222–224 · tests/p25/capture_isr_tests.py

Holds up

It keeps the source question first, with the evidence

The manual gives the built-in codec to microphone and line out and specifies I2S for an external codec, the stock disassembly puts 0xE0=0xC9 in the speaker path with no discriminator route, and the driver’s bit meanings are community reverse-engineering. So the clock and DMA evidence proves plumbing, not content. Its branch is right too: if the capture tracks the microphone, stop working on the I2S path and take the M17 discriminator tap.

manual §2.1, §4.5.1, §6 · analysis/stock-p25/REPORT.md · p25mon.c:519

New findings

Two small defects nobody else reported

The d PCM snapshot copies the first 16 samples of every 32-sample chunk into a static buffer, so the console shows a prefix rather than recent audio. And p25monApplyClockCfg() writes the clock registers through the plain SPI helper that fails silently under contention, while the periodic refresh uses the verified one, so a console clock change can report success without landing. It also flags the FM-audio path as a latent hazard and asks for a guard when 0x10 reads back 0x80.

p25mon.c:128, 596, 1326 · p25mon.c:541–552, 624–656

Critical error

Its second “PROVEN BUG” doesn’t exist

It reports that p25_golay_decode returns check bits as data when a word is uncorrectable, “injecting garbage into the outer RS codeword”, and prints a reproduction. Compiled against the real p25_fec.c, the opposite holds: the wire word carries the received systematic data in its top 12 bits, so word >> 12 is the data for every value. Four errors in the check half return the clean data; errors in the data half return the received data, which is what the comment and DSD both describe. Its proposed fix, re-reversing those bits, would corrupt a path that works today.

p25_fec.c:45–74 · p25rx.c:201–221 · grading rerun: golay_check.c

Critical miss

The vocoder stays an open measurement

It notes that mbelib synthesis runs inline in the 1 ms super-loop and that real-time behaviour is instrumented but never measured, then leaves it at step six of seven. Emulated, that synthesis needs 11–16 times the radio’s 72 MHz CPU: 134–458 ms for one voiced 20 ms frame. It is a blocker, not a measurement gap, and no front-end fix moves it.

p25mon.c:535 · p25rx.c:105–123 · mbelib.c:306 · Core/Src/main.c:269–274

Overstated

Two premises about bandwidth and rate

“C4FM energy extends past ±4 kHz” is why it calls 24 kHz the first safe rate. C4FM at 4800 baud occupies under 2.88 kHz of baseband; 8 kHz fails because it gives 1.67 samples per symbol. And its Phase 2 table puts π/4-DQPSK at 4800 symbols per second, where Phase 2 runs at 6,000, which is the actual reason the chip’s modem can’t track it. It also calls one-layer mode “not a P25 symbol source”, which holds for Phase 2 but not for Phase 1, where the mode’s 36 bytes per 30 ms is exactly 4,800 symbols per second.

p25mon.c:575 · GLM53_REVIEW.md §5, §6 · manual 2552–2638

Scorecard

How the 65 breaks down

Same rubric, weights and scale as every P25 audit on bench, so scores compare directly. GLM 5.3 leads on root cause and loses most of its margin on the reproduction it got wrong.

Dimension scores

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

0255075100Score out of 100Accuracy & evidenceweight 30%68Coverage of decode problemsweight 25%52Root cause & prioritisationweight 15%74Fix plan & acceptance gatesweight 15%68Originality & attributionweight 10%70Clarity & calibrationweight 5%72
Rubric table
DimensionWeightScorePointsWhy
Accuracy & evidence30%6820.425 of 31 claims hold, and the capture defect it proves is real and precisely described. But one of its two executed reproductions is an artifact: the Golay failure path returns the received data, not check bits, and the fix it derives from that would break a working decoder.
Coverage of decode problems25%5213.0Finds the capture bug with the reason the tests missed it, the microphone evidence, de-emphasis, the dead starvation counter, a stale PCM snapshot, the unverified clock writes and a full Phase 2 inventory. It misses the 8 kHz rule, the parser, capture epochs and the gate/clamp mismatch, and leaves the vocoder as an open measurement.
Root cause & prioritisation15%7411.1A strong ordering: the unproven sample source first, then the instrument that would settle it, with a branch to a hardware tap if the answer is the microphone. It spends its third slot on a defect that isn’t one and never names the vocoder wall.
Fix plan & acceptance gates15%6810.2Small, test-first steps in a sensible order, each host-checkable before hardware. Step four would change correct Golay code, nothing sets the receive path flat before the capture test, and the vocoder appears only as “measure it”.
Originality & attribution10%707.0Two genuinely new findings, the stale d snapshot and the unverified clock writes, plus the clearest account of why the capture tests could not catch the defect. Much of the rest restates the bring-up log and stock report, with credit.
Clarity & calibration5%723.6Well organised and specific, with a verification appendix and honest scope. It labels both defects “PROVEN BUG”, and one of them isn’t, which is the kind of confidence that moves fixes.
Weighted total100%65Grade 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

GLM 5.3’s findings cluster on the diagnostic branch and the front end: the capture export, the microphone question, de-emphasis, the starvation counter and the clock writes. The red tags are the vocoder it left open, the oracle, the parser, the 8 kHz rule, the gate mismatch, and the framing defect it reported that isn’t there.

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 tapCMFSKN1N2N3N4N5
C raised by GLM 5.3N1 missed or misreadDashed edge: no documented connection
CThe capture stages interleaved rows (proven, and the tests can’t catch it)
N1mbelib needs 11–16× the 72 MHz CPU (left “unmeasured”)
MThe I2S stream is most likely the microphone
N2The test waveform shares the receiver’s RRC
FDe-emphasis tilts the four-level eye
N3The parser certifies incomplete captures
Spcm_starve never counts; the d snapshot shows a prefix
N4±10% decode gate versus ±1% timing clamp
KClock writes bypass the verified SPI helper
N5A Golay “defect” that isn’t there
Its second proven bug

The one that isn’t there

GLM 5.3 wrote reproducers for both defects it reports, which is more than any other audit did. One reproduces. The other is an artifact of reading the encoder’s internal word layout as the wire format.

The Golay failure path, rechecked

Its §3 says an uncorrectable word returns check bits. I compiled the firmware’s own p25_fec.c, encoded 0xABC, damaged four bits in each half of the transmitted word, and decoded.

CaseReturnValue handed to the RS symbol array
Clean word00xABC — corrected data
Four errors in the check half−10xABC — the received data, undamaged
Four errors in the data half−10xA4C — the received data, as damaged on air
Every 12-bit value, encodedthe top 12 wire bits always equal the data
The decoder is handed the wire word, not the encoder’s internal layout, and the wire carries the systematic data in its top 12 bits. So *data = word >> 12 is the received data, which is what p25rx.c’s comment says and what DSD does. The review’s fix, re-reversing those bits, would hand the outer Reed–Solomon code a corrupted symbol on every failed inner word.
The front end, checked

Right about de-emphasis, conditional about the fix

GLM 5.3 treats the filtering as a qualification step for later: capture the eye, characterise the slope. The model already answers the part that matters, and the register change is a one-bit write.

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 the tilt the review predicts. Clearing 0x34 bit 5 removes it; the 3 kHz low-pass costs nothing.
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 measurement it deferred

The review puts real-time measurement at step six and calls the vocoder instrumented but unmeasured. The emulated budget shows what the measurement would find.

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

31 claims, checked one by one

Twenty-five hold, including both of the small new defects and every register and manual citation. Four are overstated or doubtful. Two are wrong, and one of those is an executed reproduction that doesn’t reproduce.

ItemWhat GLM 5.3 claimsWhereResultNotes
Verifiedpython3 tests/p25/run.py exits 0 with every suite passing, selftest 111111 and 0 raw dibit errors across ±1000 ppmtests/p25/run.pyHoldsMatches the baseline run.
VerifiedThe firmware builds to an 810,252-byte imagemake PLATFORM=RT84_DM1701 VARIANT=DM1701Holds
Verifiedp25_iW/iX/iY/iZ and both sync patterns are byte-identical to DSD’s tablesp25_tables.h; dsd-fme p25p1_const.h, dsd.h:1024–1025HoldsRechecked: all four interleave tables and both sync strings match.
§1The manual gives the built-in codec to mic and LINEOUT, specifies I2S for an external codec or vocoder PCM, and routes FM receive to the speaker DAC with no path to I2Smanual §2.1 pin table, §4.5.1, §6 Fig 6.1Holds
§1In the stock disassembly the 0xE0=0xC9 write sits in the speaker-audio path, the report finds no discriminator→I2S route, and the OpenGD77 bit comments are community reverse-engineeringanalysis/stock-p25/REPORT.md §3–§6HoldsQuoted accurately and credited.
§1The clock and DMA evidence proves the plumbing, not the content; pcm_min/max can’t tell mic noise from discriminator noise, and p25mon.c’s header comment claiming the discriminator path is stalep25mon.c header, :519HoldsThe console already prints source=UNVERIFIED, which the review credits.
§1If the I2S audio is discriminator-derived it still passes the FM receive filtering, and de-emphasis at about −6 dB per octave tilts the ±600/±1800 Hz levels; the demodulator’s single gain and DC fit can’t undo a frequency-dependent slopemanual §6; p25_4fsk.c:187–210HoldsIn the firmware-in-the-loop model de-emphasis alone leaves 0 of 180 frames. The review makes it a qualification item rather than naming the register fix.
§2PROVEN BUG: the capture stages the L/R-interleaved row (p25mon.c:460–467) instead of the left-only stream the decoder reads at [i*2], so the export is L0,R0,L1,R1…p25mon.c:443, 460–467HoldsCorrect, and its reproduction matches the code: 158 of 160 staged positions differ from what the decoder consumed.
§2The exported stream is two-channel data at twice the nominal rate while the header and tools/p25_capture.py read it as mono at nominal rate, so the RF-versus-mic comparison runs on the wrong signalp25mon_capture.c:222–224; tools/p25_capture.pyHolds
§2capture_isr_tests.py feeds contiguous int16 arrays, so it cannot catch a call-site indexing bug — which is how the defect survivedtests/p25/capture_isr_tests.py; tests/p25/run.py:264–270HoldsThe suite compiles p25mon_capture.c but never p25mon.c’s callback.
§2p25monAudioUnderruns (pcm_starve) is declared and printed but never incrementedp25mon.c:123, 517Holds
§2The d snapshot copies only the first 16 samples of every 32-sample chunk and never resets, so it shows a prefix rather than recent PCMp25mon.c:128, 596, 1326HoldsNew, and right: the copy is inside the per-chunk loop, so it keeps the first 16 samples of the last chunk processed.
§3PROVEN BUG: on an uncorrectable word p25_golay_decode returns the check bits as data (“*data = word >> 12”), planting parity garbage in the RS codeword; reproduced with four errors in the check half giving damaged datap25_fec.c:62–74; p25rx.c:201–221WrongDoesn’t reproduce. The decoder is handed the wire word, whose top 12 bits are the received systematic data: compiling the real p25_fec.c, wire>>12 equals the data for every value, four errors in the check half return the clean data (0xABC), and errors in the data half return the received data. The comment it calls contradicted is accurate, and its proposed fix would corrupt a correct path.
§3The Hamming fallback in the same file returns the received systematic bits and is correctp25_fec.c:42HoldsTrue, and the Golay path does the same thing.
§4The clocking recipe is settled: eco power saving off, verified write-readback-retry for SPI0, DMR slot engine and sync detection off, a 5 s refresh and self-heal restarts, with 300/300 reports at exactly 24,000 Hzp25mon.c:624–656; STAGE3-BRINGUP.md:328–329HoldsIt also attributes the 54% duty-cycle wobble to the C6000 power-cycling, which matches the code’s own diagnosis.
§4Suspending hrc6000Task while the monitor is active is still not implementedp25/ (no vTaskSuspend); STAGE3-BRINGUP.md:359–363Holds
§4p25monApplyClockCfg writes the clock triple through plain HRC6000SetPageRegByte, the path that fails silently under SPI0 contention, while the refresh uses the verified writerp25mon.c:541–552, 624–656HoldsFound independently; DeepSeek V4.1 Flash raised it first.
§4A mid-session radioSetAudioPath(true) writes 0x36=0x02 and 0x10=0x80 and kills the hybrid codec clock, but squelch is forced open at start, “which makes re-arming unlikely”p25mon.c:711–716; HR-C6000.c:2969–2972IncompleteThe hazard is real and its defensive guard is a good idea, but the re-arm is not unlikely: trx.c:517 re-arms whenever the RF amplifier is off, which the monitor does at start, on call changes and after 300 ms idle.
§5Status-dibit placement (every 36, counter seeded to 21), LDU geometry (144 + 6×92 + 16 + 72) and the NID layout check outp25rx.c:8–12, 285Holds
§5RS(36,20,17), RS(24,16,9), RS(24,12,13), Golay 6- and 12-bit and Hamming(10,6) match the independent DSD gold encoders bit for bittests/p25/signalling.py; fec_reference.cppHolds
§5Trellis and CRC paths are cross-checked against DSD-FME layoutstests/p25/signalling.pyOverstatedThe trunk opcodes and CRCs are, but the trellis test re-implements the production transition table (signalling.py:84) rather than checking it against a reference.
§5The channel model is synthetic RRC and AWGN, with no captured C4FM waveform, multipath or simulcastp25_4fsk.h; p25_selftest.cHoldsIt doesn’t note that the generator shapes with the receiver’s own RRC, which hides about 2 dB against standard C4FM.
§58 kHz configs are correctly decode-gated because “C4FM energy extends past ±4 kHz”, and 24 kHz is the first safe ratep25mon.c:575OverstatedThe gate is real, but C4FM at 4800 baud occupies under 2.88 kHz of baseband. 8 kHz fails because it gives 1.67 samples per symbol, and 24 kHz works because it gives five.
§5The 12,288 Hz clock candidate is unvalidated on hardwarep25mon.c:85HoldsTrue, and the codec formula puts that config at 12,000 Hz rather than 12,288.
§5Real-time behaviour at 72 MHz is instrumented but never measured under sustained voice, and mbelib synthesis runs inline in the 1 ms super-loop slicep25mon.c:535; p25rx.c:105–123; applicationMain.c:1451IncompleteRight that it is unmeasured, and it names the inline synthesis. Emulated, that synthesis needs 11–16× the 72 MHz CPU, so it is a blocker rather than an open measurement.
§5The RAM link is already at about zero headroomMDUV380_FW.map; STAGE3 follow-upsHoldsSRAM has 0 bytes free and CCM 1,160.
§6Phase 2 needs a different demodulator, an LFSR frame descrambler keyed by WACN/System ID/NAC, ISCH, SACCH/FACCH, ESS and MAC parsing, slot tracking, and AMBE+2 voice, all with DSD-FME references bundled here and mbelib’s AMBE+2 already present but unusedref-sources/dsd-fme/src/p25p2_*.c; mbelib/ambe3600x2450.cHoldsThe inventory and the reference map are accurate.
§6Phase 2 is “π/4-DQPSK at 4800 sym/s, 2 slots per 12.5 kHz channel”§6 tableWrongPhase 2 runs at 6,000 symbols per second, which is also why the chip’s 4,800-baud modem can’t track it. The slot structure is right.
§6AMBE+2 decode is cheaper than IMBE 7200×4400, so Phase 2 CPU is comparable in scale§6 feasibility notesDoubtfulBoth run through the same mbelib synthesis routine, which is where the time goes; that path already needs 11–16× the CPU for IMBE.
§6The C6000 can’t help: its DMR engine has DMR frame geometry, and the one-layer BER mode reads DMR-style frames, so “it is not a P25 symbol source”manual §5.4, §5.4.6OverstatedTrue for Phase 2, whose 6,000 baud the chip can’t track. For Phase 1 one-layer mode runs at 36 bytes per 30 ms, exactly 4,800 symbols per second, which is P25 Phase 1’s rate; whether it emits frames without DMR sync is untested, not settled.
§1If the capture tracks the microphone, stop investing in the I2S path and pivot to the M17-style receive tap (AT1846S pin 9 to an MCU input), keeping the C6000 for speaker audioanalysis/stock-p25/REPORT.md §8; CAPABILITY-REPORT.mdHoldsCredited to the repository’s notes.
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. GLM 5.3 raises more of these rows than any audit published before it except UNIONALPHA, and was the first to reproduce the capture defect against the firmware’s own staging code.

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
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
Capture parser certifies an incomplete streamabsentabsentabsentabsent
trusts the parser
absent
relies on the parser
absentabsentabsent
relies on the parser
absent
pcm_starve never incrementsabsent
documented as working
absentabsentabsent
relies on it
absentraised
F7
raised
P1-7
absentraised
§2
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”
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
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
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
absentabsent
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
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
absent
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”
±10% health gate versus ±1% timing clampabsentabsentraised
B3, impact overstated
raised
P1-3
raised
F3
absentabsentabsentabsent
Fail-closed muting at LDU cadenceabsentabsent
late-entry mute only
absentabsent
calls it an asset
absentabsentpartly
“keep it”
absentabsent
calls it tested
Non-standard MFID mutes clear callsabsentabsentabsentabsentabsentabsentabsentabsentabsent
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
MCU runs at 72 MHzraisedraised
in passing
absentraised
P1-5
absentraised
F7
absentabsentraised
§5
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
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
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
Two unverified SPI writes per decoded 20 ms frameabsentabsentabsentabsentabsent
treats them as protection
absentabsentabsentabsent
Capture sessions lack epochsabsentabsentpartly
measured=0 only
absentabsentraised
F9, 8 kHz under a 24 kHz header
absentabsentabsent
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
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”
Clock-config writes bypass the verified SPI writerpartly
SPI retry note
absentraised
M1
absentabsentraised
F9
absentabsentraised
§4.2
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”
Stale clear-call state releases a new call’s first framesabsentabsentabsentabsentabsentraised
F8, probe
absentabsentabsent
No frequency tracking; ad-hoc timing loop gainspartly
a code comment calls the DC estimate biased
absentabsentabsentabsentabsentabsentabsentabsent
Unknown talkgroup opens audio (fail-open gating)absentabsentabsentabsentabsentabsentabsentabsentabsent

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

GLM 5.3’s seven steps are ordered the way the evidence points: fix the instrument, run the experiment, branch on the answer. One step would break working code, and the two blockers it half-found sit at the end.

Keep

Worth doing as written

  • Fix the capture export first, with a host test that fills a row with distinct left and right values, then run the RF-versus-microphone contrast.
  • Branch on the result: discriminator audio means characterising the filter slope and capturing the eye; microphone means pivoting to the M17 tap.
  • Count pcm_starve, fix the d snapshot, and route the clock writes through the verified SPI helper.
  • Suspend hrc6000Task while the monitor owns the codec.
Change

Would cause new problems

  • “Fix the Golay fallback”: that path already returns the received data. The change would hand Reed–Solomon a corrupted symbol on every failed inner word.
  • Characterising the filter slope before acting: clear 0x34 bit 5 first. It is one register write, and de-emphasis alone leaves no frames.
  • “Measure real-time at 72 MHz” as step six: the answer is already known, and it decides whether any of this produces audio.
Add

Missing from its plan

  • A real-time vocoder: a wrapped per-harmonic phase and a cosine table in mbelib.
  • A strict capture parser to go with the fixed export: today it passes streams missing their first or last chunk.
  • The AT1846S DMR register set (0x58=0x9CDD, 0x40=0x0031, 12.5 kHz), which it never names.

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

The best inventory, with two wrong numbers

Its Phase 2 table is one of the most useful on this task: for each missing piece it names the bundled DSD-FME reference that already implements it, from the frame descrambler keyed by WACN, System ID and colour code, through ISCH, SACCH/FACCH, ESS and MAC parsing, to slot tracking. It also places the AMBE+2 decoder correctly: bundled in mbelib and never called.

Two numbers are wrong. Phase 2 runs at 6,000 symbols per second, not 4,800, and that rate is exactly why the chip’s 4,800-baud modem cannot track it. And AMBE+2 is not cheaper than IMBE here: both run through the same mbelib synthesis routine, which is the part that needs 11–16× the CPU, while the radio already runs a licensed DVSI AMBE+2 decoder for DMR in real time.

Against the other clean-room audits

The only one that reproduced anything, for better and worse

GLM 5.3 and UNIONALPHA were the first audits to write executable reproducers. UNIONALPHA’s two both reproduce; one of GLM 5.3’s does not, and the fix it derives from that would break working code. On everything else GLM 5.3 covers more ground than its Flash sibling, which scored 56.

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

Scores by dimension

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

  • UNIONALPHA
  • GLM 5.3 Flash
  • Qwen3.8 Max
  • GLM 5.3
0255075100Score out of 100Accuracy & evidence90647068Coverage of decode problems65343952Root cause & prioritisation60627174Fix plan & acceptance gates70627168Originality & attribution76586670Clarity & calibration80687872Weighted total74566365
Method and limits

How this was checked

I read GLM53_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 sources and the repository’s notes. Its two reproducers were not saved by the run, so I rebuilt both: the capture staging behaves as it describes, and the Golay failure path does not.

Before grading I audited the run: one model for all 143 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, with the same prompt, tools and sandbox as every other run, and came within seven steps of the 150-step cap. The de-emphasis and CPU results come from the model and emulator used for the Grok 4.6 audit.

  • Filter responses are modelled, not measured.
  • CPU figures come from instruction counts in an emulator, not timing on the radio.
  • Interactive runs write no usage file; its run facts come from the session history.
  • No firmware was flashed, and no radio was touched.
gcc -O2 -I…/p25 -o golay_check golay_check.c …/p25_fec.c && ./golay_check
# clean 0xABC · 4 errors in the check half → 0xABC · in the data half → 0xA4C
# the top 12 wire bits equal the data for every 12-bit value

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

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