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
FDCBA
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.
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.
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.
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.
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+.
Rubric table
Dimension
Weight
Score
Points
Why
Accuracy & evidence
30%
85
25.5
Thirty 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 problems
25%
56
14.0
The 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 & prioritisation
15%
80
12.0
It 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 gates
15%
84
12.6
Three 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 & attribution
10%
82
8.2
It 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 & calibration
5%
88
4.4
Ranked 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 total
100%
77
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
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.
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.
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 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
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.
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
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
Stage
Its benchmark, rerun
Cortex-M4 counts
p25_resample
0.6%
1.8%
Demodulator
12.7%
38.5%
mbelib IMBE
87.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.
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
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.
Item
What HY4 Preview claims
Where
Result
Notes
§2
The receive path is AT1846S FM → C6000 FM channel → codec → I2S slave RX with DMA, resample, demodulate, mbelib, 8 kHz PCM ring
p25mon.c:432, 599, 604; p25rx.c:105; main.c:453
Holds
The trace is correct end to end, and every line number lands.
§2
The monitor's codec refresh writes 0x06, 0x36, 0xE0, 0x26, 0x40/0x41, 0x5F, 0x2F, 0x32/0x33 — and never 0x34
p25mon.c:642–656
Holds
Every 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-1
0x34 keeps its stock value 0x3C, so de-emphasis (bit 5) and the 3 kHz audio filter stay in force for the whole session
HRC6000SetFMRx 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-1
The init table spi_init_values_7 also covers 0x34, with 0xE0 — bit 5 set again
HR-C6000.c:146, 376
Validated
The 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-1
The 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 kHz
manual 2795, 2802–2803, 2806
Holds
All three quotations are accurate; the low-pass quote sits at 2802 rather than the 2800 it cites.
§3 P1-1
C4FM 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 eye
p25_4fsk.h:9–10
Validated
The 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-1
This is “the single highest-value fix”
—
Holds
Matches the model, with one caveat the review states itself: it assumes the codec is carrying RF audio at all.
§1
The decoder is fed conditioned audio from the C6000’s analog FM receive channel, rather than the microphone
analysis/stock-p25/REPORT.md:175–176
Doubtful
The 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-2
The 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 default
manual 1218, 1127
Holds
Quoted exactly. Only one other audit found this constraint.
§3 P1-2
Raising 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 image
p25mon.c:82–85; STAGE3-BRINGUP.md
Unverified
No 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-3
The linear-interpolation resampler droops in the passband and passes images, so it is “not free” and stacks on top of P1-1
p25_resample.h:2
Overstated
Rerun 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-4
The MCU runs at 72 MHz (PLLM=4, PLLN=72, PLLP=DIV2 on an 8 MHz HSE), with 168 MHz available as a 2.3× lever
Core/Src/main.c:271–273
Holds
§3 P1-4
Benchmarked natively: resample ~1%, demodulator ~13%, mbelib IMBE ~87% of the chain, the vocoder about 7× everything else combined
analysis/cpu_bench.c
Validated
Rebuilt 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-4
x86 cycles are not Cortex-M4F cycles, so the numbers rank the stages rather than budget them; measure on target with the DWT counter
p25mon.c:535, 752
Holds
The 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-4
The hot function is mbe_synthesizeSpeechf, with nested loops doing a sinf/cosf per iteration, order 10⁴ trig evaluations per 20 ms frame
mbelib.c
Validated
The 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-4
mbe_synthesizeSpeechf is 1,744 bytes, about 870 static instructions for Cortex-M4F
mbelib.c
Miscounted
Cross-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-4
p25mon.c already initialises the vocoder at uvquality = 1, the cheapest setting
p25mon.c:749
Holds
§3 P1-5
The 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-1
p25_4fsk.h:11–12; tests/p25/run.py
Holds
The 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-6
The clock latch, eco power-cycling, slot gating and silent SPI0 failures are documented and handled; the open item is suspending hrc6000Task
STAGE3-BRINGUP.md; p25mon.c:622–623
Holds
A fair reading of the bring-up log, and the only audit to credit the work already done rather than re-reporting it as new.
§4
There is no Phase 2 support in the tree: mbelib’s ambe3600x2450.c is present but never called, and p25rx.c uses only mbe_processImbe7200x4400Frame
p25rx.c:105; mbelib/ambe3600x2450.c
Holds
Correct, and the opposite of the previous audit’s claim that the Phase 2 vocoder is missing entirely.
§4
p25_tables.h defines only the Phase 1 DUID set
p25_tables.h:85–92
Holds
§4 P2-1
Phase 2 “uses H-CPM, a quadrature modulation … it cannot be recovered from a scalar FM discriminator stream at all”
§4 P2-1
Mislabelled
A 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-1
p25monI2sHalfBuffer reads only the left channel and the fill routine hard-zeroes the right, so a stereo pair would be half discarded
p25mon.c:415–416, 443
Holds
Both lines are exactly as described. The codec is mono here, so nothing is lost today, and the review presents it as conditional.
§4 P2-2
The C6000’s one-layer mode “gives you demodulated DMR frames, not raw symbols, so it is not a bypass route either”
manual 2555–2560
Overstated
The 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-2
CAPABILITY-REPORT.md already corrected the earlier claim that MMDVM_HS implied C6000 multimode capability, because that project uses ADF7021
CAPABILITY-REPORT.md:37–41
Holds
Accurate, and it is the only audit to notice the repository correcting one of its own earlier reviews.
§4 P2-3
Phase 2 would need slot timing, a different MAC and FEC, and the AMBE+2 3600×2450 vocoder that mbelib supplies
ref-sources/dsd-fme; mbelib
Holds
§4
The M17 modification (AT1846 pin 9 → MCU pin 18) is the only route, and the bring-up log already identifies it
CAPABILITY-REPORT.md:43–47; STAGE3-BRINGUP.md:107
Holds
Right, though the bring-up reference is at line 107 rather than the 104–105 it gives.
§5
Gate 0: measure the RF→PCM transfer function from a capture before changing anything, then sweep 0x34, then check for 8 kHz triplication
p25mon.c capture command; tools/p25_capture.py
Incomplete
The 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.
§5
If the post-fix bandwidth is not flat to ~3 kHz, stop and go to the hardware modification
STAGE3-BRINGUP.md:107
Holds
The only audit to write a stop condition into its own plan.
§6
tests/p25/run.py passes end to end, a 100+ case suite, and the CPU benchmark builds and runs
tests/p25/run.py
Holds
The 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.
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
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
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
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
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
The I2S stream is most likely microphone audio missed or ranked second
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
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
AT1846S FM filters, low-frequency bit, 25 kHz missed or ranked second
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
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
Manual: I2S frame clock “must be 8KHz”
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
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
±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
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
Non-standard MFID mutes clear calls
absent
absent
absent
absent
absent
absent
absent
absent
absent
absent
absent certifies it as correct
absent
Test waveform shares the receiver’s RRC filter
partly “synthetic RRC/AWGN” caveat
absent
partly tested it, says not to fix
partly synthetic only, wants recordings
absent would extend that model
raised F5, unquantified
absent would extend that model
partly synthetic only, not the circularity
partly synthetic only
partly “ideal RRC-shaped signal”
absent
partly P1-5, circularity without the filter
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
Vocoder needs 11–16× the 72 MHz CPU missed or ranked second
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
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
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
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
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
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
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
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
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
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
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
Unknown talkgroup opens audio (fail-open gating)
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. 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.
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 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.
Same rubric and weights. Hover or focus a bar for the score.
UNIONALPHA
Grok 4.6
Gemini 3.8 Flash
HY4 Preview
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