尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

FPGA UART通信模块设计:从状态机原理到Vivado实战实现

FPGA UART通信模块设计:从状态机原理到Vivado实战实现 1. 项目概述从零构建FPGA上的UART通信链路在FPGA开发中实现与外部世界的通信是一个基础且高频的需求。UART通用异步收发传输器因其协议简单、硬件资源占用少、几乎所有的微控制器和PC都支持成为了FPGA与上位机如PC、微控制器或其他外设进行数据交换的首选“桥梁”。这次我们就来深入聊聊如何在Xilinx的Vivado设计套件中从零开始实现一个稳定可靠的UART数据发送与接收模块。很多新手可能会觉得UART不就是发个0x55、收个0xAA吗直接用IP核或者网上找个代码不就行了但实际踩过坑的同行都知道事情远没这么简单。波特率偏差导致的数据错乱、起始位/停止位的误判、跨时钟域带来的亚稳态问题以及如何设计一个健壮的收发状态机这些才是决定你的串口通信是“玩具”还是“工具”的关键。本文将围绕一个完整的UART模块设计拆解其核心原理、Vivado中的实现步骤、仿真验证方法并分享那些在官方文档里不会写的调试经验和避坑指南。无论你是正在学习FPGA的学生还是需要快速实现串口功能的工程师这篇从实战中总结的内容都能给你提供一条清晰的路径。2. UART通信核心原理与FPGA实现思路2.1 UART协议深度解析不止是0和1UART通信的本质是一种异步、串行、全双工的通信方式。说它“异步”是因为通信双方没有统一的时钟线完全依靠预先约定好的波特率Baud Rate来同步每一位数据的采样时刻。这就好比两个人约好每隔一秒说一个字虽然没有秒表对时但只要各自数秒数得准就能完成对话。一个完整的UART数据帧通常由以下几部分组成起始位Start Bit总是逻辑‘0’标志着数据帧的开始。它起到一个“唤醒”接收方的作用通知接收方“注意数据要来了”数据位Data Bits紧接起始位之后通常是5到9位最常见的是8位代表一个字节的数据。传输顺序是从最低有效位LSB开始。校验位Parity Bit可选位用于简单的错误检测。可以是奇校验、偶校验或无校验。停止位Stop Bit总是逻辑‘1’标志着数据帧的结束。可以是1位、1.5位或2位。它给接收方留出处理当前帧和准备接收下一帧的时间。在FPGA中实现UART核心任务就是设计两个独立的模块发送器TX和接收器RX。发送器需要将并行数据如一个8位寄存器按照协议转换成串行比特流输出接收器则需要从串行比特流中准确地识别出起始位并在正确的时刻采样数据位最终还原成并行数据。注意波特率如115200指的是每秒传输的符号数包括起始位、数据位、校验位、停止位。而比特率是每秒传输的二进制位数。对于常见的8N1格式8数据位、无校验、1停止位一个10位的帧传输一个字节此时比特率是波特率的10倍。但在讨论UART时我们通常混用这两个概念指的都是那个关键的速率参数。2.2 FPGA实现方案选型状态机 vs. 移位寄存器实现UART收发逻辑主要有两种主流思路各有优劣方案一基于状态机的经典实现这是最直观、最可控的方式。发送和接收都被设计成独立的状态机。发送状态机TX_FSM通常包含IDLE空闲、START发送起始位、DATA依次发送数据位、PARITY发送校验位可选、STOP发送停止位等状态。状态转移由波特率时钟一个比系统时钟慢得多的分频时钟或计数器控制。接收状态机RX_FSM包含IDLE等待起始位、START确认起始位、DATA采样数据位、PARITY检查校验位、STOP确认停止位等状态。关键在于起始位的检测通常采用“过采样”技术例如16倍波特率时钟来抗干扰在起始位下降沿的中点附近多次采样确认为真起始位后才进入数据接收状态。方案二基于移位寄存器的简洁实现对于发送可以直接用一个移位寄存器。加载数据后在波特率时钟控制下循环右移将最低位输出即可。对于接收同样使用一个移位寄存器在确定的采样点将串行输入数据移入。这种方式代码更简洁但错误处理和鲁棒性通常不如状态机方案完善。为什么我推荐状态机方案在复杂的工业环境或长距离通信中线路上的毛刺和干扰不可避免。状态机方案特别是接收状态机配合过采样逻辑可以极大地提高抗干扰能力。例如在检测起始位时不是看到一下降沿就确认而是在理论采样点附近连续采样3次如果多数为0才认为是有效的起始位这能有效滤除窄脉冲干扰。这种细节控制在移位寄存器方案中难以优雅地实现。因此下文将围绕状态机方案展开这是构建工业级可靠UART模块的基石。3. Vivado中的UART模块设计与关键参数3.1 顶层模块接口与时钟架构设计首先我们需要定义UART模块对外的接口。一个典型的UART顶层模块比如命名为uart_top的端口可能如下module uart_top #( parameter CLK_FREQ 100_000_000, // 系统时钟频率单位Hz parameter BAUD_RATE 115200 // 波特率 )( input wire clk, // 系统时钟 input wire rst_n, // 低电平复位 // UART物理接口 input wire uart_rxd, // 串行数据接收 output wire uart_txd, // 串行数据发送 // 用户逻辑接口 - 发送侧 input wire [7:0] tx_data_i, // 待发送的并行数据 input wire tx_valid_i, // 发送数据有效信号 output wire tx_ready_o, // 发送模块就绪信号可接收新数据 // 用户逻辑接口 - 接收侧 output wire [7:0] rx_data_o, // 接收到的并行数据 output wire rx_valid_o, // 接收数据有效脉冲 output wire rx_error_o // 接收错误指示如帧错误 );时钟架构是核心系统主时钟clk如100MHz频率远高于波特率时钟。我们需要生成一个波特率时钟或使能信号来驱动UART的位时序。通常不直接分频出一个低频时钟而是生成一个波特率周期脉冲baud_tick作为状态机或计数器的使能信号。这样设计更节省资源且易于同步。计算波特率分频系数的公式为BAUD_DIV CLK_FREQ / BAUD_RATE例如100MHz系统时钟115200波特率BAUD_DIV 100_000_000 / 115200 ≈ 868这意味着每868个系统时钟周期对应一个UART位的时间宽度。我们用一个计数器从0数到BAUD_DIV-1溢出时产生一个baud_tick脉冲。实操心得分频系数最好做成参数并且检查其是否为整数。CLK_FREQ和BAUD_RATE的整除关系决定了波特率是否精确。如果不能整除就会产生累积误差。对于高速波特率或长时间通信这种误差可能导致数据错位。一种改进方法是使用分数分频或DCM/PLL生成更精确的时钟但对于大多数应用只要误差在±3%以内UART协议允许范围整数分频即可接受。3.2 发送模块TX详细设计与状态机发送模块的任务是当用户逻辑给出tx_valid_i信号且模块空闲tx_ready_o1时将tx_data_i按照UART帧格式串行化输出。发送状态机设计假设8N1格式无校验位IDLE状态输出uart_txd为高电平空闲状态。tx_ready_o信号拉高表示可以接收新数据。当检测到tx_valid_i有效时锁存输入数据并跳转到START状态。START状态输出起始位0并启动一个位定时计数器基于baud_tick。计数器满后跳转到DATA状态并初始化一个位索引计数器bit_idx为0。DATA状态根据bit_idx从锁存的数据中取出相应的位从LSB开始输出到uart_txd。每过一个位时间baud_tickbit_idx加1。当bit_idx计数到数据位宽度如8时跳转到STOP状态。STOP状态输出停止位1。同样等待一个位时间。结束后跳转回IDLE状态并再次拉高tx_ready_o。关键代码段示意Veriloglocalparam IDLE 2b00; localparam START 2b01; localparam DATA 2b10; localparam STOP 2b11; reg [1:0] tx_state; reg [7:0] tx_data_latch; reg [2:0] tx_bit_idx; reg tx_baud_tick; always (posedge clk or negedge rst_n) begin if (!rst_n) begin tx_state IDLE; uart_txd 1b1; tx_ready_o 1b1; end else if (tx_baud_tick) begin // 在波特率时钟使能点处理状态转移 case (tx_state) IDLE: begin if (tx_valid_i) begin tx_data_latch tx_data_i; tx_state START; tx_ready_o 1b0; end end START: begin uart_txd 1b0; // 输出起始位 tx_bit_idx 0; tx_state DATA; end DATA: begin uart_txd tx_data_latch[tx_bit_idx]; if (tx_bit_idx 7) begin tx_state STOP; end else begin tx_bit_idx tx_bit_idx 1; end end STOP: begin uart_txd 1b1; // 输出停止位 tx_state IDLE; tx_ready_o 1b1; end endcase end end注意事项tx_ready_o信号的设计至关重要。它必须在模块真正空闲IDLE状态且准备好接收新数据时才为高。一种常见的错误是在STOP状态末尾就拉高tx_ready_o如果此时用户逻辑恰好送来新数据而当前帧的停止位还没发送完就会导致数据冲突。安全的做法是在回到IDLE状态后再拉高就绪信号。3.3 接收模块RX详细设计与过采样技术接收模块是UART实现的难点其核心挑战是如何在异步的串行数据流中准确地找到每一位数据的中心点进行采样以避开信号边沿的不稳定区域。过采样Oversampling技术是解决这一问题的标准方法。通常使用16倍波特率时钟进行采样。即系统内部生成一个频率为16 * BAUD_RATE的采样时钟sample_tick。对于每一位数据我们都有16次采样机会我们选择中间的第7、8、9次或附近采样值的多数表决结果作为该位的最终值这能有效抵抗毛刺。接收状态机设计16倍过采样8N1格式IDLE状态持续监视uart_rxd线。当检测到下降沿从1到0时启动一个计数器并进入START_DET状态。这个下降沿可能是起始位的开始也可能是噪声。START_DET状态等待7.5个采样周期即计数器计数到7*16 8 120如果从0开始计数。为什么是7.5目的是让采样点对准起始位的正中央。到达这个点后对uart_rxd采样如果仍然是低电平则确认是有效的起始位跳转到DATA状态并重置位内采样计数器如果是高电平则认为是噪声跳回IDLE。DATA状态在这个状态每16个采样周期一个位时间前进一位数据。在每个位时间的中心点例如第7、8、9次采样点进行采样通过多数表决确定该数据位的值并存入移位寄存器。重复此过程直到收满8位数据。STOP状态在停止位的中心点进行采样预期应为高电平。如果采样到低电平则可能发生了帧错误可以置位rx_error_o标志。无论停止位是否正确都产生rx_valid_o脉冲输出接收到的数据并跳转回IDLE状态。关键代码段示意采样点生成与数据捕获localparam OVERSAMPLE_RATE 16; reg [3:0] sample_cnt; // 0-15 位内采样计数器 reg [2:0] bit_cnt; // 0-7 数据位计数器 reg [7:0] rx_shift_reg; reg [7:0] rx_data_buf; always (posedge clk or negedge rst_n) begin if (!rst_n) begin sample_cnt 0; bit_cnt 0; rx_state IDLE; end else if (sample_tick) begin // 16倍波特率采样时钟 sample_cnt sample_cnt 1; case (rx_state) IDLE: begin if (!uart_rxd) begin // 检测到下降沿 sample_cnt 0; rx_state START_DET; end end START_DET: begin if (sample_cnt 4d7) begin // 在起始位中点附近采样 if (!uart_rxd) begin // 确认起始位 sample_cnt 0; bit_cnt 0; rx_state DATA; end else begin rx_state IDLE; // 噪声丢弃 end end end DATA: begin // 在每个数据位的中心点如sample_cnt7采样 if (sample_cnt 4d7) begin rx_shift_reg[bit_cnt] uart_rxd; // 采样并存入 if (bit_cnt 3d7) begin rx_state STOP; end else begin bit_cnt bit_cnt 1; end end // 每16个sample_ticksample_cnt会溢出归零自动进入下一位的计时 end STOP: begin if (sample_cnt 4d7) begin rx_data_buf rx_shift_reg; // 锁存数据 rx_valid_o 1b1; // 产生有效脉冲 if (!uart_rxd) rx_error_o 1b1; // 帧错误检测 rx_state IDLE; end end endcase end else begin rx_valid_o 1b0; // 将有效脉冲拉低确保只持续一个时钟周期 rx_error_o 1b0; end end实操心得过采样的倍数16倍是业界常用值它提供了足够的采样点来抗抖动同时分频系数也不会太大。sample_tick的生成同样基于系统时钟分频。另外rx_valid_o必须是脉冲信号仅持续一个系统时钟周期用于通知上游逻辑“数据已就绪请取走”。如果设计成电平信号上游逻辑可能无法正确捕获数据变更事件。4. 在Vivado中集成、仿真与上板调试全流程4.1 创建工程、编写代码与添加约束创建Vivado工程启动Vivado选择创建新项目。指定项目名称和位置选择“RTL Project”类型。在添加源文件步骤可以先跳过后续再添加。在选择器件步骤根据你的FPGA开发板型号选择正确的芯片型号如Xilinx Artix-7系列的xc7a35tftg256-1等。编写源代码在“Sources”窗口右键点击“Design Sources”选择“Add Sources” - “Add or create design sources”。创建新的Verilog或VHDL文件将我们设计的uart_top、uart_tx、uart_rx等模块代码写入并保存。建议将发送和接收模块分开为独立文件uart_tx.v,uart_rx.v便于管理和复用。编写仿真测试文件同样在“Sources”窗口右键点击“Simulation Sources”创建测试平台文件如tb_uart_top.v。在测试平台中需要实例化UART顶层模块。生成系统时钟和复位信号。模拟上位机行为周期性地通过uart_rxd发送数据给FPGA的接收模块。驱动FPGA的发送模块在tx_ready_o有效时给出tx_valid_i和tx_data_i。使用$display或$monitor语句在控制台打印发送和接收的数据便于观察。编写引脚约束文件XDC这是连接逻辑端口与物理引脚的关键。在“Sources”窗口右键点击“Constraints”创建或添加XDC文件。你需要根据开发板原理图找到连接USB-UART芯片如CH340、CP2102、FT232的FPGA引脚。通常需要约束两个引脚# 假设开发板上UART_TX连接了FPGA的A18引脚UART_RX连接了B18引脚 set_property PACKAGE_PIN A18 [get_ports uart_txd] set_property IOSTANDARD LVCMOS33 [get_ports uart_txd] set_property PACKAGE_PIN B18 [get_ports uart_rxd] set_property IOSTANDARD LVCMOS33 [get_ports uart_rxd]此外还需要约束系统时钟和复位按键的引脚。4.2 行为仿真与时序仿真验证运行行为仿真Behavioral Simulation在“Flow Navigator”中点击“Run Simulation” - “Run Behavioral Simulation”。Vivado会编译设计并打开Simulation窗口。在波形窗口添加需要观察的信号如uart_txd,uart_rxd,tx_data_i,tx_valid_i,rx_data_o,rx_valid_o, 各种状态机状态等。运行仿真一段时间。你应该能看到当tx_valid_i脉冲到来时uart_txd上出现一个完整的UART帧起始位08位数据停止位1。当你在测试平台中模拟的uart_rxd线上发送数据时rx_data_o会在rx_valid_o脉冲出现时更新为正确的值。重点检查波特率是否正确数据位顺序LSB first对吗tx_ready_o和rx_valid_o的时序是否符合预期进行综合与实现后仿真Post-Implementation Timing Simulation行为仿真通过后依次点击“Run Synthesis”和“Run Implementation”。实现完成后运行“Run Simulation” - “Run Post-Implementation Timing Simulation”。这时仿真会加入布局布线后的真实延迟信息。这个步骤非常重要它能暴露潜在的时序问题比如建立/保持时间违例。对于UART这种低速接口时序问题可能不突出但养成这个习惯对做高速逻辑设计至关重要。检查在加入延迟后接收模块的过采样逻辑是否依然能稳定捕捉数据。4.3 生成比特流、上板与联机调试生成比特流在Implementation完成后点击“Generate Bitstream”。Vivado会综合、布局布线并生成最终的.bit文件。连接硬件与下载用USB线连接开发板与PC。确保已安装好板载USB-UART芯片的驱动如CH340驱动在设备管理器中能看到对应的COM口。在Vivado中点击“Open Hardware Manager”连接开发板然后“Program device”下载比特流文件。使用串口调试助手验证打开串口调试助手如XCOM、SecureCRT、Putty等。选择正确的COM口设置波特率、数据位、停止位、校验位与FPGA代码中设置完全一致。发送测试在串口调试助手中发送一个字节如0x55。在Vivado的ILA集成逻辑分析仪中抓取FPGA内部rx_data_o和rx_valid_o信号看是否收到0x55并产生有效脉冲。如果没有ILA可以设计一个简单的逻辑将接收到的数据通过LED灯以二进制形式显示出来。接收测试在FPGA代码中编写一个测试逻辑例如上电后每隔1秒通过UART发送一个递增的计数器值。在串口调试助手中查看是否收到连续且正确的数据。环路测试Loopback Test最彻底的测试是将FPGA的uart_txd和uart_rxd在板上短接或者通过代码内部直接连接。然后让FPGA发送数据同时自己接收。比较发送和接收的数据是否一致。这能同时验证TX和RX模块的功能。5. 常见问题、调试技巧与性能优化5.1 典型问题排查速查表现象可能原因排查思路与解决方案上位机收不到任何数据1. 引脚约束错误。2. 波特率不匹配。3. TX线未正确输出始终为高。4. 发送模块未启动。1. 用示波器或逻辑分析仪测量uart_txd引脚看是否有波形。如果没有检查XDC文件、代码中的端口名是否一致。2. 用示波器测量位时间计算实际波特率与代码中设定的理论值对比。3. 检查发送状态机是否卡在IDLE状态tx_valid_i信号是否有效。4. 检查复位信号是否有效释放。上位机能收到数据但全是乱码1. 波特率偏差过大3%。2. 数据位、停止位、校验位设置与上位机不匹配。3. 采样点偏差接收端问题。1. 精确计算分频系数检查系统时钟频率输入参数CLK_FREQ是否正确。2. 确认代码中的帧格式如8N1与串口调试助手设置完全一致。3. 检查接收模块的过采样逻辑特别是起始位检测的中点计算和位内采样点是否准确。FPGA接收不到数据或数据错误1. RX引脚约束错误或连接问题。2. 接收状态机起始位检测逻辑太敏感或太迟钝。3. 跨时钟域问题如果rx_valid_o被用于其他时钟域。1. 测量uart_rxd引脚是否有来自上位机的信号。2. 在仿真中仔细观察起始位检测过程看是否被噪声误触发或漏掉了真正的起始位。可以适当调整起始位确认的采样次数如多数表决。3. 对rx_valid_o和rx_data_o使用双触发器同步器后再送给其他时钟域的逻辑。通信一段时间后出错1. 波特率累积误差导致采样点漂移。2. 发送/接收FIFO溢出如果使用了FIFO。3. 复位或电源不稳定。1. 对于长时间通信考虑使用更精确的时钟源如PLL生成专用波特率时钟。2. 检查FIFO的深度是否足够tx_ready_o和rx_valid_o握手信号是否被正确处理避免数据丢失。3. 检查电源纹波确保复位电路可靠。5.2 高级调试技巧与性能优化使用ILA进行在线调试Vivado的ILAIntegrated Logic Analyzer是调试FPGA的利器。将UART模块的关键信号状态机状态、tx_data_i、rx_data_o、tx_ready_o、rx_valid_o、内部计数器等添加到ILA核中。上电后可以设置触发条件如rx_valid_o上升沿实时捕获这些信号的变化其直观性远胜于仿真。这对于排查间歇性错误尤其有效。添加FIFO缓冲数据在用户逻辑接口与UART收发模块之间加入FIFO先入先出存储器是一个最佳实践。发送侧用户逻辑将数据写入TX FIFOUART发送模块从FIFO中读取数据。这样用户逻辑无需等待UART缓慢的串行化过程提高了系统吞吐率。接收侧UART接收模块将数据写入RX FIFO用户逻辑从FIFO中读取。这样即使上位机突发发送大量数据也不会因为用户逻辑来不及处理而丢失。Vivado提供了FIFO Generator IP核可以方便地配置同步或异步FIFO。参数化设计提升复用性将系统时钟频率、波特率、数据位宽、停止位数量、校验位类型等都设计为模块参数。这样同一个UART模块只需在实例化时修改参数就能灵活适配不同的项目和需求代码复用率极高。应对高速波特率当波特率很高如3Mbps以上时一个位时间对应的系统时钟周期数很少。此时状态机中基于baud_tick的计数器可能因为逻辑延迟而出错。可以考虑使用“基于系统时钟周期的计数器”来代替baud_tick使能信号在每个系统时钟周期都判断计数器是否达到目标值并进行状态转移这样对时序更友好。使用RS-232电平还是TTL电平务必清楚你的硬件连接。大多数FPGA开发板上的USB-UART芯片如CH340输出的是TTL电平0V表示逻辑03.3V表示逻辑1直接与FPGA的IO口连接。而传统的PC串口DB9接口使用的是RS-232电平3V至15V表示逻辑0-3V至-15V表示逻辑1两者不能直接连接需要电平转换芯片如MAX3232。
返回列表