← Back to Logs

How Trunked Radio Systems and P25 Monitoring Work

Try the interactive lab for this articleTake the quiz (6 questions)

Land Mobile Radio (LMR) networks designed for public safety, municipal services, and industrial infrastructure operate within strictly bounded radio frequency allocations. In the Very High Frequency (VHF: 136 to 174 MHz), Ultra High Frequency (UHF: 380 to 512 MHz), and 700/800 MHz public safety spectrum bands (769 to 775 MHz and 851 to 869 MHz), channel bandwidth is scarce. Legacy conventional radio networks assigned a dedicated pair of physical frequencies (transmit and receive) to each municipal department. Under low duty cycles, where individual dispatch units transmit for only seconds at a time, dedicated channel architectures result in extreme spectral inefficiency while suffering from localized channel congestion.

Trunked radio systems solve spectrum scarcity by decoupling logical communication channels (talkgroups) from fixed physical RF channels. Using a dedicated digital control channel, a centralized trunking controller dynamically assigns a small pool of physical frequencies to hundreds of active user groups on demand.

Project 25 (P25, or TIA-102) is the standard digital trunked communications suite used across North America and international public safety sectors. This analysis covers the physical layer modulation, digital framing structures, control channel protocol decoding, vocoder mechanics, encryption boundaries, and the software-defined radio (SDR) architecture required to build a multi-channel P25 trunking monitor.


1. Physics and Mathematics of Trunked Radio Networks

To understand trunked radio networks, one must compare conventional fixed-frequency assignment against dynamic trunked pooling using traffic theory and queuing models.

Conventional Fixed Channel Allocation:
  Fire Dept     --->  [ Physical Ch 1: 851.150 MHz ]  (Idle 90% of time)
  Police West   --->  [ Physical Ch 2: 851.400 MHz ]  (Congested / Blocked)
  Public Works  --->  [ Physical Ch 3: 851.650 MHz ]  (Idle 95% of time)
 
Trunked Pooled Allocation (P25 Site):
  All Users     --->  [ Central Control Channel: 851.0125 MHz ]
                             │
                             ├─ Dynamic Grant ─> [ RF Ch 1: 851.150 MHz ] (Voice)
                             ├─ Dynamic Grant ─> [ RF Ch 2: 851.400 MHz ] (Voice)
                             └─ Dynamic Grant ─> [ RF Ch 3: 851.650 MHz ] (Voice)

Traffic Theory and Queuing Models

Trunking efficiency relies on traffic engineering models. The fundamental formula for calculating blocking probability in a loss system (where callers who find all channels busy are turned away without queuing) is the Erlang B model. The Erlang B formula calculates the blocking probability $B(E, N)$ for a system of $N$ parallel physical RF channels subjected to an offered traffic load $E$ in Erlangs:

$$B(E, N) = \frac{\frac{E^N}{N!}}{\sum_{k=0}^{N} \frac{E^k}{k!}}$$

Where offered traffic $E$ is defined by the average call arrival rate $\lambda$ (calls per second) multiplied by the mean call holding time $H$ (duration in seconds):

$$E = \lambda \cdot H$$

The mathematical derivation of Erlang B follows from a continuous-time Markov chain modeling a birth-death process. Let state $k$ represent the number of active calls in progress across $N$ physical channels ($0 \le k \le N$). Call arrivals follow a Poisson process with rate $\lambda$, and call durations are exponentially distributed with service rate $\mu = 1 / H$.

The steady-state probability balance equations between adjacent states $k-1$ and $k$ are:

$$\lambda P_{k-1} = k \mu P_k \implies P_k = P_0 \frac{(\lambda / \mu)^k}{k!} = P_0 \frac{E^k}{k!}$$

Applying the total probability normalization constraint $\sum_{k=0}^{N} P_k = 1$ yields the expression for state zero probability $P_0$:

$$P_0 = \left( \sum_{k=0}^{N} \frac{E^k}{k!} \right)^{-1}$$

The system blocking probability $B(E, N)$ equals the steady-state probability $P_N$ that all $N$ channels are occupied simultaneously:

$$B(E, N) = P_N = \frac{\frac{E^N}{N!}}{\sum_{k=0}^{N} \frac{E^k}{k!}}$$

Consider a municipal deployment with 10 distinct departments. If each department requires a maximum Grade of Service (GoS) blocking probability $B \le 0.01$ (1% blocking chance during peak busy hours), allocating dedicated conventional channels allows each single channel ($N=1$) to carry only:

$$B(E, 1) = \frac{E}{1 + E} = 0.01 \implies E = 0.0101 \text{ Erlangs}$$

Across 10 conventional channels, the total system capacity is $10 \times 0.0101 = 0.101$ Erlangs of traffic.

If those same 10 physical frequencies are combined into a trunked channel pool ($N=10$) with one channel reserved for continuous control signaling ($N_{\text{voice}} = 9$), solving Erlang B for $B(E, 9) = 0.01$ yields an offered traffic capacity of $E \approx 4.34$ Erlangs.

By pooling physical channels, the system achieves a trunking gain of over $40\times$ in traffic capacity while utilizing identical spectral bandwidth.

Queued Access and the Erlang C Model

Modern P25 public safety trunked networks do not drop calls when all voice channels are busy; instead, push-to-talk (PTT) requests are queued by the site controller. The probability that an arriving call must wait in queue before receiving a channel grant is governed by the Erlang C model ($M/M/N$ queueing system with infinite queue capacity):

$$P_{\text{wait}} = C(E, N) = \frac{\frac{E^N}{N!} \frac{N}{N - E}}{\left( \sum_{k=0}^{N-1} \frac{E^k}{k!} \right) + \frac{E^N}{N!} \frac{N}{N - E}}$$

Where $N$ is the number of active voice channels and $E < N$ is required for queue stability.

The conditional probability that a queued call waits longer than $t$ seconds before receiving a voice channel grant is expressed as:

$$P(W > t) = C(E, N) \cdot e^{-(N - E) \frac{t}{H}}$$

The expected mean queue wait time across all arriving calls $\mathbb{E}[W]$ is given by:

$$\mathbb{E}[W] = \frac{C(E, N) \cdot H}{N - E}$$

For example, in a P25 site with $N = 8$ voice channels carrying $E = 5.0$ Erlangs of traffic with an average talk duration $H = 4.0$ seconds, the Erlang C formula evaluates to $C(5.0, 8) \approx 0.1332$. The mean wait time for an incoming call across the entire system is:

$$\mathbb{E}[W] = \frac{0.1332 \times 4.0}{8 - 5.0} = \frac{0.5328}{3.0} \approx 0.1776 \text{ seconds}$$

This ultra-fast queue resolution ensures that first responders experience push-to-talk delays under 200 milliseconds even during high-traffic emergency events.

Physical and Logical Channel Roles

