FPGA实现UART串口通信:从协议原理到Verilog设计实战

发布时间:2026/8/29 5:48:54
FPGA实现UART串口通信:从协议原理到Verilog设计实战 1. 项目缘起为什么要在FPGA上实现串口通信在嵌入式系统和数字逻辑设计的圈子里串口通信UART绝对算得上是“元老级”的通信协议。它简单、可靠虽然速度不快但几乎无处不在从早期的PC到现在的单片机、工控设备再到各种调试接口都能看到它的身影。那么一个很自然的问题就来了既然市面上有那么多现成的、廉价的、性能还不错的串口芯片比如经典的MAX232、CH340为什么我们还要费劲在FPGA内部用Verilog或VHDL去实现一个串口通信模块呢这岂不是“杀鸡用牛刀”我最初接触这个需求是在一个需要高度集成和定制的工业控制项目里。主控是一颗FPGA需要同时与一个ARM处理器、一个DSP协处理器以及几个外围传感器通信。ARM和DSP都有现成的UART外设但传感器接口五花八门有的需要特定的波特率有的需要自定义的帧格式还有的对时序抖动非常敏感。如果为每个接口都外挂一颗串口芯片不仅会增加PCB面积、布板复杂度、物料成本更重要的是多芯片间的时钟同步和信号完整性问题会变得棘手。这时在FPGA内部用逻辑“软实现”多个独立的、参数可配的UART通道就成了一个非常优雅的解决方案。你可以把它理解为一个“通信协议转换与集成的瑞士军刀”FPGA强大的并行处理能力和可编程特性在这里得到了充分发挥。所以这个“串口通信的FPGA设计”项目远不止是教科书上的一个练习。它的核心价值在于将通信协议的灵活性、可靠性与FPGA的并行性、可定制性深度融合。通过这个设计你可以获得一个完全受控于你手中代码的通信接口它的波特率、数据位、停止位、校验方式都可以在编译时甚至运行时动态调整你可以轻松实现多通道并发通信也可以将串口数据无缝接入FPGA内部强大的数据处理流水线。这对于需要高实时性、多协议适配或深度集成的应用场景来说是外置芯片方案难以比拟的优势。2. 串口通信协议核心从比特流到字节的“翻译官”在动手写代码之前我们必须像庖丁解牛一样彻底理解UART协议这个“牛”的结构。很多人觉得串口简单不就是TX发、RX收吗但魔鬼藏在细节里一个稳定的UART IP核必须精准处理协议中的每一个状态。UART通信的本质是异步串行通信。关键词是“异步”和“串行”。异步意味着通信双方没有共享的时钟线。接收方必须依靠发送方约定的波特率从数据流中自己“猜”出每个比特的位置。这就好比两个人约好每隔一秒说一个字但没有秒表对时全靠感觉对双方的“时间感”稳定性要求极高。串行数据是一位一位bit by bit在单根数据线上传输的。一个完整的UART数据帧以最常见的8N1格式为例结构如下[空闲位高电平] -- [起始位低电平] -- [数据位D0] -- [D1] -- ... -- [D7] -- [停止位高电平] -- [空闲位高电平...]空闲位Idle线路在无数据传输时保持高电平逻辑‘1’。起始位Start Bit一个比特时间的低电平逻辑‘0’这是一个帧开始的明确标志。接收端检测到这个下降沿就启动接收过程。这里第一个坑就来了如何可靠地检测这个下降沿简单的边沿检测在噪声环境下容易误触发通常需要做消抖或过采样处理。数据位Data Bits紧接起始位之后通常是5-9位LSB最低有效位先行。这是我们传输的实际信息。停止位Stop Bit至少1位的高电平用于帧结束标志并确保下一帧的起始位能被正确检测到因为起始位是低电平停止位的高电平提供了明确的电平转换。校验位Parity Bit可选位于数据位和停止位之间用于简单的错误检测奇校验或偶校验。波特率Baud Rate是另一个核心概念。它定义了每秒传输的符号数对于UART一个符号就是一个比特。常见的波特率有9600 115200等。例如波特率为115200意味着每位数据的持续时间比特时间T_bit 1 / 115200 ≈ 8.68微秒。FPGA内部通常运行在几十甚至几百MHz的时钟下我们需要用一个计数器将高速的系统时钟分频到这个比特时间以生成发送时序和接收采样点。注意很多人混淆波特率和比特率。在UART这种一个符号代表一个比特的情况下两者数值相等。但在一些调制技术中一个符号可能代表多个比特两者就不同了。理解了这些我们就明确了FPGA设计需要完成的任务发送端TX将并行数据如一个8位字节按照约定的帧格式加入起始位、停止位等以精确的波特率串行化从TX引脚输出。接收端RX持续监视RX引脚检测起始位下降沿然后在每个比特时间的“最佳时刻”通常是中间点采样数据位组装成并行字节并完成帧校验。3. 发送模块UART_TX设计精准的“发报员”发送模块相对简单它是一个典型的状态机。其核心职责是当收到发送请求时按顺序、以精确的时序将帧的各个位推到TX线上。3.1 状态机设计与波特率生成一个稳健的发送状态机可以设计为以下几个状态IDLE空闲状态TX线输出高电平。等待发送使能信号。START发送起始位。TX线拉低并启动比特周期计数器。DATA循环发送各个数据位。每发送完一位索引加一直到所有数据位发送完毕。PARITY可选发送校验位。STOP发送停止位1个或多个比特时间。RETURN_IDLE一个小的等待状态确保停止位完整输出后才回到IDLE并拉高完成信号。这个状态对于连续发送时帧与帧之间的间隔控制很有用。波特率生成是时序精准的关键。假设系统时钟clk为50MHz周期20ns波特率baud_rate为115200。 比特时间T_bit 1 / 115200 ≈ 8.68us。 需要的时钟周期数N T_bit / T_clk 8.68us / 20ns ≈ 434。 因此我们需要一个计数器从0计数到433共434个周期计满即表示一个比特时间过去。这个计数器在每个比特时间内都会复位重启。// 波特率分频计数器示例 (Verilog) parameter CLK_FREQ 50_000_000; // 50 MHz parameter BAUD_RATE 115200; localparam BIT_PERIOD CLK_FREQ / BAUD_RATE; // 计算结果应为434 reg [15:0] baud_counter; reg bit_tick; // 比特周期标志当计数器计满时拉高一个时钟周期 always (posedge clk or posedge rst) begin if (rst) begin baud_counter 0; bit_tick 1b0; end else begin if (baud_counter BIT_PERIOD - 1) begin baud_counter 0; bit_tick 1b1; // 产生一个时钟周期的脉冲 end else begin baud_counter baud_counter 1; bit_tick 1b0; end end end状态机的状态转移就由这个bit_tick信号来驱动。在每个bit_tick有效时状态机推进到下一个状态或发送下一个数据位。3.2 数据移位与输出在DATA状态我们需要将并行数据串行移出。通常使用一个移位寄存器。reg [7:0] tx_data_reg; // 待发送数据寄存器 reg [2:0] bit_index; // 当前发送的数据位索引0-7 // 在加载数据时 tx_data_reg data_in; // 在DATA状态且bit_tick有效时 if (bit_tick) begin tx_out tx_data_reg[0]; // 输出LSB tx_data_reg {1b0, tx_data_reg[7:1]}; // 右移一位 bit_index bit_index 1; end一个重要的细节发送数据的加载时机。必须在IDLE状态且发送使能有效时将输入数据锁存到内部寄存器tx_data_reg中。在整个发送过程中都使用这个寄存器的值而不是直接使用可能变化的输入端口data_in。这保证了帧数据的完整性。4. 接收模块UART_RX设计敏锐的“听风者”接收模块是设计的难点和重点因为它要在没有时钟参考的情况下从可能包含噪声和抖动的信号中准确恢复数据。其核心思想是过采样与中点采样。4.1 起始位检测与抗抖动起始位是一个下降沿。最简单的检测方式是使用一个D触发器对RX信号打拍然后判断是否出现1-0的跳变。但在实际电路中信号可能存在毛刺或缓慢上升/下降。reg rx_d1, rx_d2; // 两级同步寄存器用于跨时钟域同步和边沿检测 always (posedge clk) begin rx_d1 rx_in; // 第一级同步对抗亚稳态 rx_d2 rx_d1; // 第二级同步得到稳定的信号 end wire start_edge (~rx_d1) rx_d2; // 检测下降沿仅检测边沿是不够的。如果这个低电平只是一个小毛刺并非真正的起始位就会导致误触发。因此常见的做法是在检测到下降沿后延时半个比特周期即波特率计数器计到N/2再次采样RX线。如果此时RX线仍然是低电平则确认是有效的起始位如果是高电平则认为是噪声放弃本次接收回到空闲状态。这个过程提供了很好的噪声免疫力。4.2 过采样与数据位恢复确认起始位后接收机需要在对每个数据位采样时找到最稳定的点通常选择在比特时间的中间。为了更精确常采用过采样技术例如16倍过采样。即将一个比特时间分为16个小段sample_ticks在中间的第7、8、9个小段或附近进行多次采样然后通过多数表决来决定该比特的值。这能有效对抗信号抖动和噪声。接收状态机与发送类似但由过采样时钟sample_tick频率波特率*16驱动。IDLE等待起始位。START_DETECT检测到起始边沿等待半位时间后确认。DATA_SAMPLE循环采样各个数据位。在每个比特时间内等待到预定的采样点如第8个sample_tick读取RX值并存入移位寄存器。PARITY_SAMPLE可选采样校验位。STOP_SAMPLE采样停止位。这是一个非常重要的校验步骤。如果在停止位应该为高电平的时刻采样到低电平则说明发生了帧错误Framing Error本次接收的数据应视为无效。DONE组装数据输出有效信号rx_valid和接收到的数据rx_data。// 过采样计数器与采样点生成示例 localparam OVERSAMPLE_RATE 16; localparam SAMPLE_POINT 8; // 在第8个采样点采样 reg [3:0] sample_counter; reg sample_tick; // 过采样时钟脉冲 always (posedge clk) begin if (state IDLE) begin sample_counter 0; end else begin if (sample_counter OVERSAMPLE_RATE - 1) begin sample_counter 0; sample_tick 1b1; end else begin sample_counter sample_counter 1; sample_tick 1b0; end end end // 在DATA_SAMPLE状态当sample_counter等于SAMPLE_POINT时采样 if (sample_tick (sample_counter SAMPLE_POINT)) begin rx_shift_reg {rx_d2, rx_shift_reg[7:1]}; // 将采样值移入寄存器MSB注意顺序与发送相反 end4.3 错误检测与处理一个工业级的UART接收机必须包含错误检测机制帧错误FE停止位采样为低电平。可能原因包括波特率不匹配、线路干扰或发送方异常。奇偶校验错误PE如果使能了奇偶校验计算接收数据的奇偶性与接收到的校验位不符。溢出错误OE接收FIFO或数据寄存器已满但新数据又接收完成。如果主控逻辑没有及时取走数据就会发生溢出。在模块输出接口上除了rx_data和rx_valid最好也输出这些错误状态位供上层逻辑处理。5. 系统集成与实战优化从模块到可靠通信将TX和RX模块封装成一个完整的UART IP核只是第一步。要让它在实际系统中稳定工作还需要考虑以下几个层面的集成与优化。5.1 时钟域与亚稳态处理FPGA的系统时钟频率远高于波特率时钟。RX引脚上的信号对于FPGA内部时钟来说是异步的。直接使用rx_in信号会导致亚稳态风险。前面提到的两级D触发器同步链rx_d1, rx_d2是处理跨时钟域单比特信号的经典方法。这能将亚稳态的概率降到极低。务必为所有异步输入信号不仅仅是RX添加同步器。5.2 FIFO缓冲区的引入这是提升实用性和可靠性的关键一步。无论是发送还是接收都强烈建议使用FIFOFirst In, First Out缓冲区。发送FIFO上层应用如CPU或其它逻辑模块可以将要发送的多个字节快速写入FIFO然后由UART_TX状态机从容地、一个字节接一个字节地发送出去。这样上层逻辑就不需要等待每个字节发送完毕提高了系统吞吐率。接收FIFOUART_RX模块每收到一个字节就存入接收FIFO。上层逻辑可以定期查询或通过中断感知FIFO非空然后一次读取多个字节。这能有效避免因主逻辑繁忙而导致的数据溢出丢失。FIFO的深度需要根据具体应用设计。例如如果上层软件轮询的间隔最大为10ms波特率为115200那么10ms内最多会收到115200/10 * 0.01 ≈ 115个比特约14个字节。那么接收FIFO深度至少设为16或32就能提供一个安全的缓冲。5.3 参数化与可配置性一个优秀的UART IP核应该是高度参数化的。通过Verilog的parameter或VHDL的generic允许用户在实例化时配置CLK_FREQ系统时钟频率BAUD_RATE波特率DATA_BITS数据位宽7, 8, 9STOP_BITS停止位数1, 1.5, 2PARITY校验方式NONE, ODD, EVENOVERSAMPLE过采样率16是常见值这样同一个IP核就能复用于项目中所有不同参数的串口需求。5.4 回环测试与调试技巧设计完成后如何进行测试最基础有效的方法是内部回环测试。将模块的TX输出直接连接到RX输入。在Testbench中向发送端写入一组数据然后在接收端检查是否能正确收到相同的数据。这可以验证整个数据通路和状态机的逻辑正确性。在实际硬件调试中我习惯采用“由内而外”的方法先用内部回环验证逻辑确保FPGA内部自发自收没问题。连接外部USB转串口工具使用PC上的串口助手如Putty、SecureCRT、或者自己写的Python脚本进行收发测试。这里有一个大坑注意TTL电平和RS232电平的区别。大多数FPGA开发板的IO是3.3V TTL电平0V为逻辑03.3V为逻辑1而PC传统的COM口是RS232电平3V至15V为逻辑0-3V至-15V为逻辑1。你需要一个USB转TTL串口线通常基于CH340、CP2102、FT232等芯片而不是普通的USB转RS232串口线。使用示波器或逻辑分析仪这是终极调试武器。可以直观地看到TX/RX引脚上的波形测量起始位、数据位、停止位的宽度精确计算实际波特率检查是否有毛刺或时序偏差。逻辑分析仪甚至能直接解码出十六进制或ASCII数据一目了然。提示在编写Testbench时除了发送常规数据一定要构造一些边界和错误用例如发送全0、全1、连续帧、错误的波特率等来测试接收机的鲁棒性。6. 超越基础高级特性与性能考量当基础功能稳定后可以考虑为你的UART设计添加一些高级特性使其更加强大和实用。6.1 自动波特率检测在某些需要自适应不同设备的场景下UART能够自动检测对方的波特率是非常有用的功能。其基本原理是利用一个已知的、固定的同步字符例如字符‘U’的ASCII码0x55其二进制为01010101是一个0/1交替的方波波形。接收端以不同的波特率时钟去尝试采样这个波形当采样到的数据与已知同步字符匹配时就确定了正确的波特率。实现自动波特率检测需要设计一个更复杂的接收状态机能够动态调整其内部的波特率分频系数。6.2 硬件流控RTS/CTS在高速或大数据量传输时为了防止接收端缓冲区溢出需要硬件流控。通过RTSRequest To Send和CTSClear To Send两根信号线进行握手。接收端准备好时拉低RTS表示“我可以接收”。发送端在发送前检查CTS如果为低才允许发送。 在FPGA中实现意味着需要为UART IP核增加这两个引脚并在状态机中增加对CTS的检查逻辑以及在接收FIFO快满时控制RTS信号。6.3 多通道与DMA集成FPGA的优势在于并行。你可以轻松实例化多个独立的UART IP核实现真正的多串口通信。更进一步可以设计一个轻量级的DMA直接内存访问控制器。当接收FIFO中的数据达到一定阈值时DMA控制器自动将数据搬运到FPGA内部的块RAM或外部DDR存储器中的指定缓冲区反之发送时也从缓冲区自动取数据填充发送FIFO。这能将CPU如果系统里有软核如NIOS II从繁琐的字节搬运中彻底解放出来专注于业务逻辑。6.4 时序收敛与资源优化对于大规模设计UART作为一个小模块也需要考虑其时序和资源占用。时序波特率生成计数器、过采样计数器等是较大的计数器其计数值比较逻辑可能成为关键路径。确保这些逻辑被正确约束或者采用更高效的计数方式如使用比较器而非等于判断。资源FIFO通常用Block RAM实现深度不大时资源消耗可忽略。状态机用独热码One-Hot编码在FPGA中通常比二进制编码性能更好。如果UART实例非常多可以考虑使用资源共享技术例如多个通道共享一个波特率生成器如果波特率相同。7. 从仿真到上板完整的开发流程与避坑指南理论设计完成代码编写完毕接下来就是让它在芯片里跑起来。这个过程每一步都可能遇到坑。7.1 仿真验证Modelsim/QuestaSim实战仿真是一切的基础。你需要编写一个全面的Testbench。时钟与复位生成产生稳定的系统时钟和复位信号。任务Task封装将“发送一个字节”、“接收并检查一个字节”等操作封装成任务方便调用。测试用例基础功能测试发送随机数据回环验证。边界测试测试最大/最小波特率需在参数范围内。错误注入测试在Testbench中模拟帧错误发送错误的停止位、模拟接收端波特率轻微失配观察错误标志是否置位。压力测试连续发送大量数据测试FIFO是否溢出。查看波形仔细查看状态机跳转、计数器工作、数据移位、信号同步等关键节点的波形确保与设计预期完全一致。特别注意复位后初始状态、使能信号的建立保持时间、以及valid信号与data的对应关系。7.2 综合与实现Vivado/Quartus中的设置将代码放入工程进行综合、布局布线。引脚约束这是硬件连接的第一步也是容易出错的一步。根据你的开发板原理图将顶层模块的tx、rx以及可能的rts、cts信号分配到正确的FPGA物理引脚上。务必确认引脚的电平标准如LVCMOS33是否正确。时钟约束为系统时钟输入创建时钟约束。这是保证时序分析准确的前提。查看时序报告实现后一定要看时序报告确保没有建立时间Setup Time或保持时间Hold Time违例。对于UART这种低速接口时序通常很容易满足但养成检查的习惯很重要。7.3 板上调试当代码遇见现实生成比特流下载到FPGA开发板。连接USB转TTL线打开串口助手。现象一发送数据PC收不到或乱码。检查1波特率、数据位、停止位、校验位设置。确保PC端串口助手和FPGA设计参数完全一致。这是最常见的问题。检查2TX/RX线接反了。FPGA的TX应接USB转TTL的RXFPGA的RX接USB转TTL的TX。GND必须共地。检查3电平问题。确认USB转TTL模块是3.3V电平且与FPGA IO电压匹配。检查4用示波器看波形。测量TX引脚看波形是否规整比特时间是否准确。如果波形畸变可能是驱动能力或负载问题。现象二能收不能发或能发不能收。检查1回环测试。在FPGA内部将TX连到RX先排除外部线路问题。检查2状态机卡死。利用FPGA厂商的调试工具如Vivado的ILA、Quartus的SignalTap将关键信号状态机状态、计数器、数据寄存器引出到逻辑分析仪看程序是否运行在预期状态。我个人的经验是状态机设计时每个状态转移条件都要考虑周全特别是错误条件下的“逃生路径”确保状态机不会进入死循环或未知状态。现象三高速时如921600通信错误率高。原因1时钟精度。FPGA的系统时钟是由晶振提供的其精度如±50ppm会影响生成的波特率精度。误差累积可能导致采样点偏移。计算一下你的波特率生成器产生的实际波特率与理论值的误差误差最好在2%以内UART协议通常允许的容限。原因2过采样率不足。在高速下信号建立时间、线路延迟的影响相对更大。可以考虑提高过采样率如从16倍提高到32倍以提供更稳定的采样窗口。原因3PCB布局布线。高速信号对PCB走线有要求。确保串口信号线走线尽量短远离噪声源并做好阻抗控制如果速率非常高。一个非常实用的调试技巧在代码中嵌入“软件printf”。可以在FPGA中实现一个非常简单的调试UART模块将内部一些关键变量如状态机状态、错误计数器、接收到的特定数据通过这个调试UART发送出来在另一个串口助手上查看。这比用逻辑分析仪抓取要直观得多尤其是对于需要观察长时间运行状态的情况。通过这样一个从协议理解、模块设计、系统集成到实战调试的完整过程你得到的不仅仅是一个能用的串口模块而是一套在FPGA上实现稳健、可靠、可配置通信接口的方法论。这个方法论可以迁移到I2C、SPI等其他通信协议的实现上。当你下次再遇到需要在FPGA中与外界“对话”的需求时你就能从容地拿出这把自己锻造的“瑞士军刀”游刃有余地解决实际问题。