news 2026/9/5 10:26:39

FPGA实现UART串口通信:从协议到Verilog代码全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
FPGA实现UART串口通信:从协议到Verilog代码全解析

1. 项目概述:从0到1在FPGA上实现UART串口通信

做FPGA开发绕不开串口。无论是调试内部信号、和上位机交互、还是作为板级联调的基础通道,UART基本是每个FPGA工程师的必修课。我记得自己第一次拿到开发板,点亮LED之后做的第二件事,就是尝试用Verilog写一个UART发送模块,把“Hello”打到电脑的串口助手上。当时觉得这东西简单,无非是移位寄存器加计数器,真正动手之后才发现里面的坑不少——波特率怎么算、时钟分频误差怎么控制、接收端采样点选在哪儿、跨时钟域怎么处理,这些细节没想清楚,写出来的代码十有八九在板子上跑不通。

这篇内容我根据自己做过的几个项目整理出来,从协议原理、Verilog实现、仿真验证到板上调试全流程走一遍,最后把踩过的坑和排查方法也一并列出来。无论你是刚接触FPGA的初学者,还是已经写过一些模块、想系统梳理串口通信细节的开发者,这篇文章都值得仔细看一遍。

先说清楚本文讲的是什么:用FPGA做UART串口通信,重点在“用Verilog实现UART收发逻辑”这件事本身,不依赖任何厂商IP核,从底层寄存器级设计做起。这样做的好处是你能真正理解串口通信的每一个时钟周期发生了什么,而不是简单调用一个现成的IP然后黑盒使用。等你自己写通了收发模块,后面不管是接RS232、RS485、还是通过USB转串口和PC通信,底层逻辑都是一样的,只需改动物理层接口电路即可。

2. 协议拆解与整体设计思路

2.1 UART时序到底在传输什么

串口通信的核心就是一根线,按约定的速率把数据一位一位地发出去。UART协议在空闲时保持高电平,起始位是一个低电平,然后依次传输数据位(通常低位在前)、可选的校验位,最后是停止位(高电平)。一帧标准波形长这样:空闲高电平 → 起始位低电平 → 8个数据位 → 1个停止位高电平。

这里面的关键参数就两个:波特率(每秒传输多少个bit)和数据帧格式(起始位1位、数据位8位、校验位无、停止位1位,也就是常说的8N1)。实际工程里,8N1格式占了90%以上的场景,本文就围绕这个格式展开。

接收端的采样逻辑值得细说。发端在TX线上按波特率逐位发送电平,收端如果要正确读到数据,需要在一个bit的持续时间内选一个正确的采样点。工程上最常用的做法是波特率时钟的16倍过采样——在每个bit周期内采样16个点,然后选择第8个采样点作为该bit的最终值,也就是取bit中间位置。这样做的原因是:起始位下降沿的到来时间不一定和本地采样时钟严格对齐,如果不做同步采样,首bit很容易错位,而一旦首bit错位,后面所有数据位都会跟着错。

从协议解析的角度去理解,接收模块核心做两件事:一是检测起始位的下降沿,二是以稳定的过采样时钟去读每个数据位。发送模块则简单得多,只需要按波特率时钟把并行数据转成串行逐位输出。理解了这个基本框架,再看后面代码就不会觉得乱。

2.2 波特率生成:分频计数的边界条件

FPGA内部时钟通常是50MHz、100MHz甚至更高,而串口波特率常见的是9600、115200、460800、921600。用系统时钟去产生波特率,本质上就是一个计数器分频。以50MHz时钟产生115200波特率为例,需要计算分频计数值。

计算方法是系统时钟频率除以波特率:50000000 / 115200 ≈ 434.03。这个结果不是整数,这就引出了整个UART设计中最重要的精度问题。实际分频时只能取整数434,那么实际产生的波特率是50000000 / (434 + 1) ≈ 114943,和标准115200的误差约为0.22%。

