1. 项目概述:为什么要在FPGA上实现串口通信?
在嵌入式系统和数字逻辑设计领域,串口通信(UART)堪称是“数字世界的老黄牛”。它结构简单、协议透明,是MCU、传感器、调试终端之间最基础、最可靠的通信方式之一。那么,当我们的主角换成FPGA时,这件事就变得有趣了。你可能会问,明明有现成的UART IP核或者专用芯片,为什么还要用FPGA从头设计一个串口?这恰恰是理解FPGA设计精髓的绝佳切入点。
首先,自主可控与深度定制。使用现成IP核固然快捷,但其内部状态机、缓冲区大小、错误处理机制对你而言可能是个黑盒。当你需要实现一些非标准波特率(如自定义的115200*1.5)、添加特定的数据包帧头帧尾(比如网络热词中提到的分隔符0x0D 0x0A)、或者集成复杂的流控逻辑时,自己动手设计才能做到心手合一。其次,FPGA的并行优势。一个FPGA可以轻松实现几十路甚至上百路独立的UART通道,且互不干扰,这对于需要多串口汇聚(如热词中“多串口接收合并到单串口发送”)的网关设备来说,是MCU难以企及的性能。最后,学习与验证的价值。UART设计涵盖了FPGA开发的核心技能:同步时序设计、状态机(FSM)应用、跨时钟域处理、以及Testbench验证。搞定一个稳定的UART,就等于打通了FPGA数字系统设计的任督二脉。
本文将带你从零开始,用Verilog HDL在FPGA上实现一个功能完整、稳定可靠的UART收发器。我们会深入每个模块的细节,解释每一个参数和状态转移背后的原因,并分享在实际工程中调试UART时那些手册上不会写的“坑”和技巧。无论你是刚接触FPGA的新手,还是想深化对通信接口理解的老手,这篇内容都能提供可直接“抄作业”的实践指南。
2. 核心设计思路与架构拆解
2.1 UART协议的精髓与关键参数
在动手写代码之前,我们必须吃透协议。UART是一种异步串行通信协议,其通信帧格式是固定的:1个起始位(低电平)、5-9个数据位、可选的奇偶校验位、1个或2个停止位(高电平)。通信双方完全依靠预先约定好的波特率(Baud Rate)来同步每一位的采样时刻。
这里有一个至关重要的概念:波特率时钟(Baud Clock)。它不是直接用于驱动发送或接收数据的时钟,而是用来产生采样点。通常,为了在数据位中心获得最稳定的采样值,我们会使用一个比波特率高N倍的本地时钟(如16倍),通过计数器产生一个波特率使能信号。例如,系统时钟为50MHz,目标波特率为115200,那么每个位周期的系统时钟周期数为50,000,000 / 115200 ≈ 434。如果我们采用16倍过采样,则计数器模值应为434 / 16 ≈ 27。这个计算过程直接决定了通信的准确性。
注意:波特率误差是UART通信失败的首要元凶。误差必须控制在允许范围内(通常<3%)。计算分频系数时,应使用取整而非四舍五入,并尽可能选择高频的系统时钟来降低量化误差。例如,用50MHz时钟产生115200波特率,实际波特率为
50,000,000 / (27*16) ≈ 115740,误差约为0.47%,完全可接受。
2.2 整体系统架构设计
一个完整的FPGA UART模块通常划分为以下几个核心部分,这种划分也体现了模块化设计思想:
- 波特率发生器(Baud Rate Generator):核心是一个分频计数器,根据系统时钟和配置的波特率,产生一个周期等于位宽时间的脉冲信号(
baud_tick)以及用于接收过采样的16倍频脉冲(sample_tick)。 - 发送器(UART Transmitter, TX):一个有限状态机(FSM)。当收到发送请求后,FSM依次输出起始位、数据位、校验位(如有)和停止位。每个位的持续时间由
baud_tick控制。 - 接收器(UART Receiver, RX):这是设计的难点和重点。同样是一个FSM,但它需要持续监听RX线路。检测到起始位下降沿后,FSM启动,并在每个数据位的中心点(利用
sample_tick计数)采样RX引脚状态,组装成数据。它必须能处理毛刺和噪声。 - 顶层封装与接口(Top Module & Interface):将TX和RX模块、波特率发生器以及可能的FIFO(先入先出缓冲区)封装起来,对外提供简洁的并行数据接口(如
data_to_send,send_en,data_received,data_valid)和串行物理接口(uart_tx,uart_rx)。
为何要采用状态机?因为UART的发送和接收过程本质上是一系列按严格时间顺序执行的动作,这正是状态机的用武之地。状态清晰,逻辑明确,调试时也容易定位问题所在。
3. 核心模块的Verilog实现与细节剖析
3.1 波特率发生器的精确实现
波特率发生器是全局的“心跳”。我们不直接生成波特率时钟,而是生成周期性的使能脉冲,这更符合同步设计规范,避免产生难以处理的毛刺时钟。
module baud_gen #( parameter CLK_FREQ = 50_000_000, // 输入时钟频率,单位Hz parameter BAUD_RATE = 115200 // 目标波特率 )( input wire clk, input wire rst_n, output reg baud_tick, // 波特率使能脉冲(位周期) output reg sample_tick // 采样使能脉冲(16倍波特率) ); // 计算分频系数 localparam BIT_CYCLE = CLK_FREQ / BAUD_RATE; // 每个位周期的时钟数 localparam SAMPLE_CYCLE = BIT_CYCLE / 16; // 每个采样周期的时钟数 localparam CNT_WIDTH = $clog2(BIT_CYCLE); reg [CNT_WIDTH-1:0] bit_cnt; reg [3:0] sample_cnt; // 0-15计数器,用于产生16倍采样 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin bit_cnt <= 0; sample_cnt <= 0; baud_tick <= 1'b0; sample_tick <= 1'b0; end else begin // 默认拉低脉冲信号 baud_tick <= 1'b0; sample_tick <= 1'b0; // 位周期计数器 if (bit_cnt == BIT_CYCLE - 1) begin bit_cnt <= 0; baud_tick <= 1'b1; // 一个位周期结束,产生脉冲 end else begin bit_cnt <= bit_cnt + 1; end // 采样周期计数器(16倍频) if (sample_cnt == 15) begin sample_cnt <= 0; sample_tick <= 1'b1; // 每16个采样周期产生一个脉冲,对应位周期的开始? // 错!这里需要对齐。 end else begin sample_cnt <= sample_cnt + 1; end end end endmodule上面代码有一个典型的逻辑错误:sample_tick的产生没有与baud_tick同步。在接收端,我们需要在数据位的中心点采样,这意味着第一个采样点应该出现在起始位开始后的第8个sample_tick(假设16倍过采样,中心点是第7.5个,取整为第8个)。因此,更稳健的做法是只用一个主计数器,然后通过比较来产生两个使能信号。
修正后的核心逻辑:
// 在always块内 reg [CNT_WIDTH-1:0] main_cnt; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin main_cnt <= 0; baud_tick <= 0; sample_tick <= 0; end else begin baud_tick <= 0; sample_tick <= 0; if (main_cnt == BIT_CYCLE - 1) begin main_cnt <= 0; baud_tick <= 1‘b1; // 每个位周期结束时产生一个脉冲 end else begin main_cnt <= main_cnt + 1; end // 当主计数器计数值是SAMPLE_CYCLE的整数倍时,产生采样脉冲 // 并且,为了对准位中心,我们可以从计数到 SAMPLE_CYCLE/2 开始第一个有效采样 if (main_cnt % SAMPLE_CYCLE == (SAMPLE_CYCLE/2)) begin sample_tick <= 1‘b1; end end end这个设计确保了sample_tick精确地出现在每个“虚拟”采样窗口的中心,为接收器提供稳定的采样时机。
3.2 发送器(TX)状态机设计
发送器是一个典型的状态机,状态转移由baud_tick驱动。状态定义如下:
IDLE:空闲状态,输出线为高电平(停止位电平)。START_BIT:发送起始位(低电平),持续一个位周期。DATA_BITS:依次发送8个数据位,从最低位(LSB)开始。PARITY_BIT(可选):发送奇偶校验位。STOP_BIT:发送停止位(高电平),至少一个位周期。
module uart_tx #( parameter DATA_WIDTH = 8 )( input wire clk, input wire rst_n, input wire [DATA_WIDTH-1:0] data_in, input wire send_en, input wire baud_tick, // 来自波特率发生器 output reg tx_out, output reg tx_busy ); // 状态定义 localparam S_IDLE = 0; localparam S_START = 1; localparam S_DATA = 2; localparam S_STOP = 3; reg [1:0] state, next_state; reg [2:0] bit_index; // 用于计数已发送的数据位 reg [DATA_WIDTH-1:0] data_reg; // 状态寄存器更新 always @(posedge clk or negedge rst_n) begin if (!rst_n) state <= S_IDLE; else state <= next_state; end // 状态转移逻辑与输出逻辑(组合逻辑部分,也可拆分为两个always块) always @(*) begin next_state = state; tx_out = 1‘b1; // 默认输出高电平 tx_busy = 1‘b1; // 默认忙 case (state) S_IDLE: begin tx_out = 1‘b1; tx_busy = 1‘b0; if (send_en) begin next_state = S_START; end end S_START: begin tx_out = 1‘b0; // 起始位 if (baud_tick) begin next_state = S_DATA; end end S_DATA: begin tx_out = data_reg[bit_index]; // 发送当前位 if (baud_tick) begin if (bit_index == DATA_WIDTH-1) begin next_state = S_STOP; end // bit_index 在时钟驱动下更新,见下方时序逻辑 end end S_STOP: begin tx_out = 1‘b1; // 停止位 if (baud_tick) begin next_state = S_IDLE; end end default: next_state = S_IDLE; endcase end // 数据寄存与位索引更新(时序逻辑) always @(posedge clk or negedge rst_n) begin if (!rst_n) begin data_reg <= 0; bit_index <= 0; end else begin case (state) S_IDLE: begin if (send_en) begin data_reg <= data_in; // 锁存待发送数据 end bit_index <= 0; end S_DATA: begin if (baud_tick) begin if (bit_index < DATA_WIDTH-1) begin bit_index <= bit_index + 1; end else begin bit_index <= 0; end end end default: begin // 其他状态保持 end endcase end end endmodule实操心得:发送器的设计相对直接,关键点在于精确的时序控制。
baud_tick必须干净无毛刺,且状态转移一定要在baud_tick有效的那个时钟沿发生。tx_busy信号非常有用,可以反馈给上游逻辑(如CPU或另一个状态机),防止在新数据尚未发送完时被覆盖。
3.3 接收器(RX)状态机与抗干扰设计
接收器是UART设计的灵魂,也是调试中最容易出问题的地方。其核心挑战是在异步信号中可靠地检测起始位并同步采样。
状态设计:
IDLE:监控RX线,等待下降沿(起始位)。START_DET:检测到下降沿后,等待半个位周期(即8个sample_tick)后再次采样,确认是否为有效的起始位(依然是低电平)。这一步是抗干扰的关键,可以滤除短于半个位周期的毛刺。RECV_DATA:在随后的每个位周期中心点(即每16个sample_tick采样一次),采样RX线,并存入移位寄存器。STOP_BIT:采样停止位,验证其为高电平。如果停止位为低,则可能发生帧错误。
module uart_rx #( parameter DATA_WIDTH = 8, parameter OVERSAMPLE = 16 // 过采样倍数 )( input wire clk, input wire rst_n, input wire rx_in, input wire sample_tick, // 16倍波特率的采样脉冲 output reg [DATA_WIDTH-1:0] data_out, output reg data_valid, output reg frame_error ); // 状态定义 localparam S_IDLE = 0; localparam S_START_DET = 1; localparam S_RECV_DATA = 2; localparam S_STOP_BIT = 3; reg [1:0] state, next_state; reg [3:0] sample_cnt; // 0-15计数器 reg [2:0] bit_cnt; // 0-7数据位计数器 reg [DATA_WIDTH-1:0] shift_reg; reg rx_sync1, rx_sync2; // 两级同步器,用于跨时钟域 // 对异步的rx_in进行同步化处理,消除亚稳态 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin rx_sync1 <= 1‘b1; rx_sync2 <= 1‘b1; end else begin rx_sync1 <= rx_in; rx_sync2 <= rx_sync1; end end // 状态寄存器 always @(posedge clk or negedge rst_n) begin if (!rst_n) state <= S_IDLE; else state <= next_state; end // 状态转移与计数器逻辑 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin next_state <= S_IDLE; sample_cnt <= 0; bit_cnt <= 0; shift_reg <= 0; data_out <= 0; data_valid <= 1‘b0; frame_error <= 1‘b0; end else begin data_valid <= 1‘b0; // 默认拉低,脉冲有效 frame_error <= 1‘b0; case (state) S_IDLE: begin sample_cnt <= 0; bit_cnt <= 0; if (rx_sync2 == 1‘b0) begin // 检测到下降沿 next_state <= S_START_DET; end end S_START_DET: begin if (sample_tick) begin if (sample_cnt == (OVERSAMPLE/2 - 1)) begin // 到达起始位中心点,再次采样确认 if (rx_sync2 == 1‘b0) begin // 确认是有效的起始位 next_state <= S_RECV_DATA; end else begin // 是毛刺,回到空闲 next_state <= S_IDLE; end sample_cnt <= 0; end else begin sample_cnt <= sample_cnt + 1; end end end S_RECV_DATA: begin if (sample_tick) begin if (sample_cnt == (OVERSAMPLE - 1)) begin // 到达一个数据位的采样点(中心点) shift_reg <= {rx_sync2, shift_reg[DATA_WIDTH-1:1]}; // 右移,LSB先入 sample_cnt <= 0; if (bit_cnt == DATA_WIDTH-1) begin // 所有数据位接收完毕 next_state <= S_STOP_BIT; bit_cnt <= 0; end else begin bit_cnt <= bit_cnt + 1; end end else begin sample_cnt <= sample_cnt + 1; end end end S_STOP_BIT: begin if (sample_tick) begin if (sample_cnt == (OVERSAMPLE - 1)) begin // 采样停止位 if (rx_sync2 == 1‘b1) begin // 停止位正确 data_out <= shift_reg; // 输出数据 data_valid <= 1‘b1; end else begin // 帧错误,停止位为低 frame_error <= 1‘b1; end next_state <= S_IDLE; sample_cnt <= 0; end else begin sample_cnt <= sample_cnt + 1; end end end default: next_state <= S_IDLE; endcase end end endmodule核心技巧:接收器设计中,两级同步器(rx_sync1, rx_sync2)是必须的,因为
rx_in是来自FPGA引脚的外部异步信号,直接使用会产生亚稳态风险。其次,在起始位中心点进行二次验证,是区分真实起始位和噪声毛刺的经典方法,能极大提升通信可靠性。
4. 系统集成、仿真验证与板级调试
4.1 顶层模块集成与FIFO缓冲
将上述模块与一个FIFO(先入先出存储器)集成,可以构建一个更实用的UART控制器。FIFO用于缓冲收发数据,解决上游数据处理速度与串口速度不匹配的问题。
module uart_top #( parameter CLK_FREQ = 50_000_000, parameter BAUD_RATE = 115200, parameter DATA_WIDTH = 8, parameter FIFO_DEPTH = 16 )( input wire clk, input wire rst_n, // 并行用户接口 input wire [DATA_WIDTH-1:0] tx_data_i, input wire tx_wr_en_i, output wire tx_full_o, output wire tx_busy_o, output wire [DATA_WIDTH-1:0] rx_data_o, output wire rx_rd_en_i, output wire rx_empty_o, output wire rx_valid_o, // 串行物理接口 output wire uart_tx, input wire uart_rx ); // 内部信号声明 wire baud_tick, sample_tick; wire tx_fifo_empty, tx_fifo_full; wire [DATA_WIDTH-1:0] tx_fifo_data; wire rx_fifo_full; wire [DATA_WIDTH-1:0] rx_fifo_data; // 实例化模块 baud_gen #(.CLK_FREQ(CLK_FREQ), .BAUD_RATE(BAUD_RATE)) u_baud_gen(.clk(clk), .rst_n(rst_n), .baud_tick(baud_tick), .sample_tick(sample_tick)); // TX FIFO (例如使用FPGA内置的Block RAM IP核或自己编写的小容量FIFO) fifo #(.WIDTH(DATA_WIDTH), .DEPTH(FIFO_DEPTH)) u_tx_fifo(.clk(clk), .rst_n(rst_n), .wr_en(tx_wr_en_i), .wr_data(tx_data_i), .full(tx_full_o), .rd_en(!tx_fifo_empty && !tx_busy_o && baud_tick), // 当发送器空闲且FIFO不空时,在波特率节拍下读数据 .rd_data(tx_fifo_data), .empty(tx_fifo_empty)); uart_tx #(.DATA_WIDTH(DATA_WIDTH)) u_tx(.clk(clk), .rst_n(rst_n), .data_in(tx_fifo_data), .send_en(!tx_fifo_empty), // FIFO不空即启动发送 .baud_tick(baud_tick), .tx_out(uart_tx), .tx_busy(tx_busy_o)); uart_rx #(.DATA_WIDTH(DATA_WIDTH), .OVERSAMPLE(16)) u_rx(.clk(clk), .rst_n(rst_n), .rx_in(uart_rx), .sample_tick(sample_tick), .data_out(rx_fifo_data), .data_valid(rx_valid_wire), // 内部连线 .frame_error()); // RX FIFO fifo #(.WIDTH(DATA_WIDTH), .DEPTH(FIFO_DEPTH)) u_rx_fifo(.clk(clk), .rst_n(rst_n), .wr_en(rx_valid_wire), .wr_data(rx_fifo_data), .full(rx_fifo_full), .rd_en(rx_rd_en_i), .rd_data(rx_data_o), .empty(rx_empty_o)); assign rx_valid_o = !rx_empty_o; // 当RX FIFO非空时,数据有效 endmodule这个顶层模块提供了一个友好的并行数据接口。用户只需向tx_data_i写数据并拉高tx_wr_en_i(前提是tx_full_o为低),数据就会进入FIFO并自动发送。接收到的数据则被存入RX FIFO,用户通过读取rx_data_o并拉高rx_rd_en_i来获取。
4.2 Testbench仿真验证
仿真(Simulation)是FPGA设计流程中保证逻辑正确性的关键一步。我们需要编写一个Testbench来模拟上位机与FPGA UART的交互。
`timescale 1ns / 1ps module tb_uart_top(); reg clk; reg rst_n; reg [7:0] tx_data_i; reg tx_wr_en_i; wire tx_full_o; wire tx_busy_o; wire [7:0] rx_data_o; reg rx_rd_en_i; wire rx_empty_o; wire uart_tx; reg uart_rx; // 实例化被测单元 uart_top #(.CLK_FREQ(50_000_000), .BAUD_RATE(115200)) uut (.*); // 使用 .* 连接所有同名信号 // 生成50MHz时钟 initial begin clk = 0; forever #10 clk = ~clk; // 20ns周期,即50MHz end // 模拟上位机发送数据到FPGA的RX task uart_send_byte; input [7:0] data; integer i; begin uart_rx = 1‘b1; // 空闲高电平 #8680; // 等待一个波特率周期 (1/115200 ≈ 8.68us) uart_rx = 1‘b0; // 起始位 #8680; for (i=0; i<8; i=i+1) begin // 发送8个数据位,LSB first uart_rx = data[i]; #8680; end uart_rx = 1‘b1; // 停止位 #8680; end endtask // 主测试流程 initial begin // 初始化 rst_n = 0; tx_data_i = 0; tx_wr_en_i = 0; rx_rd_en_i = 0; uart_rx = 1‘b1; #100; rst_n = 1; #1000; // 测试1:FPGA发送数据 $display("[Test1] FPGA TX Test"); tx_data_i = 8‘h55; // 发送数据 0x55 (01010101) tx_wr_en_i = 1‘b1; #20; tx_wr_en_i = 1‘b0; // 等待发送完成,可以通过监控tx_busy_o或简单延时 wait(tx_busy_o == 1‘b0); #10000; // 测试2:FPGA接收数据 $display("[Test2] FPGA RX Test"); uart_send_byte(8‘hAA); // 模拟上位机发送 0xAA #20000; // 等待接收处理完成 // 从RX FIFO读取数据 if (!rx_empty_o) begin rx_rd_en_i = 1‘b1; #20; rx_rd_en_i = 1‘b0; $display("Received data: 0x%h", rx_data_o); if (rx_data_o == 8‘hAA) $display("RX Test PASSED!"); else $display("RX Test FAILED!"); end else begin $display("RX FIFO is empty, Test FAILED!"); end // 测试3:连续收发测试 $display("[Test3] Loopback Test"); // 将TX和RX短接(在Testbench中直接赋值) assign uart_rx = uart_tx; // 回环 // 发送一串数据 repeat(5) begin tx_data_i = $random; tx_wr_en_i = 1‘b1; #20; tx_wr_en_i = 1‘b0; wait(tx_busy_o == 1‘b0); #10000; // 等待回环接收 if (!rx_empty_o) begin rx_rd_en_i = 1‘b1; #20; rx_rd_en_i = 1‘b0; $display("Sent: 0x%h, Received: 0x%h", tx_data_i, rx_data_o); end end #10000; $finish; end endmodule通过仿真波形,我们可以清晰地观察起始位、数据位、停止位的时序,以及FIFO的读写指针变化,确保逻辑符合预期。
4.3 板级调试实战与问题排查
将设计综合、实现并下载到FPGA开发板后,真正的挑战才开始。以下是几个最常见的调试场景和解决方法:
问题1:完全无通信,TX线无波形。
- 排查:首先用示波器或逻辑分析仪测量
uart_tx引脚。如果一直为高,检查:- 复位信号:确保
rst_n已正确释放(为高电平)。 - 时钟信号:用示波器测量输入到FPGA的系统时钟是否正常。
- 波特率使能:在代码中添加一个LED,用
baud_tick信号驱动它闪烁(频率很低,肉眼可见)。如果LED不闪,说明波特率发生器未工作,检查分频计算和计数器逻辑。 - 发送使能:确认
send_en或tx_wr_en_i信号是否被正确触发。
- 复位信号:确保
问题2:能发送但接收不到数据,或接收数据乱码。
- 排查:这是最典型的问题,90%以上源于波特率不匹配或起始位检测失败。
- 示波器测量:测量FPGA发出的TX波形,计算实际位宽时间,反推实际波特率,与预设值对比。检查系统时钟频率配置是否正确(
CLK_FREQ参数)。 - 起始位检测:在接收模块中,将状态机状态
state信号引出到LED或IO口,观察是否从IDLE进入了START_DET和RECV_DATA。如果没有,可能是RX引脚电平不对(默认应为高),或者起始位检测逻辑过于敏感/迟钝。可以尝试调整起始位确认的采样点(如从第8个sample_tick改为第7或第9个)。 - 采样点偏移:在
RECV_DATA状态,确保采样点正好在数据位的中心。由于线路延迟和上升/下降时间,有时中心点需要微调。可以在代码中增加一个可配置的sample_offset参数进行补偿。
- 示波器测量:测量FPGA发出的TX波形,计算实际位宽时间,反推实际波特率,与预设值对比。检查系统时钟频率配置是否正确(
问题3:通信不稳定,偶尔出错。
- 排查:
- 信号质量:用示波器观察RX/TX波形,看是否有过冲、振铃或毛刺。过长或未端接的导线可能引起信号完整性问题。必要时在FPGA引脚附近添加串联匹配电阻(如22欧姆或33欧姆)。
- 跨时钟域(CDC):如果用户接口时钟(如AXI总线时钟)与UART内部时钟不同源,那么用户侧的
tx_wr_en_i和rx_rd_en_i信号必须经过同步器处理才能接入UART模块,否则会导致FIFO指针错误和数据丢失。 - FIFO溢出:监控
tx_full_o和rx_fifo_full信号。如果频繁变高,说明数据产生或消费速度不匹配,需要增加FIFO深度或优化流控。
调试金律:“让硬件说话”。不要只依赖仿真和想象,一定要用示波器、逻辑分析仪(LA)等工具抓取真实信号。FPGA开发中,将内部关键信号(如状态机状态、计数器值、使能信号)分配到空闲IO口,用LA抓取,是定位复杂时序问题的终极手段。
5. 高级应用与扩展思路
一个基础的UART实现只是起点。基于此,我们可以进行多种功能扩展,这也是FPGA灵活性的体现:
- 硬件流控(RTS/CTS):在高速或大数据量传输时,增加请求发送(RTS)和清除发送(CTS)信号线,实现硬件流控,防止数据丢失。
- 自动波特率检测:设计一个算法,通过测量第一个起始位的低电平持续时间,自动计算并匹配对方的波特率。这对于需要自适应不同设备的应用非常有用。
- 多协议支持:在状态机中增加对数据位(7/8/9)、停止位(1/1.5/2)、奇偶校验(奇/偶/无)的可配置支持。
- DMA集成:将UART的FIFO与FPGA内部的DMA控制器连接,实现数据在串口和外部存储器(如DDR3,参考热词)之间的大批量、零CPU开销搬运。
- 多通道聚合:如热词所述,实现“多串口接收合并到单串口发送”。可以设计一个仲裁调度器,轮询多个RX通道,将数据打包后通过一个高速TX通道发送出去,常用于数据采集网关。
- 与软核处理器协同:将UART作为外设挂载到如NIOS II、MicroBlaze或RISC-V等软核CPU的总线上,通过驱动程序进行访问,构建更复杂的片上系统(SoC)。
实现这些扩展功能,本质上是对状态机的进一步丰富和对数据通路的精心设计。每一次扩展,都是对数字逻辑设计能力的深化。从最简单的UART出发,你最终可以构建出像“FPGA万兆网例程”或复杂工业通信协议(如Modbus)控制器那样强大的系统。这条路,就从你手上这个稳定可靠的UART内核开始。