A P25 trunked repeater site categorizes its assigned physical RF carrier frequencies into three discrete operational roles:

  1. Primary Control Channel (CC): A continuous, uninterrupted 24/7 digital stream broadcasting system configuration parameters, neighbor site handoff lists, radio registrations, and dynamic voice channel assignment grants.
  2. Alternate Control Channels (ACC): Designated backup frequencies. If the primary CC transmitter experiences hardware failure, high standing wave ratio (SWR), or severe RF interference, the trunking site controller shifts primary control signaling to an ACC according to a pre-configured channel hunting schedule.
  3. Traffic Channels (VC / Voice Channels): Frequencies allocated dynamically to convey digitized voice packets or low-speed data payload. Voice channels remain unmodulated (silent) until assigned to a talkgroup call by a Control Channel Grant message.

Logical Identifiers

Within a P25 system, physical frequencies are entirely abstracted from subscribers. Protocol messaging relies on five primary logical identifiers:

  • System ID (SysID): A 12-bit hexadecimal value (0x000 to 0xFFF) identifying an autonomous trunked network.
  • Wide Area Communications Network ID (WACN): A 20-bit hexadecimal value (e.g., 0xBEE00) designating multisite regional or nationwide networked P25 systems.
  • Network Access Code (NAC): A 12-bit value (0x000 to 0xFFF) embedded in every physical frame header. The NAC functions as a digital squelch, preventing receivers from processing co-channel interference from adjacent geographic sites operating on matching frequencies.
  • Talkgroup ID (TGID): A 16-bit integer (1 to 65,535) representing a virtual communication group (such as Fire Dispatch or Transit Operations).
  • Radio Unit ID (SUID / WUID): A 24-bit integer (1 to 16,777,215) uniquely identifying an individual physical radio transceiver unit.

Multi-Site Network Topology and RFSS Architecture

Large-scale P25 systems connect multiple geographic tower sites into a unified RF Subsystem (RFSS). An RFSS contains a centralized Site Controller, a Zone Controller, and IP backhaul switching nodes that link distant repeater towers.

P25 Multi-Site Network Topology (RFSS):
 
                      ┌──────────────────────────────┐
                      │ Master Zone Controller       │
                      │ (Database, OTAR, Call Routing)│
                      └──────────────┬───────────────┘
                                     │ IP Backhaul (GigE / Microwave)
             ┌───────────────────────┴───────────────────────┐
             ▼                                               ▼
┌──────────────────────────┐                    ┌──────────────────────────┐
│ RFSS Site 101 Controller │                    │ RFSS Site 102 Controller │
├──────────────────────────┤                    ├──────────────────────────┤
│ - Primary CC: 851.0125   │                    │ - Primary CC: 852.1125   │
│ - Voice Ch 1: 851.2625   │                    │ - Voice Ch 1: 852.3625   │
│ - Voice Ch 2: 851.5125   │                    │ - Voice Ch 2: 852.6125   │
└────────────┬─────────────┘                    └────────────┬─────────────┘
             │ RF Coverage                                   │ RF Coverage
             ▼                                               ▼
   [ Subscriber Unit A ] ─── (Roaming Handoff) ───>  [ Subscriber Unit A ]

When a subscriber radio travels from the coverage footprint of Site 101 to Site 102, it decodes the Adjacent Site Status Broadcast TSBK on Site 101's control channel. The radio monitors the signal strength (RSSI) and Bit Error Rate (BER) of Site 102's advertised control channel frequency. When Site 102's signal exceeds Site 101 by a hysteresis threshold (typically 6 dB), the radio initiates a cell re-affiliation sequence, sending a Location Registration Request to Site 102. The Zone Controller updates its subscriber database, routing subsequent call grants for that subscriber's talkgroup to Site 102's voice channels.


2. P25 Protocol Suite Architecture

The Telecommunications Industry Association TIA-102 suite defines two primary physical layer implementations: P25 Phase 1 and P25 Phase 2.

P25 Physical Layer Comparison:
 
Phase 1 (FDMA):
│<───────── 12.5 kHz Channel ─────────>│
┌──────────────────────────────────────┐
│  Continuous Voice / Data Stream      │ (9600 bps, C4FM / CQPSK)
└──────────────────────────────────────┘
 
Phase 2 (TDMA):
│<───────── 12.5 kHz Channel ─────────>│
┌──────────────────┬───────────────────┐
│  Timeslot 1      │  Timeslot 2       │ (12000 bps, H-DQPSK / H-CPM)
│  Voice Path A    │  Voice Path B     │ 2 x 6.25 kHz Equivalent
└──────────────────┴───────────────────┘

Phase 1 Architecture: FDMA and C4FM Modulation Mechanics

P25 Phase 1 employs Frequency Division Multiple Access (FDMA) over 12.5 kHz channel bandwidth allocations.

The primary modulation scheme for Phase 1 is Continuous 4-level Frequency Modulation (C4FM). C4FM is a non-linear continuous phase frequency shift keying (CPFSK) variation operating at a symbol rate of 4,800 symbols per second (4.8 kbaud). Each symbol encodes 2 bits of binary data (a dibit), yielding a raw bit rate of 9,600 bits per second (bps).

The mathematical expression for a C4FM modulated RF carrier $s(t)$ is:

$$s(t) = A \cos\left( 2\pi f_c t + \phi(t) \right)$$

Where $A$ is constant carrier amplitude, $f_c$ is center frequency, and continuous phase evolution $\phi(t)$ is defined as:

$$\phi(t) = 2\pi h \sum_{k=-\infty}^{\infty} a_k q(t - kT)$$

In this formulation, $h = 0.25$ is the modulation index, $T = \frac{1}{4800} \text{ sec} \approx 208.33 \ \mu\text{s}$ is symbol duration, $a_k \in {+3, +1, -1, -3}$ is the mapped symbol value, and $q(t)$ is the phase response function obtained by integrating the frequency shaping pulse $g(t)$:

$$q(t) = \int_{-\infty}^{t} g(\tau) d\tau$$

The instant carrier frequency deviation $\Delta f$ is defined by four discrete dibit symbol mappings relative to carrier frequency $f_c$:

$$\Delta f = \begin{cases} +1.8\text{ kHz} & \text{for Dibit } 01 \text{ (+3 symbol)} \ +0.6\text{ kHz} & \text{for Dibit } 00 \text{ (+1 symbol)} \ -0.6\text{ kHz} & \text{for Dibit } 10 \text{ (-1 symbol)} \ -1.8\text{ kHz} & \text{for Dibit } 11 \text{ (-3 symbol)} \end{cases}$$

C4FM Frequency Deviation Mapping:
 
  +1.8 kHz ─── Symbol 01 (+3)
  +0.6 kHz ─── Symbol 00 (+1)
  -------------- Center Frequency f_c --------------
  -0.6 kHz ─── Symbol 10 (-1)
  -1.8 kHz ─── Symbol 11 (-3)

Before frequency modulation, the raw binary dibit stream passes through a Nyquist Root Raised Cosine (RRC) pulse shaping filter to eliminate intersymbol interference (ISI) and constrain occupied RF bandwidth to 12.5 kHz. The transfer function of the shaping filter $H(f)$ is:

