卷积编码与维特比译码的C++落地实现:从状态机到BER测试床 简介一套基于C的卷积编码实现项目面向数字通信与信道编码学习者兼顾原理演示与工程仿真。代码支持在Visual Studio下直接编译通过调整高斯白噪声、信道衰减等参数可对比不同信道条件下的编解码性能。资源共52个文件压缩包约77KB以23个cpp源码与21个h头文件为主辅以工程配置、输入文本和绘图脚本涵盖编码器、Viterbi译码器、信道模型、误码率统计等模块。Library目录集中存放随机数生成、AWGN等公共库3 Conventional Coding目录则给出卷积码主程序及测试用例结构清晰便于按模块阅读和扩展。已有587人下载学习。使用者可基于生成器多项式和Trellis结构深入理解卷积编码工作机制并获得一套可直接运行的仿真与调参环境适合具备C基础、希望掌握高级编码算法的通信方向学生或工程师。1. 从黑匣子到可运行的 C 类卷积编码与维特比译码的落地笔记做通信链路仿真时很多人把卷积编码的维特比译码当成黑匣子输入软比特输出硬比特中间过程一概不管。这份“卷积编码的 C模拟”源码把高级编码中最核心的编码器状态机、维特比加比选、回溯过程都摊开了。我从这个资源里扒出了完整的 C 实现框架解决了之前用 MATLAB 跑算法转 C 时遇到的定点化、内存爆炸和同步丢失问题。如果你是新手能照着封装出第一个可用的卷积码模块如果你是老手可以直接拿走文末的 BER 测试床代码快速验证信道编码参数。2. 编码器封装生成多项式、状态转移表和移位寄存器的 C 实现2.1 理解约束长度与生成多项式的关系卷积编码器不是把比特简单异或它是有记忆的。这个记忆由约束长度 K 决定通常取 K7对应 64 个状态。生成多项式决定了抽头位置。资源里最常用的是标准 (2,1,7) 码生成多项式为 171 和 133八进制。转换成二进制就是 1111001 和 1011011每一位代表移位寄存器对应抽头是否参与异或。我一开始没搞懂为什么 171 是 1111001 而不是 1010101踩了坑才发现八进制转二进制要逐位转换不是整体转换。在 C 里定义生成多项式时我喜欢用宏或者枚举#define K 7 // 约束长度 #define RATE 2 // 1/2 码率 #define NUM_STATES (1 (K - 1)) // 64 个状态 #define POLY_G1 0x6D // 八进制 171 - 二进制 1111001 #define POLY_G2 0x5B // 八进制 133 - 二进制 1011011这里 0x6D 对应 0b1101101注意 LSB 对齐问题。标准文档里生成多项式高位在左但移位寄存器实际操作是低位在左所以需要把多项式位序反过来。很多初学 C 模拟的人在这里直接拿八进制转成十进制然后用会发现编码结果永远对不上。我对齐的方式是在初始化时把多项式反转uint8_t reverse_bits(uint8_t poly) { uint8_t reversed 0; for (int i 0; i K; i) { reversed (reversed 1) | ((poly i) 1); } return reversed; }这段代码的逻辑是把多项式按位倒序让最高有效位对应最先进入移位寄存器的比特。参数说明poly是原始八进制转换后的 7 位二进制数K是约束长度循环次数必须等于 K。为什么这么做因为数学上生成多项式是左高右低但硬件位移寄存器是从右往左移如果不反转第一个输入比特和多项式最高位根本碰不到一起。2.2 用查表法代替实时异或运算实时计算输出比特需要不断做位运算这在仿真里没问题但如果要做实时 DSP性能就吃紧。资源里给的思路是预计算状态转移表。因为编码器本质是有限状态机当前状态和输入比特决定了下一个状态和输出。我一般会在类初始化时把 64 个状态、2 种输入的全部转移情况存成数组struct Transition { uint8_t next_state; uint8_t output_bits; // 2位输出低两位有效 }; Transition trans_table[NUM_STATES][2]; void build_transition_table() { uint8_t poly1_rev reverse_bits(POLY_G1); uint8_t poly2_rev reverse_bits(POLY_G2); for (int state 0; state NUM_STATES; state) { for (int input 0; input 1; input) { uint8_t new_state ((state 1) | input) 0x7F; uint8_t masked1 new_state poly1_rev; uint8_t masked2 new_state poly2_rev; // 奇偶校验生成输出比特 uint8_t out1 parity(masked1); uint8_t out2 parity(masked2); trans_table[state][input] {new_state, (out1 1) | out2}; } } }参数说明state是当前移位寄存器的高 6 位input是新进入的比特。new_state是把寄存器左移一位再塞入输入。parity函数可以用__builtin_parity实现或者自己循环异或。输出output_bits的低两位分别对应两个生成多项式的计算结果。查表法的好处是编码过程变成了一次数组索引时间复杂度 O(1)。而且后续维特比译码的蝶形运算也要用到这个转移表一份表两种用途内存只占 642sizeof(Transition)约 256 字节对嵌入式平台也非常友好。实际写 C 编码器类时我会把编码缓冲区分成块处理每块 512 个比特。处理完一块就把状态保存下来以便下一块接着编码。这样在流式传输场景里接收端能随时从任意位置开始译码不需要从头同步。这个设计思路在这份源码里体现得很明显。3. 维特比译码器实现分支度量、加比选与幸存路径管理3.1 硬判决与软判决的度量计算实现维特比算法的核心是找最大似然路径。对应到 C 实现第一步是计算分支度量。硬判决接收的是 0/1 比特直接算汉明距离软判决接收的是多比特量化值比如 3 bit 量化后的 0~7要算欧氏距离或闵可夫斯基距离。资源里把硬判决和软判决统一成了一个接口我觉得很实用。用一个基类定义度量计算接口两个子类分别实现硬判决和软判决的度量方法class MetricUnit { public: virtual int compute(int expected, int received) 0; virtual ~MetricUnit() default; }; class HardMetric : public MetricUnit { public: int compute(int expected, int received) override { return (expected ^ received) ? 1 : 0; // 汉明距离 } }; class SoftMetric : public MetricUnit { public: int compute(int expected, int received) override { return (expected - received) * (expected - received); // 平方欧氏距离 } };这段代码的逻辑很直白expected是编码器已知的理想输出received是信道接收到的量化值。硬判决只有 0 和 1 两种异或之后就能判断是否一致。软判决里 received 取值范围是 0~7expected 映射到 0 或 7距离用平方差计算能反映置信度。有人问为什么不直接用似然比非要平方因为 C 做定点化时平方差只涉及乘法不涉及除法对 DSP 友好。而且 3 bit 量化的温德比增益跟浮点似然比只差 0.2 dB 左右代价远小于收益。实际工程里我倾向于用 4 bit 量化软判决也就是 0~15 范围这样平方和最大 22532 位整数完全不会溢出。3.2 加比选蝶形运算的 C 内存布局加比选ACS是维特比最耗时的部分。每个状态对应两条入路径需要比较两条路径的累计度量选小的那条作为幸存路径。标准蝶形运算由状态对组成状态 i 和 状态 i32 在 K7 时共享相同的前驱状态。这个结构天然适合并行计算。我实现时把路径度量放在一个长度为 64 的数组中幸存路径放在另一个比特数组中int path_metric[NUM_STATES]; int new_metric[NUM_STATES]; unsigned char survivor_path[NUM_STATES]; int traceback_depth 64; void acs_step(const uint8_t received_pair[2], MetricUnit* metric) { // 清空新度量数组 memset(new_metric, 0xFF, sizeof(new_metric)); // 初始化为 INT_MAX for (int state 0; state NUM_STATES; state) { int base_metric path_metric[state]; // 输入0和输入1对应两条转移路径 for (int input 0; input 1; input) { Transition trans trans_table[state][input]; int branch_metric metric-compute((trans.output_bits 1) 1, received_pair[0]) metric-compute(trans.output_bits 1, received_pair[1]); int total_metric base_metric branch_metric; int next_state trans.next_state; // 比较并选择较小的度量 if (total_metric new_metric[next_state]) { new_metric[next_state] total_metric; survivor_path[next_state] input; } } } // 交换新旧度量数组 std::swap(path_metric, new_metric); }这段代码是维特比译码的心脏。received_pair是两个接收到的比特或软值metric是多态对象。内层循环对每个状态和每个输入都做一次转移查表然后累计度量。关键点在survivor_path[next_state] input它记录的是到达该状态的幸存路径上最后一位输入比特。这个信息在回溯阶段要用来还原原始比特流。内存布局上使用两个固定长度数组交替使用避免动态分配。new_metric初始化为全 1即 INT_MAX方便比较时直接覆盖。整个 ACS 步计算量为 64 状态 × 2 输入 128 次分支度量计算在 1 GHz 的处理器上跑 3G bps 的数据率不是问题。3.3 回溯深度、路径存储与窗口管理回溯深度一般取约束长度的 5~10 倍也就是 35~70 比特。太短会导致误码率平台太长会增加译码延迟。资源里默认取 64我实际测试过对于 (2,1,7) 码64 深度比 32 深度有大约 0.3 dB 的增益再往上到 128 基本没有额外收益。幸存路径存储是整个维特比最占内存的地方。如果每步把所有状态的幸存比特都存下来64 状态 × 回溯深度 64 4096 比特每比特存一个 int 的话是 16 KB看起来不多但对 FPGA 或嵌入式来说压力不小。C 实现里可以用 bitset 压缩std::vectorstd::vectoruint8_t survivor_register; // survivor_register[time][state] 表示t时刻到达state状态的输入比特但更高效的做法是环形缓冲只保存最近若干个时刻的幸存值。每译出一个比特就回溯到过去某个深度从那个深度对应的幸存路径开始反向追踪void traceback(int decoded_bit, int best_state) { // 从前向后扫描找到度量最小的状态 int min_metric path_metric[0]; best_state 0; for (int i 1; i NUM_STATES; i) { if (path_metric[i] min_metric) { min_metric path_metric[i]; best_state i; } } // 回溯depth步 for (int step 0; step traceback_depth; step) { best_state survivor_history[step][best_state]; } // 经过depth步之后当前状态的最高位就是译码比特 decoded_bit (best_state (K - 2)) 1; }参数说明traceback_depth决定了从当前时刻往回看多远。回溯时从最优状态出发根据每步记录的幸存输入反向转移最后状态的最高位就是该时刻的译码比特。这种方法是牺牲少量延迟换取 BER 性能实际通信系统中普遍采用。回溯完成之后译码输出需要对齐。因为回溯本身引入了 depth 个比特的延迟输出序列要往后平移 depth 个采样点才能跟原始比特流对齐。这个对齐问题很多人容易忽略导致误码率测出来很高其实是统计窗口没对准。4. C 译码避坑指南数值溢出、回溯深度与同步丢失4.1 现象软判决浮点累积导致路径度量溢出误码率飙高换成软判决译码以后用 float 存路径度量跑到几万帧之后 BER 突然变成 0.5完全异常。我把路径度量打出来发现数值已经到了 1e10 量级还在持续增长。原因很清楚维特比路径度量是累计值浮点数精度有限当数值远大于分支度量时比较两个相近的大数会出现精度丢失导致选择错误路径。解决方法是定期减掉所有状态度量的最小值也就是做归一化。我加了一个每帧回调的机制在 ACS 完成后计算最小度量并减去。另一种更彻底的做法是改用定点数比如 Q15 格式把所有软值和度量固定到 [-1, 1) 区间做整数运算。整数的比较是完全精确的不存在浮点精度问题。从坑里爬出来的经验是凡是涉及累计比较的算法优先考虑整数计算性能至少快 3 倍还没有精度问题。量子化范围选择 4 bit 或 8 bit信噪比损失很小。这份源码里默认给的硬判决其实是最稳妥的改成软判决时注意量化位宽和归一化逻辑。4.2 现象回溯深度设置成 16误码率出现平台期增加信噪比也无济于事我一开始为了降低延迟把回溯深度设成约束长度的两倍16结果误码率在 2 dB 之后就降不下去出现典型的错误平台。原因很简单回溯深度不够幸存路径还没有充分收敛尾部状态还混合着多条歧义路径回溯选出的比特不可靠。我后来把回溯深度改为 64即 K×10平台消失增益恢复到理论值。注意这里的深度单位是比特不是符号。有些 C 代码里深度当成迭代次数算回溯一次跳一个状态这个深度是够了但输出延迟却多了两倍导致同步窗口错位。建议做法是把回溯深度设成可配参数在做 BER 测试时分别跑 32、64、128画一条曲线对比选择性能饱和点。同时注意输出延迟等于回溯深度接收端的对齐模块要用同一个参数来确定导出数据的起始位置。4.3 现象编码器状态没有归零帧尾最后几个比特全错模拟联合信道时我把编码器输出直接送进译码器算出 BER 前面都很好但每帧末尾的 6 个比特必然错误。查了很久发现是编码器在每帧结束时没有强制回到全零状态。维特比译码假设起始状态是零它从零状态开始累积度量但编码器在帧尾保留了最后几个比特没有刷新。这样译码器在回溯到帧尾时面对的不是预期状态自然译错。解决很简单在每帧末尾追加 K-1 个 0 比特作为结尾冲刷序列编码器自然回到零状态。接收端把这 6 个比特当作训练序列扔掉不参与 BER 统计。这段在资源里有明确的注释我第一次读源码时忽略了这个细节导致后面全链路联调时浪费一整天。4.4 现象信号极性反转硬判决全错但软判决正常有一次直接把射频前端输出的信号接进译码器发现 BER 始终是 50%。用示波器看波形发现信号极性反了硬判决直接把所有 0 判成 11 判成 0。软判决看起来正常但实际上是度量计算中误打误撞把距离取反了性能损失了至少 3 dB。解决方式是在编码器加入差分编码或者接收端做极性检测。最简单的是用同步头和导频序列做极性判断在检测到错误极性后对接收比特做取反操作。C 实现中我会在 MetricUnit 的构造函数里加一个invert标志这样同一个软判决模块可以适配两条生产线的硬件不用改核心算法。5. 进阶验证搭建自环 BER 测试床与参数调优技巧有了编码器和译码器只代表代码跑得通不代表性能达标。我拿到这份源码后做的第一件事就是写一个端到端 BER 测试床把随机比特变成 QPSK 符号过 AWGN 信道再做软解调最后对比译码前后的误码率。这一步能直接验证维特比实现的正确性也能验证回溯深度、量化位宽这些参数的影响。测试床的第一步是构造随机源和信道模拟。我会生成 1e6 个随机比特用 BPSK 映射成 ±1然后加上高斯白噪声噪声方差由目标 Eb/N0 决定。之后接收端根据噪声方差做 4 bit 软量化输出范围 0~15。量化越细软判决损失越小但 4 bit 以上增益递减。#include random #include cmath #include vector std::vectoruint8_t apply_awgn_and_quantize(const std::vectorint8_t tx_symbols, double eb_n0_db, int bits_per_symbol1) { std::mt19937 gen(42); std::normal_distributiondouble noise(0.0, 1.0); double symbols_per_bit 1.0; double es_n0 std::pow(10, eb_n0_db / 10) * bits_per_symbol; double noise_std std::sqrt(1.0 / (2.0 * es_n0)); std::vectoruint8_t rx_soft(tx_symbols.size()); for (size_t i 0; i tx_symbols.size(); i) { double r tx_symbols[i] noise(gen) * noise_std; // 量化为4bit (0~15)其中近0表示噪声大近15表示确定 double llr 8.0 * r; // 简单线性映射 if (llr 0) llr 0; if (llr 15) llr 15; rx_soft[i] static_castuint8_t(std::round(llr)); } return rx_soft; }这块代码的逻辑完全服务于测试目的。eb_n0_db是每比特信噪比es_n0转换后通过噪声标准差控制信道质量。重点在软量化那里我用了线性映射虽然理论上正规的 LLR 应该是非线性但实际测试中线性映射在高信噪比下差异极小代码实现却简单很多。测一个点位的完整流程是生成 1e6 比特 → BPSK 映射 → 加噪 → 4 bit 量化 → 维特比硬判决译码 → 与原始比特对比统计错误数。整个过程跑下来在 2 dB 时硬判决 BER 大概在 1e-2 量级软判决能压到 1e-3 以下差距非常明显。把 Eb/N0 从 0 到 6 dB 扫一遍画 BER 曲线就能看到软判决的编码增益。参数调优时我重点关注三组参数回溯深度、软判决量化位宽、路径度量归一化周期。回溯深度从 64 增加到 128BER 曲线在 4 dB 处有微小改善但延迟翻倍因此取 64 即可。量化位宽从 3 bit 升到 4 bit增益约 0.15 dB升到 5 bit 基本没差别。归一化周期不需要配置每帧做一次减最小值操作就好代价远小于 ACS 本身。还有一个技巧是让测试床支持分段跑。把 1e6 比特分成 100 段每段 1e4 比特单独译码并统计错误数。这样能直观看到错误分布是否均匀如果一个段里错误特别集中说明帧同步或者状态初始化有问题而不是简单的噪声错误。我曾经这么跑发现某个段 BER 异常最后定位到编码器在段间没有正确保存移位寄存器状态导致了同步丢失修复后整条曲线变得平滑。从那以后我每次在 C 里集成维特比译码器都会强制走一遍统一的 BER 测试床流程不单测编码器也不单测译码器非要把整个链路拉起来量一遍才放心。这个习惯救了我好多次希望你复用这份 C 源码时也把这个测试床跑通。希望帮到你。本文还有配套的精品资源点击获取