3. 硬件描述语言基础(Verilog):模块结构、数据类型、运算符、assign语句、always块
好,咱们进入正题。Verilog 这玩意儿,说白了就是用来描述数字电路的。你把它想象成画电路图的“文字版”就行。我刚开始学的时候,总觉得它跟 C 语言很像,结果写出来的东西综合出来一堆莫名其妙的 latch,那叫一个惨。所以,咱们得先把它当硬件来理解,别当软件写。
3.1 模块结构:CPLD 设计的“积木块”
一个 Verilog 设计,最核心的就是 module。你可以把它看作一个黑盒子,有输入、有输出,里面装着你要实现的逻辑。
基本结构长这样:
module 模块名 (
端口1,
端口2,
...
);
// 端口声明
input 端口1;
output 端口2;
inout 双向端口;
// 内部信号声明
wire 内部连线;
reg 内部寄存器;
// 逻辑功能描述
// assign 语句、always 块、门级实例化等
endmodule
我个人习惯,模块名用下划线分隔的小写字母,比如 spi_controller。端口列表里,每个端口一行,加上注释,这样后期维护起来,你看着也舒服。
3.2 数据类型:wire 和 reg,别搞混了
这是新手最容易踩坑的地方。我见过太多人把 wire 和 reg 用错,综合出来一堆意想不到的玩意儿。
- wire:代表一根物理连线。它只能被
assign语句或者模块实例的端口驱动。说白了,它就是个“被动”的信号,你不能在 always 块里给它赋值。 - reg:代表一个寄存器或者一个变量。它可以在
always块里被赋值。注意,它不一定会被综合成寄存器,也可能是组合逻辑。
举个例子:
wire a, b; // 两根连线
wire sum; // 一根连线,用来表示 a + b 的结果
reg clk; // 一个寄存器,用来模拟时钟
reg [3:0] counter; // 一个 4 位的寄存器,用来计数
// assign 语句驱动 wire
assign sum = a + b;
// always 块驱动 reg
always @(posedge clk) begin
counter <= counter + 1'b1;
end
避坑指南: 我曾经在做一个 SPI 接口时,把一个 wire 类型的信号放在了 always 块的赋值左边,结果综合器报了一堆错。记住,wire 只能被 assign 驱动,reg 只能在 always 或 initial 里赋值。
3.3 运算符:跟 C 语言差不多,但有坑
Verilog 的运算符跟 C 语言很像,但有几个地方要特别注意。
| 类别 | 运算符 | 说明 |
|---|---|---|
| 算术 | + - * / % | 加减乘除取模。注意,除法综合出来很费资源,尽量少用。 |
| 位运算 | & | ~ ^ | 按位与、或、非、异或。这个在 CPLD 里用得非常多。 |
| 逻辑运算 | && || ! | 逻辑与、或、非。结果只有 0 或 1。 |
| 关系运算 | > < >= <= == != | 比较大小、相等、不等。结果也是 0 或 1。 |
| 移位 | << >> | 左移、右移。左移相当于乘 2,右移相当于除 2。 |
| 拼接 | { } | 把多个信号拼成一个宽的信号。比如 {a, b}。 |
| 条件 | ? : | 三目运算符,相当于 if-else 的简写。 |
注意: 位运算和逻辑运算的区别。比如 a & b 是把 a 和 b 的每一位进行与操作,结果是一个多位宽的信号。而 a && b 是把 a 和 b 当成布尔值,结果只有 1 位。我刚开始就经常搞混,写出来的逻辑完全不对。
3.4 assign 语句:组合逻辑的“直通车”
assign 语句用来描述组合逻辑。它相当于把右边的表达式“硬连线”到左边的 wire 上。只要右边表达式的值变了,左边立刻跟着变。
// 一个简单的 2 选 1 多路选择器
module mux2to1 (
input a,
input b,
input sel,
output y
);
assign y = sel ? b : a;
endmodule
你看,这个 assign 语句,用三目运算符实现了一个多路选择器。简洁明了。我个人习惯,所有简单的组合逻辑都用 assign 搞定,代码看起来清爽。
3.5 always 块:时序逻辑的“心脏”
always 块是 Verilog 里最强大的结构,也是最容易出问题的地方。它用来描述时序逻辑(比如触发器、计数器)和复杂的组合逻辑。
基本语法:
always @(敏感列表) begin
// 逻辑描述
end
敏感列表 决定了 always 块什么时候执行。常见的有:
always @(posedge clk):在时钟的上升沿执行,用来描述时序逻辑。always @(negedge clk):在时钟的下降沿执行。always @(a or b or sel):当 a、b、sel 中任何一个变化时执行,用来描述组合逻辑。
举个例子,一个带异步复位的 D 触发器:
module dff (
input clk,
input rst_n,
input d,
output reg q
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
q <= 1'b0;
else
q <= d;
end
endmodule
这里有几个关键点:
- 敏感列表里同时有
posedge clk和negedge rst_n,表示时钟上升沿或复位下降沿都会触发。 - 复位是低电平有效(
rst_n),所以用negedge。 - 赋值用的是
<=(非阻塞赋值),这是时序逻辑的标准写法。
避坑指南: 我曾经在写一个状态机时,把组合逻辑和时序逻辑混在同一个 always 块里,结果综合出来一堆奇怪的 latch。记住,描述时序逻辑用 posedge clk,描述组合逻辑用 @(*) 或列出所有输入信号。别混着来。
3.6 知识体系结构图
下面这张图,把本章的核心知识点串起来了。你一看就明白,Verilog 的模块结构、数据类型、运算符、assign 和 always 块之间是什么关系。
嗯,这张图把本章的内容都串起来了。你写代码的时候,脑子里要有这个结构图,就知道每个部分该放什么。
- module 是基本单元,端口是它的“接口”。
- wire 用于连线,reg 用于寄存器或变量。
- 运算符跟 C 语言类似,但注意位运算和逻辑运算的区别。
- assign 描述组合逻辑,always 描述时序逻辑或复杂组合逻辑。
- 时序逻辑用非阻塞赋值
<=,组合逻辑用阻塞赋值=。
好了,这一章的内容就到这儿。记住,Verilog 是描述硬件的语言,不是写软件的。多写、多仿真、多看看综合出来的 RTL 图,慢慢就有感觉了。