做过复杂FPGA项目的朋友应该都有这种体会:一个工程里几十个模块,相互之间信号错综复杂。改一个小功能,牵一发而动全身;加一个新需求,不知道从哪里下手。到最后调试的时候,一个信号不知道是哪个模块拉低的,仿真波形看得眼花缭乱。
这些问题的根源,很多时候就是模块划分没做好。
今天咱们就来聊聊:FPGA的模块划分到底应该怎么做?有哪些原则和方法,能让你的工程更清晰、更容易维护、也更容易复用?
一、先想清楚:为什么要划分模块?
很多新手写FPGA,习惯在一个顶层文件里把所有的always块、assign语句全部堆在一起。小工程没问题,一旦代码超过2000行,这种写法就会变成灾难:
-
修改一个功能,不知道会影响哪些地方
-
仿真调试时,信号铺天盖地,找不到关键节点
-
换个人接手代码,根本看不懂
-
想复用某部分功能,发现耦合太紧,剪不出来
所以模块划分的目的很简单:分而治之,降低复杂度。好的模块划分,能让每个模块功能单一、接口清晰、内部独立,就像积木一样,可以单独开发、单独仿真、单独测试,最后拼到一起就能跑。
二、模块划分的五个核心原则
原则一:功能单一,高内聚
一个模块只做一件事。比如"数据采集模块"只管从ADC读数据,"滤波模块"只管做FIR滤波,"存储模块"只管写DDR。不要做一个"全能模块",既管采集又管滤波还管存储,那样的模块内部逻辑纠缠不清,改一个功能可能影响其他功能。
判断标准:你能不能用一句话说清楚这个模块是做什么的?如果能,说明功能够单一;如果需要说三句话,大概率太复杂了,需要拆分。
原则二:接口清晰,低耦合
模块之间的连接信号越少越好。理想情况下,几个模块之间只有数据总线、握手信号(valid/ready)、和控制信号。
避免 :全局信号满天飞。很多工程师喜欢用wire把内部信号到处拉,顶层文件里几百根线,看着就头疼。更好的做法是用总线接口(比如AXI-Stream、AXI4-Lite)把多个信号打包,顶层只看到几个接口例化。
原则三:时钟和复位域独立
不同时钟域的模块一定要分开。比如一个模块跑100MHz,另一个模块跑200MHz,它们之间不能混在一起,中间必须加异步FIFO或握手桥接。
同理,不同复位信号的模块也建议分开。不要在顶层用一个全局复位把所有模块都挂上,那样会导致整个芯片在复位瞬间产生巨大的电流尖峰,而且不利于模块独立复位调试。
原则四:考虑复用性
如果一个模块将来可能用到别的项目里,设计时就尽量让它不依赖项目特定的参数。比如把数据位宽、FIFO深度、算法系数等做成可配置的参数(parameter),而不是写死。
另外,模块的输入输出接口尽量用标准的、通用的协议(AXI、AVALON、简单valid/ready),而不是自定义的、只有你自己看得懂的握手方式。
原则五:分层清晰
顶层不要放任何逻辑,只做例化和连线。第二层是"功能大块",比如数据通路、控制通路、接口桥接。第三层才是具体的功能模块。
这样做的好处是:你拿到一个新需求时,先看应该放在哪一层、哪个大块下面,而不是随便往顶层加信号。
三、一个实际的划分案例
假设我们要做一个数据采集系统:ADC进来数据,经过数字滤波,存入DDR,同时通过USB上传到电脑。
不好的划分:一个顶层文件里,ADC接口、滤波、DDR控制、USB控制全混在一起。
好的划分:
-
adc_interface:负责从ADC读取数据,做位对齐、格式转换,输出valid+data -
filter_module:接收adc_interface的数据,做滤波处理(可配置参数),输出滤波后数据 -
ddr_writer:接收滤波后数据,通过AXI接口写入DDR,带FIFO缓存 -
usb_uploader:从DDR读取数据,打包成USB格式发送 -
top:只负责例化上面四个模块,连接信号,以及一个简单的控制状态机
这样一来,任何一个模块需要修改,都不会影响其他模块。仿真时可以单独仿filter_module,不需要跑整个DDR和USB的模型。
四、模块划分中常见的坑
坑一:模块大小两极分化。有的模块只有几十行,有的模块几千行。太小的模块没必要独立,合并到上级即可;太大的模块一定要拆分。
坑二:模块间互相调用。比如模块A调用了模块B的某个内部信号,模块B又反过来用了模块A的。这种循环依赖会让设计无法综合,而且非常难调试。解决办法:把共用信号放到上一层,或者定义一个中间模块解耦。
坑三:忽略边界条件的模块划分。比如异步时钟域之间的信号直接跨模块传递,没有在接收模块做同步处理。正确的做法是:同步逻辑放在接收模块内部,让接口看起来是同步的。
坑四:过度划分。为了追求模块小而精,把本来逻辑简单、强相关的几部分拆成太多个小块,反而导致顶层连线复杂、例化代码冗长。适度就好。
五、实际工作中的建议
-
先画框图,再写代码。用Visio或者纸上画好模块、接口方向、数据流向,评审通过后再开写。花1小时画图,能省3小时重构。
-
保持一致性 。团队的代码风格、模块命名规则、接口信号命名规则要统一。比如所有模块的输出都用
xxx_o,输入用xxx_i,时钟用clk,复位用rst_n。 -
文档跟着代码走。每个模块开头写一段注释:功能描述、输入输出说明、参数列表、修改记录。不然半年后自己都不记得当初为什么这么分了。
由你创科技能做什么
我们由你创科技 在FPGA开发上承接了大量各类规模的项目,从小型接口桥接到大型多芯片协同处理系统。模块划分是我们每一个项目开始时必做的功课------不只是为了代码整洁,更是为了项目能按时交付、后期能维护迭代。
如果你正在规划一个新的FPGA项目,不确定怎么划分模块才合理,或者现有的代码已经乱成一团、改一个bug引出三个新bug,欢迎来找我们聊聊。我们可以帮你做架构梳理、模块重构,甚至把整个开发接过去。
毕竟,好的模块划分,是写出来的代码能一直用下去的基础;乱的模块划分,是每个接手的人都要骂娘的开端。