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 时钟生成 / 基准时间戳 └── 人机交互子域按键消抖 / 数码管 / LED2.2 数据流的第一层设计采集到缓存的路径先聊数据是怎么从物理世界流进 FPGA 的。大多数测控系统用的外部 ADC 都是 SPI 接口或并行接口SPI 常见的有 AD76068 通道同步采样并行/串行接口都有、ADS86888 通道 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 modeFPGA 检测到内部 FIFO 空/满时主动拉低 WAITSTM32 会自动插入等待周期数据不会丢。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 1b0; trig_sync2 1b0; end else begin trig_sync1 ext_trigger; trig_sync2 trig_sync1; end end wire trigger_pos trig_sync1 ~trig_sync2; // SPI 读取状态机简化版 localparam IDLE 2b00, START 2b01, SHIFT 2b10, DONE 2b11; 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 1b1; adc_sclk 1b0; bit_cnt 0; sample_valid 1b0; end else begin case (state) IDLE: begin adc_cs_n 1b1; if (trigger_pos || sample_valid 1b0) begin state START; adc_cs_n 1b0; // 拉低片选 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 1b1; if (bit_cnt 5d15) state DONE; end end DONE: begin adc_cs_n 1b1; sample_valid 1b1; 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 后取整。比如 Q12系数 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 数据链路。接收端用 ISERDESE2Xilinx或 ALTDDIO_INIntel把串行数据转成并行发送端用 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.3VFPGA 对应 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]) 6d0: data_bus_out ctrl_status_reg; 6d1: data_bus_out sample_fifo_rd_data; default: data_bus_out 16h0000; endcase end end但这个简化版在实际联调中是不够的。因为 STM32 的 FMC 读周期约持续 12 个 HCLK约 12ns 168MHz而 FPGA 内部逻辑从地址译码到数据总线输出需要组合逻辑延迟寄存器 setup 时间如果地址线变化后的数据总线是组合逻辑直接输出很容易满足不了 STM32 的数据建立时间。我最终的方案是地址信号先在 FPGA 内寄存一拍寄存器输出接译码逻辑译码结果寄存后再控制数据总线输出——相当于给 FMC 读操作加了一拍流水线延迟代价是多等一个时钟周期但时序余量大得多。实测下来用上面的配置AddressSetupTime5DataSetupTime6FMC 读一个寄存器的总周期约是 56112 个 HCLKCPU 跑 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 调到 8FPGA 侧把写采样沿放在 NWE 的下降沿。调整之后跑 100 万次压力测试零错误。这个经验后来我在别的项目里也反复用到——所有 MCUFPGA 总线对接只要偶发错数据的先检查总线时序参数再查逻辑往往一步到位。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 的 ILAIntegrated Logic Analyzer和 VIOVirtual I/O是排在逻辑设计之后的第二件大事。ILA 可以实时观测内部信号波形不用接外部示波器VIO 可以虚拟地读写内部寄存器调试时靠它给模块喂参数、看状态实用得很。每搭完一个模块我就先例化一个 ILA 把关键信号抓出来看一眼确认之后再继续往下做避免错误层层叠加到后期一起爆雷。另外数据流的架构决定了系统的扩展性。如果后续要加更高吞吐的采集通道只需把单个 FIFO 换成多通道仲裁共享 FIFO 的方式如果要做多板同步就给系统加一个 PPS 或 IRIG-B 时间同步模块把这些外部时间基准纳入统一时基即可。这些扩展都不需要动整个框架只要在对应子域里做加法。框架的价值就在这里——它的存在让你可以把注意力集中在当前功能点上而不是天天担心改一处会不会崩了全局。根据这段时间做测控项目的体会我还想分享两个小建议第一模块化一定不要停留在代码层面仿真层也要跟上。我见过不少人写模块不写 testbench直接在顶层上板调试出了问题再回头查效率极低。正确的做法是每个核心模块配套一个小 testbench验证完 basic 功能再上板。FPGA 编译一次十几分钟而仿真跑一遍只要几秒这个时间成本账很好算。第二多利用 FPGA 厂商提供的 IP 和参考设计但别盲用。Xilinx/Intel 的文档体系很全每一个 IP 都有 product guide里面有详细的接口时序、寄存器定义、性能参数。真正把 IP 用熟、用透比从零手写底层逻辑省下大量时间——但前提是你得理解它的工作原理否则出了时序问题你都不知道怎么排查。测控程序这件事说难也难说简单也简单。难在千变万化的接口协议和时序约束简单在框架一旦清晰、数据流理顺之后剩下的只是往固定位置填功能。希望这篇分享能把框架搭起来的过程讲透让刚开始接触 FPGA 测控的人少走一些弯路——那些时序上的坑、时钟域的坑、接口对接的坑我自己都踩过写出来就是希望大家别再踩一遍。