FPGA入门
文章目录
- FPGA入门
- 前言
- 一、完整代码总览
- 1.1 顶层演示模块
- 1.2 核心接收模块uart_byte_rx_B
- 1.2 仿真 Testbench
- 二、接收篇核心新增知识点(本文重点)
- 2.1 异步信号安全处理:两级同步 + 下降沿检测
- 2.2 为什么必须在比特中间采样?
- 2.3 进阶优化:提前结束接收,对抗累积误差
- 2.4 起始位毛刺过滤:防误触发
- 三、仿真波形
- 仿真 Testbench 代码:
- 波形分析
- 复位初始化阶段
- 总结
前言
在上一篇教程中,我们完成了工程化的 UART 发送模块设计,重点讲解了组合逻辑毛刺与时序输出规范。而串口接收的设计复杂度远高于发送 —— 发送端是主动输出时序,接收端则是被动处理外部异步输入信号,需要应对跨时钟域亚稳态、线路干扰毛刺、双方波特率偏差等一系列工程问题。
本次我们基于同款受控线性序列机架构,实现稳定可靠的 UART 字节接收模块,所有核心新增知识点如下:
- 两级同步 + 下降沿检测:解决异步输入的亚 态风险,精准捕获起始位下降沿;
- 比特中间采样:解释为什么不能采边沿、必须采中间值,最大化采样容错率;
- 提前结束接收机制:停止位中点即判定完成,对抗波特率累积误差,避免高速连续接收丢帧;
- 起始位毛刺过滤:二次校验起始位电平,滤除线路干扰导致的误触发;
- 延续发送篇规范:参数化波特率、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;endmodule1.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;endmodule1.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 组成了标准的两级同步器:
- 第一级寄存器 dff0 先把异步信号打一拍,大概率能把亚稳态稳定下来;
- 第二级寄存器 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 为什么必须在比特中间采样?
边沿采样的致命缺陷
如果我们在每个比特的起始时刻采样,会有两个严重问题:
- 跳变沿不稳定:电平跳变的瞬间存在过渡区,此时采样极易采到错误值;
- 波特率偏差会累积:收发双方的晶振都存在误差,比如发送端波特率偏快,每比特快几纳秒,一帧 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:
- w_Rx_Done 在停止位中点拉高,提前输出完成标志;
- en_baud_cnt 同步关闭,波特率计数器停止;
- 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总结
- 架构延续:完全沿用受控线性序列机模型,和发送模块逻辑对称,学习成本低;
- 接收三大核心难点:异步信号同步、中间采样、误差累积,分别通过两级同步器、中点采样、提前结束机制解决;
- 工程化细节:起始位毛刺过滤、参数化设计、时序输出规范,都是工业级代码的必备要素;
- 收发对应关系:发送端的 Send_Go 对应接收端的 nedge_uart_rx,发送端的 Tx_Done 对应接收端的 Rx_Done,接口规范统一,方便后续封装 FIFO、打包协议层。