3. Verilog基础语法(上):模块结构、数据类型、assign语句、always块、阻塞与非阻塞赋值
各位同学,咱们今天开始啃Verilog语法这块硬骨头。说实话,很多新手一上来就被语法细节搞晕了。我当年也一样,对着书看了三天,写出来的代码还是跑不通。后来我发现,学语法不能死记硬背,得理解它背后的硬件逻辑。
你想想看,Verilog不是软件语言,它描述的是硬件电路。你写的每一行代码,最终都会变成实实在在的门电路、触发器和连线。带着这个思路去学,很多问题就迎刃而解了。
3.1 模块结构——Verilog的基本单元
模块(module)是Verilog中最基本的描述单位。说白了,一个模块就是一个硬件功能块。你可以把它想象成一个芯片,有输入引脚、输出引脚,内部有逻辑电路。
一个完整的模块结构长这样:
module counter (
input wire clk, // 时钟信号
input wire rst_n, // 复位信号,低电平有效
output reg [3:0] count // 4位计数器输出
);
// 内部逻辑写在这里
endmodule
这里有几个关键点:
- module/endmodule:模块的起始和结束,成对出现
- 端口列表:括号里定义模块的输入输出
- 端口方向:input、output、inout三种
- 数据类型:wire或reg,后面会细讲
模块内部可以包含很多东西:assign语句、always块、实例化其他模块等等。这些我们后面会逐一展开。
3.2 数据类型——wire和reg的区别
这是新手最容易搞混的地方。我刚开始学的时候,也纠结过「什么时候用wire,什么时候用reg」这个问题。
其实很简单,记住一句话:wire是连线,reg是变量。
| 特性 | wire | reg |
|---|---|---|
| 本质 | 物理连线 | 存储单元 |
| 赋值方式 | assign连续赋值 | always块内赋值 |
| 默认值 | 高阻态z | 不定态x |
| 综合结果 | 组合逻辑 | 组合逻辑或时序逻辑 |
举个例子:
wire a, b, c;
assign c = a & b; // c是a和b相与的结果,用wire
reg [7:0] data_reg;
always @(posedge clk) begin
data_reg <= data_in; // data_reg在时钟上升沿更新,用reg
end
3.3 assign语句——连续赋值
assign语句用来描述组合逻辑。它的特点是:只要输入变了,输出立刻跟着变。没有时钟,没有延迟,瞬间完成。
基本语法:
assign 信号名 = 表达式;
实际例子:
module adder (
input wire [3:0] a, b,
output wire [4:0] sum
);
assign sum = a + b; // 加法器,组合逻辑
endmodule
assign的右边可以是任何合法的表达式:算术运算、逻辑运算、位运算、条件运算符等等。
我个人特别喜欢用条件运算符(?:)写简单的多路选择器:
assign max = (a > b) ? a : b; // 取两个数中的较大值
这比写if-else简洁多了,而且综合出来的电路也更高效。
3.4 always块——过程赋值
always块是Verilog中最灵活、也最容易出错的语法结构。它既可以描述组合逻辑,也可以描述时序逻辑。
基本格式:
always @(敏感列表) begin
// 过程赋值语句
end
敏感列表决定了always块什么时候执行。常见的有两种:
- 电平敏感:@(*) 或 @(a or b or c) —— 用于组合逻辑
- 边沿敏感:@(posedge clk) 或 @(negedge rst_n) —— 用于时序逻辑
看两个例子:
// 组合逻辑:电平敏感
always @(*) begin
if (sel)
out = a;
else
out = b;
end
// 时序逻辑:边沿敏感
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
q <= 0;
else
q <= d;
end
3.5 阻塞赋值与非阻塞赋值
这是Verilog里最坑的地方,没有之一。我见过太多工程师在这里翻车了,包括我自己。
先看定义:
- 阻塞赋值(=):顺序执行,前面的赋值会阻塞后面的赋值
- 非阻塞赋值(<=):并行执行,所有赋值同时完成
听起来抽象?我们看个具体例子:
// 阻塞赋值
always @(posedge clk) begin
a = b;
c = a; // c得到的是a更新后的值,即b
end
// 非阻塞赋值
always @(posedge clk) begin
a <= b;
c <= a; // c得到的是a更新前的值,即a的旧值
end
看到区别了吗?阻塞赋值是「先算a,再算c」,非阻塞赋值是「同时算a和c,但用的是旧值」。
那实际项目中怎么用?我总结了一个黄金法则:
| 场景 | 推荐赋值方式 | 原因 |
|---|---|---|
| 时序逻辑(always @(posedge clk)) | 非阻塞赋值 <= | 避免竞争冒险,仿真行为正确 |
| 组合逻辑(always @(*)) | 阻塞赋值 = | 符合组合逻辑的传播特性 |
| assign语句 | 只能用 = | 语法规定,没得选 |
3.6 知识体系总览
说了这么多,我们来梳理一下本章的核心脉络。下面这张图可以帮助你建立整体认知:
这张图把本章的知识点串起来了。模块是骨架,数据类型是血肉,assign和always是两种描述逻辑的方式,而赋值方式则是决定电路行为的关键。
好了,这一章的内容就到这里。记住,学Verilog不是背语法,而是理解硬件。下次你写代码的时候,先想想:「这段代码综合出来是什么电路?」想明白了,语法自然就通了。
公众号:蓝海资料掘金营,微信deep3321