Home › Signal processing & communications › Channel coding
algorithm low

Convolutional codes and the Viterbi decoder

Problem Radio, satellite and wireless links need cheap, low-latency error correction on streaming or short frames, so bits flipped by noise can be recovered at the receiver without retransmission.

Streaming error-correcting code: each output bit is a parity of the last K input bits; the Viterbi decoder finds the most likely path through a 2^(K-1)-state trellis.

encode O(1) per bit; decode O(2^(K-1)) add-compare-select per bit (64 for K=7); traceback memory O(2^(K-1) x depth) bits
channel-codingtrellismaximum-likelihoodsoft-decisionpuncturing
Overview

A convolutional encoder is a shift register: each input bit emits n parity bits, XORs of the last K inputs. The register is a state, so the code is a path through a 2^(K-1)-state trellis. Viterbi decoding keeps the best survivor per state and extends each by the cheaper of two incoming branches: exact maximum-likelihood decoding in linear time.

Key facts

Use when

  • Cheap low-latency FEC on streaming or short frames: Wi-Fi, DVB, satellite, LTE control; inner code before Reed-Solomon.

Avoid when

  • Near-capacity large blocks (LDPC, turbo) or K above about 10 on a CPU.

Deep dive

How it works

In: received soft or hard symbol pairs (rate 1/2), code polynomials · Out: the maximum-likelihood input bit sequence · State: one path metric and one decision bit per trellis state, per step (64 states for K=7) · Cost: 64 add-compare-selects per decoded bit for K=7; traceback memory 2^(K-1) x depth bits

Encoder (K=7, rate 1/2): state = last 6 input bits; for input u form the 7-bit register reg = u<<6 | state; output parity(reg & 0o133), parity(reg & 0o171); new state = reg >> 1. Append K-1 = 6 zero tail bits so the path ends in state 0 (cost: 6 extra input steps; tail-biting, below, avoids it).

Decoder:

  1. Per received symbol pair, a branch metric for each of the 4 possible code pairs: correlation sum(±llr) for soft input, Hamming distance for hard input.
  2. For each new state ns there are exactly two predecessors, (ns<<1 | b) & 63, b in {0,1}; the input bit is the top bit of ns. Add each predecessor's path metric to its branch metric, keep the better one (add-compare-select, ACS), and record the decision bit.
  3. After the last symbol, start in state 0 (terminated) or the best state (unterminated) and trace back: state = (state<<1 | decision[t][state]) & 63, emitting the top bit of each state.
 predecessors      new state        ns = (u, s5 s4 s3 s2 s1)   u = input bit
 (s5..s1, 0)  ──┐                   p0 = (s5..s1, 0)   p1 = (s5..s1, 1)
 (s5..s1, 1)  ──┴──► ns            p0 and p1 differ only in the dropped bit,
                                    so 32 butterflies, each with 2 sources, 2 targets

Code

const K: usize = 7;                 // constraint length
const NS: usize = 1 << (K - 1);     // 64 states
const G: [u32; 2] = [0o133, 0o171]; // generator polynomials (802.11 order)

fn parity(x: u32) -> u32 { x.count_ones() & 1 }

/// Rate 1/2 encoder. Appends K-1 zero bits so the trellis ends in state 0.
pub fn encode(bits: &[u8]) -> Vec<u8> {
    let mut state = 0u32; // last K-1 input bits, newest in bit 5
    let mut out = Vec::with_capacity(2 * (bits.len() + K - 1));
    for &u in bits.iter().chain([0u8; K - 1].iter()) {
        let reg = ((u as u32) << (K - 1)) | state; // newest bit at bit K-1
        out.push(parity(reg & G[0]) as u8);
        out.push(parity(reg & G[1]) as u8);
        state = reg >> 1;
    }
    out
}

