做测控的兄弟对FPGA应该都不陌生,我这些年经手的测控程序,十个里有八个最终都落在FPGA上,原因其实很朴素:测控最看重实时性,而FPGA的核心优势是并行执行、延迟可控。一提到FPGA里的测控程序,大家关心的其实就三件事:框架怎么搭、模块怎么划、数据流怎么走。这三件事处理好了,一个复杂系统就能拆成小块的逻辑,一层一层拼起来;处理不好,后面全是拼命的时序和调式,改一版坏一版。这篇文章我就按自己的实际项目习惯,从整体框架讲起,再拆模块,再重点聊数据流,最后给一个能直接参考的工程样本和一组排查问题的经验,希望能帮你少走点弯路。
1. 测控程序整体框架:先画好骨架再动手
1.1 测控程序到底在解决什么问题
测控程序本质上是把一个物理过程的“测量”和“控制”闭环起来。举几个最常见的场景:工业现场的温度、压力采集,伺服电机的电流环和速度环,旋转编码器的位置读取,以及实验室里的信号发生器、数据采集卡。这些场景有个共同点:不仅要持续拿到数据,还要在一定时间内做出控制和响应。偏差一点点,系统性能就差很多,甚至直接故障。
FPGA为什么适合干这个?因为它是硬件电路在跑,不是软件任务在排队。ARM或者单片机会遇到中断嵌套、任务被抢占的问题,一个优先级高的任务进来,其他任务只能等。而FPGA里每个模块都是独立电路,采样模块在处理当前数据的同时,PWM模块可以照常输出,通信模块也可以照常回传数据。换句话说,测控里的“同时性”在FPGA上是天然满足的,前提是你的框架没有把它破坏掉。
所以设计测控程序,第一步不是写代码,是画框图。你要先想清楚整个系统有几个输入、几个输出,哪些数据是必须闭环走的,哪些数据只需要旁路回传。我习惯画一张A3纸的框图,把所有模块用箭头连起来,标清楚每个箭头的位宽和数据率。框架清楚了,后面写代码就是填空。
1.2 顶层框架:采集、处理、输出、通信四步走
我通常把FPGA里的测控程序分成四个功能区:采集区、处理区、控制区、通信区。看起来简单,但整个系统的复杂度基本就由这四个区之间的连接关系决定。
采集区解决的是“怎么把外部物理量变成FPGA内部的数字信号”。常见接口包括SPI接口的ADC、并行的CMOS图像传感器、LVDS高速ADC、BISS-C编码器等。处理区负责对采样数据进行滤波、标定、单位换算、特征提取,还会跑PID、卡尔曼滤波这类控制算法。控制区把处理出来的指令变成真实的执行信号,比如PWM占空比、DA输出电压、数字开关量。通信区负责和上位机或者MCU打交道,常用UART、SPI、FMC并行总线、以太网或PCIe。
在实际项目里,这四个区不一定是物理上独立的部分,但逻辑上一定要分清楚。我把这个框架简化成一个口诀:一收、一算、一控、一传。收指的是采集,算是处理,控是输出,传是通信。核心数据流永远是“收 -> 算 -> 控”,通信只是旁路,负责监控和参数下发,不要让它插在控制主环路的中间,否则一旦通信拥塞,整个控制周期都会受到牵连。
1.3 为什么模块化在FPGA测控里更重要
软件工程的模块化是为了代码复用和维护,FPGA的模块化目的更直接:让每一个功能块都能独立仿真、独立验证、独立替换。一个测控系统往往会经历多次硬件改版,比如换一颗更快的ADC芯片,或者把电机驱动器从普通的H桥换成带使能控制的智能驱动,这时候如果采集逻辑和控制逻辑混在一个巨大的always块里,改动一个参数就可能影响另一条路径的时序。
硬件模块化要“高内聚、低耦合”。内聚的意思是每个模块只干一件事,比如UART接收模块就只负责把串行数据变成并行字节,解析帧格式的事情放到业务层去做。耦合的意思是模块之间只通过明确的接口通信,最常用的就是时钟、复位、valid、ready和data。现在很多工程采用AXI-Stream协议做模块互联,valid和ready两个信号就解决了上游和下游的握手机制,非常方便。
我遇到过很多半路接手的老项目,最大的问题不是代码难懂,而是没有边界。顶层的always块里有ADC时钟生成、FIFO读写、PID计算、PWM比较,甚至UART的波特率分频也写在里面。这种设计在仿真时可能没问题,一旦约束紧张,时序收敛很难做,出问题也不知道从哪里查。模块化不是形式主义,是给后续调试留一条活路。
2. 核心模块拆解:每个功能块的接口与实现思路
2.1 采集前端:ADC、LVDS和BISS-C这些接口怎么接
采集前端是所有控制闭环的数据源头,这个模块一旦出错,后面再好的滤波和控制算法都是白搭。先说最常见的SPI接口ADC。现在很多ADC芯片用4线SPI,sclk是时钟,cs_n是片选,mosi是配置输入,miso是转换结果输出。FPGA侧就是写一个状态机,产生片选和时钟脉冲,在正确的边沿把转换结果移位进来。这里有个常见的坑:很多ADC要求数据在sclk下降沿输出,在sclk上升沿稳定采样。如果你对相位理解错了,读回来的数据可能会整体错位,表现就是数值像噪声一样跳变。
如果测控系统用的是高速ADC,比如125MSPS以上的采样率,那么SPI通常就不够用了,必须用LVDS接口。LVDS是差分信号,抗干扰能力强,数据速率可以到几百Mbps。FPGA内部要把串行的LVDS比特流转成并行数据,这就要用到ISERDES或者Gemini接口这类高速串并转换原语。同时输入时钟也要从LVDS时钟接收器进来,然后通过MMCM/PLL生成采样时钟。这一步的时序约束特别重要,走线长度、idelay参数、比特顺序都影响最后的数据对齐。我常用的硬提示是,宁可先花一小时查LVDS datasheet的时序图,也不要上板后拿ILA瞎猜。
另一种典型接口是BISS-C,主要用在伺服电机编码器上。它是一条双向的串行同步总线,FPGA做主站,通过MA线发时钟,通过SLO线读数据。BISS-C协议里包含位置信息、状态位、CRC校验,还能支持寄存器读写。FPGA实现BISS-C主站的关键是精确控制MA时钟频率,并按时序要求切换数据方向。一个常见问题:如果编码器线缆比较长,返回的数据边沿变差,就需要在接收端加打拍延迟或调整采样点。我的经验是先跑一个“读寄存器”的命令做链路自检,确认CRC无误后再进位置闭环。
采集模块的接口设计也值得多说一句。我给采集模块定的标准输出一般是这样的:
module adc_ltc2324_if #( parameter WIDTH = 16 )( input logic clk_adc, // 来自MMCM的采样时钟 input logic rst_n, input logic [3:0] adc_ser, // LVDS串行数据输入 output logic [WIDTH-1:0] sample_data, output logic sample_valid // 单周期有效脉冲 );sample_valid半年是关键,后面的处理模块只需要采样sample_data和sample_valid,不需要关心ADC是哪颗芯片、是SPI还是LVDS。这样封装的好处很明显,以后换ADC芯片,只改这个模块。
2.2 信号处理链:从简单滤波到卡尔曼滤波落FPGA
原始采样数据基本都不能直接用,必须先滤波。最简单的是滑动平均滤波,原理就是保存最近N个采样点,去掉最旧的一个、加入最新一个,这样一直保持一个累加和。FPGA里可以用一个移位寄存器加一个累加器实现,但是如果N比较大,累加和需要较宽的位宽,否则会溢出。我的做法是定义一个满量程的位宽,做饱和处理,然后用移位代替除法,或者把除法放到后面模块处理。
如果对频率响应有要求,就要用FIR滤波器。FPGA里的FIR最常用的是乘累加结构,比如一个32阶的FIR,每出一个点要32次乘加。如果采样率是几千赫兹,时钟是100MHz,那完全可以用一个乘法器分时复用跑32个周期;如果采样率到了几十MHz,可能就要用多个乘法器并行处理。工程上选择直接使用Xilinx或Intel自带的FIR Compiler,参数可以先用MATLAB的FDATool算好,再导出系数。
再说卡尔曼滤波。很多人一听卡尔曼滤波就头大,实际上在FPGA里实现并不复杂,它就是一个预测加更新的两步循环。典型应用是传感器融合,比如编码器测位置、陀螺仪测角速度,两个数据源都有噪声,卡尔曼滤波可以估计出比单一传感器更稳的状态。FPGA实现卡尔曼滤波时,矩阵运算和浮点运算是主要挑战。如果状态变量只有两三个,完全可以用状态机写定点运算,控制周期做到几十微秒没问题。如果状态维度偏高,建议先用MATLAB把浮点模型仿真好,再把参数转成定点Q格式,最后在FPGA里实现。
卡尔曼滤波模块的接口也可以统一成下面的样子:
module kalman_filter #( parameter Q0 = 16'h0100, parameter R0 = 16'h1000 )( input logic clk, input logic rst_n, input logic [15:0] measure, input logic measure_valid, output logic [15:0] state_out, output logic state_valid );这种接口的好处是滤波模块不关心数据从哪来,只要输入有效就更新一次。我在实际项目中通常会在卡尔曼滤波前加一个简单的限幅判断,把明显异常的数据先剔除,否则一个坏采样会把整个估计状态带偏。
2.3 控制输出端:PWM、DA和电机驱动模块的衔接
采集和处理之后,控制器会输出一个控制量,它得变成执行机构看得懂的信号。最常用的输出是PWM。FPGA里产生PWM只需要一个自由运行的计数器和比较器,计数器从0数到周期值,当计数值小于占空比比较值时输出高电平,否则输出低电平。听起来简单,但细节很多:电机驱动不能直接让PWM输出乱跳,不然会产生大量毛刺和EMI,因此输出级一定要用寄存器打一拍,或者用ODDR原语直接输出到引脚,避免组合逻辑出现毛刺。
PWM和电机驱动模块之间还涉及方向和使能信号。比如TB6612这种电机驱动模块,它需要IN1、IN2和PWM三个信号来控制正转、反转和速度。FPGA侧只需要提供一个方向信号和一个占空比信号,再由一小段逻辑映射成IN1、IN2和PWM的组合。我写PWM模块的时候,一般会把最小占空比和最大占空比做限制,防止输出为0或100%时驱动器状态异常。还要注意死区时间,特别是桥式驱动,上下桥臂不能同时导通,否则会烧器件。
DA模块也是一类重要输出。比如AD9833是一个通过SPI接口配置的波形发生器,FPGA要发送频率和相位寄存器来产生正弦波、三角波等信号。如果你的测控系统需要一个模拟激励源,这类DDS芯片很常用。FPGA侧就是一个SPI主机,发送寄存器配置值就可以。关键点是AD9833的寄存器写入顺序和写保护位,不看datasheet贸然写的话,输出频率经常不对。
PWM模块的接口我一般这样设计:
module pwm_gen #( parameter COUNTER_WIDTH = 16 )( input logic clk, input logic rst_n, input logic [COUNTER_WIDTH-1:0] duty, output logic pwm_out );内部就是一个计数器,duty作为比较阈值。但我在比较之前一定会先做限幅,比如限制在0和周期值-1之间。这个操作看起来多余,实际能避免很多边界条件导致输出一直拉高或拉低的问题。
2.4 通信回传:UART、SPI、FMC与PCIe的取舍
测控系统再强大,如果上位机拿不到数据、下不了参数,那也是白搭。通信模块的选择取决于数据量、实时性、开发周期和硬件接口条件。
UART适合低速率监控,比如上报设备状态、接收参数配置。波特率一般115200或者460800,简单可靠,但带宽很低,不能承载高速连续波形。SPI适合PCB板上短距离高速通信,很多FPGA和MCU之间就通过SPI交换数据。FMC是另一种选择,比如STM32H743通过FMC接口和FPGA通信时,FPGA这边其实可以把自己伪装成一个SRAM或者NOR Flash器件。MCU像读写普通存储器一样读写FPGA的地址空间,FPGA在总线侧解码地址与读写信号,完成数据交换。这个方案并行度很高,一条总线可以映射多个寄存器,很适合做高速数据交互。
如果上位机是PC,要用FPGA传大量波形数据,那PCIe几乎是绕不开的。PCIe接口的FPGA实现通常用Xilinx的XDMA IP或者Intel的PCIe IP,FPGA作为Endpoint,上电后主机就可以枚举到设备。PCIe的好处是带宽大、延迟低、生态成熟,但工程复杂度也高,需要处理MSI中断、DMA描述符、BAR空间等。很多项目会用“FPGA + PCIe + DMA”的组合来传高速AD数据或图像数据。
通信方式选择还有一个原则:协议解析和业务逻辑分离。UART接收到一帧数据,接收模块只负责把字节收进来,用valid信号通知业务层;业务层再解析帧头、命令、CRC。这样通信波特率变了或者协议升级了,只会改协议解析部分,不会动到核心控制环路。下面是常用接口的对比表:
| 接口 | 典型带宽 | 开发复杂度 | 适用场景 |
|---|---|---|---|
| UART | 1Mbps以内 | 低 | 状态监控、参数配置 |
| SPI | 10Mbps左右 | 低 | 板级芯片间通信 |
| FMC并行 | 100MB/s以上 | 中 | FPGA与MCU高速互操作 |
| PCIe Gen2 x2 | 约1GB/s | 高 | 上位机连续采数据 |
| 以太网 | 10M/100M/1G | 中高 | 分布式测控系统 |
3. 数据流设计:决定系统实时性的命脉
3.1 单条主数据流:采集->处理->输出->上报
框架和模块都定了,接下来最关键的就是把数据流画清楚。我常用的主数据流是:传感器 -> 采集模块 -> FIFO -> 滤波/处理模块 -> PID/控制模块 -> 输出模块,同时处理结果和状态通过通信模块上报上位机。这条主链路上,每级模块都只关心自己的输入输出,几乎是流水线式的推进。
举个例子,一个电机电流闭环系统。ADC以50kHz采样率采集电流,每20us出一次采样数据。如果FPGA系统时钟100MHz,那么每个采样周期内有2000个时钟周期,足够完成滤波、PID计算和PWM更新。延迟分配可能是:ADC接口模块需要20个周期,FIR滤波需要200个周期,PID计算需要100个周期,PWM比较器1个周期,合计不超过1000个周期。也就是说在下一个采样点到来之前,控制量已经更新完了。这个“延迟预算”在项目初期就要算清楚,不然到了后期发现延迟太大,要么提高时钟,要么重新调整流水线。
数据流图虽然好画,但实际数据流里不止一条路。除了主环路上的数据,还有参数配置数据、状态上报数据、异常报警数据。我建议把所有旁路数据都别放在主环路上,而是通过寄存器接口由通信模块写入。比如PID参数Kp、Ki、Kd,上位机通过UART接收到之后,只是更新一组寄存器,PID模块在下一个控制周期读到新参数,这样控制系统不会被通信拖慢。
3.2 跨时钟域设计:握手、FIFO和乒乓缓存
测控系统里存在多个时钟域几乎是必然的。ADC有采样时钟,FPGA有系统时钟,通信模块又可能是以太网时钟或PCIe参考时钟。数据从一个时钟域进入另一个时钟域,最危险的是亚稳态和多bit数据不一致。我见过不少工程,功能仿真一切正常,上板之后数据偶尔跳变,最后查出来就是跨时钟域没处理好。
单bit信号的跨时钟域处理常用两级同步器,也就是用两个寄存器连续打两拍。多bit数据跨时钟域不能用简单打拍,因为多个bit可能在不同时刻变化,采样时可能采到半新半旧的数据。这种情况用异步FIFO最稳。现代FPGA都自带FIFO IP,可以配置读时钟和写时钟不同,而且能输出空满 flag。开发时只需要把写端数据、写时钟、写使能接上去,读端数据通过读时钟读出来,空满作为反压信号。
乒乓缓存也是很实用的结构,尤其在图像采集和连续波形采集中。假设采集模块正在往A缓存写数据,处理模块在同时读B缓存,等A写满之后交换角色,处理模块读A,采集模块写B。这样采集和处理并行进行,不用等对方完成。代价是双倍存储。
跨时钟域设计里最容易忽略的是FIFO溢出。如果读速率低于写速率且没有反压机制,FIFO满之后继续写就会丢数据。我的习惯是在每个FIFO的写端都检查full信号,一旦满就通过valid信号暂停上游模块的推进。必要的时候还要在数据里加帧号或序列号,上位机如果发现序列号不连续,就知道丢了数据。
3.3 数据流中的时序与资源平衡
数据流的宽度和数据率决定了系统资源需求。举个例子,一个8通道的数据采集系统,每通道采样率1MSPS,每通道16bit,总数据率就是8 * 1M * 16bps = 128Mbps。如果FPGA系统时钟是100MHz,数据总线32bit,那么每拍能传32bit,照理说吞吐完全够。但是在实际设计里,不能只看平均吞吐,还要看瞬时峰值和模块的并行度。
假设处理模块只有一个FIR滤波器,滤波器的计算能力是每32拍输出一个结果,也就是吞吐率大约100MHz/32 = 3.125Msps。如果输入是8个通道,每个1Msps,峰值时候有8个数据等一个滤波器,这个滤波器会塞车。两种方案:一是把时钟提高到400MHz,用更高的计算密度跑同一个滤波器;二是复制8份滤波器,每个通道独立处理。前者省逻辑,但时序压力大;后者逻辑资源翻倍,时序和延迟更好。没有绝对优劣,只能根据目标FPGA的资源余量和控制周期要求来定。
数据流的握手协议也很重要。我建议在所有处理模块之间采用valid/ready接口。上游在valid拉高时表示数据有效,下游在ready拉高时表示可以接收,只有当两个信号同时为高,这一拍才完成传输。这种机制天然处理了背压,下游慢的时候上游会等待。虽然增加了一点点逻辑,但让模块之间解耦了,调试时看着波形更容易定位卡点。
4. 工程实现参考:一个简化测控链路示例
4.1 代码结构设计
前面讲了很多框架和数据流,这部分我给出一个简化但完整的工程参考。项目目标:8路SPI ADC采集,16bit分辨率,1kSa/s采样率;每路数据经过滑动平均滤波后做PID闭环;输出PWM控制一路小型直流电机;同时通过UART上报数据和接收PID参数。这个系统不大,但麻雀虽小五脏俱全。
代码目录我一般这样组织:
rtl/ top.sv adc_spi.sv async_fifo_wrapper.sv moving_avg.sv pid_ctrl.sv pwm_gen.sv uart_tx.sv uart_rx.sv sim/ tb_top.sv tb_adc.sv文件命名和模块名保持一致,这个习惯看起来不起眼,实际上对多人协作特别重要。顶层只做例化,不做具体逻辑,这样整个系统的结构一眼能看明白。每个模块内部还要保留一个寄存器配置端口,方便调试时手动控制内部状态。
4.2 顶层模块与接口示例
顶层接口包括系统时钟复位、ADC SPI接口、PWM输出、UART接口。代码比完整产品简单,但已经能看出框架的轮廓:
module top #( parameter ADC_WIDTH = 16, parameter N_CH = 8 )( input logic clk_50m, input logic rst_n, output logic adc_sclk, output logic adc_mosi, input logic adc_miso, output logic [N_CH-1:0] adc_cs_n, output logic motor_pwm, output logic motor_dir, output logic uart_tx, input logic uart_rx ); logic [ADC_WIDTH-1:0] adc_data; logic adc_valid; logic [ADC_WIDTH-1:0] filtered_data; logic filtered_valid; logic [ADC_WIDTH-1:0] pid_duty; logic pid_valid; adc_spi #( .DATA_WIDTH(ADC_WIDTH) ) u_adc_spi ( .clk (clk_50m), .rst_n (rst_n), .adc_sclk(adc_sclk), .adc_mosi(adc_mosi), .adc_miso(adc_miso), .adc_cs_n(adc_cs_n), .data (adc_data), .valid (adc_valid) ); moving_avg #( .DATA_WIDTH(ADC_WIDTH), .AVG_N (16) ) u_moving_avg ( .clk (clk_50m), .rst_n (rst_n), .din (adc_data), .din_valid (adc_valid), .dout (filtered_data), .dout_valid(filtered_valid) ); pid_ctrl #( .DATA_WIDTH(ADC_WIDTH) ) u_pid_ctrl ( .clk (clk_50m), .rst_n (rst_n), .setpoint (16'h8000), // 目标值 .feedback (filtered_data), .fb_valid (filtered_valid), .output_val (pid_duty), .output_valid(pid_valid) ); pwm_gen u_pwm_gen ( .clk (clk_50m), .rst_n (rst_n), .duty (pid_duty[15:1]), .pwm_out(motor_pwm) ); endmodule这个顶层其实还缺UART上报,这里为了篇幅没有全部例化。实际上UART模块和PID模块一样,只需要把自己的数据接到一个发送FIFO或者状态寄存器上就能独立工作。
4.3 关键内部模块实现思路
PID控制器的FPGA实现可以很规整。输入是设定值和反馈值,误差e = setpoint - feedback。比例项直接乘Kp,积分项累加后乘Ki,微分项用最近两次误差差乘Kd。三个项相加后限幅输出。工程上必须处理积分饱和,也就是只要输出已经到限幅值,就不让积分继续累加,否则会出现系统超调后很久都拉不回来。
我用Verilog表示控制核心时通常这样写:
// 伪代码,实际需要补充位宽和时钟控制 if (pid_valid) begin err <= setpoint - feedback; if (pid_saturated) begin integ <= integ; end else begin integ <= integ + err; end output_val <= Kp * err + Ki * integ + Kd * (err - err_prev); err_prev <= err; end实际工程里Kp、Ki、Kd都是可配置寄存器,而且乘法结果要按Q格式截位,避免中间位宽爆炸。截位要注意采用饱和而不是简单的丢弃高位,否则控制量在边界上会突然跳变。
滑动平均模块相比之下简单很多。维护一个累加器,每个新数据到来时,从累加器中减去最老的数据,再加上新数据,得到新的窗口和。最老数据存在一个移位寄存器里,深度等于平均窗口。这个方法资源消耗很低,适合做ADC预处理。但要注意,如果窗口是2的幂次,除法直接右移;如果不是,就要用除法器或者干脆把结果往后传,由下游决定怎么缩放。
4.4 FPGA选型与资源评估
选型时不要只盯着FPGA厂商或者型号参数,先算算资源账。刚才那个例子,滑动平均加PID占用的资源非常少,一个普通Artix-7或者Cyclone 10就绰绰有余。但如果整个系统换成8路128MHz LVDS ADC,且每路都要跑32阶FIR,那需要的DSP和Block RAM会明显上升。32阶FIR每路大约需要16个DSP乘法器,8路就是128个DSP,这就直接排除了很多小芯片。
还要考虑Bank电压和IO标准。ADC的LVDS接口一般需要1.8V或2.5V的Bank电压,如果FPGA对应Bank的VCCO接错了,采样完全无法工作。BISS-C或RS485这类接口,通常要外接收发器芯片,FPGA侧走LVCMOS或LVDS即可,PHY芯片再转换。PCIe则需要选择带高速串行收发器的FPGA,同时供电和参考时钟都要仔细规划。我习惯在选型表里列一栏“保守资源占用”,LUT、FF、BRAM、DSP使用率都控制在70%以内,给布局布线留出余量,否则时序收敛很难受。
5. 调试方法与常见坑实录
5.1 上板前的仿真要点
测控程序千万不能一上来就上板。我先做模块级仿真,再做系统级仿真。模块级仿真里,ADC模型要能模拟真实的接口时序,比如每1000个时钟周期给一次采样结果。PID模块需要给一个阶跃输入,观察输出是否收敛到设定值、有没有超调。滑动平均模块则要检查窗口边界,前几个点输出是否正确补齐。系统级仿真会把所有模块例化在一起,用虚拟的ADC数据源来替换真实ADC,检查整条链路的valid信号是否始终推进,会不会出现某一步长期等待。
跨时钟域验证尤其重要。在testbench里要制造两个独立的时钟,一个给ADC域,一个给系统域,让它们频率和相位都不固定,模拟真实FIFO行为。我还会在关键valid信号上写SystemVerilog断言,比如检查FIFO满时写使能不能拉高,PID输出永远在限幅范围内。仿真跑到十万周期无错误,再考虑上板。
5.2 实测中容易踩的坑
上板之后,最常见的问题我一个个说。
第一个坑是ADC读回来的数据全0或者全1。大概率不是ADC坏了,而是SPI模式或者采样边沿不对。很多ADC在datasheet里写明数据在sclk下降沿更新,在上升沿采样,你反着做就会导致数据移位。还有可能是片选信号低有效时间不够,ADC还没来得及完成转换就被你读走了。这时候用逻辑分析仪看SPI四根线,逗号对比datasheet时序图,一眼能看出哪一拍有问题。
第二个坑是数据偶发跳变。这种问题最容易让人抓狂。现象是大部分数据正常,偶尔跳一个很大的值。我见过三类原因:一是跨时钟FIFO溢出,数据被丢掉,下游把旧数据当新数据用;二是ADC的采样时钟不够干净,导致偶尔采到亚稳态;三是有控制逻辑内部跨时钟域的多bit信号没有同步,读到了半新半旧的数据。排查顺序一般先看FIFO的空满信号是否频繁拉起,再看ADC采样时钟波形,最后查跨时钟域路径。
第三个坑是PWM输出毛刺。PWM比较器的输出如果直接从组合逻辑引出,很容易在计数边界产生毛刺。解决办法就是在输出端用寄存器打一拍,或者用ODDR原语把信号送到IOB。另一个容易被忽视的是PWM抖动,出现在duty值来自跨时钟域时没有经过同步。duty偶尔变化一两个Lsb,电机就能听得出来。
第四个坑是闭环发散。控制环一旦发散,第一反应是检查PID符号。反馈接反了是市面上最常见的低级错误。其次检查控制周期是否太慢,如果每隔很长时间才更新一次PWM,系统会产生大延迟,也会振荡。最后看积分限幅和输出限幅,给得太狠会造成积分饱和,导致超调和振荡。
第五个坑是UART乱码。一般就是波特率分频没算对。板子上的晶振不一定是准确的50MHz,可能实际是49.152MHz或者其他频率,按50M去分频就会产生累积误差。正确的做法是根据实际时钟频率重新计算分频值,配置串口助手按同一波特率。
5.3 问题排查速查表
把上面这些经验整理成一张速查表,方便现场排查:
| 现象 | 可能原因 | 排查方法 | 解决方向 |
|---|---|---|---|
| ADC读回全0/全1 | SPI模式错误、采样边沿错 | 逻辑分析仪对比datasheet时序 | 调整时钟极性和相位 |
| 数据偶发跳变 | 跨时钟域未同步、FIFO溢出 | 查空满flag和跨时钟路径 | 加FIFO、加同步器、加反压 |
| PWM输出毛刺 | 组合逻辑输出、无输出寄存器 | 看输出引脚波形 | 输出打拍或ODDR |
| 闭环振荡发散 | PID符号错、积分饱和、控制周期太慢 | 检查符号、延迟预算 | 修正方向、加限幅、缩小延迟 |
| UART乱码 | 波特率分频错误、时钟源不匹配 | 数波形时间宽度 | 按实际时钟重新计算分频 |
排查问题我有一条原则:一次只改一个变量。如果同时改了滤波器参数、PID参数和数据总线位宽,出了问题根本没法定位。上板调试时先分模块打点,比如先确认ADC模块输出波形正确,再确认FIFO读写计数一致,最后再让PID闭环。用ILA或者信号分析仪把valid、data、full、empty这些信号抓出来,很多问题都能直接看出来。
调试和仿真之间也不要互相矛盾。如果仿真通过但上板失败,优先怀疑异步问题、引脚约束问题和复位释放问题,而不是盲目改逻辑。仿真环境里时钟是理想化的,真实电路里存在时钟偏斜、时钟抖动、复位毛刺,这些在约束里都要合理处理。
做FPGA测控这些年,我的一个习惯是:无论项目多急,先把数据流画出来,再把模块接口定死,最后才写逻辑。框架乱了,后面所有时序问题都会来报复你。前面提到的模块拆分和跨时钟域方法,都是我用真金白银换回来的经验,希望这次梳理能帮你在设计阶段就避开大部分坑。如果你也在做类似的测控项目,欢迎把数据流画出来一起聊聊。