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

HY4 Preview’s P25 review, graded

Tencent HY4 Preview 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, rebuilt and reran the benchmark it left behind, and used the firmware-in-the-loop model and CPU emulation from the earlier clean-room audits.

Overall grade
C+
77 / 100 weighted

The first one to measure.

Nine audits before this one argued from reading. This one built something. It compiled the firmware’s own DSP sources natively, timed the three stages of the receive chain, and reported that the vocoder is 87% of the cost — then refused to convert x86 time into a Cortex-M4 budget, and asked for the on-target counter instead. I rebuilt its benchmark: the numbers reproduce within 3%, and on the target the vocoder’s share is higher still. It also found what nobody else did in the register tables: the monitor never writes 0x34, so de-emphasis and a 3 kHz filter stay in the path all session — and the boot table sets the same bit again.

Where it bets wrong is the one thing it cannot check: it assumes the codec is carrying radio audio at all, when the repository’s own analysis says the stream is more likely the microphone. Its plan tests that first, which is the right instinct, through a capture export that is broken in a way it never looked for. 24 of 30 claims hold and none is wrong — one of four audits to reach zero — which puts it at C+, the best of the audits published up to it.

Claims that check out
24 / 30
6 overstated, mislabelled or unverified · 0 wrong
Decode-critical issues found
2 of 4
with partial credit on the other two
Wrong claims
0 of 30
one of four audits to reach zero
Its benchmark, rerun here
87%
the vocoder’s share, reproduced within 3%

The run, not graded: 15 min · 40 agent steps · 62 tool calls · 27K output tokens (14K reasoning) · 2.8M tokens re-read · OpenRouter API, one-shot

Headline findings

Six things to know about this review

Holds up

The register the monitor never writes, found twice

The monitor’s codec refresh writes eight registers and not 0x34, so HRC6000SetFMRx’s 0x34 = 0x3C — “Compressor off, de-Emph on 3KHz Audio Filter” — stays in force for the session. The manual backs it: an audio filter and an optional de-emphasis module sit in the FM receive chain, 0x34[5] enables −6 dB/oct from 300 Hz to 3 kHz, and the low-pass choices are 2.55 and 3 kHz. Then it goes further than any other audit and checks the boot table: spi_init_values_7 is written to page 0x04 from register 0x11, and its 35th byte — the one that lands on 0x34 — is 0xE0, bit 5 set again. I verified that by index.

p25mon.c:642–656 · HR-C6000.c:146, 376, 2851 · manual 2795, 2802, 2806

New

It turns the manual’s 8 kHz rule into an experiment

Two audits found the constraint that the I2S frame clock must be 8KHz. This one asks the next question: if the codec is specified for an 8 kHz voice channel, does raising LRCK to 24 kHz widen the decimation filter, or just the frame rate? Its prediction is that the capture would show each sample three times, or an 8 kHz image — 24 kS/s carrying 4 kHz of real bandwidth. Nothing in the repository or the nine reviews before it had asked this, and one later audit reached the same question independently. It can be settled from a capture the project can already take.

manual 1127, 1218 · p25mon.c:82–85

Validated

It benchmarked the chain, and the numbers reproduce

It compiled the firmware’s own sources natively with vectorisation off and timed them: resample ~1%, demodulator ~13%, mbelib IMBE ~87%, the vocoder about 7× everything else combined. I rebuilt it: 0.0001 s, 0.0019 s and 0.0131 s per second of audio. Within 3%. It then declines to turn x86 time into a Cortex-M4 budget and points at the DWT counter already wired into the firmware. That caution is right, and it is also the reason it never finds the size of the gap: emulated, mbelib needs 11–16× the 72 MHz budget.

analysis/cpu_bench.c · mbelib.c:306 · p25mon.c:535, 752

Where it bets wrong

It assumes the codec is carrying radio audio

Its whole first finding is that the decoder is fed de-emphasised, filtered radio audio. The repository’s own analysis reaches the opposite conclusion: 0xE0 = 0xC9 selects LineIn1, the manual frames I2S as external-codec plumbing, stock firmware never routes the discriminator to I2S, and the bring-up camped on carriers without a sync. It lists the routing as unverified and puts a carrier-versus-microphone capture first in its plan, which is the right experiment. But if the samples are the microphone, the register it calls the highest-value fix changes nothing.

analysis/stock-p25/REPORT.md:175–176 · HR-C6000.c:2964 · STAGE3-BRINGUP.md:134

