news 2026/2/28 22:49:20

实战案例:基于FPGA的UART协议收发器构建

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
实战案例:基于FPGA的UART协议收发器构建

手把手教你用FPGA实现UART通信:从协议解析到代码落地

你有没有遇到过这样的场景?
想让FPGA把传感器数据实时传给PC查看,却发现开发板自带的UART IP核不够灵活——改个波特率要重新生成,加个CRC校验得绕一大圈。更头疼的是,一旦通信出错,根本不知道是时序偏移、采样不准,还是起始位被干扰了。

别急,今天我们不调API、不依赖黑盒IP,亲手用Verilog在FPGA里“造一个”UART收发器。整个过程就像搭乐高:先理解协议本质,再拆解核心模块,最后一步步写出可综合、可调试、真正跑得起来的RTL代码。

这不是理论课,而是面向实战的深度实践。无论你是刚入门FPGA的新手,还是需要定制串口的老工程师,这篇文章都能给你带来即插即用的设计思路和避坑指南。


UART不是“插上线就能通”:异步通信到底难在哪?

很多人觉得UART很简单:TX接RX,设好波特率,发数据就完事了。但真正在FPGA里实现时才发现,问题全藏在细节里。

比如:
- 为什么接收端总漏掉第一个字节?
- 数据偶尔乱码,是线没接好还是时钟不准?
- 换了个波特率就不工作,分频系数算错了?

关键就在于——UART是异步的。它没有时钟线来对齐双方节奏,全靠各自内部时钟“心照不宣”地同步。这就带来了三个核心挑战:

  1. 时间必须卡得准:每个bit持续多久,由波特率决定。如果FPGA主频50MHz,目标波特率115200,那每bit对应约434个系统时钟周期。差几个周期看似不多,累积起来就会导致采样偏移。
  2. 起始位要可靠检测:数据什么时候开始?只能靠检测下降沿。但在噪声环境下,毛刺也可能被误判为起始信号。
  3. 采样位置至关重要:不能在边沿采样!必须在每一位的中间时刻取值,才能避开电平跳变区,提高抗干扰能力。

所以,一个可靠的UART设计,本质上是一场精确的时间控制游戏。而FPGA的优势恰恰在这里:我们可以完全掌控时序逻辑,精细调节每一个节拍。


模块化设计:把复杂问题拆成四个积木块

我们把这个“游戏”拆成四个可独立验证的模块:

  1. 波特率发生器(Baud Rate Generator)—— 提供精准节拍
  2. 发送模块(UART_TX)—— 按帧格式输出数据
  3. 接收模块(UART_RX)—— 稳健识别未知到来的数据
  4. 顶层整合与跨时钟处理—— 系统级可靠性保障

下面逐个击破。


波特率发生器:给UART装上“节拍器”

想象你要打拍子,每秒打9600下(9600bps),但你的手腕最快只能按5000万次/秒(50MHz)。怎么办?数到第50_000_000 / 9600 ≈ 5208下时,敲一下鼓面。

这就是分频的本质。

我们设计一个计数器,每当它数到DIVIDER - 1时,产生一个单周期脉冲tick,作为后续状态机推进的使能信号。

module baud_gen #( parameter CLK_FREQ = 50_000_000, parameter BAUD = 115200 )( input clk, input rst_n, output reg tick ); localparam DIVIDER = CLK_FREQ / BAUD; reg [31:0] counter; always @(posedge clk or negedge rst_n) begin if (!rst_n) counter <= 0; else if (counter >= DIVIDER - 1) counter <= 0; else counter <= counter + 1; end always @(posedge clk or negedge rst_n) begin if (!rst_n) tick <= 0; else tick <= (counter == DIVIDER - 1); end endmodule

经验提示
分频系数建议声明为localparam,避免运行时计算;使用>=而非==判断满计数,防止因复位或跳变遗漏条件。

虽然整数分频会有微小误差(如115200bps实际得到115172bps,误差约0.24%),但在±3%容限范围内,完全可用。若需更高精度,可用小数分频或DDS,但资源开销显著增加,一般应用不必过度优化。


