Verilog语法核心(一):模块与端口声明,常用数据类型,参数化设计,赋值语句

各位同学,欢迎来到第二章。说实话,每次讲Verilog语法,我都有点感慨。很多初学者觉得语法枯燥,恨不得直接上手写RISC-V。但我要告诉你——语法是地基,地基不稳,后面写出来的代码全是雷。我自己带过的项目里,至少有一半的bug,追根溯源都是语法层面的问题。

好,咱们今天就把这四大块啃下来:模块与端口、常用数据类型、参数化设计、赋值语句。我保证,讲完这些,你就能看懂大部分Verilog代码了。

2.1 模块与端口声明——芯片设计的“黑盒子”

Verilog里,module 就是最基本的单元。你可以把它想象成一个黑盒子,有输入、有输出,内部实现什么功能,外面不用管。

我习惯把模块声明写成这样:

module counter (
    input  wire       clk,      // 时钟
    input  wire       rst_n,    // 复位,低有效
    input  wire       en,       // 使能
    output reg  [7:0] count     // 8位计数器输出
);
    // 内部逻辑
endmodule

这里有几个要点:

  • 端口方向:input、output、inout。inout我建议新手少用,双向口处理不好容易出竞争。
  • 端口类型:wire 或 reg。注意,input 只能是 wire,output 可以是 wire 或 reg。
  • 位宽声明:[7:0] 表示8位,从高位到低位。我个人习惯统一用 [N-1:0] 的写法,看着顺眼。
小技巧:我写模块时,习惯把时钟和复位放在端口列表的最前面。这样别人一看就知道时序逻辑的驱动源,代码可读性会好很多。

2.2 常用数据类型——wire 和 reg 到底怎么选?

这个问题,我几乎每次面试都会问。很多新手搞不清 wire 和 reg 的区别。说白了:

  • wire:连线。它不存储值,只是把两个点连起来。assign 语句赋值的目标必须是 wire。
  • reg:寄存器。它存储值,在 always 块里被赋值。注意,reg 不一定会综合成寄存器,也可能是组合逻辑。

举个例子:

wire a, b, c;
assign c = a & b;   // c 是 wire,用 assign 驱动

reg [3:0] d;
always @(posedge clk) begin
    d <= d + 1;      // d 是 reg,在 always 里赋值
end

还有一个数据类型叫 integer。它默认是32位有符号数,常用于循环变量或测试代码。我在项目中很少用 integer 做综合逻辑,因为它的位宽不明确,容易出问题。

注意:我曾经在一个项目里,用 integer 做计数器,结果综合出来的面积比预期大了三倍。后来换成 reg [7:0] 就正常了。所以,能不用 integer 就别用,除非你很清楚自己在做什么。

2.3 参数化设计——让代码“活”起来

你想想看,如果每个模块的位宽都写死,那换个需求就得改代码,多麻烦。参数化设计就是解决这个问题的。

Verilog 里有两个关键词:parameterlocalparam

  • parameter:可以在模块实例化时被外部修改。适合做可配置参数。
  • localparam:只能在模块内部使用,外部不能改。适合做内部常量。

看个例子:

module fifo #(
    parameter DATA_WIDTH = 8,
    parameter DEPTH      = 16
) (
    input  wire                 clk,
    input  wire                 rst_n,
    input  wire                 wr_en,
    input  wire [DATA_WIDTH-1:0] wr_data,
    // ... 其他端口
);
    localparam ADDR_WIDTH = $clog2(DEPTH);  // 地址位宽自动计算
    reg [DATA_WIDTH-1:0] mem [0:DEPTH-1];  // 存储阵列
    // ...
endmodule

这里我用 $clog2 函数自动计算地址位宽,省得自己算。嗯,这个小技巧是我从前辈那学来的,特别好用。

核心思想:参数化设计让你的模块像乐高积木一样,可以灵活拼装。写RISC-V内核时,参数化更是必不可少——比如数据位宽、寄存器数量、Cache大小,全都可以通过参数配置。

2.4 赋值语句——assign 和 always 的“分工”

Verilog 里赋值有两种方式:连续赋值过程赋值

2.4.1 assign——连续赋值

assign 用于组合逻辑,它右边的表达式一旦变化,左边立即更新。说白了,就是一根导线。

assign sum = a + b;
assign full = (count == MAX_VAL);

我习惯把简单的组合逻辑用 assign 写,清晰明了。

2.4.2 always——过程赋值

always 块可以描述组合逻辑,也可以描述时序逻辑。区别在于敏感列表:

  • 组合逻辑:always @(*) 或 always @(a or b)
  • 时序逻辑:always @(posedge clk or negedge rst_n)

举个例子:

// 组合逻辑:多路选择器
always @(*) begin
    case (sel)
        2'b00: out = a;
        2'b01: out = b;
        2'b10: out = c;
        default: out = 0;
    endcase
end

// 时序逻辑:带复位的计数器
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        count <= 0;
    else if (en)
        count <= count + 1;
end
避坑指南:我曾经犯过一个低级错误——在组合逻辑的 always 块里用了非阻塞赋值 (<=)。结果仿真和综合结果不一致,查了两天才发现。记住:组合逻辑用阻塞赋值 (=),时序逻辑用非阻塞赋值 (<=)。这是铁律。

2.5 本章知识体系

下面这张图,是我自己画的。它把本章的核心知识点串起来了。你仔细看看,就能明白模块、数据类型、参数、赋值语句之间的关系。

Verilog 模块 (module) 端口声明 (input/output/inout) 数据类型:wire / reg / integer 参数化设计:parameter / localparam 赋值语句:assign / always 连续赋值 (assign) 过程赋值 (always) 组合/时序逻辑 图:Verilog 语法核心知识体系

从这张图你能看到,所有内容都围绕 module 展开。端口是模块的“脸面”,数据类型是模块的“血肉”,参数化让模块“灵活”,赋值语句则是模块的“行为”。

好了,第二章就到这里。记住我今天讲的这些,后面写RISC-V的时候,你会感谢自己的。


公众号:蓝海资料掘金营,微信deep3321