FPGA 串口通信(UART)实战指南:从状态机设计到仿真验证

张开发
2026/6/14 15:40:43 15 分钟阅读
FPGA 串口通信(UART)实战指南:从状态机设计到仿真验证
1. FPGA串口通信的核心原理串口通信UART是嵌入式系统中最基础的通信方式之一它的魅力在于用最简单的两根线TX和RX就能实现设备间的数据交互。我在实际项目中经常用FPGA实现UART通信特别是在需要高速数据处理或自定义协议的场合。UART通信的本质是异步串行传输这意味着发送端和接收端不需要共享时钟信号。数据以固定的波特率逐位传输每个字节都被包装成包含起始位、数据位、可选校验位和停止位的完整数据帧。这里的关键点是双方必须事先约定好相同的通信参数包括波特率常见的有9600、115200等表示每秒传输的比特数数据位长度通常是8位对应一个字节校验方式可选无校验、奇校验或偶校验停止位通常1位或2位在FPGA中实现UART时最大的挑战是要用硬件逻辑精确控制每个比特的传输时序。我常用的方法是设计两个独立的状态机模块一个负责发送uart_tx一个负责接收uart_rx。这两个模块的核心都是通过计数器来精确控制每个比特的持续时间。2. 波特率生成与时钟同步波特率是UART通信的命脉计算错误会导致整个通信失败。假设我们使用50MHz的系统时钟要实现9600bps的波特率每个比特的持续时间对应的时钟周期数为50,000,000 / 9600 ≈ 5208个时钟周期在实际代码中我会用一个计数器来记录当前已经过了多少个时钟周期。当计数器达到目标值时就表示一个比特时间到了这时状态机就可以转移到下一个状态。这里有个实用技巧为了减少时钟偏差带来的影响我通常在比特时间的中间点进行采样。也就是说当计数器达到5208/22604时才真正读取或发送当前比特的值。这样可以最大限度避免在信号跳变沿附近采样。// 波特率计数器示例 localparam CYCLE CLK_FRE * 1_000_000 / BAUD_RATE; reg [15:0] baud_cnt; always (posedge i_clk_sys) begin if(baud_cnt CYCLE-1) baud_cnt 0; else baud_cnt baud_cnt 1; end // 在比特中间点生成采样脉冲 assign baud_pulse (baud_cnt CYCLE/2-1);3. 发送模块的状态机设计发送模块的状态机设计相对直观我通常将其分为五个状态空闲状态(IDLE)持续输出高电平等待发送指令起始位(START)拉低电平一个比特时间数据位(DATA)从最低位开始依次发送8个数据位校验位(PARITY)根据需要计算并发送校验位停止位(STOP)恢复高电平完成一帧传输在实际编码时我习惯用移位寄存器来简化数据发送过程。每次发送一个比特后将数据右移一位这样下一个要发送的比特总是位于最低位。// 发送状态机示例 always (posedge i_clk_sys) begin case(r_current_state) STATE_IDLE: begin o_uart_tx 1b1; if(i_data_valid) begin r_data_tx i_data_tx; r_current_state STATE_START; end end STATE_START: begin o_uart_tx 1b0; if(baud_pulse) r_current_state STATE_DATA; end STATE_DATA: begin if(baud_pulse) begin o_uart_tx r_data_tx[0]; r_data_tx {1b0, r_data_tx[7:1]}; if(r_tx_cnt 7) r_current_state STATE_PARITY; end end // 其他状态类似... endcase end4. 接收模块的稳定性设计接收模块比发送模块复杂得多因为它需要准确检测起始位并在噪声环境中可靠地采样数据。我总结了几个提高接收稳定性的技巧起始位检测不是检测到一次低电平就认为是起始位而是连续采样多次通常是3-5次只有全部都是低电平才确认是有效的起始位。这样可以有效过滤掉毛刺干扰。// 起始位检测逻辑 always (posedge i_clk_sys) begin r_flag_rcv_start {r_flag_rcv_start[3:0], sync_uart_rx}; end // 当连续5个采样都是低电平时认为检测到起始位 assign w_rcv_start (r_flag_rcv_start 5b00000);数据采样时机和发送模块一样在比特时间的中间点采样最为可靠。我通常会设计一个专门的波特率生成器来产生采样脉冲。帧同步机制一旦检测到起始位就启动一个完整的帧接收过程期间忽略线路上的任何变化直到完成停止位的接收。这样可以避免半途被干扰打断。5. 仿真验证与调试技巧仿真验证是UART设计中最关键的环节。我习惯先用简单的测试向量验证基本功能再用随机数据测试边界情况。下面是一个实用的测试方案基础功能测试发送固定的数据字节如0x55或0xAA检查接收端是否正确接收连续传输测试连续发送多个字节验证FIFO缓冲和流控制错误注入测试人为加入噪声和抖动测试模块的容错能力在仿真波形分析时我重点关注以下几个信号发送和接收的波特率是否严格同步数据位的采样点是否位于比特时间中间状态机转换是否符合预期校验位计算是否正确// 测试模块示例 initial begin // 初始化 rst_n 0; uart_in 1; #100 rst_n 1; // 发送字节0xAC #20000 uart_in 0; // 起始位 #104160 uart_in 0; // bit0 #104160 uart_in 0; // bit1 #104160 uart_in 1; // bit2 #104160 uart_in 0; // bit3 #104160 uart_in 1; // bit4 #104160 uart_in 0; // bit5 #104160 uart_in 1; // bit6 #104160 uart_in 1; // bit7 #104160 uart_in 1; // 停止位 end6. 实际应用中的优化策略在真实项目中单纯的UART通信模块往往不能满足需求。根据我的经验以下几个优化策略非常实用双缓冲设计在发送和接收端都采用双缓冲结构可以显著提高吞吐量。当一个缓冲区正在发送/接收时另一个缓冲区可以准备下一帧数据。自动波特率检测对于需要兼容不同波特率的应用可以设计自动检测电路。基本原理是通过测量起始位的持续时间来反推波特率。// 自动波特率检测简化实现 always (negedge i_uart_rx) begin if(!baud_detected) begin baud_counter 0; baud_detected 1; end end always (posedge i_clk_sys) begin if(baud_detected) begin baud_counter baud_counter 1; if(i_uart_rx) begin detected_baud SYSTEM_CLK / baud_counter; baud_detected 0; end end end硬件流控制增加RTS/CTS信号线实现硬件流控避免数据丢失。这在高速传输或大数据量场合特别重要。错误统计功能添加帧错误、奇偶校验错误等统计计数器便于系统级诊断和维护。7. 常见问题与解决方案在调试UART通信时我遇到过各种奇怪的问题。这里分享几个典型案例数据错位表现为接收到的数据看似随机错位。这通常是波特率不匹配导致的检查双方的波特率设置是否完全一致包括小数精度。偶发性错误特定模式下出现校验错误。可能是信号完整性问题尝试降低波特率或缩短通信距离也可以在PCB上加适当的终端匹配电阻。无法唤醒从低功耗模式唤醒后通信失败。检查时钟系统是否正常恢复必要时在唤醒后增加适当的延时。多设备冲突在总线式连接中多个发送设备会冲突。确保任何时候只有一个设备在发送可以考虑使用软件协议或硬件仲裁机制。8. 进阶应用自定义协议设计基础UART通信稳定后我通常会根据项目需求设计自定义协议。一个典型的协议框架包括帧头1-2个特殊字节标识帧开始长度字段指示后续数据的长度数据载荷实际传输的数据校验字段CRC或校验和帧尾可选的特殊结束标记这种结构化协议比原始UART可靠得多可以支持更复杂的应用场景。我在一个工业传感器项目中基于这种框架实现了可靠的数据采集系统连续运行三年无故障。

更多文章