1. 项目概述为什么我们需要一个带大容量FIFO的UART在嵌入式开发中UART通用异步收发传输器几乎是工程师们打交道最多的外设之一从简单的调试信息打印到复杂的设备间通信都离不开它。然而一个长期困扰着许多开发者的痛点在于大多数微控制器MCU内置的UART硬件FIFO先入先出队列深度非常有限常见的有8、16或32字节。当面对需要传输或接收较大数据包的应用场景时例如固件升级、图像数据传输、批量传感器信息采集等这个小小的FIFO很快就会成为瓶颈。想象一下你的MCU正在处理一个复杂的算法同时还需要通过UART接收一个512字节的配置包。如果UART的接收FIFO只有16字节深那么每收到16个字节MCU就必须被中断一次去读取数据否则就会发生数据溢出Overrun错误。频繁的中断会严重打断主程序的执行流消耗宝贵的CPU周期降低系统整体效率甚至可能导致实时任务超时。发送端亦然如果发送FIFO太浅CPU需要频繁等待UART发送完毕才能写入下一个数据通信吞吐量会大打折扣。这时FPGA现场可编程门阵列的优势就凸显出来了。FPGA内部通常集成了大量可灵活配置的块RAMBlock RAM这些资源可以被我们“征用”来构建深度远超普通MCU的FIFO。例如我们可以轻松实现一个512字节甚至更深的FIFO成本仅仅是消耗一小部分逻辑和存储资源。基于此我设计并实现了一个带有512字节深度FIFO、支持并行总线访问的UART IP核。这个设计可以直接挂载到CPU的总线上像操作内存一样操作UART极大地减轻了CPU在串口通信上的负担特别适合那些对通信效率和可靠性有较高要求的嵌入式系统比如工业控制、高端仪器仪表或通信网关等。2. 系统架构与设计思路拆解2.1 整体结构框图与接口定义这个带FIFO的UART核心设计思想是“总线化”和“深度缓冲”。其顶层结构可以清晰地分为几个功能模块如图1所示此处为文字描述实际设计中有对应的RTL框图。整个模块通过一个标准的并行总线接口如类似Wishbone、APB或自定义的简单总线与主控制器CPU通信这使得它能够无缝集成到各种SoC系统中。核心模块包括总线控制模块负责解析CPU发出的地址、读写信号和数据将访问路由到正确的内部寄存器。发送逻辑包含发送状态机和发送移位寄存器。其职责是从发送FIFO中读取数据按照UART协议起始位、数据位、停止位将数据逐位从TXD引脚串行发出。接收逻辑包含接收状态机和接收移位寄存器。其职责是以16倍波特率的频率对RXD引脚进行采样正确识别起始位并采集数据位将接收到的完整字节写入接收FIFO。波特率时钟生成模块根据配置的分频值从系统主时钟sysclk产生出发送和接收所需的波特率时钟。注意发送和接收使用独立的时钟生成器因为它们是异步操作。发送FIFO与接收FIFO两个独立的512x8位深度512字节宽度8位的异步FIFO。它们是数据缓冲的核心。FIFO读写控制模块协调总线操作与FIFO之间的数据流。例如当CPU向数据寄存器写入时该模块将数据推入发送FIFO当CPU读取数据寄存器时该模块从接收FIFO弹出数据。关键接口信号总线侧clk系统时钟rst_n复位addr[2:0]寄存器地址wr_en写使能rd_en读使能wdata[7:0]写入数据rdata[7:0]读出数据。UART侧txd串行发送rxd串行接收。中断输出int_out中断请求。当接收FIFO中的数据量达到预设的触发值时此信号拉高通知CPU来读取数据。2.2 寄存器映射与功能详解CPU通过访问一组内存映射寄存器来控制这个UART模块。下表详细列出了所有寄存器的功能地址寄存器名称描述读写属性0x00数据寄存器写操作数据写入发送FIFO。读操作数据从接收FIFO读出。读写0x01分频值寄存器设置波特率分频系数N。计算公式Baud sysclk / 16 / (N 1)。例如系统时钟50MHz欲得到115200波特率则 N 50e6/(16*115200) - 1 ≈ 26。只写0x02中断触发值寄存器设置接收FIFO中断触发阈值0-511。当接收FIFO中有效数据个数 ≥ 此值时int_out信号置位。只写0x03状态寄存器反映UART和FIFO的实时状态。Bit0: 接收FIFO空 (1-空)。Bit1: 接收FIFO满 (1-满)。Bit2: 发送FIFO空 (1-空)。Bit3: 发送FIFO满 (1-满)。Bit4: 接收FIFO有效个数[8] (高位)。Bit5: 接收FIFO有效个数[9] (高位)。Bit6-7: 保留。只读0x04接收数据计数寄存器直接读取接收FIFO中当前有效数据的个数低8位。结合状态寄存器的Bit4-5可得到完整的9位计数值0-511。只读0x05控制寄存器Bit0: 发送FIFO清空 (写1清空)。Bit1: 接收FIFO清空 (写1清空)。Bit2: 接收使能 (1-使能)。Bit3: 发送使能 (1-使能)。Bit4-7: 保留。只写注意地址0x00是“幻影”寄存器读写操作实际访问的是两个不同的物理实体发送FIFO和接收FIFO。这种设计简化了总线接口但要求驱动程序员心里清楚写操作是投递数据读操作是提取数据。2.3 波特率生成原理与精度考量UART通信的基石是收发双方使用相同的波特率。本设计采用经典的16倍过采样技术来增强抗干扰能力和起始位检测的可靠性。原理在接收端并非在每位数据的正中间采样一次而是以16倍于波特率的频率进行采样。对于每一位数据如数据位会采样16次通常取第7、8、9次采样的多数值作为该位的最终值这能有效滤除线上的毛刺。对于起始位的检测则是在检测到RXD线从高电平变为低电平后连续监测到8个低电平采样点即半个位周期才确认起始位有效这避免了毛刺引发的误触发。时钟生成波特率时钟baud_clk由系统时钟sysclk分频得到。公式Baud sysclk / 16 / (N 1)中N就是我们写入分频值寄存器的数。sysclk / (N1)得到的是16倍波特率时钟再经过一个16分频的计数器就产生了控制位发送/采样的使能信号。精度计算示例假设sysclk 50MHz目标波特率Baud_target 115200。理论分频值N_calc 50e6 / (16 * 115200) - 1 ≈ 26.1267。取整N 26。实际波特率Baud_actual 50e6 / (16 * (261)) ≈ 115740.7。误差率Error (115740.7 - 115200) / 115200 ≈ 0.47%。通常误差在2%以内即可保证可靠通信。对于更高精度的需求可以使用锁相环PLL产生一个与目标波特率成整数倍关系的高频时钟或者使用小数分频技术。3. 核心模块的Verilog实现细节3.1 异步FIFO的生成与集成在Altera现Intel的Quartus II或更新的Quartus Prime中我们可以利用MegaWizard Plug-In Manager工具来生成高度可配置的FIFO IP核。这是保证设计稳定性和节省开发时间的关键。创建步骤与关键配置在MegaWizard中选择Installed IP - Library - Basic Functions - On Chip Memory - FIFO。选择“SCFIFO”单时钟FIFO或“DCFIFO”双时钟异步FIFO。这里必须选择DCFIFO因为我们的读写时钟域不同写时钟是系统总线时钟读时钟是UART的波特率时钟或其衍生时钟两者频率和相位都不同。配置参数数据宽度8 (bits)。FIFO深度512 (words)。这是本设计的核心优势所在。时钟类型选择“Independent clocks”读写时钟独立。满/空标志务必勾选“Usedw[]”端口。这个端口输出当前FIFO中已使用的字数量是实现中断触发和状态查询的基础。同步化设置对于异步FIFO跨时钟域的指针比较需要同步。MegaWizard会自动插入同步器通常保持默认设置即可但需注意其带来的延迟。集成要点将生成的FIFO模块例化两次分别作为tx_fifo和rx_fifo。tx_fifo的写端连接总线控制逻辑wr_en和wdata读端连接发送逻辑。rx_fifo的写端连接接收逻辑读端连接总线控制逻辑rd_en和rdata。务必正确连接usedw信号到状态生成逻辑用于判断空、满和计算数据量。实操心得使用IP核生成FIFO比自己用寄存器堆“手搓”要可靠得多。IP核已经妥善处理了跨时钟域同步、满空标志的准确生成等棘手问题。自己实现一个深度较大且稳定的异步FIFO需要深厚的数字电路功底容易在边界条件下出错比如同时读写时的标志判断。3.2 发送状态机设计发送逻辑的核心是一个有限状态机FSM负责从FIFO中取出数据并按照UART帧格式串行化输出。状态定义示例IDLE空闲状态。检查发送使能位和发送FIFO是否非空。若条件满足则从FIFO读取一个字节加载到发送移位寄存器并进入START状态。START发送起始位低电平。持续一个位时间bit_period然后进入DATA状态并初始化位计数器。DATA发送8位数据。每个位时间将移位寄存器的最低位送到txd然后右移一位。位计数器加1发送完8位后进入STOP状态。STOP发送停止位高电平。持续一个位时间。完成后检查发送FIFO是否还有数据如果有则回到START状态发送下一个字节如果没有则回到IDLE状态。关键代码片段Verilog风格描述always (posedge clk_tx or posedge rst) begin // clk_tx 是波特率时钟域 if (rst) begin state_tx IDLE; tx_shift_reg 8‘hFF; bit_cnt 0; txd 1‘b1; // 空闲时为高电平 end else begin case (state_tx) IDLE: begin txd 1‘b1; if (tx_enable !tx_fifo_empty) begin tx_shift_reg tx_fifo_rdata; // 从FIFO读取数据 tx_fifo_rdreq 1‘b1; state_tx START; end end START: begin tx_fifo_rdreq 1‘b0; txd 1‘b0; // 起始位 if (baud_tick) begin // 每个位时间一个脉冲 state_tx DATA; bit_cnt 0; end end DATA: begin txd tx_shift_reg[0]; if (baud_tick) begin tx_shift_reg {1‘b1, tx_shift_reg[7:1]}; // 右移高位补1停止位预备 bit_cnt bit_cnt 1; if (bit_cnt 7) state_tx STOP; end end STOP: begin txd 1‘b1; // 停止位 if (baud_tick) begin if (!tx_fifo_empty) begin state_tx START; end else begin state_tx IDLE; end end end endcase end end3.3 接收状态机与过采样接收状态机比发送更复杂因为它需要从异步的串行线中可靠地恢复出数据和时钟。状态定义示例IDLE监视RXD线。当检测到连续多个如8个低电平采样点时认为有效的起始位到来进入START状态并复位采样计数器。DATA对每一位数据等待16个采样时钟。在第7、8、9个采样点附近进行采样采用“三取二”投票法确定该位的值。每接收完一个位将结果移入接收移位寄存器。接收完8位后进入STOP状态。STOP检测停止位应为高电平。如果检测到有效的停止位则将接收移位寄存器中的完整字节写入接收FIFO。无论停止位是否有效最终都回到IDLE状态准备接收下一帧。过采样逻辑接收端有一个运行在16倍波特率时钟clk_16x下的计数器sample_cnt。在DATA状态当sample_cnt计数到7、8、9时对RXD进行采样存入一个3位的寄存器然后通过判断其中1的个数是否2来决定该数据位是1还是0。这种多数判决法极大地提高了在噪声环境下的可靠性。注意事项起始位的检测需要“去抖”逻辑。简单的边沿检测在噪声下极易误触发。可靠的做法是在IDLE状态下以16倍波特率频率采样RXD只有当连续采样到8个可配置低电平时才确认是起始位并从此刻开始计算位边界。这相当于对起始位进行了滤波。4. 功能仿真与验证策略仿真验证是数字设计不可或缺的一环。我使用ModelSim对设计进行了全面的仿真采用“自发自收”的环路测试方法这是验证UART基本功能最直接有效的方式。4.1 测试平台搭建顶层测试模块将UART模块的txd输出直接连接到rxd输入构成环路。总线任务模拟编写Verilog任务Task或使用SystemVerilog接口来模拟CPU的读写行为。例如task write_reg; input [2:0] addr; input [7:0] data; begin (posedge clk); bus_addr addr; bus_wr_en 1; bus_wdata data; (posedge clk); bus_wr_en 0; end endtask测试序列系统复位。写入分频值寄存器例如N1得到高波特率便于快速仿真。写入中断触发值寄存器例如5。写入控制寄存器先写0x03清空发送和接收FIFO再写0x0C使能发送和接收。通过连续写入数据寄存器地址0x00向发送FIFO填入一组测试数据如0x55, 0x56, ..., 0x5E共10个字节。监控与检查观察txd波形看其是否按正确的波特率串行输出测试数据。等待一段时间足够10个字节发送完毕通过总线读取状态寄存器和接收FIFO数据个数寄存器。连续从数据寄存器地址0x00读取10次检查读出的数据是否与发送的数据完全一致。特别观察int_out信号是否在接收FIFO数据量达到5时拉高并在数据被读走后低于触发值拉低。4.2 仿真结果分析通过仿真波形如图3、4、5所示此处为描述我们可以清晰地看到图4初始化阶段总线操作序列清晰可见控制寄存器配置完成后txd引脚开始出现串行波形。图3全局视图txd上连续出现了10个UART数据帧。同时由于环路连接rxd实际与txd短路也收到了相同的数据。在发送完第5个字节后int_out信号如预期般跳变为高电平产生中断。图5读取阶段总线读取接收FIFO数据个数寄存器返回值为100x0A。随后连续10次读操作返回的数据依次为0x55至0x5E与发送数据完全吻合。这充分证明了整个数据通路总线写入 - 发送FIFO - 发送逻辑 - 串行线 - 接收逻辑 - 接收FIFO - 总线读出是完整且正确的。仿真技巧为了更全面地测试边界情况还应该设计额外的测试用例FIFO满测试连续写入513个字节检查第513次写入时状态寄存器的“发送FIFO满”标志是否置位且该数据是否被丢弃或根据设计是否阻塞总线。FIFO空测试在接收FIFO为空时进行读操作检查返回的数据是否无效或保持上一次值以及“接收FIFO空”标志位。波特率容错测试稍微改变接收端的分频值模拟收发双方波特率微小偏差测试数据是否仍能正确接收。中断触发测试分别测试触发值设为1、511等边界情况以及读取数据使数量低于触发值后中断是否及时清除。5. 实际应用中的问题排查与优化建议将这样一个IP核集成到真实的系统中可能会遇到一些在仿真中不易发现的问题。以下是我在实际项目中总结的一些经验。5.1 常见问题与排查指南问题现象可能原因排查思路与解决方案发送数据丢失或错位1. 波特率不匹配。2. 发送FIFO已满但CPU未检查状态继续写入。3. 发送状态机逻辑错误未正确处理背靠背发送。1. 用示波器测量txd引脚的实际位宽计算波特率与配置值对比。检查系统时钟频率和分频计算。2. 在CPU驱动程序中每次写入前检查“发送FIFO满”标志。或采用中断方式当发送FIFO有空闲时触发发送中断。3. 仿真时重点测试FIFO从非空到空以及从空到非空的转换瞬间状态机能否正确跳转。接收不到数据或数据错误1.rxd引脚连接错误或电气电平不匹配。2. 接收使能位未开启。3. 起始位检测太敏感或太迟钝受噪声干扰。4. 过采样点选择不当在信号边沿采样。1. 硬件上检查线路连接用示波器看rxd是否有信号。确认是TTL/CMOS电平还是RS-232电平必要时加电平转换芯片。2. 确认控制寄存器的接收使能位(Bit2)已设置为1。3. 调整起始位检测的连续低电平采样数如从8调整为6或10在可靠性和抗噪性间取得平衡。4. 确保采样点位于位周期的中间如第8个采样点。检查波特率时钟的相位。中断不产生或常产生1. 中断触发值寄存器设置错误如设为0。2. 中断信号int_out是电平信号CPU需要边沿触发。3. 中断清除逻辑有问题。读取FIFO后数据量低于触发值但中断标志未及时撤销。1. 确认写入中断触发值寄存器的值在1-511之间。值为0可能被定义为永不中断或始终中断。2. 在CPU端将中断信号配置为边沿敏感上升沿或高电平并在中断服务程序ISR中读取足够数据使FIFO数据量低于触发值。如果IP输出是电平CPU是边沿检测需要在IP内部或CPU端添加一个边沿检测电路。3. 检查中断生成逻辑int_out (rx_fifo_usedw trigger_value)。确保rx_fifo_usedw的变化能及时反映到比较器。读写FIFO时数据混乱1. 异步FIFO的跨时钟域同步问题未处理好。2. 总线读写时序不满足FIFO IP核的建立/保持时间要求。3. 地址解码错误误写了其他寄存器。1.这是最可能的原因。检查生成的DCFIFO IP核是否使用了正确的同步策略。仿真时添加对读写指针的监控看其在跨时钟域时是否出现亚稳态导致的跳变。2. 检查总线接口的时序确保wr_en/rd_en信号与data/address信号的相对关系符合FIFO IP核的数据手册要求。在总线时钟较慢时问题不大但在高速总线如100MHz以上下需特别注意。3. 仔细核对寄存器地址映射确保驱动程序的访问地址正确。5.2 性能优化与功能扩展建议基础的512字节FIFO UART已经很强大了但根据具体应用还可以进一步优化和扩展FIFO深度动态配置可以将FIFO深度做成参数化在例化时传入。这样同一个IP核可以适应从64字节到2048字节甚至更深的不同应用需求提高代码复用率。module uart_with_fifo #( parameter TX_FIFO_DEPTH 512, parameter RX_FIFO_DEPTH 512 )( // ... 端口列表 ); // 在例化FIFO IP核时使用参数代替固定值 fifo_async #(.DEPTH(TX_FIFO_DEPTH)) tx_fifo_inst (...);增加流控信号添加RTS请求发送和CTS清除发送硬件流控引脚。当接收FIFO快满时拉高RTS通知对方暂停发送本机发送前检查CTS是否为低。这能从根本上防止数据丢失特别适合高速或不可靠的通信链路。支持多种数据格式当前设计固定为8位数据位、1位停止位、无校验。可以增加配置寄存器支持5-9位数据位、1/1.5/2位停止位、奇偶校验奇校验、偶校验、无校验等。这需要修改发送和接收状态机以及相应的帧长度逻辑。DMA集成对于需要极高吞吐量的场景可以设计一个DMA控制器与之配合。当发送FIFO有空闲或接收FIFO达到一定水位时DMA控制器自动从系统内存搬运数据到发送FIFO或将接收FIFO数据搬移到系统内存完全解放CPU。软件驱动优化编写高效的设备驱动程序。推荐采用中断环形缓冲区的模式。在中断服务程序中只做最少的操作如从接收FIFO读取数据到内存中的环形缓冲区或从环形缓冲区填充数据到发送FIFO将数据处理任务留给后台的主循环。避免在中断中处理复杂逻辑或调用可能阻塞的函数。这个带深度FIFO的UART IP核其价值不仅在于提供了一个大缓冲的串口更在于它展示了一种用FPGA来增强和定制外设的经典思路。当你觉得MCU的外设不够用、性能不够强时不妨考虑用FPGA来做一个“外设加速器”或“协议转换器”这往往是解决复杂嵌入式系统通信瓶颈的利器。