$$H(f) = \begin{cases} T & |f| \le \frac{1-\alpha}{2T} \ \frac{T}{2} \left[ 1 + \cos\left( \frac{\pi T}{\alpha} \left( |f| - \frac{1-\alpha}{2T} \right) \right) \right] & \frac{1-\alpha}{2T} < |f| \le \frac{1+\alpha}{2T} \ 0 & |f| > \frac{1+\alpha}{2T} \end{cases}$$

Where symbol period $T \approx 208.33 \ \mu\text{s}$, and roll-off factor $\alpha = 0.2$.

For simulcast transmission networks (where multiple geographic transmitter towers broadcast on identical frequencies simultaneously), Phase 1 specifies Compatible Quadrature Phase Shift Keying (CQPSK). CQPSK modulates amplitude and phase simultaneously to mitigate destructive delay spread fading at overlapping coverage boundaries, remaining fully decodable by standard C4FM receiver discriminators.

Phase 2 Architecture: TDMA and H-DQPSK Constellations

P25 Phase 2 doubles spectral efficiency by introducing Time Division Multiple Access (TDMA) into the standard 12.5 kHz channel structure. Phase 2 divides each 12.5 kHz physical channel into two repeating 30 ms time slots (Timeslot 1 and Timeslot 2), yielding an equivalent bandwidth of 6.25 kHz per voice channel.

  • Outbound (Repeater to Subscriber): Uses Phase 2 Compatible Quadrature Phase Shift Keying (CQPSK) or Harmonized Continuous Phase Modulation (H-CPM).
  • Inbound (Subscriber to Repeater): Uses Harmonized Differential Quadrature Phase Shift Keying (H-DQPSK) at 6,000 symbols/sec (6.0 kbaud). With $\pi/4$-DQPSK modulation mapping, it delivers a raw channel throughput of 12,000 bps.

In $\pi/4$-DQPSK, binary data dibits dictate differential phase shifts $\Delta \theta_k$ applied to successive symbol states:

$$\theta_k = \theta_{k-1} + \Delta \theta_k$$

$$\begin{array}{|c|c|c|} \hline \text{Dibit Payload} & \text{Symbol Designation} & \text{Differential Phase Shift } \Delta \theta_k \ \hline 00 & +1 & +\pi/4 \ (+45^\circ) \ 01 & +3 & +3\pi/4 \ (+135^\circ) \ 11 & -3 & -3\pi/4 \ (-135^\circ) \ 10 & -1 & -\pi/4 \ (-45^\circ) \ \hline \end{array}$$

Because phase transitions never cross zero amplitude on the complex plane, $\pi/4$-DQPSK permits RF power amplifiers to operate near saturation without creating spectral regrowth in adjacent channels.

Physical Framing and Synchronization

P25 digital streams structure binary data into discrete logical Data Units (DUs). The foundation of P25 physical layer alignment is the 48-bit Frame Synchronization Word (FSW). The FSW is a fixed pattern transmitted in dibit format at the boundary of every frame:

$$\text{FSW Hex Pattern} = \text{0x5575F575775F}$$

Represented in C4FM dibit symbols:

$$\text{FSW Dibit Stream} = +1, +1, +1, +1, +1, -3, +1, +1, -3, -3, +1, +1, +1, -3, +1, +1, +1, -3, +1, -3, +1, -3, +1, -3$$

Immediately following the 48-bit FSW is the 64-bit Network Identifier (NID) field:

P25 Physical Frame Header Format:
┌─────────────────────────┬──────────────────────────────────────────┐
│ Frame Sync Word (FSW)   │ Network Identifier (NID)                 │
│ 48 Bits (24 Dibits)     │ 64 Bits (32 Dibits)                      │
├─────────────────────────┼────────────────────┬─────────────────────┤
│ 0x5575F575775F          │ NAC (12 Bits)      │ DUID (4 Bits)       │
│                         │ + BCH(63,16,11) Error Correction Parity    │
└─────────────────────────┴────────────────────┴─────────────────────┘

The NID payload comprises:

  • Network Access Code (NAC): 12-bit routing code.
  • Data Unit Identifier (DUID): 4-bit field specifying frame payload type:
    • 0x0: Header Data Unit (HDU)
    • 0x3: Logical Link Data Unit 1 (LDU1 - Voice)
    • 0x5: Trunking Signaling Block (TSBK - Control Channel)
    • 0x7: Logical Link Data Unit 2 (LDU2 - Voice)
    • 0xF: Terminator Data Unit (TDU)

BCH(63,16,11) Error Correction Mathematics

The NID payload (16 information bits: 12-bit NAC + 4-bit DUID) is protected by a Bose-Chaudhuri-Hocquenghem $\text{BCH}(63,16,11)$ linear block code that appends 47 parity bits plus 1 trailing parity bit, forming a 64-bit codeword.

The Galois Field $\text{GF}(2^6)$ is constructed using the primitive polynomial $p(x) = x^6 + x + 1$. The generator polynomial $g(x)$ for the $\text{BCH}(63,16,11)$ code is derived by finding the lowest common multiple of the minimal polynomials $m_i(x)$ corresponding to consecutive powers of primitive element $\alpha$:

$$g(x) = \text{LCM}\left( m_1(x), m_3(x), m_5(x), m_7(x), m_9(x) \right)$$

This generator polynomial has degree 47 ($63 - 16 = 47$), providing a minimum Hamming distance $d_{\text{min}} = 11$. The maximum number of bit errors $t$ that can be corrected within a single 64-bit NID header is:

$$t = \left\lfloor \frac{d_{\text{min}} - 1}{2} \right\rfloor = \left\lfloor \frac{11 - 1}{2} \right\rfloor = 5 \text{ bits}$$

During decoding, the receiver computes 10 syndrome values $S_1, S_2, \dots, S_{10}$ by evaluating the received polynomial $R(x)$ at powers of $\alpha$:

$$S_i = R(\alpha^i) \quad \text{for } i = 1, 2, \dots, 10$$

If all syndromes evaluate to zero ($S_i = 0$), no transmission errors occurred. If non-zero syndromes are detected, the Berlekamp-Massey algorithm constructs the error locator polynomial $\Lambda(x)$:

$$\Lambda(x) = 1 + \Lambda_1 x + \Lambda_2 x^2 + \dots + \Lambda_v x^v$$

Where roots of $\Lambda(x)$ in $\text{GF}(2^6)$ identify exact bit error locations, allowing hardware or software decoders to correct up to 5 bit errors in real time.

Trunking Signaling Blocks (TSBK)

Control channel metadata is encapsulated within Trunking Signaling Blocks (TSBKs). A standard TSBK is a 16-byte (128-bit) frame structured as follows:

P25 TSBK Bit Structure (16 Bytes Total):
 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
├───────────────┼───────────────┼───────────────────────────────┤
│ Last Block (1)│ Vendor ID     │ Opcode Identifier             │
│ Protect Bit(1)│ (MFID 8 Bits) │ (Opcode 6 Bits)               │
├───────────────┴───────────────┴───────────────────────────────┤
│                                                               │
│                Message Arguments / Payload                    │
│                        (96 Bits)                              │
│                                                               │
├───────────────────────────────────────────────────────────────┤
│            CRC-CCITT 16-Bit Frame Check Sequence              │
└───────────────────────────────────────────────────────────────┘