发送模块:状态机驱动的帧构造器

发送相对简单,因为我们掌握主动权:知道啥时候发、发什么。

典型UART帧结构如下:

[起始位][D0][D1][D2][D3][D4][D5][D6][D7][停止位] 低 LSB -----------------------> MSB 高

流程清晰:
1. 空闲等待启动信号
2. 输出起始位(低电平)
3. 依次发送8位数据(LSB先行)
4. 输出停止位(高电平)
5. 标记完成

我们用有限状态机(FSM)来控制这个流程:

module uart_tx ( input clk, input rst_n, input tx_start, input [7:0] tx_data, output reg txd, output reg tx_done ); parameter STATE_IDLE = 3'd0; parameter STATE_START = 3'd1; parameter STATE_DATA = 3'd2; parameter STATE_STOP = 3'd3; parameter STATE_DONE = 3'd4; reg [2:0] state; reg [2:0] bit_cnt; wire baud_tick; // 实例化波特率发生器 baud_gen #( .CLK_FREQ(50_000_000), .BAUD(115200) ) u_baud_tx (.clk(clk), .rst_n(rst_n), .tick(baud_tick)); always @(posedge clk or negedge rst_n) begin if (!rst_n) begin state <= STATE_IDLE; bit_cnt <= 0; txd <= 1'b1; // 空闲态为高 tx_done <= 0; end else begin case (state) STATE_IDLE: begin txd <= 1'b1; tx_done <= 0; if (tx_start) begin state <= STATE_START; bit_cnt <= 0; end end STATE_START: begin if (baud_tick) begin txd <= 1'b0; // 起始位:低 state <= STATE_DATA; end end STATE_DATA: begin if (baud_tick) begin txd <= tx_data[bit_cnt]; if (bit_cnt == 7) state <= STATE_STOP; else bit_cnt <= bit_cnt + 1; end end STATE_STOP: begin if (baud_tick) begin txd <= 1'b1; // 停止位:高 state <= STATE_DONE; end end STATE_DONE: begin tx_done <= 1; state <= STATE_IDLE; end default: state <= STATE_IDLE; endcase end end endmodule

📌关键点解析
-baud_tick是唯一推动状态迁移的条件,确保每个bit持续准确时间;
- 数据位通过[bit_cnt]索引逐位输出,自然实现LSB先行;
-tx_done在STOP后仅维持一个时钟周期,便于外部逻辑清空缓冲或触发中断。


接收模块:如何听懂“突然开口”的对话?

相比发送,接收更像是在嘈杂环境中捕捉一段突如其来的讲话。

难点在于:
- 不知道数据何时到来;
- 必须快速锁定节奏,在每位中心采样;
- 还要判断帧是否完整有效。

我们的策略是:
1.持续监听RxD电平变化
2.检测到下降沿后,延迟半比特时间进行首次采样(对齐中心)
3.此后每隔一比特时间采样一次
4.最后检查停止位是否为高,否则报帧错误

此外,必须处理两个底层问题:

🛡️ 跨时钟域同步:防亚稳态两极寄存器

输入信号rxd可能来自外部设备,其时钟与FPGA系统时钟无关。直接采样可能导致亚稳态(metastability)。标准做法是使用两级触发器同步:

reg rxd_sync1, rxd_sync2; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin rxd_sync1 <= 1'b1; rxd_sync2 <= 1'b1; end else begin rxd_sync1 <= rxd; rxd_sync2 <= rxd_sync1; end end

这样可将亚稳态传播概率降至极低水平。

⏱️ 半比特对齐:为何要用两个波特率发生器?