那0.22%的误差能不能接受?串口通信的误差容限经验值是±2%,对于115200这个速率,收发两端各自有0.22%的误差,而且方向可能相反,叠加起来也只有0.44%,完全在容限内。但要注意,如果系统时钟是27MHz或33MHz这类非整数倍关系,分频误差可能会接近甚至超过1%,此时建议用更高的系统时钟(100MHz以上)再做分频,误差会显著下降。

具体计算为什么是cnt = 频率/波特率 - 1?因为计数器从0计数到目标值后再清零重新计数,实际分频后的频率是f = clk / (cnt + 1)。以115200波特率为例,cnt = 50000000 / 115200 - 1 = 433.03,取整为433,实际波特率 = 50000000 / 434 = 115207,误差0.007%,反而比用434更精确。这里有一个细节:四舍五入还是向下取整,要看实际算出来的小数部分是否接近0.5,以及你的系统容错能力。工程上最简单实用的方法是:算出精确值,四舍五入到最接近的整数,然后反算一下实际波特率和误差,控制在±1%以内就可以放心用。

还有一个很多人忽略的问题:如果计数器溢出判断条件写成cnt == 433,会产生一个时钟周期的脉冲,这个脉冲作为波特率时钟使能信号来驱动后续状态机,而不是用它去生成占空比50%的时钟。这两者的区别在于:发送数据时用脉冲使能方式,可以保证移位寄存器在波特率时钟的上升沿准确移位,而不会因为时钟抖动产生不确定状态。我在工程中一直用的就是这个脉冲使能方式。

2.3 接收端采样点选择:为什么是十六分之一与二分之一采样

接收端的代码比发送端复杂一个量级,难点在于你不知道数据什么时候来。发端和收端的时钟有微小偏差,起始位到达的时刻也可能偏离你的采样周期。如果只用一个波特率时钟直接采样,遇到时钟偏差稍大的情况就容易读错位。

主流方案是16倍过采样。系统时钟分频产生16倍波特率的采样时钟,例如115200波特率对应1.8432MHz采样时钟,50MHz主频下分频值cnt = 50000000 / 1843200 - 1 = 26.13,取26即可。每次检测到起始位下降沿,从下一个采样时钟开始计数,计数到7或8时读取电平作为该bit的值。7或8就是15个采样点或16个采样点对应半个bit周期的位置,取bit正中位置,远离跳变沿,抗干扰能力最优。

这里有个实际心得:在工程中不要只采样一次,而是在第6、7、8个采样点上连续采3次,采取多数表决机制,取出现次数最多的电平作为最终值。我做过对比测试,直接用8号点单次采样,在信号质量较差的rs232线缆连接场景下,偶尔会出现误码;改用三点多数表决后,长时间压力测试零误码。代价仅仅是多两个触发器和简单的组合逻辑,非常划算。

为什么要检测下降沿而不是直接等待?因为总线空闲时是高电平,起始位是低电平,高到低的跳变就是有效的起始标志。采样检测时,一般连续检测到若干个采样点为低才确认为有效起始位,这样可以滤除毛刺干扰。这个“防抖”逻辑在实际工程中非常重要,我后面会专门讲。

3. Verilog代码实现与核心逻辑细化

3.1 发送模块:状态机设计思路

发送模块我用三段式状态机实现,状态定义很简单:IDLE、START、DATA、STOP。

IDLE状态下,tx线保持高电平,当发送使能信号tx_start拉高时,锁存待发送的并行数据,进入START状态。START状态下,tx线输出低电平,持续1个波特率周期,然后进入DATA状态。DATA状态是一个计数器驱动的移位循环,bit_cnt从0到7,每个波特率周期输出一位数据,先发低位。发完8位后进入STOP状态,tx线恢复高电平,持续1个波特率周期,最后回到IDLE状态。

伪代码如下,核心是发送状态机状态转换:

