十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

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

FPGA入门(十二):受控线性序列机 UART 接收工程化实现 FPGA入门文章目录FPGA入门前言一、完整代码总览1.1 顶层演示模块1.2 核心接收模块uart_byte_rx_B1.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.BAUD115200;// 接收成功一次LED翻转一次always(posedge Clk or negedge Reset_n)if(!Reset_n)Led0;elseif(Rx_Done)Led~Led;endmodule1.2 核心接收模块uart_byte_rx_Btimescale1ns/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_FREQ50_000_000;parameter BAUD9600;parameter MCNT_BAUDCLOCK_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_cntMCNT_BAUD/2)(bit_cnt9);// 波特率分频计数器仅接收使能开启时计数always(posedge Clk or negedge Reset_n)if(!Reset_n)baud_div_cnt0;elseif(en_baud_cnt)beginif(baud_div_cntMCNT_BAUD)baud_div_cnt0;elsebaud_div_cntbaud_div_cnt1d1;endelsebaud_div_cnt0;// --------------------------// 下降沿检测两级同步电路// --------------------------reg r_uart_rx;reg dff0_uart_rx;reg dff1_uart_rx;// 前两级异步信号同步器消除亚稳态always(posedge Clk)begin dff0_uart_rxuart_rx;dff1_uart_rxdff0_uart_rx;end// 第三级寄存前一时刻状态用于边沿检测always(posedge Clk)r_uart_rxdff1_uart_rx;// 下降沿判定前一拍高当前拍低wire nedge_uart_rx;assign nedge_uart_rx(dff1_uart_rx0)(r_uart_rx1);// --------------------------// 接收使能控制逻辑// --------------------------always(posedge Clk or negedge Reset_n)if(!Reset_n)en_baud_cnt0;// 条件1检测到下降沿启动接收elseif(nedge_uart_rx)en_baud_cnt1;// 条件2起始位中点发现是高电平判定为毛刺终止接收elseif((baud_div_cntMCNT_BAUD/2)(bit_cnt0)(dff1_uart_rx1))en_baud_cnt0;// 条件3停止位中点正常接收完成关闭使能elseif((baud_div_cntMCNT_BAUD/2)(bit_cnt9))en_baud_cnt0;// --------------------------// 比特步数计数器// --------------------------always(posedge Clk or negedge Reset_n)if(!Reset_n)bit_cnt0;// 提前清零停止位中点复位计数器为下一次接收做准备elseif((baud_div_cntMCNT_BAUD/2)(bit_cnt9))bit_cnt0;// 每个比特周期结束步数1elseif(baud_div_cntMCNT_BAUD)bit_cntbit_cnt1d1;// --------------------------// 中间采样数据移位// --------------------------always(posedge Clk or negedge Reset_n)if(!Reset_n)r_Rx_Data8d0;// 仅在每个比特的中间时刻采样电平elseif(baud_div_cntMCNT_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_Datar_Rx_Data;endcase end// --------------------------// 接收完成标志数据输出// --------------------------// 延续发送篇规范组合信号打一拍输出无毛刺always(posedge Clk)Rx_Donew_Rx_Done;// 接收完成时刻将缓存数据输出到端口always(posedge Clk)if(w_Rx_Done)Rx_Datar_Rx_Data;endmodule1.2 仿真 Testbenchtimescale1ns/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 Clk1;always #10Clk~Clk;initial begin Reset_n0;uart_rx1;#201;Reset_n1;#200;// 发送第一帧8b0101_0101 (0x55)uart_rx0;#(5208*20);//起始位uart_rx1;#(5208*20);//bit0uart_rx0;#(5208*20);//bit1uart_rx1;#(5208*20);//bit2uart_rx0;#(5208*20);//bit3uart_rx1;#(5208*20);//bit4uart_rx0;#(5208*20);//bit5uart_rx1;#(5208*20);//bit6uart_rx0;#(5208*20);//bit7uart_rx1;#(5208*20);//停止位#(5208*20*10);// 发送第二帧8b1010_1010 (0xAA)uart_rx0;#(5208*20);//起始位uart_rx0;#(5208*20);//bit0uart_rx1;#(5208*20);//bit1uart_rx0;#(5208*20);//bit2uart_rx1;#(5208*20);//bit3uart_rx0;#(5208*20);//bit4uart_rx1;#(5208*20);//bit5uart_rx0;#(5208*20);//bit6uart_rx1;#(5208*20);//bit7uart_rx1;#(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_rxuart_rx;dff1_uart_rxdff0_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_rx0)(r_uart_rx1);这个 nedge_uart_rx 脉冲就是同步后的、无毛刺的起始位触发信号对应发送篇的 Send_Go 作用。2.2 为什么必须在比特中间采样边沿采样的致命缺陷如果我们在每个比特的起始时刻采样会有两个严重问题跳变沿不稳定电平跳变的瞬间存在过渡区此时采样极易采到错误值波特率偏差会累积收发双方的晶振都存在误差比如发送端波特率偏快每比特快几纳秒一帧 10 比特下来就会偏移几十纳秒连续多帧后采样点会偏移到相邻比特彻底乱码。中间采样的容错原理每个比特周期的中间位置是电平最稳定、离两边跳变沿最远的时刻。即使双方波特率有 ±5% 的偏差采样点依然落在当前比特的有效电平范围内不会采到相邻比特。举个直观例子一个比特周期有 5208 个时钟周期50MHz/9600中间点就是第 2604 个周期。哪怕发送端偏快实际只有 5000 个周期中间点 2604 依然落在这个比特的电平范围内不会采错。baud_div_cntMCNT_BAUD/2通俗类比上课点名不要在打铃瞬间点要在课程上到一半的时候点所有人都坐稳了不会有人刚进门、刚出门点名最准确。2.3 进阶优化提前结束接收对抗累积误差什么是波特率累积误差实际工程中很多低成本设备的串口波特率并不精准可能每一帧都比标准值快十几纳秒。如果我们必须等停止位完全结束计数器计满 MCNT_BAUD才复位、才允许检测下一个起始位连续接收几十帧后误差会累积到半个比特以上下一个起始位已经来了我们的计数器还没复位直接漏掉起始位造成丢帧。提前结束的实现方式代码中所有 “完成判定” 都放在了停止位bit_cnt9的中间时刻也就是 baud_div_cnt MCNT_BAUD/2w_Rx_Done 在停止位中点拉高提前输出完成标志en_baud_cnt 同步关闭波特率计数器停止bit_cnt 同步清零立刻进入空闲状态准备检测下一个起始位。带来的核心收益误差不累积每接收一帧就提前半个比特复位相当于每帧都重新对齐一次误差不会逐帧叠加连续接收不丢帧预留出半个比特的余量哪怕发送端波特率偏快、帧间隔很短我们也能及时准备好捕获下一个起始位停止位后半段无用停止位是高电平我们只需要确认它的中间是高电平就够了后半段等待完全没有意义。这是工业级 UART 接收的标准优化手段也是新手最容易忽略的工程细节。2.4 起始位毛刺过滤防误触发实际电路板上可能存在电磁干扰会在串口线上产生几纳秒的窄低脉冲看起来像起始位但其实是干扰毛刺。代码中加入了二次校验机制elseif((baud_div_cntMCNT_BAUD/2)(bit_cnt0)(dff1_uart_rx1))en_baud_cnt0;逻辑很简单检测到下降沿启动接收后等到起始位的中间点再检查一次电平。如果此时已经变回高电平说明刚才是窄毛刺直接终止接收不继续采样后续数据避免输出错误字节。三、仿真波形仿真 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 Clk1;always #10Clk~Clk;initial begin Reset_n0;#201;Reset_n1;Send_Go1;Data8b0010_1001;//0x29#20;Send_Go0;#30000000;Send_Go1;Data8b1101_0110;//0xD6#20;Send_Go0;#30000000;Send_Go1;Data8b0010_1001;//0x29#20;Send_Go0;#30000000;$stop;end endmodule波形分析本次仿真基于 50MHz 系统时钟周期 20ns波特率设置为 9600Testbench 中按照 UART 协议低位在前的规则依次发送 4 字节测试数据0x55、0xAA、0xF0、0x0F用于全流程验证接收逻辑的正确性。整体波形全貌如下复位初始化阶段波形现象仿真最左侧Reset_n为低电平Rx_Done保持 0Rx_Data显示为红色不定态XX。对应代码逻辑模块中所有带异步复位的时序逻辑都会在!Reset_n时执行初始化// 波特率计数器复位清零baud_div_cnt0;// 接收使能复位关闭en_baud_cnt0;// 比特步数计数器复位清零bit_cnt0;// 内部数据缓存寄存器复位清零r_Rx_Data8d0;而对外输出的Rx_Data寄存器我们的原始代码没有加入复位分支always(posedge Clk)if(w_Rx_Done)Rx_Datar_Rx_Data;Verilog 仿真规则中reg型变量的默认初始值为不定态X只有被赋值语句主动更新后才会变为确定值。由于第一次接收完成前Rx_Data从未被赋值触发因此复位后依然保持仿真初始的不定态,波形中显示为红色XX。补充说明该现象仅存在于仿真环境。实际 FPGA 芯片上电后寄存器会有确定的初始电平默认低电平不会出现不定态。如果希望仿真波形更规整只需给Rx_Data的 always 块加上异步复位即可复位后输出固定为8’d0always(posedge Clk or negedge Reset_n)if(!Reset_n)r_Rx_Data8d0;elseif(baud_div_cntMCNT_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_Datar_Rx_Data;endcase end总结架构延续完全沿用受控线性序列机模型和发送模块逻辑对称学习成本低接收三大核心难点异步信号同步、中间采样、误差累积分别通过两级同步器、中点采样、提前结束机制解决工程化细节起始位毛刺过滤、参数化设计、时序输出规范都是工业级代码的必备要素收发对应关系发送端的 Send_Go 对应接收端的 nedge_uart_rx发送端的 Tx_Done 对应接收端的 Rx_Done接口规范统一方便后续封装 FIFO、打包协议层。
返回列表