FPGA UART串口接收抗干扰设计:从毛刺滤波到多次采样的完整工程实践
如果你在调试FPGA的UART串口接收发现实验室里跑得稳稳当当一到现场就乱码、丢字节、甚至一错错一串那这篇就是为你准备的。围绕FPGA、UART、串口接收这三个关键词我把自己在强干扰环境下打磨接收逻辑的完整过程捋了一遍重点说说高干扰的情况到底会踩哪些坑以及怎么从架构上把问题解决掉。全文没有花架子都是可以直接抄到工程里的思路和代码片段适合正在搞FPGA开发、被串口通信折磨过的朋友。先说结论UART本身是个很老的异步串行协议教科书上那套“检测下降沿、延时半位、中心采样”的接收方式在干净环境里确实够用但放到继电器乱跳、变频器呼啸、电机启停频繁的工业现场基本就是裸奔。真正的抗干扰接收必须把输入滤波、起始位确认、多次采样、错误恢复这几件事全部做进状态机里而不是指望外部电路帮你擦屁股。1. 高干扰场景下的UART接收到底难在哪1.1 教科书代码在高干扰环境里为什么扛不住很多人学FPGA入门时写UART接收都是这么干的空闲时rxd为高检测到下降沿就认为起始位来了然后数半个位时间在数据位中心连续采样8个bit最后等停止位。这套逻辑在信号干净的时候没有任何问题因为时序是确定的电平是稳定的采样点永远踩在bit正中间。但高干扰环境下问题立刻就暴露出来了。干扰信号会在rxd线上叠加出大量窄毛刺这些毛刺宽度可能只有几十纳秒但幅度足够越过TTL或CMOS电平阈值。一旦毛刺出现在空闲态状态机就会误判成起始位紧接着开始接收一帧根本不存在的数据。更麻烦的是毛刺不光是增加假起始位它还会叠加在真正的数据信号上让沿的位置左右抖动。你原本算好的“延时半位再采样”就会踩偏轻则这一位采错重则后面所有位全部错位整帧报废。我遇到过最头疼的情况是现场一开变频器接收端就开始连续帧错误。用示波器一看rxd线上的毛刺已经严重到看起来像随机噪声了。这时你才会意识到教科书代码本质上是“信任信号”的代码它假设电平只有0和1没有第三种状态。但干扰环境下信号在0和1之间来回弹跳随便信一个边沿后面必然崩。1.2 干扰从哪里来常见工业场景的噪声源先别急着改代码搞清楚干扰从哪来更重要。在我接触过的项目里UART高干扰主要有几个来源感性负载通断继电器、接触器、电磁阀线圈断开瞬间会产生很高的反向电动势通过空间辐射或地线耦合进串口线。变频器与电机变频器输出的是高dv/dt的PWM波对附近的弱信号线来说是极强的干扰源尤其是长距离走线时串口线就像一根天线。长线缆与地电位差两个设备距离几十米地电位不完全相等共模电压会把接收端的电平参考打得乱七八糟。电源纹波如果串口芯片或FPGA的供电被电机驱动电路污染接收引脚的电平阈值就会抖动本来1.5V以下算低电平结果噪声一来判定边界全乱了。这些噪声有个共同特点它们不以“完整的一帧数据”形式出现而是以极窄的毛刺、边沿抖动、电平漂移的形式叠加在信号上。所以接收逻辑的核心任务不是“更快地采样”而是在噪声中把真实的数据沿和数据位挑出来。想通这一点方案就清晰了。2. 接收方案设计先把“抗干扰”刻进架构里2.1 分层设计思路输入滤波、起始位确认、动态采样、错误处理既然干扰主要在信号边缘和数据电平上做手脚那接收端就得分层设防。我在实际工程里把接收路径拆成了四道工序输入滤波原始rxd先过一级滑窗滤波器连续多个采样点都是同一个电平才认为电平真正翻转了。这一层干掉大部分窄毛刺。起始位确认检测到滤波后的下降沿后不急着开始收数而是等半个bit时间再采一次确认rxd仍然是低电平才认定这是真起始位。这一层干掉“毛刺伪装起始位”的情况。数据位多次采样每个数据位不止采一个点而是采三个点做多数表决避免单个采样点落在抖动上导致误判。错误检测与恢复停止位检查、超时退出、错误标志输出保证一帧被干扰弄坏之后状态机还能回到空闲态而不是卡死在某个状态。这四个工序缺一不可。我见过很多工程师只加了滤波觉得毛刺滤掉就万事大吉结果起始位确认没做照样会被宽一点的干扰脉冲骗到也有人只做多次采样却不做输入滤波结果毛刺直接让采样结果乱跳。我的经验是滤毛刺、确认起始位、多次采样这三件事必须同时做到缺一个高干扰下都会露出破绽。2.2 为什么宁愿牺牲一点实时性也要做滤波和多次采样有人会问滤波和多次采样会让接收逻辑变重会不会影响吞吐率答案是影响微乎其微但可靠性提升是数量级的。UART本身是异步低速协议常用波特率从9600到921600哪怕921600一个bit也只有1.085微秒左右。FPGA里的系统时钟通常是50MHz或更高一个bit对应几十个甚至几百个时钟周期拿几个周期出来做滤波和多次采样完全不心疼。真正要权衡的是滤波窗口大小。窗口太短毛刺滤不干净窗口太长会把真实的边沿变化也抹掉导致采样点偏移。我的经验法则是滤波窗口至少能覆盖你现场实测到的最宽毛刺但不能超过半个bit时间。比如采样时钟是5MHz一个bit时间是200个时钟周期那滤波窗口取4到8个时钟周期比较合适既滤掉了大多数窄毛刺又不影响正常跳变。2.3 采样点的选择中心采样 vs 多次表决教科书喜欢“中心采样”因为理论上bit中心离两边沿最远最安全。但高干扰环境下信号沿会抖动中心点本身也可能被噪声污染。我后来改成了每个bit内做三次采样位置分别放在bit周期的40%、50%、60%附近比如一个bit共16个采样钟就取第6、7、8个点然后三选二投票。这样做的好处是哪怕其中一次采样正好踩在毛刺上另外两次仍然能正确反映该bit的电平整体判断不会被带偏。关于为什么选40%、50%、60%而不是20%、50%、80%原因很简单UART在工业现场最容易在bit边沿处出问题起始沿和结束沿附近的信号质量最差所以三个采样点都应该尽量靠近bit中心区域但又要有一定间隔才能起到“多数表决”的效果。实践下来第6、7、8这三个点既不会太靠近边沿又有足够的分散度表现最稳。3. 核心代码实现一个带抗干扰处理的UART接收模块3.1 模块接口与参数定义下面这个模块是我在实际项目中经过多轮现场验证的版本做了一些精简便于说明但核心逻辑都保留着。参数化设计直接修改CLK_FREQ和BAUD_RATE就能适配不同平台。module uart_rx_ahb #( parameter CLK_FREQ 50_000_000, // 系统时钟 parameter BAUD_RATE 115200, // 波特率 parameter SAMPLE_MULT 16, // 每个bit采样倍数 parameter FILTER_CNT 4, // 滤波窗口连续N拍同电平 parameter DATA_WIDTH 8 )( input wire clk, input wire rst_n, input wire rxd, output reg [DATA_WIDTH-1:0] rx_data, output reg rx_valid, output reg frame_err, output reg parity_err );这里有几个参数要重点说明。SAMPLE_MULT通常取16也就是一个bit时间被划分成16个采样时钟周期这样起始位半位确认就是8个采样时钟。FILTER_CNT是高干扰工程里调得最频繁的参数建议做成可配置方便现场调试时用寄存器现场改而不是每次改代码重新综合。3.2 输入滤波与沿检测把毛刺先干趴下原始rxd进来之后先打一到多拍移位寄存器。当移位寄存器里所有位都是1时认为滤波后输出高电平所有位都是0时认为输出低电平中间状态保持上一次输出不变。这种“迟滞”效果能有效过滤掉窄毛刺。always (posedge clk or negedge rst_n) begin if (!rst_n) begin rxd_shift {FILTER_CNT{1b1}}; rxd_filtered 1b1; rxd_filtered_prev 1b1; end else begin rxd_shift {rxd_shift[FILTER_CNT-2:0], rxd}; if (rxd_shift) rxd_filtered 1b1; else if (~|rxd_shift) rxd_filtered 1b0; rxd_filtered_prev rxd_filtered; end end assign rxd_negedge rxd_filtered_prev ~rxd_filtered; assign rxd_posedge ~rxd_filtered_prev rxd_filtered;要点是沿检测必须基于滤波后的信号而不是原始rxd。我见过有人直接对原始rxd做边沿检测然后滤波只用于数据采样结果假起始位照样触发。滤波和沿检测必须是一体的滤波之后的边沿才可信。3.3 起始位确认半位后再验证检测到滤波后下降沿后状态机从IDLE跳到START但这时还不能确认起始位是真的。我的做法是在START状态等8个采样时钟半位时间然后再采一次rxd_filtered。如果这时候已经是高电平说明刚才那个下降沿只是干扰脉冲状态机立刻回IDLE如果仍然是低电平说明rxd确实被拉低了至少半个bit这才认定是有效起始位。这一步非常关键。很多干扰脉冲虽然窄但滤波窗口滤不掉所有情况比如现场有很宽的共模干扰时毛刺宽度可能达到几个微秒。如果没有半位确认这种宽毛刺就会被当成起始位。加了半位确认之后只有持续低电平超过半个bit的下降沿才能触发接收假起始位的概率会下降好几个数量级。START: begin if (baud_x16_tick) begin if (half_cnt SAMPLE_MULT / 2 - 1) half_cnt half_cnt 1; else begin if (rxd_filtered 1b1) begin state IDLE; // 假起始位丢弃 end else begin state DATA; bit_cnt 0; end half_cnt 0; end end end3.4 数据位采样三选二多数表决怎么落地进入DATA状态后每个bit持续SAMPLE_MULT个采样时钟。我在一个bit的第6、7、8个采样时钟分别采样一次得到三个电平然后多数表决。表决逻辑很简单三个里至少两个为1就判该位为1否则为0。always (posedge clk or negedge rst_n) begin if (!rst_n) begin sample_cnt 0; bit_cnt 0; shift_reg 0; end else if (state DATA) begin if (baud_x16_tick) begin if (sample_cnt 6 || sample_cnt 7 || sample_cnt 8) begin sample_1bit[sample_cnt - 6] rxd_filtered; end if (sample_cnt SAMPLE_MULT - 1) begin shift_reg {vote3(sample_1bit), shift_reg[DATA_WIDTH-1:1]}; if (bit_cnt DATA_WIDTH - 1) state STOP; else begin bit_cnt bit_cnt 1; sample_cnt 0; end end else begin sample_cnt sample_cnt 1; end end end end function automatic vote3; input [2:0] samples; begin vote3 (samples[0] samples[1]) | (samples[0] samples[2]) | (samples[1] samples[2]); end endfunction这里有一个很容易犯的错sample_cnt必须和波特率时钟严格对齐不能出现某些位采样点偏移的情况。我在调试时就遇到过前面几个bit正常到后面某个bit开始错位最后发现是sample_cnt在状态切换时没有清零导致采样点越走越偏。所以状态跳转时所有计数器都要有明确的复位或清零动作。3.5 停止位校验与错误恢复别让状态机卡死数据位收完之后进入STOP状态正常停止位应该是高电平。我在停止位的中间位置采样如果采到低电平说明这一帧有问题置位frame_err。但不管停止位对不对状态机都要回IDLE同时把rx_valid拉起来或者根据需求选择错误时不拉valid。这时候上层协议可以根据frame_err判断这一帧要不要丢弃。还有一种极端情况必须处理对端设备故障把txd拉死为低或者强干扰让rxd长时间保持低电平。如果状态机没有超时机制它会一直停在DATA状态里后面的数据全都不收。我的做法是在DATA状态加一个超时计数器如果超过正常一帧所需时间的1.5倍还没收满8个bit强制回IDLE并置frame_err。这个机制在真实现场帮了我大忙否则每次对端设备抽风板子就得重新上电才能恢复。4. 关键参数计算波特率、采样时钟与滤波窗口怎么配4.1 采样时钟频率与分频计算假定系统时钟50MHz目标波特率115200SAMPLE_MULT取16。那么采样时钟频率应该是115200 × 16 1.8432MHz。分频系数为50MHz / 1.8432MHz ≈ 27.126取整为27实际采样时钟频率是50MHz / 27 ≈ 1.85185MHz换算成实际波特率约为1.85185MHz / 16 115740bps与目标115200的误差约0.47%。这个误差在UART容差范围内可以正常工作。但如果偷懒把分频系数取成28实际波特率约111607bps误差约3.12%高于普通UART的±2%容差高波特率下就会随机出错。这里要特别提醒分频系数一定要按整数参数算好不能随便四舍五入。常用配置我整理了一张表直接照着填就行系统时钟目标波特率采样倍数分频系数实际波特率误差50MHz96001632596150.16%50MHz11520016271157400.47%50MHz921600163104166713%100MHz11520016541157400.47%100MHz921600166104166713%921600那一行误差很大原因是分频系数太小取整损失比例太高。如果目标波特率很高要么提高系统时钟要么降低采样倍数到8否则误差会大到无法通信。我一般建议串口波特率在工业现场不超过460800采样倍数不低于16留足抗干扰余量。4.2 滤波窗口宽度怎么定滤波窗口FILTER_CNT是高干扰调试里最值得花时间的参数。它表示“连续多少个采样点同电平才认为信号真正变化”。窗口太短没效果窗口太长会影响正常信号。举个实际计算的例子。系统时钟25MHz波特率115200SAMPLE_MULT16那么采样时钟是1.8432MHz一个采样周期约542纳秒一个bit时间约8.68微秒对应16个采样周期。如果现场毛刺宽度在几十到几百纳秒滤波窗口取2到4个采样周期约1到2微秒足够滤掉大部分毛刺。如果线特别长、干扰特别严重可以把窗口加到6到8个采样周期但千万别超过8否则起始位的低电平持续时间会被滤波吃掉太多半位确认那一步就可能判断失败。我通常的做法是先把FILTER_CNT设成4上板实测。如果还有误触发就往大调如果发现正常帧开始丢失就往小调。反复试几次总能找到一个稳定区间。另外滤波参数最好做成寄存器可配这样现场调试时不用反复重新综合烧写效率高很多。4.3 采样点偏移与波特率容差UART是异步协议收发两端各自用自己的时钟并没有同步机制。所以波特率误差、时钟抖动、干扰引起的边沿偏移都会叠加起来。接收端判断一个bit是否正确的依据是采样点是否落在该bit有效区间内。以115200为例一个bit时间约8.68微秒。假设用中心采样理论上采样点在4.34微秒处。如果发送端和接收端的时钟误差总计1%边沿抖动消耗掉1微秒那实际采样点会偏移到距离真实bit中心几百纳秒的位置仍在有效区间内问题不大。但如果误差到3%再加上干扰造成的沿抖动采样点就可能落到bit边沿附近甚至越过边界这一位就采错了。所以我的经验是接收端自身分频导致的波特率误差最好控制在±1%以内。加上对端误差总误差控制在±3%以内。如果算出来误差接近或超过2%优先提高系统时钟频率或降低目标波特率。也别迷信“误差在容差范围内就行”高干扰场景下余量越大越稳。5. 仿真与板级调试把干扰“造”出来再验证5.1 仿真激励怎么写人为注入毛刺很多人写仿真激励就规规矩矩地按UART时序发送正常数据跑通了就以为完事。这种仿真在高干扰工程里几乎没有价值因为干扰恰恰不在正常时序里。我的做法是专门写一个带干扰注入的testbench在正常数据帧里随机插入毛刺和沿抖动。思路大概是正常发送起始位、数据位、停止位在发送过程中用随机数控制在某个时刻往rxd上叠加一个短脉冲把rxd强制拉低或拉高几个时钟周期再模拟一下边沿抖动把起始沿和停止沿的位置随机偏移几十个纳秒。跑一百帧统计接收正确的帧数。如果误帧率还在可接受范围再拿板上实测。用ModelSim或者Vivado自带的仿真器都能做关键是组合出各种极端情况而不是只跑一遍通过就行。高干扰环境下的验证本质上是在赌“哪种干扰组合会让逻辑崩掉”仿真的价值就是提前把这些组合都试一遍。5.2 实测波形怎么看抓关键信号上板调试时不要只看最终收到的数据对不对要抓FPGA内部的中间信号。我用SignalTap或ILA通常会同时抓以下几组信号rxd原始输入和rxd_filtered滤波后输入对比看滤波是否把毛刺滤干净以及滤波造成的延迟有多大。rxd_negedge看假起始位触发的频率如果这个信号频繁拉高但状态机并没有正确接收说明干扰在起始位确认阶段还有漏网。state监控状态机在哪里停留最久。如果经常卡在DATA很可能是对端信号异常或超时机制没生效。frame_err这个信号一旦频繁拉高说明停止位经常被破坏优先去查外部电路和地线。我调试时还有个习惯把rx_valid配合误帧统计一起看。比如上位机每发100帧FPGA统计收到多少有效帧、多少帧错误再把统计数据通过另一路串口或LED灯状态发出来这样不用反复插拔调试线也能远程判断现场情况。5.3 常见问题速查表现象可能原因排查方向整帧乱码、数据完全不对波特率分频参数错误、采样点偏移过大核对分频系数减小波特率误差只有第一个字节错后面正常假起始位把数据帧起点搞偏加滤波窗口、半位确认偶尔丢一个字节数据位采样点踩到边沿抖动多次采样多数表决一直报frame_err停止位被拉低、对端异常检查停止位采样点加外部隔离状态机卡死、收到几帧后不再响应对端拉低或长时间低电平增加超时强制恢复开变频器/继电器时错码增多共模干扰通过地线耦合外部光耦隔离、屏蔽线处理这张表基本覆盖了我这几年遇到的大部分串口高干扰问题建议收藏。实际调试时先对照表定位方向再针对性处理比盲目改代码高效得多。5.4 我踩过的几个坑第一个坑是分频参数用非整数。早期我图省事直接在代码里写50_000_000 / (115200 * 16)综合工具会给个浮点结果我顺手取整成28结果高波特率下错码率惨不忍睹。后来改成手动算好整数分频值误差立刻降到0.5%以内问题消失。第二个坑是忘记处理空闲时rxd被异常拉低的情况。有次现场设备通信一段时间后整条链路静默排查半天发现是对方单片机上电瞬间txd会拉低几个毫秒正好被FPGA当成起始位然后状态机一路收了个乱七八糟的数据后卡死。后来加上超时机制这个问题再也没出现过。第三个坑是滤波窗口调太大。有次为了滤掉一个特别顽固的毛刺我把FILTER_CNT一路加到10结果发现正常帧也开始丢。原因前面说过滤波窗口超过半位时间正常起始位的下降沿被滤波逻辑“磨平”了沿检测根本触发不了。这个教训让我记住了滤波窗口必须小于半位时间这条铁律。6. 扩展与延伸这套思路还能用在哪些地方6.1 从UART扩展到其他异步协议滤波、起始位确认、多次采样、错误恢复这套组合拳其实不止能用在UART上。只要是从一根线上异步接收数据的协议比如LIN总线、DMX512灯光控制、红外遥控接收、单线CAN都会面临类似的干扰问题。我之前做红外遥控接收时直接把UART的滤波逻辑搬过去稍微改一下载波周期参数效果立竿见影。所以建议把这套代码保留成通用模块别只当UART专用。6.2 配合FPGA其他资源协同工作接收端做好之后别忘了和FPGA内部的其他资源搭配起来。最常用的组合是接收模块后面接一个FIFO把rx_valid写入FIFO再由上层逻辑按自己的节奏读取这样即使瞬时数据量很大也不会因为后端处理不过来而丢字节。另外如果上位机发送频率不稳定可以在FPGA里做一个超时判断超过一定时间没收到新数据就认为通信结束方便上层协议处理。6.3 后续还可以做的改进这套接收逻辑还有几个可以继续优化的方向。比如自动波特率检测上电后先让对方发送一个特定字符比如0x55FPGA测量起始位到第一个上升沿的时间反推出波特率并自动配置分频系数省去手动设置参数的麻烦。再比如增加CRC校验在数据帧尾部加校验字节进一步提高噪声环境下的可靠性。不过这些都属于锦上添花先把滤波、采样、错误恢复这套基本功做扎实才是高干扰串口接收的正确打开方式。最后再分享一个我自己的经验高干扰串口接收关键不是代码写得有多聪明而是能不能在噪声面前保持“冷静”。滤波、半位确认、多次采样、超时恢复每一条都是在和干扰博弈之后总结出来的。调试时一定要带着示波器去现场看波形别只在仿真里自嗨。拿示波器探头夹在rxd线上亲眼看看毛刺长什么样、宽度多少、频率多高再回来调FILTER_CNT和采样点位置比盲调代码高效十倍。工程里“能通”和“稳”是两回事实验室里调通只是起点能扛住现场电磁环境才算真正做完。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →