基于FPGA的深度FIFO UART IP核设计与实现
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内部通常集成了大量可灵活配置的块RAM(Block 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 / (N+1)得到的是16倍波特率时钟,再经过一个16分频的计数器,就产生了控制位发送/采样的使能信号。
精度计算示例:假设sysclk = 50MHz,目标波特率Baud_target = 115200。
- 理论分频值
N_calc = 50e6 / (16 * 115200) - 1 ≈ 26.1267。 - 取整
N = 26。 - 实际波特率
Baud_actual = 50e6 / (16 * (26+1)) ≈ 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 - 测试序列:
- 系统复位。
- 写入分频值寄存器(例如,
N=1,得到高波特率便于快速仿真)。 - 写入中断触发值寄存器(例如,
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数据个数寄存器,返回值为10(0x0A)。随后连续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来做一个“外设加速器”或“协议转换器”,这往往是解决复杂嵌入式系统通信瓶颈的利器。
