news 2026/9/7 19:24:58

FPGA入门(十二):受控线性序列机 UART 接收工程化实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
FPGA入门(十二):受控线性序列机 UART 接收工程化实现

FPGA入门


文章目录

  • FPGA入门
  • 前言
  • 一、完整代码总览
    • 1.1 顶层演示模块
    • 1.2 核心接收模块uart_byte_rx_B
    • 1.2 仿真 Testbench
  • 二、接收篇核心新增知识点(本文重点)
    • 2.1 异步信号安全处理:两级同步 + 下降沿检测
    • 2.2 为什么必须在比特中间采样?
    • 2.3 进阶优化:提前结束接收,对抗累积误差
    • 2.4 起始位毛刺过滤:防误触发
  • 三、仿真波形
    • 仿真 Testbench 代码:
    • 波形分析
    • 复位初始化阶段
  • 总结

前言

在上一篇教程中,我们完成了工程化的 UART 发送模块设计,重点讲解了组合逻辑毛刺与时序输出规范。而串口接收的设计复杂度远高于发送 —— 发送端是主动输出时序,接收端则是被动处理外部异步输入信号,需要应对跨时钟域亚稳态、线路干扰毛刺、双方波特率偏差等一系列工程问题。
本次我们基于同款受控线性序列机架构,实现稳定可靠的 UART 字节接收模块,所有核心新增知识点如下:

  1. 两级同步 + 下降沿检测:解决异步输入的亚 态风险,精准捕获起始位下降沿;
  2. 比特中间采样:解释为什么不能采边沿、必须采中间值,最大化采样容错率;
  3. 提前结束接收机制:停止位中点即判定完成,对抗波特率累积误差,避免高速连续接收丢帧;
  4. 起始位毛刺过滤:二次校验起始位电平,滤除线路干扰导致的误触发;
  5. 延续发送篇规范:参数化波特率、w_前缀组合中间信号 + 寄存器打拍输出,保持代码风格统一。

一、完整代码总览

1.1 顶层演示模块

