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

GPT-5.6 Sol’s P25 review, graded

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

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.

p25mon.c:432–466 · p25mon_capture.c:455–490 · tests/p25/run.py:264–270

Holds up

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%.

applicationMain.c:964–969 · trx.c:515–587 · spi.c:53–73 · p25mon.c:550–552, 572–577

New

A two-second stall nobody had counted

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.

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

Missed

The 8 kHz rule, and the muting it calls correct

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.

0255075100Score out of 100Accuracy & evidenceweight 30%91Coverage of decode problemsweight 25%70Root cause & prioritisationweight 15%84Fix plan & acceptance gatesweight 15%90Originality & attributionweight 10%84Clarity & calibrationweight 5%86
Rubric table
DimensionWeightScorePointsWhy
Accuracy & evidence30%9127.327 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 problems25%7017.5The 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 & prioritisation15%8412.6It 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 gates15%9013.5Four 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 & attribution10%848.4The 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 & calibration5%864.3Evidence, 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 total100%84Grade 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.

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 tapF1F2F3F4F5F6N1N2N3N4
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.

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

What the inherited 0x34=0x3C does to C4FM

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

04590135180IMBE frames decoded, noise-free (of 180)no audio filtering180 / 1803 kHz low-pass only180 / 180de-emphasis only0 / 180de-emphasis + 3 kHz low-pass (0x34=0x3C)0 / 180
De-emphasis alone, −6 dB per octave from 300 Hz, leaves no frames at all. Its resolution — find and document a flat register configuration, or stop trying to undo a voice chain in software — follows from that.
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 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.

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

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.

