
1. 项目概述与核心价值最近在整理手头的FPGA项目发现串口通信这块很多刚入门的朋友总是卡在发送逻辑的实现上。要么是时序对不上要么是数据发出去对方收不到调试起来一头雾水。今天我就结合“小梅哥Xilinx FPGA学习笔记10——串口通信发送”这个主题把FPGA实现串口发送的里里外外彻底拆解一遍。这不仅仅是照搬一个代码更重要的是理解从并行数据到串行比特流的完整转换过程以及如何用Verilog硬件描述语言精准地控制每一个时间点。串口UART作为最古老、最简单、也最常用的异步通信接口之一在FPGA与上位机如电脑、单片机、传感器模块的调试和数据交换中扮演着核心角色。掌握它就等于打通了FPGA与外界对话的第一道关卡。无论你是正在学习小梅哥教程的学生还是工作中需要快速实现通信功能的工程师这篇笔记都能帮你绕过我当年踩过的那些坑直接构建一个稳定、可靠的串口发送引擎。2. 串口通信发送的整体设计与思路拆解2.1 异步串口通信的核心原理在动手写代码之前我们必须先抛开FPGA搞清楚串口通信本身是怎么一回事。串口通信是异步的这意味着通信双方没有统一的时钟线来同步数据。那如何保证接收方能在正确的时间点采样到正确的数据呢秘诀就在于波特率和数据帧格式的严格约定。一个标准的串口数据帧以最常见的8N1格式为例包含以下部分起始位固定为1个比特时间的低电平逻辑0。这是帧开始的标志接收端检测到这个下降沿就启动一个数据帧的接收流程。数据位紧接着起始位之后通常是5-9位数据我们最常用的是8位即一个字节。数据位是实际要传输的信息从最低位LSB开始发送。校验位可选用于简单的错误检测比如奇校验或偶校验。在8N1格式中“N”就代表无校验位。停止位固定为1个或1.5个、2个比特时间的高电平逻辑1。标志着数据帧的结束并为下一帧的起始位下降沿提供必要的空闲状态。整个通信过程建立在双方预先约定好的波特率上例如9600 bps。波特率表示每秒传输的符号数在这里1个符号就是1个比特。因此每个比特的持续时间即比特时间是波特率的倒数对于9600波特率比特时间约为104.17微秒。发送端和接收端各自使用一个精度足够的本地时钟通常是波特率的16倍或更高来度量这个比特时间从而实现“自同步”。注意异步通信的关键在于“约定大于同步”。只要双方波特率误差在可接受范围内通常3%且严格按照帧格式收发通信就能成功。FPGA实现的核心就是生成一个精准的本地波特率时钟并严格按照帧格式控制TX引脚的电平变化。2.2 FPGA实现发送功能的模块化设计思路用FPGA实现串口发送本质上是一个并串转换和精确时序控制的过程。我们不能像单片机那样用一个UART外设而是要自己用逻辑门和寄存器把这个外设“造”出来。一个稳健的发送模块通常命名为uart_tx应该包含以下几个关键部分波特率时钟生成器这是发送时序的“心跳”。FPGA的主时钟频率如50MHz远高于波特率如9600Hz。我们需要通过分频或更精确的时钟管理产生一个与目标波特率同步的脉冲信号baud_clk或baud_en。这个脉冲的周期等于一个比特时间每个脉冲到来就意味着需要输出下一个比特。发送状态机这是模块的“大脑”。它定义了发送过程的各个阶段空闲IDLE、发送起始位START、发送数据位DATA、发送停止位STOP。状态机根据波特率时钟和当前状态决定下一个状态以及输出到TX引脚的电平。数据移位寄存器这是数据的“搬运工”。在开始发送时它将待发送的并行数据如8位加载进来。每发送一个数据位寄存器就右移一位将下一个要发送的位送到最低位方便输出。比特计数器这是进度的“标尺”。它记录当前已经发送了多少个比特起始位、数据位、停止位当计数达到一帧的总比特数时通知状态机可以结束当前帧回到空闲状态。这样的模块化设计使得代码结构清晰各司其职易于调试和维护。发送模块的对外接口通常很简单一个时钟输入clk一个复位输入rst_n一个数据有效信号data_valid一个8位并行数据输入data_in以及一个串行数据输出tx。3. 核心细节解析与实操要点3.1 波特率时钟的精准生成分频计数法生成波特率时钟最直接的方法是计数器分频。假设系统时钟clk为50MHz周期20ns目标波特率为9600bps。比特时间 1 / 9600 ≈ 104166.67 ns。需要计数的系统时钟周期数 比特时间 / 系统时钟周期 104166.67 ns / 20 ns ≈ 5208.33。这不是一个整数。如果我们简单取整为5208那么实际生成的波特率将是50e6 / 5208 ≈ 9600.61 bps误差约为0.006%完全在允许范围内。因此我们可以设置一个计数器从0计数到5207共5208个周期在每个计数周期结束时产生一个高脉冲作为波特率使能信号baud_en。这个baud_en脉冲的宽度就是一个系统时钟周期它用于驱动状态机和移位寄存器。Verilog代码片段示例parameter CLK_FREQ 50_000_000; // 50MHz parameter BAUD_RATE 9600; localparam BAUD_CNT_MAX CLK_FREQ / BAUD_RATE - 1; // 计算分频系数 reg [12:0] baud_cnt; // 需要13位才能容纳5208 reg baud_en; always (posedge clk or negedge rst_n) begin if(!rst_n) begin baud_cnt 0; baud_en 1b0; end else begin baud_en 1b0; // 默认拉低 if(baud_cnt BAUD_CNT_MAX) begin baud_cnt 0; baud_en 1b1; // 计数满产生一个时钟周期的使能脉冲 end else begin baud_cnt baud_cnt 1b1; end end end实操心得对于更高的系统时钟或更低的波特率分频系数会很大计数器位宽要设够。另外这种方法的误差对于标准波特率如9600, 115200通常可以接受。但如果追求极致精度尤其是在高波特率下可以考虑使用FPGA内部的锁相环PLL或时钟管理器MMCM来直接生成所需的波特率时钟或者使用分数分频算法。3.2 发送状态机的精妙设计状态机设计是串口发送的核心逻辑。一个典型的状态机包含4个状态其转换完全由baud_en信号驱动。IDLE空闲状态输出tx引脚保持高电平代表线路空闲。转换条件当外部输入的data_valid信号有效通常为高脉冲时表示有新的数据要发送。状态机在下一个baud_en到来时跳转到START状态同时将输入数据锁存到移位寄存器中。START发送起始位输出tx引脚输出低电平持续一个比特时间。转换条件当在此状态下检测到baud_en脉冲时表示起始位发送完毕。状态机跳转到DATA状态并复位比特计数器。DATA发送数据位输出tx引脚输出移位寄存器的最低位shift_reg[0]。每来一个baud_en脉冲就右移一位将新的最低位输出。转换条件比特计数器随着每个baud_en递增。当计数器指示所有数据位例如8位都已发送完毕时状态机跳转到STOP状态。STOP发送停止位输出tx引脚输出高电平持续一个或两个比特时间。转换条件当在此状态下检测到baud_en脉冲对于1个停止位来一次就跳转对于2个停止位来两次才跳转表示一帧数据发送完成。状态机跳转回IDLE状态并可以输出一个“发送完成”信号tx_done通知外部逻辑可以准备下一个数据。状态机设计的关键点状态编码可以使用可读性好的参数定义localparam也可以使用独热码One-Hot以提高性能但消耗更多触发器。输出逻辑可以采用组合逻辑输出与状态和当前数据有关也可以采用时序逻辑输出在baud_en边沿更新。后者更能保证输出信号的稳定性避免毛刺。比特计数器它是一个独立的计数器但在DATA状态下才有效。其最大值等于数据位宽。3.3 数据缓冲与流控的简单实现基础的发送模块在tx_done信号有效前无法接收新的数据。在实际应用中数据可能连续到来。为了避免数据丢失我们可以在发送模块前增加一个FIFO先入先出缓冲区。工作原理外部逻辑将数据写入FIFO。发送模块在空闲IDLE状态且FIFO非空时自动从FIFO读取一个数据启动发送过程。这样即使外部数据突发到达也能被缓冲起来平滑地发送出去。FPGA实现Xilinx FPGA的IP Catalog中提供了FIFO Generator IP核可以非常方便地配置一个同步或异步FIFO。我们只需要例化这个IP将它的写端口连接数据源读端口连接uart_tx模块的data_in和data_valid即可。流控更复杂的系统可能需要硬件流控RTS/CTS但对于大多数简单应用FIFO缓冲已经足够。我们可以通过监控FIFO的“几乎满”信号来通知数据发送方暂停发送实现简单的流控。4. 实操过程与核心环节实现4.1 完整的Verilog发送模块代码实现下面是一个针对Xilinx FPGA平台基于状态机设计的、包含FIFO接口提示的完整uart_tx模块代码。我们假设使用8位数据、无校验、1位停止位8N1系统时钟50MHz波特率9600。module uart_tx #( parameter CLK_FREQ 50_000_000, // 输入时钟频率 parameter BAUD_RATE 9600, // 波特率 parameter DATA_WIDTH 8, // 数据位宽 parameter STOP_BIT_NUM 1 // 停止位个数 )( input wire clk, // 系统时钟 input wire rst_n, // 低电平复位 // 用户接口 input wire [DATA_WIDTH-1:0] data_in, // 待发送数据 input wire data_valid, // 数据有效脉冲高有效 output reg tx_done, // 单字节发送完成脉冲 output wire tx_busy, // 发送忙标志 // UART物理接口 output reg tx // 串行输出 ); // 波特率分频计数 localparam BAUD_CNT_MAX CLK_FREQ / BAUD_RATE - 1; reg [12:0] baud_cnt; reg baud_en; // 发送状态机定义 localparam IDLE 2b00; localparam START 2b01; localparam DATA 2b10; localparam STOP 2b11; reg [1:0] state, next_state; // 数据移位寄存器与比特计数器 reg [DATA_WIDTH-1:0] shift_reg; reg [3:0] bit_cnt; // 计数数据位4位足够计到15 // 波特率时钟生成逻辑 always (posedge clk or negedge rst_n) begin if (!rst_n) begin baud_cnt 0; baud_en 1b0; end else begin baud_en 1b0; if (baud_cnt BAUD_CNT_MAX) begin baud_cnt 0; baud_en 1b1; end else begin baud_cnt baud_cnt 1b1; end end end // 状态机第一段时序逻辑状态寄存器 always (posedge clk or negedge rst_n) begin if (!rst_n) state IDLE; else state next_state; end // 状态机第二段组合逻辑状态转移 always (*) begin next_state state; // 默认保持状态 case (state) IDLE: begin if (data_valid) // 有数据要发送 next_state START; end START: begin if (baud_en) next_state DATA; end DATA: begin if (baud_en (bit_cnt DATA_WIDTH - 1)) // 数据位发完 next_state STOP; end STOP: begin if (baud_en) // 停止位发完回到空闲 next_state IDLE; end default: next_state IDLE; endcase end // 状态机第三段时序逻辑输出与寄存器更新 always (posedge clk or negedge rst_n) begin if (!rst_n) begin tx 1b1; // 空闲时为高电平 shift_reg 0; bit_cnt 0; tx_done 1b0; end else begin tx_done 1b0; // 默认拉低 case (state) IDLE: begin tx 1b1; // 保持高电平 if (data_valid) begin // 锁存数据 shift_reg data_in; end end START: begin if (baud_en) begin tx 1b0; // 输出起始位 bit_cnt 0; // 复位比特计数器 end end DATA: begin if (baud_en) begin tx shift_reg[0]; // 输出最低位 shift_reg {1b0, shift_reg[DATA_WIDTH-1:1]}; // 右移 bit_cnt bit_cnt 1b1; // 计数加1 end end STOP: begin if (baud_en) begin tx 1b1; // 输出停止位 if (bit_cnt STOP_BIT_NUM - 1) begin // 停止位发够个数 tx_done 1b1; // 产生完成脉冲 end else begin bit_cnt bit_cnt 1b1; end end end endcase end end // 发送忙信号只要不在IDLE状态就是忙 assign tx_busy (state ! IDLE); endmodule4.2 测试平台搭建与仿真验证代码写好了但绝不能直接上板子。仿真Simulation是保证设计正确的关键一步。我们需要编写一个测试平台Testbench来模拟真实环境。测试平台的核心任务生成时钟和复位信号。实例化Instantiate待测试的uart_tx模块。编写激励Stimulus模拟上游逻辑在特定时间向uart_tx模块发送有效数据data_valid脉冲和data_in。观察输出在仿真波形中查看tx引脚上的波形检查其是否符合8N1格式数据是否正确。也可以将tx信号连接到一个虚拟的接收器模型进行自动比对。一个简单的测试平台示例框架timescale 1ns / 1ps module tb_uart_tx(); reg clk; reg rst_n; reg [7:0] data_to_send; reg send_en; wire tx; wire tx_done; wire busy; // 实例化被测模块 uart_tx u_uart_tx ( .clk(clk), .rst_n(rst_n), .data_in(data_to_send), .data_valid(send_en), .tx_done(tx_done), .tx_busy(busy), .tx(tx) ); // 生成50MHz时钟 initial clk 0; always #10 clk ~clk; // 20ns周期即50MHz // 测试过程 initial begin // 初始化 rst_n 0; data_to_send 8h00; send_en 0; #100; // 复位一段时间 rst_n 1; #1000; // 测试用例1发送数据 0x55 (01010101) (posedge clk); data_to_send 8h55; send_en 1; (posedge clk); send_en 0; // 产生一个时钟周期的高脉冲 // 等待发送完成 wait(tx_done 1); #10000; // 等待一段时间 // 测试用例2发送数据 0xAA (10101010) (posedge clk); data_to_send 8hAA; send_en 1; (posedge clk); send_en 0; // 等待发送完成 wait(tx_done 1); #10000; $finish; // 结束仿真 end endmodule在仿真工具如Vivado Simulator、ModelSim中运行此测试平台观察波形。你应该能看到tx信号上出现清晰的帧结构先变低起始位然后是8个数据比特0x55的LSB在前所以波形是10101010最后变高停止位。通过测量高低电平的持续时间可以验证其是否符合104us左右的比特时间。4.3 上板实测与调试技巧仿真通过后就可以进行上板实测了。你需要分配引脚在Xilinx Vivado的约束文件.xdc中将tx信号分配到FPGA开发板上UART接口对应的物理引脚。连接硬件用USB转串口线将FPGA开发板的UART接口与电脑连接。使用串口调试助手在电脑上打开串口调试助手如SecureCRT、Putty、或者小梅哥资料包里常带的调试工具设置正确的COM口、波特率9600、数据格式8N1。编写顶层测试代码创建一个顶层模块例化uart_tx并编写一个简单的逻辑例如每隔1秒发送一个递增的字符或固定的字符串“Hello FPGA!”。下载比特流将综合实现后生成的.bit文件下载到FPGA中。调试常见问题与技巧电脑收不到任何数据检查引脚分配确认.tx引脚是否分配正确是否与原理图一致。检查波特率确认FPGA代码中的波特率与串口助手设置的完全一致。计算分频系数时检查是否溢出或计算错误。检查线序USB转串口线通常只需要连接TX、RX和GND三根线。确保FPGA的TX连接到了USB线的RXFPGA的RX连接到了USB线的TX交叉连接。用示波器或逻辑分析仪抓取波形这是最直接的调试手段。观察FPGA的TX引脚是否有波形输出波形是否符合串口帧格式比特时间是否正确。收到乱码最常见原因波特率不匹配。即使计算值只有微小误差在长时间传输后也可能造成累积错误。仔细核对计算过程或尝试使用PLL生成更精确的时钟。数据位序错误确认发送模块是从LSB开始发送。串口调试助手也要设置为LSB first通常默认就是。停止位设置确认代码中停止位个数与串口助手设置一致。发送不连续或丢数据检查tx_busy信号确保在发送模块忙时没有新的data_valid信号进来。或者如前所述加入FIFO进行缓冲。检查时序约束确保系统时钟的时序约束正确没有建立/保持时间违规。实操心得上板调试时先从一个最简单的功能开始——固定发送一个字节比如0x55或0xAA。因为它们的比特模式是交替的01010101或10101010在示波器上看起来是标准的方波非常容易判断波特率是否正确。成功后再测试发送连续数据。5. 性能优化与高级应用扩展5.1 使用PLL精确生成波特率时钟当系统时钟频率不是波特率的整数倍时分频计数法会引入误差。对于高波特率如115200、921600或对时钟精度要求极高的应用如长距离通信可以使用FPGA内部的PLL来生成一个频率正好是波特率16倍或更高的时钟。例如要生成精确的115200 Hz波特率时钟可以配置PLL输入50MHz输出1.8432 MHz115200 * 16。这个1.8432 MHz的时钟再通过一个简单的16分频计数器就能得到精准的115200 Hz波特率使能信号。Vivado中的Clock Wizard IP可以轻松配置PLL。5.2 添加FIFO实现连续发送如前所述添加FIFO是提升模块实用性的关键。下面简要说明集成FIFO的顶层设计例化FIFO IP核配置为同步FIFO宽度8位或更宽深度根据数据吞吐量设定如512、1024。启用“Almost Full”标志。修改顶层逻辑外部数据写入FIFOwr_en和din。uart_tx模块的data_valid信号不再由外部直接控制而是由FIFO非空!empty且uart_tx空闲!tx_busy的条件触发。当上述条件满足时从FIFO读取一个数据rd_en拉高送到uart_tx的data_in同时产生一个data_valid脉冲。可以将FIFO的“Almost Full”信号输出给数据产生模块作为反压Back Pressure信号防止FIFO溢出。这样只要FIFO里有数据发送模块就会自动、连续地发送直到FIFO为空实现了简单的流量控制。5.3 自定义协议帧封装单纯的字节发送能力是基础。在实际项目中我们通常需要传输有意义的命令或数据包。这就需要定义简单的通信协议。一个非常常用的轻量级协议帧格式是帧头 长度 命令/数据 校验和。帧头1-2个特殊的字节如0xAA、0x55用于标识一帧的开始帮助接收方进行帧同步。长度1个字节指示后面“命令/数据”字段的字节数。命令/数据可变长度的有效载荷。校验和1个字节可以是前面所有字节的累加和取低8位或者异或和用于检测传输错误。在FPGA中我们可以编写一个协议封装模块。该模块接收需要发送的有效载荷自动为其添加帧头、长度和校验和然后将完整的帧字节流写入到UART发送模块或前面的FIFO中。接收端通常是PC软件或单片机按照相同的协议进行解析从而实现了可靠的结构化数据传输。6. 常见问题与排查技巧实录在多年的FPGA开发和教学实践中我总结了以下几个最常遇到的串口发送问题及其排查思路希望能帮你快速定位问题。问题现象可能原因排查方法与解决思路完全无数据输出1. 引脚分配错误或约束文件未生效。2. 顶层模块未正确例化或信号未连接。3. 复位信号一直有效逻辑未启动。4. 波特率生成逻辑错误baud_en永远为低。1. 检查.xdc文件确认端口名、引脚号、电平标准正确。上板后可用万用表测量引脚电压。2. 检查综合后的原理图RTL Schematic看模块是否被优化掉信号连接是否正确。3. 在代码中暂时注释掉复位逻辑或确保上电后复位信号已释放。仿真时检查复位时序。4. 仿真时观察baud_cnt和baud_en的波形看计数器是否正常循环使能脉冲是否产生。有波形输出但电脑端乱码1.波特率不匹配最常见。2. 数据位、停止位、校验位设置与串口助手不一致。3. 比特顺序LSB/MSB错误。4. 信号毛刺或抖动严重。1.用示波器测量比特时间。发送0x5501010101测量一个脉冲周期高或低电平。对于9600波特率应为104us左右。偏差过大则调整分频系数。2. 核对代码中的帧格式定义与串口助手设置必须完全一致。3. 确认发送代码是从shift_reg[0]LSB开始输出。串口助手通常默认LSB first。4. 检查PCB走线过长或干扰大可能引起波形畸变。可在FPGA输出端串联一个小电阻如22欧姆以减少反射。发送单个字节正常连续发送出错1. 状态机未能正确回到IDLE状态或tx_done信号有问题。2. 连续发送间隔太短新数据冲掉了正在发送的数据。3. FIFO逻辑有误导致读写出错。1. 仿真连续发送多个字节仔细观察状态机转换和tx_done信号。确保发送完一帧后tx_busy信号拉低且模块准备好接收新数据。2.必须等待tx_done或!tx_busy信号有效后再送入下一个data_valid。或者引入FIFO缓冲。3. 仿真FIFO的读写逻辑检查空满标志、读写使能时序是否正确。高波特率下如115200不稳定1. 系统时钟频率不够高分频后波特率时钟误差大。2. 时序违例逻辑速度跟不上。3. 串口线或接口质量差不支持高波特率。1. 使用PLL生成更精准的时钟。计算理论误差115200要求较高50MHz系统时钟分频误差很小但921600可能就需要PLL了。2. 在Vivado中运行时序报告Timing Report检查是否有建立/保持时间违规。优化关键路径逻辑或对baud_en等信号使用时钟使能CE寄存器打拍。3. 更换质量好的USB转串口模块如FTDI、CP2102芯片的模块。一个关键的调试习惯分层验证。不要试图一次性调通整个复杂系统。先从波特率生成模块开始仿真看baud_en的周期对不对。然后验证状态机看状态转移是否符合预期。接着用仿真发送固定数据看tx波形。最后再上板用示波器看实际波形。每一步都确认无误最终的问题往往就迎刃而解了。FPGA的串口发送实现是一个非常好的同步时序逻辑设计练习。它涵盖了时钟分频、状态机、计数器、移位寄存器等数字逻辑的核心要素。吃透这个模块不仅能让你轻松应对各种串口通信需求更能深刻理解FPGA如何用硬件思维来实现软件工程师看来很简单的功能。当你看到自己编写的代码在示波器上呈现出完美的串口波形在电脑终端上显示出预期的字符时那种成就感就是学习硬件描述语言最大的乐趣所在。