`timescale1ns/1ps moduleuart_byte_rx_test(Clk,Reset_n,uart_rx,Led,Rx_Data);input Clk;input Reset_n;input uart_rx;output reg Led;output[7:0]Rx_Data;wire Rx_Done;// 例化接收核心,可通过参数修改波特率uart_byte_rx_Buart_byte_rx_B(.Clk(Clk),.Reset_n(Reset_n),.uart_rx(uart_rx),.Rx_Done(Rx_Done),.Rx_Data(Rx_Data));defparam uart_byte_rx_B.BAUD=115200;// 接收成功一次,LED翻转一次always@(posedge Clk or negedge Reset_n)if(!Reset_n)Led<=0;elseif(Rx_Done)Led<=~Led;endmodule

1.2 核心接收模块uart_byte_rx_B

`timescale1ns/1ps moduleuart_byte_rx_B(Clk,Reset_n,uart_rx,Rx_Done,Rx_Data);input Clk;input Reset_n;input uart_rx;output reg Rx_Done;output reg[7:0]Rx_Data;parameter CLOCK_FREQ=50_000_000;parameter BAUD=9600;parameter MCNT_BAUD=CLOCK_FREQ/BAUD-1;// 内部寄存器定义reg[29:0]baud_div_cnt;// 波特率分频计数器reg en_baud_cnt;// 接收序列使能开关reg[3:0]bit_cnt;// 序列步数计数器 0~9reg[7:0]r_Rx_Data;// 接收数据缓存寄存器wire w_Rx_Done;// 组合逻辑接收完成中间信号// 核心:停止位中点提前判定完成,对抗波特率误差assign w_Rx_Done=(baud_div_cnt==MCNT_BAUD/2)&&(bit_cnt==9);// 波特率分频计数器,仅接收使能开启时计数always@(posedge Clk or negedge Reset_n)if(!Reset_n)baud_div_cnt<=0;elseif(en_baud_cnt)beginif(baud_div_cnt==MCNT_BAUD)baud_div_cnt<=0;elsebaud_div_cnt<=baud_div_cnt+1'd1;endelsebaud_div_cnt<=0;// --------------------------// 下降沿检测+两级同步电路// --------------------------reg r_uart_rx;reg dff0_uart_rx;reg dff1_uart_rx;// 前两级:异步信号同步器,消除亚稳态always@(posedge Clk)begin dff0_uart_rx<=uart_rx;dff1_uart_rx<=dff0_uart_rx;end// 第三级:寄存前一时刻状态,用于边沿检测always@(posedge Clk)r_uart_rx<=dff1_uart_rx;// 下降沿判定:前一拍高,当前拍低wire nedge_uart_rx;assign nedge_uart_rx=(dff1_uart_rx==0)&&(r_uart_rx==1);// --------------------------// 接收使能控制逻辑// --------------------------always@(posedge Clk or negedge Reset_n)if(!Reset_n)en_baud_cnt<=0;// 条件1:检测到下降沿,启动接收elseif(nedge_uart_rx)en_baud_cnt<=1;// 条件2:起始位中点发现是高电平,判定为毛刺,终止接收elseif((baud_div_cnt==MCNT_BAUD/2)&&(bit_cnt==0)&&(dff1_uart_rx==1))en_baud_cnt<=0;// 条件3:停止位中点,正常接收完成,关闭使能elseif((baud_div_cnt==MCNT_BAUD/2)&&(bit_cnt==9))en_baud_cnt<=0;// --------------------------// 比特步数计数器// --------------------------always@(posedge Clk or negedge Reset_n)if(!Reset_n)bit_cnt<=0;// 提前清零:停止位中点复位计数器,为下一次接收做准备elseif((baud_div_cnt==MCNT_BAUD/2)&&(bit_cnt==9))bit_cnt<=0;// 每个比特周期结束,步数+1elseif(baud_div_cnt==MCNT_BAUD)bit_cnt<=bit_cnt+1'd1;// --------------------------// 中间采样+数据移位// --------------------------always@(posedge Clk or negedge Reset_n)if(!Reset_n)r_Rx_Data<=8'd0;// 仅在每个比特的中间时刻采样电平elseif(baud_div_cnt==MCNT_BAUD/2)begincase(bit_cnt)1:r_Rx_Data[0]<=dff1_uart_rx;2:r_Rx_Data[1]<=dff1_uart_rx;3:r_Rx_Data[2]<=dff1_uart_rx;4:r_Rx_Data[3]<=dff1_uart_rx;5:r_Rx_Data[4]<=dff1_uart_rx;6:r_Rx_Data[5]<=dff1_uart_rx;7:r_Rx_Data[6]<=dff1_uart_rx;8:r_Rx_Data[7]<=dff1_uart_rx;default:r_Rx_Data<=r_Rx_Data;endcase end// --------------------------// 接收完成标志+数据输出// --------------------------// 延续发送篇规范:组合信号打一拍输出,无毛刺always@(posedge Clk)Rx_Done<=w_Rx_Done;// 接收完成时刻,将缓存数据输出到端口always@(posedge Clk)if(w_Rx_Done)Rx_Data<=r_Rx_Data;endmodule

1.2 仿真 Testbench