ItemWhat GPT-5.6 Sol claimsWhereResultNotes
Blocker 1The 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 treep25mon.c:519–520, 698–700; manual 496–506, 2661–2676; analysis/stock-p25/REPORT.md:130–177HoldsEvery 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 1A stable 24 kHz DMA rate proves clocks and DMA run, not that the samples are RF-derivedSTAGE3-BRINGUP.mdHoldsThe distinction the whole bring-up effort turned on.
Blocker 1Test three synchronized captures — no carrier, keyed RF, microphone excitation — and require RF content to follow the carrier and be absent during mic excitationHoldsThe 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 2mbe_processImbe7200x4400Frame is in the linked image; mbe_processAmbe3600x2450Frame is absent from the symbol table, so the Phase 2 vocoder is dead-code eliminatedbuild/MDUV380_FW.elfValidatedChecked 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 2Phase 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 firmwareref-sources/dsd-fme/src/p25p2_*.cHoldsAn accurate inventory, and it correctly separates the desktop reference from the firmware.
HighThe monitor selects an analog voice path: HRC6000SetFMRx writes 0x34=0x3C (de-emphasis plus 3 kHz audio filter) and the refresh never replaces itHR-C6000.c:2846–2855; p25mon.c:642–655, 727–734HoldsExact. Modelled, de-emphasis alone leaves 0 of 180 frames decodable, so this is the decode blocker rather than one contributor among several.
HighThe AT1846S is deliberately placed in 25 kHz analog bandwidth while P25 occupies a 12.5 kHz channelradioHardwareInterface.c:249–274HoldsThe 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.
HighThe 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 registersapplicationMain.c:964–969; trx.c:515–587; HR-C6000.c:2967–2978HoldsIndependently 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.
HighEvery decoded IMBE frame calls radioSetAudioPath(false), repeating SPI writes and changing C6000 mode while the same chip supplies receive samplesp25mon.c:472–492HoldsThe two unverified per-frame writes (M2), with the right consequence.
HighPTT is not disabled in monitor mode, and prompts, melody, VOX, APRS, menus and the power-saving tick all run before p25monTick()applicationMain.c:1024–1429HoldsCorrect, and the broadest ownership analysis of the audits up to it.
HighSPI0WritePageRegByte 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–640spi.c:53–73; HR-C6000.c:2981–2984; p25mon.c:550–552, 619–640HoldsExactly the clock-write defect (M11), traced through all four files.
HighThe ten-register refresh loop’s per-register retry delays can stall the application task by about 2 seconds at a 1 ms tickp25mon.c:619–656Validatedp25monWriteReg 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.
HighThe 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 syntheticp25_4fsk.c:1–71, 273–344; p25_4fsk.h:8–12HoldsThe sharpest statement of the circular oracle (M1) in any audit — it names the shared filter, not just the synthetic channel.
HighThe integrated capture path, C6000 route, analog response, DMA cadence and simultaneous playback are not exercised by the native teststests/p25/run.pyHolds
HighThe 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 CCMMDUV380_FW.mapHoldsEvery figure matches the map, including the zero-headroom reading.
HighFlash headroom is about 189 KiBMDUV380_FW.map; build/OpenGD77_RT84_DM1701_DM1701.binMiscountedThe 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.
HighThe firmware builds clean: tests pass, both trees compile, and the image is 810,252 bytesbuild/OpenGD77_RT84_DM1701_DM1701.binValidatedThe byte count matches the baseline build exactly. It is the first audit to build the firmware itself as well as run the tests.
MediumDecode 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 onep25mon.c:572–577; p25_4fsk.c:223–255HoldsThe health-gate mismatch (M7), stated with the resampler consequence the answer key also notes.
MediumThe 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 streamp25mon.c:432–466; p25mon_capture.c:455–490HoldsThe 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.
MediumModule-level capture tests pass because they never test the integrated stride conversiontests/p25/run.py:264–270HoldsTrue: the suite compiles p25mon_capture.c but never the monitor callback that feeds it.
MediumPhase 1 CQPSK/LSM is unsupported: the only demodulator makes scalar four-level decisions, with no equalizer or simulcast testp25_4fsk.cHolds
MediumThe 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 systemstrx.c:44–83ValidatedRADIO_HARDWARE_FREQUENCY_BANDS ends at maxFreq 56,400,000 (564 MHz). A practical constraint no audit before it mentioned.
SoundThe framer handles sync, polarity, status symbols, BCH NID, DUID rejection, LDU extraction, HDU/LC/ESS, clear/encrypted gating, terminators, TSBK CRC and bounded MBTp25rx.cHolds
SoundUnknown and encrypted calls are muted rather than emittedp25rx.c:89–124IncompleteTrue 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.
SoundSignalling FEC is tested against independently built DSD-derived encoders, and the NID and framing tests use independently constructed BCH and wire framingtests/p25/signalling.py; tests/p25/run.py:59–89HoldsCorrect, and a fair distinction from the circular channel model it criticises elsewhere.
SoundRetunes, rate-health changes and ring overruns reset partial stream, vocoder and audio statep25mon.c:264–297, 565–616HoldsRight, though it does not notice that a missed call boundary inside the idle timeout leaves stale clear-call state (M13).
Milestone 4Benchmark AMBE 2450 synthesis and the proposed demodulator on the 72 MHz target before committing to Phase 2IncompleteIt 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.
ScopeIts verification list: tests/p25/run.py exit 0, both trees build, git diff --check clean, map inspectionValidatedReproduced: 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.

