十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于NMS算法的LDPC译码器FPGA实现与Verilog设计实战

基于NMS算法的LDPC译码器FPGA实现与Verilog设计实战 简介面向FPGA教研与LDPC算法学习者的NMS归一化最小和译码工程在Vivado 2019.2平台下采用纯Verilog实现码长为9216覆盖从参数配置、译码迭代到结果输出的完整硬件链路。既适合高校本科、硕士、博士作为通信基带课程设计或科研预研的实践参考也可帮助工程师快速验证归一化最小和译码算法的性能表现。工程源码结构清晰配套testbench激励与完整操作录屏学习者可对照AVI录像打开Vivado工程完成编译、仿真和波形分析直观理解NMS译码的迭代过程与FPGA实现方法。资源共1457个文件压缩包约31.27MB包含Verilog/VHDL源码、Tcl运行脚本、仿真波形VCD/FSDB/LXT/WDB、bat批处理脚本以及操作录像等各类型文件分工明确便于按需检索和二次开发。目前已有977人学习下载适合需要结合具体工程代码深入掌握NMS译码原理、并能在Vivado中实际运行的FPGA开发者工程内还包含完整的仿真波形记录对照波形可有效辅助算法调试。 做FPGA通信基带的朋友应该都有体会信道编解码这块属于看着不难、动手就跪的典型。尤其是LDPC译码器算法原理看一遍觉得懂了真到了写Verilog的时候光是那一堆变量位宽、迭代时序、校验节点更新逻辑就能折腾好几个通宵。我之前在Xilinx Vivado 2019.2平台上用纯Verilog实现过一个基于NMSNormalized Min-Sum归一化最小和算法的LDPC译码器配套完整的testbench支持仿真波形直接观察迭代过程和译码结果。这篇就把整个设计思路、工程结构、实现细节和踩坑记录都整理出来给正在做相关方向或者准备入门LDPC译码器数字实现的朋友一个参考。这套东西适合谁一是学校里做通信算法验证、需要快速在FPGA上跑原型的学生二是刚转行做数字IC或FPGA基带开发的工程师三是想对比NMS、OMS、MS几种近似算法硬件代价的硬件架构师。整个工程完全用Verilog 2001语法编写不依赖Xilinx的CORE Generator或HLS意味着你可以直接移植到其他FPGA平台或ASIC流程里这也是我当初坚持用纯Verilog而不是调用IP核的原因。1. 内容整体设计与思路拆解1.1 为什么选择NMS算法而不是标准BP或MSLDPC译码的经典算法是置信传播Belief PropagationBP性能最好但硬件实现要处理tanh和atanh运算资源消耗和时序收敛都是噩梦。最小和Min-SumMS算法把校验节点的运算简化为找最小值和次小值硬件友好度直接提升一个数量级代价是译码性能有约0.2~0.5 dB的损失。NMS在MS的基础上引入一个小于1的归一化因子把校验节点更新的幅度整体缩小用来补偿MS算法对LLRLog-Likelihood Ratio对数似然比幅度的过估计。这个因子取值一般在0.7~0.9之间具体取多少要看码型和量化位宽。我在工程里默认取了0.75配合8bit定点量化实测性能折中比较理想。当然这个值是可以在RTL里通过参数调整的方便做性能扫描。1.2 硬件架构选型全并行还是部分并行LDPC译码器的硬件架构大致分三类全并行、部分并行和串行。全并行吞吐率高但布线拥塞严重资源消耗巨大串行资源最省但速度感人。我这次选择的是部分并行架构按校验矩阵的块结构来分配计算单元具体来说就是把H矩阵按QC-LDPC的循环移位方式组织每个块对应一个移位寄存器链这样既能保证吞吐率又不至于把LUT烧穿。用QC-LDPC码型还有个额外的好处校验矩阵可以通过基矩阵加循环移位系数来唯一确定存ROM的时候只需要存基矩阵和移位系数不需要把整个H矩阵都存下来。我用的码型是IEEE 802.11n标准里的码率1/2、码长648的QC-LDPC这个码型规范公开、测试向量好找适合做验证基准。1.3 为什么坚持纯Verilog开发我知道现在很多人喜欢用HLS或者SystemVerilog但我在这个工程里刻意选了纯Verilog 2001理由很实际最保守的综合兼容性。不管你是用Vivado、Quartus还是后续要跑DC综合纯Verilog 2001几乎不会遇到语法兼容问题。而且LDPC译码器这种控制流复杂的模块用Verilog写反而逼着你把每个周期的行为都想清楚不会出现HLS里C模型仿真通过、RTL综合后时序烂掉的情况。2. 核心细节解析与实现要点2.1 校验矩阵的预处理与存储拿到一个QC-LDPC码型后第一步不是写代码而是把校验矩阵转换成硬件友好的存储格式。我在工程里写了一个MATLAB脚本做预处理输出一个h_matrix.vh头文件里面定义基矩阵的行数、列数、循环移位系数表以及每个非零块在译码过程中的读写地址映射。这里有个细节容易踩坑校验矩阵的循环移位方向不同标准定义不一样IEEE 802.11n是右移但有些码型是左移搞反了译码器输出全是错的。我的做法是在预处理脚本里强行统一成向右循环移位然后生成地址映射表这样RTL代码里只需要一种移位逻辑。2.2 LLR量化和定点格式浮点转定点是整个设计里影响性能最直接的一步。我选用8bit定点符号位1bit 整数位3bit 小数位4bit简称Q3.4格式。这个选择的依据是输入LLR的动态范围一般不超过±8Q3.4能表示的范围是-8~7.9375精度0.0625对应BER在10^-4量级时量化损失可以控制在0.1dB以内。信道LLR的计算我放在testbench里用软件完成RTL接收的是已经量化好的8bit LLR序列。这样做的好处是RTL只关注译码本身调试时能干净地区分是量化损失还是译码逻辑错误。实际工程中如果要做片上系统LLR计算单元一般会用CORDIC或者查表实现但那是另一个话题了。2.3 校验节点更新NMS的核心计算单元校验节点更新Check Node UpdateCNU是NMS算法的关键。对于每个校验节点需要找出所有连接到它的变量节点LLR的绝对值最小值和次小值同时记录最小值对应的位置和符号累乘结果。更新规则可以写成对于校验节点m对每个变量节点n 新信息 符号累乘 / 符号_n × 归一化因子 × (n的绝对值是否等于最小值 ? 次小值 : 最小值)用Verilog实现时我先在一个周期内并行读取该校验节点所有相连的变量节点LLR然后用两级流水线做最小值/次小值查找。第一级两两比较第二级合并结果整个比较树深度是log2(变量节点数)。对于802.11n码率1/2的648码长每行最多20个非零元素两层比较就能出结果。2.4 变量节点更新与硬判决变量节点更新Variable Node UpdateVNU相对简单把信道LLR和所有相连的校验节点传来的外部信息相加就得到后验LLR。后验LLR的符号就是硬判决位。要注意的是送给校验节点的外部信息必须减去对应那条边上的旧校验信息否则会形成正反馈导致译码器错误收敛。硬判决后需要检查是否满足所有校验方程这个检查我放在每次迭代的最后做。做法是把硬判决向量和校验矩阵相乘模2得到一个校验子向量如果全零就说明译码成功可以提前终止迭代。提前终止对能耗和平均时延的优化非常明显尤其在信道条件好的时候平均迭代次数能下降30%以上。3. 实操过程与核心模块实现3.1 工程目录结构与模块划分整个Vivado工程我按下面的目录组织清晰且方便回归验证src/ ldpc_decoder_top.v // 译码器顶层控制状态机 cn_u.v // 校验节点更新单元 vn_u.v // 变量节点更新单元 llr_ram.v // LLR存储器 h_matrix_rom.v // 基矩阵和移位系数ROM barrel_shifter.v // 循环移位器 sim/ tb_ldpc_decoder.v // testbench顶层 llr_gen.v // 信道LLR生成与量化 encode_ref.v // LDPC编码器用于生成测试向量 scoreboard.v // 结果对比与统计 run/ simulate.tcl // Vivado仿真脚本顶层模块ldpc_decoder_top.v是整个译码器的核心控制器用有限状态机管理译码状态IDLE、LOAD加载LLR、ITERATE迭代译码、OUTPUT输出结果。核心设计思想是把迭代次数做成输入信号max_iter这样testbench可以灵活扫描不同迭代次数对误码率的影响。3.2 桶形移位器的实现QC-LDPC译码中反复用到循环移位操作我直接用桶形移位器来实现。桶形移位器的好处是移位量可以在一个周期内完成不随移位位数增加而增加延迟代价是布线资源较多。对于8bit数据宽度和最大移位量127我用两级MUX实现第一级按低4位移位第二级按高3位移位。核心代码如下省略了部分边界信号主体思路就是一个可综合的循环右移module barrel_shifter #( parameter DATA_WIDTH 8, parameter SHIFT_WIDTH 7 )( input wire [DATA_WIDTH-1:0] data_in, input wire [SHIFT_WIDTH-1:0] shift_val, output wire [DATA_WIDTH-1:0] data_out ); wire [DATA_WIDTH-1:0] stage1, stage2; assign stage1 shift_val[3] ? {data_in[3:0], data_in[7:4]} : data_in; assign stage2 shift_val[2] ? {stage1[5:0], stage1[7:6]} : stage1; assign stage3 shift_val[1] ? {stage2[6:0], stage2[7:7]} : stage2; assign data_out shift_val[0] ? {stage3[7:7], stage3[6:0]} : stage3; endmodule这里要特别注意位宽参数化写不好很容易出现仿真正确、上板错误的情况。我之前踩过一坑DATA_WIDTH和SHIFT_WIDTH不匹配导致高位移位被综合工具优化掉仿真全对跑到板子上译码结果就错。3.3 译码状态机的时序控制译码状态机是整个模块的灵魂。我采用三段式状态机当前状态、次态逻辑、输出逻辑分开写。每次迭代的时序可以分成四个阶段读阶段从LLR RAM读取当前层的变量节点信息经过桶形移位器对准到对应的校验节点。计算阶段CNU并行计算产生新的校验信息。更新阶段VNU更新变量节点LLR同时写回LLR RAM。判决阶段检查校验子是否全零决定是否提前终止。这里有个量化细节校验节点信息在CNU内部我保持为比输入多1bit的带符号整数原本8bit的LLR在校验节点里因为要累加多个符号信息动态范围可能扩展到9bit才能保证精度。归一化乘法用右移两位代替乘以0.75算是省了DSP资源换来的是约0.05dB的性能损失完全在可接受范围。3.4 testbench的完整设计testbench我做得比较完整不只是给一组激励然后看波形。核心模块有三个encode_ref.v做LDPC编码生成正确的码字然后经过一个加噪模块叠加高斯白噪声用伪随机数发生器模拟得到带噪的接收序列。llr_gen.v把接收的BPSK符号转换成LLR公式是LLR 2 * rx / sigma^2然后用固定缩放因子量化成8bit。这里我刻意在testbench里暴露了sigma参数方便做不同SNR点位的仿真。scoreboard.v做结果比对它把译码输出和编码前的原始信息比特做比较统计误码率和错误帧数同时在迭代成功后打出一条PASS信息。下面是testbench里迭代控制的一小段显示如何产生带噪声的LLR激励// 产生高斯噪声sigma由外部参数控制 // 简单实现用LFSR产生均匀分布再通过Box-Muller变换近似高斯 wire signed [15:0] noise; gaussian_noise #(.SEED(32hA5A5)) u_noise ( .clk(clk), .sigma(sigma_q), .out(noise) ); // LLR 2 * rx / sigma^2再加量化 always (posedge clk) begin if (load_en) begin llr_q $signed({2b00, rx_bpsk}) * $signed(llr_scale) 4; end end实际跑仿真的时候建议先在无噪声情况下验证testbench自身是正确的——就是加噪模块的sigma设成0此时LLR接近无穷大译码器必须一次迭代直接出正确结果。这一步过了才能放心去扫SNR。4. 常见问题与排查技巧实录4.1 怎么确认testbench仿真结果是可信的很多新手拿到LDPC工程仿真波形出来一堆信号不知道到底对不对。我在testbench里加了两个自动判据一是scoreboard在每个帧结束自动比较译码结果和原始发送比特并把累计错误数打印到终端二是译码状态机拉出decode_done和decode_success两个指示信号。如果decode_success一直为0先别急着怀疑CNU/VNU逻辑先回去检查编码器出来的码字是不是真的满足校验矩阵。我遇到过一个尴尬情况编码器实现有个bit序错误导致编译码整体不匹配折腾了两天才发现。4.2 定点量化迭代不收敛的排查顺序如果NMS译码器在纯浮点模型里正常但定点RTL不收敛按这个顺序排查第一步看输入LLR的量化范围是不是饱和了第二步看变量节点LLR在迭代过程中是否溢出建议在testbench里用$display监视中间值第三步看归一化因子是不是太小NMS因子取0.75如果性能反而更差试试0.875第四步检查符号位扩展。实测中有一次问题出在VNU模块里两个8bit数相加后直接截断到8bit负数补码截断后绝对值变小导致越来越多的迭代错误。4.3 Vivado 2019.2仿真库与编译的坑Vivado 2019.2自带仿真器但直接跑混合代码时有些旧IP核的仿真模型会报编译错误。这个工程因为是纯Verilog不涉及仿真库编译的问题比较省心。如果后续要调用Xilinx的Block RAM IP记得在工程设置里把xsim的库路径指对否则仿真会报找不到glbl模块。另外timescale一定要写在文件头部并且每个文件保持一致我之前吃过亏timescale 1ns/1ps和1ns/100ps混用时xsim会警告但不会报错导致仿真时序偏移波形和预期对不上。4.4 综合时序与优化经验在Vivado里综合这个译码器要点是把流水寄存器打够。我建议两个寄存器级之间不要超过LUT3LUT4LUT5的路径这样在100MHz时钟下时序能收敛。如果时序不过优先检查CNU里的比较树它是最长的组合逻辑路径。我的做法是在CNU内部再插两级流水代价是译码延迟多两个周期但最高频率能拉高约25%。另外块RAM的读写地址计算别放在同一个always块里否则BRAM的读写冲突会导致仿真和综合行为不一致。4.5 用批量仿真做误码率扫描为了验证译码性能我用Tcl脚本批量跑仿真对不同SNR点位各跑若干帧数据。Vivado的xsim支持通过Tcl设置参数在testbench里用$value$plusargs解析外部参数可以控制sigma、最大迭代次数等。批量仿真脚本的核心逻辑是对每个SNR点位生成不同的随机种子避免每次都编同一帧数据导致误码率曲线没有统计意义。实测下来每个SNR点位跑200帧以上在BER 10^-3量级已经能看出明显的性能区分度。5. 资源占用与性能实测记录我用Vivado 2019.2综合到xc7z020Zynq-7020在100MHz时钟约束下资源占用情况大致如下资源类型使用量占比LUT1243623.4%FF89028.4%BRAM 36Kb1821.4%DSP48E00%NMS算法相比纯Min-Sum多出来的资源主要是归一化乘法但我用移位代替乘法后DSP使用量为0LUT增加了少量。整个译码器对于码长648的码型最大迭代10次时平均译码吞吐大约在86 Mbps左右。如果改用块并行度更高的架构吞吐还能再往上拉但资源会相应增加这个折中点需要根据实际项目需求来切。最后再分享一个调试小技巧做LDPC译码器调试强烈建议在testbench里把每轮迭代的校验子向量syndrome拉出来看波形。如果第一轮迭代后校验子数量开始下降说明整体方向是对的如果校验子从头到尾没变化大概率是初始化LLR读取顺序和H矩阵存储不匹配。这个排查思路比我见过的任何调试工具都管用。本文还有配套的精品资源点击获取
返回列表