4. Verilog HDL工业通信模块设计

工业通信模块的 Verilog 实现,难点通常不在单个语法点,而在于把“帧边界识别、串并转换、缓冲、校验、错误反馈”这几件事在同一个时钟域或跨时钟域中协调起来。本节以一个典型的 UART 半双工从站接收通道为例,把状态机、FIFO、CRC 校验、位同步与字节对齐串成一条可综合的数据通路,并给出关键参数的取值依据与观察方法。

一、整体数据通路与模块划分

接收通道从 rx 引脚进入,先经过位同步与起始位检测,再按 8 倍或 16 倍过采样完成位判决,得到串行位流;随后进入移位寄存器完成串并转换,形成字节;字节写入接收 FIFO,同时送入 CRC 计算模块;当帧结束标志出现时,比较本地 CRC 与帧尾 CRC,一致则置 frame_ok,否则置 crc_err 并丢弃该帧。发送方向则相反:CPU 写入发送 FIFO,状态机取出字节并附加 CRC,经并串转换后从 tx 输出。

工业通信接收通道:位同步 → 字节对齐 → FIFO → CRC 校验 位同步 16× 过采样 中点采样判决 字节对齐 起始位下降沿 移位寄存器 bit_cnt 接收 FIFO 深度 256×8 空/满/半满标志 CRC 校验 CRC-16/MODBUS 多项式 0x8005 帧判决 frame_ok crc_err 接收状态机(IDLE / START / DATA / CRC / DONE) IDLE START DATA CRC DONE CRC 错误或帧超时 → 回到 IDLE 并丢弃当前帧

二、位同步与字节对齐的实现要点

位同步的核心是“在正确的位置采样”。以 50 MHz 时钟、波特率 115200 为例,分频计数约为 434,若采用 16 倍过采样,则每个位周期内产生 16 个采样节拍。起始位检测采用下降沿触发:当 rx 由 1 变 0 时,状态机从 IDLE 进入 START,并启动采样计数器。为避免毛刺误判,通常在下降沿后连续采样 3 个节拍,若仍为 0 才确认起始位有效。

字节对齐依赖两个计数器:bit_cnt 记录已接收的数据位数(0~7),sample_cnt 记录当前位内的采样位置(0~15)。当 sample_cnt == 7 或 8 时采样数据位中点,可容忍约 ±3 个时钟周期的抖动。每采完 8 位,bit_cnt 归零并产生一个 byte_done 脉冲,把移位寄存器内容写入 FIFO。若在停止位位置采样到 0,则判定为帧错误 frame_err,状态机回到 IDLE。

三、FIFO 的读写与跨时钟域处理

接收 FIFO 建议采用同步 FIFO,深度 256、位宽 8,读写位于同一时钟域时可直接使用计数器实现。若 CPU 侧总线时钟与通信时钟不同,则必须使用异步 FIFO,读写指针分别用格雷码跨域,并通过两级触发器同步。以下给出一个同步 FIFO 的关键片段:

module sync_fifo #(parameter DEPTH=256, WIDTH=8)(
  input clk, rst_n,
  input wr_en, rd_en,
  input [WIDTH-1:0] din,
  output reg [WIDTH-1:0] dout,
  output full, empty
);
  reg [WIDTH-1:0] mem [0:DEPTH-1];
  reg [$clog2(DEPTH):0] wr_ptr, rd_ptr;
  assign full  = (wr_ptr - rd_ptr) == DEPTH;
  assign empty = (wr_ptr == rd_ptr);
  always @(posedge clk or negedge rst_n)
    if(!rst_n) begin wr_ptr <= 0; rd_ptr <= 0; end
    else begin
      if(wr_en && !full)  begin mem[wr_ptr[$clog2(DEPTH)-1:0]] <= din; wr_ptr <= wr_ptr + 1; end
      if(rd_en && !empty) begin dout <= mem[rd_ptr[$clog2(DEPTH)-1:0]]; rd_ptr <= rd_ptr + 1; end
    end
endmodule

注意 full 与 empty 的判断必须使用扩展一位的指针,否则深度为 2 的幂时无法区分“满”和“空”。工业现场常要求 FIFO 具备半满中断,可在 wr_ptr - rd_ptr == DEPTH/2 时拉高 half_full,供 CPU 提前搬运数据,降低溢出风险。

四、CRC 校验的编码实现

工业通信中 CRC-16/MODBUS 使用多项式 0x8005,初值 0xFFFF,输入输出均反射。逐位计算便于理解,但吞吐率低;实际工程中常用“字节异或 + 8 次移位”的查表法或并行展开法。下面给出逐位实现的参考写法:

function [15:0] crc16_byte;
  input [15:0] crc_in;
  input [7:0]  data;
  integer i;
  reg [15:0] c;
  begin
    c = crc_in ^ {data, 8'h00};
    for(i=0;i<8;i=i+1)
      c = c[15] ? ((c << 1) ^ 16'h8005) : (c << 1);
    crc16_byte = c;
  end
endfunction

在状态机中,每收到一个字节就调用一次该函数,帧尾两个字节不参与计算,而是与本地 CRC 比较。若一致,置 frame_ok 并允许 CPU 读取 FIFO;若不一致,置 crc_err,同时清空 FIFO 中本帧已写入的字节。这里有一个容易忽略的细节:FIFO 写入与 CRC 计算必须同步进行,否则出错时无法准确回退写指针。推荐做法是记录帧起始时的 rd_ptr 快照,出错时把 wr_ptr 恢复到该快照。

五、参数取值与结果判读

参数示例取值依据与影响
系统时钟50 MHz决定分频计数与采样精度
波特率115200分频值 ≈ 434,误差需 < 2%
过采样倍数16×中点采样,容忍 ±3 周期抖动
FIFO 深度256×8覆盖一帧最大长度并留余量
CRC 多项式0x8005MODBUS 标准,初值 0xFFFF

判读结果时,建议同时观察三个信号:byte_done 的脉冲间隔是否等于 10 个位周期(1 起始 + 8 数据 + 1 停止),frame_ok 是否在帧尾后 2~3 个周期内拉高,以及 crc_err 是否只在干扰或波特率偏差过大时出现。若 byte_done 间隔忽长忽短,优先检查分频计数是否被复位或采样计数器是否溢出;若 crc_err 持续出现而波形正常,则检查 CRC 初值与反射设置是否与对端一致。

调试建议:先用固定数据帧(如 01 03 00 00 00 01)在仿真中验证 CRC 结果,再接入实际串口。仿真时把 rx 按位周期逐位驱动,可直观看到 bit_cnt 与 sample_cnt 的配合关系。
注意:跨时钟域传递 FIFO 指针时,必须使用格雷码并做两级同步,直接同步二进制计数器会在多位同时翻转时产生亚稳态,导致空满判断错误。