为了实现“半比特延时”,我们实例化两个baud_gen
- 一个用于整比特定时(baud_tick
- 一个设置为双倍波特率(230400bps),生成half_baud_tick

利用后者计数至一半时长,即可实现精确的中心对齐。

baud_gen #(.CLK_FREQ(50_000_000), .BAUD(115200)) u_baud_rx (.clk(clk), .rst_n(rst_n), .tick(baud_tick)); baud_gen #(.CLK_FREQ(50_000_000), .BAUD(230400)) u_half_baud (.clk(clk), .rst_n(rst_n), .tick(half_baud_tick));
完整接收代码如下:
module uart_rx ( input clk, input rst_n, input rxd, output reg rx_valid, output reg frame_err, output reg [7:0] rx_data ); reg [2:0] state; reg [2:0] bit_cnt; reg [31:0] counter; wire baud_tick; wire half_baud_tick; // 同步链 reg rxd_sync1, rxd_sync2; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin rxd_sync1 <= 1'b1; rxd_sync2 <= 1'b1; end else begin rxd_sync1 <= rxd; rxd_sync2 <= rxd_sync1; end end // 波特率分频 baud_gen #(.CLK_FREQ(50_000_000), .BAUD(115200)) u_baud_rx (.clk(clk), .rst_n(rst_n), .tick(baud_tick)); baud_gen #(.CLK_FREQ(50_000_000), .BAUD(230400)) u_half_baud (.clk(clk), .rst_n(rst_n), .tick(half_baud_tick)); // 主状态机 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin state <= 0; bit_cnt <= 0; counter <= 0; rx_valid <= 0; frame_err <= 0; rx_data <= 0; end else begin rx_valid <= 0; // 默认无效 case (state) 0: begin // IDLE:等待起始位 if (rxd_sync2 == 0 && rxd_sync1 == 1) begin // 下降沿 counter <= 0; state <= 1; end end 1: begin // WAIT_HALF:等待半比特时间以对齐中心 if (half_baud_tick) begin if (counter == (50_000_000 / 230400 / 2 - 1)) begin state <= 2; bit_cnt <= 0; end else begin counter <= counter + 1; end end end 2: begin // DATA:逐位采样 if (baud_tick) begin rx_data[bit_cnt] <= rxd_sync2; if (bit_cnt == 7) state <= 3; else bit_cnt <= bit_cnt + 1; end end 3: begin // STOP:验证停止位 if (baud_tick) begin if (rxd_sync2 == 1) begin rx_valid <= 1; // 数据有效 end else begin frame_err <= 1; // 帧错误 end state <= 0; end end default: state <= 0; endcase end end endmodule

💡设计亮点
- 使用rxd_sync2rxd_sync1的组合判断下降沿,避免单次采样误判;
- 半比特延时完成后立即进入数据采样,无需额外计数器;
- 停止位合法性检查是最后一道防线,极大提升通信鲁棒性。


实战部署:FPGA+ADC+PC构建实时监控系统

现在我们把它用起来。

假设你有一个FPGA开发板连接着ADC芯片,想把采集到的电压值实时显示在PC上。

系统架构如下:

[ADC] → [FPGA采样 & 缓存] → [uart_tx] → [USB-TTL] → [PC串口助手]

工作流程

  1. FPGA定时启动ADC转换(如每1ms一次);
  2. 将原始数据打包成二进制帧或ASCII字符串;
  3. 调用uart_tx模块逐字节发送;
  4. PC端使用PuTTY / Tera Term / 串口调试助手接收并绘图。

优势对比传统方案

功能标准IP核自研UART
波特率切换需重新配置参数化即时切换
帧结构扩展固定格式可添加ID/CRC/时间戳
错误重传不支持可加入ACK机制
调试可视性黑盒难追踪全信号可见,配合ILA轻松抓波形

🎯真实案例
曾有项目中发现数据偶发丢包,通过ILA抓取发现是CPU响应延迟导致发送请求堆积。我们在顶层加了一级异步FIFO缓冲,瞬间解决问题。这种灵活性,只有自己写的模块才具备。


设计优化清单:让UART更稳定、更通用

经过多个项目打磨,总结出以下最佳实践:

项目推荐做法
时钟源使用板载有源晶振(如50MHz),禁用内部RC振荡器
波特率误差控制在±2%以内,可通过DIVIDER = CLK_FREQ / BAUD + 0.5四舍五入优化
输入滤波RxD引脚外接100Ω+0.1μF低通滤波,软件也可加入去抖计数器
缓冲机制高层添加FIFO,解耦慢速CPU与高速通信
参数化设计使用parameter DATA_WIDTH=8, STOP_BITS=1提升复用性
IP封装将TX/RX打包为Vivado IP核,一键导入新工程

特别提醒:永远不要省略帧错误检测!很多现场故障都是因为停止位异常未被发现,导致后续所有数据错位。


写在最后:简单的协议,不简单的思想

UART看起来是个“古老”的协议,但它背后体现的时间控制、状态建模、抗干扰设计,正是数字系统开发的核心能力。

当你亲手实现一次从0到1的通信建立,那种“我掌控了每一个上升沿”的感觉,远比调用现成IP来得踏实。

更重要的是,这套方法论可以平移到SPI、I2C甚至自定义高速协议的设计中:
- SPI?不过是带SCLK的同步版本;
- I2C?加上开漏输出和仲裁逻辑;
- 自定义协议?自由定义帧头、长度、校验即可。

所以,别小看这个“最基础”的UART。它是通往复杂系统设计的第一扇门

如果你正在学习FPGA,不妨今晚就动手试试:写完代码,连上串口助手,看着第一个自己发出的“Hello FPGA”出现在屏幕上——那一刻,你会明白,硬件编程的魅力,就藏在一个个精准跳动的比特之中。

欢迎在评论区分享你的实现体验,或者提出你在调试中遇到的问题,我们一起解决。

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

3D球体抽奖系统:打造企业年会的沉浸式互动盛宴

3D球体抽奖系统&#xff1a;打造企业年会的沉浸式互动盛宴 【免费下载链接】log-lottery &#x1f388;&#x1f388;&#x1f388;&#x1f388;年会抽奖程序&#xff0c;threejsvue3 3D球体动态抽奖应用。 项目地址: https://gitcode.com/gh_mirrors/lo/log-lottery l…

作者头像 李华
网站建设 2026/2/28 7:36:38

Cloudpods MCP Server:AI驱动的多云管理新范式

Cloudpods MCP Server&#xff1a;AI驱动的多云管理新范式 【免费下载链接】cloudpods 开源、云原生的多云管理及混合云融合平台 项目地址: https://gitcode.com/yunionio/cloudpods Cloudpods MCP Server作为多云管理平台的核心组件&#xff0c;开创了AI驱动的云资源管…

作者头像 李华
网站建设 2026/2/28 13:23:40

还在手动写材料?Open-AutoGLM一键生成模板的3种高阶玩法,错过=落后

第一章&#xff1a;Shell脚本的基本语法和命令 Shell 脚本是 Linux/Unix 系统中自动化任务的核心工具&#xff0c;它允许用户通过编写一系列命令来执行复杂的操作。编写 Shell 脚本时&#xff0c;通常以 #!/bin/bash 作为首行&#xff0c;声明脚本使用的解释器。 脚本的结构与…

作者头像 李华
网站建设 2026/2/14 21:45:26

终极游戏库整合神器:3分钟搞定多平台游戏统一管理

终极游戏库整合神器&#xff1a;3分钟搞定多平台游戏统一管理 【免费下载链接】BoilR Synchronize games from other platforms into your Steam library 项目地址: https://gitcode.com/gh_mirrors/boi/BoilR 还在为电脑上杂乱无章的游戏启动器烦恼吗&#xff1f;BoilR…

作者头像 李华
网站建设 2026/2/27 4:18:08

企业活动新体验:3D球体动态抽奖系统完整部署手册

企业活动新体验&#xff1a;3D球体动态抽奖系统完整部署手册 【免费下载链接】log-lottery &#x1f388;&#x1f388;&#x1f388;&#x1f388;年会抽奖程序&#xff0c;threejsvue3 3D球体动态抽奖应用。 项目地址: https://gitcode.com/gh_mirrors/lo/log-lottery …

作者头像 李华