/// Soft-decision Viterbi. `soft[i] > 0` means "coded bit i is probably 0"
/// (LLR-like, any scale); +-1 gives hard decisions. Returns the info bits.
pub fn viterbi(soft: &[i16]) -> Vec<u8> {
    let steps = soft.len() / 2;
    // Expected output pair for each (state, input): precomputed, 2 bits each.
    let mut expect = [[0u8; 2]; NS * 2];
    for s in 0..NS as u32 {
        for u in 0..2u32 {
            let reg = (u << (K - 1)) | s;
            expect[(s * 2 + u) as usize] = [parity(reg & G[0]) as u8, parity(reg & G[1]) as u8];
        }
    }
    let mut pm = [i32::MIN / 2; NS]; // path metric: larger = better (correlation)
    pm[0] = 0;                       // encoder starts in state 0
    let mut decisions = vec![0u64; steps]; // 1 bit per state: which predecessor won
    for t in 0..steps {
        let (l0, l1) = (soft[2 * t] as i32, soft[2 * t + 1] as i32);
        let mut next = [i32::MIN; NS];
        let mut dec = 0u64;
        for ns in 0..NS {
            let u = (ns >> (K - 2)) as usize;      // input bit = MSB of the new state
            let (mut best, mut pick) = (i32::MIN, 0);
            for b in 0..2 {                          // two predecessors differ in the dropped LSB
                let s = ((ns << 1) & (NS - 1)) | b;
                let e = expect[s * 2 + u];
                let bm = (if e[0] == 0 { l0 } else { -l0 }) + (if e[1] == 0 { l1 } else { -l1 });
                let m = pm[s].saturating_add(bm);
                if m > best { best = m; pick = b; }  // compare-select
            }
            dec |= (pick as u64) << ns;
            next[ns] = best;
        }
        pm = next;
        decisions[t] = dec;
    }
    // Terminated code: trace back from state 0.
    let mut state = 0usize;
    let mut bits = vec![0u8; steps];
    for t in (0..steps).rev() {
        bits[t] = (state >> (K - 2)) as u8;
        let b = ((decisions[t] >> state) & 1) as usize;
        state = ((state << 1) & (NS - 1)) | b;
    }
    bits.truncate(steps - (K - 1)); // drop the tail
    bits
}

Verified by round trip (clean and with four isolated bit flips) and a Monte-Carlo BER run (see below).

