第 10讲:UART 发送驱动设计 —— 串行帧状态引擎、握手协议与定时发报实战
课前导读:从“节拍心跳”到“真正的发报员”
在上一讲中,我们完成了串口通信最核心的时钟分频基石——波特率发生器(baud_tick)。它以每秒 115,200 次的频率,精确吐出一个个宽度为 20ns 的时钟使能脉冲。
这一讲,我们要把这个节拍器作为动力源,组装出负责向外界电脑发送数据的“发报员”驱动引擎(uart_tx)!
很多初学者觉得发送很简单:“不就是把 8 位数据按顺序一位一位挪出去吗?”
但一个工业级可靠的发送驱动,绝不仅仅是简单的移位:
- 外界给的数据在中途如果突然变化了,会不会导致发到一半的数据错乱撕裂?
- 怎么告诉外界“我正在忙,请不要给我塞新数据”(忙碌标志
busy)? - 发送完成的瞬间,怎么精准敲响铃铛通知上层(完成脉冲
done)? - 为什么全世界所有硬件工程师在测试串口时,最喜欢发送大写字母
'U'(十六进制0x55)?
本讲将带你深入串行发射引擎底层,彻底攻克数据帧状态机编码、前级快照锁存机制、流水线握手协议以及顶层 1 秒自动发报系统的构建。
第一章:发报员的物理使命与接口握手模型
在进入代码前,我们先把发送引擎 uart_tx 当成一个独立的芯片黑盒子,看看它与上下游之间是如何协同工作的:
【上游总指挥 (CPU / 控制逻辑)】
│
├─► data[7:0] (准备发送的 8 位字节内容)
├─► start (启动命令脉冲:拍一下发报员肩膀)
│
▼
+--------------------------------------------------------------+
| 【uart_tx 发报员核心】 |
| |
| * 听从外界注入的 tick 节拍器指挥 (每隔 434 拍前进一步) |
| * 将 8 位并行数据切片,打包成符合 8-N-1 标准的 10 位串行波形 |
+--------------------------------------------------------------+
│ │
├─► tx (单根物理发送线,连电脑) ├─► busy (忙碌红灯)
│ └─► done (发毕绿灯)
▼
【外部世界 (电脑屏幕 / USB串口芯片)】
1.1 关键接口信号的物理定义:
input wire [7:0] data:上层系统递给发报员的 8 位数据包裹(例如字符'A'或数值8'h55);input wire start:发射触发脉冲。平时为 0,当上层想发送时,猛跳 1 个周期的脉冲(持续 20ns);input wire tick:第 10 讲做的波特率发生器送进来的使能脉冲。每来一个高电平,发报员才往前走一步;output reg tx:芯片对外的物理输出引脚。平时空闲时牢牢锁死在 高电平1;output reg busy:忙碌标志红灯。busy = 0:空闲中,欢迎送新数据;busy = 1:发报员正在专心发信,严禁外界打扰!
output reg done:完成报告绿灯。发完停止位的那一瞬间,猛敲一下铃铛(持续 20ns 的高电平脉冲),通知上层:“这封信送到了,可以下达下一项任务了!”
第二章:uart_tx.v 核心驱动源码与手术级解剖
下面是符合工业标准的完整 UART 发送驱动核心代码:
module uart_tx (
input wire clk, // 50MHz 系统主时钟
input wire rst_n, // 系统复位信号 (低有效)
input wire tick, // 波特率发生器给出的 115200 节拍脉冲
input wire start, // 上层下达的发信启动脉冲 (高有效,1拍)
input wire [7:0] data, // 待发送的 8 位并行数据载荷
output reg tx, // 物理串行输出信号线 (连电脑 RXD)
output reg busy, // 状态汇报:1 代表忙碌,0 代表空闲
output reg done // 完成汇报:发毕瞬间敲响 1 拍高电平
);
reg [3:0] phase; // 阶段计数器 (记着当前进行到第几步,0~10)
reg [7:0] data_latched; // 内部数据快照抽屉 (防止外界中途窜改)
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
tx <= 1'b1; // 空闲态总线必须焊死在高电平 1!
busy <= 1'b0; // 开机不忙
done <= 1'b0; // 没完成
phase <= 4'd0;
data_latched <= 8'd0;
end else begin
done <= 1'b0; // 【门铃弹簧设计】:默认每一拍都把完成信号按住为 0
// 状态一:发报员处于空闲态 (busy == 0)
if (!busy) begin
tx <= 1'b1; // 空闲时维持高电平
if (start) begin // 上级下达启动命令!
data_latched <= data; // 核心:瞬间拍照锁存数据!
busy <= 1'b1; // 升起“忙碌”红灯!
phase <= 4'd0; // 阶段归零,准备开跑
end
end
// 状态二:发报员处于忙碌发信态 (busy == 1)
else if (tick) begin // 只有当波特率节拍敲响时,才往前推移一步!
case (phase)
// 步骤 0:发送起始位
4'd0: begin
tx <= 1'b0; // 强制拉低为 0!唤醒电脑
phase <= 4'd1;
end
// 步骤 1 至 8:低位先行,逐位送出 8 个数据位
4'd1, 4'd2, 4'd3, 4'd4,
4'd5, 4'd6, 4'd7, 4'd8: begin
tx <= data_latched[phase - 1'b1]; // 核心:抽取对应位送上导线
phase <= phase + 1'b1;
end
// 步骤 9:发送停止位
4'd9: begin
tx <= 1'b1; // 强制拉高为 1!通知信件封口
phase <= 4'd10;
end
// 步骤 10:发毕总结收工
default: begin
tx <= 1'b1; // 总线重回空闲高电平
busy <= 1'b0; // 熄灭红灯,解除忙碌!
done <= 1'b1; // 叮!敲响完成电铃!
phase <= 4'd0; // 阶段归零,静待下次任务
end
endcase
end
end
end
endmodule
2.1 显微镜剖析与设计哲学
① 防撕裂黑科技:为什么必须有 data_latched <= data;?
在第 10 讲我们算过,发完 1 个字节(10 个波特位)需要花费:
$$10 \times 8.68\mu\text{s} = 86.8\text{ 微秒 } = \mathbf{4340\text{ 个时钟周期!}}$$
- 物理危机:上层的
data信号是由外部传感器或内存送过来的。在这漫长的 4340 拍里,外部数据线随时可能更新变动。如果发报员直接拿data[0] ~ data[7]往外发,发到一半外界数据变了,发出去的字节将变成前半段属于旧数据、后半段属于新数据的**“杂交畸变数据包(Data Tearing)”**! - 解法:在
start命令触发的最初 20ns 窗口内,发报员立刻使用抽屉data_latched对输入数据进行一次性“拍照复印(Latch)”!此后无论外界数据怎么翻江倒海,发报员只按拍好的快照发信,从物理上杜绝了数据撕裂。
② else if (tick) —— 节拍驱动的绝对时序严谨性
请注意这一层的嵌套结构:
- 整个发送状态机不是随时钟沿乱动的,而是被紧紧包裹在
else if (tick)内部! - 微观效果:平时 50MHz 时钟跳动 434 次期间,
tick恒为 0,发报员就纹丝不动、绝对稳定地保持当前导线电平; - 只有当第 434 拍
tick = 1脉冲到来的那唯一一个上升沿瞬间,case (phase)才准许往前走一步!波特率的精准度在此得到了完美的硬件继承。
③ data_latched[phase - 1'b1] 的数学之美
很多同学惊叹于这一行的简洁:
tx <= data_latched[phase - 1'b1];
我们代入实际数字推算:
- 当
phase = 4'd1时:发送data_latched[1 - 1]$\to$ 即送出最低位data_latched[0]! - 当
phase = 4'd2时:发送data_latched[2 - 1]$\to$ 送出第 1 位data_latched[1]! - …
- 当
phase = 4'd8时:发送data_latched[8 - 1]$\to$ 送出最高位data_latched[7]! - 单行代码,直接把一个 8 位并行寄存器,拆解成了时间轴上逐位飞驰的串行波形!低位先行(LSB First)规则在此被完美兑现!
第三章:微观发送状态转移真值推演表
假设我们要向电脑发送字符 'U'(十六进制 8'h55,展开成二进制就是 0101_0101)。
从外界按下发送按键那一刻起,各变量在连续 11 个 tick 节拍下的微观推演真值表:
| 节拍序号 | 阶段 phase |
发报员微观动作 | 物理引脚 tx 电平 |
忙碌标志 busy |
完成标志 done |
物理帧对应的通信含义 |
|---|---|---|---|---|---|---|
| 平时空闲 | 0 |
无任务发呆中 | 1 |
0 |
0 | 串口空闲态(IDLE) |
| 启动瞬时 | 0 |
收到 start=1,锁存快照,挂起红灯 |
1 |
0 -> 1 |
0 | 握手建立,闭门谢客 |
| 第 1 个 tick | 0 -> 1 |
强制将导线拉低 | 0 |
1 |
0 | 起始位(Start Bit) |
| 第 2 个 tick | 1 -> 2 |
发送 data_latched[0] |
1 |
1 |
0 | 数据位 Bit 0 (值为1) |
| 第 3 个 tick | 2 -> 3 |
发送 data_latched[1] |
0 |
1 |
0 | 数据位 Bit 1 (值为0) |
| 第 4 个 tick | 3 -> 4 |
发送 data_latched[2] |
1 |
1 |
0 | 数据位 Bit 2 (值为1) |
| 第 5 个 tick | 4 -> 5 |
发送 data_latched[3] |
0 |
1 |
0 | 数据位 Bit 3 (值为0) |
| 第 6 个 tick | 5 -> 6 |
发送 data_latched[4] |
1 |
1 |
0 | 数据位 Bit 4 (值为1) |
| 第 7 个 tick | 6 -> 7 |
发送 data_latched[5] |
0 |
1 |
0 | 数据位 Bit 5 (值为0) |
| 第 8 个 tick | 7 -> 8 |
发送 data_latched[6] |
1 |
1 |
0 | 数据位 Bit 6 (值为1) |
| 第 9 个 tick | 8 -> 9 |
发送 data_latched[7] |
0 |
1 |
0 | 数据位 Bit 7 (最高位,值为0) |
| 第 10 个 tick | 9 -> 10 |
强制将导线拉高封口 | 1 |
1 |
0 | 停止位(Stop Bit) |
| 第 11 个 tick | 10 -> 0 |
结算收工,熄灭红灯,敲响绿灯 | 1 |
1 -> 0 |
1! |
帧结束结算!发出 done 脉冲! |
| 下一拍 (20ns) | 0 |
完成信号自动回弹熄灭 | 1 |
0 |
0 |
重回空闲态,等待下一次发信 |
第四章:顶层系统组装——uart_tx_demo_top.v 实战演练
现在,我们站在芯片架构师的视角,用一个顶层模块把**“1 秒定时器”、“第 10 讲的 baud_tick 节拍器”以及“刚写好的 uart_tx 发报员”焊接在一起,实现开发板每隔整整 1.0 秒,自动向电脑串口发送一个英文字母 'U'**!
module uart_tx_demo_top #(
parameter integer CLK_FREQ_HZ = 50_000_000, // 50MHz 系统时钟
parameter integer BAUD_RATE = 115_200, // 115200 波特率
parameter integer SEND_INTERVAL_CLKS = 50_000_000, // 5000万拍 = 1.0秒发送间隔
parameter [7:0] DEMO_BYTE = 8'h55 // 测试字符:大写字母 'U'
)(
input wire sys_clk, // 板载 50MHz 晶振
input wire sys_rst_n, // 复位按键
output wire uart_tx // 直通开发板 USB 转串口芯片的 TXD 引脚!
);
reg [31:0] interval_cnt; // 1 秒秒表计数器
reg tx_start; // 触发脉冲:拍发报员肩膀的信号
wire tick_115200; // 导线:接节拍器的 tick
wire tx_busy; // 导线:接发报员的 busy 红灯
wire tx_done; // 导线:接发报员的 done 绿灯
// 1. 业务调度大脑:1 秒倒计时与发信保护
always @(posedge sys_clk or negedge sys_rst_n) begin
if (!sys_rst_n) begin
interval_cnt <= 32'd0;
tx_start <= 1'b0;
end else begin
tx_start <= 1'b0; // 经典门铃弹簧:默认置 0
// 核心安全保护:只有当发报员“不忙”时,才允许计时并发动下一次发送!
if (!tx_busy) begin
if (interval_cnt == SEND_INTERVAL_CLKS - 1) begin
interval_cnt <= 32'd0;
tx_start <= 1'b1; // 1秒到!猛拍发报员肩膀!
end else begin
interval_cnt <= interval_cnt + 1'b1;
end
end
end
end
// 2. 安装节拍器 (第 10 讲成果)
baud_tick #(
.CLK_FREQ_HZ(CLK_FREQ_HZ),
.BAUD_RATE (BAUD_RATE)
) u_baud_tick (
.clk (sys_clk),
.rst_n(sys_rst_n),
.tick (tick_115200) // 吐出的单脉冲挂在 tick_115200 线上
);
// 3. 安装发报员 (本讲核心成果)
uart_tx u_uart_tx (
.clk (sys_clk),
.rst_n(sys_rst_n),
.tick (tick_115200), // 听节拍器的指挥!
.start(tx_start), // 听 1 秒定时器的命令!
.data (DEMO_BYTE), // 把测试字符 8'h55 递给它
.tx (uart_tx), // 发报线直通外部物理引脚!送往电脑!
.busy (tx_busy), // 红灯拉回给总指挥看
.done (tx_done) // 绿灯拉回备用
);
endmodule
第五章:为什么全世界硬件工程师都深爱字符 'U'(8'h55)?
在代码中,参数定义了:
parameter [7:0] DEMO_BYTE = 8'h55;
很多初学者问:“老师,为什么不发字母 'A',不发数字 '0',偏偏挑大写字母 'U'?”
这是整个电子通信界一个令人拍案叫绝的工程智慧!
示波器下的方波奇迹:
- 我们把十六进制
8'h55展开成完整的 8-N-1 帧格式:- 起始位:
0 - 数据位(低位先行):
1, 0, 1, 0, 1, 0, 1, 0 - 停止位:
1
- 起始位:
- 把这 10 个位按发送顺序连起来看:
$$\mathbf{0 \ 1 \ 0 \ 1 \ 0 \ 1 \ 0 \ 1 \ 0 \ 1}$$ - 看到了吗?从头到尾,每一个位都与相邻位严格高低反转!
- 物理意义:
在示波器屏幕上,如果发送别的字符,电平会忽长忽短;唯独发送8'h55时,示波器上展现的是一道极其完美的标准等宽对称方波!- 工程师只要拿示波器点在 TX 引脚上,量一下这个方波的脉冲宽度是不是正好等于 $8.68\mu\text{s}$,就能在 1 秒钟内彻底确诊波特率是否精准无误、上升沿是否有阻抗畸变!
- 而在电脑串口助手里,
8'h55按照 ASCII 码表解析出来的字符,正好就是大写的英文字母'U'!
第六章:实验室上板测试与看图识 Bug
把工程编译生成比特流烧录到板子上,用 USB 数据线连上电脑:
1. 电脑端标准配置
打开任意串口助手软件(如 XCOM、SSCOM、MobaXterm、Putty):
- 端口号(COM Port):在设备管理器中查看“端口(COM和LPT)”,选择对应的
USB Serial Port; - 波特率(Baudrate):填入
115200; - 数据位(Data bits):选择
8; - 校验位(Parity):选择
None(无校验); - 停止位(Stop bits):选择
1; - 接收设置:勾选“ASCII / 字符显示”。
2. 正常实验现象
点击“打开串口”,你将亲眼见证电脑屏幕上,每隔整整 1 秒钟,极为稳定、安静地自动弹出一个大写字母 'U':
U
U
U
U
...
3. 常见现场翻车排错指南
故障一:屏幕上一片死寂,连一个字符都收不到
- 排查 1:管脚约束中
uart_tx引脚是否与板载原理图对齐?(注意:FPGA 的uart_tx应该连到板载 USB 转串口芯片的RXD端!有的图纸标注混淆,尝试把 TX/RX 引脚对调); - 排查 2:确认 1 秒定时器是否因为
if (!tx_busy)死锁。若发报员复位未完成导致busy一直为 1,定时器将永远被挂起。
故障二:屏幕每秒都在弹东西,但弹出来的全是乱七八糟的乱码(如 ? 或黑色问号菱形)
- 排查 1:波特率对错!电脑端是否误选成了 9600?
- 排查 2:板载晶振频率与代码参数不符!若板载晶振实际是 25MHz,而代码写的是
50_000_000,实际发出的波特率相差一倍,电脑必然乱码。
本讲核心总结
- 发送的核心本质:在时间轴上将 8 位并行寄存器通过阶段计数器(
phase)解构成 10 位的标准串行方波。 - 前级快照锁存(
data_latched):彻底粉碎发送中途数据变化导致的“数据撕裂(Tearing)”。 busy与done的握手机制:busy建立防打扰闭门保护,done提供单周期完成触发,构成高鲁棒性的系统闭环。- 字符
'U'(8'h55)的示波器传奇:交替的01010101构筑起硬件测试中无可替代的标准方波基准。