ARTICLE DETAIL

资讯详情

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

FPGA参数化UART模块设计:Verilog实现与工程实践指南

FPGA参数化UART模块设计:Verilog实现与工程实践指南 1. 项目概述为什么我们需要一个参数化的UART模块在FPGA开发中UART通用异步收发传输器几乎是每个项目都会用到的“基础设施”。无论是与上位机调试、连接传感器还是作为简单的控制接口UART都因其简单、可靠而备受青睐。然而很多开发者尤其是初学者常常陷入一个误区从网上复制一段固定的UART代码修改几个波特率参数就匆匆用在自己的项目里。一旦项目需求变更比如波特率从9600改为115200或者数据位从8位改为9位甚至需要添加奇偶校验就得重新翻出代码小心翼翼地修改多处硬编码的常量稍有不慎就会引入时序错误或功能缺陷。这就是为什么我们需要一个参数化、高通用性的UART模块。这个项目的核心不仅仅是实现UART通信更是要构建一个像“乐高积木”一样可以灵活配置的Verilog组件。通过将波特率、数据位、停止位、奇偶校验等关键参数“传参化”我们可以在实例化模块时像调用函数一样指定这些配置从而实现代码的“一次编写多处复用”。这对于团队协作、项目迭代和快速原型开发至关重要。想象一下你手头有一个经过充分验证的、支持多种配置的UART核在新项目中你只需要在顶层模块里改几行参数就能立刻获得一个稳定可靠的串口这能节省多少调试和验证的时间本文将手把手带你从UART协议的本质出发深入理解其时序逻辑然后重点讲解如何用Verilog实现一个完全参数化的UART收发器。我们会把模块的通用性做到极致并分享在快速开发流程中集成和使用这个模块的实战技巧与避坑指南。无论你是FPGA新手还是希望优化自己代码库的资深工程师这篇文章都将提供可直接“抄作业”的解决方案。2. UART协议核心与参数化设计要点在动手写代码之前我们必须吃透UART协议的精髓并明确哪些部分是需要被参数化的。UART是一种异步串行通信协议这意味着通信双方没有统一的时钟信号完全依靠预先约定好的波特率来同步数据。2.1 UART帧格式的拆解与参数映射一个标准的UART数据帧包含以下几个部分起始位Start Bit总是1个逻辑‘0’标志着数据帧的开始。数据位Data Bits承载实际数据通常是5-9位。最常见的是8位。奇偶校验位Parity Bit可选用于简单的错误检测。可以是奇校验、偶校验或无校验。停止位Stop Bit(s)总是1个、1.5个或2个逻辑‘1’标志着数据帧的结束。为了实现通用性我们必须将上述所有可变部分都设计成模块参数Parameter。在Verilog中我们使用parameter关键字来定义。一个设计良好的参数化UART模块其头部可能看起来像这样module uart_core #( parameter CLK_FREQ 50_000_000, // 系统时钟频率单位Hz parameter BAUD_RATE 115200, // 波特率 parameter DATA_BITS 8, // 数据位宽5-9 parameter STOP_BITS 1, // 停止位数1, 1.5, 2 parameter PARITY_MODE 0 // 奇偶校验模式0-无1-奇校验2-偶校验 )( input wire clk, input wire rst_n, // ... 其他端口 );为什么这样设计参数CLK_FREQ和BAUD_RATE是计算波特率分频器的基石。分频系数BAUD_DIV CLK_FREQ / BAUD_RATE。这里有一个关键点这个除法可能得不到整数。因此我们的设计必须能处理非整数分频通常采用累加器或高精度计数器来逼近目标波特率这是精度和资源消耗的权衡点。DATA_BITS,STOP_BITS,PARITY_MODE直接决定了状态机跳转和数据处理的逻辑。例如当DATA_BITS9时内部移位寄存器就需要是9位宽当STOP_BITS2时接收状态机需要在停止位状态停留两倍的时间。2.2 波特率生成精度与稳定性的博弈波特率生成是UART稳定性的核心。最简单的方法是使用计数器当计数器计满BAUD_DIV个系统时钟时产生一个波特率时钟使能信号baud_en而不是直接生成一个新的时钟域。绝对不要在模块内部用分频产生一个新的时钟信号如baud_clk去驱动逻辑这会引起跨时钟域问题增加设计复杂性。正确的做法是使用使能信号让所有状态转移和采样都在主时钟clk的上升沿且仅在baud_en有效时进行。对于BAUD_DIV不是整数的情况例如CLK_FREQ50MHz,BAUD_RATE115200理论分频值约为434.03。我们可以采用以下两种方法四舍五入取整BAUD_DIV 434。这会导致约0.007%的误差在短距离、低速率通信中通常可接受。分数分频累加器使用一个累加器每次累加目标波特率对应的时钟周期分数值当累加器溢出时产生baud_en。这种方法精度更高但逻辑稍复杂。在我们的通用模块中可以提供一个参数BAUD_DIV的计算函数或者让用户直接输入计算好的整数值以兼顾灵活性和简便性。3. 参数化UART收发器的Verilog实现详解有了清晰的设计蓝图我们就可以开始构建收发两个核心状态机了。我们将采用经典的“状态机 移位寄存器”架构并确保每一个环节都与我们的参数绑定。3.1 发送器UART_TX的状态机设计与代码发送器的任务是将并行数据组装成符合参数的串行帧。其状态机可以设计为以下几个状态IDLE空闲、START发送起始位、DATA发送数据位、PARITY发送校验位、STOP发送停止位。// 发送器部分关键逻辑 localparam S_IDLE 0; localparam S_START 1; localparam S_DATA 2; localparam S_PARITY 3; localparam S_STOP 4; reg [2:0] tx_state; reg [3:0] bit_index; // 用于计数已发送的数据位 reg [DATA_BITS-1:0] tx_data_reg; reg tx_parity_bit; always (posedge clk or negedge rst_n) begin if (!rst_n) begin tx_state S_IDLE; txd 1‘b1; // 空闲时为高电平 // ... 其他寄存器复位 end else if (baud_en) begin // 关键只在波特率使能时动作 case (tx_state) S_IDLE: begin if (tx_start) begin // 收到发送请求 tx_state S_START; tx_data_reg tx_data; // 锁存待发送数据 bit_index 0; txd 1‘b0; // 起始位 end end S_START: begin tx_state S_DATA; txd tx_data_reg[0]; // 发送LSB bit_index 1; end S_DATA: begin if (bit_index DATA_BITS) begin txd tx_data_reg[bit_index]; bit_index bit_index 1; end else begin bit_index 0; if (PARITY_MODE ! 0) begin tx_state S_PARITY; // 根据PARITY_MODE计算奇偶校验位 tx_parity_bit ^tx_data_reg; // 奇校验示例 if (PARITY_MODE 2) tx_parity_bit ~^tx_data_reg; // 偶校验 end else begin tx_state S_STOP; end end end S_PARITY: begin txd tx_parity_bit; tx_state S_STOP; end S_STOP: begin txd 1‘b1; if (stop_bit_counter STOP_BITS) begin // 停止位计数器 tx_state S_IDLE; tx_done 1‘b1; // 发送完成标志 end end endcase end end关键实现细节与通用性技巧位序处理UART协议规定先发送最低有效位LSB。所以我们在S_DATA状态时是从tx_data_reg[0]开始依次移出。参数化控制流注意S_DATA状态中判断条件是bit_index DATA_BITS。当DATA_BITS参数改变时这个逻辑自动适应。同样在数据位发送完毕后通过if (PARITY_MODE ! 0)来决定是否跳转到校验位状态。停止位处理STOP_BITS可能是1、1.5或2。对于1.5和2我们需要一个计数器stop_bit_counter在S_STOP状态累加累加值基于BAUD_DIV进行换算。例如对于1.5个停止位计数器需要累加1.5倍的BAUD_DIV周期。这通常通过一个两倍于波特率时钟的计数器来实现。3.2 接收器UART_RX的同步、采样与抗抖动接收器比发送器更复杂因为它需要从异步的串行线中可靠地恢复出数据和时钟。核心挑战是起始位检测和数据位中心采样。// 接收器部分关键逻辑简化版 localparam RX_IDLE 0; localparam RX_START 1; localparam RX_DATA 2; localparam RX_PARITY 3; localparam RX_STOP 4; reg [2:0] rx_state; reg [3:0] sample_counter; // 波特率周期计数器 reg [3:0] bit_index_rx; reg [DATA_BITS-1:0] rx_shift_reg; reg rx_parity_check; // 关键对输入信号rxd进行同步和滤波防止亚稳态和毛刺 reg [1:0] rxd_sync; always (posedge clk or negedge rst_n) begin if (!rst_n) rxd_sync 2‘b11; else rxd_sync {rxd_sync[0], rxd}; end wire rxd_filtered (rxd_sync[1] rxd_sync[0]); // 简单的多数表决滤波 always (posedge clk or negedge rst_n) begin if (!rst_n) begin rx_state RX_IDLE; // ... end else begin case (rx_state) RX_IDLE: begin if (rxd_filtered 1‘b0) begin // 检测到潜在起始位下降沿 sample_counter 0; rx_state RX_START; end end RX_START: begin if (baud_en) begin sample_counter sample_counter 1; // 在起始位理论中心点例如第BAUD_DIV/2个采样点再次确认 if (sample_counter (BAUD_DIV/2)) begin if (rxd_filtered 1‘b0) begin // 确认是起始位不是毛刺 rx_state RX_DATA; sample_counter 0; bit_index_rx 0; end else begin rx_state RX_IDLE; // 是毛刺回到空闲 end end end end RX_DATA: begin if (baud_en) begin sample_counter sample_counter 1; // 在每个数据位的中心点采样 if (sample_counter (BAUD_DIV/2)) begin rx_shift_reg[bit_index_rx] rxd_filtered; bit_index_rx bit_index_rx 1; if (bit_index_rx DATA_BITS-1) begin if (PARITY_MODE ! 0) begin rx_state RX_PARITY; end else begin rx_state RX_STOP; end sample_counter 0; end end end end // ... PARITY 和 STOP 状态处理类似 endcase end end接收器设计的核心经验同步与滤波Synchronizer and Deglitch异步的rxd信号必须经过两级触发器同步以消除亚稳态。同步后最好再进行简单的滤波如判断连续2-3个周期值相同以抵抗线缆上的毛刺。这是工程稳定性的基石很多“时好时坏”的通信问题都源于此。起始位确认在RX_IDLE检测到下降沿后不要立刻认为起始位开始。应该等待到起始位的理论中心点例如波特率周期的中点再次采样如果仍是低电平才确认是有效的起始位。这能有效避免噪声引起的误触发。数据位中心采样这是提高抗干扰能力的关键。在RX_DATA状态我们不是在每个波特率周期的开始就采样而是等到周期中心BAUD_DIV/2再采样。此时信号最稳定。参数化集成和发送器一样DATA_BITS和PARITY_MODE参数无缝地集成在状态判断和位计数逻辑中。4. 模块集成、封装与快速开发工作流实现独立的TX和RX模块后我们需要将它们封装成一个完整的、接口简洁的UART顶层模块并思考如何将其融入高效的FPGA开发流程。4.1 顶层模块封装与FIFO缓冲一个实用的UART顶层模块通常包含以下部分实例化TX和RX子模块。波特率生成器根据CLK_FREQ和BAUD_RATE生成公用的baud_en。FIFO先进先出缓冲区这是提升易用性的关键。发送FIFO允许用户连续写入多个字节的数据而无需等待每个字节发送完成接收FIFO则缓存接收到的数据供用户随时读取。FIFO的深度也是一个可参数化的选项如FIFO_DEPTH。简洁的用户接口如tx_data,tx_wr_en,tx_full,rx_data,rx_rd_en,rx_empty等。module uart_top #( parameter CLK_FREQ 50_000_000, parameter BAUD_RATE 115200, parameter DATA_BITS 8, parameter STOP_BITS 1, parameter PARITY_MODE 0, parameter TX_FIFO_DEPTH 16, parameter RX_FIFO_DEPTH 16 )( input wire clk, input wire rst_n, // 用户接口 input wire [DATA_BITS-1:0] tx_data_i, input wire tx_wr_en_i, output wire tx_full_o, output wire tx_busy_o, output wire [DATA_BITS-1:0] rx_data_o, input wire rx_rd_en_i, output wire rx_empty_o, // 物理接口 output wire txd, input wire rxd ); // 内部信号定义 wire baud_en; wire [DATA_BITS-1:0] tx_fifo_out; wire tx_fifo_rd_en, tx_fifo_empty; wire [DATA_BITS-1:0] rx_fifo_in; wire rx_fifo_wr_en; // 实例化波特率生成器 baud_gen #(.CLK_FREQ(CLK_FREQ), .BAUD_RATE(BAUD_RATE)) u_baud_gen(...); // 实例化发送FIFO fifo_sync #(.WIDTH(DATA_BITS), .DEPTH(TX_FIFO_DEPTH)) u_tx_fifo(...); // 实例化发送器核心 uart_tx_core #(.DATA_BITS(DATA_BITS), .STOP_BITS(STOP_BITS), .PARITY_MODE(PARITY_MODE)) u_tx_core(...); // 实例化接收器核心 uart_rx_core #(.DATA_BITS(DATA_BITS), .STOP_BITS(STOP_BITS), .PARITY_MODE(PARITY_MODE)) u_rx_core(...); // 实例化接收FIFO fifo_sync #(.WIDTH(DATA_BITS), .DEPTH(RX_FIFO_DEPTH)) u_rx_fifo(...); // 连接逻辑... endmodule4.2 在Vivado/VSCode中建立快速开发模板为了真正实现“快速开发”我们不能每次新建项目都复制粘贴文件。可以创建一个“UART IP核”模板。在Vivado中你可以将uart_top.v及其依赖的所有子模块uart_tx_core.v,uart_rx_core.v,baud_gen.v,fifo_sync.v打包成一个自定义IP。在IP封装器中设置好所有参数CLK_FREQ,BAUD_RATE等并生成一个AXI4-Lite接口如果需要或者保持简单的流接口。这样在新项目中你只需要从IP Catalog中拖出这个IP在图形界面中修改参数连线即可。在VSCode配合开源工具链如Verilator/Icarus中可以创建一个模块化的uart文件夹里面包含所有源文件和一个顶层的uart_pkg.svSystemVerilog或uart_defines.vhVerilog头文件用define或parameter定义默认参数。在新项目中直接将该文件夹作为子模块Submodule加入然后在顶层实例化时重载参数。4.3 参数重载与实例化示例在顶层设计文件中实例化我们的通用UART模块变得非常直观// 顶层设计文件 top.v module top( input wire sys_clk, input wire rst_n, output wire led, input wire uart_rxd, output wire uart_txd ); // 定义参数 localparam CLK_FREQ 50_000_000; localparam BAUD_115200 115200; localparam BAUD_9600 9600; // 实例化一个115200波特率8N1的UART用于调试 uart_top #( .CLK_FREQ(CLK_FREQ), .BAUD_RATE(BAUD_115200), .DATA_BITS(8), .STOP_BITS(1), .PARITY_MODE(0), .TX_FIFO_DEPTH(32), .RX_FIFO_DEPTH(32) ) u_uart_debug ( .clk(sys_clk), .rst_n(rst_n), .tx_data_i(debug_tx_data), .tx_wr_en_i(debug_tx_wr_en), .tx_full_o(debug_tx_full), .rx_data_o(debug_rx_data), .rx_rd_en_i(debug_rx_rd_en), .rx_empty_o(debug_rx_empty), .txd(uart_txd), .rxd(uart_rxd) ); // 实例化一个9600波特率7E1的UART用于连接某个老设备 uart_top #( .CLK_FREQ(CLK_FREQ), .BAUD_RATE(BAUD_9600), .DATA_BITS(7), .STOP_BITS(1), .PARITY_MODE(2), // 偶校验 .TX_FIFO_DEPTH(8), .RX_FIFO_DEPTH(8) ) u_uart_legacy ( .clk(sys_clk), .rst_n(rst_n), // ... 连接到其他逻辑 ); // ... 其他用户逻辑 endmodule通过这种方式你可以在一个设计中使用多个不同配置的UART实例而无需修改底层核心代码分毫真正做到了模块的通用性和复用性。5. 实测调试、常见问题与进阶优化代码写完了下载到FPGA里却发现串口助手上一片乱码或者没反应别急这是每个FPGA开发者的必经之路。下面分享一套系统的调试方法和常见坑点。5.1 调试第一步硬件与信号完整性检查在怀疑代码之前先排除硬件问题。电平匹配FPGA的IO通常是3.3V LVCMOS。你的USB转串口模块如CH340、CP2102、FT232是3.3V还是5V务必确保电平匹配否则可能无法通信甚至损坏器件。接线检查TX接RXRX接TXGND接GND。这个“交叉接法”说过无数次但依然是新手最常犯的错误。用万用表量一下通断。上拉电阻UART总线在空闲时应为高电平。如果线路较长或干扰大在接收端FPGA的RXD引脚加一个4.7kΩ-10kΩ的上拉电阻到VCC可以增强抗干扰能力。5.2 使用内部逻辑分析仪ILA进行波形抓取Vivado的ILAIntegrated Logic Analyzer是调试数字逻辑的利器。你需要抓取以下关键信号baud_en确认波特率使能信号是否以预期的频率如115200Hz周期性出现。如果这个信号不对后面的一切都无从谈起。发送通路tx_state状态机、txd最终输出、tx_data_reg正在发送的数据。观察状态机跳转是否与baud_en同步txd波形是否符合UART帧格式起始位低、停止位高。接收通路rxd_filtered滤波后的输入、rx_state、rx_shift_reg。观察在起始位下降沿后状态机是否进入RX_START并在数据位中心点是否正确采样到了数据。一个典型的发送波形问题如果你发现txd上的脉冲宽度明显宽于或窄于一个波特率周期那很可能是baud_en的计算或生成逻辑有误或者状态机没有在baud_en有效时才跳转。5.3 常见问题排查清单问题能发送但不能接收或反之。排查检查TX和RX模块的baud_en是否来自同一个波特率生成器实例且频率一致。检查顶层模块的txd和rxd端口是否与约束文件.xdc中的引脚正确对应。问题接收数据错位如0x55收成0xAA。排查这通常是位序反了。检查接收移位寄存器是低位先入LSB first还是高位先入MSB first。UART标准是LSB first。检查发送端的数据位赋值顺序。问题高速率如1Mbps下通信不稳定。排查时序约束是否为txd和rxd设置了正确的I/O延迟约束使用set_input_delay和set_output_delay来告知工具外部信号与板级时钟的关系。时钟精度系统时钟CLK_FREQ是否准确使用示波器测量实际晶振频率。波特率分频系数BAUD_DIV的舍入误差在高速率下会被放大考虑使用更精确的分数分频。布线延迟在FPGA内部如果baud_en信号扇出很大或路径很长可能会产生skew导致状态机不同部分动作不同步。可以尝试对baud_en信号进行寄存器打拍再使用。问题添加FIFO后数据丢失。排查检查FIFO的满full和空empty标志信号。在写入FIFO前必须检查!tx_full在读取FIFO前必须检查!rx_empty。忽略这些标志是导致数据覆盖或读空值的常见原因。另外确认FIFO的读写时钟域是否正确我们的设计通常是单时钟同步FIFO。5.4 进阶优化方向当基本功能稳定后可以考虑以下优化让模块更专业、更健壮自动波特率检测对于需要自适应不同设备的应用可以增加自动波特率检测功能。通常通过检测起始位的低电平宽度来反推波特率。硬件流控RTS/CTS增加请求发送RTS和清除发送CTS信号用于防止数据溢出特别适用于与慢速设备通信。中断支持为“发送完成”、“接收FIFO非空”、“接收错误如帧错误、奇偶校验错误”等事件提供中断输出信号方便与处理器如软核CPU配合。更高级的滤波使用更长的窗口如3/5个周期进行多数表决滤波或使用边沿检测结合去抖逻辑以应对更恶劣的噪声环境。形式验证Formal Verification使用SVASystemVerilog Assertions为状态机和数据通路编写属性断言用形式化工具如JasperGold、VC Formal证明设计在某些条件下不会死锁或进入非法状态。这对于高可靠性设计至关重要。通过以上从原理到实现从模块设计到系统集成再到调试排错和进阶优化的完整梳理这个参数化的UART模块就不再是一段孤立的代码而是一个可以信赖、可以随项目需求灵活配置的可靠通信基础组件。将它纳入你的个人代码库下次项目需要串口时你会感谢现在花时间打造通用模块的自己。
返回列表