A hidden model was asked to 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.
I checked every claim against the code, reran its evidence, and modelled what it left out through the firmware’s own demodulator.
Overall grade
C−
65 / 100 weighted
FDCBA
Accurate to the line number. Silent on what stops the radio decoding.
Everything UNIONALPHA checked, it checked well. Its line references, test results, byte counts and reproductions all hold up.
The problem is what it chose to check. Its four “confirmed findings” cover a diagnostic export, a Python parser, a telemetry counter and the linker map,
and all four already sat, in the same order, in the repository’s Sep 10 review. None of them explains why the radio would fail to decode P25.
The configuration that most plausibly does is never mentioned. For a P25 session the monitor switches the receiver into analog FM mode:
the AT1846S high- and low-pass filters on, low-frequency audio blocked, 25 kHz channel bandwidth, and HR-C6000 de-emphasis enabled.
Modelled through the firmware’s demodulator, any audio high-pass at 75 Hz or above leaves zero decodable voice frames.
And even with a perfect signal, emulated instruction counts show the IMBE vocoder needs 11 to 16 times the radio’s 72 MHz CPU.
Claims that check out
21 / 23
2 overstated · 0 false
Confirmed findings already on record
4 / 4
F1–F4 of the Sep 10 review, uncited
Decode-critical issues missed
4
plus 2 moderate issues
Voice frames with a ≥ 75 Hz high-pass
0 / 180
firmware demodulator, noise-free model
Headline findings
Six things to know about this review
Holds up
What it checked is correct
The host suite passes with byte-identical output (104 lines). The capture-parser defect reproduces exactly. SRAM reservations end on 0x20020000 with CCM 1,160 bytes short of full.
The Golay Gray-code argument and the mismatch between a ±10% health gate and a ±1% timing clamp are both right.
p25mon.c:724 calls trxSetModeAndBandwidth(RADIO_MODE_ANALOG, true). That loads the AT1846S FM set: filters on (0x58=0xBC85), the “low freq audio” bit cleared (0x40=0x0030), AF gain 80%, 25 kHz.
HR-C6000 de-emphasis and a 3 kHz filter come on too. OpenGD77’s DMR set, built for the same 4800-baud 4FSK, does the opposite.
AT1846S.c:191–199 vs 204–217 · HR-C6000.c:2846–2854 · manual §6.2
Major gap
It treats the microphone as a coin flip
It calls the RF route unproven “either direction”, but the evidence leans. OpenGD77 reads the identical I2S slot for transmit audio and labels it “Mic Audio”.
Bit 6 of 0xE0, set in 0xC9, enables mic → LineIn1. The manual says the I2S frame clock “must be 8KHz”, yet the monitor runs it at 24 kHz.
sound.c:444 · HR-C6000.c:2849, 2872, 2964 · manual line 1218
Not original
Its four confirmed findings were already in the repo
F1 capture stride, F2 parser, F3 starvation counter and F4 memory map match F1–F4 of P25-PROJECT-REVIEW-2026-09-10.md in order and substance, down to the samples=66 fixture. Its C3 is that review’s F5.
None of it is cited, while that review’s analog-filter, test-oracle and 72 MHz points were dropped.
P25-PROJECT-REVIEW-2026-09-10.md §4 · file dated 10 Sep, six days earlier
Moderate
It accepted a circular test oracle
It cites “C4FM channel vectors”, but the generator uses the same RRC filter as the receiver. Against a standard C4FM waveform (raised cosine × inverse sinc), the noise-free eye closes from 0.93 to 0.37, costing about 2 dB.
A band-limited integrate-and-dump FIR restores it to 0.98.
The vocoder cannot run in real time, and it never says so
Its tick arithmetic stops at the sample ring. This port clocks the STM32F405 at 72 MHz (PLLN=72). In an emulated Cortex-M4F with the firmware’s own compiler and newlib, the demodulator uses about 40% of that core, but mbelib’s IMBE synthesis needs 11–16 times all of it. A loud 20 ms frame costs roughly 430 ms of CPU, because unwrapped harmonic phases push cosf onto newlib’s slow large-argument path.
Weights reflect the question asked: find what stops P25 decoding and say how to fix it. Accuracy carries the most weight because a wrong review does harm; coverage and root cause together outweigh it because an accurate review of the wrong things does not help.
Dimension scores
Hover or focus a bar for the reasoning. Weighted total 65, grade C−.
Rubric table
Dimension
Weight
Score
Points
Why
Accuracy & evidence
30%
90
27.0
Every checked line reference, test result and byte count holds; the evidence bundle reproduces. Two overstatements: RRC test vectors called “C4FM channel vectors”, trellis coverage described as independently referenced.
Coverage of decode problems
25%
45
11.2
Misses the analog-FM receive chain, the microphone evidence, the manual’s 8 kHz frame-clock limit and the circular RRC test oracle, and never quantifies that the IMBE vocoder needs 11–16× the 72 MHz CPU. Its confirmed findings are about tooling, telemetry and the linker map.
Root cause & prioritisation
15%
55
8.2
Fixing the instruments first is sound, but two software-fixable blockers (the analog-FM settings and a vocoder an order of magnitude over budget) are absent, and the RF-route question is framed as unproven “either direction” despite evidence that leans to the microphone.
Fix plan & acceptance gates
15%
74
11.1
Clear phases with measurable gates. Missing the cheapest high-value step (a flat receive configuration before any RF test), a concrete receive-filter change, and the tap precedent the repo already documents.
Originality & attribution
10%
40
4.0
F1–F4 match F1–F4 of the repo’s Sep 10 review in order and substance, C3 matches its F5, with the same parser fixture; none of it is cited. Genuinely new: the gate-versus-clamp mismatch, the Golay check, a fuller Phase 2 section.
Clarity & calibration
5%
76
3.8
Scrupulous about what it does not claim. Over-hedged where evidence leans one way, and dense enough to slow a decision-maker.
Weighted total
100%
65
Grade scale: A ≥ 90 · B 80–89 · C+ 75–79 · C 70–74 · C− 65–69 · D 50–64 · F < 50
Receive chain
Where the problems sit
The monitor’s intended path runs from RF to speaker; the capture export branches off at the DMA interrupt for diagnostics. UNIONALPHA’s issues cluster on that diagnostic branch. The issues it missed sit on the path the voice actually takes.
F1 raised by UNIONALPHAM1 missed by UNIONALPHADashed edge: no documented connection
M2Evidence that the codec ADC samples the microphone
F2Parser certifies incomplete captures
M3Manual: I2S frame clock must be 8 kHz
F3pcm_starve never increments
M4RRC receive filter validated only against itself
F4Zero static RAM margin
M5IMBE vocoder needs 11–16× the 72 MHz CPU
C1Tick budget unmeasured
M6Two SPI writes per decoded voice frame
C2±10% gate versus ±1% clamp
C3Capture epochs missing
The missed blocker, measured
C4FM does not survive a voice audio path
P25 Phase 1 is four-level FSK at 4800 symbols per second, and its baseband reaches almost down to DC. Runs of equal symbols, such as the frame sync’s five +3s, carry energy that any audio high-pass removes, and the filter’s phase shift smears neighbouring symbols. OpenGD77 already knows this: its DMR mode sets the AT1846S “magic” low-frequency bit and bypasses its filters for the same symbol rate. The P25 monitor selects the FM set instead.
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.
Voice frames decoded versus audio high-pass corner
Standard C4FM through a 2nd-order Butterworth high-pass, then the firmware’s live demodulator and framer. 20 back-to-back LDUs: 180 IMBE frames and 20 NIDs possible.
Noise-free
20 dB SNR
Typical voice high-pass range
Decoding holds up to 20 Hz, loses a quarter of its frames at 35 Hz, and fails completely from 75 Hz. Filters that reject CTCSS tones (67–254 Hz) sit well past that point, so the exact AT1846S corner, which is undocumented, doesn’t change the conclusion.Data table
Corner (Hz)
Noise-free frames · NIDs
Noise-free SER
20 dB frames · NIDs
20 dB SER
off
100% · 20/20
0.00%
100% · 20/20
0.00%
10
100% · 20/20
0.00%
100% · 20/20
0.01%
20
100% · 20/20
0.13%
100% · 20/20
0.23%
35
75% · 15/20
2.31%
60% · 12/20
2.80%
50
10% · 2/20
7.04%
20% · 4/20
7.45%
75
0% · 0/20
15.17%
0% · 0/20
15.37%
100
0% · 0/20
21.29%
0% · 0/20
21.70%
150
0% · 0/20
30.21%
0% · 0/20
30.58%
200
0% · 0/20
36.59%
0% · 0/20
36.19%
300
0% · 0/20
44.46%
0% · 0/20
44.67%
Eye diagrams at the slicer
Matched-filter output folded over two symbols at the ideal sampling instant, normalised so the four levels sit at ±1 and ±3. Horizontal hairlines mark levels and decision thresholds.
Test model
RRC transmit → firmware RRC
opening 0.93180/180 frames · 20/20 NIDs
Standard C4FM
→ firmware RRC (as shipped)
opening 0.36180/180 frames · 20/20 NIDs
Standard C4FM
→ band-limited integrate-and-dump FIR
opening 0.98180/180 frames · 20/20 NIDs
+ 150 Hz high-pass
C4FM → firmware RRC
eye closed0/180 frames · 0/20 NIDs
+ analog FM chain
HPF 300 Hz + LPF 3 kHz + de-emphasis
eye closed0/180 frames · 0/20 NIDs
The first panel is what the test suite validates. The last two are what a voice audio path would feed the same code. “Opening” is the worst-case gap between neighbouring levels across 17,200 symbols; ≤ 0 means at least one decision is wrong even without noise.All eight modelled cases
Case
Signal → receive filter
Eye
Noise-free frames · NIDs
Noise-free SER
20 dB frames · SER
14 dB frames · SER
S0
Project test model: RRC transmit -> firmware RRC
+0.93
180/180 · 20/20
0.00%
180/180 · 0.00%
180/180 · 0.00%
S1
Standard C4FM -> firmware RRC (as shipped)
+0.36
180/180 · 20/20
0.00%
180/180 · 0.00%
180/180 · 0.05%
S2
Standard C4FM -> integrate-and-dump (TIA reference)
+0.96
180/180 · 20/20
0.00%
180/180 · 0.04%
180/180 · 0.38%
S2b
Standard C4FM -> band-limited integrate-and-dump FIR (81 taps)
The tests only prove the receiver agrees with itself
The self-test and timing sweeps synthesise the transmitter with the receiver’s own 81-tap root-raised-cosine filter, at 40 dB SNR in the timing sweeps. Standard C4FM uses a full raised cosine with inverse-sinc compensation, and the reference receiver integrates and dumps. The shipped filter still decodes clean signals, but the tests hide its margin loss.
Symbol error rate versus SNR
Firmware demodulator and framer, white noise at the discriminator. Hollow markers sit on the floor where no errors were seen in 51,600 symbols.
Test model → shipped RRC
Standard C4FM → shipped RRC
Standard C4FM → band-limited integrate-and-dump FIR
At 0.2% symbol errors the test model needs 10 dB and standard C4FM needs 12 dB. A band-limited integrate-and-dump FIR removes the inter-symbol interference (eye 0.98) and matches the RRC under white noise, so the fix is mostly a better oracle plus the correct filter, not a rescue.Data table
SNR (dB)
Test model → RRC
C4FM → RRC
C4FM → band-limited I&D
C4FM → 8-sample I&D
6
4.238% · 174
6.496% · 129
7.008% · 150
11.880% · 90
8
1.279% · 180
2.802% · 177
3.306% · 171
6.246% · 153
10
0.182% · 180
0.897% · 177
0.975% · 180
2.709% · 168
12
0.012% · 180
0.223% · 180
0.194% · 180
0.955% · 174
14
0 · 180
0.045% · 180
0.027% · 180
0.576% · 174
16
0 · 180
0.006% · 180
0 · 180
0.184% · 177
20
0 · 180
0 · 180
0 · 180
0.054% · 180
Each cell: symbol error rate · mean IMBE frames decoded of 180.
Why: the filters side by side
Magnitude responses. The C4FM transmitter boosts toward 2.88 kHz to pre-compensate the integrate-and-dump receiver; the voice-path model cuts the low end and rolls off above 300 Hz.
C4FM transmit shaping (RC × inverse sinc)
Firmware RRC receive filter
Band-limited integrate-and-dump FIR
Analog-FM path model (HPF + LPF + de-emphasis)
C4FM occupied baseband
The analog-FM model assumes a 300 Hz 2nd-order high-pass, a 3 kHz 4th-order low-pass and first-order 300 Hz de-emphasis, matching the manual’s described slope. The real AT1846S corners are not documented; the high-pass sweep above shows the conclusion does not hinge on them.Data table (dB)
Hz
C4FM transmit shaping (RC × inverse sinc)
Firmware RRC receive filter
Band-limited integrate-and-dump FIR
Analog-FM path model (HPF + LPF + de-emphasis)
100
+0.0
-0.0
-0.0
-19.6
199
+0.0
-0.0
-0.0
-9.5
301
+0.1
-0.0
-0.1
-6.0
601
+0.2
-0.0
-0.2
-7.3
998
+0.6
+0.0
-0.6
-10.9
1902
+2.4
+0.0
-2.4
-16.3
2394
-1.9
-3.0
-3.9
-18.8
2878
< −40
-22.0
-6.7
-22.2
3973
< −40
< −40
< −40
-33.5
CPU and memory
The vocoder needs an order of magnitude more CPU
UNIONALPHA’s memory finding is exactly right: static reservations fill SRAM to the last byte. Its real-time discussion stops at ring arithmetic, which is where the budget holds. This port runs the Cortex-M4F at 72 MHz rather than the part’s 168 MHz, and even a 168 MHz clock would leave the vocoder several times over budget.
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
Static memory after linker reservations
Fresh ELF section table: _ebss = 0x2001F9FC, end = 0x2001FA00, plus 0x600 of heap and stack reservations = 0x20020000.
.data / .ccmram
.bss
newlib heap + alignment
main stack
Link success is not runtime headroom: task stacks come from the 20,480-byte FreeRTOS heap inside .bss, and interrupts share the 1 KiB main stack.
Claim check
23 claims, checked one by one
Twenty-one hold as written, some with omissions noted. Two are overstated. None is false.
Item
What UNIONALPHA claims
Where
Result
Notes
F1
Live decoder reads even (left-slot) halfwords; the capture export stages the contiguous first 80 of each row, so 158 of 160 positions differ
p25mon.c:432–467, 443, 464–465
Holds
Code matches; OpenGD77’s own mic path (sound.c:444) reads the same [j][i*2] slot.
F2
Parser anchors to the first observed sequence number and never reconciles the footer’s samples= count; missing chunk 0 exits 0 with complete=True
tools/p25_capture.py:104–190, 218–222
Holds
Reproduced. A lost final chunk passes too (not mentioned, but covered by the proposed fix).
F3
p25monAudioUnderruns (pcm_starve) is declared and printed but never incremented
p25mon.c:123, 517
Holds
Two references firmware-wide; STAGE3-BRINGUP.md documents the counter as working.
F4
SRAM .data 17,000 + .bss 112,532 + heap/stack 1,540 = 131,072 bytes, ending at 0x20020000; CCM 64,376 of 65,536
build/MDUV380_FW.elf
Holds
_ebss 0x2001F9FC, end 0x2001FA00, 0x600 reserved; ucHeap 20,480 in .bss from heap_4.o.
G1
Monitor writes 0xE0=0xC9 and the codec refresh set; the source itself tags the route UNVERIFIED
p25mon.c:642–656, 648, 733, 519, 699–700, 1345
Holds
The refresh list omits 0x06=0x21, which is harmless.
G1
Manual §4.5 and §6.2 document no discriminator-to-I2S tap
HR_C6000_user_manual.txt
Incomplete
True, but §4.6 also says the I2S frame clock “must be 8KHz”, and §6.2 puts de-emphasis before the codec.
G1
HR-C6000.c input labels are community reverse-engineering, not datasheet proof
HR-C6000.c:2778, 2872, 2964
Understated
Those labels say mic → LineIn1 via E0 bit 6, and sound.c:444 calls the same I2S slot “Mic Audio”.
G1
ref-sources/openrtx_hrc6000.cpp contains no 0xE0 writes
ref-sources/openrtx_hrc6000.cpp
Holds
grep finds none.
C1
Drain ≤ 4×32 = 128 samples per tick, needing 187.5 calls/s at 24 kS/s against a 512-sample ring in a 1 ms super-loop; synthesis load unmeasured
NID/status handling corrected: 35 data + 1 status per 36 dibits; LDU = 784 payload + 24 sync + 32 NID, 864 on air
p25rx.c:347–355
Holds
Checked the full LDU1/LDU2 layout, HDU/TDULC lengths and status placement.
P1
Golay decoder’s i ^ (i >> 1) is correct Gray-code enumeration, not a bug
p25_fec.c:65–70
Holds
g(i) ⊕ g(i−1) = 1 ≪ ctz(i), and the code is linear.
P1
Golay, Hamming and RS are checked against independent reference encoders
tests/p25/fec_reference.cpp; signalling.py:12–40
Holds
DSD’s Golay24, Hamming and ReedSolomon classes.
P1
Trellis, CRC and trunk coverage, acceptance plus rejection, listed under “independently referenced”
signalling.py:78–124
Overstated
The trellis test re-implements the production transition table, and trunk tests live in trunk.py.
P1
No confirmed new Phase 1 protocol bug
p25rx.c, p25_fec.c, p25_nid.c, p25_trunk.c
Agree
An independent read found none; non-standard MFID link control mutes audio by design.
V1
Host suite exits 0 with a 104-line log, selftest=PASS tests=111111
python3 tests/p25/run.py
Holds
Rerun is byte-identical apart from debug lines.
V1
Timing sweeps −1000…+1000 ppm, 0/6480 raw dibit errors; described as “C4FM channel vectors”
tests/p25/run.py, p25_selftest.c
Mislabelled
Numbers are right, but the generator is RRC-shaped, the same filter the receiver uses, at 40 dB SNR.
V1
Incremental relink produced an 810,252-byte image
build log
Holds
File size and timestamp match.
P2
Phase 2 needs H-DQPSK at 6000 sps, TDMA and ISCH, WACN/SYSID/NAC descrambling and AMBE+2 half-rate voice
§6
Holds
Misses that the radio already runs an AMBE+2 decoder for DMR.
P2
mbelib path is IMBE-only; TDMA identifier update 0x33 is rejected
p25rx.c:105; p25_trunk.c:26
Holds
Exact lines.
P2
Wider RS configurations are needed beyond the current bounded FEC
p25_fec.c:77, 104 (n ≤ 36)
Holds
Phase 2 uses codes up to n = 63 over GF(64).
Provenance
What was already known
Two earlier reviews sit in the same repository. UNIONALPHA plainly read the second: it corrects that review’s memory figure and reuses its parser fixture. What it kept, added and dropped:
Issue
P25-HARDWARE-REVIEW Sep 8
P25-PROJECT-REVIEW Sep 10
UNIONALPHA Sep 16
Capture export stages the wrong I2S halfwords
absent
raised F1
raised F1
Capture parser certifies an incomplete stream (samples=66 fixture)
absent
raised F2
raised F2
pcm_starve never increments
partly asks for underrun counts
raised F3
raised F3
No static RAM margin
partly 648 bytes then
raised F4, 0 in workspace map
raised F4, 0 bytes
Capture sessions lack epochs
absent
raised F5
raised C3
Ring and tick real-time budget
raised
raised
raised C1
RF-to-I2S route unproven; OpenGD77 calls it mic/LineIn1
raised cites sound.c
raised
partly “either direction”
Analog FM mode: high/low-pass, de-emphasis, 25 kHz IF dropped
raised
raised
absent
Test waveform shares the receiver’s RRC filter dropped
raised
raised F6
absent
MCU runs at 72 MHz, not 168 MHz dropped
raised
raised
absent
Direct discriminator-to-ADC tap (M17 mod precedent)
raised with pins
raised
partly no precedent, no pins
±10% health gate versus ±1% symbol-period clamp
absent
absent
raised new
Golay Gray-code enumeration verified correct
absent
absent
raised new
Phase 2 requirements
partly scope note
partly scope note
raised detailed
Manual: I2S frame clock “must be 8KHz”
absent
absent
absent
Modelled decode loss from audio-path high-pass filtering
absent
absent
absent
Two unverified SPI writes per decoded 20 ms frame
absent
absent
absent
Vocoder cost measured: 11–16× the 72 MHz CPU
absent
partly cycles per frame, unmeasured
partly “synthesis load” to measure
The last four rows are new in this audit. Dropped rows were raised by both earlier reviews in the same repository and are absent from UNIONALPHA.
What to do instead
A fix plan that starts with the signal path
UNIONALPHA’s phases are a good skeleton. This version keeps its instrument fixes and gates, then does everything that can be verified in software before the RF experiments, so a negative result on the radio actually means something.
Agrees with UNIONALPHA phase 0
Make the instruments trustworthy
Stage i2s_Rx_Buffer[bufNum][j][i*2] for i < 80 in the capture export, exactly what the decoder consumes. Make p25_capture.py strict: sequence origin 0, footer samples= reconciliation, non-zero exit. Increment pcm_starve only during an active clear call.
GateA ramp through the real DMA adapter matches 160/160. Missing-first-chunk and missing-last-chunk fixtures fail. A forced dry ring counts, and intentional silence does not.
New
Give P25 a flat receive path before any RF test
For the P25 session, send the AT1846S the DMR register set over I2C: 0x40 low-frequency bit set, 0x58=0x9CDD, 0x44=0x07FF, 12.5 kHz bandwidth. Leave the HR-C6000 codec state the bring-up notes found fragile alone. Call radioSetAudioPath(false) once at start instead of on every decoded frame.
GateAT1846S register readback in the 1 Hz report. No new codec-clock dips over a 10-minute run.
New
Make the vocoder real-time
Replace mbelib’s per-sample cosf calls with a per-harmonic phase accumulator and a cosine table, and wrap phases every frame. Wrapping alone is not enough: even newlib’s fast path, at 40–100 instructions per call, leaves a loud frame near the whole core. The demodulator’s 81-tap FIR, at about 40%, is the next target; its symmetric taps halve the multiplies. A 168 MHz clock, after checking the USB and I2S clock trees, adds headroom but does not close an 11–16× gap.
GateOn host and emulator, decoded audio matches unmodified mbelib within a small tolerance. On the radio, DWT shows the worst voiced frame under 10 ms and zero ring overruns across a 10-minute voice replay.
New
Replace the self-referential oracle
Generate standard TIA C4FM vectors (raised cosine α = 0.2 × inverse sinc), add real recordings, and keep high-pass and DC-offset sweeps as regressions. Offer a band-limited integrate-and-dump FIR next to the RRC.
GateSymbol error rate on independent vectors within 1 dB of the ideal slicer. The high-pass regression fails loudly if someone reintroduces a voice filter.
Sharper than UNIONALPHA phase 1
Test the microphone hypothesis first, and time-box it
With the flat path and fixed capture, compare keyed RF against mic taps on both I2S slots. Also check the 24 kHz stream is really 24 kHz: energy above 4 kHz and no repeated samples, since the manual says the frame clock must be 8 kHz. Modulate a 20–30 Hz tone to confirm the low end survives.
GateRF-correlated samples with mic coupling at least 30 dB down and a flat 20 Hz–3 kHz response. Otherwise stop register experiments after two days.
More concrete than UNIONALPHA’s fallback
Build the discriminator tap the repo already points to
The repo’s own analysis cites the M17 DM-1701 modification: AT1846S pin 9 to MCU pin 18. For receive-only P25, keep the path DC-coupled or give it a single-digit-hertz corner, since the model loses frames by 35 Hz. Add anti-alias filtering, then timer-triggered ADC + DMA into SRAM (not CCM) at 24 or 48 kS/s. Keep the HR-C6000 as the speaker DAC only.
GateAn independently recorded clear P25 signal decodes with the expected NAC and talkgroup, repeatedly, with audio keeping pace.
Agrees with UNIONALPHA ordering
Only then: conventional calls, trunking, and Phase 2 as its own project
Qualify late entry, encrypted muting and terminators, then trunk following. Consider holding an established clear call through a single failed RS decode rather than muting nine frames. Treat Phase 2 separately, as below.
GateIntelligible known speech against a trusted receiver, then passive grant following on one in-band system.
Phase 2
Mostly right, one useful fact missing
UNIONALPHA’s Phase 2 section is accurate. Downlink H-DQPSK at 6000 symbols per second is a different modulation. TDMA slots, ISCH, descrambling seeded from WACN, System ID and NAC, FACCH/SACCH, and Reed–Solomon codes up to n = 63 are all absent. p25rx.c:105 is IMBE-only, and p25_trunk.c:26 invalidates TDMA identifier updates.
What it misses is that the vocoder is not the hard part. The radio already runs a licensed AMBE+2 half-rate decoder for DMR. DSD-FME, bundled in ref-sources/, sends P25 Phase 2 voice frames through the same 3600×2450 AMBE+2 path it uses for DMR (dsd_mbe.c, “DMR and P25p2” handling). The work is the demodulator, framing and descrambling.
A discriminator tap can, in principle, recover H-DQPSK phase steps by integrating frequency over each symbol, but with less margin than an IQ receiver. Plan the tap in step 4 to be wide and DC-coupled, and budget for IQ if Phase 2 matters.
Who wrote it?
A hunch, from the prose alone
The model’s name was hidden. I didn’t open any session logs, so this is a reading of the document’s style, not a lookup.
Best guess: an OpenAI GPT-5 family model, probably a Codex variant
Subjective probabilities, not a classifier score.
Negation as rigour. Non-claims appear throughout: No actual stack overflow is asserted, No pin prescription is offered here, with more than a dozen “X, not Y” antitheses. That anti-overclaiming register is characteristic of recent GPT-5 models.
Telegraphic compression. Noun-phrase fragments (Tagged lower priority next to F1/F2.), semicolon chains and a line reference in nearly every sentence. No tables and no first person in 2,200 words.
Workspace boilerplate. A dirty-tree preamble, “Scope: documentation only”, and a bold closing statement listing everything left untouched. That is the house style of Codex CLI runs.
Orchestration fingerprints. “Parallel source reviews”, “disputed findings were checked”, and an evidence index mentioning a brief and a tracker. A verifier loop that keeps only host-reproducible claims would also explain why hardware-configuration issues vanished.
Against the alternatives. Heavy em-dash use (38) is the one Claude-like trait. A Claude review of this length would usually carry summary tables and more explanatory prose, and a Gemini review would usually propose code patches; this one proposes none.
Method and limits
How this was checked
I read every P25 receiver source file in the integrated OpenGD77 tree (mbelib only in part), plus the OpenGD77 radio drivers they depend on: AT1846S, HR-C6000, trx, sound, I2S and application main. I also read the HR-C6000 manual translation, the stock-firmware analysis and the bring-up notes, and checked both earlier reviews. Each UNIONALPHA claim was checked at its cited lines.
The simulations compile the firmware’s own p25_4fsk.c, p25rx.c, NID, FEC and mbelib sources for the host exactly as tests/p25/run.py does. They drive them with 20 back-to-back LDUs built by that suite’s independent framing helpers. Only the waveform, the audio-path filters and, in scratch copies, the receive-filter taps were changed.
The CPU budget replays the firmware’s streaming loop in the Unicorn emulator. It uses the firmware’s compiler, flags, newlib and libm; instruction counts were cross-checked with Capstone, and 108 of 108 IMBE frames decoded inside the emulator.
Filter corners for the AT1846S and HR-C6000 are assumptions; the sweep shows the conclusion holds across them. A model is not a measurement.
Noise is white Gaussian at the discriminator. A real FM discriminator’s noise rises with frequency, and the 24 kHz ADC and resampler are not modelled.
CPU figures come from instruction counts in an emulator, not cycle-accurate timing on the radio.
No firmware was edited or flashed, and no hardware was touched.