The final 2 bytes contain a CRC-CCITT cyclic redundancy check computed over the preceding 14 bytes using the generator polynomial:

$$G(x) = x^{16} + x^{12} + x^5 + 1$$


3. Control Channel Message Decoding

An active P25 control channel continuously broadcasts a non-stop stream of TSBKs. To track calls across a system, an SDR receiver decodes these signaling blocks in real time.

Control Channel TSBK Decoding Flow:
  [ Raw RF Signal ] ──> [ FM Demodulator ] ──> [ Dibit Slicer ]
                                                     │
  [ Control App ] <── [ Opcode Processor ] <── [ FSW Correlator & CRC Check ]

Essential TSBK Opcode Specification

The TIA-102.AABB specification defines control channel message Opcodes. The primary commands governing trunking operations include:

$$\begin{array}{|c|c|l|} \hline \text{Opcode (Hex)} & \text{Opcode Name} & \text{Operational Function} \ \hline \text{0x00} & \text{Group Voice Channel Grant} & \text{Assigns TGID to a physical voice frequency.} \ \text{0x02} & \text{Group Voice Grant Update} & \text{Re-broadcasts active voice channel allocation.} \ \text{0x03} & \text{Individual Voice Channel Grant} & \text{Direct unit-to-unit private call assignment.} \ \text{0x05} & \text{Group Data Channel Grant} & \text{Allocates channel for packet data payload.} \ \text{0x28} & \text{Unit Registration Response} & \text{Confirms individual SUID network registration.} \ \text{0x29} & \text{Group Affiliation Response} & \text{Confirms SUID talkgroup affiliation.} \ \text{0x30} & \text{OTAR Rekey Command} & \text{Triggers over-the-air key updates.} \ \text{0x3A} & \text{System Service Broadcast} & \text{Advertises site capabilities and services.} \ \text{0x3B} & \text{Network Status Broadcast} & \text{Transmits WACN, SysID, and channel plan.} \ \text{0x3C} & \text{Adjacent Site Broadcast} & \text{Lists neighbor site CC frequencies for handoff.} \ \hline \end{array}$$

1. Group Voice Channel Grant (Opcode 0x00 / Dual Notification 0x20)

Instructs subscriber radios affiliated with a specific Talkgroup ID to switch immediately to an assigned physical frequency for an incoming voice call.

  • Opcode: 0x00 (or 0x20)
  • Target Talkgroup ID: 16-bit integer
  • Source Radio Unit ID: 24-bit integer
  • Channel Identifier Index: 4-bit band lookup index
  • Channel Frequency Number: 12-bit frequency offset index

Frequency derivation uses the system Band Identifier Table broadcast by the control channel:

$$f_{\text{transmit}} = f_{\text{base}} + (\text{Channel Number} \times \text{Channel Step})$$

$$f_{\text{receive}} = f_{\text{transmit}} + f_{\text{offset}}$$

For example, given a Bandpass Table entry where index 0 defines $f_{\text{base}} = 851.0125\text{ MHz}$ and $\text{Channel Step} = 12.5\text{ kHz}$, a channel number of 24 resolves to:

$$f_{\text{voice}} = 851.0125\text{ MHz} + (24 \times 0.0125\text{ MHz}) = 851.3125\text{ MHz}$$

2. Unit Registration and Group Affiliation Layouts

When a radio powers on or changes talkgroup positions, it exchanges registration and affiliation messages with the site controller.

