编码规范复盘:Verilog/VHDL编码风格、参数化设计、状态机编码规范、可综合与不可综合代码

各位同学好,我是老李。做FPGA这行十几年了,见过太多因为编码不规范导致的惨案。今天咱们聊聊编码规范,这玩意儿看着基础,但真能决定你项目的生死。

我刚开始带项目那会儿,有个同事写的代码,自己三天后就看不懂了。你想想看,一个团队里如果各写各的风格,那维护成本得多高?所以,规范不是束缚,是保护。

一、Verilog/VHDL编码风格:看着舒服,用着放心

编码风格这事儿,说白了就是「约定俗成」。我个人习惯用Verilog,但VHDL也有它的铁粉。不管用哪个,核心原则就一条:让别人能看懂,让工具能读懂。

命名规范

  • 信号命名:小写字母+下划线,比如 data_validclk_50m。别用驼峰,也别全大写,看着累。
  • 参数命名:大写字母+下划线,比如 DATA_WIDTHFIFO_DEPTH。一眼就能看出是常量。
  • 模块命名:首字母大写,比如 UartRxFifoController。模块名最好能反映功能。

重要原则:命名要自解释。别用 abtmp 这种名字。我见过有人用 signal_1signal_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设计的核心。我见过太多人把状态机写得乱七八糟,最后仿真没问题,上板就挂。这里我分享几个经验。

三段式状态机

我个人强烈推荐三段式写法:

  1. 第一段:时序逻辑,描述状态跳转
  2. 第二段:组合逻辑,描述下一状态
  3. 第三段:时序逻辑,描述输出
// 三段式状态机示例
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 连续赋值
  • caseif-else 条件语句
  • 寄存器类型(reg)和线网类型(wire)
  • 算术运算(+、-、*、/)——除法慎用

不可综合代码

  • initial 块(除了测试文件)
  • #delay 延时语句
  • fork-join 并行块
  • forcerelease 强制赋值
  • 文件操作($readmemh 等)
  • 动态数组、队列等高级数据结构

核心原则:可综合代码描述的是硬件结构,不是软件行为。你写代码的时候,脑子里要想着「这个语句会综合成什么电路」,而不是「这个语句会执行什么操作」。

五、知识体系总览

下面这张图是我自己总结的编码规范知识体系,你把它记在心里,写代码的时候对照着检查,能省不少事。

FPGA编码规范知识体系 编码规范 编码风格 参数化设计 状态机规范 可综合/不可综合 命名规范 代码布局 注释规范 位宽参数 深度参数 generate复用 三段式写法 编码方式选择 default处理 可综合语法 不可综合语法

这张图把编码规范的四个核心维度都串起来了。你写代码的时候,可以对照着检查:命名规范吗?参数化了吗?状态机写对了吗?有没有用不可综合的语法?

最后说一句:编码规范这事儿,不是一天两天能养成的。我建议你每次写完代码,花10分钟做一次「代码审查」,对照规范检查一遍。坚持一个月,你就会发现自己的代码质量上了一个台阶。

专注资料整理