【数字IC】Verilog UART实战:从状态机到可配置串口收发器

张开发
2026/4/19 3:58:23 15 分钟阅读

分享文章

【数字IC】Verilog UART实战:从状态机到可配置串口收发器
1. UART协议与状态机设计基础第一次接触UART协议时我被它简洁的硬件连接方式所吸引——仅需两根线TX和RX就能实现全双工通信。但在实际用Verilog实现时才发现这个看似简单的协议背后藏着不少设计门道。UART的核心在于状态机设计这也是数字IC工程师必须掌握的看家本领。UART协议的基本帧结构包含起始位低电平、数据位5-8位、可选的校验位和停止位高电平。我在实际项目中遇到过最典型的配置是8位数据位、无校验位、1位停止位波特率9600。这种配置下每个字节传输需要10个时钟周期1起始8数据1停止。状态机的设计关键在于明确状态转移条件。以发送模块为例我通常定义五个状态IDLE等待发送使能信号START发送起始位DATA按顺序发送数据位PARITY发送校验位如果使能STOP发送停止位localparam IDLE 3d0; localparam START 3d1; localparam DATA 3d2; localparam PARITY 3d3; localparam STOP 3d4;2. 可配置参数化设计实战在实际工程中固定的UART配置往往不够用。我做过一个需要支持多种配置的项目参数化设计帮了大忙。通过Verilog的parameter机制我们可以轻松实现可配置的UART收发器。关键参数包括DATA_WIDTH数据位宽5-8位PARITY_EN校验使能0无/1奇校验/2偶校验STOP_WIDTH停止位宽度1或2BAUD_RATE波特率2400/4800/9600等module uart_tx #( parameter DATA_WIDTH 8, parameter PARITY_EN 0, parameter STOP_WIDTH 1 )( input clk, input [DATA_WIDTH-1:0] data_in, output reg tx_out ); // 模块实现... endmodule波特率生成是另一个重点。假设系统时钟是100MHz要实现9600波特率分频系数计算如下分频系数 系统时钟频率 / (波特率 × 过采样率)对于发送端通常不需要过采样接收端建议16倍过采样以提高抗干扰能力。我在一个项目中实测发现当波特率误差超过3%时通信就会开始出现误码。3. 发送模块(TX)的精细实现发送模块的状态机实现有几个容易踩坑的地方。首先是位顺序问题——UART协议规定先传输最低位(LSB)。我曾因为搞反顺序导致整个项目调试了两天。状态转移逻辑的核心代码always (posedge clk) begin case(state) IDLE: if(tx_en) next_state START; START: next_state DATA; DATA: if(bit_cnt DATA_WIDTH-1) next_state PARITY_EN ? PARITY : STOP; PARITY: next_state STOP; STOP: if(stop_cnt STOP_WIDTH-1) next_state IDLE; endcase end校验位生成是另一个关键点。奇校验要求数据中1的个数为奇数偶校验则要求为偶数。用Verilog实现非常简洁assign parity_bit (PARITY_EN 1) ? ^data_in : // 奇校验 (PARITY_EN 2) ? ~^data_in : // 偶校验 1b0; // 无校验在实际测试中我发现发送模块最容易出问题的是停止位处理。特别是当STOP_WIDTH2时必须确保完整发送两个周期的高电平否则接收端可能会误判为新的起始位。4. 接收模块(RX)的抗干扰设计接收模块的设计比发送模块复杂得多主要挑战在于时钟同步和噪声过滤。我的经验是必须采用过采样技术通常使用16倍波特率时钟进行采样。一个实用的抗干扰方案是三取二投票法在每位数据的采样窗口内取中间3个采样点按多数表决确定最终值。这能有效滤除毛刺// 在每位数据的第7、8、9个采样点进行投票 always (posedge clk) begin if(sample_cnt 7) mid_samples[0] rx_in; if(sample_cnt 8) mid_samples[1] rx_in; if(sample_cnt 9) begin mid_samples[2] rx_in; data_bit (mid_samples[0] mid_samples[1]) | (mid_samples[1] mid_samples[2]) | (mid_samples[0] mid_samples[2]); end end起始位检测需要特别注意。我建议使用边沿检测电路当检测到下降沿时启动接收过程。同时要加入超时机制防止长时间卡在接收状态// 边沿检测 always (posedge clk) begin rx_in_dly rx_in; if(~rx_in_dly rx_in) start_detected 1b1; end // 超时计数器 always (posedge clk) begin if(state ! IDLE) begin if(timeout_cnt 16*12) // 允许12个位周期 timeout_cnt timeout_cnt 1; else timeout_reset 1b1; end else begin timeout_cnt 0; timeout_reset 1b0; end end5. 验证与调试技巧搭建完善的测试环境对UART设计至关重要。我的验证方案通常包括三个层次单元测试单独验证TX和RX模块initial begin // 发送测试序列 (posedge clk); tx_en 1; data_in 8h55; // 01010101 (posedge clk); tx_en 0; // 检查接收结果 wait(rx_valid); if(rx_data ! 8h55) $error(验证失败); end回环测试将TX输出直接连接到RX输入uart_tx tx_inst(.*); uart_rx rx_inst(.rx_in(tx_out), .*);实际设备测试通过USB转UART芯片与PC通信调试时我最常用的工具是逻辑分析仪。建议重点观察以下信号TX/RX数据线波形状态机状态变化波特率时钟边沿校验错误标志一个实用的调试技巧是注入错误故意发送错误的校验位或停止位验证接收模块的错误检测功能是否正常。我在一个项目中就曾发现校验功能未正确实现的bug。6. 工程化改进方向虽然基础UART已经能工作但要达到工业级标准还需要以下增强FIFO缓冲添加16/32字节的FIFO可以解决数据吞吐不匹配问题fifo #(.DEPTH(16)) tx_fifo ( .clk(clk), .wr_en(tx_wr_en), .data_in(tx_data_in), .rd_en(tx_rd_en), .data_out(tx_data_out), .full(tx_full), .empty(tx_empty) );自动波特率检测通过测量起始位宽度自动适配波特率中断机制当接收FIFO达到阈值或出现错误时触发中断DMA支持大块数据传输时减轻CPU负担在时钟处理方面建议添加时钟域交叉(CDC)处理特别是当发送和接收使用不同时钟时。我在一个多时钟域项目中就遇到过亚稳态问题最终通过双触发器同步解决always (posedge rx_clk) begin tx_data_sync[0] tx_data; tx_data_sync[1] tx_data_sync[0]; end7. 常见问题与解决方案在实际项目中我遇到过几个典型问题问题1接收数据错位现象接收到的数据位顺序错乱原因LSB/MSB传输顺序搞反解决检查数据移位方向UART通常先传LSB问题2高波特率下数据丢失现象波特率高于1Mbps时数据出错原因组合逻辑路径过长解决流水线设计优化关键路径问题3长距离传输不稳定现象电缆超过5米时误码率升高原因信号衰减和干扰解决添加RS-232/485电平转换改用差分信号一个实用的调试方法是分频测试先用极低波特率如300bps验证功能正确性再逐步提高波特率。我在调试一个115200bps的UART时就是先用9600bps验证基本功能再切换到目标速率。8. 进阶优化技巧对于需要更高性能的场景可以考虑以下优化预取机制当接收FIFO半满时提前通知主机读取硬件流控添加RTS/CTS信号防止数据丢失自适应均衡长距离传输时补偿信号衰减多通道设计单模块实现多个UART通道在资源受限的FPGA设计中可以采用时间复用技术共享硬件资源。例如一个UART核可以通过时分复用支持4个通道always (posedge clk) begin case(chan_sel) 2b00: begin tx_out chan0_tx; chan0_rx rx_in; end 2b01: begin tx_out chan1_tx; chan1_rx rx_in; end // 其他通道... endcase chan_sel chan_sel 1; end在功耗敏感应用中可以添加时钟门控功能当没有数据传输时关闭波特率时钟。我在一个电池供电的项目中采用这种设计使UART模块的静态功耗降低了60%。

更多文章