FPGA入门
文章目录
- FPGA入门
- 前言
- 一、新版模块各功能改动逐段解析
-
- 完整代码
- [1.1 参数化通用分频设计](#1.1 参数化通用分频设计)
- [1.2 新增触发信号 Send_Go:按需启动发送](#1.2 新增触发信号 Send_Go:按需启动发送)
- [二、本文核心重点:组合逻辑毛刺科普 + W_Tx_Done 分层时序设计](#二、本文核心重点:组合逻辑毛刺科普 + W_Tx_Done 分层时序设计)
-
- [2.1 基础科普:什么是组合逻辑,它有什么致命缺陷?](#2.1 基础科普:什么是组合逻辑,它有什么致命缺陷?)
- [2.2 工业标准解决方案:对外控制 / 完成标志统一用时序逻辑输出](#2.2 工业标准解决方案:对外控制 / 完成标志统一用时序逻辑输出)
- [2.3 分层信号讲解:W_Tx_Done 与 Tx_Done 各自定位](#2.3 分层信号讲解:W_Tx_Done 与 Tx_Done 各自定位)
- [2.4 单独拆分 W_Tx_Done 三层核心意义](#2.4 单独拆分 W_Tx_Done 三层核心意义)
- [2.5 通俗类比理解](#2.5 通俗类比理解)
- 三、仿真波形
-
- [仿真 Testbench 代码:](#仿真 Testbench 代码:)
- 波形分析
- 总结
前言
在上一篇教程中,我们基于受控线性序列机实现了内置 1 秒定时器、自动循环发送的串口发送模块,适合简单调试打印场景。
但实际项目开发里,串口很少需要固定周期自动发送,更多是按需触发:按键按下发送、FIFO 数据就绪发送、外部指令应答发送。同时旧版本缺少发送完成反馈信号,上层逻辑联动十分麻烦。
因此本次对 UART 发送模块做全面工程化迭代,所有改动如下:
- 移除内置 1s 延时计数器delay_cnt,把延时控制交给上层,模块解耦通用;
- 新增触发输入Send_Go,外部脉冲即可启动一次串口发送;
- 新增缓存逻辑:仅Send_Go有效时,用 D 触发器锁存输入数据,避免发送中途数据跳变乱码;
- 参数化设计CLOCK_FREQ、BAUD,自动计算波特率分频值,无需手动计算;
- 新增发送完成标志Tx_Done,反馈一帧数据发送完毕;
- 核心时序优化(本文重点):拆分组合逻辑中间信号W_Tx_Done,寄存器打一拍同步输出Tx_Done,解决组合逻辑毛刺,普及工业标准时序输出规范。
一、新版模块各功能改动逐段解析
完整代码
c
module uart_0(
input Clk,
input Reset_n,
input Send_Go, // 单次发送触发脉冲
input [7:0] Data, // 待发送8bit数据
output reg Led, // 发送状态指示灯
output reg Tx_Done, // 同步发送完成标志
output reg uart_tx // 串口发送引脚
);
reg [29:0] baud_div_cnt; // 波特率分频计数器
reg en_baud_div_cnt; // 发送序列使能开关
reg [3:0] bit_cnt; // 序列步数计数器 0~9
reg [7:0] r_Data; // D触发器缓存待发送数据
// 通用参数,适配任意时钟、波特率
parameter BAUD = 9600;
parameter CLOCK_FREQ = 50_000_000;
parameter MCNT_BAUD = CLOCK_FREQ / BAUD - 1;
parameter MCNT_BIT = 9; // 串口序列总步数:0起始位~9停止位
// 组合逻辑中间信号:瞬时抓拍发送完成临界状态(存在毛刺)
wire W_Tx_Done;
assign W_Tx_Done = (bit_cnt == MCNT_BIT && baud_div_cnt == MCNT_BAUD);
// 时序逻辑打一拍,输出无毛刺同步完成脉冲(对外标准接口)
always@(posedge Clk)
Tx_Done <= W_Tx_Done;
// 波特率分频计数器,仅发送使能开启时计数
always@(posedge Clk or negedge Reset_n)
if(!Reset_n)
baud_div_cnt <= 0;
else if(en_baud_div_cnt)begin
if(baud_div_cnt == MCNT_BAUD)
baud_div_cnt <= 0;
else
baud_div_cnt <= baud_div_cnt + 1'd1;
end
else
baud_div_cnt <= 1'd0;
// 序列步数计数器,发送完成自动清零
always@(posedge Clk or negedge Reset_n)
if(!Reset_n)
bit_cnt <= 0;
else if(Tx_Done)
bit_cnt <= 0;
else if(baud_div_cnt == MCNT_BAUD)
bit_cnt <= bit_cnt + 1'd1;
// D触发器锁存待发送数据,Send_Go触发时缓存输入Data
always@(posedge Clk or negedge Reset_n)
if(!Reset_n)
r_Data <= 0;
else if(Send_Go)
r_Data <= Data;
// 发送使能控制:触发打开,发送完成自动关闭
always@(posedge Clk or negedge Reset_n)
if(!Reset_n)
en_baud_div_cnt <= 0;
else if(Tx_Done)
en_baud_div_cnt <= 0;
else if(Send_Go)
en_baud_div_cnt <= 1;
// 线性序列查表输出uart_tx电平
always@(posedge Clk or negedge Reset_n)
if(!Reset_n)
uart_tx <= 1;
else if(!en_baud_div_cnt)
uart_tx <= 1; // 串口空闲默认高电平
else begin
case(bit_cnt)
0: uart_tx <= 1'd0; // 第0步:起始位低电平
1: uart_tx <= r_Data[0];
2: uart_tx <= r_Data[1];
3: uart_tx <= r_Data[2];
4: uart_tx <= r_Data[3];
5: uart_tx <= r_Data[4];
6: uart_tx <= r_Data[5];
7: uart_tx <= r_Data[6];
8: uart_tx <= r_Data[7];
9: uart_tx <= 1'd1; // 第9步:停止位高电平
default : uart_tx <= uart_tx;
endcase
end
// LED状态指示:每完整发送一帧翻转一次
always@(posedge Clk or negedge Reset_n)
if(!Reset_n)
Led <= 0;
else if(Tx_Done)
Led <= !Led;
endmodule
1.1 参数化通用分频设计
c
parameter BAUD = 9600;
parameter CLOCK_FREQ = 50_000_000;
parameter MCNT_BAUD = CLOCK_FREQ / BAUD - 1;
parameter MCNT_BIT = 9;
旧版本分频阈值固定写死,更换 100M 时钟、115200 波特需要手动重新计算数值;新版通过参数自动运算,只需要修改顶层传入参数,代码复用性大幅提升。
分频公式原理回顾:单个 bit 所需时钟周期 = 系统时钟频率 ÷ 波特率,计数器从 0 开始计数,因此末尾-1。
1.2 新增触发信号 Send_Go:按需启动发送
c
always@(posedge Clk or negedge Reset_n)
if(!Reset_n)
r_Data<=0;
else if(Send_Go)
r_Data<=Data;
always@(posedge Clk or negedge Reset_n)
if(!Reset_n)
en_baud_div_cnt<=0;
else if(Tx_Done)
en_baud_div_cnt<=0;
else if(Send_Go)
en_baud_div_cnt<=1;
功能说明:
- 检测到Send_Go高电平时,利用 D 触发器锁存当前输入 8bit 数据到r_Data;
- 同时拉高发送使能en_baud_div_cnt,启动波特率计数器,开始输出串口时序;
- 一帧发送完成后,Tx_Done拉高,自动关闭发送使能,等待下一次触发脉冲。
二、本文核心重点:组合逻辑毛刺科普 + W_Tx_Done 分层时序设计
2.1 基础科普:什么是组合逻辑,它有什么致命缺陷?
- 组合逻辑定义
没有时钟、没有寄存器,输入实时直接映射输出,典型写法:assign 信号 = 判断条件,代码中W_Tx_Done就是标准组合逻辑信号。 - 组合逻辑固有缺陷:毛刺(竞争冒险)
FPGA 内部由大量门电路组成,不同寄存器、线路的信号传输延迟不一致。
当判断条件依赖两个及以上独立寄存器时,寄存器更新不会完全同步,会产生几纳秒~几十纳秒的窄脉冲,也就是毛刺。
本模块中判断条bit_cnt9&&baud_div_cntMCNT_BAUD同时依赖两组独立计数器:
- bit_cnt:记录序列步数
- baud_div_cnt:波特率分频计数器
同一个时钟上升沿,两组寄存器更新速度存在硬件延迟差,会短暂出现 "条件同时成立" 的瞬时窗口,组合逻辑信号会产生毛刺。
2.2 工业标准解决方案:对外控制 / 完成标志统一用时序逻辑输出
工程规范:所有对外有效标志、完成脉冲、触发信号,禁止直接输出组合逻辑,必须用寄存器打一拍同步输出。
用时钟寄存器锁存一拍,可以过滤窄毛刺,输出脉冲宽度固定为 1 个完整时钟周期,波形干净、时序稳定,不会出现随机误触发。
2.3 分层信号讲解:W_Tx_Done 与 Tx_Done 各自定位
- wire W_Tx_Done:纯组合中间信号
c
wire W_Tx_Done;
assign W_Tx_Done = (bit_cnt == MCNT_BIT && baud_div_cnt == MCNT_BAUD);
作用:实时瞬时抓拍 "一帧发送完毕" 的临界瞬间
只要两个计数条件同时满足,信号立刻拉高;条件不满足立刻拉低。
它仅用于内部临界状态检测,本身带有硬件毛刺,不能直接对外输出,也不能直接用于内部复位控制。
- 时序寄存器 Tx_Done:模块对外标准输出
c
always@(posedge Clk)
Tx_Done <= W_Tx_Done;
作用:同步缓存W_Tx_Done瞬时状态,输出无毛刺、宽度固定 1 个时钟周期的标准脉冲。
模块内部bit_cnt清零、发送使能关闭、LED 翻转,外部上层逻辑全部使用同步后的Tx_Done,彻底规避毛刺干扰。
2.4 单独拆分 W_Tx_Done 三层核心意义
意义 1:代码分层解耦,调试更直观
如果不单独定义W_Tx_Done,直接写:
c
always@(posedge Clk)
Tx_Done <= (bit_cnt == MCNT_BIT && baud_div_cnt == MCNT_BAUD);
语法可以正常运行,但仿真波形无法单独观测内部瞬时判断信号。拆分后仿真可同时拉出W_Tx_Done、Tx_Done对比,一眼看到毛刺、时序延迟,快速定位时序 bug。
意义 2:逻辑修改只改一处,维护简单
后续如果需要修改发送完成判定规则(比如增加校验位、停止位延时),仅修改W_Tx_Done一行赋值即可,无需改动时序输出代码,逻辑耦合度低。
意义 3:隔离内部瞬时组合信号,保护全局时序逻辑
模块内部所有复位、使能控制全部使用同步后的Tx_Done,不会直接读取带毛刺的W_Tx_Done,从根源避免毛刺造成的随机逻辑异常,提升模块稳定性。
2.5 通俗类比理解
W_Tx_Done= 高速相机抓拍一瞬间画面,抓拍瞬间存在抖动、杂点(毛刺);
Tx_Done= 把抓拍画面存入内存缓存一整帧,画面稳定无杂点,交给内部、外部逻辑读取使用。
直接用抖动的抓拍画面做判断,很容易看错;缓存一帧稳定后再使用,行为完全可预期。
三、仿真波形
仿真 Testbench 代码:
c
`timescale 1ns / 1ns
module uart_0_tb();
reg Clk;
reg Reset_n;
wire Led;
reg [7:0]Data;
wire uart_tx;
wire Tx_Done;
reg Send_Go;
uart_0 uart_0(
.Clk(Clk),
.Reset_n(Reset_n),
.Data(Data),
.Led(Led),
.uart_tx(uart_tx),
.Tx_Done(Tx_Done),
.Send_Go(Send_Go)
);
initial Clk = 1;
always #10 Clk = ~Clk;
initial begin
Reset_n = 0;
#201;
Reset_n = 1;
Send_Go =1;
Data = 8'b0010_1001; //0x29
#20;
Send_Go =0;
#30000000;
Send_Go =1;
Data = 8'b1101_0110; //0xD6
#20;
Send_Go =0;
#30000000;
Send_Go =1;
Data = 8'b0010_1001; //0x29
#20;
Send_Go =0;
#30000000;
$stop;
end
endmodule
波形分析

波形现象总结:
- 每次拉高Send_Go一个时钟周期,模块锁存输入数据并启动串口发送;
- uart_tx输出标准 UART 8N1 时序,空闲电平保持高;
- 核心观测点:Tx_Done在整帧串口数据完全发送结束后,输出宽度 1 个时钟周期的同步高脉冲。
结合前文时序知识点:
Tx_Done是经过寄存器打一拍后的同步时序信号,波形稳定无毛刺,适合直接对接上层状态机、FIFO 控制逻辑;
如果拉出内部组合信号W_Tx_Done对比,可以直观观察到两者相差一个时钟周期,验证 "组合瞬时判断信号 → 时序同步输出" 分层设计思路。
总结
- 架构延续:完全复用前文受控线性序列机时序模型,学习门槛低,适合系列教程连贯学习;
- 功能升级:移除内置延时,新增Send_Go触发、参数化分频、Tx_Done完成标志,适配绝大多数工程串口场景;
- 时序核心知识点(本文重点):
组合逻辑天生存在竞争冒险,会产生毛刺;
对外完成、有效标志信号,工程标准做法是时序寄存器打一拍输出;
W_Tx_Done作为中间组合信号,用于瞬时临界判断、波形调试,分层设计提升代码可维护性与稳定性; - 底层逻辑不变:依托 D 触发器边沿锁存特性缓存数据,避免发送过程数据跳变导致串口乱码。