`timescale1ns/1ps moduleuart_byte_rx_tb();reg Clk;reg Reset_n;reg uart_rx;wire Rx_Done;wire[7:0]Rx_Data;uart_byte_rx_Buart_byte_rx_B(.Clk(Clk),.Reset_n(Reset_n),.uart_rx(uart_rx),.Rx_Done(Rx_Done),.Rx_Data(Rx_Data));initial Clk=1;always #10Clk=~Clk;initial begin Reset_n=0;uart_rx=1;#201;Reset_n=1;#200;// 发送第一帧:8'b0101_0101 (0x55)uart_rx=0;#(5208*20);//起始位uart_rx=1;#(5208*20);//bit0uart_rx=0;#(5208*20);//bit1uart_rx=1;#(5208*20);//bit2uart_rx=0;#(5208*20);//bit3uart_rx=1;#(5208*20);//bit4uart_rx=0;#(5208*20);//bit5uart_rx=1;#(5208*20);//bit6uart_rx=0;#(5208*20);//bit7uart_rx=1;#(5208*20);//停止位#(5208*20*10);// 发送第二帧:8'b1010_1010 (0xAA)uart_rx=0;#(5208*20);//起始位uart_rx=0;#(5208*20);//bit0uart_rx=1;#(5208*20);//bit1uart_rx=0;#(5208*20);//bit2uart_rx=1;#(5208*20);//bit3uart_rx=0;#(5208*20);//bit4uart_rx=1;#(5208*20);//bit5uart_rx=0;#(5208*20);//bit6uart_rx=1;#(5208*20);//bit7uart_rx=1;#(5208*20);//停止位#(5208*20*10);// 后续测试帧省略...$stop;end endmodule

二、接收篇核心新增知识点(本文重点)

2.1 异步信号安全处理:两级同步 + 下降沿检测

为什么不能直接用 uart_rx?
外部串口信号 uart_rx 和 FPGA 系统时钟是完全异步的两个时钟域信号。如果直接把它接到寄存器的输入端,会违反寄存器的建立 / 保持时间要求,产生亚稳态—— 输出电平既不是 0 也不是 1,会导致后续逻辑判断混乱,这是数字设计的经典问题。
两级同步器:消除亚稳态

reg dff0_uart_rx;reg dff1_uart_rx;always@(posedge Clk)begin dff0_uart_rx<=uart_rx;dff1_uart_rx<=dff0_uart_rx;end

代码中 dff0_uart_rx、dff1_uart_rx 组成了标准的两级同步器:

  1. 第一级寄存器 dff0 先把异步信号打一拍,大概率能把亚稳态稳定下来;
  2. 第二级寄存器 dff1 再打一拍,进一步降低亚稳态传播到后续逻辑的概率。
    经过两级同步后,dff1_uart_rx 就是和系统时钟完全同步、无亚稳态风险的信号,可以安全用于后续逻辑判断。
    通俗类比:就像接力赛跑,第一棒接住不稳的接力棒,第二棒拿稳了再往后传,避免掉棒。
    下降沿检测:精准捕获起始位
    UART 协议规定,空闲电平为高,起始位是一个完整周期的低电平。我们需要在起始位的下降沿(高变低时刻)启动接收序列。
    边沿检测的核心逻辑:前一个时钟周期是高电平,当前时钟周期是低电平,就说明发生了下降沿。
    代码中用 r_uart_rx 寄存 dff1_uart_rx 的前一拍状态,再通过组合逻辑判断:
assign nedge_uart_rx=(dff1_uart_rx==0)&&(r_uart_rx==1);

这个 nedge_uart_rx 脉冲就是同步后的、无毛刺的起始位触发信号,对应发送篇的 Send_Go 作用。

2.2 为什么必须在比特中间采样?

边沿采样的致命缺陷
如果我们在每个比特的起始时刻采样,会有两个严重问题:

  1. 跳变沿不稳定:电平跳变的瞬间存在过渡区,此时采样极易采到错误值;
  2. 波特率偏差会累积:收发双方的晶振都存在误差,比如发送端波特率偏快,每比特快几纳秒,一帧 10 比特下来就会偏移几十纳秒,连续多帧后采样点会偏移到相邻比特,彻底乱码。
    中间采样的容错原理
    每个比特周期的中间位置,是电平最稳定、离两边跳变沿最远的时刻。即使双方波特率有 ±5% 的偏差,采样点依然落在当前比特的有效电平范围内,不会采到相邻比特。
    举个直观例子:一个比特周期有 5208 个时钟周期(50MHz/9600),中间点就是第 2604 个周期。哪怕发送端偏快,实际只有 5000 个周期,中间点 2604 依然落在这个比特的电平范围内,不会采错。
baud_div_cnt==MCNT_BAUD/2

通俗类比:上课点名不要在打铃瞬间点,要在课程上到一半的时候点,所有人都坐稳了,不会有人刚进门、刚出门,点名最准确。

2.3 进阶优化:提前结束接收,对抗累积误差

什么是波特率累积误差?

实际工程中,很多低成本设备的串口波特率并不精准,可能每一帧都比标准值快十几纳秒。如果我们必须等停止位完全结束(计数器计满 MCNT_BAUD)才复位、才允许检测下一个起始位:

  • 连续接收几十帧后,误差会累积到半个比特以上;
  • 下一个起始位已经来了,我们的计数器还没复位,直接漏掉起始位,造成丢帧。
    提前结束的实现方式
    代码中所有 “完成判定” 都放在了停止位(bit_cnt=9)的中间时刻,也就是 baud_div_cnt == MCNT_BAUD/2:
  1. w_Rx_Done 在停止位中点拉高,提前输出完成标志;
  2. en_baud_cnt 同步关闭,波特率计数器停止;
  3. bit_cnt 同步清零,立刻进入空闲状态,准备检测下一个起始位。
    带来的核心收益
  • 误差不累积:每接收一帧就提前半个比特复位,相当于每帧都重新对齐一次,误差不会逐帧叠加;
  • 连续接收不丢帧:预留出半个比特的余量,哪怕发送端波特率偏快、帧间隔很短,我们也能及时准备好捕获下一个起始位;
  • 停止位后半段无用:停止位是高电平,我们只需要确认它的中间是高电平就够了,后半段等待完全没有意义。
    这是工业级 UART 接收的标准优化手段,也是新手最容易忽略的工程细节。

2.4 起始位毛刺过滤:防误触发

实际电路板上可能存在电磁干扰,会在串口线上产生几纳秒的窄低脉冲,看起来像起始位,但其实是干扰毛刺。
代码中加入了二次校验机制:

elseif((baud_div_cnt==MCNT_BAUD/2)&&(bit_cnt==0)&&(dff1_uart_rx==1))en_baud_cnt<=0;

逻辑很简单:检测到下降沿启动接收后,等到起始位的中间点再检查一次电平。如果此时已经变回高电平,说明刚才是窄毛刺,直接终止接收,不继续采样后续数据,避免输出错误字节。


三、仿真波形

仿真 Testbench 代码:

`timescale1ns/1ns moduleuart_0_tb();reg Clk;reg Reset_n;wire Led;reg[7:0]Data;wire uart_tx;wire Tx_Done;reg Send_Go;uart_0uart_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 #10Clk=~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