Complexity and constants

  • Encode: 2 AND+popcount per input bit; effectively free.
  • Decode per info bit: 64 states x 2 branches = 128 adds, 64 compares/selects, 8 bytes of decision storage (one bit per state). Cost scales as 2^(K-1): K=9 is 256 states, K=15 is 16,384.
  • Traceback memory: steps x 2^(K-1) bits; a 1000-bit frame at K=7 is about 8 KB.
  • Measured here (this page's scalar code, one Apple-silicon core, 100k-bit frames): order of 10-15 Mbit/s of decoded bits. Production SIMD decoders are far faster.
  • Measured BER, 1000-bit frames, AWGN, BPSK, 300 frames (300k bits) per point, tail included:
Eb/N0hard decision3-bit soft8-bit soft
2 dB1.1e-16.8e-35.1e-3
3 dB3.1e-28.0e-44.7e-4
4 dB5.7e-33e-50 errors seen

Efficiency

  • Bound: compute on 64 independent ACS operations per step; the working set (64 path metrics) fits in registers or L1, so it is not memory bound. Decision storage is the only growing state.
  • Use soft input: at 3 dB the measured error rate drops from 3.1e-2 to 8e-4 for the same code and decoder size. Cost: a few bits per symbol instead of one. CCSDS recommends at least 3-bit quantization when decoder complexity permits.
  • Branch-metric sharing: only 2 distinct metric magnitudes per step (l0+l1, l0-l1, and their negatives); compute them once, not per state.
  • 8/16-bit saturating path metrics and SIMD: the 32 butterflies are independent, so one vector instruction handles 16-32 of them (libfec, srsRAN use SSE/AVX2/NEON).
  • Truncated traceback: decide bits with a fixed decision depth (rule of thumb: several times K) instead of storing the whole frame, and trace back every N steps; memory and latency become constant.
  • Puncturing: rate 3/4 or 7/8 costs no extra decoder, only skipped transmissions; fewer coded bits means fewer symbols on air (energy) at lower coding gain.
  • Cheaper alternative: a short code (K=3..5) or hard decisions when the link margin is large; a Hamming or CRC-only scheme when errors are rare.

Implementation notes

  • Renormalize path metrics (subtract the minimum every few steps, or use wrap-around arithmetic with modular comparison) so they never overflow; the code above saturates for clarity.
  • Use a contiguous [u64] of decision words (64 states = one word) so traceback touches one word per step; this is a single bit-test per step.
  • Soft input convention matters: keep one sign convention from demapper to decoder (here positive = bit 0) and scale LLRs consistently; clip to about ±127 for 8-bit.
  • CCSDS 131.0-B inverts the G2 output and emits G1 first; 802.11 emits 133 then 171. Both ends must agree on order, inversion and bit ordering of the polynomials (write them as taps, newest bit first or last), or the decoder will "work" at 50 % BER.
  • Puncture on the encoder by deleting coded bits per a periodic pattern; on the decoder insert zero-confidence (llr = 0) symbols in the deleted slots and run the same trellis. CCSDS tables: 2/3, 3/4, 5/6, 7/8.
  • Tail-biting (LTE control channels): start in the state equal to the last K-1 input bits so no tail is sent; the decoder runs the circular trellis (wrap-around or try all start states).

Pitfalls

  • Hard-decision decoding when a soft demapper is available throws away roughly the gain that makes the code worthwhile.
  • Forgetting tail bits (or the matching terminated traceback) leaves the last few bits unprotected.
  • Starting traceback from state 0 on an unterminated stream: pick the best path metric instead.
  • Mixing up which polynomial is first, or the bit order of the octal polynomial; both ends must use the same one.
  • Traceback depth too short: errors appear at the window end; punctured high rates need deeper windows (tune by simulation).
  • Burst errors: a convolutional code corrects scattered errors, but a Viterbi decoder that goes wrong emits a burst; interleave and add Reed-Solomon outside it.

Variants and alternatives

Recursive systematic convolutional (RSC) codes + BCJRconstituent codes of turbo codes; decoder outputs soft values, not hard bits.
Sequential decoding (Fano, stack)very long constraint lengths with variable decoding effort; Viterbi wins at K up to about 10-15.
Soft-output Viterbi (SOVA)Viterbi plus reliability values for iterative schemes.
LDPC / polarbetter near capacity at large blocks; more complex.
Large K (K=15, rate 1/6)deep-space missions with big parallel decoders.

In production

IEEE 802.11a/g/n/ac (K=7, 133/171, punctured to 2/3, 3/4, 5/6), DVB-S and DVB-T (K=7 with puncturing, outer Reed-Solomon), CCSDS telemetry (K=7, optional outer Reed-Solomon), GSM (K=5 speech/control channels), LTE and 5G NR control channels (LTE: K=7 tail-biting rate 1/3; NR moved to polar codes), NASA deep-space links (K=7 on Voyager-era and later missions). The same Viterbi search decodes hidden Markov models in speech recognition (Rabiner).

Implementations

namelangrepoinstallverdictlicencehealthas_ofnotes
srsRAN 4GCsrsran/srsRAN_4Ggit clone + cmake build————LTE K=7 rate 1/3 Viterbi with SSE, AVX2 and NEON kernels (lib/src/phy/fec/convolutional/)
GNU Radio gr-fecC++gnuradio/gnuradiobrew install gnuradio————cc_decoder_impl.cc, generic polynomials, puncturing
AFF3CTC++aff3ct/aff3ctgit clone + cmake build————FEC simulation toolbox incl. RSC/BCJR decoders

Sources

  • Andrew J. Viterbi, Error bounds for convolutional codes and an asymptotically optimum decoding algorithm, IEEE Trans. Information Theory 13(2), 1967. paper
  • G. David Forney Jr., The Viterbi algorithm, Proc. IEEE 61(3), 1973. paper
  • J. A. Heller, I. M. Jacobs, Viterbi decoding for satellite and space communication, IEEE Trans. Communication Technology 19(5), 1971. paper
  • Y. Yasuda, K. Kashiki, Y. Hirata, High-rate punctured convolutional codes for soft decision Viterbi decoding, IEEE Trans. Communications 32(3), 1984. paper
  • Phil Karn, libfec (viterbi27, viterbi29; reference code). code
  • Lawrence R. Rabiner, A tutorial on hidden Markov models and selected applications in speech recognition, Proc. IEEE 77(2), 1989. paper
  • CCSDS, TM Synchronization and Channel Coding, CCSDS 131.0-B-5, Sept 2023 (K=7 polynomials, G2 inversion, puncturing patterns, 3-bit soft quantization). spec

Used by

forced alignment · plannedAlign transcripts to audio for word timestamps (CTC/Viterbi alignment)
map matching hmm · plannedHMM/Viterbi map matching of GPS traces onto the road graph
Named entity recognitionA pipeline receives free text (articles, notes, contracts, logs, listings) and needs the things it mentions, such as people, organisations, places, amounts, codes or drugs, as typed pieces with exact positions, to index, redact or store them.
profile hmm search · plannedProfile HMMs (HMMER, HHblits) for sensitive homology search
turbo codes · plannedParallel concatenated convolutional codes with iterative BCJR/max-log-MAP decoding

Systems (6):

Alternatives and related

ldpc codes · plannedLow-density parity-check codes with soft-decision min-sum decoding, used in NAND flash
forward error correction · plannedAdd redundancy (LDPC, Reed-Solomon, XOR parity) so receivers repair loss without retransmit
qam soft demapping · plannedPSK/QAM constellation mapping and max-log LLR soft demapping for decoders
hybrid arq · plannedHARQ with chase/incremental-redundancy soft combining and parallel stop-and-wait processes
Reed-Solomon codesStorage and transmission systems (discs, QR codes, satellite links, RAID) must recover data when bursts of bytes are corrupted or whole blocks are lost, using a small amount of added redundancy.
line coding scrambling · plannedScramblers and line codes (8b/10b, 64b/66b) for DC balance and clock recovery