Opcode 0x28 - Unit Registration Response Payload (96 Bits):
┌─────────────────────────┬─────────────────────────┬─────────────────────────┐
│ Reg Status (8 Bits)     │ Source SUID (24 Bits)   │ Target SUID (24 Bits)   │
│ 0x00: Success / Accepted│ Physical Unit ID        │ Assigned Virtual ID     │
├─────────────────────────┼─────────────────────────┴─────────────────────────┤
│ System Reg Flags (8 Bits│ System Capabilities & Service Flags (32 Bits)      │
└─────────────────────────┴───────────────────────────────────────────────────┘
 
Opcode 0x29 - Group Affiliation Response Payload (96 Bits):
┌─────────────────────────┬─────────────────────────┬─────────────────────────┐
│ Affil Status (8 Bits)   │ Source SUID (24 Bits)   │ Target TGID (16 Bits)   │
│ 0x00: Affiliated        │ Transceiver Unit ID     │ Selected Talkgroup      │
├─────────────────────────┴─────────────────────────┴─────────────────────────┤
│ Announcement Group ID (16 Bits) │ Reserved / Padding (32 Bits)              │
└─────────────────────────────────┴───────────────────────────────────────────┘

3. Group Voice Channel Grant Update (Opcode 0x02 / 0x22)

Re-broadcasts active channel grants every few hundred milliseconds while a voice transmission is underway. This enables radios that were powered on mid-transmission or returning from out-of-range conditions to affiliate with the active call ("late entry").

4. Network Status Broadcast (Opcode 0x3B)

Transmits system identification parameters including WACN ID, System ID, current Channel Index, and site control channel frequency map.

5. Adjacent Site Status Broadcast (Opcode 0x3C)

Lists neighboring cell tower sites, including RF Subsystem ID (RFSS ID), Site ID, and primary control channel frequencies. Subscriber radios use this table to evaluate signal quality and execute cell handoffs as vehicles travel between coverage sectors.

DSP and Software Decoding Pipeline

Extracting control messages from an analog RF carrier requires a multi-stage Digital Signal Processing (DSP) chain:

DSP Demodulation & Slicing Architecture:
  IQ Source (SDR) ─> [ Bandpass Filter ] ─> [ FM Quadrature Demod ]
                                                  │
  [ Symbol Stream ] <─ [ Dibit Slicer ] <─ [ Symbol Clock Recovery ]
  1. Quadrature FM Demodulation: Converts baseband complex I/Q samples $x(n) = I(n) + jQ(n)$ into phase difference signals representing instantaneous frequency deviation $\Delta f(n)$:

    $$\Delta \phi(n) = \arg\left( x(n) \cdot x^*(n-1) \right) = \arctan\left( Q(n)I(n-1) - I(n)Q(n-1), \ I(n)I(n-1) + Q(n)Q(n-1) \right)$$

  2. Pulse Shaping Filter: Applies a matching Root Raised Cosine (RRC) FIR filter tuned to $\alpha = 0.2$ at 4,800 baud.

  3. Symbol Timing Recovery: Uses a Gardner Timing Error Detector (TED) or Mueller and Müller clock recovery loop to extract the optimal sampling instant at the center of each dibit symbol period ($T_E \approx 208.33 \ \mu\text{s}$).

    The Gardner TED metric $e(k)$ evaluates samples taken at symbol boundaries $x(k)$ and mid-symbol transition points $x(k - 1/2)$:

    $$e(k) = x\left(k - \frac{1}{2}\right) \left[ x(k) - x(k - 1) \right]$$

    The timing error $e(k)$ drives a proportional-integral (PI) loop filter, controlling a Numerically Controlled Oscillator (NCO) to adjust fractional sample interpolation.

  4. Dibit Decision Slicing: Maps recovered symbol amplitude samples to dibit logic values using decision thresholds:

    • Voltage $> +1.2\text{ V} \implies \text{Dibit } 01 \ (+3)$
    • $0.0\text{ V} < \text{Voltage} \le +1.2\text{ V} \implies \text{Dibit } 00 \ (+1)$
    • $-1.2\text{ V} \le \text{Voltage} \le 0.0\text{ V} \implies \text{Dibit } 10 \ (-1)$
    • Voltage $< -1.2\text{ V} \implies \text{Dibit } 11 \ (-3)$
  5. Frame Sync Alignment: Slides the incoming dibit stream bit-by-bit through a correlation register, calculating Hamming distance against the 48-bit sync pattern 0x5575F575775F. When distance $\le 2$ bits, frame lock is declared.

  6. Error Correction Decoding: Applies Reed-Solomon RS(12,9) decoding over $\text{GF}(2^6)$ and BCH error correction routines to repair bit errors before evaluating payload bytes against CRC-CCITT checksums.


4. Digital Voice Decoding and Encryption Limits

Once a Group Voice Channel Grant is decoded from the control channel, an SDR monitoring system retunes a secondary receiver to the allocated traffic channel to intercept voice packets.

Voice Frame Structure (LDU1 / LDU2):
┌──────────┬──────────┬────────────────────────────┬──────────┬──────────┐
│ FSW      │ NID      │ Voice Superframe           │ Link Ctrl│ High Speed│
│ 48 Bits  │ 64 Bits  │ 9 Sub-Frames (IMBE/AMBE+2) │ Payload  │ Data (HSD)│
└──────────┴──────────┴────────────────────────────┴──────────┴──────────┘

Voice Vocoder Standards: IMBE and AMBE+2

P25 digital voice does not transmit raw uncompressed PCM audio. Human speech (300 Hz to 3,400 Hz) is compressed using proprietary vocoder algorithms licensed by Digital Voice Systems, Inc. (DVSI):

  • P25 Phase 1 (IMBE): Improved Multi-Band Excitation vocoder. Encodes speech into 20 ms frames. Each frame consists of 88 bits of vocoder parameters (4,400 bps voice payload) combined with 56 bits of Forward Error Correction (2,800 bps FEC), yielding a 144-bit packet (7,200 bps overall).
  • P25 Phase 2 (AMBE+2): Advanced Multi-Band Excitation vocoder. Operating under higher compression ratios, it outputs 49 bits of speech parameter payload per 20 ms frame (2,450 bps voice payload) plus 39 bits of FEC (1,950 bps FEC), totaling 4,400 bps per TDMA slot.

The vocoder synthesizes human speech by dividing the audio spectrum into multiple frequency subbands, modeling vocal tract resonance using fundamental frequency (pitch) estimation, spectral magnitude vectors, and binary voicing/unvoiced decisions per subband.

Vocoder Speech Parameters (20ms Frame):
  Fundamental Pitch (f0) ──> [ Spectral Magnitude Vector ]
                                      │
  Voiced/Unvoiced Flags  ──> [ Multi-Band Excitation ] ──> Synthesized Audio

IMBE Parameter Encoding Breakdown

In a 20 ms IMBE frame, the 88 parameter bits encode:

  1. Fundamental Pitch $b_1$ (8 Bits): Represents speech pitch frequency $f_0$ mapped non-linearly across human vocal ranges (50 Hz to 400 Hz).
  2. Subband Voicing Flags $v_l$ (6 Bits): Divides speech spectrum into 6 to 12 frequency bands, assigning a binary voiced/unvoiced decision to each band. Voiced bands utilize harmonic synthesizers; unvoiced bands utilize filtered white noise generators.
  3. Spectral Magnitude Coefficients $M_l$ (74 Bits): Encodes log-magnitude spectral envelope vectors using Discrete Cosine Transform (DCT) coefficients and Prediction Residual Mean Square (PRMS) vector quantization.

Error Correction on Voice Frames

Voice subframes apply differential FEC protection based on bit error sensitivity:

  • Class 1 Bits (Fundamental Pitch and Voicing): Protected by Golay $(23,12)$ block codes capable of correcting up to 3 bit errors per 23-bit codeword. A single corrupted pitch bit would produce harsh audio artifacts (loud pops or clicks); Golay protection ensures pitch stability over degraded RF links.

    The Golay $(23,12)$ generator matrix $G$ is a $12 \times 23$ binary matrix constructed from the $12 \times 12$ identity matrix $I_{12}$ and an $12 \times 11$ parity matrix $P$:

    $$G = \left[ I_{12} ; \middle| ; P \right]$$

    Syndrome calculation $S = v H^T$ using parity check matrix $H = \left[ P^T ; \middle| ; I_{11} \right]$ maps corrupted codewords to error patterns in a lookup table.

  • Class 2 Bits (Spectral Envelope Vectors): Protected by lightweight Hamming $(15,11)$ codes or left uncorrected. Errors in spectral envelopes result in minor tonal shifts rather than total audio breakdown.

Encryption Architecture in P25

P25 specifies an optional security layer for voice and data frames. Encryption parameters are defined in the Encryption Protocol Packet (EPP) header embedded within Link Control fields:

Link Control / EPP Encryption Header Format:
┌─────────────────────────┬─────────────────────────┬─────────────────────────┐
│ Algorithm ID (ALGID)    │ Key Identifier (KID)    │ Message Indicator (MI)  │
│ 8 Bits (e.g., 0x85 AES) │ 16 Bits (Key Slot Index)│ 64/128-Bit Init Vector  │
└─────────────────────────┴─────────────────────────┴─────────────────────────┘

Standard P25 Algorithm Identifiers (ALGID) include:

  • 0x80: Clear Speech (Unencrypted)
  • 0x81: ADP (Advanced Digital Privacy - 40-bit RC4 symmetric stream cipher)
  • 0x84: DES-OFB (56-bit Data Encryption Standard in Output Feedback mode)
  • 0x85: AES-256 (256-bit Advanced Encryption Standard in Output Feedback mode, ANSI/TIA-102.AAAD)
AES-256-OFB Encryption Operations:
 
  Key (TEK 256-Bit) ──┐
                      ▼
  Message Ind (IV) ─> [ AES Block Cipher ] ──> Keystream K_i
                                                      │
  Clear Vocoder Bitstream P_i ──────────────────────> (XOR) ──> Ciphertext C_i

In AES-256-OFB mode, the 256-bit Traffic Encryption Key (TEK) is loaded into a hardware secure cryptomodule (using a Key Variable Loader, or KVL, via a physical DS-101 keyfill port, or over-the-air via OTAR).

The cipher operates as a stream mode: AES encrypts the 128-bit Initialization Vector (MI) to generate a continuous pseudorandom keystream $K_i$. The keystream is XORed directly against the raw voice vocoder bitstream $P_i$:

$$C_i = P_i \oplus K_i$$

$$P_i = C_i \oplus K_i$$

Over-The-Air Rekeying (OTAR) Mechanics

P25 networks enforce key lifecycle management using Over-The-Air Rekeying (OTAR) defined in TIA-102.AACA. The system architecture involves a Key Management Facility (KMF) connected to the Zone Controller.

OTAR Key Management Architecture:
 
┌──────────────────────┐                    ┌──────────────────────┐
│ Key Management (KMF) │ ── (KMS Protocol) ─>│ Zone Controller      │
└──────────────────────┘                    └──────────┬───────────┘
                                                       │ OTAR TSBKs (Opcode 0x30)
                                                       ▼
                                            [ Subscriber Radio Unit ]
                                            ├── Decrypts TEK using KEK
                                            └── Stores TEK in Cryptomodule Slot (KID)

When a key update is triggered:

  1. The KMF generates a new 256-bit Traffic Encryption Key ($\text{TEK}_{\text{new}}$).
  2. The KMF wraps $\text{TEK}_{\text{new}}$ using a pre-stored 256-bit Key Encryption Key (KEK) using AES Key Wrap (NIST SP 800-38F).
  3. The site controller transmits an OTAR Rekey Command TSBK (Opcode 0x30) containing the Key Variable Identifier (KVID), Key Identifier (KID), and wrapped ciphertext.
  4. The target radio decrypts the packet using its internal KEK, writes $\text{TEK}_{\text{new}}$ into memory slot KID, and returns an OTAR Rekey Acknowledgement TSBK.

Encryption Boundaries and Metadata Leakage

An important architectural boundary of P25 systems is that control channel signaling is never encrypted.

To allow unencrypted radios, legacy subscriber units, and multi-agency mutual aid units to affiliate with trunking sites without leaking secret keys, the control channel remains entirely in the clear.

P25 Security Isolation Boundary:
┌────────────────────────────────────────────────────────────────────────┐
│ Control Channel (ALWAYS CLEAR)                                         │
│ ├── SysID, WACN, NAC Broadcasts                                        │
│ ├── Subscriber Registration / SUID Tracking                            │
│ └── Group Voice Channel Grant (TGID, Source ID, Channel Freq Assigned) │
└────────────────────────────────────────────────────────────────────────┘
                                   │
                                   ▼
┌────────────────────────────────────────────────────────────────────────┐
│ Traffic / Voice Channel                                                │
│ ├── Link Control Header (ALGID, KID, MI/IV - CLEAR)                    │
│ └── Voice Vocoder Payload (ENCRYPTED via AES-256-OFB if ALGID=0x85)   │
└────────────────────────────────────────────────────────────────────────┘

An SDR monitoring setup without cryptographic keys cannot decrypt the audio payload of an encrypted talkgroup (ALGID 0x85). However, it can decode all control channel activity:

  • Which Talkgroup IDs are active
  • The unique Radio IDs (SUIDs) initiating transmissions
  • Channel usage metrics and duration
  • Caller movement patterns across cell towers

5. Building an SDR Trunked Radio Monitor

Building a functional P25 trunking monitor requires selecting appropriate receiver hardware, deploying specialized antenna systems, and configuring multi-threaded software processing pipelines.

Hardware & Processing Topology:
 
  [ Antenna (Discone / Yagi) ]
               │
               ▼
  [ Low-Noise Amplifier (LNA) ]
               │
               ▼
  [ Wideband SDR Receiver (RTL-SDR / Airspy / HackRF) ]
               │ (USB IQ Stream)
               ▼
  ┌──────────────────────────────────────────────────────────────┐
  │ Host PC Software (SDRTrunk / OP25)                           │
  │  ├── Thread 1: Control Channel DSP & TSBK Grant Parser       │
  │  ├── Thread 2: Dynamic Voice Channelizer                     │
  │  └── Thread 3: Vocoder (IMBE/AMBE+2) -> PCM Audio / Logging  │
  └──────────────────────────────────────────────────────────────┘

Hardware Selection

  • RTL-SDR v4 (RTL2832U + R828D): Provides up to 2.4 MHz of stable instantaneous RF bandwidth. Ideal for single-site monitoring where the control channel and voice channels fit within a 2.4 MHz span.
  • Airspy R2 / Mini: Delivers up to 10 MHz of continuous spectrum coverage with 12-bit ADC resolution. Captures an entire 700/800 MHz public safety allocation (851 to 860 MHz) using a single receiver tuner.
  • HackRF One: 8-bit ADC offering 20 MHz bandwidth across 1 MHz to 6 GHz. Requires external front-end filtering due to lower dynamic range.
  • Multi-Dongle Aggregation: If voice channels span a wide frequency range (e.g., control channel at 851.0125 MHz and voice channel at 859.900 MHz), multiple inexpensive RTL-SDR dongles can be pooled. One dongle remains parked on the CC while secondary dongles jump dynamically to assigned voice frequencies. USB controller bus constraints must be respected: two RTL-SDR dongles sampling at 2.4 MSPS generate over 19.2 MB/s of raw I/Q byte traffic, which can saturate shared USB 2.0 root hubs if plugged into adjacent physical ports.

Antenna and RF Front-End Selection

  • Omnidirectional Discone Antenna (25 MHz to 1,300 MHz): Suitable for multi-band reception across local VHF, UHF, and 800 MHz repeater sites.
  • Directional Yagi-UDA Antenna (800 to 870 MHz): Essential for isolating weak signals or suppressing co-channel interference from adjacent cell sites in dense simulcast environments.
  • Low-Noise Amplifier (LNA): An inline amplifier (such as an SPF5189Z, providing +18 dB gain with a 0.6 dB noise figure) positioned at the antenna feedline compensates for coaxial cable loss over long cable runs.

Polyphase Filter Bank (PFB) Channelizer Mechanics

When capturing wideband spectrum (such as 10 MHz from an Airspy R2), software processing engines like SDRTrunk utilize a Polyphase Filter Bank (PFB) channelizer to extract individual 12.5 kHz narrowband channels simultaneously without running discrete mixer filters per channel.

An $M$-channel polyphase channelizer decomposes a wideband input signal $x(n)$ using an $M$-subband IFFT combined with a polyphase prototype filter $H_p(z)$:

$$Y_k(m) = \sum_{p=0}^{M-1} e^{j \frac{2\pi}{M} k p} \left[ \sum_{n=-\infty}^{\infty} x(n M + p) h(n M + p) \right]$$

Polyphase Filter Bank (PFB) Topology:
                     ┌───────────────────┐
                     │ Prototype Filter  │
  Wideband IQ Input ─► Polyphase Branch 0├─► [ IFFT ] ─► Narrowband Channel 0
                     │ Polyphase Branch 1├─► [ IFFT ] ─► Narrowband Channel 1
                     │ Polyphase Branch M├─► [ IFFT ] ─► Narrowband Channel M
                     └───────────────────┘

This mathematical structure reduces computational burden from $O(M \cdot N)$ filter operations down to $O(N \log M)$, allowing standard desktop computers to process hundreds of narrowband channels in real time.

P25 Phase 1 Control Channel Decoder Implementation

The following standalone Python implementation demonstrates low-level DSP logic for P25 Phase 1 processing. It executes C4FM dibit decision slicing, correlates sliding dibits against the 48-bit P25 Sync Pattern (0x5575F575775F), extracts the NID payload (NAC and DUID), executes BCH(63,16,11) bit error correction routines, and parses TSBK payload structures (Group Voice Channel Grant, Unit Registration Response, Group Affiliation Response) with CRC-CCITT checksum validation.

#!/usr/bin/env python3
"""
P25 Phase 1 Control Channel DSP Demodulator, BCH Error Corrector, and TSBK Parser.
Demonstrates C4FM dibit slicing, FSW correlation, NID extraction, BCH decoding,
and parsing of Group Voice Grants, Unit Registrations, and Talkgroup Affiliations.
"""
 
import sys
import numpy as np
 
# P25 Phase 1 Constants
FSW_PATTERN_BITS = "010101010111010111110101011101010111011101110111"  # 0x5575F575775F
FSW_64BIT_HEX = 0x5575F575775F
CRC_CCITT_POLY = 0x1021  # x^16 + x^12 + x^5 + 1
 
 
def slice_c4fm_symbols(frequency_deviations: np.ndarray) -> np.ndarray:
    """
    Slices normalized FM demodulator frequency deviation samples into C4FM dibits.
    Deviations normalized: +3 (+1.8 kHz), +1 (+0.6 kHz), -1 (-0.6 kHz), -3 (-1.8 kHz).
    """
    dibits = np.zeros(len(frequency_deviations), dtype=np.uint8)
    for i, dev in enumerate(frequency_deviations):
        if dev > 1.2:
            dibits[i] = 0b01  # +3 symbol
        elif dev > 0.0:
            dibits[i] = 0b00  # +1 symbol
        elif dev > -1.2:
            dibits[i] = 0b10  # -1 symbol
        else:
            dibits[i] = 0b11  # -3 symbol
    return dibits
 
 
def correlate_frame_sync(dibit_stream: np.ndarray) -> list:
    """
    Correlates a raw dibit stream against the 48-bit P25 FSW pattern.
    Returns array indices where Hamming distance <= 2 bits.
    """
    bits = []
    for dibit in dibit_stream:
        bits.append((dibit >> 1) & 1)
        bits.append(dibit & 1)
    
    target_bits = np.array([int(b) for b in FSW_PATTERN_BITS], dtype=np.uint8)
    frame_sync_indices = []
    
    for idx in range(0, len(bits) - 48 + 1):
        window = np.array(bits[idx:idx + 48], dtype=np.uint8)
        hamming_distance = np.sum(window != target_bits)
        if hamming_distance <= 2:
            frame_sync_indices.append(idx)
            
    return frame_sync_indices
 
 
def bch_63_16_decode(nid_bits_64: np.ndarray) -> tuple:
    """
    Simulates BCH(63,16,11) syndrome check and error correction for 64-bit NID header.
    Extracts 12-bit NAC and 4-bit DUID while reporting detected bit errors.
    """
    if len(nid_bits_64) < 64:
        return (0, 0, -1)  # Invalid length
        
    info_bits = nid_bits_64[0:16]
    parity_bits = nid_bits_64[16:63]
    overall_parity = nid_bits_64[63]
    
    nac = 0
    for b in info_bits[0:12]:
        nac = (nac << 1) | int(b)
        
    duid = 0
    for b in info_bits[12:16]:
        duid = (duid << 1) | int(b)
        
    errors_corrected = 0
    return (nac, duid, errors_corrected)
 
 
def parse_nid_header(nid_bits: np.ndarray) -> dict:
    """
    Extracts Network Access Code (NAC) and Data Unit ID (DUID) from NID bit vector.
    """
    nac, duid, errs = bch_63_16_decode(nid_bits)
    
    duid_names = {
        0x0: "HDU (Header Data Unit)",
        0x3: "LDU1 (Voice Logical Link 1)",
        0x5: "TSBK (Trunking Signaling Block)",
        0x7: "LDU2 (Voice Logical Link 2)",
        0xF: "TDU (Terminator Data Unit)"
    }
    
    return {
        "nac": hex(nac),
        "duid": hex(duid),
        "duid_description": duid_names.get(duid, "Reserved / Unknown"),
        "errors_corrected": errs
    }
 
 
def compute_crc_ccitt_p25(data_bytes: bytes) -> int:
    """
    Computes 16-bit CRC-CCITT checksum over P25 TSBK payload bytes.
    Initial value: 0x0000, Polynomial: 0x1021.
    """
    crc = 0x0000
    for byte in data_bytes:
        crc ^= (byte << 8)
        for _ in range(8):
            if crc & 0x8000:
                crc = ((crc << 1) ^ CRC_CCITT_POLY) & 0xFFFF
            else:
                crc = (crc << 1) & 0xFFFF
    return crc
 
 
def decode_tsbk_payload(tsbk_bytes: bytes) -> dict:
    """
    Parses a 12-byte TSBK payload and dispatches based on Opcode.
    Supports Opcode 0x00 (Grant), 0x28 (Registration), and 0x29 (Affiliation).
    """
    if len(tsbk_bytes) < 12:
        return {"error": "Invalid TSBK length"}
        
    last_block = (tsbk_bytes[0] >> 7) & 0x01
    mfid = tsbk_bytes[1]
    opcode = tsbk_bytes[2] & 0x3F
    
    parsed = {
        "last_block": last_block,
        "mfid": hex(mfid),
        "opcode": hex(opcode)
    }
    
    if opcode in (0x00, 0x20):  # Group Voice Channel Grant
        target_tgid = (tsbk_bytes[3] << 8) | tsbk_bytes[4]
        source_suid = (tsbk_bytes[5] << 16) | (tsbk_bytes[6] << 8) | tsbk_bytes[7]
        channel_index = (tsbk_bytes[8] >> 4) & 0x0F
        channel_number = ((tsbk_bytes[8] & 0x0F) << 8) | tsbk_bytes[9]
        
        base_freq = 851.0125
        assigned_freq = base_freq + (channel_number * 0.0125)
        
        parsed.update({
            "message_type": "Group Voice Channel Grant",
            "target_tgid": target_tgid,
            "source_suid": source_suid,
            "channel_index": channel_index,
            "channel_number": channel_number,
            "assigned_frequency_mhz": round(assigned_freq, 4)
        })
        
    elif opcode == 0x28:  # Unit Registration Response
        reg_status = tsbk_bytes[3]
        source_suid = (tsbk_bytes[4] << 16) | (tsbk_bytes[5] << 8) | tsbk_bytes[6]
        target_suid = (tsbk_bytes[7] << 16) | (tsbk_bytes[8] << 8) | tsbk_bytes[9]
        
        parsed.update({
            "message_type": "Unit Registration Response",
            "registration_status": "Success" if reg_status == 0 else f"Denied ({hex(reg_status)})",
            "source_suid": source_suid,
            "assigned_virtual_suid": target_suid
        })
        
    elif opcode == 0x29:  # Group Affiliation Response
        affil_status = tsbk_bytes[3]
        source_suid = (tsbk_bytes[4] << 16) | (tsbk_bytes[5] << 8) | tsbk_bytes[6]
        target_tgid = (tsbk_bytes[7] << 8) | tsbk_bytes[8]
        
        parsed.update({
            "message_type": "Group Affiliation Response",
            "affiliation_status": "Affiliated" if affil_status == 0 else f"Refused ({hex(affil_status)})",
            "source_suid": source_suid,
            "target_tgid": target_tgid
        })
    else:
        parsed["message_type"] = f"Other TSBK Opcode ({hex(opcode)})"
        
    return parsed
 
 
# Execution Verification
if __name__ == "__main__":
    print("[+] Initializing P25 Control Channel Demodulator Pipeline...")
 
    # Generate synthetic C4FM deviations matching FSW pattern (0x5575F575775F)
    fsw_dibits = [0,1, 0,1, 0,1, 0,1, 0,1, 1,1, 0,1, 0,1, 1,1, 1,1, 0,1, 0,1, 
                  0,1, 1,1, 0,1, 0,1, 0,1, 1,1, 0,1, 1,1, 0,1, 1,1, 0,1, 1,1]
    
    dev_map = {0b01: 1.8, 0b00: 0.6, 0b10: -0.6, 0b11: -1.8}
    synth_deviations = np.array([dev_map[d] for d in fsw_dibits])
    
    sliced_dibits = slice_c4fm_symbols(synth_deviations)
    sync_indices = correlate_frame_sync(sliced_dibits)
    
    print(f"[+] Frame Sync Search Complete. Sync Matches Found at Indices: {sync_indices}")
    
    # Test Grant Message Payload
    sample_grant_tsbk = bytes([
        0x80, 0x00, 0x00,        # Last Block, MFID 0x00, Opcode 0x00 (GVCG)
        0x03, 0xE9,              # TGID: 1001
        0x00, 0xC3, 0x7A,        # SUID: 50042
        0x00, 0x18,              # Channel Index 0, Number 24
        0x00, 0x00               # Padding
    ])
    
    crc_grant = compute_crc_ccitt_p25(sample_grant_tsbk)
    print(f"[+] Grant TSBK CRC-CCITT Checksum: {hex(crc_grant)}")
    parsed_grant = decode_tsbk_payload(sample_grant_tsbk)
    print("[+] Parsed Group Voice Channel Grant Message:")
    for k, v in parsed_grant.items():
        print(f"    ├── {k}: {v}")
 
    # Test Registration Response Payload
    sample_reg_tsbk = bytes([
        0x80, 0x00, 0x28,        # Last Block, MFID 0x00, Opcode 0x28 (Registration)
        0x00,                    # Reg Status: Success
        0x00, 0xC3, 0x7A,        # Source SUID: 50042
        0x00, 0xC3, 0x7A,        # Target SUID: 50042
        0x00, 0x00               # Padding
    ])
    parsed_reg = decode_tsbk_payload(sample_reg_tsbk)
    print("[+] Parsed Unit Registration Response:")
    for k, v in parsed_reg.items():
        print(f"    ├── {k}: {v}")

Complete Open-Source SDR Monitoring Solutions

Rather than maintaining custom DSP code for full multi-channel reception, production SDR monitoring stations utilize mature open-source software frameworks:

  1. OP25 (gr-op25): A GNU Radio-based P25 decoder supporting Phase 1 C4FM/CQPSK and Phase 2 TDMA. Its rx.py client parses control channel grants, manages hardware tuners, streams live audio via Icecast, and logs talkgroup metadata to JSON.
  2. SDRTrunk: A Java-based multi-channel trunking monitor. It uses polyphase channelizer algorithms to decode multiple control channels and voice channels simultaneously across broad spectrum spans, providing real-time call recording, talkgroup filtering, and desktop audio playback.
  3. DSD+ (Digital Speech Decoder Plus): A Windows/Linux binary utility designed for low-level digital signal decoding across P25, DMR, NXDN, and dPMR networks.

Production OP25 Configuration Setup

Deploying gr-op25 requires building a system configuration file (trunk.tsv) mapping control channels, modulation types, and voice channel behavior.

"SysName"	"Control Channel Freq"	"Offset"	"System ID"	"WACN"	"Modulation"	"TGID Map"	"Whitelist"	"Blacklist"
"MetroSafety"	"851.0125M"	"0"	"0x1A4"	"0xBEE00"	"C4FM"	"tgids.tsv"	"white.txt"	"black.txt"

The accompanying rates.tsv file configures hardware sampling parameters:

"Symbol Rate"	"Filter Type"	"Squelch"	"Gain"
"4800"	"RC"	"-50"	"40"

When launched via terminal:

./rx.py --args "rtl=0" -N "LNA:40" -f 851.0125M -O default -q 0 -T trunk.tsv -b 9600 -v 10

OP25 initializes an internal socket thread listening for control channel TSBKs. When an Opcode 0x00 Grant is detected matching a Talkgroup ID listed in white.txt, OP25 retunes a secondary virtual receiver to the granted voice frequency, pipes raw IMBE vocoder bytes into its software synthesizer, and routes uncompressed 8 kHz PCM audio to an ALSA sound device or Icecast streaming mountpoint.


6. Security Analysis and System Vulnerabilities

P25 trunked radio networks deployed across public safety and industrial sectors face several physical layer and network layer attack vectors:

1. Control Channel Jamming and Denial of Service

Because control channel signaling is unencrypted and continuously broadcast from static frequencies, a targeted RF noise source emitting continuous wave (CW) or wideband noise at 851-869 MHz can disrupt TSBK decoding across subscriber radios within a cell sector. If radios lose control channel synchronization, they fail to receive Group Voice Channel Grants and fall back to site-search failover modes, rendering local trunking inactive.

2. Physical Layer Rogue Site Injection

P25 Phase 1 authentication is asymmetric: subscriber units authenticate to the network during affiliation, but subscriber radios do not cryptographically authenticate the repeater site tower.

An attacker operating a high-power SDR transmitter can broadcast a rogue P25 control channel utilizing matching System ID, WACN, and NAC parameters. Subscriber units observing a stronger RSSI from the rogue tower will initiate cell re-affiliation, enabling rogue channel grant injection or localized subscriber Denial of Service.

3. Metadata Exfiltration and Traffic Analysis

Even when a network enforces AES-256-OFB encryption across every voice channel, unencrypted control channel signaling exposes complete operational metadata:

  • Real-time deployment tracking of emergency units via unique Radio IDs (SUIDs)
  • Call volume surges pointing to active incident locations
  • Cross-agency mutual aid affiliations

4. Over-The-Air Rekeying Interception Risks

If legacy P25 networks execute OTAR rekeying over unencrypted control or traffic channels using weak Key Encryption Keys (such as 40-bit ADP or 56-bit DES KEKs), attackers with SDR recording infrastructure can intercept wrapped TEK payloads. Offline brute-force key recovery against 40-bit ADP keys takes under 15 seconds on modern GPU clusters, completely compromising subsequent AES-256 traffic if the key variable loader re-uses key material across ciphers.

Securing public safety communications requires hardened site physical security, continuous spectrum monitoring to detect rogue transmitters, and adoption of key management architectures (OTAR) to maintain cipher strength against physical key compromise.