坏味道识别:过长模块、重复代码、过度耦合、命名混乱、魔法数字
各位同学,今天我们来聊聊代码的「坏味道」。
这个词不是我发明的,是《重构》那本书里来的。说白了,就是代码里那些让你看着不舒服、改着很痛苦的地方。我做了十几年FPGA,见过太多这样的代码了。嗯,今天就把最常见的五种坏味道拎出来,一个个说清楚。
一、过长模块:一个文件写几千行,你怕不怕?
我刚开始做FPGA那会儿,接过一个前辈留下的项目。一个.v文件,整整3800行。我打开的时候,编辑器都卡了一下。你想想看,一个模块里,从复位逻辑到状态机,从数据通路到调试接口,全塞在一起。
为什么会这样?很多人觉得「反正都是这个模块的功能,写在一起方便」。但实际维护起来,你根本找不到哪里是哪里。改一个信号,要翻好几屏才能确认它有没有被其他地方用到。
过长模块的典型特征:
- 一个模块超过500行(我个人建议300行就是上限了)
- 一个always块里同时处理多个不相关的逻辑
- 一个模块里既有控制逻辑,又有数据通路,还有调试接口
我记得有一次,为了修一个时序违例,我在那个3800行的文件里找了整整两天。最后发现,是一个很早期的信号定义和后面的逻辑对不上。如果当时拆成几个小模块,这种问题一眼就能看出来。
我的建议:每个模块只做一件事。控制逻辑单独一个模块,数据通路单独一个模块。调试接口?用generate条件编译包起来,或者单独放一个文件。
二、重复代码:Ctrl+C/V 一时爽,维护火葬场
重复代码,这可能是FPGA工程师最容易犯的毛病。为什么?因为很多逻辑看起来「差不多」。
比如,一个8位的计数器,你写了8个一模一样的always块,只是位宽不同。或者,一个状态机,你在三个不同的地方写了几乎一样的跳转逻辑。
// 坏味道示例:重复的计数器逻辑
always @(posedge clk) begin
if (rst) counter_0 <= 8'd0;
else if (en_0) counter_0 <= counter_0 + 1'b1;
end
always @(posedge clk) begin
if (rst) counter_1 <= 8'd0;
else if (en_1) counter_1 <= counter_1 + 1'b1;
end
// 重复了8次... 你想想看,如果改成16位,要改8个地方
我曾经在一个项目里,看到有人把同样的FIFO读写逻辑复制了6份。后来需求变了,要加一个水线寄存器。改完第3份的时候,他自己都忘了第4份在哪里了。
避坑指南:我曾经因为重复代码,在仿真时漏改了一个地方,导致板级调试时数据一直对不上。花了三天才找到原因。从那以后,我只要看到重复超过两次的代码,就立刻用generate或者函数包起来。
三、过度耦合:牵一发而动全身
过度耦合,说白了就是模块之间「你中有我,我中有你」。一个模块改了接口,另外五个模块都得跟着改。
我见过最夸张的例子:一个顶层模块,直接把内部寄存器的地址暴露给了外部接口。外部要读某个状态,得知道这个寄存器在哪个子模块的哪个层级下面。后来那个子模块重构了,地址变了,整个系统的驱动软件都得重写。
过度耦合的典型表现:
- 模块之间直接传递内部状态信号,而不是经过标准接口
- 一个模块的修改,导致多个模块需要同步修改
- 模块A的代码里,直接引用了模块B的内部寄存器地址
我个人习惯,模块之间的接口越简单越好。能用valid-ready握手的,就别传复杂的状态信号。能用AXI-Stream的,就别自己定义一堆乱七八糟的使能信号。
四、命名混乱:a、b、c、tmp,你是在写密码吗?
命名这件事,看起来小,但影响特别大。我见过有人用a、b、c、d作为信号名,然后注释里写「a是使能信号,b是数据信号」。你想想看,三个月后他自己回来看,还记得a是什么吗?
// 坏味道示例:命名混乱
wire a, b, c, d;
reg [7:0] tmp1, tmp2;
wire [3:0] sig1, sig2, sig3;
// 你能猜出这些信号是干什么的吗?
我建议的命名原则:
- 信号名要能自解释:比如
fifo_wr_en就比wen好 - 统一前缀/后缀:所有时钟用
_clk结尾,所有复位用_rst_n结尾 - 避免缩写过度:
cnt可以,但ctr就有点模糊了
我的小技巧:命名的时候,想象一下三个月后的自己。如果那个自己看到这个名字,能在一秒钟内猜出它的用途,那就合格了。
五、魔法数字:3.14159?还是 42?
魔法数字,就是代码里直接出现的、没有解释的常数。比如 if (counter == 8'd42),这个42是什么?是超时时间?是数据长度?还是某个状态编号?
// 坏味道示例:魔法数字
always @(posedge clk) begin
if (counter == 8'd100) begin // 100是什么?
state <= 3'd4; // 4又是什么?
end
end
// 改进后:用参数代替
parameter TIMEOUT_CYCLES = 8'd100;
parameter STATE_DONE = 3'd4;
always @(posedge clk) begin
if (counter == TIMEOUT_CYCLES) begin
state <= STATE_DONE;
end
end
我记得有一次,一个同事在代码里写了 if (data == 16'hA5A5)。我问他是干什么的,他说「这是帧头」。我说那你为什么不定义一个 FRAME_HEADER 参数?他说「反正就一个地方用」。结果后来帧头改了,他找了半天才找到那个16'hA5A5。
避坑指南:我曾经在一个项目里,因为魔法数字导致仿真和综合结果不一致。一个 8'd255 被误写成了 8'd256,综合工具直接截断成了0。如果当时用参数定义,这种低级错误根本不会发生。
总结一下
这五种坏味道,其实都有一个共同点:它们让代码变得难以理解和维护。
你想想看,FPGA开发本身就不容易,时序、面积、功耗,哪个不是硬骨头?如果代码本身还乱七八糟,那调试起来简直是雪上加霜。
我个人习惯,每次写完代码,都会花10分钟做一次「坏味道自查」:
- 这个模块是不是太长了?能不能拆?
- 有没有重复的代码?能不能用generate?
- 模块之间的耦合是不是太紧了?接口能不能简化?
- 信号命名是不是清晰?三个月后我还能看懂吗?
- 有没有魔法数字?能不能用参数代替?
嗯,今天就先聊到这里。下一节,我们讲讲怎么用工具自动检测这些坏味道。