Major gap

Its measurements run through instruments it never checked

Gate 0 says: capture on a keyed carrier, measure the RF→PCM transfer function, sweep 0x34, re-measure. Every step of that goes through the capture export and tools/p25_capture.py. The export stages the contiguous first 80 halfwords of each row while the decoder reads [j][i*2], so 158 of 160 positions differ from what was decoded, and the parser certifies captures with missing chunks. The transfer function it would measure is of the wrong samples. No tooling defect appears anywhere in the review.

p25mon.c:443, 460–466 · tools/p25_capture.py:104–190 · tests/p25/run.py:264–270

Wrong premise, right conclusion

Phase 2, and the mode it dismisses

It rules Phase 2 out because H-CPM is quadrature and this path is scalar. A monitor receives the outbound channel, which is H-DQPSK; H-CPM is the inbound modulation, and being constant-envelope it is one a discriminator handles poorly rather than not at all. The conclusion survives on the signal it did not name. It also dismisses the chip’s one-layer mode as demodulated DMR frames, not raw symbols. The manual says 36 bytes per frame — 4,800 symbols per second, P25’s own rate. Whether the framing carries them is untested, not settled.

manual 2555–2560 · p25mon.c:415–416, 443

Scorecard

How the 77 breaks down

Same rubric, weights and scale as every P25 audit on bench, so scores compare directly. HY4 Preview led the field on four of the six dimensions and is held back by coverage.

Dimension scores

Hover or focus a bar for the reasoning. Weighted total 77, grade C+.

0255075100Score out of 100Accuracy & evidenceweight 30%85Coverage of decode problemsweight 25%56Root cause & prioritisationweight 15%80Fix plan & acceptance gatesweight 15%84Originality & attributionweight 10%82Clarity & calibrationweight 5%88
Rubric table
DimensionWeightScorePointsWhy
Accuracy & evidence30%8525.5Thirty claims checked, none wrong: 47 citations, 31 with line numbers, and every one I opened landed, down to the boot table’s 35th byte. Six are qualified, and the notable one is a Cortex-M4 instruction count it appears to have derived by halving a byte count.
Coverage of decode problems25%5614.0The best decode-critical coverage of the audits up to it — the de-emphasis register, the manual’s 8 kHz rule, the vocoder’s share, the circular test oracle. It finds no tooling defect at all, and no fault in the muting, the health gate or the call state.
Root cause & prioritisation15%8012.0It separates the signal problem from the work already done: the clock, the gating and the eco cycling were real fixes to a path carrying the wrong signal. The gap is that it ranks the filtering above the routing, when the routing decides whether the filtering matters.
Fix plan & acceptance gates15%8412.6Three gates with stop conditions, measurement before modification, and a named fallback — the only plan on this task that tells you when to abandon it. It leans on the capture tooling without checking it.
Originality & attribution10%828.2It wrote and ran a benchmark, sized the hot function by cross-compiling, and turned the manual’s 8 kHz rule into a prediction — sample triplication — that no audit or document had made. No new defect in the code itself.
Clarity & calibration5%884.4Ranked findings with severities, a traced signal path, and two sections most reviews leave out: what it verified by running, and what it could not verify. Its uncertainty is marked where it belongs.
Weighted total100%77Grade 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

HY4 Preview’s findings cluster on the conditioning and the compute: the filters in the capture path, the frame clock above them, and the vocoder at the end, which it measured. The red tags are the routing question it ranked second and the instruments it never opened.

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 tapF1F2F3F4N1N2N3N4N5N6
F1 raised by HY4 PreviewN1 missed or ranked secondDashed edge: no documented connection
F1HR-C6000 de-emphasis: 0x34 is never written, twice over
N1The I2S stream is most likely the microphone
F2The manual’s 8 kHz frame clock rule, and what 24 kHz buys
N2AT1846S voice filters and low-frequency bit
F3The vocoder is 87% of the chain (measured)
N3The capture stages the wrong halfwords
F4The test suite validates against its own channel model
N4The parser certifies incomplete captures
N5pcm_starve never counts
N6Calls the linear resampler “not free”
The register it found

What de-emphasis costs, measured

Its first finding is that 0x34 keeps de-emphasis and a 3 kHz filter in the capture path all session. The model puts a number on that, and the number is worse than its estimate of 20 dB of tilt.

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 fix — add 0x34 to the refresh and sweep it — is the right move, and the AT1846S filter set it never opens is the other half.
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

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.
The measurement it made

