P25 Phase 1 receive

P25 Phase 1 conventional receive and transmit: C4FM at 4800 baud, the frame sync, the Network ID, voice and the conventional call path. The receiver in core/p25/ starts from sliced symbols, so it works with any demodulator that supplies one of four deviations per symbol. Trunking is a separate path in the same tree.

Sources: TIA-102.BAAA (the common air interface) and a real conventional call, NAC 0x293, used to pin every constant below.

Contents
1 Modulation and symbols
2 Frame sync
3 Status symbols
4 Network ID and BCH
5 Validated receiver
6 Voice and what is open

1. Modulation and symbols

Phase 1 is four-level FM (C4FM) at 4800 symbols/s, two bits per symbol, 9600 bit/s gross. The four deviations are -1800, -600, +600 and +1800 Hz. A slicer returns one of the four symbol values below; the dibit mapping is fixed by the standard.

Deviation Symbol Dibit Bits, first sent first
-1800 Hz -3 11 1, 1
-600 Hz -1 10 1, 0
+600 Hz +1 00 0, 0
+1800 Hz +3 01 0, 1

An FM discriminator can invert the whole alphabet (a conjugated I/Q input). That is a rotation of every dibit by 2; it is the only rotation a C4FM stream can present. The sync search tries rotations 0 and 2.

2. Frame sync

Every data unit opens with the same 48-bit frame sync, 0x5575F5FF77FF, 24 dibits:

1 1 1 1 1 3 1 1 3 3 1 1 3 3 3 3 1 3 1 3 3 3 3 3

Only the outer symbols (-3 and +3) appear, so a receiver may correlate up to 4 mismatched dibits of 24 and still lock. The sync is contiguous: no status symbol is inserted inside it.

3. Status symbols

One 2-bit status symbol replaces every 36th on-air dibit. The count starts at the sync's first dibit, so the first status symbol is at dibit 35 from that point (synchronously: after 35 data dibits), then at 71, 107, and so on. The NID's 32 data dibits are therefore not the 32 that follow the sync on air: the status symbol at 35 must be removed first. A decoder that ignores this reads a plausible but wrong NID.

4. Network ID and BCH

The 64-bit NID follows the sync: a 63-bit BCH(63,16,11) codeword and a flag bit. The codeword is systematic: the 16 information bits are the 12-bit NAC and the 4-bit DUID, then 47 parity bits.

Field Bits Meaning
NAC 12 network access code
DUID 4 data unit ID: the frame type
BCH parity 47 BCH(63,16,11) over the first 63 bits
flag 1 1 for LDU1/LDU2, 0 for every other DUID

The code is binary BCH over GF(2^6) with primitive polynomial x^6 + x + 1 and roots alpha^1..alpha^22. Its generator is 0xCD930BDD3B2B; it corrects up to 11 bit errors. The 64th bit is not a parity bit: it is a fixed per-DUID flag, so a decoder must check it against the decoded DUID.

DUID Frame
0x0 HDU, voice call header
0x3 TDU, terminator
0x5 LDU1, voice with link control
0x7 TSDU, trunking signalling
0xA LDU2, voice with encryption sync
0xC PDU, packet data
0xF TDULC, terminator with link control

5. Validated receiver

core/p25/p25_phy.c and core/p25/p25_bch.c implement sections 1 to 4. They take sliced symbols, correlate the sync with both possible rotations, remove the status symbols, collect the 64 NID bits, BCH-decode them and report the NAC, DUID, corrected error count and flag. tests/host/p25PhyTest.c checks the BCH code to its correction radius and decodes two real LDUs (NAC 0x293, one LDU1 and one LDU2) with zero corrected bits, including the polarity-flipped case.

The receiver needs 4800 baud symbols from the radio. A modem with a raw-symbol or physical-layer receive path can supply them; the chip's registers and receive buffers are on its modem page. A software demodulator from discriminator audio is the fallback, but the audio must be wide enough for the 4FSK (the voice-band tap is not).

5.1 Bench probe

CONFIG_P25_PROBE builds a probe that puts the HR_C7000 into its physical-layer receive test mode with the P25 frame sync and logs the raw receive RAM (modem-hrc7000.md 2.11). Tune the radio to the channel and leave it idle, then rfoundry_debug.py radio p25 start, wait for a transmission, radio p25 dump, and log. radio p25 stop restores the modem. The switch needs CONFIG_DEBUGLINK and CONFIG_DMR, and on a full image a feature is dropped to make RAM room (a probe build can use CONFIG_APRS=n for the RAM; otherwise about 0.5 KB is free).