always @(posedge clk or negedge rst_n) begin if (!rst_n) begin tx_state <= IDLE; tx_byte <= 8'h00; bit_cnt <= 3'd0; tx_line <= 1'b1; end else begin case (tx_state) IDLE: begin if (tx_start) begin tx_byte <= tx_data; tx_state <= START; end end START: begin tx_line <= 1'b0; tx_state <= DATA; end DATA: begin if (baud_pulse) begin tx_line <= tx_byte[bit_cnt]; bit_cnt <= bit_cnt + 1'b1; if (bit_cnt == 3'd7) tx_state <= STOP; end end STOP: begin tx_line <= 1'b1; if (baud_pulse) tx_state <= IDLE; end endcase end end

这段代码有几个细节值得抠一下。DATA状态下,tx_line <= tx_byte[bit_cnt]bit_cnt <= bit_cnt + 1是在同一个波特率脉冲下完成的,这意味着tx_line输出的第0位持续一个完整波特率周期后,bit_cnt变为1,下一次脉冲到来时再输出第1位。起始位持续一个波特率周期后进入DATA,数据位的第一个bit也在下一个时钟沿输出,所以每个bit持续完整的一个波特率周期,时序上没有问题。

发送完成标志怎么做?我习惯在STOP状态结束前拉高一个tx_done脉冲,持续一个系统时钟周期,用于通知外部模块“这一帧发完了,可以发下一帧了”。这个标志配合FIFO使用时非常重要,如果做数据回环测试,没有这个标志位,你很难确定发送FIFO该什么时候弹出下一个数据。

3.2 接收模块:过采样与位同步逻辑

接收模块我用一个always块处理采样计数,一个always块处理状态机,关键代码如下:

// 采样时钟分频,产生16倍波特率时钟 reg [7:0] sample_cnt; wire sample_pulse = (sample_cnt == SAMPLE_CNT_MAX - 1); // 每个采样脉冲时采样一次rx线,同时进行状态机处理 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin rx_state <= IDLE; sample_cnt <= 8'd0; rx_data <= 8'h00; bit_cnt <= 3'd0; end else begin case (rx_state) IDLE: begin if (!rx_line) begin // 检测到下降沿 rx_state <= START; sample_cnt <= 8'd0; end end START: begin // 在起始位中点采样,判断是否确实为低电平 if (sample_pulse) begin if (sample_cnt == 8'd7) begin if (!rx_line) rx_state <= DATA; else rx_state <= IDLE; // 毛刺,重新等待 sample_cnt <= 8'd0; end else begin sample_cnt <= sample_cnt + 1'b1; end end end DATA: begin // 每隔16个采样周期采样一次数据位 if (sample_pulse) begin if (sample_cnt == 8'd15) begin rx_data[bit_cnt] <= rx_line; bit_cnt <= bit_cnt + 1'b1; sample_cnt <= 8'd0; if (bit_cnt == 3'd7) rx_state <= STOP; end else begin sample_cnt <= sample_cnt + 1'b1; end end end STOP: begin if (sample_pulse) begin if (sample_cnt == 8'd15) begin rx_state <= IDLE; rx_done <= 1'b1; end else begin sample_cnt <= sample_cnt + 1'b1; end end end endcase end end

START状态里,检测到下降沿后并不会立即开始读数据位,而是等16个采样周期中的第7~8个采样点处再确认一下rx线确实为低电平。这个延迟一个半bit的操作本质是“确认这真的是起始位而不是噪声”。如果你在这个中点采样点发现rx线已经回到了高电平,说明刚才的下降沿只是毛刺干扰,可以放弃这帧数据回到IDLE等待。

进入DATA状态后,从start状态中点采样的那个周期开始算,每隔16个采样周期采样一次,正好落在每个数据位的中间位置。比如第7个采样点确认起始位为低,接着计数到第15个采样点读第0位,之后每隔16个采样点读下一bit。总之,通过过采样和位同步,可以确保采样点稳定落在每个bit的中心。

3.3 参数化设计:怎么让模块适配任意波特率和时钟频率

写模块时如果不做参数化,每次换系统时钟或换波特率都要重改代码,非常烦。我的做法是全部用parameter定义,顶层例化时覆写即可。