Right ranking, unfinished budget

It is the only audit to time the chain rather than argue about it. The ranking holds. The step it declined to take — converting that ranking into a budget on the target — is where the wall it never names sits.

The ranking it measured, checked against the target

Left: its analysis/cpu_bench.c rebuilt against the same firmware sources and rerun here. Right: the same three stages counted as Cortex-M4 instructions under emulation, where the vocoder’s share rises because the host’s floating-point and trig are far cheaper than the MCU’s.

  • Its benchmark, rerun (x86)
  • Cortex-M4 instruction counts
0255075100Share of the chainp25_resample12Demodulator1338mbelib IMBE8796
Its conclusion — the vocoder is the only stage where effort matters — is correct and conservative. What the host measurement cannot show is that the vocoder needs 11–16× the whole budget, which is the difference between an optimisation and a blocker.
Data table
StageIts benchmark, rerunCortex-M4 counts
p25_resample0.6%1.8%
Demodulator12.7%38.5%
mbelib IMBE87.3%96.3%

Its own figures were 1%, 13% and 87%. Rerunning gave 0.0001 s, 0.0019 s and 0.0131 s per second of audio.

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

30 claims, checked one by one

Twenty-four hold, four of them validated by rerunning what it ran. Six are qualified. None is wrong, which has happened once before in ten audits.

