3. 硬件描述语言基础(Verilog):模块结构、数据类型、运算符、assign语句、always块、组合逻辑与时序逻辑
好,咱们进入正题。Verilog 这玩意儿,说白了就是硬件工程师的“普通话”。你写出来的每一行代码,最终都会变成实实在在的门电路、触发器和连线。这跟写软件完全是两码事——软件是跑在 CPU 上的指令,而 Verilog 描述的是硬件本身的结构和行为。
我个人习惯把 Verilog 看作“画电路图的另一种方式”。你想想看,画原理图时,你需要芯片(模块)、导线(连线)、逻辑门(运算)。Verilog 里,这些东西都有对应的语法。今天咱们就把最核心的几个概念捋清楚。
3.1 模块结构:你的第一个“芯片”
一个 Verilog 模块,就是一个硬件单元。小到一个与门,大到整个 CPU,都可以用一个模块来描述。模块的基本骨架长这样:
module 模块名 (
input 端口1,
input 端口2,
output 端口3
);
// 内部逻辑写在这里
endmodule
嗯,这里要注意:module 和 endmodule 是成对出现的,就像括号一样,千万别漏了。端口列表里,input、output 定义了信号的方向。还有一个 inout,双向端口,用的少,但做总线接口时会碰到。
我在项目中遇到过新手把端口方向搞反,结果仿真怎么都不对。查了半天,原来是输出信号写成了 input。这种低级错误,一查一个准。
3.2 数据类型:线网 vs 变量
Verilog 的数据类型,主要就两类:wire 和 reg。很多人一开始搞不清它们的区别,我当年也迷糊过。
- wire(线网):代表物理连线。它本身不存储值,只是把驱动端的值传递过来。比如 assign 语句的输出,或者模块端口的连接,都用 wire。
- reg(寄存器):代表存储单元。它可以在 always 块里被赋值,并且会保持上一次的值,直到被新的赋值覆盖。
但是!这里有个大坑:reg 不一定真的综合成寄存器。如果你在 always 块里写的是组合逻辑,那 reg 综合出来可能只是一堆门电路,而不是触发器。说白了,reg 是语法上的“变量”,不是硬件上的“寄存器”。
我建议你一开始就这么记:
| 使用场景 | 数据类型 |
|---|---|
| assign 语句左侧 | wire |
| always 块内被赋值的信号 | reg |
| 模块端口连接 | wire(默认) |
另外,还有 integer、time 这些,但做逻辑设计时用得少。咱们先把 wire 和 reg 玩明白。
3.3 运算符:跟 C 语言差不多,但有区别
Verilog 的运算符,大部分跟 C 语言一样。比如 +、-、*、/、&、|、^、~。但有几个地方要特别小心:
- 位宽问题:两个 4 位宽的数相加,结果默认是 4 位宽,高位会被截断。如果你想保留进位,必须把结果定义成 5 位宽。
- 拼接运算符
{}:这个很实用。比如{a, b}可以把两个信号拼在一起。我经常用它来组合数据总线。 - 三目运算符
?::写组合逻辑时,可以替代简单的 if-else,代码更简洁。
举个例子:
wire [3:0] a, b;
wire [4:0] sum;
assign sum = a + b; // 注意:a+b 是 4 位,赋值给 5 位的 sum,高位补 0
我曾经在做一个加法器时,忘了考虑进位,结果仿真结果总是差一点。后来用 {1'b0, a} + {1'b0, b} 才搞定。嗯,细节决定成败。
3.4 assign 语句:连续赋值,组合逻辑的基石
assign 语句,说白了就是“只要输入变了,输出立刻跟着变”。它描述的是组合逻辑,没有时钟,没有边沿。比如:
assign y = (a & b) | c;
这行代码综合出来就是一个与或门。只要 a、b、c 任何一个变化,y 会立即更新。注意,assign 的左边必须是 wire 类型。
3.5 always 块:时序逻辑与组合逻辑的“万能工具”
always 块是 Verilog 里最灵活、也最容易出错的语法。它的基本格式是:
always @(敏感列表) begin
// 逻辑代码
end
敏感列表决定了这个块什么时候执行。常见的有两种:
- 组合逻辑:敏感列表里写所有输入信号,比如
@(a or b or c)。或者用@(*)自动推导,我强烈推荐用这个,省事又安全。 - 时序逻辑:敏感列表里写时钟边沿和复位,比如
@(posedge clk or negedge rst_n)。
这里有个经典错误:在同一个 always 块里,既写组合逻辑又写时序逻辑。我见过有人把 always @(posedge clk) 里的赋值,跟 always @(*) 里的赋值混在一起,结果综合出来一堆奇怪的锁存器。记住:组合逻辑和时序逻辑,最好分开写。
3.6 组合逻辑 vs 时序逻辑:本质区别
这两个概念,是数字设计的核心。我简单总结一下:
| 特性 | 组合逻辑 | 时序逻辑 |
|---|---|---|
| 输出依赖 | 只依赖当前输入 | 依赖当前输入和过去状态 |
| 存储能力 | 无 | 有(触发器) |
| 时钟 | 不需要 | 需要 |
| 典型写法 | assign 或 always @(*) | always @(posedge clk) |
举个例子,一个 D 触发器:
// 时序逻辑:在时钟上升沿,把 d 的值传给 q
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
q <= 1'b0;
else
q <= d;
end
而组合逻辑的写法:
// 组合逻辑:q 等于 d 取反
assign q = ~d;
你想想看,如果我把 q <= d 写成 q = d,会怎样?在时序逻辑里,用阻塞赋值 = 会导致仿真结果完全错误,甚至综合出意想不到的电路。这就是为什么我反复强调:时序逻辑用非阻塞赋值 <=,组合逻辑用阻塞赋值 =。这是铁律,别问为什么,先记住。
3.7 知识体系总览
为了让你更直观地理解这些概念之间的关系,我画了一张图。它展示了 Verilog 基础的核心脉络:从模块结构出发,到数据类型、运算符,再到两种主要的逻辑描述方式(assign 和 always),最后落到组合逻辑与时序逻辑的区别上。
这张图把今天讲的内容串起来了。你写任何 Verilog 代码,都逃不出这个框架。先确定模块结构,再选数据类型,然后用 assign 或 always 描述行为,最后搞清楚你写的是组合逻辑还是时序逻辑。思路清晰了,代码就不会乱。
好了,这一章的内容就到这儿。记住:Verilog 不是软件语言,它是硬件的“图纸”。每一行代码,都要想清楚它对应什么电路。多练、多仿真、多上板,慢慢就有感觉了。