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

资讯详情

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

FPGA UART通信全流程设计:从Verilog实现到Vivado上板调试

FPGA UART通信全流程设计:从Verilog实现到Vivado上板调试 1. 项目概述从零构建FPGA的UART通信链路在FPGA开发中实现与上位机或其他微控制器的数据交互UART通用异步收发传输器几乎是每个工程师的必修课。它不像I2C或SPI那样需要时钟线仅凭两根数据线TX和RX就能完成全双工通信结构简单协议直观是调试和传输少量数据的理想选择。然而在Vivado这样的高级综合工具中实现一个稳定可靠的UART模块远不是调用一个IP核那么简单。你需要理解从比特率生成、数据帧组装到跨时钟域处理、错误检测等一系列底层细节。这个项目就是带你深入这些细节用Verilog/VHDL手撕一个带FIFO缓冲的UART收发器并完成在Vivado中的仿真、综合、上板调试全流程。无论你是刚接触FPGA通信的新手还是想夯实底层时序逻辑设计的老手这套从理论到实战的完整方案都能让你对异步串行通信有脱胎换骨的理解。2. UART核心原理与Vivado设计考量2.1 UART协议帧结构再审视很多人以为UART就是简单的发送0和1其实它的每一帧数据都包含严格的起止标志。一个标准的帧由1位起始位低电平、5-9位数据位、可选的1位校验位和1位或1.5位或2位停止位高电平组成。在FPGA中我们通常采用8位数据位、无校验、1位停止位8N1的配置。关键在于接收端必须在一个比特周期的中间点对数据进行采样以避开信号边沿的不稳定区域这就需要精确的波特率时钟。在Vivado中设计时系统主时钟频率如100MHz与波特率如115200 bps通常不是整数倍关系。直接分频会产生累积误差影响长时间通信的稳定性。因此我们不能用简单的计数器分频而需要采用累加器或DDS直接数字频率合成的思想来生成波特率使能信号。例如对于100MHz时钟和115200波特率每个比特周期需要约868个主时钟周期。我们可以用一个累加器每次累加一个固定的步进值Step 目标周期数 * 2^N / 主时钟周期数当累加器溢出时产生一个波特率使能脉冲。这种方法能将误差分散到每个比特周期避免累积。2.2 Vivado环境下异步信号处理的关键UART的RX信号对于FPGA内部系统时钟来说是纯粹的异步信号。直接将其接入内部时序逻辑极易导致亚稳态即触发器输出在较长时间内处于不确定的中间电平进而导致后续逻辑误判。在Vivado中处理这个问题有标准流程两级同步器这是必须的。使用两个级联的触发器对异步的RX信号进行采样将亚稳态发生的概率降到可接受的水平。注意同步器只能降低概率不能完全消除。边沿检测对同步后的信号进行边沿检测以捕捉起始位的下降沿。通常采用一个寄存器缓存上一拍的值当(previous_rx 1‘b1) (current_rx 1’b0)时表示检测到起始位。时序约束对于同步器电路需要在XDC约束文件中使用set_false_path或set_clock_groups -asynchronous来告诉Vivado时序分析工具忽略这条路径的建立/保持时间检查因为异步信号本身就没有确定的时序关系。不添加这个约束Vivado可能会报告无法实现的时序路径但实际这是正常的。注意绝对不要在代码里直接用系统时钟去采样异步的RX引脚也绝对不要试图用组合逻辑去检测异步信号的边沿。这是新手最常犯的错误会导致系统行为随机极难调试。3. 发送模块UART_TX设计与实现细节3.1 状态机设计与比特率生成发送模块的核心是一个有限状态机FSM。典型的状态包括IDLE空闲、START_BIT发送起始位、DATA_BITS发送数据位、STOP_BIT发送停止位。状态转移由波特率使能信号baud_tick驱动。比特率生成模块是独立的。我们创建一个名为baud_gen.v的模块。假设系统时钟clk为100MHz目标波特率baud_rate为115200。module baud_gen #( parameter CLK_FREQ 100_000_000, // 100 MHz parameter BAUD_RATE 115200 )( input wire clk, input wire rst_n, output reg baud_tick ); // 计算每个波特率时钟周期需要的系统时钟周期数 localparam integer CLKS_PER_BIT CLK_FREQ / BAUD_RATE; // 约868 // 使用更精确的累加器方法这里以计数器简化示例 reg [15:0] counter; always (posedge clk or negedge rst_n) begin if (!rst_n) begin counter 0; baud_tick 1b0; end else begin if (counter CLKS_PER_BIT - 1) begin counter 0; baud_tick 1b1; end else begin counter counter 1; baud_tick 1b0; end end end endmodule实际项目中为了更精确我们会使用累加器accumulator accumulator BAUD_STEP;当累加器溢出时产生baud_tick。BAUD_STEP (BAUD_RATE ACCUMULATOR_WIDTH) / CLK_FREQ。3.2 并串转换与输出控制在DATA_BITS状态我们需要将并行数据转换为串行数据。通常会用一个移位寄存器。当baud_tick有效时将数据位右移一位最低位输出到tx引脚。// 在UART_TX模块的状态机DATA_BITS状态中 always (posedge clk or negedge rst_n) begin if (!rst_n) begin tx_data_shift 8‘hFF; end else if (state DATA_BITS baud_tick) begin if (bit_index 3’d7) begin // 最后一位数据已发送准备进入STOP_BIT状态 nxt_state STOP_BIT; end else begin bit_index bit_index 1; // 右移一位高位补1空闲状态为高电平 tx_data_shift {1‘b1, tx_data_shift[7:1]}; end end end assign uart_tx (state START_BIT) ? 1‘b0 : (state DATA_BITS) ? tx_data_shift[0] : 1’b1; // IDLE和STOP_BIT时为高电平这里的关键是在空闲状态和停止位期间tx线必须保持高电平。起始位是强制拉低一个比特周期。4. 接收模块UART_RX设计与抗干扰策略4.1 起始位检测与采样点对齐接收模块比发送模块复杂因为它要从异步信号中可靠地提取数据。起始位检测后我们不能立即开始采样数据位。为了在数据位最稳定的中心点采样通用的策略是检测到起始位下降沿后等待1.5个比特周期再采样第一个数据位LSB之后每等待1个比特周期采样下一位。这1.5个周期的等待是为了让采样点对齐到第一个数据位的正中心。实现时我们同样需要一个状态机并利用波特率计数器。检测到起始位后计数器从0开始在计数值达到CLKS_PER_BIT * 1.5时采样第一个数据位并重置计数器。之后每当计数器达到CLKS_PER_BIT时采样下一个数据位。// 在UART_RX模块中 always (posedge clk or negedge rst_n) begin if (!rst_n) begin state_rx IDLE; sample_counter 0; bit_index_rx 0; rx_data_shift 8‘h00; data_valid 1’b0; end else begin case (state_rx) IDLE: begin if (rx_sync_d2 1‘b0) begin // 检测到起始位下降沿 state_rx START_DETECTED; sample_counter 0; end end START_DETECTED: begin if (baud_tick) begin if (sample_counter SAMPLE_MID_POINT) begin // 例如 1.5 * CLKS_PER_BIT // 到达第一个数据位的采样点 state_rx RECEIVE_DATA; sample_counter 0; bit_index_rx 0; end else begin sample_counter sample_counter 1; end end end RECEIVE_DATA: begin if (baud_tick) begin if (sample_counter CLKS_PER_BIT - 1) begin // 采样当前数据位 rx_data_shift[bit_index_rx] rx_sync_d2; sample_counter 0; if (bit_index_rx 3‘d7) begin // 8位数据接收完毕 state_rx STOP_BIT; end else begin bit_index_rx bit_index_rx 1; end end else begin sample_counter sample_counter 1; end end end STOP_BIT: begin // 等待停止位结束可做校验 data_valid 1‘b1; // 产生一个时钟周期的有效脉冲 state_rx IDLE; end endcase end end4.2 多数表决与毛刺滤除在实际的电路板上串口线可能较长会引入噪声和毛刺。为了提高可靠性不要在一个精确的点采样一次。更稳健的做法是在每个数据位的采样窗口内例如以理论采样点为中心前后各取1-2个系统时钟周期进行多次采样如3次或5次然后采用“多数表决”原则确定该数据位的值。例如采样3次有2次或3次为高则判定该位为‘1’。这需要在波特率时钟的基础上再生成一个更高速的采样时钟例如波特率时钟的16倍用于在一个比特周期内进行多次采样。这是很多成熟UART IP核的做法能极大增强抗干扰能力。对于要求不高的实验室环境中心单点采样可以工作但产品化设计必须考虑多数表决。5. FIFO缓冲集成与数据流控制5.1 为何需要FIFO如果没有FIFO发送模块必须等上位机完全取走当前数据后才能接收新数据接收模块必须在下一个字节到来前被读取否则数据会丢失。这在实时性要求高的系统中是不可行的。FIFO先进先出存储器作为缓冲解耦了数据生产接收/待发送和数据消费读取/发送的速度。在Vivado中你可以用寄存器自己搭建一个简单的同步FIFO但更高效的方式是使用Xilinx提供的FIFO Generator IP核。它经过高度优化能生成基于Block RAM或Distributed RAM的高性能FIFO并且自带满、空、可编程几乎满/几乎空等状态标志。5.2 发送与接收FIFO的连接发送路径上位机通过AXI总线或自定义接口将数据写入TX_FIFO。UART发送模块的状态机在IDLE状态且TX_FIFO非空时从FIFO中读取一个字节加载到发送移位寄存器然后启动发送过程。发送期间状态机不再查询FIFO直到当前字节发送完毕回到IDLE状态再读取下一个字节。接收路径UART接收模块每完整接收一个字节并校验通过后产生一个data_valid脉冲将数据写入RX_FIFO。上位机定期查询RX_FIFO的非空信号非空时则读取数据。这种结构使得上位机可以一次性写入多个字节进行发送也可以在方便的时候一次性读取多个接收到的字节大大提高了系统的吞吐量和可靠性。5.3 FIFO深度计算FIFO深度是设计关键。深度不够会导致溢出丢数据深度过大浪费资源。一个简单的估算方法是发送FIFO深度 上位机最大突发写入数据量 - UART发送期间上位机无法写入的时间 * 写入速率。接收FIFO深度 UART持续接收速率 * 上位机最大响应时间。 例如如果上位机每秒最多处理10个字节而UART以115200波特率约每秒11520字节接收那么即使上位机延迟1毫秒10个字节处理时间FIFO也可能被填满。因此需要根据最坏情况计算。通常深度设为32或64对于中等速度的串口通信是安全的起点。6. Vivado工程实操从IP核封装到上板调试6.1 创建工程与添加源文件打开Vivado创建新工程选择正确的FPGA器件型号。将编写好的uart_tx.vuart_rx.vbaud_gen.vuart_top.v顶层模块例化前述所有模块和FIFO IP等源文件添加到工程中。使用IP Integrator或者直接搜索FIFO Generator添加一个FIFO IP核。关键配置如下Interface Type: Native标准FIFO接口FIFO Implementation: Common Clock Block RAM如果收发时钟同源读写宽度: 8 字节读写深度: 例如 512使能满、空、几乎满、几乎空标志。几乎满/空阈值可以根据需要设置用于提前触发中断或DMA请求。6.2 编写顶层模块与引脚约束顶层模块uart_top.v负责将所有子模块和IP核连接起来并引出对外的接口如系统时钟、复位、UART TX/RX引脚以及与上位机交互的接口如简单的并行总线或AXI-Stream接口。接着创建或编辑XDC约束文件。最关键的是引脚位置和电平标准约束# 时钟引脚约束 create_clock -period 10.000 [get_ports sys_clk] # UART引脚约束假设连接到FPGA的某个Bank电平为LVCMOS33 set_property PACKAGE_PIN Y11 [get_ports uart_tx] set_property PACKAGE_PIN AA11 [get_ports uart_rx] set_property IOSTANDARD LVCMOS33 [get_ports {uart_tx uart_rx}] # 异步复位信号的约束如果有时钟 set_property ASYNC_REG TRUE [get_cells {sync_reg0 sync_reg1}] ;# 对同步器寄存器进行优化约束对于异步的UART RX输入信号建议在约束文件中添加set_false_path约束避免不必要的时序报错。6.3 仿真验证与调试技巧在综合实现之前必须进行充分的仿真。编写一个Testbench模拟上位机向UART发送数据并检查FPGA接收是否正确同时让FPGA发送数据检查Testbench接收是否正确。常见仿真问题与排查发送数据正确但接收端收不到或乱码首先检查波特率是否一致。用仿真工具测量baud_tick的实际周期看是否与理论值相符。其次检查起始位检测逻辑确保在下降沿后正确启动了接收状态机。接收数据不稳定偶尔出错在Testbench中尝试给RX信号加入轻微抖动#delay。检查接收模块的采样点是否在数据位中心。如果未做多数表决考虑加入这个逻辑。FIFO溢出在Testbench中制造数据洪流观察FIFO的full信号是否被正确拉高以及你的控制逻辑是否在FIFO满时停止写入。一个实用的调试技巧是在设计中添加一个ILA集成逻辑分析仪IP核将uart_txuart_rxbaud_tick 接收状态机信号FIFO的空满标志等关键信号抓取出来下载到板子上进行实时调试。这比仿真更接近真实情况。6.4 综合、实现与生成比特流完成仿真后运行综合Synthesis。检查综合报告中的警告特别是关于未连接的端口或时序问题的警告。对于UART这类低速接口时序通常很容易满足。综合通过后运行实现Implementation包括布局布线。完成后必须查看时序报告确认没有建立时间Setup Time或保持时间Hold Time违例。尽管UART接口本身是异步的但FPGA内部处理这些信号的逻辑如状态机、计数器是同步电路必须满足时序。最后生成比特流文件Generate Bitstream。7. 上板验证与常见硬件问题排查7.1 连接与硬件检查将比特流下载到FPGA开发板。使用一根USB转串口线如CH340 CP2102 FT232等连接电脑和FPGA板。注意电平匹配确保FPGA Bank的IO电压与USB转串口模块的电平兼容通常是3.3V LVCMOS。不匹配会损坏设备。交叉连接FPGA的TX应连接USB模块的RX FPGA的RX连接USB模块的TX。GND必须连接。驱动安装在电脑上安装正确的USB转串口芯片驱动。打开串口调试助手如Putty Tera Term 或国产的XCOM SSCOM选择正确的COM口设置与FPGA程序完全一致的波特率、数据位、停止位、校验位。7.2 问题排查速查表现象可能原因排查步骤串口调试助手无法打开COM口驱动未安装或冲突线缆损坏端口被其他程序占用。检查设备管理器端口识别换USB口重启电脑关闭可能占用串口的软件如旧版Vivado硬件服务器。能打开COM口但收发无任何数据FPGA程序未运行引脚约束错误电平不匹配硬件连接错误。1. 确认比特流已下载。2. 用万用表测量FPGA的TX引脚在空闲时应为高电平3.3V。3. 检查约束文件中的引脚号是否与实际硬件连接一致。4. 将FPGA的TX和RX短接自发自收测试。发送数据给FPGAFPGA无反应收不到FPGA接收模块波特率错误起始位检测逻辑故障同步器或亚稳态问题。1. 用ILA抓取RX引脚信号和内部baud_tick看信号是否进入FPGA以及波特率时钟是否正确。2. 检查接收状态机是否被起始位触发。3. 在代码中增加一个“心跳灯”当接收到任何字节时让LED闪烁快速验证数据通路是否进入FPGA。FPGA发送数据电脑端收到乱码波特率不匹配最常见停止位/校验位设置不一致发送模块时序错误。1.双倍确认电脑端和FPGA代码中的波特率是否精确一致。计算分频系数考虑时钟误差。2. 用示波器或逻辑分析仪测量FPGA的TX引脚波形测量一个比特的实际时长反推实际波特率。3. 检查串口调试助手的参数设置。通信一段时间后出错或死机FIFO深度不足导致溢出抗干扰能力差电源噪声。1. 监控FIFO的满信号。2. 为RX信号在PCB上增加串联电阻如22欧姆和下拉/上拉电阻增强信号完整性。3. 检查电源纹波。4. 在代码中增加超时复位机制防止状态机卡死。7.3 性能优化与扩展思路一个基础的UART通信框架搭建完成后可以考虑以下优化和扩展自动波特率检测让接收模块能够自动识别发送端的波特率提高兼容性。硬件流控增加RTS/CTS信号线实现更可靠的数据流控制防止FIFO溢出。DMA集成如果FPGA与处理器如ARM Cortex通过AXI总线互联可以将UART的FIFO与DMA控制器连接实现数据块的不经CPU干预的直接搬运极大提升效率。多字节协议封装在UART字节流之上定义自己的应用层协议如增加帧头、帧尾、长度、校验和等实现数据包的有序和可靠传输。调试UART的过程是对数字逻辑设计、时序分析、硬件调试的综合训练。最深刻的体会是仿真环境再完美也无法完全模拟真实的物理世界。示波器、逻辑分析仪和ILA这些工具是连接代码与硬件的桥梁。当你第一次在串口调试助手上看到FPGA如期回传数据时那种对底层硬件掌控的确信感是单纯软件编程无法给予的。
返回列表