ItemWhat HY4 Preview claimsWhereResultNotes
§2The receive path is AT1846S FM → C6000 FM channel → codec → I2S slave RX with DMA, resample, demodulate, mbelib, 8 kHz PCM ringp25mon.c:432, 599, 604; p25rx.c:105; main.c:453HoldsThe trace is correct end to end, and every line number lands.
§2The monitor's codec refresh writes 0x06, 0x36, 0xE0, 0x26, 0x40/0x41, 0x5F, 0x2F, 0x32/0x33 — and never 0x34p25mon.c:642–656HoldsEvery value in its table matches, including the exact comment “Undocumented register Turns on FM receive” for 0x26=0xFD. The absence of 0x34 is real and is the review’s central point.
§3 P1-10x34 keeps its stock value 0x3C, so de-emphasis (bit 5) and the 3 kHz audio filter stay in force for the whole sessionHR-C6000.c:2851; radioHardwareInterface.c:185, 403HoldsHRC6000SetFMRx writes 0x34=0x3C with the comment “Compressor off, de-Emph on 3KHz Audio Filter”, and the monitor takes the analog RX path that calls it.
§3 P1-1The init table spi_init_values_7 also covers 0x34, with 0xE0 — bit 5 set againHR-C6000.c:146, 376ValidatedThe table is written to page 0x04 from register 0x11 for 0x2C bytes, so 0x34 is element 35, and element 35 is 0xE0. Checked by index; no other audit has read this table.
§3 P1-1The manual puts an audio filter and optional de-emphasis in the FM receive chain, with 2.55 kHz and 3 kHz low-pass options and 0x34[5] enabling −6 dB/oct de-emphasis over 300 Hz–3 kHzmanual 2795, 2802–2803, 2806HoldsAll three quotations are accurate; the low-pass quote sits at 2802 rather than the 2800 it cites.
§3 P1-1C4FM needs about 2.9 kHz of flat baseband ((1+α)·4800/2), and de-emphasis imposes roughly 20 dB of tilt across it, collapsing the four-level eyep25_4fsk.h:9–10ValidatedThe arithmetic is right (3.32 octaves × 6 dB), and the firmware-in-the-loop model is harsher than its estimate: de-emphasis alone leaves 0 of 180 frames decodable.
§3 P1-1This is “the single highest-value fix”HoldsMatches the model, with one caveat the review states itself: it assumes the codec is carrying RF audio at all.
§1The decoder is fed conditioned audio from the C6000’s analog FM receive channel, rather than the microphoneanalysis/stock-p25/REPORT.md:175–176DoubtfulThe repository’s own analysis says the opposite is more likely: 0xE0=0xC9 selects LineIn1, the manual frames I2S as external-codec plumbing, and the stock firmware never routes the discriminator to I2S. The review flags the routing as unverified and puts a carrier-versus-microphone test first in its plan, but its headline finding rests on the weaker hypothesis.
§3 P1-2The manual specifies the I2S frame clock for 8 kHz voice: “the configured I2S_FS_M clock frequency must be 8KHz”, and 8 kHz is the defaultmanual 1218, 1127HoldsQuoted exactly. Only one other audit found this constraint.
§3 P1-2Raising LRCK to 24 kHz changes the frame rate but may not widen the codec’s decimation filter; test the capture for 8 kHz triplication or a spectral imagep25mon.c:82–85; STAGE3-BRINGUP.mdUnverifiedNo audit or document has tested this, and it cannot be settled without hardware. It is the sharpest new hypothesis in any clean-room review: it takes the manual’s 8 kHz rule and turns it into a measurement on a capture the project can already take.
§3 P1-3The linear-interpolation resampler droops in the passband and passes images, so it is “not free” and stacks on top of P1-1p25_resample.h:2OverstatedRerun through the firmware’s own resampler, 24 kHz decodes the same frames as a direct 38.4 kHz path with equal or fewer symbol errors and images 41 dB down. The cost is not measurable at this SNR. Its fix is conditional, so the plan does not turn on it.
§3 P1-4The MCU runs at 72 MHz (PLLM=4, PLLN=72, PLLP=DIV2 on an 8 MHz HSE), with 168 MHz available as a 2.3× leverCore/Src/main.c:271–273Holds
§3 P1-4Benchmarked natively: resample ~1%, demodulator ~13%, mbelib IMBE ~87% of the chain, the vocoder about 7× everything else combinedanalysis/cpu_bench.cValidatedRebuilt and rerun: resample 0.0001 s, demodulator 0.0019 s, vocoder 0.0131 s per second of audio, so 87% and 6.6×. On the target the share is higher still — the emulator puts mbelib at 96% of the chain — so its ranking is conservative and correct.
§3 P1-4x86 cycles are not Cortex-M4F cycles, so the numbers rank the stages rather than budget them; measure on target with the DWT counterp25mon.c:535, 752HoldsThe instrumentation it points at exists and is reported by the r command. This is the right caution — and it is also why the review never reaches the size of the gap: emulated, mbelib needs 11–16× the 72 MHz budget, so the vocoder is not an optimisation target but a blocker.
§3 P1-4The hot function is mbe_synthesizeSpeechf, with nested loops doing a sinf/cosf per iteration, order 10⁴ trig evaluations per 20 ms framembelib.cValidatedThe emulator counts 13,072 cosf and 12,965 rem_pio2f calls per IMBE frame, and mbe_synthesizeSpeechf is 98.6% of mbelib’s instructions. It identified the right function and the right order of magnitude from static reading alone.
§3 P1-4mbe_synthesizeSpeechf is 1,744 bytes, about 870 static instructions for Cortex-M4Fmbelib.cMiscountedCross-compiled for cortex-m4 with fpv4-sp-d16, the function is 1,796 bytes at -O2 and 1,336 at -Os, so the size is close. The instruction count is not: 522 at -O2. 1,744 ÷ 2 = 872 suggests it halved the byte count, but Thumb-2 mixes 16- and 32-bit encodings.
§3 P1-4p25mon.c already initialises the vocoder at uvquality = 1, the cheapest settingp25mon.c:749Holds
§3 P1-5The suite passes but validates the DSP against its own channel model, which contains no de-emphasis or 3 kHz filter, so it cannot detect P1-1p25_4fsk.h:11–12; tests/p25/run.pyHoldsThe header says exactly that, and the point is the one the project needs to hear. It does not add that the generator shapes with the receiver’s own RRC filter, which is the sharper half of the circularity.
§3 P1-6The clock latch, eco power-cycling, slot gating and silent SPI0 failures are documented and handled; the open item is suspending hrc6000TaskSTAGE3-BRINGUP.md; p25mon.c:622–623HoldsA fair reading of the bring-up log, and the only audit to credit the work already done rather than re-reporting it as new.
§4There is no Phase 2 support in the tree: mbelib’s ambe3600x2450.c is present but never called, and p25rx.c uses only mbe_processImbe7200x4400Framep25rx.c:105; mbelib/ambe3600x2450.cHoldsCorrect, and the opposite of the previous audit’s claim that the Phase 2 vocoder is missing entirely.
§4p25_tables.h defines only the Phase 1 DUID setp25_tables.h:85–92Holds
§4 P2-1Phase 2 “uses H-CPM, a quadrature modulation … it cannot be recovered from a scalar FM discriminator stream at all”§4 P2-1MislabelledA monitor receives the outbound channel, which is H-DQPSK; H-CPM is the inbound, subscriber-to-repeater modulation. And CPM is constant-envelope and frequency-based, so a discriminator is a poor demodulator for it rather than a useless one. The conclusion — this path cannot carry Phase 2 — holds, but on the linear outbound signal it did not name.
§4 P2-1p25monI2sHalfBuffer reads only the left channel and the fill routine hard-zeroes the right, so a stereo pair would be half discardedp25mon.c:415–416, 443HoldsBoth lines are exactly as described. The codec is mono here, so nothing is lost today, and the review presents it as conditional.
§4 P2-2The C6000’s one-layer mode “gives you demodulated DMR frames, not raw symbols, so it is not a bypass route either”manual 2555–2560OverstatedThe manual says the chip stores 36 demodulated bytes per frame in receive RAM at 0x30, which is 4,800 symbols per second — P25 Phase 1’s own rate. Whether those frames carry symbols without DMR sync is untested, not settled; this is the one untried hardware route and it is dismissed in a sentence.
§4 P2-2CAPABILITY-REPORT.md already corrected the earlier claim that MMDVM_HS implied C6000 multimode capability, because that project uses ADF7021CAPABILITY-REPORT.md:37–41HoldsAccurate, and it is the only audit to notice the repository correcting one of its own earlier reviews.
§4 P2-3Phase 2 would need slot timing, a different MAC and FEC, and the AMBE+2 3600×2450 vocoder that mbelib suppliesref-sources/dsd-fme; mbelibHolds
§4The M17 modification (AT1846 pin 9 → MCU pin 18) is the only route, and the bring-up log already identifies itCAPABILITY-REPORT.md:43–47; STAGE3-BRINGUP.md:107HoldsRight, though the bring-up reference is at line 107 rather than the 104–105 it gives.
§5Gate 0: measure the RF→PCM transfer function from a capture before changing anything, then sweep 0x34, then check for 8 kHz triplicationp25mon.c capture command; tools/p25_capture.pyIncompleteThe right first move, but it runs through instruments the review never checked: the capture export stages the contiguous first 80 halfwords of each row while the decoder reads [j][i*2], so 158 of 160 positions differ, and the parser certifies incomplete captures. Gate 0 would measure a transfer function of the wrong samples.
§5If the post-fix bandwidth is not flat to ~3 kHz, stop and go to the hardware modificationSTAGE3-BRINGUP.md:107HoldsThe only audit to write a stop condition into its own plan.
§6tests/p25/run.py passes end to end, a 100+ case suite, and the CPU benchmark builds and runstests/p25/run.pyHoldsThe suite prints 78 PASS lines over far more than 100 individual cases, and its benchmark reproduces here within 3%.
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. HY4 Preview raises fewer rows than most, and is the only one to reach the boot table behind the de-emphasis register and to make the frame-clock rule testable.

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
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
Capture parser certifies an incomplete streamabsentabsentabsentabsent
trusts the parser
absent
relies on the parser
absentabsentabsent
relies on the parser
absentabsentabsentabsent
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
absent
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
absent
The I2S stream is most likely microphone audio missed or ranked secondpartly
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
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
AT1846S FM filters, low-frequency bit, 25 kHz missed or ranked secondpartly
“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
absent
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
Manual: I2S frame clock “must be 8KHz”absentraised
§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
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
±10% health gate versus ±1% timing clampabsentabsentraised
B3, impact overstated
raised
P1-3
raised
F3
absentabsentabsentabsentabsentabsentabsent
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
absent
Non-standard MFID mutes clear callsabsentabsentabsentabsentabsentabsentabsentabsentabsentabsentabsent
certifies it as correct
absent
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
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
Vocoder needs 11–16× the 72 MHz CPU missed or ranked secondpartly
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
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
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
Two unverified SPI writes per decoded 20 ms frameabsentabsentabsentabsentabsent
treats them as protection
absentabsentabsentabsentabsentpartly
the SPI0inUse mechanism
partly
silent SPI0 failures
Capture sessions lack epochsabsentabsentpartly
measured=0 only
absentabsentraised
F9, 8 kHz under a 24 kHz header
absentabsentabsentabsentabsentabsent
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
absentabsentabsent
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”
absentabsentabsent
Clock-config writes bypass the verified SPI writerpartly
SPI retry note
absentraised
M1
absentabsentraised
F9
absentabsentraised
§4.2
absentabsentabsent
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”
absentabsentabsent
Stale clear-call state releases a new call’s first framesabsentabsentabsentabsentabsentraised
F8, probe
absentabsentabsentabsentabsentabsent
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
absent
Unknown talkgroup opens audio (fail-open gating)absentabsentabsentabsentabsentabsentabsentabsentabsentabsentabsentabsent

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 24 kHz bandwidth question this review adds is not a row here: no earlier audit or document raised it.

