Part 5 of DMR End to End, a 14-part deep dive that follows the world’s most widely deployed digital PMR protocol through GopherTrunk — from a 4FSK carrier to two simultaneous recorded calls, direct-mode handhelds, and decrypted Enhanced Privacy voice. Part 4 built the FEC stack. This part is about what the protected bits *say: the Full Link Control word that names a call, the three places DMR puts it, and late entry — the mechanism every subscriber radio uses and the scanner once ignored.*
TL;DR: A DMR call is named by a 72-bit Full Link Control PDU (
internal/radio/dmr/flc.go: PF, 6-bit FLCO, FID, service options, 24-bit destination, 24-bit source). It is sent whole in the Voice LC Header (slot type0x1, RS(12,9) seed0x96) and the Terminator with LC (0x2, seed0x99), and again as four 32-bit fragments embedded in voice bursts B–E, framed by a 16-bit EMB (emb.go) and reassembled under BPTC(128,72) + a 5-bit checksum. The conventional path used to grant only on headers — one chance per PTT.ingestVoiceSuperframe(tier2/conventional.go) now grants a header-less transmission from two agreeing CRC-valid embedded LCs (lateEntryConfirm = 2, ~720 ms in), gated byendedAtDibitso a dead call’s closing superframes cannot resurrect it.GT_DMR_DROP_HEADERS=1scrubs every header from a real capture and 5/5 transmissions still grant. The same embedded LC carries the talker alias (talker_alias.go).
Key takeaways
- DMR names a call three times so a receiver can join it late. Header, terminator and the embedded copy in every superframe carry the same FLC.
- Late entry is confirm-twice, never confirm-once. One CRC-valid embedded LC can be a miscorrection; two agreeing copies within 3 s grant and declare the lock.
- Stream order is not arrival order. The superframe assembler runs a span
behind the burst slicer, so a call’s closing LCs surface after its
terminator —
endedAtDibitstops them re-granting it. - The header is a train, not a burst. The re-key anchor follows the
last copy, or every keyup opens with a phantom release
(
TestConventionalHeaderTrainIsOneKeyup).
Cheat sheet
| Concern | What it does | Where it lives |
|---|---|---|
| FLC parse | 9 octets → PF / FLCO / FID / options / dst / src | internal/radio/dmr/flc.go (ParseFLC, AsGroupVoiceUser) |
| Header / terminator | BPTC(196,96) + RS(12,9), seed 0x96 / 0x99 |
tier2/conventional.go (handleVoiceHeader, terminatorDest) |
| Embedded LC | EMB split, 4 × 32 → BPTC(128,72) + checksum → FLC | internal/radio/dmr/emb.go (SplitEmbeddedField, ReassembleEmbeddedLCInfo) |
| Late entry | two agreeing LCs grant a header-less over | conventional.go (ingestVoiceSuperframe, lateEntryConfirm) |
| Dead-call gate | drop LCs whose superframe predates the terminator | endedAtDibit, set in releaseCall |
| Talker alias | header 0x04 + blocks 0x05–0x07 → display name | internal/radio/dmr/talker_alias.go (TalkerAliasAssembler) |
In this post
- The Full Link Control word — nine octets, two call shapes.
- Three carriages — header, terminator, embedded copy.
- Embedded signalling — the EMB frame and what is not yet corrected.
- Late entry — granting a transmission whose header was never heard.
- The header train — why the anchor follows the last copy.
- Talker alias — the other passenger.
The Full Link Control word
Everything downstream of the burst — the grant, the recording’s folder, the Radio IDs roster — hangs off nine octets. Protocol Decoders Part 5 introduced the struct; this is the reading GopherTrunk performs:
// internal/radio/dmr/flc.go (shape)
// octet 0 PF|Res|FLCO(6) · 1 FID · 2 ServiceOptions
// octets 3-5 destination (24-bit) · 6-8 source (24-bit)
const (
FLCOGroupVoiceUser FLCO = 0x00
FLCOUnitToUnitVoice FLCO = 0x03
FLCOTalkerAlias FLCO = 0x04 // header; blocks 0x05, 0x06, 0x07
FLCOGPS FLCO = 0x08
)
Two opcodes define a voice call. FLCOGroupVoiceUser makes the destination a
talkgroup; FLCOUnitToUnitVoice makes it a called subscriber, and
AsUnitToUnitVoice marks the grant Individual so a private call’s
destination RID never lands in the talkgroup list. The service-options octet
is shared — bit 7 emergency, bit 6 privacy, bits 2..0 priority — and is
the only place DMR signals encryption; a Tier III grant CSBK has no options
octet (Part 8).
The reference page
has the ETSI view; what matters here is that one struct feeds every
reader — header, terminator, embedded LC and the composer’s slot router all
call ParseFLC, so the group-versus-individual decision cannot drift.
Three carriages
The Voice LC Header (DTVoiceLCHeader, 0x1) opens a transmission: 9
octets of FLC plus a 24-bit RS(12,9) trailer XORed with the seed
0x96 0x96 0x96. handleVoiceHeader requires BPTC and RS — BPTC cannot
catch a systematic miss, and the RS check is what earns the lock (FECPass,
dmr/tier2 cc locked). A failing header is dumped at DEBUG with its exact
132 dibits (burst_dibits) — an instrument in the sense of
From Spec to Shipping Part 13.
The Terminator with LC (DTTerminatorWithLC, 0x2) closes it under the
seed 0x99 0x99 0x99 (RS(12,9) reference);
terminatorDest returns the destination it names, so a TS1 terminator
releases only the TS1 call.
The embedded copy is the interesting one. Voice bursts carry no slot type and no FLC; their sync field holds the voice sync on burst A and embedded signalling on B–F. Bursts B–E each carry 32 bits of a 128-bit block that reassembles into the same FLC, repeated every 360 ms superframe.
Embedded signalling: the EMB frame
// internal/radio/dmr/emb.go (shape)
// bits 0..7 : EMB MSB half
// bits 8..39 : embedded signalling fragment (32 bits)
// bits 40..47 : EMB LSB half
const (
LCSSSingle LCSS = 0 // single-fragment LC (CSBK/RC) or null
LCSSFirst LCSS = 1 // burst B
LCSSLast LCSS = 2 // burst E
LCSSCont LCSS = 3 // bursts C, D
)
The 16-bit EMB — colour code in bits 15..12, privacy indicator in
bit 11, the 2-bit LCSS in bits 10..9 — frames the fragment and says where
it sits. ReassembleEmbeddedLCInfo concatenates the four ordered fragments
and hands 128 on-air bits to framing.DecodeEmbeddedLC, the variable
BPTC(128,72) the
embedded-LC reference
describes. A negative correction count rejects the whole word — a corrupted
fragment yields no LC, never a garbled one.
Two honesty notes. The EMB’s own QR(16,7) protection is not applied — its
bits are read systematically and the embedded LC’s BPTC + checksum is the
integrity gate (EMB reference).
And because one bit error can flip an EMB colour-code nibble, the superframe
decoder exposes EMBColorCode as the majority over the four LC-bearing
bursts (VoiceSuperframe.HasEMB) — all an IPSC color_code filter has on
a header-less transmission.
Late entry
The field report that forced this layer: an operator with a handheld beside the scanner watched GopherTrunk say a call had ended while the conversation continued on the radio. The tap was a weak −60 dBFS bin edge; the next headers were lost to a fade at keyup, and the conventional path had no other way to grant. Every subscriber radio does late entry from the embedded LC. Now so does the scanner:
// internal/radio/dmr/tier2/conventional.go (shape)
const lateEntryConfirm = 2 // agreeing superframes to grant
const lateEntryWindow = 3 * time.Second
func (c *ConventionalChannel) ingestVoiceSuperframe(sf dmrvoice.VoiceSuperframe) {
if end, ok := c.endedAtDibit[dest]; ok && sf.StartDibit < end {
return // closing superframe of a call its terminator already released
}
cand := c.lateEntry[dest]
if cand == nil || cand.src != src || now.Sub(cand.firstAt) > lateEntryWindow {
c.lateEntry[dest] = &lateEntryCandidate{src: src, firstAt: now, seen: 1}
return
}
if cand.seen++; cand.seen < lateEntryConfirm { return }
c.cnt.lateEntries.Add(1)
c.maybeLock(LockState{FrequencyHz: c.freqHz, ColorCode: sf.EMBColorCode})
c.publishGrant(dest, src, individual, enc, emer, prio, ts, sf.EMBColorCode, true)
}
Process (tier2/process.go) runs a dmrvoice superframe assembler beside
the burst slicer and hands every CRC-valid embedded LC here. Three rules make
it safe.
Confirm twice. A lone CRC-valid LC is not enough
(TestConventionalLateEntryNeedsTwoAgreeingLCs): the copy repeats every
superframe, so two agreeing (destination, source) cost ~720 ms and rule out
one miscorrected word forging a phantom call. Two copies are as strong as a
BPTC+RS-clean header — they grant, count in late_entries, and declare the
lock.
Respect the terminator. The assembler cannot emit a superframe until
burst F arrives, so it runs behind the slicer and a transmission’s closing
superframes surface after its terminator. Ungated, they re-granted every dead
call — measured on air, 7 phantom grant/release pairs in a 120 s capture.
releaseCall records the terminator’s dibit index in endedAtDibit[dest];
any LC whose superframe started before it is dropped.
Let the header win. A header grant deletes any pending candidate; later
LCs only refresh the call (TestConventionalHeaderAndEmbeddedLCGrantOnce).
The real-air check is TestDMRIPSCReplay with GT_DMR_DROP_HEADERS=1, which
scrubs every Voice LC Header out of the operator’s 9 Sep 442.3875 MHz
captures: all 5/5 transmissions still grant (late_entries=5), and
un-scrubbed runs grant exactly once per over.
TestConventionalLateEntryGrantsHeaderlessTransmission is the synthetic
failing-first regression — before this layer it produced no grant at all, the
rule
From Spec to Shipping Part 12
insists on.
The header train
The embedded LC cannot say where a transmission started. That stays with
the header — and the header is not one burst. A repeater sends two or three
copies at keyup, 60 ms (288 dibits) apart; the direct-mode handheld of
#836 repeats its
Voice LC Header ten times over 0.6 s. Copies are deduped by the
headerRekeyDibits rule — 1200 dibits, 0.25 s — from an anchor. Measured
from the first copy, the handheld’s sixth was already a “new transmission”,
and every PTT opened with a phantom release and re-grant. The fix: the anchor
moves up to this copy.
// internal/radio/dmr/tier2/conventional.go (shape)
if existing.src == src {
if c.ingestDibit-existing.anchorDibit <= headerRekeyDibits {
if c.ingestDibit > existing.anchorDibit {
existing.anchorDibit = c.ingestDibit // anchor follows the LAST copy
}
existing.touch(c.ingestDibit)
return
}
/* re-key: release + re-grant — Part 6 */
}
TestConventionalHeaderTrainIsOneKeyup lays a ten-copy train on the
288-dibit direct-mode grid, two seconds of silence with the terminator lost,
then a three-copy train, and asserts exactly two grants and Rekeys == 1.
The train is one keyup; the genuine re-key still re-grants — the subject of
Part 6,
where the same anchor arithmetic recovers replies an IPSC tap was dropping.
Talker alias: the other passenger
The embedded LC is a carriage for any 72-bit FLC, and the second most
useful thing it carries is a name. FLCO 0x04 is a talker alias header
declaring a 2-bit format and a 5-bit character count plus six octets of text;
0x05–0x07 are blocks of seven octets. TalkerAliasAssembler buffers
fragments per source radio — they carry no address, so the voice chain keys
them on the call’s current source — and emits the name once the text covers
the declared length:
// internal/radio/dmr/talker_alias.go (shape)
// Header (FLCO 0x04): bits 16..17 format · 18..22 length · 24..71 text (6 octets)
// Blocks (0x05–0x07): bits 16..71 text (7 octets)
// formats: 0 = 7-bit packed, 1 = ISO 8859-1, 2 = UTF-8, 3 = UTF-16BE
const dmrAliasStaleAfter = 10 * time.Second
The composer’s DMR chain runs the assembler on every superframe of both
slots before the slot-router gate — alias and GPS LCs are call metadata, not
one slot’s audio — and publishes KindTalkerAlias with the INFO line
composer: dmr talker alias. The layout is cross-checked against ok-dmrlib,
and the code flags its working model: header bit 23 is treated as reserved,
and “a capture that decodes to garbage should re-check bit 23’s
inclusion first.” That is the posture the P25 alias took in
Protocol Decoders Part 10
and Part 11
— except DMR’s alias is plaintext where Motorola’s is an unsolved cipher. Both
feed the one trunking.TalkerAlias shape
Trunking Engine Part 7
plumbs into the roster.
How link control shaped the Go code
- One parser, many readers.
ParseFLCplus the twoAs*accessors are the only group/individual decision in the DMR tree. - Evidence is stream-positioned, not wall-clocked.
anchorDibit,lastDibitandendedAtDibitare dibit indices, so the rules hold in an offline replay running faster than real time. - Raw octets survive the parse.
ReassembleEmbeddedLCInforeturns the 9-octet block beside the FLC for the alias and GPS readers. - Counters name the mechanism.
late_entriesclimbing on a live repeater means a weak tap is catching conversations mid-transmission.
Where this goes next
Late entry and the header anchor are half the conventional state machine. Part 6 supplies the other half: how one IPSC repeater’s two timeslots become two concurrent calls, and the 10 Sep field report in which replies inside hangtime were deduped against calls the engine had already ended.
FAQ
What is DMR Full Link Control? A 72-bit PDU that identifies a voice call: a 6-bit FLCO opcode, feature-set ID, service options (emergency, privacy, priority) and 24-bit destination and source addresses. GopherTrunk parses it from the Voice LC Header, the Terminator with LC, and the embedded signalling of bursts B–E.
How does DMR late entry work in a scanner? Every voice superframe embeds the call’s Link Control across bursts B–E, so a receiver that missed the header still learns the talkgroup and source. GopherTrunk grants a header-less transmission once two agreeing CRC-valid embedded LCs arrive, about 720 ms in.
Why does GopherTrunk need two embedded LCs before granting?
Because one CRC-valid word can be a miscorrection. The embedded LC repeats
every 360 ms, so a second agreeing copy costs little and rules out a phantom
call; two copies grant, lock the channel and count in late_entries.
What is the DMR EMB field? The 16-bit embedded-signalling header in the sync region of voice bursts B–F: colour code, privacy indicator and the 2-bit LCSS marking a fragment first, continuation or last. GopherTrunk reads it systematically and relies on the embedded LC’s BPTC(128,72) plus checksum as the integrity gate.
Does GopherTrunk decode the DMR talker alias?
Yes. The alias header (FLCO 0x04) declares format and length, blocks 0x05–0x07
carry the rest, and TalkerAliasAssembler reassembles the radio’s display
name per source in 7-bit, ISO 8859-1, UTF-8 or UTF-16 form and publishes it
on the event bus.
Series navigation
Part 5 of 14 · ← Part 4: The FEC Stack & the Forged Terminator · Next → Part 6: Conventional IPSC — Two Slots as Two Calls