FPGA高干扰环境下UART接收的抗干扰设计与Verilog实现

发布时间:2026/10/3 5:54:14
FPGA高干扰环境下UART接收的抗干扰设计与Verilog实现 我最早开始写UART接收逻辑的时候照着教程搭了一个状态机在实验室桌面怎么测怎么好。结果把板子搬到电机驱动器旁边数据直接乱成一锅粥。那会儿我才真正理解FPGA做串口接收考验人的从来不是协议本身而是怎么在高干扰场景下把毛刺、地噪声、串扰这些脏东西滤干净。这篇文章就围绕串口接收在高干扰情况下的工程化处理展开从干扰来源、设计思路、Verilog实现到实测和排查方法完整走一遍。适用对象是那些正在做FPGA串口通信、产品需要过现场环境、或者被概率性乱码折磨的同学。不管你是刚写完一个能亮灯的UART接收模块还是已经在量产项目里调试串口可靠性这篇内容应该都能给你一些可以参考的思路。1. 高干扰环境下UART接收容易翻车的根因很多朋友第一次写UART接收参考的是开发板附带的demo程序。这类Demo通常默认的理想条件短走线、干净电源、无外部辐射。但实际项目里UART走线往往要经过连接器、排线、甚至跨越整个板子旁边可能还躺着DCDC电感、电机驱动、显示屏排线。这时候问题就来了。1.1 干扰到底从哪里来从工程现场看串口收到的脏信号主要来自三个方向。第一是电源和地平面噪声。数字电路开关瞬间会在地平面上制造压差如果发送端和接收端的参考地之间存在较高噪声串口线上的逻辑电平就会叠加额外的纹波。表现在接收端就是本来干净的高电平和低电平之间出现了不规则的上下跳变。第二是相邻信号线的串扰。排线中串口线和PWM控制线走在一起PWM跳变沿通过寄生电容耦合到串口线上特别容易触发接收端的下降沿误判。这类干扰的宽度通常从几十纳秒到几百纳秒不等非常接近TTL信号本身的跳变沿普通示波器不仔细看都容易忽略。第三是外部电磁辐射耦合。电机、继电器、电磁阀这些感性负载在开关瞬间会产生很高的尖峰电压若屏蔽和滤波做得不到位干扰会直接耦合进串口线缆。线缆越长天线效应越明显问题也越严重。1.2 干扰破坏了接收的哪个环节UART接收有三个关键判断点任何一个出问题都会产生误码。首先是起始位判定。UART空闲时为高电平发送端拉低一位时间表示起始位。接收端通过检测下降沿来启动接收过程。如果线上出现一个窄毛刺接收端就会误以为起始位来了进入接收状态然后采样到的数据全是乱的。其次是数据位采样。标准UART接收在每一位的中点附近采样并通过多次采样来确定该位的电平值。若干扰使某一位的电平在采样点附近发生跳变这个位就会被判错。最后是停止位校验。停止位必须是高电平若干扰让停止位被拉低接收端会报帧错误。很多上位机软件的串口调试助手会把这类错误显示成乱码或者直接丢掉一帧数据。1.3 传统教科书代码为什么不够用很多UART接收例程都长得很像检测到下降沿后延时半个位周期然后在中点采一次电平采满8位就输出一个字节。这种代码在理想环境下工作没问题但存在两个致命弱点。一是没有任何毛刺滤除能力一次下降沿就触发接收毛刺来了照单全收。二是数据位采样只做单点判断一旦干扰恰好落在采样点上这一位大概率就错了而且可能引发后续所有位的连锁错位。所以高干扰环境下第一要务不是把接收波特率调快而是把接收端的免疫能力做上去。抗干扰的核心可以归纳为两件事对起始位的确认要足够保守对数据位的判断要有冗余。2. 抗干扰UART接收的整体设计思路我在实际项目里采用的方案是在传统UART接收逻辑基础上增加三部分过采样、电平滤波、多数表决。整体不需要额外硬件纯逻辑改进却能让接收可靠性提升几个数量级。2.1 过采样用时间冗余换可靠性过采样的思想很简单用远高于波特率的时钟去抽样接收引脚上的电平。比如115200波特率一个位周期约为8.68微秒。若系统时钟是50MHz一个位周期对应约434个时钟周期那就是434倍过采样。不过工程上常用的做法是设定一个固定的过采样倍数比如16倍即每个位周期均匀采样16次。16倍采样的好处是采样点的位置分辨率足够高可以比较精确地定位位中心又不会因为采样点数太多而浪费寄存器资源。8倍采样在噪声环境下概率上不够稳32倍采样对逻辑资源消耗略大16倍是性价比不错的选择。下采样后的序列会保留原始信号中脉宽大于一个采样周期的特征。如果毛刺宽度小于采样周期它可能被采样到也可能被漏掉这样后续滤波才有意义。所以先把原始信号同步到系统时钟域这是第一件必做的事。2.2 电平滤波连续确认才算是真电平FPGA引脚进来的信号首先要做的是消除亚稳态。标准做法是打两拍或者三拍同步。我在同步器之后还会再加一个连续N拍确认逻辑只有连续多个时钟周期都采样到同一个电平才认为引脚电平真的变了。这个思路很像按键消抖一次跳变不算数连续稳定才代表真实状态。对于50MHz时钟若做4拍连续确认大约能滤掉80纳秒宽的毛刺若做8拍连续确认能滤掉160纳秒宽的毛刺。具体N值看系统噪声情况我一般取4到8之间。需要注意的是电平滤波会影响起始位下降沿的检测响应时间但对115200这类常用波特率来说多几十纳秒的响应延迟完全不影响正常接收。2.3 位中心多点采样加多数表决电平滤波可以把窄毛刺挡在门外但有些干扰脉宽较宽仍可能穿过滤波。这时候数据位采样的冗余就派上用场了。在每个位周期的中心点附近连续采样三到五次然后取多数值作为这一位的最终判断结果。比如在中心点前一个时钟、中心点、中心点后一个时钟各采一次三值里至少两个为高才算高。这样即使某一次采样被干扰拉偏另外两次正常采样也能把结果纠正回来。多数表决的代价是每个数据位需要额外几个寄存器保存采样中间值相对于可靠性的提升这点代价非常值得。我做过对比测试在高干扰环境下单点采样的误码率约为千分之一到百分之一加了三取二表决后误码率直接掉到万分之一以下效果非常明显。2.4 波特率偏差预留容忍空间还有一个容易被忽视的问题收发两端的时钟源存在偏差。比如发送端用的晶振是1%精度的接收端FPGA用的是50ppm的晶振理论上不超过1%就能工作但加上干扰导致的边沿抖动误码率会显著上升。设计时建议把采样点放在位中心附近5%到10%的窗口内偏移不要精确卡在某一个时钟沿上。这样即使波特率存在千分之几的误差采样点也不会滑出安全区域。3. 基于Verilog的核心实现解析接下来是全文的重点部分直接上代码级方案。下面的设计以50MHz系统时钟、115200波特率、16倍过采样为例完整实现一个带抗干扰处理的UART接收模块。代码用Verilog编写具备参数化能力可复用到不同工程。3.1 模块框架与参数定义module uart_rx_robust #( parameter CLK_FREQ 50_000_000, parameter BAUD_RATE 115200, parameter SAMPLE_TIMES 16, parameter FILTER_TAPS 8 )( input wire clk, input wire rst_n, input wire rx, output reg [7:0] data_out, output reg data_valid ); localparam BIT_CLK_CNT CLK_FREQ / (BAUD_RATE * SAMPLE_TIMES); localparam IDLE 3d0; localparam START 3d1; localparam DATA 3d2; localparam STOP 3d3; reg [2:0] state; reg [7:0] bit_clk_cnt; reg [3:0] sample_idx; reg [2:0] bit_idx; reg [7:0] shift_reg; reg [FILTER_TAPS-1:0] rx_filter_reg; reg rx_filtered; reg rx_filtered_d; wire start_fall_edge;模块参数里FILTER_TAPS是电平滤波阶数SAMPLE_TIMES是过采样倍数。BIT_CLK_CNT是每个采样点的时钟周期数由系统时钟、波特率、采样倍数共同决定。3.2 输入同步与毛刺滤波always (posedge clk or negedge rst_n) begin if (!rst_n) begin rx_filter_reg {FILTER_TAPS{1b1}}; rx_filtered 1b1; end else begin rx_filter_reg {rx_filter_reg[FILTER_TAPS-2:0], rx}; if (rx_filter_reg) rx_filtered 1b1; else if (~|rx_filter_reg) rx_filtered 1b0; end end always (posedge clk or negedge rst_n) begin if (!rst_n) rx_filtered_d 1b1; else rx_filtered_d rx_filtered; end assign start_fall_edge rx_filtered_d (~rx_filtered);这里用了两段逻辑完成滤波。rx_filter_reg是一个FILTER_TAPS位的移位寄存器只有当所有位都是1时输出高电平所有位都是0时输出低电平其他情况保持原值不变。这种设计的本质是只有在连续FILTER_TAPS个时钟周期内信号稳定在一个电平上才认为信号真正切到了该电平任何宽度小于FILTER_TAPS个时钟周期的毛刺都会被过滤掉。对于50MHz时钟和FILTER_TAPS8能滤掉160纳秒以内的毛刺基本覆盖了板级串扰和地弹引起的窄脉冲。start_fall_edge是下降沿脉冲只在rx_filtered从高变低的瞬间拉高一个时钟周期是状态机离开IDLE状态的触发信号。3.3 状态机与数据采样always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; bit_clk_cnt 8d0; sample_idx 4d0; bit_idx 3d0; shift_reg 8d0; data_out 8d0; data_valid 1b0; end else begin data_valid 1b0; case (state) IDLE: begin bit_clk_cnt 8d0; sample_idx 4d0; if (start_fall_edge) state START; end START: begin if (bit_clk_cnt BIT_CLK_CNT - 1) begin bit_clk_cnt 8d0; if (sample_idx SAMPLE_TIMES/2 - 1) begin if (~rx_filtered) state DATA; else state IDLE; end else begin sample_idx sample_idx 1b1; end end else begin bit_clk_cnt bit_clk_cnt 1b1; end end DATA: begin if (bit_clk_cnt BIT_CLK_CNT - 1) begin bit_clk_cnt 8d0; if (sample_idx SAMPLE_TIMES - 1) begin sample_idx 4d0; if (bit_idx 3d7) begin bit_idx 3d0; state STOP; end else begin bit_idx bit_idx 1b1; end end else begin sample_idx sample_idx 1b1; end if (sample_idx SAMPLE_TIMES/2 - 1) begin shift_reg {shift_reg[6:0], rx_filtered}; end end else begin bit_clk_cnt bit_clk_cnt 1b1; end end STOP: begin if (bit_clk_cnt BIT_CLK_CNT - 1) begin bit_clk_cnt 8d0; if (sample_idx SAMPLE_TIMES/2 - 1) begin data_out shift_reg; data_valid 1b1; state IDLE; end else begin sample_idx sample_idx 1b1; end end else begin bit_clk_cnt bit_clk_cnt 1b1; end end endcase end end endmodule这个状态机的关键点有四个。第一起始位确认不是检测到下降沿就马上进数据接收而是在起始位的中心点再确认一次仍然是低电平。如果只是一个下降沿毛刺滤波后信号很快回到高电平在中心点检查时会发现不是低电平于是回到IDLE状态不会误触发。第二数据位采样在sample_idx等于SAMPLE_TIMES/2 - 1时进行也就是位中心偏前一个采样点。这样做是为了留出时钟偏差余量即使收发两端波特率有千分之几的偏差采样点也能落在数据位的稳定区间内。第三shift_reg是左移寄存器先接收低位8个数据位接收完成后正好拼成一个字节。UART协议规定LSB先发送这个移位方向是对的。第四stop位处理这里只做中心点确认不校验停止位是否真的为高。实际使用中如果关心帧错误检测可以在这里加一个判断逻辑。不过多数应用场景中数据错位已经能被起始位确认和多点采样兜住是否需要停止位校验要看具体需求。3.4 多数表决的补充实现上面代码做的是单点采样加电平滤波如果干扰脉宽较宽还可以在数据位采样位置做三取二多数表决。方法是在位中心前后三个采样点分别采集电平值然后投票输出。reg rx_sample_pre; reg rx_sample_mid; reg rx_sample_post; wire rx_bit_value (rx_sample_pre rx_sample_mid) | (rx_sample_pre rx_sample_post) | (rx_sample_mid rx_sample_post);这三个寄存器分别在sample_idx等于SAMPLE_TIMES/2 - 2、SAMPLE_TIMES/2 - 1、SAMPLE_TIMES/2时采集rx_filtered值然后通过三取二逻辑得到最终的位数据。相比单点采样这种方案可以抵抗一个采样点宽度的脉冲干扰。需要付出的代价是数据位输出会比原来多延迟一两个时钟周期。UART本身是异步协议只要保证每一帧内部的时间关系正确这十几个时钟周期的延迟不会影响通信。3.5 参数计算与配置实例以50MHz系统时钟、115200波特率为例每个位周期包含约434个系统时钟。若定义16倍过采样则每个采样点间隔为27.125个时钟周期。取整后BIT_CLK_CNT27实际波特率变成了50MHz除以27再除以16等于115740和标准115200的偏差约为0.47%。这个偏差完全在UART容差范围内不需要额外修正。如果你的系统有精细化要求可以用计数器累加的方式做波特率生成把小数分频误差平均分配到整个位周期内。但对绝大多数应用来说直接取整已经足够。如果波特率是921600那一个采样点只有约3.39个时钟周期取整为3后偏差约13%。这个偏差就不可忽略了。所以当波特率较高时建议提高系统时钟频率或者把SAMPLE_TIMES降为8倍以减小取整误差的影响。4. 高干扰场景的实测与验证方法代码写完只能证明逻辑没语法错误能不能在高干扰场景下稳定工作必须靠实测。这一节分享我在实际项目里的验证方法。4.1 实验室里如何模拟高干扰环境没有现场设备时可以自己做干扰源。最方便的方法是拿一个带PWM输出的电机驱动板靠近被测FPGA板放置让PWM驱动电机运转同时把串口线从驱动板和电机旁边穿过。电机换相时电刷产生的火花和PWM跳变沿会在串口线上感应出大量毛刺。这种场景能复现现场80%以上的干扰情况。如果连电机都没有可以找一个继电器让单片机以10Hz左右的频率反复吸合继电器线圈线圈两端不接续流二极管。继电器触点通断瞬间会产生较强的电磁脉冲效果也很直观。实测时不能只看上位机有没有收到正确数据要统计一段时间内的误码率。方法是在发送端发一组已知的数据包接收端收到后回传或者比对把错误数量记录下来。我习惯的做法是发送端每隔10毫秒发送一帧256字节的伪随机数接收端通过串口回传接收结果上位机比较后统计错误字节数和错误帧数。4.2 用逻辑分析仪抓关键信号FPGA调试时片内的信号不好直接观察可以用在线逻辑分析仪抓内部状态。关注信号包括rx原始输入、rx_filtered滤波后信号、start_fall_edge、state状态值、shift_reg接收结果。把触发条件设为start_fall_edge上升沿抓一段带干扰的波形就能清楚看到滤波前后的差异。在有电机干扰的场景下我实际看过很多次这样的波形rx原始信号在正常的高电平和低电平之间夹杂着密集的窄毛刺经过滤波后的rx_filtered信号几乎是一条干净的电平线只有真正的起始位和停止位才发生跳变。这时候我就知道滤波逻辑真正在起作用。4.3 误码率数据和现场效果在同一套硬件上我把三种方案做了对比。方案一是不加滤波、单点采样方案二是加8拍滤波、单点采样方案三是加8拍滤波、三取二多数表决。电机干扰场景下连续发送1000帧256字节数据方案一的错误帧数有时超过500而且一旦错一帧往往整个缓冲区内大量数据错乱。方案二的错误帧数降到个位数。方案三在相同测试条件下基本能保持零错误偶尔的个别字节错误也在应用层校验的容错范围内。这个数据说明在一般工业现场干扰程度下滤波加多数表决基本能解决UART接收的可靠性问题。当然如果干扰强到让波特率周期都发生了明显抖动那就需要从硬件层面加强屏蔽和隔离了。5. 常见问题与排查技巧实录即使有了这套抗干扰接收逻辑实际应用中仍然可能遇到各种诡异问题。这里把我踩过的一些坑和排查思路整理出来。5.1 概率性误码但又不是全错如果上位机收到的数据大部分正常偶尔出现一两个字节错误优先怀疑的不是干扰而是波特率偏差。可以先用示波器测发送端的位周期是否标准再用逻辑分析仪看接收端采样点是否在数据位中心。排查思路是用上位机发送0x55这个字节即二进制01010101这是最容易暴露波特率偏差的测试数据。因为每一位都在翻转如果采样点偏移会出现某些位判错而某些位正常的情况。如果0x55都能稳定通过再换0xAA、0xFF、0x00测试。如果0x55收发正确但实际数据偶发错误那重点检查发送端是否存在位之间的长度抖动比如使用了不稳定的时钟源或者发送端在中断里拼凑字节时出现时序抖动。5.2 起始位被反复误触发但收不到完整数据这种情况通常是滤波阶数不够或者干扰毛刺宽度超过了滤波能力范围。排查方法是把FILTER_TAPS从8调到16再看FPGA内部在线逻辑分析仪抓到的start_fall_edge是否还在频繁产生。如果滤波阶数加到16还不行那就要怀疑不是毛刺问题而是地电位差把整个串口的空闲电平抬到了低于阈值。这时可以用万用表测接收端和发送端之间的地电压差。如果存在几百毫伏的压差需要加共地线或者在接收端增加电平整形芯片。5.3 接收方FPGA没丢数据但上位机丢数据有时候FPGA侧逻辑分析仪显示接收数据完全正常data_valid脉冲也一帧不落但上位机收到的字节数变少。这种问题往往出在USB转串口芯片的驱动上或者上位机串口缓冲区的读取不及时。排查方法是在FPGA内部加一个计数器记录data_valid产生次数同时在上位机统计收到的字节数。两边对不上再往USB转串口芯片驱动的层面排查。常见的情况是某些USB转串口芯片在高速率下自动丢包需要调整驱动参数或者换更可靠的芯片方案。5.4 FPGA平台移植的注意事项这套代码在Xilinx、Altera、高云等主流FPGA平台上都能直接使用但有几个平台相关的差异需要注意。首先是时钟频率不同平台的PLL配置不同系统时钟一旦变化BIT_CLK_CNT就会变需要通过参数重新计算。其次是复位风格Xilinx推荐使用同步复位Altera和高云对异步复位的支持也不完全一致建议在顶层根据平台统一处理复位信号。另外部分平台的综合工具会对移位寄存器自动推断成SRL16如果希望在在线逻辑分析仪里看到滤波寄存器的中间值可以在综合属性里关闭SRL推断。不过这个只影响调试观察不影响功能。5.5 硬件布线层面的补充建议抗干扰是系统问题不能只靠接收逻辑兜底。我每做一个带串口的板子都会跟硬件工程师确认几个点。一是串口线尽量短远离功率电路和高频信号线二是串口信号线建议串接22到33欧姆的电阻放在发送端用来抑制过冲和振铃三是在接收端引脚附近加一个10nF到100nF的对地电容可以进一步吸收高频毛刺但这只适用于波特率不高的场景高速率下电容会劣化边沿。如果走线必须穿过干扰源优先选屏蔽线屏蔽层单端接地。浅谈一点很多人觉得屏蔽层两端接地更好但在低频串口信号场景单端接地反而能避免地环路带来的共模干扰。根据我个人经验把接收逻辑和硬件处理结合起来才是最稳妥的做法。逻辑部分挡住常规毛刺硬件部分减少毛刺的产生源头两边一起做串口在恶劣环境下才能长期稳定工作。最后再分享一个小技巧。调试UART接收时别只盯着波形看一定要养成统计误码率的习惯。开发板的demo程序能跑通不代表什么连续跑一个晚上不出错才能说明问题。我这边所有串口模块验收前都要经历至少12小时的持续收发测试这个习惯帮我挡住了不少潜在的量产问题。