FPGA入门(十一):受控线性序列机 UART 发送升级

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 发送模块做全面工程化迭代,所有改动如下:

  1. 移除内置 1s 延时计数器delay_cnt,把延时控制交给上层,模块解耦通用;
  2. 新增触发输入Send_Go,外部脉冲即可启动一次串口发送;
  3. 新增缓存逻辑:仅Send_Go有效时,用 D 触发器锁存输入数据,避免发送中途数据跳变乱码;
  4. 参数化设计CLOCK_FREQ、BAUD,自动计算波特率分频值,无需手动计算;
  5. 新增发送完成标志Tx_Done,反馈一帧数据发送完毕;
  6. 核心时序优化(本文重点):拆分组合逻辑中间信号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;

功能说明:

  1. 检测到Send_Go高电平时,利用 D 触发器锁存当前输入 8bit 数据到r_Data;
  2. 同时拉高发送使能en_baud_div_cnt,启动波特率计数器,开始输出串口时序;
  3. 一帧发送完成后,Tx_Done拉高,自动关闭发送使能,等待下一次触发脉冲。

二、本文核心重点:组合逻辑毛刺科普 + W_Tx_Done 分层时序设计

2.1 基础科普:什么是组合逻辑,它有什么致命缺陷?

  1. 组合逻辑定义
    没有时钟、没有寄存器,输入实时直接映射输出,典型写法:assign 信号 = 判断条件,代码中W_Tx_Done就是标准组合逻辑信号。
  2. 组合逻辑固有缺陷:毛刺(竞争冒险)
    FPGA 内部由大量门电路组成,不同寄存器、线路的信号传输延迟不一致。
    当判断条件依赖两个及以上独立寄存器时,寄存器更新不会完全同步,会产生几纳秒~几十纳秒的窄脉冲,也就是毛刺。

本模块中判断条bit_cnt9&&baud_div_cntMCNT_BAUD同时依赖两组独立计数器:

  • bit_cnt:记录序列步数
  • baud_div_cnt:波特率分频计数器
    同一个时钟上升沿,两组寄存器更新速度存在硬件延迟差,会短暂出现 "条件同时成立" 的瞬时窗口,组合逻辑信号会产生毛刺。

2.2 工业标准解决方案:对外控制 / 完成标志统一用时序逻辑输出

工程规范:所有对外有效标志、完成脉冲、触发信号,禁止直接输出组合逻辑,必须用寄存器打一拍同步输出。

用时钟寄存器锁存一拍,可以过滤窄毛刺,输出脉冲宽度固定为 1 个完整时钟周期,波形干净、时序稳定,不会出现随机误触发。

2.3 分层信号讲解:W_Tx_Done 与 Tx_Done 各自定位

  1. wire W_Tx_Done:纯组合中间信号
c 复制代码
wire W_Tx_Done;
assign W_Tx_Done = (bit_cnt == MCNT_BIT && baud_div_cnt == MCNT_BAUD);

作用:实时瞬时抓拍 "一帧发送完毕" 的临界瞬间

只要两个计数条件同时满足,信号立刻拉高;条件不满足立刻拉低。

它仅用于内部临界状态检测,本身带有硬件毛刺,不能直接对外输出,也不能直接用于内部复位控制。

  1. 时序寄存器 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

波形分析

波形现象总结:

  1. 每次拉高Send_Go一个时钟周期,模块锁存输入数据并启动串口发送;
  2. uart_tx输出标准 UART 8N1 时序,空闲电平保持高;
  3. 核心观测点:Tx_Done在整帧串口数据完全发送结束后,输出宽度 1 个时钟周期的同步高脉冲。
    结合前文时序知识点:
    Tx_Done是经过寄存器打一拍后的同步时序信号,波形稳定无毛刺,适合直接对接上层状态机、FIFO 控制逻辑;
    如果拉出内部组合信号W_Tx_Done对比,可以直观观察到两者相差一个时钟周期,验证 "组合瞬时判断信号 → 时序同步输出" 分层设计思路。

总结

  1. 架构延续:完全复用前文受控线性序列机时序模型,学习门槛低,适合系列教程连贯学习;
  2. 功能升级:移除内置延时,新增Send_Go触发、参数化分频、Tx_Done完成标志,适配绝大多数工程串口场景;
  3. 时序核心知识点(本文重点):
    组合逻辑天生存在竞争冒险,会产生毛刺;
    对外完成、有效标志信号,工程标准做法是时序寄存器打一拍输出;
    W_Tx_Done作为中间组合信号,用于瞬时临界判断、波形调试,分层设计提升代码可维护性与稳定性;
  4. 底层逻辑不变:依托 D 触发器边沿锁存特性缓存数据,避免发送过程数据跳变导致串口乱码。
相关推荐
jianqiang.xue2 小时前
ESP-IDF保姆级入门29|FreeRTOS任务管理与调度全解:任务创建/状态切换/优先级调度/双核负载均衡/实时性优化,掌握嵌入式多任务编程核心
stm32·单片机·mcu·物联网·51单片机·iot
自小吃多2 小时前
PCB Editor软件走线、修线操作笔记
笔记·嵌入式硬件
X_xcccc3 小时前
2026硬核拆解:高性能异构计算板卡选型指南
fpga开发
闪亮的屁灯儿3 小时前
DMA 学习复盘笔记
stm32
新晨单片机设计4 小时前
S008A-基于STM32单片机八路抢答器(数码管)【Proteus仿真+Keil程序+原理图】
stm32·单片机·proteus
米饭不加菜4 小时前
STM32——数码管显示
stm32·单片机·嵌入式硬件
梵亿4 小时前
基于 STM32F103C8T6 的无人机低功耗设计与实现
stm32·嵌入式硬件·无人机·低功耗
hrw_embedded4 小时前
Keil开发与STM32Cube开发的差异点总结(未完待续)
stm32·单片机·嵌入式硬件
Brilliantwxx5 小时前
【STM32】RCC与时钟树 深度详解
stm32·单片机·嵌入式硬件·架构