GPT-5.6 Sol 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. I checked its claims against the code, the manual and the repository’s notes, reran the builds and inspections it reports, and used the firmware-in-the-loop model and CPU emulation from the earlier clean-room audits.
Overall grade
B
84 / 100 weighted
FDCBA
Fixes the instrument first.
Eleven audits argued about what the samples mean. This one noticed that the instrument used to look at them is broken, and made fixing it step one.
The capture command exports the first 80 interleaved words of each block instead of the 80 left-channel samples the decoder consumed — so the diagnostic
built to prove whether the radio or the microphone feeds the codec is itself temporally wrong. From there: the squelch path that rewrites the codec
mid-session, the clock writes that fail silently through an unchecked wrapper, the health gate that accepts ten times what the timing loop can track,
and both halves of the analog chain.
Its verification is more thorough than any audit before it. It built the firmware, ran the suite, read the map, and checked the linked symbol table to show that
the Phase 2 vocoder is dead-code eliminated — a check nobody else thought to make. 27 of 28 claims hold and none is wrong.
What it never does is cost the vocoder: mbelib needs 11–16× the 72 MHz budget, and this review leaves CPU as an open measurement for later.
That, and the manual’s 8 kHz frame-clock rule, are what keep a B from being higher.
Claims that check out
27 / 28
1 overstated or miscounted · 0 wrong
Decode-critical issues found
2 of 4
both halves of the analog chain, and the source
Wrong claims
0 of 28
27 hold, one unit slip
Worst-case stall it found
2 seconds
ten registers × 200 retries × a 1 ms tick
The run, not graded: 14 min · 35 agent steps · 142 tool calls · 28K output tokens (11K reasoning) · 5.4M tokens re-read · OpenAI subscription, one-shot
Headline findings
Six things to know about this review
Holds up
It finds the broken instrument, and fixes it first
The decoder takes the left sample of each interleaved pair, [j][i*2]. The capture hands the exporter a contiguous pointer and a count of 80 for each 160-word block, so what lands in the file is the first 80 interleaved words — half of them the other channel — labelled as a mono stream at the nominal rate.
It derives this from the stride rather than from a failing test, notes that the module tests pass because they never exercise the conversion, and makes correcting it step one of the plan: the capture is the instrument that would prove whether the radio or the microphone feeds the codec.
Three integration defects, each traced to its line
The application loop keeps running analog squelch while the monitor is active; an open-squelch transition calls radioSetAudioPath(true), enables the amplifier and rewrites C6000 mode registers. Forcing minimum squelch does not bypass that state machine.
SPI0WritePageRegByte returns −1 when the interrupt handler holds the bus, HRC6000SetPageRegByte throws that status away, and the clock configuration uses the unchecked wrapper — while a verified writer sits twenty lines below. And decode is accepted within ±10% of nominal while the timing loop is clamped to ±1%.
The codec refresh re-asserts ten registers. Each write goes through a helper that retries up to 200 times with a one-tick delay between attempts, because SPI writes fail silently while the C6000 interrupt owns the bus.
Ten registers × 200 retries × a 1 ms tick is a two-second worst-case stall of the application task — with a 512-sample receive ring that empties in about 21 ms. I checked the arithmetic against the code; it is exact.
p25mon.c:619–640, 642–656
Validated
It read the link, not just the source
Its Phase 2 case rests on something no other audit checked: the linked symbol table. mbe_processImbe7200x4400Frame resolves in the image; mbe_processAmbe3600x2450Frame does not, so the Phase 2 vocoder is dead-code eliminated rather than merely unused.
Confirmed with nm on the built ELF. It also built the firmware itself — 810,252 bytes, the same size as the baseline image — making it the first audit to compile what it reviewed.
build/MDUV380_FW.elf · mbelib/ambe3600x2450.c
Major gap
The vocoder never enters the argument
CPU appears three times in the review, always as something to measure later: read max_process_cycles in Milestone 2, benchmark AMBE 2450 before Phase 2. The Phase 1 vocoder is never costed.
Emulated on a 72 MHz core, mbelib’s IMBE synthesis needs 11–16× the available budget — the one blocker that lives entirely in software, and the one its “plausible after resolving the sample path” conclusion assumes away.
It analyses the clock transition in depth — how it fails, why it is unverified, what to do — without ever reaching the manual’s constraint that the I2S frame clock must be 8KHz. The 24 kHz configuration everything depends on is out of specification, not merely fragile.
It also lists the muting logic among the things that are sound. A group call whose link control carries a non-standard manufacturer ID is never opened, and one failed signalling word mutes at LDU cadence.
manual 1218 · p25rx.c:42–49, 159–163
Scorecard
How the 84 breaks down
Same rubric, weights and scale as every P25 audit on bench, so scores compare directly. GPT-5.6 Sol leads every dimension except clarity, and is the first review to reach a B.
Dimension scores
Hover or focus a bar for the reasoning. Weighted total 84, grade B.
Rubric table
Dimension
Weight
Score
Points
Why
Accuracy & evidence
30%
91
27.3
27 of 28 claims hold and none is wrong — the best record of the audits up to it. 55 citations, 38 with line numbers, every one resolving, and five claims I could only check by rerunning its builds and inspections. The single qualified claim is a kB/KiB slip on flash headroom.
Coverage of decode problems
25%
70
17.5
The broadest sweep so far: both analog halves, the sample source, the capture export, the squelch conflict, the clock writes, the health gate, the shared-filter test oracle and the RAM ceiling. It misses the manual’s 8 kHz rule and the vocoder’s CPU cost entirely.
Root cause & prioritisation
15%
84
12.6
It separates what is known from what must be measured and says so: protocol work should not compensate for an unproven input. Ownership comes second, qualification third. The gap is that CPU never enters the causal story at all.
Fix plan & acceptance gates
15%
90
13.5
Four milestones, each with an exit criterion, plus a fault ladder that maps each counter to the layer that failed. Step one fixes the capture so the later measurements mean something — the ordering every other plan got wrong.
Originality & attribution
10%
84
8.4
The linked-symbol check, the two-second refresh stall, the 564 MHz band ceiling and the ownership inventory are all new. It also rediscovers the squelch re-arm independently, from the same call chain.
Clarity & calibration
5%
86
4.3
Evidence, impact and resolution under every finding, and an honest closing list of what the run could not establish. Dense and list-heavy rather than explanatory, and it certifies the muting logic as sound in passing.
Weighted total
100%
84
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
GPT-5.6 Sol covers the chain from the front end to the diagnostics: the filters, the source, the capture, the clock writes and the test oracle. The red tags are the vocoder it never costs and the two audio-policy defects it certifies.
F1 raised by GPT-5.6 SolN1 missed or certified as correctDashed edge: no documented connection
F1AT1846S held at 25 kHz for a 12.5 kHz channel
F2HR-C6000 de-emphasis and 3 kHz filter at 0x34
F3The I2S source is unproven; test carrier against microphone
F4The capture exports the wrong halfwords
F5Squelch and clock writes rewrite the codec mid-session
F6The test oracle shares the receiver’s RRC filter
N1mbelib needs 11–16× the 72 MHz CPU
N2The parser certifies incomplete captures
N3pcm_starve never counts
N4The manual’s 8 kHz frame-clock rule
The analog chain
Both halves, and what they cost
It is one of the audits to name both halves of the conditioning — the C6000’s de-emphasis and the AT1846S bandwidth — and the only one to ask for a measured comparison of 12.5 and 25 kHz rather than assuming which is right.
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 alone, −6 dB per octave from 300 Hz, leaves no frames at all. Its resolution — find and document a flat register configuration, or stop trying to undo a voice chain in software — follows from that.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 one it left open
CPU as a measurement, not a finding
Its plan says to read the cycle counter in Milestone 2 and to benchmark the Phase 2 vocoder before committing to Phase 2. Neither step reaches the Phase 1 vocoder already in the path, which is where the budget goes.
Where one second of decoding goes
The firmware’s streaming loop, compiled with its own GCC flags and newlib and run in an emulated Cortex-M4F over 2.2 s of input (12 LDUs, 108 IMBE frames, all decoded). Instruction counts become time by assuming one clock cycle per instruction, the optimistic end.
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
28 claims, checked one by one
Twenty-seven of twenty-eight hold, five of them validated by rerunning its builds and inspections. One is a unit slip. None is wrong, which no audit before it managed at this breadth.
Item
What GPT-5.6 Sol claims
Where
Result
Notes
Blocker 1
The I2S source is not proven to be RF: the manual has no discriminator-to-I2S route, the code reports source=UNVERIFIED, the stock analysis found none, and no RF fixture exists in the tree
Every strand checks out. It stops one step short of the repository’s own reading, which is that the stream is most likely the microphone (0xE0=0xC9 selects LineIn1), but it draws the same operational conclusion.
Blocker 1
A stable 24 kHz DMA rate proves clocks and DMA run, not that the samples are RF-derived
STAGE3-BRINGUP.md
Holds
The distinction the whole bring-up effort turned on.
Blocker 1
Test three synchronized captures — no carrier, keyed RF, microphone excitation — and require RF content to follow the carrier and be absent during mic excitation
—
Holds
The acceptance test the answer key asks for, specified more completely than any other audit: polarity, clipping, DC offset, continuity, deviation scale and transfer function.
Blocker 2
mbe_processImbe7200x4400Frame is in the linked image; mbe_processAmbe3600x2450Frame is absent from the symbol table, so the Phase 2 vocoder is dead-code eliminated
build/MDUV380_FW.elf
Validated
Checked with nm on the linked ELF: the IMBE entry point resolves, the AMBE 3600×2450 entry point is not present. No other audit inspected the link.
Blocker 2
Phase 2 needs H-DQPSK demodulation, burst/slot/ISCH sync, scrambling from system parameters, SACCH/FACCH/MAC decoding, voice deinterleave and AMBE+2 2450 assembly, none of which exist in firmware
ref-sources/dsd-fme/src/p25p2_*.c
Holds
An accurate inventory, and it correctly separates the desktop reference from the firmware.
High
The monitor selects an analog voice path: HRC6000SetFMRx writes 0x34=0x3C (de-emphasis plus 3 kHz audio filter) and the refresh never replaces it
HR-C6000.c:2846–2855; p25mon.c:642–655, 727–734
Holds
Exact. Modelled, de-emphasis alone leaves 0 of 180 frames decodable, so this is the decode blocker rather than one contributor among several.
High
The AT1846S is deliberately placed in 25 kHz analog bandwidth while P25 occupies a 12.5 kHz channel
radioHardwareInterface.c:249–274
Holds
The other half of the analog chain, which only two other audits reached. It asks for a measured comparison rather than assuming which is better — the right call.
High
The normal application loop still runs analog squelch while the monitor is active; an open-squelch transition calls radioSetAudioPath(true), enabling the amplifier and rewriting C6000 mode registers
Independently rediscovers the squelch re-arm that one earlier audit proved (M12), with the same call chain and the added point that forcing minimum squelch does not bypass the state machine.
High
Every decoded IMBE frame calls radioSetAudioPath(false), repeating SPI writes and changing C6000 mode while the same chip supplies receive samples
p25mon.c:472–492
Holds
The two unverified per-frame writes (M2), with the right consequence.
High
PTT is not disabled in monitor mode, and prompts, melody, VOX, APRS, menus and the power-saving tick all run before p25monTick()
applicationMain.c:1024–1429
Holds
Correct, and the broadest ownership analysis of the audits up to it.
High
SPI0WritePageRegByte returns −1 when SPI0inUse, HRC6000SetPageRegByte discards that status, and the clock application at p25mon.c:550–552 uses the unchecked wrapper while a verified writer exists at 619–640
Exactly the clock-write defect (M11), traced through all four files.
High
The ten-register refresh loop’s per-register retry delays can stall the application task by about 2 seconds at a 1 ms tick
p25mon.c:619–656
Validated
p25monWriteReg retries up to 200 times with vTaskDelay(1) between attempts, and the refresh writes ten registers: 200 × 1 ms × 10 = 2 s worst case. New to the answer key; no earlier audit quantified this.
High
The host channel is not independent: p25_channel_generate() and the receiver share the same RRC table and level mapping, and the header says validation is synthetic
p25_4fsk.c:1–71, 273–344; p25_4fsk.h:8–12
Holds
The sharpest statement of the circular oracle (M1) in any audit — it names the shared filter, not just the synthetic channel.
High
The integrated capture path, C6000 route, analog response, DMA cadence and simultaneous playback are not exercised by the native tests
tests/p25/run.py
Holds
High
The link has .data 17,000, .bss 112,532 and a 1,540-byte heap/stack reservation, totalling exactly 131,072 bytes, with 1,160 bytes left in CCM
MDUV380_FW.map
Holds
Every figure matches the map, including the zero-headroom reading.
The flash region is 0xF4000 = 999,424 bytes and the image is 810,252, so 189,172 bytes remain — 189 kB, but 184.7 KiB. The number is right; the unit is not.
High
The firmware builds clean: tests pass, both trees compile, and the image is 810,252 bytes
build/OpenGD77_RT84_DM1701_DM1701.bin
Validated
The byte count matches the baseline build exactly. It is the first audit to build the firmware itself as well as run the tests.
Medium
Decode is accepted within ±10% of nominal rate while the timing loop is clamped to ±1% (7.92–8.08 samples/symbol), and the resampler uses the nominal rate rather than the measured one
p25mon.c:572–577; p25_4fsk.c:223–255
Holds
The health-gate mismatch (M7), stated with the resampler consequence the answer key also notes.
Medium
The raw capture exports the first 80 interleaved words of each 160-word block instead of the 80 left-channel samples the decoder consumed, and labels it a mono stream
p25mon.c:432–466; p25mon_capture.c:455–490
Holds
The capture export defect (T1), correctly derived from the stride rather than from a failing test — and it draws the consequence that matters: the diagnostic meant to prove RF-versus-mic routing is itself wrong.
Medium
Module-level capture tests pass because they never test the integrated stride conversion
tests/p25/run.py:264–270
Holds
True: the suite compiles p25mon_capture.c but never the monitor callback that feeds it.
Medium
Phase 1 CQPSK/LSM is unsupported: the only demodulator makes scalar four-level decisions, with no equalizer or simulcast test
p25_4fsk.c
Holds
Medium
The radio’s bands top out in UHF and even the extended software range ends at 564 MHz, which excludes most 700/800 MHz Phase 2 systems
trx.c:44–83
Validated
RADIO_HARDWARE_FREQUENCY_BANDS ends at maxFreq 56,400,000 (564 MHz). A practical constraint no audit before it mentioned.
Sound
The framer handles sync, polarity, status symbols, BCH NID, DUID rejection, LDU extraction, HDU/LC/ESS, clear/encrypted gating, terminators, TSBK CRC and bounded MBT
p25rx.c
Holds
Sound
Unknown and encrypted calls are muted rather than emitted
p25rx.c:89–124
Incomplete
True as written, and it is also the defect: a group link control with a non-standard manufacturer ID never establishes a clear call (M9), and one failed signalling word mutes at LDU cadence (M8). The review treats the muting as purely a safety property.
Sound
Signalling FEC is tested against independently built DSD-derived encoders, and the NID and framing tests use independently constructed BCH and wire framing
tests/p25/signalling.py; tests/p25/run.py:59–89
Holds
Correct, and a fair distinction from the circular channel model it criticises elsewhere.
Sound
Retunes, rate-health changes and ring overruns reset partial stream, vocoder and audio state
p25mon.c:264–297, 565–616
Holds
Right, though it does not notice that a missed call boundary inside the idle timeout leaves stale clear-call state (M13).
Milestone 4
Benchmark AMBE 2450 synthesis and the proposed demodulator on the 72 MHz target before committing to Phase 2
—
Incomplete
It treats CPU as an open measurement and never estimates it. Emulated, the Phase 1 vocoder already needs 11–16× the 72 MHz budget, which makes the existing Phase 1 path — not just Phase 2 — a real-time blocker.
Scope
Its verification list: tests/p25/run.py exit 0, both trees build, git diff --check clean, map inspection
—
Validated
Reproduced: the suite passes, and the baseline image is byte-identical in size to the one it reports.
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. GPT-5.6 Sol raises more of these rows than any audit before it, and is the first to reach the capture export, the squelch path and the clock writer in the same review.
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
DeepSeek V4 Pro Sep 17
Gemini 3.8 Flash Sep 17
HY4 Preview Sep 17
GPT-5.6 Sol 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
absent calls the capture module sound
absent certifies the capture path
absent its plan depends on it
raised derived from the stride
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
absent
absent
absent
partly module tests miss the stride
pcm_starve never increments
absent documented as working
absent
absent
absent relies on it
absent
raised F7
raised P1-7
absent
raised §2
absent
absent named only as a predicted symptom
absent
absent
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”
partly from the docs, not the map
raised §3.5, exact map symbols
absent
raised exact map figures
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
raised §1–§2, with the tap as the fix
raised §1 and §3.1
partly listed unverified, tested first
raised Blocker 1, source unproven
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
absent
raised §3.3, 0x34=0x3C
raised P1-1, and the boot table too
raised 0x34=0x3C, never replaced
AT1846S FM filters, low-frequency bit, 25 kHz
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
absent
raised §3.3, with the DMR fix
absent
raised 25 kHz for a 12.5 kHz channel
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
partly “inconsistent hybrid state”
partly 0x36 and 0xE0, not 0x10
partly 0x36, 0xE0, 0x26; not 0x10
partly FM mode writes, not 0x10
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
absent
absent
raised P1-2, with a test for it
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”
absent “no raw modem mode”
absent “zero internal silicon capability”
partly cited, then dismissed
absent
±10% health gate versus ±1% timing clamp
absent
absent
raised B3, impact overstated
raised P1-3
raised F3
absent
absent
absent
absent
absent
absent
absent
raised with the resampler consequence
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
absent
absent certifies it as correct
absent
absent certifies it as correct
Non-standard MFID mutes clear calls
absent
absent
absent
absent
absent
absent
absent
absent
absent
absent
absent certifies it as correct
absent
absent certifies it as correct
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
partly “ideal RRC-shaped signal”
absent
partly P1-5, circularity without the filter
raised names the shared table
MCU runs at 72 MHz
raised
raised in passing
absent
raised P1-5
absent
raised F7
absent
absent
raised §5
raised §7, with the OpenRTX precedent
raised §3.5, with the PLL settings
raised P1-4, with the PLL lines
partly named as the target, never costed
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
partly cites 4.36M, calls it 60%
absent puts it at 15–18 ms per frame
partly 87% measured, budget unresolved
absent left as a later measurement
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
raised its central recommendation
raised §5.1, with the ADC and DMA design
raised the fallback if Gate 0 fails
raised ADC with bias and anti-alias
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
partly no symbol rate or sync
raised accurate on rate and slots
partly right conclusion, H-CPM mislabelled
raised plus the linked-symbol check
Two unverified SPI writes per decoded 20 ms frame
absent
absent
absent
absent
absent treats them as protection
absent
absent
absent
absent
absent
partly the SPI0inUse mechanism
partly silent SPI0 failures
raised radioSetAudioPath per frame
Capture sessions lack epochs
absent
absent
partly measured=0 only
absent
absent
raised F9, 8 kHz under a 24 kHz header
absent
absent
absent
absent
absent
absent
partly asks for source metadata
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
absent
absent
absent
raised the 2 s refresh stall
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”
absent
absent
absent
absent
Clock-config writes bypass the verified SPI writer
partly SPI retry note
absent
raised M1
absent
absent
raised F9
absent
absent
raised §4.2
absent
absent
absent
raised traced through four files
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”
absent
absent
absent
raised same call chain, independently
Stale clear-call state releases a new call’s first frames
absent
absent
absent
absent
absent
raised F8, probe
absent
absent
absent
absent
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
raised §4, new
absent reads the loop as sound
absent
partly the ±1% clamp only
Unknown talkgroup opens audio (fail-open gating)
absent
absent
absent
absent
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. Three findings this review adds are not rows here because no earlier audit or document raised them: the two-second refresh stall, the dead-code-eliminated Phase 2 vocoder and the 564 MHz band ceiling.
Its plan
What to keep, change and add
Four milestones, each with an exit criterion, and a diagnostic ladder that maps each counter to the layer that failed. It is the only plan of the twelve that repairs the measuring equipment before taking a measurement.
Keep
Worth doing as written
Fix the capture stride first, with an integration test that fills both channels and both halves with unique counters.
Establish exclusive ownership: disable transmit entry, bypass the stock squelch, stop prompts and VOX, configure source and sink once.
Make clock writes transactional: status-returning, with readback of all three registers and a hard fault on failure.
Keep RF captures as permanent fixtures and replay them through the exact streaming path.
The exit criterion: keyed RF produces a four-level eye and microphone excitation does not appear.
Change
Would cause new problems
“Plausible after resolving the sample path”: the vocoder needs 11–16× the CPU budget, which no amount of ingress work changes.
Leaving CPU to Milestone 2: it belongs in the feasibility gate, beside the sample path.
Certifying the muting logic: the MFID and LDU-cadence rules drop clear calls.
Add
Missing from its plan
The manual’s 8 kHz frame-clock rule, which makes the 24 kHz configuration out of specification rather than merely fragile.
A real-time vocoder plan: wrapped per-harmonic phase and a cosine table, or a fixed-point port.
The parser’s completeness check, so a capture with a missing chunk cannot pass as whole.
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 most rigorous case against it
It enumerates what Phase 2 would need — complex demodulation, burst and ISCH synchronisation, scrambling from system parameters, SACCH/FACCH/MAC decoding, voice deinterleaving, AMBE+2 2450 assembly, slot and call state — and separates the desktop DSD reference in the tree from anything that runs on the radio. Then it checks the linked image and finds the Phase 2 vocoder absent from the symbol table, which is a stronger statement than “present but unused”.
It adds a constraint no audit before it mentions: the radio’s band table tops out at 564 MHz, so most 700/800 MHz Phase 2 systems are out of reach whatever the DSP does. What it leaves out is the symbol rate itself — 6,000 symbols per second, the number that rules out the chip’s own modem — and it never returns to the vocoder cost that would decide whether Phase 2 could run even with perfect samples.
Against the other clean-room audits
The first B, and the first to distrust its own instruments
Fourteen models have now read this repository. GPT-5.6 Sol is the first to find the capture defect, the squelch conflict, the unchecked clock writes and both halves of the analog chain in one review, and the first to build the firmware and inspect the link. It is also, with UNIONALPHA and HY4 Preview, one of four audits to make no wrong claim — and the first of them to reach the instruments.
Same rubric and weights. Hover or focus a bar for the score.
HY4 Preview
UNIONALPHA
Grok 4.6
GPT-5.6 Sol
Method and limits
How this was checked
I read GPT56SOL_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. Where it reported a verification I repeated it: the test suite, the retry arithmetic behind its two-second stall, the link map, the band table, and nm on the built ELF for the two vocoder entry points.
Before grading I audited the run: one model throughout, no network, no skills or memory, and nothing read outside its copy of the repository. It ran one-shot through the Hermes bench profile on an OpenAI Codex subscription, with the same prompt, tools, reasoning setting 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 two-second stall is a worst case derived from the retry structure, not an observed stall.
No firmware was flashed, and no radio was touched.
arm-none-eabi-nm build/MDUV380_FW.elf | grep -c processAmbe3600x2450Frame # 0
arm-none-eabi-nm build/MDUV380_FW.elf | grep -c processImbe7200x4400Frame # 2
# its two-second stall: 200 retries x 1 ms tick x 10 registers in the refresh
grep -n "guard = 200" -A8 p25mon.c