ARTICLE DETAIL

资讯详情

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

FPGA UART接收抗干扰设计:毛刺滤除与采样容差实战

FPGA UART接收抗干扰设计:毛刺滤除与采样容差实战 高速串口通信在高噪声环境下的接收可靠性一直是FPGA工程里一个看起来简单做起来抓狂的模块。很多朋友在实验板上用杜邦线连来连去115200波特率跑得飞快数据也稳如老狗一旦把设备装到工业现场、电机旁边、开关电源附近或者线缆稍微长一点就开始出现随机乱码、丢字节、甚至整个接收状态机卡死。这篇文章我想把UART串口接收在高干扰情况下的设计思路、代码实现、采样容差计算和实测排障过程完整梳理一遍尤其适合正在用FPGA做数据采集、仪器控制、通信网关的朋友参考。先说清楚一个容易被忽视的事实FPGA里的UART接收逻辑如果是自己写的默认是裸奔的。MCU的硬件UART外设一般自带模拟滤波、数字滤波、错误标志、超时中断而FPGA里的寄存器传输级代码如果不做抗干扰设计一个毛刺就能让起始位误触发然后整帧错位、校验失败、状态机飞掉。这篇文章就围绕怎么把这些坑填平来展开。1. 高干扰环境下UART接收到底会坏在哪1.1 干扰进入接收链路的三种典型路径UART接收出问题绝大多数情况不是协议本身难而是干扰怎么进来的问题没想清楚。我在实际项目里总结下来干扰主要通过三条路径进入接收链路。第一条是信号线本身被耦合。长线传输的时候UART的TX/RX线就像两根天线电机启停、继电器吸合、变频器PWM切换都会在空间里辐射电磁波线缆越长耦合进来的噪声能量越大。这种情况在工业设备里极其常见我在一个步进电机驱动项目里就遇到过电机一启动串口立刻开始出乱码电机停下来就好非常典型。第二条是电源和地平面的噪声。FPGA的IO接收阈值是相对于芯片电源和参考地的如果板子上电源纹波大或者地平面有瞬态电压跌落接收引脚的电平判断就会发生偏移。本来高电平的门槛是2.0V地弹导致参考地瞬间抬升1V那2.5V的高电平信号可能就被判成低电平。这个问题在FPGA和电机驱动共板、共电源时特别严重。第三条是阻抗不匹配造成的反射。线缆长了之后如果收发两端没有匹配电阻信号边沿会产生振铃在采样点附近形成来回抖动的电平。这种振铃和毛刺还不太一样它有规律性而且示波器上看波形方波不方,上升沿带台阶。FPGA的IO如果配置成施密特触发器模式对抗这种振铃会有一定帮助。1.2 干扰造成的四类典型失效模式顺着干扰路径往里走接收逻辑本身会表现出四种故障特征我列个表把现象和根因对应起来。失效现象根因典型场景偶发一两个字节乱码数据位采样点踩到毛刺或振铃线缆靠近逆变器、开关电源连续多帧错位空闲态毛刺误触发起始位帧边界丢失电机启动瞬间、继电器动作接收状态机卡死不再响应后续数据停止位采样错误导致状态机陷入错误分支波特率偏差干扰叠加收帧丢失但示波器看波形正常接收端地弹导致阈值漂移板级电源布局不合理这四类失效模式我全都在项目里遇到过其中第二类和第三类最坑因为它们不会自恢复。UART接收一旦因为一个毛刺误判了起始位后面的数据位采样全都错位而且如果没有超时保护状态机就永远停在那里等了后续发多少数据都进不来。这就是为什么很多工程师发现拔掉重插一下就好了——其实是重新上电把状态机复位了。1.3 为什么FPGA的UART接收要单独做抗干扰设计有人可能会问MCU的UART也工作在同样环境里为什么没那么容易出现这种问题这里有两个原因。一个原因是硬件层面的。MCU的UART外设通常自带输入滤波比如STM32的USART就有数字滤波器和模拟滤波器能够滤掉窄于一定宽度的毛刺。FPGA里如果你直接用always (posedge clk) rx_d rx;这样简单的打拍同步那完全没有任何滤波能力毛刺打到寄存器上是什么就是什么。另一个原因是逻辑层面的。MCU的UART外设是经过验证的IP起始位检测、帧错误标志、噪声标志、溢出标志都齐全固件里可以检测到错误然后做恢复处理。FPGA里自己写的接收状态机如果不主动设计超时复位、错误帧恢复这些逻辑那出了问题就只能靠全局复位这在工程上是不可接受的。所以结论很清楚在高干扰环境下使用FPGA做UART接收抗干扰设计不是锦上添花而是保命必备。2. 抗干扰接收架构三件套组合拳2.1 过采样率与采样时钟的工程取舍最经典的做法是用系统时钟对UART RX线做16倍过采样。所谓16倍过采样就是采样时钟频率是波特率的16倍这样每一位数据持续16个采样时钟周期接收逻辑在每个数据位的理想中点附近取采样值避开数据跳变沿附近的信号不稳定区域。为什么偏偏是16倍这是工程上的一个折中。过采样率太高比如32倍抗干扰能力确实更强但对时钟频率要求更高比如115200波特率需要3.6864MHz采样时钟如果系统只有50MHz分频出来要满足整倍数关系反而麻烦。过采样率太低比如8倍采样点位置精度不够波特率容差也变小。16倍是几十年来串行通信领域的经典取值RS-232的UART控制器基本都是这个标准。实际工程里采样时钟不是非要用PLL去生成一个独立时钟的直接用一个计数器对系统时钟分频就行。以50MHz系统时钟、115200波特率为例16倍采样频率是1.8432MHz分频系数是50_000_000 / 1_843_200 27.126取整27后实际采样频率是1.851MHz偏差0.45%完全在容差范围内。// 16倍过采样时钟分频50MHz / 16 / 115200 localparam integer OVERSAMPLE_CNT 27; // 27.126取整 reg [4:0] clk_div_cnt; reg sample_clk_en; always (posedge clk or negedge rst_n) begin if (!rst_n) begin clk_div_cnt 5d0; sample_clk_en 1b0; end else if (clk_div_cnt OVERSAMPLE_CNT - 1) begin clk_div_cnt 5d0; sample_clk_en 1b1; // 单周期脉冲 end else begin clk_div_cnt clk_div_cnt 1b1; sample_clk_en 1b0; end end2.2 毛刺滤除边沿必须眼见为实高干扰环境下UART RX线在空闲态时是高电平如果有毛刺把线拉低接收逻辑就会认为这是一个起始位下降沿然后开始采样。这是最典型的误触发方式。解决思路是对RX线的低电平状态做持续确认只有连续N个采样时钟都读到低电平才认为这是一个真正的起始位。这个思路和我们平时判断一个人是不是真的死了要先探几分钟脉搏是一个道理。毛刺一般是窄脉冲持续时间短而真正的起始位会拉低至少一个完整位时间也就是16个采样周期。所以我们可以设计一个毛刺滤除窗口比如要求连续采样到8个低电平才确认起始位有效。reg [3:0] start_glitch_cnt; wire rx_n !rx_sync; // 低有效检测 // 起始位确认连续8个采样周期为低才触发 always (posedge clk or negedge rst_n) begin if (!rst_n) start_glitch_cnt 4d0; else if (rx_sync) // 一旦变高立刻清零 start_glitch_cnt 4d0; else if (sample_clk_en start_glitch_cnt 4d8) start_glitch_cnt start_glitch_cnt 1b1; end wire start_valid (start_glitch_cnt 4d8);这个滤除窗口的宽度要结合实际的干扰脉宽来定。如果现场的干扰是窄脉冲比如几百纳秒级别的ESD放电8个采样周期在115200下约4.3us足够滤掉。但如果干扰是宽脉冲比如电机反向电动势造成的几十微秒电平跌落那8个周期不够得相应加大窗口。不过窗口也不能太长否则遇到相邻字节间隔极短的连续传输可能会漏掉起始位。工程上我一般默认取半个位时间也就是8个采样周期然后再用示波器实际测一下干扰脉宽来调整。2.3 数据位采样中点还是多点确认起始位有效之后接收逻辑进入数据位采样流程。每个数据位持续16个采样周期最简单的做法是从起始位下降沿开始计数数到每个数据位的第8个采样周期也就是位时间的正中间采一次样。中点采样的原理是数据位跳变之后要经过一段时间信号才稳定中点离两边跳变沿最远抗干扰余量最大。这个思路没错但高干扰环境下我建议更进一步采用多次采样、多数判决。具体做法是在每个数据位的中点附近连续采3次比如在第6、7、8个采样周期各取一次然后三个值做多数投票至少两个相同才作为该位的最终采样值。// 数据位三段采样与多数判决 reg [2:0] bit_sample; // 三段采样结果 always (posedge clk or negedge rst_n) begin if (!rst_n) bit_sample 3d0; else if (sample_clk_en bit_cnt 4d6) bit_sample[0] rx_sync; else if (sample_clk_en bit_cnt 4d7) bit_sample[1] rx_sync; else if (sample_clk_en bit_cnt 4d8) bit_sample[2] rx_sync; end wire bit_value (bit_sample[0] bit_sample[1]) | (bit_sample[0] bit_sample[2]) | (bit_sample[1] bit_sample[2]);多数判决能扛住比单点采样更宽的干扰。单点采样只要采样时刻踩到毛刺这一位就错了三点采样即使有一点踩到毛刺另外两点还能把正确值投出来。代价是多一点组合逻辑对FPGA来说资源开销完全可以忽略。2.4 接收主状态机带超时兜底最后一道防线是状态机的容错设计。经典UART接收状态机是IDLE→START→DATA→STOP→IDLE这么一圈每个状态之间靠位计数器和采样点转移。高干扰下状态机最怕的是因为某个位采样错误转移条件判断不到状态机卡在DATA或者STOP状态出不来。我的做法是给每一个状态都加超时判断。比如STOP状态正常情况下持续16个采样周期就该回IDLE但如果因为某些原因16个周期之后条件不满足就强制回IDLE并置一个帧错误标志。DATA阶段同理如果位计数器走到超过数据位长度两倍还没正常结束也强制复位。这相当于给状态机上了保险丝任何异常状态都能在有限时间内自恢复。// 超时兜底状态机卡住超过MAX_WAIT自动回到IDLE localparam integer MAX_WAIT 100; reg [6:0] wait_cnt; always (posedge clk or negedge rst_n) begin if (!rst_n) begin wait_cnt 7d0; rx_state IDLE; end else if (sample_clk_en) begin if (wait_cnt MAX_WAIT) begin rx_state IDLE; // 超时强制复位 wait_cnt 7d0; end else begin wait_cnt wait_cnt 1b1; // 正常状态转移逻辑... end end end这个超时兜底逻辑平时用不上但一旦用上就是救命稻草。我在一个现场总线项目中因为接地不良导致干扰极强接收状态机频繁卡死加了超时逻辑之后系统虽然还是会误码但至少不会再一次卡死、必须重启的情况整体的可靠性上了一个台阶。3. Verilog代码实现完整的抗干扰UART接收模块3.1 顶层信号与参数定义把上面的设计思路拼到一起一个完整的抗干扰UART接收模块可以这样组织。首先定义参数系统时钟频率、目标波特率、数据位宽、是否启用校验。用参数化的方式写方便换到不同项目里直接改参数复用。module uart_rx_hardened #( parameter integer CLK_FREQ 50_000_000, // 系统时钟Hz parameter integer BAUD_RATE 115200, // 波特率 parameter integer DATA_WIDTH 8, // 数据位宽 parameter integer OVERSAMPLE 16, // 过采样率 parameter integer GLITCH_REJECT 8 // 毛刺滤除窗口 )( input wire clk, // 系统时钟 input wire rst_n, // 异步复位低有效 input wire rx, // UART接收引脚 output reg [DATA_WIDTH-1:0] rx_data, // 接收数据 output reg rx_valid, // 数据有效脉冲 output reg rx_frame_err, // 帧错误指示 output reg rx_busy // 接收忙指示 );其中GLITCH_REJECT参数就是起始位毛刺滤除窗口默认半个位时间。rx_frame_err这个信号特别有用在调试阶段把它接到LED上或者用逻辑分析仪抓可以很直观地看到干扰出现的频率。3.2 输入同步与毛刺滤除逻辑异步信号进入FPGA必须先做跨时钟域同步否则会出亚稳态。这里RX信号是异步的需要先用两级触发器同步再做毛刺滤除。// 两级触发器同步消除亚稳态 reg rx_sync1, rx_sync2; always (posedge clk or negedge rst_n) begin if (!rst_n) begin rx_sync1 1b1; // UART空闲态是高电平 rx_sync2 1b1; end else begin rx_sync1 rx; rx_sync2 rx_sync1; end end wire rx_sync rx_sync2;毛刺滤除逻辑把起始位检测分成两步先等RX线连续出现GLITCH_REJECT个采样时钟的低电平确认真的发生起始位了再进入数据位采样流程。localparam IDLE 3d0; localparam START 3d1; localparam DATA 3d2; localparam STOP 3d3; reg [2:0] rx_state; reg [3:0] bit_cnt; // 位内采样计数 0~15 reg [3:0] data_bit_cnt; // 数据位计数 reg [DATA_WIDTH-1:0] shift_reg; // 移位寄存器接收数据 wire sample_mid sample_clk_en (bit_cnt 4d7); // 中点采样标志 always (posedge clk or negedge rst_n) begin if (!rst_n) begin rx_state IDLE; rx_valid 1b0; end else if (sample_clk_en) begin case (rx_state) IDLE: begin rx_valid 1b0; rx_busy 1b0; bit_cnt 4d0; if (start_valid) begin rx_state DATA; rx_busy 1b1; data_bit_cnt 4d0; end end // DATA / STOP 状态逻辑详见下节 endcase end end3.3 数据采样与状态机主体DATA状态下每个数据位持续16个采样周期在每个位的中点附近bit_cnt6/7/8做三次采样多数判决得到该位的值移入移位寄存器。数据位收满DATA_WIDTH个后进入STOP状态。STOP状态中点采样如果采样到高电平说明帧结束正确如果还是低电平说明产生了帧错误。DATA: begin bit_cnt bit_cnt 1b1; // bit_cnt 枚举 0~15 if (sample_mid) begin // 数据位采样完成移位 shift_reg {bit_value, shift_reg[DATA_WIDTH-1:1]}; if (data_bit_cnt DATA_WIDTH - 1) begin rx_state STOP; bit_cnt 4d0; end else begin data_bit_cnt data_bit_cnt 1b1; end end end STOP: begin bit_cnt bit_cnt 1b1; if (bit_cnt 4d7) begin if (rx_sync) begin // 正确停止位 rx_data shift_reg; rx_valid 1b1; rx_frame_err 1b0; end else begin // 停止位期间采样到低电平帧错误 rx_frame_err 1b1; end rx_state IDLE; rx_busy 1b0; end end这里有个细节rx_valid信号我设计成在STOP状态中点确认停止位正确时产生这样输出的数据就已经是被确认为正确帧的数据而不是边收边给。如果接收端处理器不需要帧错误信息可以直接把rx_valid当成数据可用标志来用。3.4 状态机超时兜底代码状态机超时保护的实现放在最顶层不需要进到每个状态里单独判断。只要当前状态不是IDLE并且持续超过特定时间没有完成全部转移就从任何状态强制回到IDLE。// 状态机超时自恢复逻辑 always (posedge clk or negedge rst_n) begin if (!rst_n) begin fsm_cnt 0; end else if (rx_state IDLE) begin fsm_cnt 0; end else if (fsm_cnt FSM_TIMEOUT) begin rx_state IDLE; // 超时强制复位 rx_frame_err 1b1; fsm_cnt 0; end else if (sample_clk_en) begin fsm_cnt fsm_cnt 1; end endFSM_TIMEOUT参数取多少合适我一般取完整一帧时间起始位数据位停止位的2~3倍。以8位数据、1位停止位为例一帧共10个位时间FSM_TIMEOUT可以取10 * OVERSAMPLE * 2 320个采样时钟。这样即使因为干扰导致某个边界没识别到状态机也会在最多两帧时间内自动恢复不会永久卡死。3.5 异步复位与跨时钟域处理提醒整个模块的复位信号建议用异步复位、同步释放的结构避免复位信号释放时与时钟沿冲突造成亚稳态。同时如果上层还要把接收数据和系统内部其他时钟域的模块对接需要在输出端再做一次跨时钟域握手或者FIFO缓冲我这里输出rx_valid单脉冲和rx_data总线信号是一种最简单的模式适合单字节接收的场景。// 异步复位同步释放 reg rst_n_sync1, rst_n_sync2; always (posedge clk or negedge rst_n) begin if (!rst_n) begin rst_n_sync1 1b0; rst_n_sync2 1b0; end else begin rst_n_sync1 1b1; rst_n_sync2 rst_n_sync1; end end wire rst_n_async rst_n_sync2;4. 干扰与波特率偏差叠加极限容差的计算4.1 分频截断误差到底有多大前面提到过采样分频系数存在取整截断这个截断会带来实际波特率与理论波特率的偏差。以50MHz时钟、115200波特率、16倍过采样为例理论分频系数是27.126代码取整数27后实际采样时钟是50MHz / 27 1.8519MHz对应的实际波特率是1.8519MHz / 16 115740偏差约0.47%。这个偏差看起来不大但它会随着传输的字节数累积。每个位时间采样点会逐渐偏离理想位置到了帧末尾偏差最大。对于8位数据1位起始位1位停止位的10位帧累积偏差最大是0.47% * 10 4.7%的位时间。也就是说停止位采样点相对理想位置偏移了不到半个位时间还在容差范围内。4.2 干扰导致的边沿抖动如何叠加波特率偏差是温水煮青蛙式的慢性偏移干扰造成的边沿抖动则是突然袭击式的瞬时误差。两者叠加之后采样点离理想中点有多远直接决定了误码率。我画一个简单的场景来算。假设接收端和发送端各偏离0.5%波特率方向相反那么最大相对偏移是1%。对于一帧10位的数据到停止位时累积偏移已经达到1% * 10 10%位时间也就是1.6个采样周期16倍过采样下。如果此时干扰刚好把数据电平抖动了一下而采样点又因为偏移靠近了位边界那误码概率就急剧升高。所以抗干扰设计不是只看毛刺滤除波特率匹配同样至关重要。一个工程经验UART收发双方的波特率误差之和要控制在±2%以内最好是±1%以内。超过这个范围即使没有干扰长帧传输也会开始出错。4.3 设计时的容差预算表项目推荐值说明收发双方波特率误差之和≤ ±2%建议用高精度晶振或校准时钟毛刺滤除窗口4~8个采样周期太短滤不干净太长影响起始位识别采样点距位边沿距离≥ 4个采样周期留足干扰抖动余量多数判决采样点位时间 6/16、7/16、8/16比中点略偏前留出边沿余量状态机超时时间2~3倍帧时间过长恢复慢过短误判多4.4 用仿真验证容差极限写完代码之后我强烈建议做一次蒙特卡洛式的仿真验证把发送端的波特率参数故意偏调几个百分点同时在RX线上注入叠加不同宽度、不同频率的毛刺观察接收模块是否还能正确恢复数据。我自己的经验是用Verilog的#延时或者随机数发生器来模拟干扰波形比较麻烦更高效的方式是用一个模拟干扰的testbench在发送字节的过程中随机翻转RX线几个采样周期。比如在起始位附近注入一个持续2~3个采样周期的低电平毛刺验证毛刺滤除逻辑能不能挡住。这种仿真跑下来基本上能暴露出所有设计缺陷。我在一个项目里就发现毛刺滤除窗口设成4个采样周期时对500ns的窄干扰有效但对付不了1us以上的宽干扰于是果断改成了8个周期。这个结论不通过仿真全靠现场调试的话浪费的时间会非常可观。5. 实测复盘ILA抓到的那些见鬼波形5.1 一次真实的干扰排查过程去年做一个电机驱动板的通信接口FPGA和上位机之间用RS-232转TTL的模块通信115200波特率线缆长度大约1.5米。样机测试时一切正常装进金属机箱、接上电机负载之后通信立刻乱码频繁。一开始我怀疑是电源问题用示波器测了FPGA核心电压纹波在电机启动瞬间确实有300mV左右的毛刺。把电源的滤波电容加大之后纹波降到100mV以内但乱码并没有完全消失。然后用示波器直接测RX引脚发现信号本身在电机启动时会产生明显的振铃尤其在上升沿和下降沿之后有±1V左右的过冲。这个时候我意识到问题可能不只是电源更可能出在接收逻辑对边沿的处理上。于是用ILA在FPGA内部抓信号触发放到了rx_frame_err上终于看到了真实的情况。5.2 ILA抓到的关键波形和结论ILA抓到的内部信号显示在电机启动的瞬间空闲态的RX线上出现了连续多个低电平毛刺每个毛刺宽度大约2~3个采样时钟周期大约1.6us左右。我原来的接收代码是简单的下降沿触发起始位检测所以每来一个毛刺就会触发一次起始位检测状态机反复进入DATA状态等到真正有效的数据帧到达时状态机还卡在DATA状态里自然就漏掉了帧头。这个波形直接印证了前面的分析干扰导致起始位误触发帧边界丢失后续数据全部错位。解决了根因之后我做了三个修改乱码率从测试中几乎无法稳定通信降到了连续传输100万字节无错码。第一是毛刺滤除窗口从默认的1个采样周期改成了8个采样周期。第二是数据位采样从单点改成了三点多数判决。第三是状态机加了超时自恢复。这三个修改加进去之后除了电机启动瞬间偶尔还会有帧错误标志产生之外用户数据完全不受影响。5.3 误码率测试方法别靠感觉正常验收一个抗干扰UART接收模块不能靠感觉好像没错了来判断。我建议用回环测试和压力测试两种方式配合。回环测试是让FPGA同时跑一个发送模块和一个接收模块通过上位机下发已知数据FPGA收到后原样返回上位机校验对比。这种方式能快速暴露收发链路的错误率。压力测试则是长时间连续跑随机数据或递增数据统计错误帧数和重传次数。我用Python写了个简单的测试脚本通过串口连续发100万字节统计接收端返回的数据一致性基本能在一个晚上跑完。压力测试的时间要覆盖现场的干扰周期。比如电机不是一直转的是走走停停的那测试时间至少要超过几个完整的启停周期才能把最恶劣的工况测进去。5.4 硬件层面的配合动作抗干扰设计不能只靠FPGA内部逻辑硬件上的配合也很重要。实测中效果最明显的是这三件事RX线上串一个100Ω电阻靠近FPGA引脚端既可以限制过冲电流又能和引脚的寄生电容组成低通滤波对高频毛刺有明显压制。然后是RX线对地并联一个10pF到100pF的小电容进一步滤高频噪声。注意电容不能太大否则会拖慢信号边沿影响高速波特率下的信号完整性115200波特率下100pF是完全没问题的但如果你跑921600这种高速率就要谨慎使用了。最后是隔离和接地。如果干扰实在压不住就要考虑用数字隔离器把FPGA的地和外部设备的地隔离开比如ADuM1201这种。我见过不少现场问题本质上都是地环路惹的祸信号线屏蔽层两端都接地反而造成了环路电流改成单端接地之后世界清净了。5.5 留给你的自检清单在实际项目中用一个抗干扰UART接收模块我建议你按这个顺序做自检。先把模块的毛刺滤除窗口和多数判决功能用仿真验证一遍注入不同宽度的毛刺确认滤除和透传行为符合预期。然后上板做实机测试用示波器看RX引脚的信号质量重点观察电机启停、继电器动作这些干扰源工作瞬间的波形。再用ILA或者逻辑分析仪抓一次接收状态机的运行轨迹确认状态转移是否和设计一致。最后做误码率压力测试并要求在测试期间记录帧错误标志的有效次数而不是只看最后数据对不对。这一套流程走下来基本能保证你的UART接收模块在高干扰环境下是可靠的。但也别指望一次就能调好我自己的经验是现场干扰的情况五花八门常常需要反复调几次毛刺滤除窗口的大小才能找到最适合当前干扰特征的参数组合。特别是那种实验室一切正常、现场随机乱码的情况大多数时候不是逻辑写错了而是你没有给接收模块装上足够的盔甲。希望这篇文章的架构思路和代码细节能让你的FPGA串口通信在恶劣环境里也能稳如老狗。
返回列表