1. 项目背景与整体设计思路
1.1 为什么用 FPGA 做测控
做测控系统的人,迟早会碰到 FPGA。以前我用单片机做采集和控制的时候,最头疼的问题就是实时性——多路 ADC 采样、传感器解析、波形输出、上位机通信,这些任务全挤在一个 CPU 上,中断嵌套写起来心惊胆战,时序一旦紧张起来,整个系统就不稳定。后来项目中引入 FPGA,很多问题迎刃而解。
FPGA 在测控领域最核心的优势就两个字:并行。所有逻辑模块在硬件层面同时运行,不抢占、不排队,每个数据通路都有自己独立的硬件资源。拿一个典型的多通道同步采集系统来说,四路 ADC 同时采样、同时做滤波、同时打包上传,在单片机里你得用一个循环轮流处理,在 FPGA 里就是四个一模一样的模块挂在那,各干各的活。而且 FPGA 的时序是确定性的,每个时钟沿做什么事、节点在哪个周期产生,都是设计阶段定死的,不会出现软件里那种"这次跑得快、下次跑得慢"的抖动。
标题里提到的"测控程序",在 FPGA 语境下其实不是说跑在软核上的 C 代码,而是指整个硬件逻辑工程——RTL 代码、IP 核、约束文件加在一起,构成一个完整的测控系统。不管你是用 Verilog 还是 VHDL 写的,最终都要落成一套可综合的数字电路,它干的事情是:从外界传感器读数据、做处理、然后输出控制信号或者把结果交给上位机。
1.2 这套程序的设计目标与适用场景
我这次要讲的 FPGA 测控程序,目标定位在中小规模测控系统上,典型的应用场景包括:
- 多通道模拟量采集与波形记录(16 通道以内、采样率 1MSPS 左右)
- 正交编码器计数、PWM 波生成(伺服/步进电机控制)
- 与 STM32 或其他主控芯片做 FMC 通信(高速数据交换)
- 基于 LVDS 接口的数据收发(高速 ADC/SAR 或远程数据传输)
- 显示、按键、LED 等简单人机交互
这套程序适合谁参考?我觉得有三类人:一是刚入门 FPGA、想做一个完整工程的开发者,二是用 MCU 做测控但觉得实时性不够、想往 FPGA 上迁移的嵌入式工程师,三是已经有 FPGA 基础、但没系统梳理过测控程序框架的人。文章里不会只丢一堆代码,我会把"为什么这么拆模块""数据为什么这么流"这些设计思考都讲透。
2. 整体框架设计:模块划分与数据通路构建
2.1 先搭骨架:自顶向下的模块划分原则
一个 FPGA 测控程序,最忌讳的就是把所有代码堆在一个顶层文件里。我见过有人用 3000 行 Verilog 写一个采集控制程序,功能都能跑,但改个通道数要翻半天代码,出个 bug 排查三天,这种工程基本没有可维护性可言。
我的习惯是自顶向下拆模块,遵循三个原则:
第一,按功能域分块,不按功能点分块。一个"采集"功能域里可能有 SPI 接口、FIFO 缓冲、数据对齐逻辑,它们在功能上紧密耦合,归到一个模块文件里;"控制输出"功能域里有 PWM 生成、脉冲计数、方向控制,也单独放。这样文件之间接口清晰,单个文件内部逻辑聚合度高。
第二,接口简单明确,尽量少用全局信号。模块之间的握手信号就两套:一是 valid/ready 握手(源端拉高 valid 表示数据有效,目的端拉高 ready 表示可以接收),二是简单的请求/应答电平信号。接口越简单,联调越容易,也越容易做模块级仿真验证。
第三,跨时钟域的信号,必须经过专门的处理模块,绝不能直接连。这一点我有一个血的教训,后面在排查章节再细讲。
拆完模块之后,整个测控程序的顶层结构长这样:
顶层测控核心 ├── 采集子域(ADC 接口 + 采样控制 + FIFO 缓冲) ├── 处理子域(均值滤波 / 触发逻辑 / 数据打包) ├── 控制输出子域(PWM 生成 / 编码器计数 / 逻辑输出) ├── 通信子域(FMC 从接口 / UART / LVDS 收发) ├── 同步与时基子域(PLL 时钟生成 / 基准时间戳) └── 人机交互子域(按键消抖 / 数码管 / LED)2.2 数据流的第一层设计:采集到缓存的路径
先聊数据是怎么从物理世界流进 FPGA 的。大多数测控系统用的外部 ADC 都是 SPI 接口或并行接口,SPI 常见的有 AD7606(8 通道同步采样,并行/串行接口都有)、ADS8688(8 通道 16 位),并行接口的多用于高速场景,比如 AD9226。
ADC 接口模块的任务就两个:产生正确的时序去读数据,以及把读到的数据整理成统一格式。以 SPI 模式 AD7606 为例,配置字节要按帧格式发送,每次转换完 BUSY 引脚拉高一段时间,然后 SCLK 一次移出 16 位数据。这个模块内部用一个状态机控制:等待 BUSY 上升沿→拉高 CS→计数 16 个 SCLK→锁存数据→拉低 CS→回到等待。过程中最需要注意的就是时序参数匹配——SCLK 频率不能超过 ADC 手册规定的上限,CS 拉低到第一个 SCLK 上升沿之间要满足 t2 时间,这些都要严格看手册并写进约束文件。
数据采进来之后,不能直接往处理逻辑里扔。因为采集连续不断地来,而处理或通信是间歇工作的,中间必须加缓冲。我用的最多的是 FPGA 内部的 FIFO——Xilinx 的 FIFO Generator IP 或 Intel 的 in-System Sources and Probes 里的 FIFO IP 都可以,配成"标准读模式、同步时钟、数据计数输出"即可。FIFO 深度根据突发长度来定,通常 1024×16bit 就够大多数测控场景用了。
这里有个设计细节容易忽略:FIFO 的读侧如果挂在通信接口上(比如 FMC 总线),那它的读时钟和读使能必须由被动侧控制,不能让处理器频繁地轮询"FIFO 还有多少数据",那样效率太低。推荐的方案是让 FIFO 输出一个可屏蔽中断信号给处理器,处理器在中断里批量读取,读完后清标志。这个思路在后面 FMC 通信章节会展开。
2.3 数据流的第二层设计:处理与回环控制
数据从 FIFO 出来之后进入处理子域。处理子域做的活依项目而定,但最常见的是三样:
滤掉高频噪声。最简单的是滑动平均滤波器,在 FPGA 里实现就是移位寄存器+加法树,用一个 N 个寄存器构成的流水线,每个时钟周期把新采样值推入、把最老的丢掉,然后求和取平均。N 取 4、8、16 都行,N 越大滤波越平滑但滞后越大,针对 50Hz 工频干扰的场景,我会先采一段数据跑 Matlab,看频谱分布再定 N 的值。如果需求是去除特定频率的噪声,就得用 FIR/IIR 滤波器,Xilinx 有 FIR Compiler IP,直接把系数文件导进去就行。
触发逻辑实现数据捕获。很多测控场景不关心稳态数据,只关心某个条件满足前后的波形,比如在电机启动瞬间捕捉电流波形。我在 FPGA 里做触发逻辑的思路是:设一个阈值比较器,输入数据与预设阈值比较,超过阈值后立即产生触发标志,同时把 FIFO 切换成"预触发深度+触发后深度"的捕获模式,这样最终保存的数据里既有触发前的历史,也有触发后的过程,非常适合故障分析。
闭环控制回环。如果是控制类应用——比如电机转速闭环——那处理子域里还会有一个 PID 计算模块。FPGA 里做 PID 的好处是控制周期可以做到微秒级,MCU 上你跑 10kHz 的控制中断已经很吃力,FPGA 里 1MHz 的电流环都不是问题。PID 模块的输入是误差信号,输出是 PWM 占空比,中间是三路乘加运算,全部定点化处理,注意积分项要加饱和限制,防止积分饱和导致超调。
2.4 通信子域的数据流设计
通信子域是数据流向外的出口。在中小测控系统里,FPGA 和处理器之间的数据交换最常用三种接口:UART(低速调试)、SPI(中速简单)、FMC(高速并行总线)。
FMC 接口算是 STM32 系列跟 FPGA 通信的经典方案。FMC 全称 Flexible Memory Controller,可以把它理解成 STM32 外部存储器的总线控制器,它访问 FPGA 就像访问普通 SRAM 一样。关键是硬件连线:FMC 的地址线、数据线、读/写控制线、片选线分别接到 FPGA 的 GPIO 上。STM32 侧把 FPGA 当作一个外部存储区域,基地址可在 FMC 配置里设,写一个数据就是往某个内存地址写一次。FPGA 侧则需要做一个从设备模块,解析 FMC 总线时序,把数据总线上的数据按地址译码后写入内部寄存器或 FIFO。
FMC 时序对接是个坑比较多的环节。因为 STM32 的 FMC 总线的建立时间、保持时间是可以编程配置的,配置得太紧 FPGA 来不及响应,配置得太松性能上不去。我一般先按官方参考值设定,然后用示波器实测数据总线与读写信号间的时序配合,再逐项调整。还有一点,当 FPGA 从端数据没准备好时,可以通过 FMC 的 WAIT 信号拉长总线周期,这个功能很多工程师没用上——ST 的 FMC 手册里管这个叫"Extended mode",FPGA 检测到内部 FIFO 空/满时主动拉低 WAIT,STM32 会自动插入等待周期,数据不会丢。
LVDS 接口则用于高速场景,比如连接高速 ADC 或者板间数据传输。FPGA 里的 LVDS 接收模块用 IDDR 原语做双倍速率采样,把高速串行数据恢复为并行数据。要注意输入差分对需要加终端电阻,FPGA 内部引脚约束要绑定到支持 LVDS 的 bank 上,而且电平标准要选 LVDS_25 而不是 LVDS(后者是 3.3V 的,Xilinx 7 系列上一般不直接用)。LVDS 做得好不好,跟 PCB 布线关系很大,差分对要等长、阻抗要控制,这些虽然是硬件的事,但 FPGA 工程师最好也懂一点,不然出了问题排查方向都找不到。
3. 核心模块拆解与实操要点
3.1 采集控制模块的时钟与触发设计
采集控制模块是整个测控系统的"心脏",它产生采样时钟、触发信号和转换控制信号。
采样时钟来源有两种:一是直接用 FPGA 板载晶振分频,二是用专门的采样时钟芯片(比如 AD9510)给 FPGA 输入。前者简单便宜,但抖动较大,适合一般测控;后者用于高精度同步采样场景,成本高但性能好。我的建议是:对精度要求不高的直接分频,要求高的用 MMCM/PLL 把外部参考时钟倍频到 ADC 所需的采样频率,并在输出端加 BUFG 全局时钟缓冲,确保采样时钟网络上每个触发器的 skew 控制在皮秒级。
触发信号没那么玄学,本质上就是一个使能信号——当某条件满足时,把触发拉高,采集逻辑才开始真正写入 FIFO。但有一个细节:同步触发和异步触发的区别。如果外部触发信号来自机械开关或者别的时钟域,必须先做两级同步(打两拍)消除亚稳态,然后接入采集逻辑。这个"打两拍"的细节看着微不足道,但忽略它很容易导致 FIFO 写指针偶尔错乱、数据少写一帧之类的怪问题。
下面是采集控制模块的一个简化 Verilog 示例框架:
module adc_ctrl #( parameter DATA_WIDTH = 16, parameter CHANNELS = 8 )( input wire clk, input wire rst_n, input wire ext_trigger, input wire [DATA_WIDTH-1:0] adc_data_in, output wire adc_cs_n, output wire adc_sclk, output reg [DATA_WIDTH-1:0] sample_out, output reg sample_valid ); // 同步外部触发信号 reg trig_sync1, trig_sync2; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin trig_sync1 <= 1'b0; trig_sync2 <= 1'b0; end else begin trig_sync1 <= ext_trigger; trig_sync2 <= trig_sync1; end end wire trigger_pos = trig_sync1 & ~trig_sync2; // SPI 读取状态机(简化版) localparam IDLE = 2'b00, START = 2'b01, SHIFT = 2'b10, DONE = 2'b11; reg [1:0] state; reg [4:0] bit_cnt; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin state <= IDLE; adc_cs_n <= 1'b1; adc_sclk <= 1'b0; bit_cnt <= 0; sample_valid <= 1'b0; end else begin case (state) IDLE: begin adc_cs_n <= 1'b1; if (trigger_pos || sample_valid == 1'b0) begin state <= START; adc_cs_n <= 1'b0; // 拉低片选 end end START: begin bit_cnt <= 0; state <= SHIFT; end SHIFT: begin adc_sclk <= ~adc_sclk; if (adc_sclk) begin // 在下降沿采样 sample_out[15 - bit_cnt] <= adc_data_in; bit_cnt <= bit_cnt + 1'b1; if (bit_cnt == 5'd15) state <= DONE; end end DONE: begin adc_cs_n <= 1'b1; sample_valid <= 1'b1; state <= IDLE; end endcase end end endmodule注意这个示例是简化的,实际工程里还要加 FIFO 接口、通道选择译码、超时保护等逻辑。有一个实操捷径:Xilinx/Intel 的 FPGA 开发工具里都带 SPI 接口的参考设计,可以拿来改,不必完全从零写状态机。
3.2 处理模块的定点化计算细节
FPGA 里做信号处理,第一原则就是全部转成定点数。浮点在 FPGA 里可以用(Xilinx 有 Floating-Point IP),但资源消耗多、流水线延迟大,对实时测控来说往往没有必要。绝大多数测控场景下,ADC 输出的是 16 位整数,我们做滤波和控制计算时用的中间系数也都用定点表示,精度完全够。
以 PID 控制模块为例,我用的是增量式 PID 公式:
Δu(n) = Kp * [e(n) - e(n-1)] + Ki * e(n) + Kd * [e(n) - 2*e(n-1) + e(n-2)]这里的 Kp、Ki、Kd 是定点化后的系数。定点化的基本方法是:先确定一个小数位数 Q,然后把浮点系数乘以 2^Q 后取整。比如 Q=12,系数 0.35 就存成整数 0.35×4096≈1434。所有运算都按整数做,最终结果再右移 Q 位还原。注意每一步乘加的位宽要留够余量——Kp、Ki、Kd 乘上 16 位误差信号,乘积至少 32 位宽,中间累加结果建议 40 位以上,防止溢出。
定点化有个绕不开的矛盾:数据范围和精度是一对冤家。Q 越大精度越高但能表示的整数范围越小,一旦信号幅度大就溢出。我给的建议是:在系统设计阶段先用 Matlab/Simulink 做浮点仿真,把输入信号的范围、噪声幅度、闭环响应都摸清楚,然后再定 Q 值。千万不要在 FPGA 里用仿真脚本边跑边试——那效率太低了。
滤波器模块也有同样的定点问题。FIR 滤波器的系数通常自带浮点数,我用 FIR Compiler 或手写乘加树时,会给每个系数右移一个共享的小数位,然后统一移位对齐,避免在每个乘加后做截断导致误差累积。实际上 Xilinx FIR Compiler 会自动处理系数量化和输出位宽,但你要懂它怎么做的,才能在输出位宽设置上不留坑——输出位宽设太窄会截断有效信号幅值,设太宽浪费 DSP Slice。
3.3 通信模块的设计要点与注意事项
UART 发送模块可能是所有测控程序里最不起眼但使用频率最高的部分。示波器接上、串口助手打开、printf 一打,系统的运行状态就一目了然了。FPGA 里做 UART,最核心的是波特率生成——用系统时钟除以波特率得到分频计数值,理论上分频值和整数分频存在误差,时间长了位误差会累积。解决思路是:计数到中间点采样 RX 信号(这样对抖动容忍度最高)、每个位的采样窗口中心对齐、以及根据误差预算选择合适的分频系数。比如 50MHz 时钟下跑 115200 波特率,分频计数是 434,实际波特率误差很小,可以直接用;但如果你跑 2M 波特率,可能就要选能整除的时钟源,或者在空闲间隙重置计数器避免误差累积。
FMC 从设备模块是通信模块里最容易出问题的。FMC 总线时序本质上就是一个"地址/数据复用总线读写"动作,FPGA 侧要做的事是:根据地址线译码出内部寄存器地址,根据读写信号的方向决定数据流方向。我通常在内部分三块寄存器空间:
- 控制/状态寄存器区(32 位地址,每寄存器一个功能)
- 数据缓冲 FIFO 区(CPU 读数据从 FIFO 出,CPU 写数据进 FIFO)
- 配置参数区(存放采样参数、触发阈值等)
设计时要特别注意地址对齐——STM32 的 FMC 数据宽度可以配成 8/16/32 位,如果你用 16 位模式,地址线最低位 A0 就无效了,寄存器索引要按地址偏移来映射。另外 FPGA 内部寄存器读写要放到统一时钟域,对 FMC 总线的异步控制信号先做同步处理,否则容易偶发读写错误。
LVDS 收发模块设计的数据宽度由传输协议决定,比如 7:1 串行化的常用于显示接口,1:4/1:8 的则用于 ADC 数据链路。接收端用 ISERDESE2(Xilinx)或 ALTDDIO_IN(Intel)把串行数据转成并行,发送端用 OSERDESE2 或 ALTDDIO_OUT 做并转串。接口处的关键是位对齐——高速串行数据本身没有对齐标志,通常协议里会定义帧同步字,比如 K28.5 或 0x1F 开头+数据长度,接收端做滑动匹配对齐。如果你只是点对点传输,可以用简单的同步头方案:每帧数据开头固定发送 0xAAAA_5555 之类的同步码,接收端连续检测到两个同步码后,再按固定字节顺序解出完整帧。
4. 实操过程中的底层机制与关键环节实现
4.1 时钟与复位的正确处理姿势
整个测控程序跑起来,第一个要处理好的就是时钟和复位。很多人随手把外部晶振直接接进逻辑当全局时钟用,外部晶振的信号质量往往不够好,而且驱动能力、同步性都难以保证。正确做法是:外部晶振信号先进全局时钟缓冲器(BUFG)或 MMCM/PLL 的专用时钟输入引脚,由 MMCM/PLL 产生内部各模块所需的时钟。MMCM/PLL 输出经过 BUFG 接到全局时钟网络,这样片上所有触发器共享同一时钟树,时钟偏斜可控制在很低的水平。
多时钟域的设计需要仔细规划。高速采样时钟、FMC 接口时钟、UART 时钟、处理逻辑时钟,它们可能由不同 PLL 输出产生,彼此不同频也不同相。跨时钟域的数据传输必须经过同步处理,常用的方式包括:
- 电平信号:两级触发器同步(打两拍)
- 多位并行数据:使用异步 FIFO 做缓冲
- 脉冲信号:使用 toggle 同步器或者脉冲展宽后再同步
异步 FIFO 是我在测控程序里用得最多的跨时钟域方案。比如 ADC 采样时钟域(2MHz)产生的数据要传给 FMC 时钟域(100MHz)让 CPU 读取,中间就用一个异步 FIFO 衔接。FPGA 工具都提供异步 FIFO IP,注意配置时要选择"独立时钟"模式,并合理设置读写指针的格雷码同步深度。IP 例化出来之后,读写侧的信号各自归属各自时钟域,只要 FIFO 深度和读写速率匹配,就不会丢数据。
复位设计也常常被人看轻。全局异步复位、同步释放是 FPGA 复位的基本准则——复位信号进来后先打两拍,产生的同步复位信号再分发到各个模块。注意不能用这段异步复位直接复位模块内的所有触发器,那样容易造成复位释放时刻不一致,出现部分逻辑还没退出复位、另一部分已经开始跑状态机的竞态问题。
4.2 FMC 与 STM32H743 联调的实测记录
热词里有"stm32h743和fpga实现fmc通信",这个组合我之前实际调过,把过程分享一下。
硬件连接上,STM32H743 的 FMC 引脚分配到 FPGA 的 Bank 上,注意电平标准要匹配——H743 的 FMC I/O 一般配置成 3.3V,FPGA 对应 Bank 的 VCCO 就要接 3.3V,别把 FPGA 的 1.8V Bank 接上去。地址线用了 6 根(A0~A5,映射 64 个 16 位寄存器空间),数据线 16 根,另有 NOE(读使能)、NWE(写使能)、NE1(片选)三个控制信号。
STM32 侧使用 STM32CubeMX 配置 FMC:
// FMC 初始化参数(HAL 库风格) FMC_NORSRAM_TimingTypeDef Timing = {0}; Timing.AddressSetupTime = 5; // 地址建立时间 Timing.AddressHoldTime = 5; // 地址保持时间 Timing.DataSetupTime = 6; // 数据建立时间 Timing.BusTurnAroundDuration = 0; Timing.CLKDivision = 16; Timing.DataLatency = 0; Timing.AccessMode = FMC_ACCESS_MODE_A;初始化完成后,FPGA 侧的寄存器空间就挂在 0x60000000 起的内存区域上。读一个寄存器就是*(volatile uint16_t *)0x60000000取数据,写就是赋值。这个体验确实很爽——FPGA 就像一个外扩 SRAM 一样操作。
FPGA 侧的核心模块用一个读写状态机解析 FMC 总线:
// 简化版 FMC 从设备读时序 always @(posedge clk) begin if (!ne1_n && !noe_n) begin // 地址已经由地址线给出 case (addr[5:0]) 6'd0: data_bus_out <= ctrl_status_reg; 6'd1: data_bus_out <= sample_fifo_rd_data; default: data_bus_out <= 16'h0000; endcase end end但这个简化版在实际联调中是不够的。因为 STM32 的 FMC 读周期约持续 12 个 HCLK(约 12ns @ 168MHz),而 FPGA 内部逻辑从地址译码到数据总线输出需要组合逻辑延迟+寄存器 setup 时间,如果地址线变化后的数据总线是组合逻辑直接输出,很容易满足不了 STM32 的数据建立时间。我最终的方案是:地址信号先在 FPGA 内寄存一拍,寄存器输出接译码逻辑,译码结果寄存后再控制数据总线输出——相当于给 FMC 读操作加了一拍流水线延迟,代价是多等一个时钟周期,但时序余量大得多。
实测下来,用上面的配置(AddressSetupTime=5,DataSetupTime=6),FMC 读一个寄存器的总周期约是 5+6+1=12 个 HCLK,CPU 跑 480MHz 时一个 16 位读操作耗时约 80ns。对大多数测控数据吞吐来说绰绰有余。如果还不够快,可以开 FMC 的 Burst 模式,配合 FPGA 侧的连续地址译码,一次突发读多个数据,带宽能翻好几倍。
4.3 数据打包与上位机协议设计
测控程序最后一步是把数据呈给上位机或操作人员。直接在 FPGA 里实现一个完整的上位机通信协议,比如 Modbus、CANopen,是可行的但工作量不小。我一般只在 FPGA 里做一个简单的数据打包模块,负责把 FIFO 里的裸数据组成帧格式,然后通过 UART 或 FMC 发给处理器,由处理器完成上层协议解析。
帧格式设计要兼顾简单和可靠。我常用的帧格式是:
帧头(0xAA 0x55) + 帧长度(1字节) + 通道号(1字节) + 数据(N字节) + CRC16(2字节)帧头和长度用来做帧同步与完整性校验。CRC16 用查表法在 FPGA 里实现很简单,也或者直接用多项式除法状态机,时序开销都很小。
有一点要特别提醒:数据打包模块必须在发送侧加 FIFO 或移位寄存器缓冲,不能直接从采集 FIFO 读数据往 UART 发送逻辑里塞。因为 UART 发送速率是固定的(比如 1Mbps),而采集 FIFO 的读速率由数据到达速率决定,两者直接连接会出现"采集快时来不及发、采集慢时发空数据"的问题。正确做法是:打包模块先采集够一整帧数据放到发送 FIFO,发送逻辑检测到发送 FIFO 非空就逐字节发出,形成一个松耦合的流水线。这个套路几乎适用于所有"数据采集+串行发送"的测控场景。
5. 常见问题与排查技巧实录
5.1 采样数据偶发跳变:跨时钟域的处理失误
这是我在一个 8 通道采集项目里遇到的最头疼的问题。现象是:系统长期运行后,某些通道的数据偶尔会跳变几百个 LSB,持续几微秒后恢复。一开始怀疑是硬件信号干扰,拿示波器怎么抓都抓不到,后来用逻辑分析仪在 FPGA 内部抓信号才发现,居然是 ADC 转换完成信号(BUSY)直接连到了采集状态机上,而它来自 ADC 的时钟域,跟 FPGA 内部采样时钟不同步。
BUSY 信号的上升沿到达 FPGA 时,如果恰好落在触发器时钟沿附近,触发器的输出就可能进入亚稳态——既不按 0 也不按 1,而是介于两者之间,最终被随机解析成 0 或 1。这就会导致一次采样被跳过或重复,数据跳变由此而来。
排查过程给了我一个很深的教训:任何进入 FPGA 的外部异步信号,都必须在模块入口做两级同步。后来我在所有外部信号输入端(ADC BUSY、按键、外部触发等)统一加了同步器,问题彻底消失。这也是我为什么在第一版设计时就反复强调:外部信号进 FPGA 必须先过同步器,一步都不能省。
5.2 FMC 读写偶发失败:时序约束与总线保持时间
STM32H743 和 FPGA 做 FMC 通信时还遇到过一个典型问题:连续大量读写时偶发数据错误,单步调试却完全正常。用示波器看数据总线发现,写数据在 NWE 上升沿过后只能保持约 2~3ns,而 FPGA 寄存器的建立时间要求可能大于这个数,偶发错误就是数据线在采样时刻处于不确定状态导致的。
问题根源在于 FMC 的时序参数匹配。STM32 的 FMC 有一个"数据总线保持时间"的概念,它指的是写操作后数据线保持有效的时间。这个时间如果太短,FPGA 侧的寄存器采样就可能不稳定。解决办法有两个方向:一是把 STM32 侧的时序参数加大——主要是数据建立时间(DataSetupTime)和地址保持时间(AddressHoldTime);二是 FPGA 侧改用"在写信号下降沿采样"的策略,此时数据线上已经是稳定值,时序余量最大。
我最终的方案是双管齐下:STM32 侧把 DataSetupTime 从 5 调到 8,FPGA 侧把写采样沿放在 NWE 的下降沿。调整之后跑 100 万次压力测试,零错误。这个经验后来我在别的项目里也反复用到——所有 MCU+FPGA 总线对接,只要偶发错数据的,先检查总线时序参数再查逻辑,往往一步到位。
5.3 FIFO 溢出与读空:缓冲深度怎么算才够
FIFO 深度选多大?这个问题几乎每个测控项目都会被问到。选小了数据会丢,选大了浪费 BRAM 资源。给一个工程化的估算方法:
假设 ADC 以 1MSPS 采样、16 位分辨率,连续采样 1 秒产生 2MB 数据。若 CPU 与 FPGA 之间 FMC 的实测吞吐是 20MB/s(上节测的约 12MB/s,某些配置下更高),那么数据产生速率远小于传输速率,FIFO 只要有几十个时钟周期的缓冲就够。但是如果 CPU 会被其他任务抢占 10ms,这 10ms 内 FPGA 产生的数据(2KB)就必须都能装进 FIFO——那么 FIFO 深度就得至少 1024×16bit 以上。
所以 FIFO 深度估算公式是:
FIFO 深度 >= (数据产生速率) × (最坏读写延迟差)最坏读写延迟差包括 CPU 抢占时间、DMA 配置时间、中断响应延迟等。工程上我习惯留 3~5 倍余量——BRAM 资源通常不缺,没必要在深度上抠门。
如果 FIFO 还是会偶尔溢出,另一个思路是加"丢弃策略",比如光有最新数据才有用(示波器模式),那溢出时就直接丢弃旧数据、保留最新帧;如果历史数据更重要(故障录波模式),那就溢出时暂停采集,等 CPU 读取后再继续。两种模式对应不同的 FIFO 配置和读控制逻辑,设计时要提前想清楚需求。
5.4 LVDS 接收不稳定的硬件排查清单
LVDS 接收问题很多时候不是逻辑代码的问题,而是硬件链路的问题。我总结了一份排查清单:
- 检查差分对是否接反。A/B 信号接反会导致全部数据位反转,逻辑上表现为一个固定的 bit 翻转——比如所有通道读数都变成 0x7FFF 而不是 0x8000。交换两根线即可。
- 检查终端电阻。LVDS 接收端通常需要 100Ω 差分终端电阻,如果板子没设计这个电阻,信号反射严重,高速下数据必然出错。有的 FPGA 开发板在 Bank 端集成了可切换的终端电阻(Xilinx 的 DIFF_TERM 属性),可以软件开启。
- 检查电源噪声。LVDS 接口的 Bank 供电要干净,如果 VCCO 纹波大,接收器阈值电压抖动也会造成随机比特错误。给 Bank 电源加磁珠和电容滤波是标准做法。
- 检查 PCB 走线。差分对要等长、靠近、避开时钟线——这些 PCB 设计规范直接影响信号完整性,FPGA 工程师如果参与原理图评审,一定要盯住差分对走线。
以上排查全部做完还不行,再回头检查 FPGA 逻辑——比如采样边的选择(用上升沿还是下降沿采样,取决于接口时序定义)、ISERDESE2 的位宽配置是否与发送端一致、有没有把 LOC 约束绑到 LVDS 专用引脚上。
6. 扩展思路:这套框架还能怎么用
写到这里,其实整份测控程序的核心框架已经讲完了。按这个结构去搭工程,再往里填细节,基本上一到两周就能出一个可用的原型。但用好 FPGA 的潜力,框架搭完之后还有不少扩展空间。
我最想强调的一点是:FPGA 的调试能力会直接影响开发效率。Xilinx 的 ILA(Integrated Logic Analyzer)和 VIO(Virtual I/O)是排在逻辑设计之后的第二件大事。ILA 可以实时观测内部信号波形,不用接外部示波器;VIO 可以虚拟地读写内部寄存器,调试时靠它给模块喂参数、看状态,实用得很。每搭完一个模块,我就先例化一个 ILA 把关键信号抓出来看一眼,确认之后再继续往下做,避免错误层层叠加到后期一起爆雷。
另外,数据流的架构决定了系统的扩展性。如果后续要加更高吞吐的采集通道,只需把单个 FIFO 换成多通道仲裁+共享 FIFO 的方式;如果要做多板同步,就给系统加一个 PPS 或 IRIG-B 时间同步模块,把这些外部时间基准纳入统一时基即可。这些扩展都不需要动整个框架,只要在对应子域里做加法。框架的价值就在这里——它的存在让你可以把注意力集中在当前功能点上,而不是天天担心"改一处会不会崩了全局"。
根据这段时间做测控项目的体会,我还想分享两个小建议:
第一,模块化一定不要停留在代码层面,仿真层也要跟上。我见过不少人写模块不写 testbench,直接在顶层上板调试,出了问题再回头查,效率极低。正确的做法是每个核心模块配套一个小 testbench,验证完 basic 功能再上板。FPGA 编译一次十几分钟,而仿真跑一遍只要几秒,这个时间成本账很好算。
第二,多利用 FPGA 厂商提供的 IP 和参考设计,但别盲用。Xilinx/Intel 的文档体系很全,每一个 IP 都有 product guide,里面有详细的接口时序、寄存器定义、性能参数。真正把 IP 用熟、用透,比从零手写底层逻辑省下大量时间——但前提是你得理解它的工作原理,否则出了时序问题你都不知道怎么排查。
测控程序这件事,说难也难,说简单也简单。难在千变万化的接口协议和时序约束,简单在框架一旦清晰、数据流理顺之后,剩下的只是往固定位置填功能。希望这篇分享能把框架搭起来的过程讲透,让刚开始接触 FPGA 测控的人少走一些弯路——那些时序上的坑、时钟域的坑、接口对接的坑,我自己都踩过,写出来就是希望大家别再踩一遍。