module uart_tx #( parameter CLK_FREQ = 50_000_000, parameter BAUD_RATE = 115200 )( input wire clk, input wire rst_n, input wire [7:0] tx_data, input wire tx_start, output reg tx_line, output reg tx_done ); localparam BAUD_CNT_MAX = CLK_FREQ / BAUD_RATE - 1; // ... 内部逻辑 endmodule

这样设计的好处显而易见:顶层需要9600波特率时,例化时传入#(.BAUD_RATE(9600))即可,内部逻辑完全不用动。同样,接收模块参数化采样时钟,SAMPLE_CNT_MAX = CLK_FREQ / (BAUD_RATE * 16) - 1

还有一个小点:CLK_FREQ和BAUD_RATE如果写死成数字,将来做9000波特率、2M波特率这类特殊速率时,组合参数运算可能会因为取整误差导致实际波特率偏离较大。这时可以在仿真环境里加一个$display打印实际波特率和误差百分比,确保参数设计正确后再上板。

3.4 模块例化与顶层连接:一次性搞定收发

顶层模块需要例化一个发送器和一个接收器,同时考虑回环测试和数据交互。最简单的回环测试做法:把接收到的数据立即发送回去。这样不需要电脑参与,只用一根杜邦线把tx和rx短接,就能观察数据是否自发自收成功。

顶层伪代码:

wire [7:0] rx_data; wire rx_done; wire [7:0] tx_data = rx_data; wire tx_start = rx_done; uart_rx #( .CLK_FREQ(50_000_000), .BAUD_RATE(115200) ) u_rx ( .clk(clk), .rst_n(rst_n), .rx_line(uart_rx_pin), .rx_data(rx_data), .rx_done(rx_done) ); uart_tx #( .CLK_FREQ(50_000_000), .BAUD_RATE(115200) ) u_tx ( .clk(clk), .rst_n(rst_n), .tx_data(tx_data), .tx_start(tx_start), .tx_line(uart_tx_pin), .tx_done(tx_done) );

回环方式是最快的功能验证方式,但在实际项目中,收发两端通常连着不同的数据源和目的地。比如上位机发来控制指令,FPGA解析后执行动作,再通过UART返回状态信息。这种情况下,接收完成信号rx_done要接进一个命令解析器,而不是直接连到发送使能。解析器的设计我会在第5章结合多字节协议一并讲。

4. 实操与验证:用仿真和板上调试检验代码

4.1 搭建一个能用的Testbench

仿真在UART开发中占比非常高。我见过不少初学者直接上板写代码,结果全编译烧进去波形出来不对,然后开始痛苦地找问题。正确的做法是先写好testbench,仿真过了再上板,排错时间能减少一半以上。

仿真testbench的核心是模拟发送端发送一帧数据给uart_rx模块,观察rx_data输出是否正确;同时验证uart_tx模块,从外部给它一个tx_start脉冲,观察tx_line输出波形是否符合UART协议。

Testbench关键代码:

