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
FDCBA
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.
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.
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.
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.
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.
“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.
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−.
Rubric table
Dimension
Weight
Score
Points
Why
Accuracy & evidence
30%
68
20.4
25 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 problems
25%
52
13.0
Finds 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 & prioritisation
15%
74
11.1
A 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 gates
15%
68
10.2
Small, 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 & attribution
10%
70
7.0
Two 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 & calibration
5%
72
3.6
Well 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 total
100%
65
Grade scale: A ≥ 90 · B 80–89 · C+ 75–79 · C 70–74 · C− 65–69 · D 50–64 · F < 50
Receive chain
Where its findings sit
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.
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.
Case
Return
Value handed to the RS symbol array
Clean word
0
0xABC — corrected data
Four errors in the check half
−1
0xABC — the received data, undamaged
Four errors in the data half
−1
0xA4C — the received data, as damaged on air
Every 12-bit value, encoded
—
the 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.
Register
Monitor today (analog FM)
OpenGD77 DMR mode
Effect on C4FM
AT1846S 0x40
0x0030, low-frequency bit cleared: “so it should be cleared to receive FM”
0x0031, “allows low freq audio”
Removes the near-DC content that runs of equal symbols depend on
AT1846S 0x58
0xBC85, “enable some filters for FM e.g. high and low pass filters”
0x9CDD, “disable all filters in DMR mode”
High- and low-pass shaping of the symbol stream
AT1846S 0x44
0x06CC, AF gain 80%
0x07FF, 100%; the DM-1701 “needs higher AF gain” to decode 4FSK
Lower discriminator level into the next stage
AT1846S bandwidth
25 kHz
12.5 kHz, forced for digital
Wider noise bandwidth and deviation scaling set for ±5 kHz FM
HR-C6000 0x34
0x3C, “compressor off, de-emph on, 3 kHz audio filter”
not written; the FM value persists
−6 dB/octave from 300 Hz and a 3 kHz low-pass on the FM receive path (manual §6.2)
Sources: AT1846S.c:191–217, HR-C6000.c:2846–2854 and 2858–2875, trx.c:228–279, p25mon.c:724. Register comments are OpenGD77’s; the manual translation documents 0x34.
What the inherited 0x34=0x3C does to C4FM
Standard C4FM through each HR-C6000 receive stage alone, then the firmware’s live demodulator and framer. 20 LDUs: 180 IMBE frames possible.
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 stage
Noise-free frames · NIDs
Symbol errors
20 dB frames · NIDs
no audio filtering
180/180 · 20/20
0.0%
180/180 · 20/20
3 kHz low-pass only
180/180 · 20/20
0.0%
180/180 · 20/20
de-emphasis only
0/180 · 0/20
51.3%
0/180 · 0/20
de-emphasis + 3 kHz low-pass (0x34=0x3C)
0/180 · 0/20
49.8%
0/180 · 0/20
The CPU budget
The 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.
The demodulator fits: about 40% of the core, 60% at 1.5 cycles per instruction. The vocoder needs roughly 11 times the whole CPU, 16 times at 1.5. Building with -O2 changes these by under 1%, because the time is spent inside newlib’s prebuilt libm.Per-stage table
Stage
-Os, M instr/s
Share at 1.0 · 1.5 c/i
-O2, M instr/s
Resampler ×1.6
1.40
1.9% · 2.9%
1.32
RRC FIR, timing and level fit
28.00
38.9% · 58.3%
27.70
Framing, NID and FEC
0.74
1.0% · 1.5%
0.73
mbelib IMBE decode and synthesis
781.90
1,086.0% · 1,629.0%
781.60
Total
812.1
1,128% · 1,692%
811.4
CPU time for each 20 ms of speech
One call to mbelib’s IMBE decoder per frame, uvquality 1, FEC-valid codewords with chosen pitch and voicing, mean over 100 consecutive frames.
Every frame type misses its 20 ms deadline, from about 2× for the simplest frames to more than 20× for loud voiced ones. The cause is mbelib’s per-harmonic phase, which accumulates without wrapping (mbelib.c:306). Once a call has run for about half a second, nearly every voiced-band cosf argument exceeds about 201 radians, and newlib’s cosf takes its __kernel_rem_pio2f path at roughly 1,700 instructions per call instead of 40 to 100. The real firmware links that same path.Per-frame table
Frame type
Mean, M instr
Worst, M instr
Mean ms at 1.0 · 1.5 c/i
L=20, all bands unvoiced
3.00
5.02
42 · 62
L=20, typical (15 of 20 voiced)
9.68
10.23
134 · 202
L=30, typical (21 of 30 voiced)
13.97
14.76
194 · 291
Random pitch and voicing
15.86
26.17
220 · 330
L=56, all bands voiced
31.25
32.97
434 · 651
Claim check
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.
Item
What GLM 5.3 claims
Where
Result
Notes
Verified
python3 tests/p25/run.py exits 0 with every suite passing, selftest 111111 and 0 raw dibit errors across ±1000 ppm
tests/p25/run.py
Holds
Matches the baseline run.
Verified
The firmware builds to an 810,252-byte image
make PLATFORM=RT84_DM1701 VARIANT=DM1701
Holds
Verified
p25_iW/iX/iY/iZ and both sync patterns are byte-identical to DSD’s tables
Rechecked: all four interleave tables and both sync strings match.
§1
The 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 I2S
manual §2.1 pin table, §4.5.1, §6 Fig 6.1
Holds
§1
In 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-engineering
analysis/stock-p25/REPORT.md §3–§6
Holds
Quoted accurately and credited.
§1
The 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 stale
p25mon.c header, :519
Holds
The console already prints source=UNVERIFIED, which the review credits.
§1
If 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 slope
manual §6; p25_4fsk.c:187–210
Holds
In 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.
§2
PROVEN 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–467
Holds
Correct, and its reproduction matches the code: 158 of 160 staged positions differ from what the decoder consumed.
§2
The 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 signal
p25mon_capture.c:222–224; tools/p25_capture.py
Holds
§2
capture_isr_tests.py feeds contiguous int16 arrays, so it cannot catch a call-site indexing bug — which is how the defect survived
The suite compiles p25mon_capture.c but never p25mon.c’s callback.
§2
p25monAudioUnderruns (pcm_starve) is declared and printed but never incremented
p25mon.c:123, 517
Holds
§2
The d snapshot copies only the first 16 samples of every 32-sample chunk and never resets, so it shows a prefix rather than recent PCM
p25mon.c:128, 596, 1326
Holds
New, and right: the copy is inside the per-chunk loop, so it keeps the first 16 samples of the last chunk processed.
§3
PROVEN 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 data
p25_fec.c:62–74; p25rx.c:201–221
Wrong
Doesn’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.
§3
The Hamming fallback in the same file returns the received systematic bits and is correct
p25_fec.c:42
Holds
True, and the Golay path does the same thing.
§4
The 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 Hz
p25mon.c:624–656; STAGE3-BRINGUP.md:328–329
Holds
It also attributes the 54% duty-cycle wobble to the C6000 power-cycling, which matches the code’s own diagnosis.
§4
Suspending hrc6000Task while the monitor is active is still not implemented
p25/ (no vTaskSuspend); STAGE3-BRINGUP.md:359–363
Holds
§4
p25monApplyClockCfg writes the clock triple through plain HRC6000SetPageRegByte, the path that fails silently under SPI0 contention, while the refresh uses the verified writer
p25mon.c:541–552, 624–656
Holds
Found independently; DeepSeek V4.1 Flash raised it first.
§4
A 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–2972
Incomplete
The 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.
§5
Status-dibit placement (every 36, counter seeded to 21), LDU geometry (144 + 6×92 + 16 + 72) and the NID layout check out
p25rx.c:8–12, 285
Holds
§5
RS(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 bit
tests/p25/signalling.py; fec_reference.cpp
Holds
§5
Trellis and CRC paths are cross-checked against DSD-FME layouts
tests/p25/signalling.py
Overstated
The 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.
§5
The channel model is synthetic RRC and AWGN, with no captured C4FM waveform, multipath or simulcast
p25_4fsk.h; p25_selftest.c
Holds
It doesn’t note that the generator shapes with the receiver’s own RRC, which hides about 2 dB against standard C4FM.
§5
8 kHz configs are correctly decode-gated because “C4FM energy extends past ±4 kHz”, and 24 kHz is the first safe rate
p25mon.c:575
Overstated
The 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.
§5
The 12,288 Hz clock candidate is unvalidated on hardware
p25mon.c:85
Holds
True, and the codec formula puts that config at 12,000 Hz rather than 12,288.
§5
Real-time behaviour at 72 MHz is instrumented but never measured under sustained voice, and mbelib synthesis runs inline in the 1 ms super-loop slice
Right 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.
§5
The RAM link is already at about zero headroom
MDUV380_FW.map; STAGE3 follow-ups
Holds
SRAM has 0 bytes free and CCM 1,160.
§6
Phase 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 unused
Phase 2 is “π/4-DQPSK at 4800 sym/s, 2 slots per 12.5 kHz channel”
§6 table
Wrong
Phase 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.
§6
AMBE+2 decode is cheaper than IMBE 7200×4400, so Phase 2 CPU is comparable in scale
§6 feasibility notes
Doubtful
Both run through the same mbelib synthesis routine, which is where the time goes; that path already needs 11–16× the CPU for IMBE.
§6
The 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.6
Overstated
True 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.
§1
If 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 audio
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.
Issue
Project docs in the repository
Grok 4.6 Sep 17
DeepSeek V4.1 Flash Sep 17
Muse Spark 1.3 Contributor Sep 17
GLM 5.3 Flash Sep 17
UNIONALPHA Sep 17
Qwen3.8 Flash Sep 17
Qwen3.8 Max Sep 17
GLM 5.3 Sep 17
Capture export stages the wrong I2S halfwords
absent
raised §1, first code change
absent its first experiment uses that capture
absent trusts the capture
absent relies on the capture
raised F3, fixed and verified
raised P1-2, tests miss the adapter
absent calls the capture validated
raised §2, proven with a reproducer
Capture parser certifies an incomplete stream
absent
absent
absent
absent trusts the parser
absent relies on the parser
absent
absent
absent relies on the parser
absent
pcm_starve never increments
absent documented as working
absent
absent
absent relies on it
absent
raised F7
raised P1-7
absent
raised §2
No static RAM margin
partly margins still to measure
absent
raised M6, 0 bytes free
partly “nearly full”, from the docs
raised F5, byte-exact map audit
raised F6, byte-exact
partly calls the reservation headroom
absent
partly “~zero headroom”
The I2S stream is most likely microphone audio
partly open, leaning sceptical
raised §1, 0x89 versus 0xC9
raised B1, four-value test
raised P1-1
partly open, leans towards RF
partly blocking, but never says microphone
raised P1-4, 0xE0 mic bit
raised A, the clearest case yet
raised §1, three sources
HR-C6000 de-emphasis on the capture path
absent
raised §2, bit 5 of 0x34
absent calls it benign
raised P1-2, 0x34=0x3C
raised F2, closes the eye
partly cited, called unmeasured
raised P1-3, 0x34=0x1C
raised C, with the 0x34 bit 5 fix
raised §1, tilts the eye
AT1846S FM filters, low-frequency bit, 25 kHz missed
partly “require characterization”
raised §2, register level
raised B2, filter register
raised P1-2, 0x58 filters
partly “voice filtering”; wrong bandwidth premise
partly cited, called unmeasured
raised P1-3, DMR 0x58 probe
absent
absent
0x10=0x6E hybrid state; 0x36 dual role
partly bring-up clock rules
partly misses 0x6E and the 0x36 clock gate
partly “undocumented hybrid state”
partly quiet-chip registers
raised F4, 0x10=0x80 kills the clock
partly F1, 0x6E against 0x80
partly “hybrid I2S state”
partly slot engine and eco only
partly §4.3, 0x10=0x80 hazard
Manual: I2S frame clock “must be 8KHz” missed
absent
raised §3
absent quotes the paragraph, not the rule
absent
absent
raised F4
absent quotes the formulas, not the rule
absent cites the section, not the rule
absent
One-layer 4FSK test mode as a P25 tap
partly stock BER-test block only
raised Gate D
raised B5, exact recipe
absent dismissed
absent
partly worth a bounded test
absent ruled out at “9600 Bd”
raised step 2, a symbol source
absent “not a P25 symbol source”
±10% health gate versus ±1% timing clamp
absent
absent
raised B3, impact overstated
raised P1-3
raised F3
absent
absent
absent
absent
Fail-closed muting at LDU cadence
absent
absent late-entry mute only
absent
absent calls it an asset
absent
absent
partly “keep it”
absent
absent calls it tested
Non-standard MFID mutes clear calls
absent
absent
absent
absent
absent
absent
absent
absent
absent
Test waveform shares the receiver’s RRC filter
partly “synthetic RRC/AWGN” caveat
absent
partly tested it, says not to fix
partly synthetic only, wants recordings
absent would extend that model
raised F5, unquantified
absent would extend that model
partly synthetic only, not the circularity
partly synthetic only
MCU runs at 72 MHz
raised
raised in passing
absent
raised P1-5
absent
raised F7
absent
absent
raised §5
Vocoder needs 11–16× the 72 MHz CPU missed
partly decode timing unmeasured
absent “fine on a 1 ms tick”
absent “vocoder question settled”
partly unmeasured; fix order backwards
absent “in good shape”
partly deadlines “unproven”
absent “not the problem”
absent “not the problem”
partly inline, unmeasured
Direct discriminator tap (M17 mod)
raised
raised uncredited
raised pins, timer ADC, 48 kS/s
raised fallback, pin 9
raised fallback
raised fallback
raised fallback
raised step 5, the likely answer
raised §1 pivot
Phase 2 architecture and scope
partly not implemented
partly misplaces the AMBE+2 decoder
partly voice via mbelib AMBE+2
raised with RF band limits
partly says mbelib has no AMBE+2
raised most accurate section
partly DMR and X2-TDMA parts
raised AMBE+2 present but uncalled
raised with a reference map
Two unverified SPI writes per decoded 20 ms frame
absent
absent
absent
absent
absent treats them as protection
absent
absent
absent
absent
Capture sessions lack epochs
absent
absent
partly measured=0 only
absent
absent
raised F9, 8 kHz under a 24 kHz header
absent
absent
absent
Ring and tick real-time budget
partly deadlines unproven
partly calls it fine
absent
partly overruns look like weak RF
absent
raised F7, 1 ms is a minimum
partly register stalls against the ring
absent
partly 1 ms tick as a constraint
Clock config 3 assumes 12,288 Hz; the codec formula gives 12,000
absent
absent
raised B3, clock model
absent
partly “guessed semantics”
absent
raised P1-6, for a different reason
absent
partly “unvalidated on hardware”
Clock-config writes bypass the verified SPI writer
partly SPI retry note
absent
raised M1
absent
absent
raised F9
absent
absent
raised §4.2
Stock squelch re-arms FM audio (0x10=0x80) during monitoring
absent assumes it can’t re-arm
absent
absent
absent
partly names squelch logic as a risk
raised F1, new
absent “fixed” by forcing squelch open
partly names the squelch path as a writer
partly hazard flagged, “unlikely”
Stale clear-call state releases a new call’s first frames
absent
absent
absent
absent
absent
raised F8, probe
absent
absent
absent
No frequency tracking; ad-hoc timing loop gains
partly a code comment calls the DC estimate biased
absent
absent
absent
absent
absent
absent
absent
absent
Unknown talkgroup opens audio (fail-open gating)
absent
absent
absent
absent
absent
absent
absent
absent
absent
Project docs: README.md, CAPABILITY-REPORT.md, STAGE3-BRINGUP.md, STAGE4-TX-FEASIBILITY.md, analysis/stock-p25/REPORT.md, analysis/capture-milestone-REPORT.md, dm1701-p25-demo/README.md and source comments. The clock-config rows were first raised by DeepSeek V4.1 Flash, the last two by UNIONALPHA.
Its plan
What to keep, change and add
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.
Register
Value
Manual description
0x01
xxxx 0000
IF receive mode
0x07 0x08 0x09
0x0B 0xD9 0x54
24-bit IF word, 455 kHz by default (OpenGD77 boots at 450 kHz)
0x10
0x02
One-layer mode, continuous receive; bit 5 for slot receive
0x40
0x40
Receive enable, test mode
0x41
0x41
Receive test enable
Output: 36 demodulated bytes per frame in RX RAM from 0x30, frame type in 0x51, system interrupt bit 0 when ready.
Rate fits exactly: 36 bytes every 30 ms is 9,600 bit/s, which is 4,800 symbols per second. Contiguous frames would lose nothing.
Open: Figure 5.15 is DMR burst geometry, so the mode may only emit frames after DMR sync. That’s the first thing to test.
Also needed: the AT1846S DMR register set, since the demodulator is fed by the AT1846S. And the vocoder still has to run in real time.
Phase 2
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.
Same rubric and weights. Hover or focus a bar for the score.
UNIONALPHA
GLM 5.3 Flash
Qwen3.8 Max
GLM 5.3
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