DMR (ETSI TS 102 361): native Tier I/II reception
A self-contained, clean-room receiver for DMR (Digital Mobile Radio) Tier I/II: the C4FM 4-level modem is shared verbatim with P25, the 264-bit burst is split into payload / sync-or-embedded / payload, data and control bursts protect 96 information bits with a BPTC(196,96) product code and a Golay(20,8) slot type, the full link-control word is Reed-Solomon-masked, CSBKs are CRC-masked, and voice bursts carry three AMBE+2 channel frames each — six bursts to a superframe, with the full LC dribbled across the middle four under 16-bit EMB words. The whole decodable path runs in-repo on NumPy; only the AMBE+2 → PCM vocoder step is delegated (see AMBE).
Rafe project · app/radio/dmr.py · in-repo NumPy
Abstract
DMR (ETSI TS 102 361) is the European digital land-mobile-radio standard for licensed PMR, amateur and commercial fleets. It is a two-slot TDMA system in a 12.5 kHz channel: the RF carrier is 4-level FSK (C4FM) at 4800 sym/s, the same continuous-phase 4-level baseband as P25, so Rafe's P25 modem (app/radio/p25/demod.py) is reused unchanged. Each 30 ms timeslot carries a 264-bit burst — 108 payload | 48 sync-or-embedded | 108 payload — optionally prefixed by a 24-bit CACH inserted by a repeater. Data and control bursts carry 96 information bits protected by BPTC(196,96) (Hamming(15,11) on rows crossed with Hamming(13,9) on columns) plus a Golay(20,8) slot-type header; voice bursts carry three 72-bit AMBE+2 channel frames, six bursts A–F to a superframe, with the 72-bit full link-control word embedded across bursts B–E under 16-bit EMB words.
This document specifies Rafe's native DMR receiver in full, from the code in app/radio/dmr.py. It is a clean-room implementation from TS 102 361-1: burst geometry, the four 48-bit sync words, the BPTC dimensions and interleave step, the slot-type / LC / CSBK layouts and their CRC masks are the published values. The EMB (16,7) generator, the embedded-LC fragment interleave, the CACH TACT interleave and the RS(12,9) generator are structurally faithful and round-trip self-consistently; their exact on-air bit orderings are the interoperability checks (see §8). The decode chain — demod → sync → BPTC/Golay/RS/CRC → LC/CSBK metadata and the 18-frame AMBE parameter set of a voice superframe — is fully native. Only the AMBE+2 channel-frame → speech vocoder is delegated to the AMBE parameter codec + MBE vocoder.
1. Motivation and scope
DMR is the most common amateur digital-voice mode after D-STAR and, on commercial bands, ubiquitous. A monitor receiver wants two things from a DMR signal: the call metadata — colour code, source and destination IDs, the CSBK control traffic — and the voice. The first needs only the modem and the block codes; the second additionally needs the AMBE+2 vocoder. Rafe's app/radio/dmr.py moves the entire decodable part in-house — everything from the 4-level baseband to the recovered link control and the 18 AMBE parameter frames of a voice superframe — leaving only the AMBE+2 channel-frame → PCM step to the shared vocoder:
NATIVE (app/radio/dmr.py + p25/demod.py, p25/fec.py)
FM-disc audio ─▶ C4FM demod ─▶ sync search ─▶ burst dispatch ┐
(48 kHz) p25/demod.py find_sync (voice vs data) │
(reused as-is) │
┌─ data/control ─▶ BPTC ─▶ slot type ─▶ LC(+RS) / CSBK(+CRC) ─▶ metadata
└─ voice A–F ─▶ split ─▶ AMBE channel_to_hr ×18 ─▶ embedded-LC/EMB
│
└▶ AMBE params ─▶ [AMBE codec] ─▶ [MBE vocoder] ─▶ PCM
The design split mirrors the P25 package: the DSP + protocol layers (demod, framing, FEC, LC/CSBK extraction, voice-frame de-framing) are fully native; only the AMBE+2 vocoder step is external. The module header states the honesty line explicitly — burst geometry, sync words, BPTC dimensions/interleave and the slot-type/LC/CSBK layouts and CRC masks are published values; the EMB generator, embedded-LC interleave, CACH TACT interleave and RS(12,9) generator are structurally faithful and self-consistent, with their exact on-air bit orderings flagged as the interop checks (app/radio/dmr.py docstring).
This spec covers Tier I/II FDMA-framed C4FM as implemented: one logical channel decoded per call. Full two-slot TDMA multiplexing, over-the-air data services beyond the CSBK/data-header type tags, and encryption are out of scope (§8).
2. Background — real DMR
2.1 Two-slot TDMA, Tier I/II
DMR divides each 12.5 kHz channel by time: two 30 ms timeslots (TS1, TS2) alternate on a single carrier, giving two independent conversations — a 2:1 capacity gain over a 12.5 kHz analogue channel and the reason DMR is often described as "12.5 kHz equivalent." Tier I is licence-exempt / simplex, Tier II is licensed conventional (and the base of Tier III trunking); both share the same physical burst and coding, which is what this receiver decodes. A repeater retransmits both slots and, to let mobiles track slot timing and signalling between bursts, inserts a CACH (Common Announcement Channel) burst ahead of each payload burst.
2.2 The burst / superframe hierarchy
Everything on air is a 264-bit burst occupying one 30 ms slot. A burst is either:
- a data / control burst — 196 BPTC-coded bits carrying 96 information bits (a full LC header, a terminator, a CSBK, or a data header), plus a 20-bit slot-type header and one of the fixed sync patterns; or
- a voice burst — three 72-bit AMBE+2 channel frames, with a 48-bit centre that is either a sync word (first burst of a superframe) or an EMB word wrapped around an embedded-LC fragment.
Voice bursts group into a superframe of six bursts, labelled A–F. Burst A carries the voice sync word in its centre; bursts B–E carry the four fragments of the 72-bit full LC (protected by an extended-Hamming block + checksum) under 16-bit EMB words; burst F carries a null fragment. So the LC that names the call is recovered twice: once from the LC-header data burst that precedes the voice, and again, incrementally, from the embedded fragments spread across the superframe.
This receiver surfaces, for a data/control burst, the colour code, data type and (for LC headers/terminators) the parsed link control with an RS "ok" flag, or the CSBK opcode/FID/payload with a CRC "ok" flag; for a voice superframe, the colour code, the embedded LC, and the 18 AMBE+2 parameter frames ready for the vocoder. All of it comes from the modem + block codes alone — the vocoder is only needed to turn those 18 parameter frames into speech.
3. Signal and symbol structure — exact numbers
Every value here is a constant in the code or directly derivable from one; the source is cited.
Symbol layer (app/radio/p25/demod.py, reused unchanged)
- Modulation: continuous-phase 4-level FSK (C4FM), identical baseband to P25.
- Symbol rate:
SYMRATE = 4800sym/s. - Bits per symbol: 2 (one dibit) → gross channel rate 9600 bit/s.
- Four levels {+3, +1, −1, −3} from
_LEVEL = {1: 3, 0: 1, 2: -1, 3: -3}(dibit value → level); deviations ±1800 Hz (outer) / ±600 Hz (inner). - Default audio rate
fs = 48000→ samples/symbolsps = fs // SYMRATE = 10. - Matched filter: root-raised-cosine, roll-off α = 0.2, span 8 symbols.
Because the DMR baseband is C4FM, dmr.py calls demod.modulate / demod.demodulate / demod.dibits_to_bits directly — no DMR-specific modem code exists. See P25 §4 for the full demod description (RRC matched filter, maximum-eye-opening timing search, amplitude normalisation, 4-level slice).
Burst layer (app/radio/dmr.py)
- Burst length:
BURST = 264bits = 108 payload | 48 sync-or-embedded | 108 payload (dmr.py). - CACH:
CACH = 24bits, prepended by a repeater (dmr.py). - Stride between successive bursts:
STRIDE = CACH + BURST = 288bits (dmr.py). - Four 48-bit sync words in
SYNCS(§6.1).
Coding layer (dmr.py, app/radio/p25/fec.py) — full constants in §6: BPTC(196,96) (Hamming(15,11) rows × Hamming(13,9) columns, interleave step _BPTC_STEP = 181); Golay(20,8) slot type over colour code(4) + data type(4); full LC 72→96 bits by RS(12,9) over GF(2⁸) with a data-type mask; CSBK 80-bit body
- CRC-16-CCITT masked
0xA5A5; EMB (16,7) generator0x2EDover CC(4)+PI(1)+ LCSS(2); embedded LC 72+5 bits → 128-bit extended-Hamming block → four 32-bit fragments; CACH TACT Hamming(7,4) over AT/TC/LCSS + a 17-bit short-LC slot.
Voice layer (dmr.py, app/radio/ambe.py) — 3 × 72-bit AMBE+2 channel frames per voice burst; 6 bursts A–F per superframe → 18 channel frames = 18 × 49-bit AMBE+2 parameter frames. Each channel frame is Golay(24,12) over the 12 most-sensitive bits + PN-scrambled Golay(23,12) over the next 12 + 25 raw bits (AMBE).
4. The bursts and sync
4.1 Data / control burst (264 bits)
build_data_burst(bits96, cc, dtype, sync) (dmr.py) BPTC-encodes the 96 info bits to 196 bits, Golay-encodes the slot type to 20 bits, and interleaves them around the 48-bit sync word:
return coded[:98] + st[:10] + SYNC(48) + st[10:] + coded[98:]
bit 0 98 108 155 166 263
┌────────────────────────────┬─────┬──────────────────┬─────┬──────────────────────────┐
│ BPTC payload (98 bits) │ ST │ SYNC (48b) │ ST │ BPTC payload (98 bits) │
│ coded[0:98] │[0:10]│ 48-bit sync word │[10:20]│ coded[98:196] │
└────────────────────────────┴─────┴──────────────────┴─────┴──────────────────────────┘
└──────── 108 ────────┘ └────── 48 ──────┘ └──────── 108 ─────────┘
The 196 BPTC bits are split 98 | 98 around the centre; the 20 slot-type bits are split 10 | 10 flanking the sync. parse_data_burst reverses this: slot type from bits[98:108] + bits[156:166], BPTC info from bits[:98] + bits[166:] (dmr.py).
4.2 Voice burst (264 bits)
build_voice_burst(f1, f2, f3, centre48) (dmr.py) lays three 72-bit AMBE channel frames around a 48-bit centre, splitting the middle frame f2 across it:
return f1 + f2[:36] + centre48 + f2[36:] + f3
bit 0 72 108 155 192 263
┌──────────────┬──────────┬──────────────────┬──────────┬──────────────┐
│ AMBE f1 │ f2[:36] │ centre (48b) │ f2[36:] │ AMBE f3 │
│ 72 bits │ 36 bits │ sync-or-EMB+frag │ 36 bits │ 72 bits │
└──────────────┴──────────┴──────────────────┴──────────┴──────────────┘
split_voice_burst reverses it: f1 = bits[:72], f2 = bits[72:108] + bits[156:192], f3 = bits[192:], centre = bits[108:156] (dmr.py).
4.3 The four sync words
DMR defines four 48-bit sync patterns — base-station vs mobile-station, voice vs data (SYNCS, dmr.py):
bs_voice = 0x755FD7DF75F7 ms_voice = 0x7F7D5DD57DFD
bs_data = 0xDFF57D75DF5D ms_data = 0xD5D7F77FD757
find_sync(bits, start, max_err=4) (dmr.py) slides a 48-bit window across the bit stream and returns (index, kind) for the first position whose Hamming distance to any of the four patterns is ≤ max_err (default 4), with an early-out once the error count exceeds the budget:
for i in range(start, n - 47):
for kind, pat in _SYNC_BITS.items():
err = Σ_{j<48} [ bits[i+j] ≠ pat[j] ] # break as soon as err > max_err
if err ≤ max_err: return i, kind
return -1, None
The kind string is what dispatches the burst: *_data → data/control path, *_voice → voice superframe path (§5.7). Because the sync is 48 fixed bits, a 4-bit tolerance is far below the random-collision floor.
4.4 The stream a repeater emits
modulate_voice_call prefixes every burst with a 24-bit CACH (cach_encode(1, 0, 0)), so a full voice call on the wire is:
[CACH][LC-header data burst (dtype 1, MASK_VOICE_HEADER)]
[CACH][voice burst A] [CACH][B] [CACH][C] [CACH][D] [CACH][E] [CACH][F]
[CACH][terminator data burst (dtype 2, MASK_TERMINATOR)]
padded front and back with 24 zero bits, then modulated. Successive bursts are therefore STRIDE = 288 bits apart, and the decoder steps by that stride when it walks the six bursts of a superframe (§5.7).
5. Decode — step by step
The top-level entry is decode(audio, fs=48000, max_err=4) (dmr.py): demodulate to a bit stream, then repeatedly find_sync, dispatch on the sync kind, and advance. The building blocks:
5.1 BPTC(196,96) — the product code (dmr.py)
DMR's data/control payload is a Block Product Turbo Code: a 13-row × 15-column matrix (plus one pad bit = 196 bits) in which every one of columns is a Hamming(13,9) codeword and rows 0–8 are Hamming(15,11) codewords. The 96 info bits sit at row 0 cols 3–10 (8 bits, cols 0–2 reserved) + rows 1–8 cols 0–10 (88 bits) (_bptc_rows).
Interleave. The 196 matrix bits are permuted by a fixed step _BPTC_STEP = 181:
_bptc_interleave: out[i] = m[(i*181) % 196] # 181 is coprime to 196
_bptc_deinterleave: m[(i*181) % 196] = a[i]
Encode (bptc_encode): Hamming(15,11)-encode each of the 9 rows (row 0 is 3 reserved zeros + 8 info bits), append four all-zero rows 9–12, then for each of the 15 columns compute the Hamming(13,9) parity of rows 0–8 and write the 4 parity bits down rows 9–12. Flatten with a leading pad bit and interleave.
Decode (bptc_decode): deinterleave to the 13×15 grid, correct columns first (Hamming(13,9), single-error per column), then rows (Hamming(15,11)), and return row0[3:] + rows 1–8. Correcting columns before rows lets the product code fix a scatter of single errors that neither dimension could fix alone (the test flips 3 bits in distinct rows and columns and still recovers — §7). BPTC carries no CRC of its own; integrity is checked by the layer above (slot-type Golay, LC RS, CSBK CRC).
5.2 Slot type — Golay(20,8) (dmr.py)
The slot-type header carries colour code (4 bits) + data type (4 bits), protected by a shortened Golay(24,12): slot_type_encode(cc, dtype) packs d8 = (cc<<4)|dtype, Golay(24,12)-encodes it (the top 4 of the 12 data bits are zero), and drops those 4 leading zeros → 20 bits. slot_type_decode prepends [0,0,0,0], runs golay24_decode, and returns (cc, dtype) — or None if the Golay fails or the decoded value exceeds 8 bits. Data types (DATA_TYPES):
1: voice_lc_header 2: terminator_lc 3: csbk 6: data_header
5.3 Full link control + RS(12,9) masks (dmr.py)
The full LC is 72 bits, laid out (MSB-first) as FLCO(8) + FID(8) + service-options(8) + destination(24) + source(24) (build_lc / parse_lc). Note the on-air field order places the 24-bit destination before source, even though build_lc(flco, src, dst, …) takes src first in its signature.
It is protected by a Reed-Solomon(12,9) code over GF(2⁸) (primitive polynomial 0x11D), whose 3 parity bytes are XORed with a data-type-specific CRC mask so that decoding under the wrong mask deliberately fails (this is how DMR ties the LC to its slot type):
MASK_VOICE_HEADER = 0x969696 # voice LC header (dtype 1)
MASK_TERMINATOR = 0x999999 # terminator with LC (dtype 2)
- GF(2⁸) log/antilog tables
_EXP/_LOGare stepped from α (start 1,g <<= 1, XOR0x11Dwhen bit 8 sets) over 255 elements, with_EXPdoubled to 512 to avoid a modulo in_gmul. - Parity (
_rs12_9_parity): the generator is built as(x−α⁰)(x−α¹)(x−α²); the 9 data bytes are fed through a systematic LFSR remainder to produce 3 parity bytes. - Encode (
lc_encode): split the 72-bit LC into 9 bytes, compute the RS parity, XOR the mask bytes in, append → 96 bits. - Decode (
lc_decode): un-mask the 3 parity bytes, form the 12-byte codeword, evaluate the three syndromesSⱼ = Σ_k c_k · α^(j·(11−k))for roots α⁰…α² and return(LC[:72], ok)whereok = (all syndromes zero). A wrong mask (or a corrupted bit) leaves non-zero syndromes →ok = False.
5.4 CSBK — Control Signalling Block (dmr.py)
A CSBK is 96 bits: LB(1) + PF(1) + CSBKO(6) + FID(8) + payload(64) + CRC-16(16). build_csbk(csbko, fid, payload64) forms the 80-bit head [1,0] + CSBKO + FID + payload (LB=1, PF=0), then a CRC-16-CCITT (poly 0x1021, init 0xFFFF, MSB-first, no final inversion — _crc16) XORed with MASK_CSBK = 0xA5A5. parse_csbk recomputes the CRC over bits[:80], XORs the mask, compares to bits[80:], and returns {csbko, fid, payload, ok}.
5.5 EMB + embedded LC (dmr.py)
EMB (16,7). The 48-bit voice-burst centre (bursts B–F) is `EMB[:8] + fragment
- EMB[8:]
— a 16-bit EMB word split around a 32-bit embedded-LC fragment. The EMB carries **CC(4) + PI(1) + LCSS(2)** = 7 data bits protected by a (16,7) shortened cyclic code, generator_EMB_GEN = 0x2ED(degree 9)._emb_paritydoes binary polynomial division to get 9 parity bits; a 128-entry codeword table_EMB_TABLE[d] = (d<<9) | parityis precomputed.emb_encode(cc, pi, lcss)looks up the codeword;emb_decodedoes a **nearest-codeword** search (minimum Hamming distance, threshold < 3 → corrects up to 2 errors) and returns(cc, pi, lcss)`.
Embedded LC. embed_lc_encode(bits72) builds the block that dribbles the LC across bursts B–E:
info = LC(72) + checksum(5) # 77 bits = 7 rows × 11
rows = 7 × extended-Hamming(16,11) # _ham16_enc: Hamming(15,11) + overall parity
row 8 = column parity (XOR of the 7 rows) # 16 bits
flat = 8 rows × 16 = 128 bits → four 32-bit fragments
The 5-bit checksum _cs5 is the sum of the nine LC octets mod 31. On decode, embed_lc_decode(fragments) flattens the four 32-bit fragments, Hamming(15,11)- corrects each of the 7 data rows (the first 15 bits of each 16-bit row), extracts LC = info[:72] and checksum = info[72:77], and returns (LC, ok) where ok = (checksum == _cs5(LC)). (The 8th column-parity row is generated on encode but not consulted on decode — the per-row Hamming correction plus the checksum are what gate the result.)
5.6 CACH — Common Announcement Channel (dmr.py)
The 24-bit CACH is a 7-bit TACT codeword + a 17-bit short-LC slot (zeros here). The TACT packs AT(1) + TC(1) + LCSS(2) into 4 bits and protects them with a Hamming(7,4) code (_ham7_enc parity p = [d0⊕d1⊕d2, d1⊕d2⊕d3, d0⊕d1⊕d3]); _ham7_dec corrects a single error via a syndrome→bit table. cach_decode returns {at, tc, lcss}. (Rafe emits CACH on every burst; a real system only inserts it on repeater outbound slots — §8.)
5.7 Voice superframe assembly (dmr.py)
Encode — modulate_voice_call(src, dst, ambe_frames, cc=1, fs=48000, flco=0):
- Assert exactly 18 AMBE+2 parameter frames (3 per burst × 6 bursts).
- Build the LC (
build_lc(flco, src, dst)) and its four embedded fragments (embed_lc_encode). - Emit
CACH + build_data_burst(lc_encode(lc, MASK_VOICE_HEADER), cc, 1)— the LC-header data burst. - Convert all 18 parameter frames to channel frames (
ambe.hr_to_channel). - For bursts A–F (k = 0…5):
- k = 0 (A): centre =
bs_voicesync word. - k = 1…5 (B–F):
lcss = 1 if k==1 else (2 if k<5 else 3)(first / continuation / last),frag = frags[k-1]for k ≤ 4 else a 32-bit zero fragment,centre = emb_encode(cc,0,lcss)[:8] + frag + emb_encode(...)[8:]. - Emit
CACH + build_voice_burst(ch[3k], ch[3k+1], ch[3k+2], centre).
- k = 0 (A): centre =
- Emit
CACH + build_data_burst(lc_encode(lc, MASK_TERMINATOR), cc, 2)— the terminator. - Zero-pad 24 bits each side and
demod.modulate(bits_to_dibits(...)).
Voice superframe (6 bursts, centre 48 bits each)
┌──────┬───────────────────────────┬──────┐
│ A │ centre = bs_voice sync (48)│ │ ← sync, no LC fragment
├──────┼───────────────────────────┼──────┤
│ B │ EMB(8) │ frag0 (32) │ EMB(8) │ LCSS=1 first
│ C │ EMB(8) │ frag1 (32) │ EMB(8) │ LCSS=2 cont
│ D │ EMB(8) │ frag2 (32) │ EMB(8) │ LCSS=2 cont
│ E │ EMB(8) │ frag3 (32) │ EMB(8) │ LCSS=2 cont
│ F │ EMB(8) │ zeros (32) │ EMB(8) │ LCSS=3 last (null fragment)
└──────┴───────────────────────────┴──────┘
each burst also carries 3 × 72-bit AMBE frames = 18 total
Decode — inside decode, when find_sync returns a *_voice kind:
burst0 = i - 108(the sync sits at burst bits 108–155, so back up to the burst start).- For k = 0…5, take the burst at
burst0 + k*STRIDE,split_voice_burstit, andambe.channel_to_hreach of the three 72-bit frames → append 18 parameter frames. - For k = 1…4, collect the fragment
centre[8:40]andemb_decode(centre[:8] + centre[40:])(the last successful EMB gives the colour code). - If all four fragments were collected,
embed_lc_decodethem → LC + ok, andparse_lcpopulatessrc/dst/etc. on the event. - Advance
pos = burst0 + 6*STRIDE.
The emitted voice event is {"type": "voice", "frames": [18 param frames], "cc": …, "src"/"dst"/… , "lc_ok": …}.
Data/control decode — for a *_data sync: parse_data_burst → slot type. If slot_type is None the burst is skipped. Data types 1/2 decode the LC under MASK_VOICE_HEADER / MASK_TERMINATOR respectively and emit voice_lc_header / terminator_lc events; data type 3 parses the CSBK; type 6 is surfaced as data_header. Each event carries the colour code.
6. Constants and tables
All values quoted verbatim from app/radio/dmr.py unless noted.
6.1 Sync words and geometry
SYNCS = {"bs_voice": 0x755FD7DF75F7, "bs_data": 0xDFF57D75DF5D,
"ms_voice": 0x7F7D5DD57DFD, "ms_data": 0xD5D7F77FD757} # 48 bits each
BURST = 264 # 108 payload | 48 sync-or-embedded | 108 payload
CACH = 24
STRIDE = CACH + BURST = 288
6.2 Data types and CRC masks
DATA_TYPES = {1: "voice_lc_header", 2: "terminator_lc", 3: "csbk", 6: "data_header"}
MASK_VOICE_HEADER = 0x969696 # full-LC RS parity mask, data type 1
MASK_TERMINATOR = 0x999999 # full-LC RS parity mask, data type 2
MASK_CSBK = 0xA5A5 # CSBK CRC-16 mask
6.3 BPTC(196,96)
_BPTC_STEP = 181 # out[i] = m[(i*181) % 196]
matrix = 1 pad bit + 13 rows × 15 columns
rows 0–8 = Hamming(15,11) codewords (fec.hamming15_encode/decode)
columns = Hamming(13,9) codewords (fec.hamming13_encode/decode)
info bits = row 0 cols 3–10 (8) + rows 1–8 cols 0–10 (88) = 96
decode = correct columns (13,9) then rows (15,11)
hamming13_encode/decode use data columns _H13_COLS = [3,5,6,7,9,10,11,12,13] (p25/fec.py); hamming15_* use _H15_COLS = [3,5,6,7,9,10,11,12,13,14,15].
6.4 Slot type — Golay(20,8)
d8 = (cc<<4) | dtype # 4-bit colour code + 4-bit data type
slot_type_encode = golay24_encode(d8)[4:] # shortened: 4 leading data zeros dropped → 20 bits
slot_type_decode = golay24_decode([0,0,0,0]+bits20) → (cc, dtype) (None if d>0xFF)
Golay(24,12) is p25/fec.py's perfect code (generator _G23 = 0xC75, 2048-entry syndrome table, +overall-parity extension).
6.5 Full LC + RS(12,9)
LC(72) = FLCO(8) | FID(8) | opts(8) | dst(24) | src(24) # dst BEFORE src on air
GF(2^8) primitive polynomial = 0x11D
RS generator = (x−α^0)(x−α^1)(x−α^2) # 3 parity bytes
lc_encode: LC(72) + (RS parity ⊕ mask bytes) → 96 bits
lc_decode: ok ⇔ syndromes S_j = Σ_k c_k·α^(j·(11−k)) all zero under this mask
6.6 CSBK
CSBK(96) = LB(1)=1 | PF(1)=0 | CSBKO(6) | FID(8) | payload(64) | CRC16(16)
CRC-16-CCITT: poly 0x1021, init 0xFFFF, MSB-first, no final XOR, then ⊕ 0xA5A5
6.7 EMB (16,7)
_EMB_GEN = 0x2ED # degree-9 (16,7) generator
d7 = (cc<<3) | (pi<<2) | lcss # CC(4) | PI(1) | LCSS(2)
_EMB_TABLE[d] = (d<<9) | _emb_parity(d) # 128 codewords
emb_decode = nearest codeword (Hamming distance < 3 ⇒ corrects ≤ 2)
6.8 Embedded LC
info = LC(72) + checksum(5) # checksum = Σ(nine LC octets) mod 31
7 rows of extended Hamming(16,11) [Hamming(15,11) + overall parity bit]
+ 1 column-parity row → 128 bits → 4 × 32-bit fragments
decode: per-row Hamming(15,11) correct + checksum gate
LCSS across B–F = 1 (first), 2, 2, 2 (cont), 3 (last, null fragment)
6.9 CACH
CACH(24) = TACT(7) + short-LC(17, zeros here)
TACT = Hamming(7,4) over AT(1) | TC(1) | LCSS(2)
_ham7 parity = [d0⊕d1⊕d2, d1⊕d2⊕d3, d0⊕d1⊕d3]
6.10 AMBE+2 channel frame (AMBE)
hr_to_channel(bits49) → 72 bits: Golay(24,12) over C0(12) + PN-scrambled
Golay(23,12) over C1(12) + 25 unprotected bits
channel_to_hr(bits72) → (49-bit parameter frame, corrected-bit count) | (None, -1)
7. Interoperability and validation
7.1 The test suite (test_dmr.py)
Eight tests exercise every native layer; all use fixed RNG seeds and are self-consistent (encode → corrupt → decode). All pass.
| test | what it proves | key assertions |
|---|---|---|
test_bptc_roundtrip_and_correction |
BPTC(196,96) round-trip + product-code correction | encode → 196 bits, decode == info; 3 errors placed in distinct rows and columns are all corrected |
test_slot_type_golay20 |
Golay(20,8) slot type | encode(5,3) → 20 bits, decodes to (5,3); still (5,3) after 3 random bit flips |
test_sync_detection_with_errors |
error-tolerant sync search | a bs_voice pattern with 3 flipped bits, padded, is found at the right index with the right kind |
test_full_lc_rs_masks |
RS(12,9)-masked LC | round-trips FLCO/FID/opts/dst/src exactly; decoding under the wrong mask fails; a single flipped bit fails the syndrome |
test_embedded_lc |
embedded-LC block | 4 × 32-bit fragments; one error injected into three different rows still recovers the exact 72-bit LC |
test_csbk |
CSBK CRC-mask + parse | csbko=61, fid=16, payload=0xDEADBEEF12345678 round-trips with ok; a flipped bit clears ok |
test_voice_call_end_to_end |
full call assembly + decode | events == [voice_lc_header, voice, terminator_lc]; LC ok on all three; all 18 AMBE frames recovered exactly; cc == 1 |
test_voice_call_through_noise |
call through AWGN (≈30 dB) | exactly one voice event; ≥ 16 of 18 AMBE frames match; LC header still reads src correctly |
The end-to-end test is the strongest evidence the framing is internally correct: it generates 18 real AMBE+2 parameter frames, modulate_voice_calls a complete header + superframe + terminator, runs the whole modem+decode chain, and requires bit-exact recovery of all 18 parameter frames plus correct LC and colour code. The noise test adds AWGN to ≈30 dB SNR and still recovers ≥ 16/18 frames and the header LC — the BPTC/Golay/RS/embedded-Hamming correction working end-to-end.
7.2 On-air reference
These tests validate the transcription against itself (encode → corrupt → decode round-trips). The published quantities — burst geometry, the four 48-bit sync words, the BPTC dimensions and 181-step interleave, the slot-type / LC / CSBK layouts and the RS/CRC masks — are taken directly from ETSI TS 102 361-1 and can be checked against any DMR reference. The structurally-faithful quantities — the EMB (16,7) generator, the embedded-LC fragment interleave, the CACH TACT interleave and the RS(12,9) generator construction — round-trip self-consistently but their exact on-air bit orderings are not yet verified against a real DMR signal (§8). The final interoperability check — decoding an off-air DMR capture — would compare against an external reference decoder (e.g. DSD/dsd-fme), the same role mbelib/dsd-fme play for the P25 path.
8. Limitations
Stated honestly, roughly in order of impact. The module docstring draws the line explicitly; this section preserves it.
- The interop-flagged items are self-consistent, not bit-exact on air. The EMB (16,7) generator (
0x2ED), the embedded-LC fragment interleave, the CACH TACT bit interleave and the RS(12,9) generator are structurally the DMR codes and round-trip perfectly against themselves, but their exact on-air bit orderings have not been confirmed against a real DMR transmission. A decoder built to this spec will interoperate with Rafe's own modulator; matching a commercial radio bit-for-bit on these four items is the outstanding interop check. - AMBE+2 audio is not bit-exact. The decodable path recovers the 18 AMBE+2 parameter frames exactly, but turning them into speech goes through the AMBE codec + MBE vocoder, whose quantiser tables, PN seed and inter-frame prediction are structurally faithful rather than DVSI-exact — intelligible-but-approximate audio until those are transcribed (same stance as the P25/IMBE path).
- Single logical channel, no TDMA multiplexing. The decoder locks a sync and walks one call's bursts; it does not de-multiplex the two 30 ms TDMA timeslots or track slot timing across a repeater's interleaved TS1/TS2 stream. CACH is emitted on every burst by the modulator (a real system inserts it only on repeater outbound slots).
- LCSS/EMB semantics are approximate. Burst F is marked
LCSS = 3(last) while carrying a null fragment; the single-burst LCSS = 0 case and PI-bit handling are not exercised. Colour code is taken from whichever EMB decodes last in the superframe. - Limited data/control coverage. Data type 6 is surfaced as
data_headerwith no payload parse; only CSBK (type 3) and the LC header/terminator (types 1/2) are decoded to fields. Confirmed/unconfirmed data, Rate 1/2 and 3/4 data-burst FEC, and Tier III trunking messages are out of scope. - No encryption handling. Encrypted voice/data is decoded at the FEC layer but not deciphered; there is no key management.
- Block-at-a-time timing. The shared C4FM demod does a one-shot max-eye-opening symbol-timing search with no tracking loop (see P25 §4); fine for clean captures, weaker on drifting or very weak signals.
None of these change the architecture — they are the natural next increments on a working native DMR receive chain.
9. References
- ETSI TS 102 361-1 — Digital Mobile Radio (DMR) Systems; Part 1: DMR Air Interface (AI) protocol — the 264-bit burst, the four 48-bit sync patterns, BPTC(196,96), the Golay(20,8) slot type, the full-LC/RS masks, CSBK/CRC, EMB and the embedded-LC block. (Parts 2–4 cover the voice/data services, trunking and conformance.)
- ETSI TS 102 361-2 / -3 — DMR voice and generic services / DMR data protocol — context for the AMBE+2 voice superframe and CSBK/data-header traffic.
- In-repo implementation:
app/radio/dmr.py(this spec's subject) and its teststest_dmr.py. - Shared modem and block codes:
app/radio/p25/demod.py(the C4FM 4-level modem, reused unchanged) andapp/radio/p25/fec.py(Golay(24,12)/(23,12), Hamming(15,11)/(13,9)). See P25 Phase 1 for the full modem and Golay/Hamming description. - Voice vocoder front end:
app/radio/ambe.py— the AMBE+2 half-rate channel codec (hr_to_channel/channel_to_hr), documented in AMBE; the underlying model layer is the MBE vocoder. - External reference decoders (DSD /
dsd-fme) — the off-air interoperability yardstick for the interop-flagged items above.