Its plan

What to keep, change and add

Three gates, each with a stop condition. It is the only review on this task that says what to do when its own plan fails, and the only one that refuses to spend effort on Phase 2 before the first gate passes.

Keep

Worth doing as written

  • Gate 0 before anything else: measure the RF→PCM transfer function on a keyed carrier and on microphone excitation, which is also the test that settles the routing question.
  • Add 0x34 to the codec refresh and sweep de-emphasis off with the widest filter, re-measuring after each change.
  • The stop condition: if the bandwidth is not flat to ~3 kHz, abandon the codec path and take the discriminator tap.
  • Measure the vocoder on target with the DWT counter already in the firmware, instead of arguing about it.
Change

Would cause new problems

  • Fix the capture export and parser first: every Gate 0 measurement runs through them, and both are broken.
  • The CPU option order: 168 MHz is 2.3× against an 11–16× gap; the vocoder rework is not optional.
  • Dismissing the one-layer mode in a sentence: it is the one untried route, at P25’s own symbol rate.
Add

Missing from its plan

  • The AT1846S half of the front end: the DMR filter set, which its analysis never reaches.
  • The shared-RRC oracle: its hostile-channel test should also stop the generator from using the receiver’s own filter.

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 right answer, from the wrong modulation

It is right that Phase 2 is out of reach here, right that nothing in the tree implements it, and right — against the previous audit — that mbelib’s ambe3600x2450.c is present but never called. It verifies that only Phase 1 DUIDs are defined, and it recommends not attempting Phase 2 at all, which is the honest answer.

