我最早开始写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 = 3'd0; localparam START = 3'd1; localparam DATA = 3'd2; localparam STOP = 3'd3; 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{1'b1}}; rx_filtered <= 1'b1; end else begin rx_filter_reg <= {rx_filter_reg[FILTER_TAPS-2:0], rx}; if (&rx_filter_reg) rx_filtered <= 1'b1; else if (~|rx_filter_reg) rx_filtered <= 1'b0; end end always @(posedge clk or negedge rst_n) begin if (!rst_n) rx_filtered_d <= 1'b1; 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_TAPS=8,能滤掉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 <= 8'd0; sample_idx <= 4'd0; bit_idx <= 3'd0; shift_reg <= 8'd0; data_out <= 8'd0; data_valid <= 1'b0; end else begin data_valid <= 1'b0; case (state) IDLE: begin bit_clk_cnt <= 8'd0; sample_idx <= 4'd0; if (start_fall_edge) state <= START; end START: begin if (bit_clk_cnt == BIT_CLK_CNT - 1) begin bit_clk_cnt <= 8'd0; if (sample_idx == SAMPLE_TIMES/2 - 1) begin if (~rx_filtered) state <= DATA; else state <= IDLE; end else begin sample_idx <= sample_idx + 1'b1; end end else begin bit_clk_cnt <= bit_clk_cnt + 1'b1; end end DATA: begin if (bit_clk_cnt == BIT_CLK_CNT - 1) begin bit_clk_cnt <= 8'd0; if (sample_idx == SAMPLE_TIMES - 1) begin sample_idx <= 4'd0; if (bit_idx == 3'd7) begin bit_idx <= 3'd0; state <= STOP; end else begin bit_idx <= bit_idx + 1'b1; end end else begin sample_idx <= sample_idx + 1'b1; 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 + 1'b1; end end STOP: begin if (bit_clk_cnt == BIT_CLK_CNT - 1) begin bit_clk_cnt <= 8'd0; if (sample_idx == SAMPLE_TIMES/2 - 1) begin data_out <= shift_reg; data_valid <= 1'b1; state <= IDLE; end else begin sample_idx <= sample_idx + 1'b1; end end else begin bit_clk_cnt <= bit_clk_cnt + 1'b1; 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_CNT=27,实际波特率变成了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小时的持续收发测试,这个习惯帮我挡住了不少潜在的量产问题。