IssueProject docs
in the repository
Grok 4.6
Sep 17
DeepSeek V4.1 Flash
Sep 17
Muse Spark 1.3 Contributor
Sep 17
GLM 5.3 Flash
Sep 17
UNIONALPHA
Sep 17
Qwen3.8 Flash
Sep 17
Qwen3.8 Max
Sep 17
GLM 5.3
Sep 17
DeepSeek V4 Pro
Sep 17
Gemini 3.8 Flash
Sep 17
HY4 Preview
Sep 17
GPT-5.6 Sol
Sep 17
Capture export stages the wrong I2S halfwordsabsentraised
§1, first code change
absent
its first experiment uses that capture
absent
trusts the capture
absent
relies on the capture
raised
F3, fixed and verified
raised
P1-2, tests miss the adapter
absent
calls the capture validated
raised
§2, proven with a reproducer
absent
calls the capture module sound
absent
certifies the capture path
absent
its plan depends on it
raised
derived from the stride
Capture parser certifies an incomplete streamabsentabsentabsentabsent
trusts the parser
absent
relies on the parser
absentabsentabsent
relies on the parser
absentabsentabsentabsentpartly
module tests miss the stride
pcm_starve never incrementsabsent
documented as working
absentabsentabsent
relies on it
absentraised
F7
raised
P1-7
absentraised
§2
absentabsent
named only as a predicted symptom
absentabsent
No static RAM marginpartly
margins still to measure
absentraised
M6, 0 bytes free
partly
“nearly full”, from the docs
raised
F5, byte-exact map audit
raised
F6, byte-exact
partly
calls the reservation headroom
absentpartly
“~zero headroom”
partly
from the docs, not the map
raised
§3.5, exact map symbols
absentraised
exact map figures
The I2S stream is most likely microphone audiopartly
open, leaning sceptical
raised
§1, 0x89 versus 0xC9
raised
B1, four-value test
raised
P1-1
partly
open, leans towards RF
partly
blocking, but never says microphone
raised
P1-4, 0xE0 mic bit
raised
A, the clearest case yet
raised
§1, three sources
raised
§1–§2, with the tap as the fix
raised
§1 and §3.1
partly
listed unverified, tested first
raised
Blocker 1, source unproven
HR-C6000 de-emphasis on the capture pathabsentraised
§2, bit 5 of 0x34
absent
calls it benign
raised
P1-2, 0x34=0x3C
raised
F2, closes the eye
partly
cited, called unmeasured
raised
P1-3, 0x34=0x1C
raised
C, with the 0x34 bit 5 fix
raised
§1, tilts the eye
absentraised
§3.3, 0x34=0x3C
raised
P1-1, and the boot table too
raised
0x34=0x3C, never replaced
AT1846S FM filters, low-frequency bit, 25 kHzpartly
“require characterization”
raised
§2, register level
raised
B2, filter register
raised
P1-2, 0x58 filters
partly
“voice filtering”; wrong bandwidth premise
partly
cited, called unmeasured
raised
P1-3, DMR 0x58 probe
absentabsentabsentraised
§3.3, with the DMR fix
absentraised
25 kHz for a 12.5 kHz channel
0x10=0x6E hybrid state; 0x36 dual rolepartly
bring-up clock rules
partly
misses 0x6E and the 0x36 clock gate
partly
“undocumented hybrid state”
partly
quiet-chip registers
raised
F4, 0x10=0x80 kills the clock
partly
F1, 0x6E against 0x80
partly
“hybrid I2S state”
partly
slot engine and eco only
partly
§4.3, 0x10=0x80 hazard
partly
“inconsistent hybrid state”
partly
0x36 and 0xE0, not 0x10
partly
0x36, 0xE0, 0x26; not 0x10
partly
FM mode writes, not 0x10
Manual: I2S frame clock “must be 8KHz” missedabsentraised
§3
absent
quotes the paragraph, not the rule
absentabsentraised
F4
absent
quotes the formulas, not the rule
absent
cites the section, not the rule
absentabsentabsentraised
P1-2, with a test for it
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”
absent
“no raw modem mode”
absent
“zero internal silicon capability”
partly
cited, then dismissed
absent
±10% health gate versus ±1% timing clampabsentabsentraised
B3, impact overstated
raised
P1-3
raised
F3
absentabsentabsentabsentabsentabsentabsentraised
with the resampler consequence
Fail-closed muting at LDU cadenceabsentabsent
late-entry mute only
absentabsent
calls it an asset
absentabsentpartly
“keep it”
absentabsent
calls it tested
absentabsent
certifies it as correct
absentabsent
certifies it as correct
Non-standard MFID mutes clear callsabsentabsentabsentabsentabsentabsentabsentabsentabsentabsentabsent
certifies it as correct
absentabsent
certifies it as correct
Test waveform shares the receiver’s RRC filterpartly
“synthetic RRC/AWGN” caveat
absentpartly
tested it, says not to fix
partly
synthetic only, wants recordings
absent
would extend that model
raised
F5, unquantified
absent
would extend that model
partly
synthetic only, not the circularity
partly
synthetic only
partly
“ideal RRC-shaped signal”
absentpartly
P1-5, circularity without the filter
raised
names the shared table
MCU runs at 72 MHzraisedraised
in passing
absentraised
P1-5
absentraised
F7
absentabsentraised
§5
raised
§7, with the OpenRTX precedent
raised
§3.5, with the PLL settings
raised
P1-4, with the PLL lines
partly
named as the target, never costed
Vocoder needs 11–16× the 72 MHz CPU missedpartly
decode timing unmeasured
absent
“fine on a 1 ms tick”
absent
“vocoder question settled”
partly
unmeasured; fix order backwards
absent
“in good shape”
partly
deadlines “unproven”
absent
“not the problem”
absent
“not the problem”
partly
inline, unmeasured
partly
cites 4.36M, calls it 60%
absent
puts it at 15–18 ms per frame
partly
87% measured, budget unresolved
absent
left as a later measurement
Direct discriminator tap (M17 mod)raisedraised
uncredited
raised
pins, timer ADC, 48 kS/s
raised
fallback, pin 9
raised
fallback
raised
fallback
raised
fallback
raised
step 5, the likely answer
raised
§1 pivot
raised
its central recommendation
raised
§5.1, with the ADC and DMA design
raised
the fallback if Gate 0 fails
raised
ADC with bias and anti-alias
Phase 2 architecture and scopepartly
not implemented
partly
misplaces the AMBE+2 decoder
partly
voice via mbelib AMBE+2
raised
with RF band limits
partly
says mbelib has no AMBE+2
raised
most accurate section
partly
DMR and X2-TDMA parts
raised
AMBE+2 present but uncalled
raised
with a reference map
partly
no symbol rate or sync
raised
accurate on rate and slots
partly
right conclusion, H-CPM mislabelled
raised
plus the linked-symbol check
Two unverified SPI writes per decoded 20 ms frameabsentabsentabsentabsentabsent
treats them as protection
absentabsentabsentabsentabsentpartly
the SPI0inUse mechanism
partly
silent SPI0 failures
raised
radioSetAudioPath per frame
Capture sessions lack epochsabsentabsentpartly
measured=0 only
absentabsentraised
F9, 8 kHz under a 24 kHz header
absentabsentabsentabsentabsentabsentpartly
asks for source metadata
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
absentabsentabsentraised
the 2 s refresh stall
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”
absentabsentabsentabsent
Clock-config writes bypass the verified SPI writerpartly
SPI retry note
absentraised
M1
absentabsentraised
F9
absentabsentraised
§4.2
absentabsentabsentraised
traced through four files
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”
absentabsentabsentraised
same call chain, independently
Stale clear-call state releases a new call’s first framesabsentabsentabsentabsentabsentraised
F8, probe
absentabsentabsentabsentabsentabsentabsent
No frequency tracking; ad-hoc timing loop gainspartly
a code comment calls the DC estimate biased
absentabsentabsentabsentabsentabsentabsentabsentraised
§4, new
absent
reads the loop as sound
absentpartly
the ±1% clamp only
Unknown talkgroup opens audio (fail-open gating)absentabsentabsentabsentabsentabsentabsentabsentabsentabsentabsentabsentabsent

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.

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 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.

HY4 Preview clean-room review, graded C+ (77) →

Scores by dimension

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

  • HY4 Preview
  • UNIONALPHA
  • Grok 4.6
  • GPT-5.6 Sol
0255075100Score out of 100Accuracy & evidence85907491Coverage of decode problems56655970Root cause & prioritisation80607084Fix plan & acceptance gates84707590Originality & attribution82767284Clarity & calibration88807886Weighted total77747084
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

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