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 做综合逻辑,因为它的位宽不明确,容易出问题。
2.3 参数化设计——让代码“活”起来
你想想看,如果每个模块的位宽都写死,那换个需求就得改代码,多麻烦。参数化设计就是解决这个问题的。
Verilog 里有两个关键词:parameter 和 localparam。
- 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 函数自动计算地址位宽,省得自己算。嗯,这个小技巧是我从前辈那学来的,特别好用。
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
2.5 本章知识体系
下面这张图,是我自己画的。它把本章的核心知识点串起来了。你仔细看看,就能明白模块、数据类型、参数、赋值语句之间的关系。
从这张图你能看到,所有内容都围绕 module 展开。端口是模块的“脸面”,数据类型是模块的“血肉”,参数化让模块“灵活”,赋值语句则是模块的“行为”。
好了,第二章就到这里。记住我今天讲的这些,后面写RISC-V的时候,你会感谢自己的。
公众号:蓝海资料掘金营,微信deep3321