4. Verilog语法基础(上):模块结构、端口定义、assign语句、always块、阻塞与非阻塞赋值
好,咱们今天开始啃Verilog语法。说实话,很多新手一上来就被各种语法细节吓住了。我当年也是,看着一堆begin、end、assign,心里直犯嘀咕:这跟C语言长得像,但怎么用起来完全不是一回事?
别急。咱们今天不讲那些花里胡哨的,就讲四个最核心的东西:模块长什么样、端口怎么接、assign怎么用、always块怎么写。最后再聊聊那个让无数人翻车的阻塞与非阻塞赋值。
4.1 模块结构:Verilog的基本单元
Verilog里,所有东西都装在模块(module)里。你可以把模块想象成一个芯片:有引脚(端口),有内部电路(逻辑),有功能描述。
一个完整的模块长这样:
module counter (
input wire clk, // 时钟
input wire rst_n, // 复位,低有效
output reg [3:0] cnt // 4位计数器输出
);
// 内部逻辑写在这里
endmodule
嗯,这里要注意:module和endmodule是成对出现的,就像C语言里的花括号。我见过有人写了一大堆代码,最后忘了写endmodule,编译报错找了半天——这种低级错误,谁还没犯过呢?
4.2 端口定义:输入、输出、双向
端口就是模块跟外界打交道的通道。有三种类型:
| 端口类型 | 关键字 | 说明 |
|---|---|---|
| 输入 | input |
只能从外部读,内部不能赋值 |
| 输出 | output |
只能从内部驱动,外部不能赋值 |
| 双向 | inout |
既能读又能写,常用于总线 |
端口还有数据类型:wire(线网)和reg(寄存器)。简单记:
- wire:用
assign赋值,或者直接连模块端口 - reg:在
always块里赋值
我个人习惯:输入端口一律用wire,输出端口如果是在always里赋值就用reg,否则用wire。这样思路清晰,不容易乱。
output reg [7:0] data。这样写更简洁,我推荐新手这么用。
4.3 assign语句:组合逻辑的直通车
assign是Verilog里最直观的语句。说白了就是:左边等于右边,随时更新。它描述的是组合逻辑,没有时钟,没有边沿,信号一变就立刻反应。
assign led = button ? 1'b1 : 1'b0;
上面这行代码的意思是:如果button按下(高电平),led就亮;否则灭。硬件上就是一个多路选择器。
我在项目中遇到过一个问题:有人用assign给reg类型赋值,结果编译报错。记住,assign的左边必须是wire类型,不能是reg。
多个assign可以同时存在,它们之间是并行的,跟书写顺序无关。这一点跟软件完全不同,你想想看:硬件电路本来就是同时工作的,哪来的先后顺序?
4.4 always块:时序逻辑的根据地
always块是Verilog里最灵活、也最容易出问题的地方。它的基本格式:
always @(敏感列表) begin
// 要执行的语句
end
敏感列表决定了这个块什么时候被触发。常见的有两种:
- 组合逻辑:
always @(*)或always @(a or b or c) - 时序逻辑:
always @(posedge clk or negedge rst_n)
举个例子,一个带异步复位的D触发器:
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
q <= 1'b0;
else
q <= d;
end
这里要注意:always块里可以写if-else、case等语句,但不能写assign。而且,always块里赋值的变量必须是reg类型。
always块里对同一个变量赋值!这是硬件设计的大忌。我曾经调试一个项目,发现某个信号莫名其妙地变成X态,查了两天才发现是两个always块都在驱动它。
4.5 阻塞与非阻塞赋值:新手翻车重灾区
好,终于到了这个让无数人头疼的话题。Verilog里有两种赋值方式:
| 赋值方式 | 符号 | 特点 |
|---|---|---|
| 阻塞赋值 | = |
立即执行,顺序执行 |
| 非阻塞赋值 | <= |
并行执行,块结束时统一更新 |
说白了:
- 阻塞赋值(=):就像C语言,写完这句再写下一句
- 非阻塞赋值(<=):所有赋值同时发生,块结束时才生效
为什么会有这种区别?因为硬件是并行的。你想想看,一个寄存器在时钟上升沿同时采样多个信号,这些信号必须是同一时刻的值,不能有先后。
我给大家一个铁律:
千万别混!我见过有人把<=用在组合逻辑里,结果仿真波形怎么看怎么不对。还有人在时序逻辑里用=,综合出来的电路多了一堆没用的锁存器。
看个对比:
// 错误示例:时序逻辑用了阻塞赋值
always @(posedge clk) begin
a = b;
c = a; // 这里a已经是新值了,不是我们想要的
end
// 正确示例:时序逻辑用非阻塞赋值
always @(posedge clk) begin
a <= b;
c <= a; // 这里a还是旧值,符合硬件行为
end
第一个例子,因为阻塞赋值是顺序执行的,c拿到的是a的新值,相当于两级寄存器串在一起。第二个例子,c拿到的是a的旧值,两个寄存器是并行的。硬件上,这两种电路完全不同!
always块时,先想清楚这是组合逻辑还是时序逻辑。组合逻辑就用=,时序逻辑就用<=。养成习惯后,基本不会出错。
4.6 小结
今天咱们讲了四个基础知识点:
- 模块结构:module/endmodule是基本框架
- 端口定义:input/output/inout,wire/reg要分清
- assign语句:组合逻辑,左边必须是wire
- always块:时序逻辑的根据地,敏感列表要写对
- 阻塞与非阻塞:组合用=,时序用<=,这是铁律
这些是Verilog的骨架,后面的所有内容都建立在这之上。下一节咱们会讲运算符、条件语句和case语句,到时候你会发现,有了今天的基础,那些东西学起来就快多了。
嗯,今天就到这儿。记住,写Verilog不是写C语言,脑子里要时刻想着硬件电路。你写的是电路,不是程序。