`timescale 1ns/1ps module uart_tb; reg clk = 0; reg rst_n = 0; reg rx_line = 1; reg [7:0] tx_data = 8'hA5; reg tx_start = 0; wire [7:0] rx_data; wire rx_done; wire tx_line; // 时钟50MHz always #10 clk = ~clk; uart_rx #(.CLK_FREQ(50_000_000), .BAUD_RATE(115200)) u_rx ( .clk(clk), .rst_n(rst_n), .rx_line(rx_line), .rx_data(rx_data), .rx_done(rx_done) ); uart_tx #(.CLK_FREQ(50_000_000), .BAUD_RATE(115200)) u_tx ( .clk(clk), .rst_n(rst_n), .tx_data(tx_data), .tx_start(tx_start), .tx_line(tx_line), .tx_done(tx_done) ); // 模拟串口主机发送数据给rx模块 task uart_send_byte(input [7:0] data); integer i; begin #8680 rx_line = 0; // 起始位 for (i = 0; i < 8; i = i + 1) begin #8680 rx_line = data[i]; end #8680 rx_line = 1; // 停止位 #8680; end endtask initial begin #100 rst_n = 1; #200 uart_send_byte(8'hA5); #20000; // 测试发送:使能tx tx_start = 1; #20 tx_start = 0; #100000; $finish; end endmodule

注意#8680这个延时是怎么来的:115200波特率时,1个bit的时间为1/115200 ≈ 8.68us,即8680ns。这个延时要和仿真时间尺度匹配,否则波形会错位。

测试中一个常见的坑是:testbench里的#8680与实际代码中的baud_pulse位置不同步,可能在起始位还没发完时采样点已经偏移。如果仿真波形中rx_data读出的值不稳定,不要急着改代码,先检查testbench的时序是否和模块期望的对齐。我一般会在testbench的uart_send_byte里加一个#4340的延时(半个bit周期),确保起始位中点落在采样窗口内。

4.2 上板验证:串口助手与回环测试实操流程

仿真过了之后上板,准备材料:FPGA开发板(我用的是Xilinx Artix-7系列)、一根USB转串口线(CP2102或FT232芯片的都行)、上位机串口工具(我最常用的是MobaXterm自带的串口功能或XCOM)。

上板测试第一步:把tx引脚和rx引脚用杜邦线短接,实现自发自收。把上面写的顶层代码烧录进去,在串口助手里发送一个字节,如果回显相同字节,说明收发通路正常工作。回环测试的优势是可以先排除外部连接问题,专注验证FPGA内部逻辑。

第二步:断开回环,把FPGA的rx引脚和USB转串口的tx引脚连接,同时FPGA的tx引脚连接USB转串口的rx引脚,再配合地线(GND)。这个接线是很多新人容易出错的地方:tx对rx、rx对tx、GND必须共地。不共地会导致电平参考不一致,偶尔能收到数据但大多时候乱码。

上板如果发现接收乱码,优先怀疑三点:波特率不匹配(差太大直接乱码)、电平不匹配(FPGA的3.3V TTL直接接RS232电平会烧接口或者读不到数据)、接线错误(tx接tx会自收不到)。我见过一个典型案例:开发板上标着“RS232”接口,但实际上是板载电平转换芯片转换过的DB9接口,直接拿TTL逻辑去接,死活不通。后来查原理图发现需要接板载串口引脚,而不是DB9座子。

4.3 用逻辑分析仪辅助确认时序

波特率跑在460800以上时,单纯用示波器看帧格式已经比较吃力,我习惯用逻辑分析仪抓波形。Saleae Logic 16这种级别的逻辑分析仪很够用了,采样率调到25MHz以上,抓tx和rx两个通道的波形,可以直接在软件里解码UART协议,看到每一帧的起始位、数据位、停止位,以及那一位出现错误。

排查时一个实用技巧:抓rx引脚的波形,看起始位下降沿到停止位结束的时间宽度,换算成波特率是否准确。比如标准115200波特率下一帧9.6bit大约83.3us。如果实测有90us,说明波特率偏慢,检查分频计数值和系统时钟频率是否匹配。

如果逻辑分析仪显示rx波形正常,但FPGA读出的数据不对,问题大概率出在接收模块的采样逻辑上,回仿真去看到底哪个采样点选错了。仿真和实测结合是最快的排查路径。

5. 常见问题与排查技巧实录

5.1 乱码问题的六大可能原因对照

做了几年FPGA,串口乱码大概是我被问过最多的问题。有些是低级错误,有些确实隐蔽,我整理了一个排查顺序表,基本能覆盖99%的乱码场景:

排查项检查方法解决方案
波特率不匹配核对上位机和代码参数统一为115200,检查分频参数
时钟频率不对看系统时钟是50M还是100M确认PLL输出频率与代码参数一致
电平不匹配TTL接了RS232,或别的电平标准加电平转换电路或改接对应引脚
数据位和停止位设置不对检查串口助手配置统一为8N1格式
TX/RX接反检查杜邦线连接TX接RX,RX接TX
地线未共地检查GND连接USB转串口和FPGA板必须共地

其中最隐蔽的其实是“异步时钟域”问题。如果你的FPGA系统时钟来自PLL,而USB转串口使用的是PC端晶振,两边时间基准本来就有微小误差(几ppm到几百ppm),但115200波特率下这点误差不足以导致乱码。真正的灾难性问题是PLL配置错误导致系统时钟和你以为的不一致。比如代码里写的是50MHz分频参数,实际PLL输出却是100MHz,这时波特率会整整偏一倍,逻辑分析仪上一看就露馅了。

5.2 接收首字节丢失的排查思路

还有一个高频问题:收数据时第一个字节总是丢失,后续数据正常。我遇到过三四次这个现象,排查到最后基本都是同一个原因——复位释放之后,接收模块的初始状态没问题,但发送方在FPGA准备好之前就已经发出了起始位。

解决方案有几种。最简单的方式是上电后加一个延时逻辑,等系统稳定后再释放内部复位;或者在接收模块里加一个“busy”状态,上电后短时间内(比如10ms)不检测起始位,强制等待线路变为空闲高电平后再进入IDLE。加上这个“空闲检测”逻辑后,即使发送端在FPGA复位期间就开始发数据,FPGA也会等当前帧结束后再拾取下一帧,不会造成首字节丢失。

如果你用的是USB转串口模块,还要注意它的枚举时间。CP2102插入电脑后,Windows需要几百毫秒完成驱动加载,如果FPGA复位一结束就自动向上位机发送“Hello”,大概率会丢失,因为上位机串口根本没打开。最好的做法是做一个“等待上位机下发一个字节后再开始握手通信”的逻辑,这在实际产品中也更合理。

5.3 高速率下的信号完整性注意事项

115200波特率下,你基本不用关心信号完整性,随便飞线都能通。但当你把波特率提高到921600甚至2Mbps时,电平转换芯片、杜邦线、接口电容都会成为限制因素。

我实测过两款常见的USB转串口芯片:CP2102支持最高1Mbps,FT232RL可以到3Mbps,但实际用杜邦线飞线时,921600下的波形已经有明显的振铃和过冲。这时建议把波特率控制到460800以下,或者换用屏蔽线、尽量缩短杜邦线长度,如果是PCB设计,尽量把串口走线控制在5cm以内。

还有一个容易忽视的点:RS232电平转换芯片(比如MAX3232)的压摆率有限,高速率下波形边沿会变缓,可能导致接收端采样出错。如果项目要求高速率长距离传输,优先考虑RS485方案而非RS232,RS485在差分信号和速率距离方面都有明显优势。

5.4 短帧连续发送时的FIFO与流控策略

另一个常见的项目需求是连续发送大量数据,比如把FPGA内部采集的ADC数据流通过串口发到上位机。如果直接用发送状态机,上一帧还没发完,新数据又来了,就需要加FIFO缓冲。

我建议调试时先在发送端加一个8字节深度的FIFO,数据写入FIFO,发送模块空闲时从FIFO取出数据发送。当FIFO接近满时,通过一个almost_empty/full信号通知上游模块暂停写入。这个设计避免了数据覆盖,也让发送节奏更加可控。

接收端同理,如果上位机发送的数据是连续的,而且FPGA处理速度不够快,接收侧FIFO很容易溢出丢数据。最简单的流控策略是:FPGA在接收完一个字节后,如果有需要,向PC发送一个CTS/RTS控制信号(或者用软件流控XON/XOFF),让PC配合暂停发送。但这种方式在上位机开发时会比较麻烦,实际项目中我更喜欢用“分帧接收+应答机制”:PC每发一帧(比如4个字节的命令),FPGA处理完并应答一个字节,PC收到应答后再发下一帧。虽然效率没有连续DMA高,但可靠性非常好,调试起来也直观。

6. 真实项目中的串口协议扩展与工程化思考

6.1 当UART不够用时:RS485与多机通信

单条UART是点对点通信,但在工业现场经常要挂多条设备,这时候RS485就派上用场了。RS485物理层用差分信号,支持多点总线,半双工模式,理论上一条总线可以挂32个节点。FPGA实现RS485其实不难,关键是控制方向引脚DE/RE。

我的实现思路是:发送数据时拉高方向引脚(使能发送),发送完成后拉低方向引脚(回到接收状态),期间注意方向切换的时延。RS485的硬件层需要加一个收发器芯片,比如MAX3485或SP3485,FPGA侧只需要输出一个方向控制信号即可。用Verilog实现时,方向控制可以在发送状态机的IDLE和STOP状态之间切换。要注意的是,RS485总线在发送完后必须释放总线(方向引脚拉低),否则会一直占用总线,其他节点无法通信。

如果是多节点通信,还需要一个简单的链路层协议。我做过的一个项目里,总线上的设备通过地址区分:上位机先发一帧地址+命令,所有从机都接收并比对地址,匹配的设备才执行动作并回复,不匹配的忽略。这种模式在modbus over RS485里也有类似的实现方式,不过modbus更严格一些,用的是RTU帧格式,带CRC16校验。

6.2 多字节帧协议设计:从裸数据到可靠指令

单字节收发只是基础,真实项目里很少只传输一个字节。你需要定义帧格式,通常包括:帧头(0xAA 0x55或者0x5A)、长度、数据、校验。FPGA端收到一帧完整数据后再解析执行,而不是收到单个字节就执行,否则上位机拆包发的话很难保证完整性。

我自己常用的帧格式:

帧头(2字节) 长度(1字节) 数据(N字节) 校验(1字节) 0xAA 0x55 0x0C ... CRC8/SUM

接收端状态机:IDLE等帧头,收到0xAA后进入等待0x55状态,收到0x55后进入长度接收状态,按长度值继续接收后续数据,最后校验。校验失败则丢弃整帧回到IDLE。这种设计看起来简单,但在关键时刻能帮你省掉无数调试时间。上位机发出错误数据时,帧头校验会直接丢弃,而不是触发一堆错误动作。

实现时我最小的经验是:把命令解析和命令执行拆开。解析模块只负责验证帧格式并输出解析后的指令数据,执行模块再根据指令内容去操作寄存器、更新PWM占空比、启停电机等。这样逻辑清晰,修改指令集时不用动底层收发逻辑。

6.3 多机协同项目中的UART应用:从STM32到FPGA

UART在FPGA里很少是孤立存在的,最常见的是FPGA和MCU(比如STM32)协同工作。STM32H743和FPGA之间用FMC并行总线通信,但FPGA通过UART和PC交互来配置参数、上报状态的情况也很普遍。

我做过一个采集系统:STM32作为主控负责界面逻辑和用户交互,FPGA负责高速AD采集和数字信号处理,两边通过FMC并行总线交换实时采样数据,同时FPGA还有一路UART用于本地调试和固件参数升级。这种情况下,UART的应用就不仅是简单的数据传输,而是要配合一套完善的调试指令集——通过串口发命令可以控制FPGA内部的开环/闭环切换、修改滤波参数、读取实时采样值,这在实际调试中的价值远超想象。

这套调试指令集的设计思路是:定义一系列“寄存器访问”命令,比如写参数命令(地址+值)、读状态命令(地址),FPGA收到后通过一个寄存器桥去读写内部维护的参数寄存器组。这样上位机调试软件只需要发地址和值,不用关心FPGA内部信号细节,非常灵活。

调试参数寄存器组的Verilog实现不复杂,但有一个注意点:寄存器地址空间要定义清晰,写权限和读权限要区分开(比如某些寄存器是只读的状态寄存器,写无效),调试命令解析模块要能识别非法地址并返回错误码,这能帮你在调试过程中快速定位是不是上位机发错了地址。

6.4 进阶方向:如何把UART经验迁移到其他协议

学完UART后,一定不要只停留在UART本身。串口通信的核心方法论——协议解析、状态机设计、时序控制、参数化设计——在I2C、SPI、CAN甚至PCIe的开发中都通用。

以I2C为例,它是同步串行协议,有SCL时钟线和SDA数据线,比UART多了主从仲裁机制。但你会发现,I2C的SDA数据采样跳变沿、起始条件和停止条件的检测,跟你学过的UART起始位检测思路完全一致,只是协议逻辑更复杂。SPI则更像“没有起始位的并行移位寄存器”,主从设备通过片选和时钟共享数据。如果你能透彻理解UART的状态机设计和时序对齐思路,迁移到I2C和SPI会非常快。

我自己从UART入门后,第二个写通的就是SPI,第三个是I2C,再后面是给FPGA加PCIe接口时用的AXI接口。之所以学得比较顺利,就是因为UART把“寄存器级的状态机设计”和“协议时序分析”这两项基本功打扎实了。所以说,UART不是终点,它是整个FPGA通信技术栈的起点,值得投入时间学好。

7. 我的使用心得:这样写代码三年后回头看依然经得起推敲

最后结合经验再说几句。串口模块看似简单,但如果你是从零开始设计,很容易发现自己写的代码在某些边界条件下跑飞。比如一次连续发送大量数据时FIFO溢出、复位时序不对导致首字节丢失、上位机发送帧数据被噪声打断导致解析卡死——这些问题如果不在一开始就做防御性设计,后面要面对多个模块联调时就会非常痛苦。

我有三个坚持了很多年的习惯,可以分享给正在做FPGA通信模块的同学。第一,所有通信模块必须有完善的空闲态和超时保护。UART接收在数据帧中间突然超时(比如上位机关掉连接),模块要能自动回到IDLE状态,而不是卡死在某个状态等一个永远不来的bit。第二,所有跨时钟域信号(比如来自外部芯片的引脚信号)在进入FPGA后必须打两拍同步,避免亚稳态传播到状态机。第三,所有接口信号命名统一规范,xxx_enxxx_donexxx_data,这是最基础的工程素养。

这些习惯在UART阶段可能看不出来有多大作用,但当你的工程扩大到FMC、PCIe、DDR控制器以后,规范命名的信号和严谨的状态机设计,会让你的维护成本大幅低于那些“能跑就行”的代码。我见过不少同事被别人留下的一堆无名信号折磨得崩溃,也见过一个人维护多个FPGA模块却井井有条,区别就在这些细节里。

串口通信的FPGA实现,这个题目我做了很多年还在不断有新体会。每一次回头重写,都能找到可以优化或者简化的地方。希望这篇文章里写到的协议拆解、代码逻辑、调试经验,能帮你在自己做串口实验时少走一些弯路。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/5 10:23:12

2026年实战:图片格式图纸识别技术在质量检验计划中的应用

在 2026 年的数字化制造环境下&#xff0c;虽然 3D 标注&#xff08;PMI&#xff09;已广泛应用&#xff0c;但大量的历史存档、供应商交付以及外协加工仍涉及大量的 PDF、JPG 或 TIFF 等非矢量格式图纸。如何高效实现图片格式图纸识别&#xff08;Image Format Drawing Recogn…

作者头像 李华
网站建设 2026/9/5 10:20:31

注塑产品气泡的原因分析与解决方案15

1. 引言在注塑成型过程中&#xff0c;产品表面或内部出现气泡是常见的质量缺陷之一。气泡不仅影响产品的外观&#xff0c;还会降低其力学性能和密封性&#xff0c;严重时甚至导致产品报废。本文将从气泡的形成机理出发&#xff0c;系统分析产生气泡的主要原因&#xff0c;并给出…

作者头像 李华
网站建设 2026/9/5 10:17:13

Cadence Allegro高速PCB设计:绕等长从原理到实战全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/5 10:14:37

Coze工作流实战:从零构建AI视频自动化生产线

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/5 10:13:26

上云不翻车:用AI Skills让Agent告别工具调用的不确定性

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/5 10:12:03

周四上午多家AI前沿模型故障,是巧合还是另有隐情?

多家AI前沿模型周四上午集体“趴窝”周四上午&#xff0c;Anthropic、OpenAI和xAI的前沿模型均出现罕见故障&#xff0c;致使对应的AI聊天机器人陷入宕机。xAI的Grok从太平洋时间上午6点30分开始故障&#xff0c;到上午10点05分解决&#xff1b;Anthropic于周四太平洋时间上午6…

作者头像 李华