编码规范复盘:Verilog/VHDL编码风格、参数化设计、状态机编码规范、可综合与不可综合代码
各位同学好,我是老李。做FPGA这行十几年了,见过太多因为编码不规范导致的惨案。今天咱们聊聊编码规范,这玩意儿看着基础,但真能决定你项目的生死。
我刚开始带项目那会儿,有个同事写的代码,自己三天后就看不懂了。你想想看,一个团队里如果各写各的风格,那维护成本得多高?所以,规范不是束缚,是保护。
一、Verilog/VHDL编码风格:看着舒服,用着放心
编码风格这事儿,说白了就是「约定俗成」。我个人习惯用Verilog,但VHDL也有它的铁粉。不管用哪个,核心原则就一条:让别人能看懂,让工具能读懂。
命名规范
- 信号命名:小写字母+下划线,比如
data_valid、clk_50m。别用驼峰,也别全大写,看着累。 - 参数命名:大写字母+下划线,比如
DATA_WIDTH、FIFO_DEPTH。一眼就能看出是常量。 - 模块命名:首字母大写,比如
UartRx、FifoController。模块名最好能反映功能。
重要原则:命名要自解释。别用 a、b、tmp 这种名字。我见过有人用 signal_1、signal_2... 这种命名,调试的时候简直想哭。
代码布局
代码排版这事儿,我建议用4空格缩进。别用Tab,不同编辑器Tab宽度不一样,一换环境代码就乱了。
// 好的风格
module counter #(
parameter WIDTH = 8
)(
input wire clk,
input wire rst_n,
input wire en,
output reg [WIDTH-1:0] count
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
count <= 'd0;
else if (en)
count <= count + 1'b1;
end
endmodule
看到没?begin-end对齐,赋值箭头对齐,端口声明对齐。这些细节看着不起眼,但能让你少掉很多头发。
二、参数化设计:一次编写,到处复用
参数化设计是我最想强调的一点。为什么?因为我在项目中吃过亏。有一次做个图像处理项目,数据位宽从8位改到10位,我硬是改了20多个模块。后来学乖了,全部参数化。
// 参数化FIFO设计
module fifo #(
parameter DATA_WIDTH = 8,
parameter ADDR_WIDTH = 4
)(
input wire clk,
input wire rst_n,
input wire wr_en,
input wire [DATA_WIDTH-1:0] wr_data,
input wire rd_en,
output reg [DATA_WIDTH-1:0] rd_data,
output wire full,
output wire empty
);
localparam DEPTH = 1 << ADDR_WIDTH;
// ... 内部实现
endmodule
我的习惯:所有可能变化的参数都提出来,包括数据位宽、地址位宽、FIFO深度、计数器最大值等。这样下次复用的时候,改几个参数就行,不用动逻辑。
三、状态机编码规范:别让状态机变成「状态机」
状态机是FPGA设计的核心。我见过太多人把状态机写得乱七八糟,最后仿真没问题,上板就挂。这里我分享几个经验。
三段式状态机
我个人强烈推荐三段式写法:
- 第一段:时序逻辑,描述状态跳转
- 第二段:组合逻辑,描述下一状态
- 第三段:时序逻辑,描述输出
// 三段式状态机示例
reg [1:0] state, next_state;
// 第一段:状态跳转
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
state <= IDLE;
else
state <= next_state;
end
// 第二段:下一状态逻辑
always @(*) begin
case (state)
IDLE: next_state = start ? READ : IDLE;
READ: next_state = done ? IDLE : READ;
default: next_state = IDLE;
endcase
end
// 第三段:输出逻辑
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
data_out <= 'd0;
else if (state == READ && data_valid)
data_out <= data_in;
end
避坑指南:我曾经在项目中用了一段式状态机,结果输出有毛刺,查了两天才发现是组合逻辑输出没寄存。从那以后,我再也不用一段式了。
状态编码方式
| 编码方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 二进制编码 | 节省寄存器 | 译码复杂 | 状态数多(>16) |
| 格雷码 | 相邻状态只变1位 | 编码复杂 | 跨时钟域 |
| 独热码 | 译码快,无毛刺 | 浪费寄存器 | 状态数少(<8) |
嗯,这里要注意:独热码虽然浪费资源,但综合工具优化得好,实际用起来并不差。我一般状态数少于8个就用独热码。
四、可综合与不可综合代码:别让仿真骗了你
这个问题太常见了。很多新手在仿真里跑得好好的,一综合就报错。为什么?因为写了不可综合的代码。
可综合代码
always @(posedge clk)或always @(*)assign连续赋值case、if-else条件语句- 寄存器类型(reg)和线网类型(wire)
- 算术运算(+、-、*、/)——除法慎用
不可综合代码
initial块(除了测试文件)#delay延时语句fork-join并行块force、release强制赋值- 文件操作(
$readmemh等) - 动态数组、队列等高级数据结构
核心原则:可综合代码描述的是硬件结构,不是软件行为。你写代码的时候,脑子里要想着「这个语句会综合成什么电路」,而不是「这个语句会执行什么操作」。
五、知识体系总览
下面这张图是我自己总结的编码规范知识体系,你把它记在心里,写代码的时候对照着检查,能省不少事。
这张图把编码规范的四个核心维度都串起来了。你写代码的时候,可以对照着检查:命名规范吗?参数化了吗?状态机写对了吗?有没有用不可综合的语法?
最后说一句:编码规范这事儿,不是一天两天能养成的。我建议你每次写完代码,花10分钟做一次「代码审查」,对照规范检查一遍。坚持一个月,你就会发现自己的代码质量上了一个台阶。