The dump answers the front-end question before any P25 stack work: whether the chip's deframer accepts the P25 sync, which of the four sync slots matched, and what bit order the 264-bit block comes out in.

5.2 Continuous-mode stream (bench, 2026-11-04)

The working set-up is WORK_MODE 0x02 (continuous) with the P25 frame sync in the MS/BS/TDMA receive-sync registers; 0x22 (timeslot) reported slot-abnormal on a P25 control channel. On SYS_INTERP_LIST == 0x800 (PHY_RX; the datasheet's figure 71) the chip appends 33 bytes (264 bits) at RX RAM 0x33; PHY_RDY (0x1000) is the 30 ms slot clock and must not be taken for a block, which is what made an earlier probe read a stale block forever.

The 264-bit chunks are a sliding window of the demodulated symbol stream, not frame-aligned. The first chunk after start is sync-aligned: the 48-bit sync sits at bit 20 (the two 10-bit slot-type fields of the datasheet's SyncType = 2 layout come first), and the NID's post-sync info starts 88 bits after the sync, i.e. bit 156 of the chunk. The 88-bit gap is DMR-shaped filler the chip leaves between the sync and the info; a wide search for the NID also lands on a false BCH codeword inside it, so the decoder pins the offset. The P25 status symbol at NID dibit 11 is present in the info and must be dropped before the BCH decode, exactly as in section 3.

With the probe accumulating seven consecutive chunks, every capture decoded NAC 0x293, DUID 0x7 (TSDU) with 3-4 corrected bits; the radio's p25p nid log line repeated it on every dump. The BCH decoder is core/p25/p25_bch.c, linked only with CONFIG_P25_PROBE.

6. Voice and what is open

Item State
NID, NAC, DUID Done and on-air checked against a capture
LDU voice framing Done: the LDU structure, deinterleaving and the Golay, Hamming and trellis codes (p25_rx)
IMBE voice Done: the fixed-point IMBE vocoder in core/p25/imbe decodes and encodes 7200x4400 (GPL-3.0)
TSBK Done and on-air checked: core/p25/p25_tsbk.c decodes the 196-bit block with valid CRC (7)
Control channel Done: the monitor and grant parser use the continuous discriminator receiver (section 8)
Symbol source The wide discriminator, through the software demodulator (p25_cc_rx, p25_demod); the HR_C7000 PHY capture is bench-only
Conventional transmit Done: microphone PCM, IMBE, LDU framing and the PHY transmitter (p25_tx_voice, section 9)

The IMBE vocoder is the only part that is not specified by the channel. The fixed-point source in core/p25/imbe is GPL-3.0-or-later; its notices travel with the tree.

7. TSBK

A TSBK is the 196 bits that follow the NID in a TSDU. It is a 1/2-rate trellis code over 49 four-bit symbols, then a 98-dibit interleaver, and its 12 output bytes are a 10-byte message and a CRC-16.

Step Detail
Interleave 98 dibits in four offset groups of eight, 13 + 12 + 12 + 12 pairs (the standard's pattern)
Trellis 4 states, 49 symbols; expected nibble `dtm[prev << 2
Pack The 49 next states, two bits each, give the first 48 into 12 bytes, MSB first
CRC CRC-16 CCITT over the first 80 bits, init 0, polynomial 0x1021, final complement; it must equal the last 16 bits
Message The first byte holds the protected bit (0x80) and the opcode; e.g. 0x3B Network Status Broadcast

The transition set and the CRC come from dsd-neo's ISC p25_12.c / p25_crc.c; the interleaver is implemented from the standard's pattern. tests/host/p25TsbkTest.c decodes four control-channel frames captured at NAC 0x293 (opcodes 0x3B, 0x3D, 0x3A and an idle) with valid CRCs. Decoding on the radio itself still needs the stream framing of 5.2: the chip's 264-bit chunks are not contiguous.

7.1 What the HR_C7000 can and cannot give (bench, 2026-11-04)

Path Result
PHY receive, continuous, P25 FSW NID decodes (NAC/DUID); the block is DMR-framed and keeps only the 196 on-air bits of each 288-bit slot, so the TSBK after the NID is not contiguous and every CRC search fails
FM receive audio, FM mode 8 kHz recording of the voice-filtered AF: no usable 4-level eye
FM receive audio, RF chip in its DMR/data receive table 8 kHz recording of the wide discriminator: a 4-level eye (centers -8602/-2878/+970/+7189, residual/spacing 0.32) but 1.67 samples per symbol; the best frame-sync correlation is 9/24 mismatches, not enough for the 4800-baud slicer
Raw 38.4 kHz ADC not exposed: the baseband ADC feeds the FM/DMR blocks and the recording is decimated to 8 kHz

The PHY capture cannot supply a contiguous P25 stream and is kept for the bench probe only. The continuous monitor uses the wide discriminator with the software demodulator instead (section 8); the 8 kHz tap is enough for that demodulator's timing hypotheses.

7.2 The p25_tx control channel (ground truth for the bench)

The portable SDR's P25 TX app (Mayhem, GPL, the author's) builds one frame as: the 24-dibit FSW, the 32-dibit NID (BCH of (NAC << 4) | 0x7), then the 98 trellis and interleaved TSBK dibits, with one 2-bit status symbol (0x02) after every 35 data dibits and zero padding to the next status. A frame is 180 dibits; the app sends two per buffer and cycles five TSBKs (IdenUp 0x3D, NetStatus 0x3B, RFSSStatus 0x3A, GroupVoiceGrant 0x00, NetStatus). Its defaults are NAC 0x293, WACN 0xBEEF0, SYS 0x001, RFSS 1, SITE 1, TG 1, VCH 1.

The HR_C7000 stores each 30 ms slot as CACH/slot type, FSW, the pre-sync data, then the post-sync data. Un-reordering a 288-bit slot as A(0:44 bits) B(92:180) FSW(44:92) P(180:288) and concatenating the slots puts the NID at the on-air +48 and the TSBK after it. The chip's DMR deframer overwrites the slot's A field (bits 0..43, the CACH/slot type) and F field (bits 132..179, the embedded signalling) with its own decoding, so those TSBK dibits are unknown; the decoder marks them erased for the trellis. Because the 37.5 ms frame is longer than the 30 ms slot, each frame's sync lands at a different place in the slot and the A/F overlap with the TSBK changes, so the decoder tries every sync in the stream and takes the TSBK from whichever one the erasure trellis finishes (p25_tsbk_decode with a per-dibit erasure mask). Matching the five known frames is only a bench shortcut and must not be the scanner's decoder.

The bench decodes all five TSBKs this way at NAC 0x293: IdenUp (the 433.575 MHz channel), NetStatus (WACN 0xBEEF0, SYS 0x001), RFSSStatus (SYS 0x001, RFSS 1, SITE 1) and GroupVoiceGrant (TG 1, VCH 1), with a clean B/P field and no bit errors outside the erasures.

8. Control-channel monitor

A radio with the discriminator tap (hal_p25_probe_t.audio_start and audio_read) can show the decoded control channel on its own screen. The monitor takes the receiver from the idle channel and starts p25_cc_rx (core/p25/p25_cc_rx.c), which feeds the wide discriminator to the software demodulator in its own task. The continuous stream keeps symbol timing across frames: the framer accepts the control-channel DUID (7), the TSBK is extracted after its status symbols and decoded with trellis/CRC, and the view keeps a field until a later TSBK sets it. The receiver resets the demodulator only after a quiet second, so a bad NID no longer stops the monitor.

The RF carrier and the decoded control channel are separate freshness inputs. OUT OF RANGE follows the PHY block activity; a carrier whose NID cannot be decoded keeps the receiver running but does not count as a fresh control channel for PTT. Leaving the screen gives the receiver back to the channel.

The screen shows the NAC and DUID, the WACN and SYS, the RFSS and SITE, the TG and VCH, the IdenUp frequency and a count of control-channel bursts. Nothing is hardcoded: every value comes from the stream, and a field stays blank until a TSBK carries it. Receive only; nothing is transmitted.

The switch is CONFIG_P25, off by default: it needs CONFIG_DMR and a board with the discriminator tap. A radio without it hides the Tools entry. The debug link's p25 view section (GET_VIEW "p25") reports the same fields for the bench and the HIL test.

8.1 The trunking system entry

settings_t.p25 and p25_alt[3] (settings_p25_t, settings-store.md 3) keep up to four systems: control-channel frequency, subscriber uplink frequency, WACN, system ID, NAC, RFSS, site, talkgroup, our unit ID, the band plan (automatic IDEN_UP learning, or a manual base / spacing / offset), a short name and a valid flag. p25_count is how many exist and p25_active is the selected one. An older record reads the defaults: one system, 438.800 MHz, NAC 0x123, WACN 0xBEEF0, SYS 0x123, RFSS/SITE 1, talkgroup 1, unit ID 2, the automatic band plan, and a manual plan of 0.

Tools > P25 system (p25syslist, ui.md 2) lists the systems; OK opens a system for editing and a New system row adds another. The editor's Name row labels the system; the control channel, NAC, WACN, System ID, RFSS, Site, Talkgroup, Unit ID and band plan are per system. A trunked channel stores a 0-based system slot, so selecting the channel makes that system active. The channel editor's P25 system row chooses it from the same list. The trunking monitor and the conventional voice path read the active system; the screen itself does not transmit.

9. Conventional voice

CONFIG_P25_VOICE receives conventional Phase 1 voice on the current FM channel: p25_voice routes the wide discriminator through the software demodulator, validates the LDUs, applies the clear/encrypted policy and decodes the IMBE payload to the speaker. The receiver does not filter by NAC; the call's NAC and talkgroup are shown and logged.

CONFIG_P25_TX adds the conventional transmitter: p25_tx_voice takes 20 ms microphone blocks, encodes nine IMBE frames per LDU, and sends HDU, LDUs and TDU through the PHY transmitter. The call is clear group voice with the channel's NAC and talkgroup when the channel has the P25 conventional flag, else the system values; the unit ID is the system's. The LDU is not built when those values are outside their protocol widths; the conventional path then uses the defaults (NAC 0x293, talkgroup 1, unit 2).

The channel editor's P25 conventional row marks the memory and shows P25 talkgroup and P25 NAC; P25 trunked is the other kind and clears the conventional flag. On Home, QUICK opens the grid instead of toggling P25: tile 9 is P25/FM, and while P25 is on tile 3 is NAC and tile 6 the talkgroup. Those quick changes are temporary until the channel is saved. The open-firmware record keeps the conventional flag in flag2 bit 7, its NAC in the color-code byte (low 8 bits) and flag2 bits 0-3 (high 4), and its talkgroup in the contact field. The system values remain the fallback for a channel without the flag and for Tools > P25 voice. A channel with the flag still enters P25 voice through * or Tools > P25 voice, like any conventional channel.

A short * on Home opens Quick settings; in the P25 VFO tile 9 is CNV or a stored system. Choosing a system makes the VFO (or the channel being edited) a trunked memory for that system; CNV keeps the conventional path. The NAC and talkgroup are tiles 3 and 6 while conventional P25 is on. Tools > P25 voice also starts the conventional path. Long * keeps its key-lock meaning. The mode is receive-first: PTT starts the conventional transmit path on the channel's TX frequency, and release returns to the receiver. CONFIG_P25_STAR no longer changes this.

10. PHY transmit test engine

CONFIG_P25_TX adds a bounded test transmitter for the HR_C7000's physical-layer transmit mode. It sends the fixed P25 test stream from core/p25/tx/p25_test_tx.c: the 240-byte preamble, the HDU, 28 alternating LDU1/LDU2 pairs and the TDU (12453 bytes, about 10.4 s), one complete 36-byte block per 30 ms boundary. It is a diagnostic; the conventional microphone path is section 9 (p25_tx_voice).

The platform implementation is platform/hrc7000/p25_tx.c; the core sees only hal_p25_tx_t (hal/hal_p25_tx.h) through board.p25_tx. The engine refuses to arm unless the radio is explicitly armed with an ALLOW_RF token, the frequency is in a transmit band, the factory calibration is valid, the battery is above the critical level, and neither the DMR engine nor the P25 receive probe owns the modem. One ARM grants one START, the ARM lives 30 s, and a START sets a hard 12 s timer. A PTT release (when a caller supplies one), a late or stalled block, a timeout, STOP or a radio switch-off stops the engine: RF and PA off, RF-safe, then the modem's and RF chip's previous state restored in task context.

The debug link command is 0x21; rfoundry_debug.py does not wrap it yet. The payloads are:

Op Payload Meaning
0 none status text (p25tx armed= hw= state= reason= hz= variant= invert= arm_s= cal= offset= blocks= ...)
1 hz u32, variant u8 (0-11), invert u8 (0/1), ALLOW_RF arm one transmission
2 ALLOW_RF start the armed transmission
3 none stop now

It needs CONFIG_DEBUGLINK, CONFIG_P25_TX, CONFIG_P25_VOICE and CONFIG_HRC_MODEM. Timer 3 is the PA one-shot (the DMR PA timer) and Timer 6 the hard stop; the modem system, timeslot and RF TX interrupts carry the block boundaries and the per-burst RF/PA keying.

View the source of this page