The argument it uses is H-CPM: a quadrature modulation, so a scalar discriminator stream cannot carry it. A monitor listens to the outbound channel, which is H-DQPSK; H-CPM is what a subscriber transmits. And because CPM is constant-envelope, a discriminator is a poor demodulator for it rather than a useless one. The conclusion holds — on the linear outbound signal the review did not name.

Against the other clean-room audits

The best of the audits before it, and the narrowest sweep

HY4 Preview is the first audit to build a measurement instead of an argument, the first to write stop conditions into its plan, and the second to make no wrong claim. It is also, in the top half of the table, the only one to find no defect in the capture path, the muting or the call state — the work UNIONALPHA did and this review does not.

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

Scores by dimension

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

  • UNIONALPHA
  • Grok 4.6
  • Gemini 3.8 Flash
  • HY4 Preview
0255075100Score out of 100Accuracy & evidence90746585Coverage of decode problems65594656Root cause & prioritisation60707480Fix plan & acceptance gates70757084Originality & attribution76727082Clarity & calibration80788488Weighted total74706477
Method and limits

How this was checked

I read HY4PREVIEW_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, including the boot table byte it counts on and every manual line it quotes. I rebuilt and reran the benchmark it left behind, against the same sources with the same flags, and cross-compiled mbelib for Cortex-M4 to check the size it gives for the hot function.

Before grading I audited the run: one model for all 40 calls, no network, no skills or memory, and nothing read outside its copy of the repository. It ran one-shot through OpenRouter with the same prompt, tools and sandbox as every other run. The de-emphasis, filter and instruction-count 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 24 kHz bandwidth hypothesis cannot be settled without hardware; it is recorded as untested, not as right or wrong.
  • No firmware was flashed, and no radio was touched.
gcc -O2 -fno-tree-vectorize -ffp-contract=off -DP25_DEMOD_WINDOW=768 \
    -I$P -I$P/mbelib -o cpu_bench analysis/cpu_bench.c $P/p25_*.c $P/mbelib/*.c -lm && ./cpu_bench
# resample 0.0001 s · demod 0.0019 s · vocoder 0.0131 s per 1 s audio -> 87%

arm-none-eabi-nm -S mbelib.o | grep synthesizeSpeechf
# 0x704 = 1796 bytes at -O2 (it said 1744); 522 instructions, not ~870

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