波形分析

本次仿真基于 50MHz 系统时钟(周期 20ns),波特率设置为 9600,Testbench 中按照 UART 协议低位在前的规则,依次发送 4 字节测试数据:0x55、0xAA、0xF0、0x0F,用于全流程验证接收逻辑的正确性。
整体波形全貌如下:

复位初始化阶段

波形现象:仿真最左侧,Reset_n为低电平,Rx_Done保持 0,Rx_Data显示为红色不定态XX。
对应代码逻辑:
模块中所有带异步复位的时序逻辑,都会在!Reset_n时执行初始化:

// 波特率计数器复位清零baud_div_cnt<=0;// 接收使能复位关闭en_baud_cnt<=0;// 比特步数计数器复位清零bit_cnt<=0;// 内部数据缓存寄存器复位清零r_Rx_Data<=8'd0;

而对外输出的Rx_Data寄存器,我们的原始代码没有加入复位分支:

always@(posedge Clk)if(w_Rx_Done)Rx_Data<=r_Rx_Data;

Verilog 仿真规则中,reg型变量的默认初始值为不定态X,只有被赋值语句主动更新后才会变为确定值。由于第一次接收完成前,Rx_Data从未被赋值触发,因此复位后依然保持仿真初始的不定态,波形中显示为红色XX。

补充说明:该现象仅存在于仿真环境。实际 FPGA 芯片上电后,寄存器会有确定的初始电平(默认低电平),不会出现不定态。如果希望仿真波形更规整,只需给Rx_Data的 always 块加上异步复位即可,复位后输出固定为8’d0:

always@(posedge Clk or negedge Reset_n)if(!Reset_n)r_Rx_Data<=8'd0;elseif(baud_div_cnt==MCNT_BAUD/2)begincase(bit_cnt)1:r_Rx_Data[0]<=dff1_uart_rx;2:r_Rx_Data[1]<=dff1_uart_rx;3:r_Rx_Data[2]<=dff1_uart_rx;4:r_Rx_Data[3]<=dff1_uart_rx;5:r_Rx_Data[4]<=dff1_uart_rx;6:r_Rx_Data[5]<=dff1_uart_rx;7:r_Rx_Data[6]<=dff1_uart_rx;8:r_Rx_Data[7]<=dff1_uart_rx;default:r_Rx_Data<=r_Rx_Data;endcase end

总结

  1. 架构延续:完全沿用受控线性序列机模型,和发送模块逻辑对称,学习成本低;
  2. 接收三大核心难点:异步信号同步、中间采样、误差累积,分别通过两级同步器、中点采样、提前结束机制解决;
  3. 工程化细节:起始位毛刺过滤、参数化设计、时序输出规范,都是工业级代码的必备要素;
  4. 收发对应关系:发送端的 Send_Go 对应接收端的 nedge_uart_rx,发送端的 Tx_Done 对应接收端的 Rx_Done,接口规范统一,方便后续封装 FIFO、打包协议层。
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 19:23:52

别再把“对标某系统”当口号!一套可落地的拆解方法论

“对标某系统”这句话&#xff0c;我在太多的评审会、规划会和周报里听到过。说的人往往神情笃定&#xff0c;仿佛只要把那个“某系统”三个字换成具体名字&#xff0c;项目的方向感和专业度就立刻拉满。但真被追问一句“对标它的什么、为什么对标、对标之后咱的差异化和生存空…

作者头像 李华
网站建设 2026/9/7 19:22:02

同城拼车项目面试问题_01

目录 1简单介绍一下你这个项目的整体架构&#xff1f; 2为什么选择微服务架构&#xff1f;和单体架构相比有什么优缺点&#xff1f; 3 服务是怎么拆分的&#xff1f;拆分的依据是什么&#xff1f; 4各服务之间是怎么通信的&#xff1f; 5Nacos 在你的项目中起什么作用&…

作者头像 李华
网站建设 2026/9/7 19:21:07

CAMS大气成分预报数据从下载到GRIB处理:Python实战与污染分析

1. CAMS到底是什么&#xff1a;一份能直接用于业务分析的大气成分预报数据做大气环境分析、空气质量预测或者新能源功率预测的朋友&#xff0c;一定绕不开一个问题&#xff1a;上哪儿弄一套覆盖范围广、更新及时、又不需要自己搭WRF-Chem去跑的大气成分数据。我最早接触CAMS纯属…

作者头像 李华
网站建设 2026/9/7 19:20:59

Hadoop+协同过滤:民宿推荐系统设计与实现全流程解析

1. 项目全景解读&#xff1a;任务书里没写明白但你必须知道的事2018年我第一次接手类似的课程设计时&#xff0c;以为“基于Hadoop的民宿推荐系统”就是搭个Hadoop环境、装个MySQL写个Web页面交差。结果真正做下来才发现&#xff0c;任务书里每个字都值得细品——这个项目表面是…

作者头像 李华
网站建设 2026/9/7 19:20:36

WIN32基础

1、VS2026生成的默认Win32程序 // HelloWindows.cpp : 定义应用程序的入口点。 //#include "framework.h" #include "HelloWindows.h"#define MAX_LOADSTRING 100// 全局变量: HINSTANCE hInst; // 当前实例 WCHAR szTitl…

作者头像 李华
网站建设 2026/9/7 19:20:22

水质监测指标计算与自动化分析实战指南

1. 水质指标计算项目概述水质监测是环境保护工作的基础环节&#xff0c;而指标计算代码则是实现自动化分析的核心工具。这个项目主要解决水质监测数据&#xff08;包括pH值、浊度等关键参数&#xff09;的批量处理与达标判定问题。在实际环境监测站工作中&#xff0c;每天需要处…

作者头像 李华