1. 先想清楚一件事:FPGA里的测控程序,到底是写逻辑还是在搭框架
前几天有个刚入行的朋友问我:FPGA测控程序是不是就是写Verilog,把采集、处理、输出这几块逻辑堆在一起就行。我反问他一句:你现在写个SPI采集模块没问题,那如果后续要加一路CAN总线、再加一路模拟量输出,你是继续往顶层文件里塞代码,还是打算怎么组织?
他沉默了一下,说还没想过这个问题。
这正是大部分FPGA工程师从入门到进阶的真实分水岭。测控程序,尤其是面向工业、科研、装备场景的测控程序,难点从来不在某个具体接口能不能打通,而在于:一个系统里同时存在几十路采集、好几路控制输出、多种通信协议时,代码结构怎么搭,模块边界怎么划,数据从物理引脚到上位机、从上位机到物理引脚这整条链路怎么走,才不至于项目做到一半自己都看不懂自己的工程。
我做过几年设备测控方向的FPGA开发,从最初纯逻辑堆叠、后来开始思考框架、再到现在能提前预判数据流的冲突点,差不多也是踩了三四个项目的大坑才逐渐有些心得。这篇文章不打算讲某个具体外设怎么驱动,那属于数据手册的范畴。我想聊的是更往上一层的东西:FPGA测控程序里,框架应该怎么搭、模块应该怎么拆、数据流应该怎么理。这些内容没法直接用某一段代码体现,但它决定了你后续每一个模块写起来是省心还是糟心。
先说一个我的核心观点:FPGA测控程序,本质上不是一个逻辑设计问题,而是一个系统设计问题。逻辑只是最终的表达形式。如果你一开始就把整个系统理解成一套有序的数据流框架,而不是一堆零散的逻辑模块,你会发现后续无论是加功能、调时序、还是多人协作,都会顺畅很多。
这篇文章适合的读者,是已经能独立完成简单FPGA开发、比如写过SPI或UART接口、做过基础信号处理,但还没有系统思考过测控程序整体架构的人。如果你正处在“单点能通、系统难搞”的阶段,那这篇文章应该能帮你在动手前先建立起一个相对完整的框架认知。
2. 测控程序的核心矛盾:既有连续流,又有离散事件
2.1 “测”和“控”本质上是两种完全不同的数据行为
要拆解FPGA测控程序的框架,第一步不是画框图,而是想清楚你手里的数据到底长什么样。我做过的测控项目,大体上所有数据都能分为两类。
第一类是流式数据。比如ADC以1kHz采样率连续输出的数据,或者编码器反馈的连续位置脉冲。这类数据的特点是有明确的速率、没有尽头、每个样本之间地位平等。它们需要的是FIFO、是数据通路、是流水线处理。处理这类数据的模块,关心的是吞吐量、是连续不中断、是延迟。
第二类是事件型数据。比如上位机下发一条控制指令、设备上报一个故障状态、某个阈值触发了保护动作。这类数据的特点是稀疏、突发、带有明确的语义含义。它们天然适合寄存器、适合握手协议、适合中断标志位。处理这类数据的模块,关心的是可靠性、是丢不了、是不能被覆盖。
很多测控程序设计乱套,根源就在于这两类数据被混在一起处理了。比如有人用一个统一的读写寄存器组来管理所有数据,结果流式数据需要通过轮询寄存器来搬运,CPU和FPGA之间被迫频繁交互,性能上不去。也有人反过来,把所有东西都设计成无脑流式,连一条控制指令也要走数据通路,结果控制指令的响应延迟完全不可控。
这两类数据的存在本身就是测控程序区别于其他FPGA应用的根本特征。纯通信设备可以只做流式,纯逻辑控制器可以只做离散事件,但测控程序两者都要面对。好的框架设计,第一步就是把这两类数据在架构层面就区分开。
2.2 从“采集-处理-输出”的单向思维升级为“数据行为驱动”的设计思维
相比通用软件框架,FPGA测控程序的框架受数据行为驱动更加明显。
在通用软件领域,框架往往围绕交互逻辑和函数调用组织,比如前后端分离、MVVM、事件驱动等。但在FPGA测控里,硬件的并行性和时序性是天然的框架约束——你不可能让一条数据通路像CPU执行指令一样“停下来等你”,也不可能在一个时钟周期内通过依赖全局锁来实现互斥。
因此,模块设计必须以数据行为为核心:哪条路径是周期性连续运水的管道,哪条路径是偶发发令的驿站,一开始就要明确。
我自己的经验是,在项目开工第一天,别急着写代码,先做数据流梳理。把系统里所有数据罗列出来,标清楚:数据从哪来、到哪去、是什么类型、什么速率、允许多少延迟、可靠性要求多高。这份梳理做完,框架基本就出来了一半。
3. 顶层框架怎么搭:接口层、功能层、管理层相互独立
3.1 我实战中验证过的一套三层结构
我自己做下来的经验,FPGA测控程序可以清晰分成三个逻辑层面:物理接口层、功能处理层、系统管理层。这三个层各司其职,不越俎代庖,是整个框架能长期演进的基础。
物理接口层负责和外界打交道。ADC芯片、DAC芯片、编码器、通信收发器的时序逻辑都在这层实现。这层模块的核心任务是:把物理世界的电信号变成内部统一的逻辑表达,或者把内部逻辑表达还原成物理世界的时序波形。这一层不关心数据是什么意思、要送到哪里去,只关心时序有没有满足器件要求、位宽转换和时钟域转换有没有做对。
功能处理层负责真正有价值的业务逻辑。滤波、标定、PID控制、阈值比较、协议解析都在这层实现。这层模块的输入是接口层已经格式化的数据,输出是进一步处理后的结果。这一层不关心数据是从哪个引脚来的,也不关心另一个模块内部怎么处理数据。
系统管理层负责协调全局。寄存器读写、全局配置、运行状态机、错误日志、停机和复位逻辑都归属这一层。它是整个测控程序的大脑和中枢,也是和上位机交互的主要通道。上位机的大部分命令,最终通过管理层的寄存器接口下发到各功能处理模块。
这套三层结构的好处,我一句话总结:它把“和世界打交道”和“和系统打交道”这两件事彻底分开了。接口层坏了,只改接口层;算法换了,只动功能层;需求新增了控制逻辑,主要在管理层加状态机。模块之间真正的耦合关系被降到了最低。
3.2 为什么要用AXI-Lite和AXI-Stream这类现成总线来衔接
层级划分出来后,层与层之间的通信规范就成了下一个问题。最初我做这类项目时,习惯每个模块自定义接口,寄存器名字、握手信号全凭个人喜好。后来发现两个问题:一是自己项目做大了容易忘,二是想复用别人的IP核时接口对不上,还得加转换逻辑。
后来我统一在FPGA内部采用两套总线规范来衔接层与层之间的关系:
寄存器读写类操作用AXI4-Lite接口。这个接口非常轻量,单次读写,时序简单,网上参考实现也多。管理层和功能处理层之间、管理层和接口层之间的寄存器配置通路,全部换成AXI-Lite之后,模块之间能看到的数据通路变得极其规范。新增一个模块,只要它提供AXI-Lite接口,挂到总线上就能被统一管理。
流式数据传输用AXI4-Stream接口。这个接口简洁到只有一组握手信号加上数据线,非常契合流式数据的搬运场景。接口层和功能处理层之间的连续数据通路,统一用Stream接口。
为什么选这两套而不是自己定义一套?原因很简单:可复用性。Xilinx和Intel家的IP核标准接口就是这些,你用现成标准,就能省去大量接口适配工作。就算Fliform FPGA,像紫光、安路的开发环境也都支持用户自定义总线的挂载。模块的输入输出全部标准化之后,模块之间像积木一样插拔,极大提升开发效率和协作便利性。
3.3 在FPGA框架中设置“数据总线”而非“点对点连线”——从连接器到高速公路
有一个框架层面的习惯,也可以提供给正在设计架构的人参考:尽量别在顶层用一根根信号线把模块手拉手连起来,而是设计一个数据通路主干,让模块挂上去。
打个比方:点对点连线就像每个城市和每个城市之间各修一条专属公路,连接数量一多,接线根本拉不开。而总线方式相当于修一条国家高速公路,各城市通过匝道上下主路。匝道口可能就是一个小小的地址译码逻辑或数据选择逻辑,成本很低,但换来的可扩展性非常可观。
在FPGA里,这个“高速公路”可以是厂商提供的AXI互联IP核,也可以是自己写的一个简单写优先译码交叉开关。加新功能时,只要新模块支持统一接口,就能直接挂上主干路,既不需要动上层,也不影响其他模块。这种架构对后期调试和对接上位机极其友好。
4. 模块拆分的实操方法论:从功能、时钟域到复用粒度
4.1 模块边界划分原则:功能单一、接口标准化、状态独立
很多人在模块划分上犯的错误,是把“功能”和“文件”混为一谈。比如一个文件写了800行,包含了ADC初始化、数据滤波、FIFO缓冲、心跳灯闪烁——看起来是一个模块,实际是四五个职责绑在一起,任何一处改动都可能引起其他功能异常。
我划分模块的实操原则有三个:
功能单一。一个模块只做一件事,比如“做二阶低通滤波”,或者“把32位并行数据转成RS232串行发出”,不要搞开天窗式的多功能合体。
接口标准化。模块的对外接口优先采用统一总线标准(如AXI-Lite、AXI-Stream),让模块成为一个“标准件”。
状态独立。每个模块应该有自己独立的内部状态机,模块之间的交互尽量通过接口消息完成,而不是通过共享全局变量式信号。FPGA里所谓的“共享全局”往往就是跨模块引用打散的寄存器,是一种隐式耦合,务必减少。
遵循这三条原则拆出来的模块,单独测试、单独仿真都极其方便。每个模块拿到仿真环境里跑一把,验证完功能再往顶层挂,问题定位会非常快。
4.2 时钟域划分:比功能更先确定的边界条件
在拆模块之前,还有一件比功能更重要的事情:确定时钟域划分。测控程序里往往同时存在多个时钟,主控逻辑一个时钟、ADC采样一个时钟、以太网或USB芯片一个时钟、编码器计数一个独立时钟。跨时钟域是FPGA开发中最大的故障源之一,而没有之一。
我通常优先采用以下方式划分模块:
- 同一个时钟域内的逻辑尽量放一个模块,减少跨模块跨域出现的概率。
- 不同时钟域之间必须明确划分CDC边界位置。所有跨时钟域的交互数据必须经过同步处理。
- CDC交点越少越好,尽量把多个功能模块的数据同步汇总到一个点上,统一由一个跨时钟域模块处理。
这里特别提醒:同步器不是万能的。单bit信号可以用两级同步器,多bit总线数据通常需要用异步FIFO或握手同步;慢时钟到快时钟与快时钟到慢时钟的处理也不同。不同领域的系数、位宽变化、尤其是不规则脉冲信号,都必须单独分析设计,不能套模板。
4.3 可复用模块的粒度把握:太小和太大都是坑
模块粒度,也就是模块的粗细程度,是不少人忽视的痛点。模块拆太细,一个简单的地址译码也单独一个模块,整个工程可能有上百个小文件,管理成本极高,仿真和综合也变慢;模块拆太大,就像前面说的,改动一点就要重新回归验证全部功能。
我看过的合适的粒度,大概是:一个模块能够独立完成一整个外部设备的功能对接(比如“与ADS1278的完整数据接口”)、或者独立完成一个有明确算法边界的过程(比如“滑动窗FFT”)、或者独立完成一类管理功能(比如“全局中断管理”)。这个粒度和IP核的粒度相当,最利于复用。
实际开发中,我会给每个模块单独建仿真testbench,做到模块级的仿真通过后再集成。这个习惯帮我省下了大量联调时间。集成时如果出了问题,我只需要检查模块之间接口的时序关系,而不用怀疑模块内部逻辑。
5. 数据流分析实战——从引脚到寄存器,从寄存器到引脚
5.1 上行链路:物理信号 → 接口层 → 功能层 → 管理层
测控数据的上行链路,是设备把外部物理量采集进来、处理成可理解的信息、最终交到上位机的过程。我拿一个实际项目里的链路来拆解。
假设我们有一个8通道ADC,以500kSPS采样速率连续采集电压信号,FPGA需要把这些数据滤波后通过UART发给上位机显示。
数据链路是这样的:
- SPI接口模块负责从ADC芯片按帧读取采样结果。这个模块就是物理接口层,只关心SPI时序,每次转换完成收到一个16bit数据。
- 接口层把16bit数据写入一个异步FIFO,这个FIFO的写侧时钟是SPI时钟域,读侧时钟是系统逻辑时钟。CDC问题在这一步就解决了。
- 功能处理层的滤波模块从这个FIFO里连续读取数据,做均值滤波、去除毛刺,处理完输出一路平滑数据。
- 系统管理层周期性地把滤波模块的输出结果打包成帧格式,送到UART发送模块。UART发送模块其实也属于接口层的一部分。
整个过程里,数据像流水一样从物理世界流向逻辑世界,每一层的模块只跟自己的上下游打交道,没有任何一环需要知道全局。这就是数据流设计最舒服的状态。
5.2 下行链路:控制命令 → 参数解析 → 状态更新 → 物理输出
下行链路和上行链路有本质的不同,它是离散的、突发的。
举个例子:上位机通过UART发送一个“设定PID目标转速为1200rpm”的命令。数据流是这样的:
- UART接收模块(接口层)收到一串字节,解析出命令字和参数值。
- 管理层里的协议解析状态机识别出这是一个PID目标设定命令,通过AXI-Lite接口把目标转速值写入PID控制模块对应的寄存器。
- PID控制模块(功能处理层)感知到目标值变化,重新计算控制输出。
- 控制输出通过PWM模块(接口层)的占空比调节,物理上改变执行机构的输出。
下行链路的关键是可靠性和语义完整性。FPGA内部可能同时存在多个模块向同一个寄存器发起写操作,这时候就需要仲裁逻辑保证后写入的数据生效。我始终会给每个寄存器增加一个简单的写有效标志,在调试时非常有用,能快速定位是谁在什么时候改了这个值。
5.3 设计状态寄存器组时容易忽略的几个细节
状态寄存器组是管理层和上位机之间的窗口。我在做状态寄存器设计时,有几个踩过的坑值得提一下。
第一,每个寄存器的位宽、地址、读写权限要提前规划完整。规划表里至少要有:偏移地址、寄存器名称、位域说明、读写属性、默认值、所属模块。这听起来繁琐,但没有这个表,后期联调会陷入灾难——你会一直在“这个bit到底是低有效还是高有效、默认值到底是多少”这些问题上反复拉扯。
第二,流式数据和状态数据不要挤在同一个寄存器里。比如一个16bit寄存器,高8bit是ADC实时采样值、低8bit是状态标志位,这种设计看起来很省地址,实际上给时序收敛和模块划分都带来了麻烦。采样值走的是流式通路,状态位走的是事件通路,两条路非要挤在一起,只会让管理更乱。
第三,寄存器读写路径上,最好加一个总线错误响应机制。比如对保留地址进行写操作时,返回一个错误标志。这在上位机误操作时非常有价值。
6. 时序设计与跨时钟域在数据通路中的落地
6.1 跨时钟域四种常见场景:从慢到快、从快到慢、多bit对齐和复位域
CDC是FPGA测控程序里最值得单独拿出一个章节聊的内容,因为测控系统几乎天然是多时钟环境。
从慢到快的典型场景是:一个低速UART模块(比如115200bps,对应的字符周期远长于系统时钟周期)产生一个接收完成脉冲,被系统时钟域的模块使用。这种单脉冲同步,只需要两级同步器就能安全处理。
从快到慢的典型场景是:系统时钟域产生的高频脉冲信号,要被一个很低速的模块采样。这种如果直接两级同步,很容易漏掉脉冲。解决办法一般是展宽信号,或者把信息转化为电平/握手方式传递。
多bit数据的跨时钟域处理,通常是异步FIFO解决。AD采集写时钟域和逻辑读时钟域之间的数据搬运,我统一用异步FIFO,深度按数据速率和突发长度估算,再加一点裕量。不要在这个地方省硬件资源,该用FIFO就用FIFO,手写握手逻辑做大数据搬运很容易出错。
复位域的同步也是一个容易忽略的点。不同模块在不同时刻进入复位态,最怕的是A模块还在复位中、已经向B模块发送了数据。所有跨域交互信号,复位释放时至少要保证一段时间的稳定期,一般建议对跨域信号再增加一级“数据有效”的同步,防止复位撤除瞬间的亚稳态传播。
6.2 数据流中的背压机制与FIFO深度估算
连续流数据的跨时钟域交互,背压机制(反压)比固定时序重要得多。AXI-Stream的TDREADY信号本质上就是背压机制:当前级处理不过来时,后级可以拉低ready来让前级暂停发送。
我之前做过一个8通道同步采集系统,ADC输入数据率是250kSPS×16bit×8通道,加起来32Mbps。FPGA端需要实时对数据进行抽点并编码转发。最初我直接把SPI读取速率设计成等于采集速率,结果一旦系统偶尔去处理其他中断,FIFO就出现溢出。
后来我用FIFO深度公式做了一次估算:FIFO深度=吞吐率×最大响应等待时间。采集端突发持续时间内不能消费数据,就存在FIFO,深度必须大于这段时间累积的数据量。算下来需要约2KB容量,我直接用了4KB深度,留了余量。从那以后这个模块的overflow信号再没拉高过。
不要凭感觉定FIFO深度,一定要按最坏情况下突发流量和最大等待延迟来推算。这是数据流设计里为数不多可以精确计算的地方,别浪费它的确定性优势。
6.3 时序约束的关键路径选择:把寄存器和关键数据通路拉直
FPGA测控程序里最容易出现的时序违例,往往不在逻辑最复杂的地方,而在跨时钟域的同步路径上。同步器需要经过两级寄存器链,一旦布线工具把这些寄存器打散分配到不同SLICE,可能出现建立时间违例。
我的处理方式是,在XDC约束文件里对异步FIFO的跨时钟信号路径统一加ASYNC_REG约束和false_path约束。前者告诉工具这些寄存器是用于异步同步的,允许特殊处理;后者告诉工具不需要检查它们之间的时序关系,因为信号本身已满足异步安全条件。
另外,在测控数据通路的稳定前提下,尽量做流水级拆分,把长组合逻辑链路分段寄存。比如一个单独的组合逻辑超过3层以上,我就会考虑插入中间寄存。虽然会多消耗一个或两个时钟周期的延迟,但时序收敛容易得多。延迟可计算,时序不收敛就是玄学,这两者很好选。
7. 一个实际的测控程序框架案例:从零到一的完整拆解
前面讲的都是方法论,可能有些抽象,我拿一个自己做过的过程控制模拟样机项目来具体说明整套框架是怎么落地的。
这个项目要求FPGA同时完成:
- 16路模拟量采集(16bit ADC,通过SPI接口)
- 4路PWM输出(用于控制比例阀)
- 1路增量式编码器计数
- 1路RS232通信(与上位机)
- 1路隔离IO输入输出(用于手动启停和故障联锁)
7.1 模块划分实例
根据三层框架,我把工程分成了下面这些模块:
- 接口层:
adc_spi_master、pwm_gen、encoder_counter、uart_rx、uart_tx、gpio_in_filt、gpio_out_ctrl - 功能层:
channel_scale_filter(完成工程单位换算和均值滤波)、pid_controller(完成4路PID控制)、fault_detect_logic(完成报警判断) - 管理层:
reg_bank(寄存器组)、cmd_parser(命令解析)、system_heartbeat(运行状态心跳) - 支撑层:
async_fifo_16x1024、clk_wiz(时钟管理)、reset_synchronizer(复位管理)
整个工程大概13个模块,比很多“一个顶层文件战到底”的工程看起来多不少,但每个模块都短小精悍,最大的reg_bank也就400多行代码。调试时我只开涉及相关模块的仿真,速度非常快。
7.2 数据流走查实例
拿“1路AI通道送到PID控制PWM输出”这条链路来走一遍,看整个数据流是怎么打通的:
adc_spi_master在SPI时钟域完成AD转换数据读取,将16bit结果写入async_fifo_16x1024。- 系统逻辑时钟(50MHz)从FIFO读侧把数据带出来,进入
channel_scale_filter,完成物理量纲转换(比如把原始码值变成工程单位数值)。 channel_scale_filter输出标定后的数值,通过内部AXI-Stream接口送往pid_controller模块的输入缓存。pid_controller模块内部有4路独立的PID算法实例,目标值由reg_bank下发,每路输出一个控制量。控制量经过限幅后,pid_controller将它转换为PWM占空比参数,通过标准接口写入pwm_gen`模块。pwm_gen根据占空比参数生成对应的PWM波形,送到物理引脚。
这条链路里,数据从SPI时钟域跳到系统时钟域,是通过异步FIFO完成的;从功能层到管理层,是通过AXI-Lite寄存器的读写完成的;从功能层到物理输出,是直接的数据通路完成的。整个工程不存在任何一根“裸奔”的跨域信号线。
7.3 运行时遇到的问题与调整方案
这个项目在调试中遇到的一个典型问题:16路ADC的SPI时序,在最初版本里全部由adc_spi_master按照最慢器件参数来驱动,结果导致整体采样率偏低,系统响应速度不满足控制需求。
我的调整方案是:把16路ADC分成4组,每组共享一个SPI总线但片选分开,利用SPI的全双工特性连续读。这个改动涉及接口层某个模块的时序设计,但并未影响任何功能层和管理层模块。因为接口层的变化被FIFO隔离了,功能层根本感觉不到底层时序的变化。
这其实也是三层框架设计的好处最直观的体现——底层改了,上层完全无感。如果把所有逻辑都揉在一层里,这种改动几乎不可能局部完成。
8. 我踩过的框架设计相关的坑与调整总结
框架设计没有标准答案,但有标准教训。我踩过不少坑,也修正过不少架构,挑几个比较有共性的说一说。
8.1 总想把所有模块做成通用IP导致过度设计
有一段时间我非常痴迷于把每个模块都设计成“万能IP”,不管项目需不需要,加了一堆可配置参数和灵活模式。后来发现这些参数在真实项目里90%都用不到,反而给验证和时序收敛增加了大量负担。
现在的做法是:第一次做,按当前需求设计,但预留标准化接口;第二次在别的项目里要用,再根据新需求抽象公共能力。迭代式复用比一次性过度设计健康得多。模块设计是一门适应项目、逐步提炼的艺术。
8.2 寄存器规划时没有考虑扩展性,后期改动成本翻倍
有个项目前期寄存器地址分配得太紧凑,预留空间不够。后来要加功能,发现可用地址被占满,只能挤占原有保留位,或者在总线上挂第二段地址空间。这两种方案都非常别扭,还会带来兼容性问题。
现在我的寄存器规划表至少预留50%的地址空间不分配,且每个功能模块固定分配一段地址区域,比如8路通道的控制寄存器,第一版只用了4路,也按8路的地址范围规划。这样后续扩展几乎不需要动地址映射表的结构。
8.3 跨时钟域方案太“随缘”,导致偶尔性故障无法复现
这个教训代价最大。早期的项目跨时钟域处理非常随性,有时候用两级同步器,有时候直接连,有时候觉得加个FIFO麻烦就用多bit寄存器。结果系统在实验室跑几天才出现一次数据错误,而且无法稳定复现。后来统一按前面说的方法做了跨时钟域方案评估和实现,同类问题基本绝迹。
CDC不是“怀疑有问题再去查”的问题,而是在设计初期就必须全部列出、逐个给出明确处理方案的问题。在我的代码注释里,每一个跨时钟域信号都会标注:源时钟域、目标时钟域、数据宽度、同步方案。这让评审和后续维护都轻松得多。
8.4 预留调试接口:逻辑分析仪和ILA的位置最好提前设计
很多工程师在调试FPGA时才想起来要加ILA(集成逻辑分析仪)或者外部逻辑分析仪引脚,结果发现信号没引出来,或者引脚分配全被占用,只能改约束重新综合布局布线,一改就是半小时起步。
我现在每个模块都预留一组调试信号输出,通过一个顶层调试MUX连接到一个固定的调试引脚区域。平时这些引脚悬空,一旦需要调试,只要改MUX选择信号就能看到任意模块内部的关键波形,而不需要动架构设计。这个习惯在复杂系统联调阶段省下的时间非常可观。
9. 给正在做或者准备做FPGA测控项目的人几条实操建议
框架、模块、数据流这套思路,听起来可能像一个大工程,但实际上它的起点非常简单。在你下一个项目里,我建议从这三件小事开始:
第一,开工前花半天时间,把系统里所有数据通路的走向画出来。哪怕只是拿草稿纸画几根带箭头的线,也算完成了数据流设计的第一步。
第二,模块的对外接口统一用标准总线,哪怕只是自己给自己定的内部标准。这样你的模块就有了可复用的基础。
第三,每个跨时钟域的接口都写清楚方案再动手,不要边写边想。写好设计方案大概花几小时,但省下的调试时间往往是几天甚至几周的级别。
我多年做测控程序最大的体会是:FPGA开发最怕的不是代码写不出来,而是写完以后系统行为不可控。框架思维的本质,其实就是让系统的每一个行为都有可追溯的原因。数据流理顺了,绝大多数“莫名其妙”的问题都能在设计表里找到对应位置,剩下的,只是按图施工的事。
希望这篇拆解对你有用。如果你正准备开始一个带采集和控制功能的FPGA项目,不妨把我上面提到的这些点对照着过一遍,把框架先立起来,再动手写代码。这样开发过程中的每个后续步骤,都会比之前顺手得多。