3. 硬件描述语言基础(Verilog):模块结构、数据类型、运算符与赋值
各位同学,咱们今天聊聊Verilog。说实话,很多初学者一上来就被各种语法吓住了。其实没那么复杂。你把它想象成画电路图,只不过是用文字来描述而已。我刚开始学的时候,也是对着代码发懵,后来发现——嗯,只要抓住几个核心概念,剩下的都是套路。
3.1 模块结构——Verilog的基本单元
Verilog里最重要的东西就是模块(module)。一个模块就是一个硬件单元。你可以把它想象成一个黑盒子,有输入、有输出,里面装着逻辑。
模块的基本结构长这样:
module 模块名 (
端口列表
);
// 端口声明
// 内部信号声明
// 逻辑功能描述
endmodule
举个例子,一个最简单的与门:
module and_gate (
input a,
input b,
output y
);
assign y = a & b;
endmodule
你看,是不是很直观?input和output就是告诉工具,哪些是输入,哪些是输出。我个人习惯把端口列表写在括号里,这样一眼就能看清模块的接口。
3.2 数据类型——wire和reg
Verilog里最常用的两种数据类型就是wire和reg。很多新手搞不清它们的区别。我简单说:
- wire:相当于一根导线。它只能被连续赋值(assign)驱动。说白了,就是组合逻辑的输出。
- reg:相当于一个存储单元。它只能在always块里被赋值。说白了,就是时序逻辑的寄存器。
但注意,reg不一定会综合成寄存器。如果你在always块里写组合逻辑,reg综合出来可能只是一堆门电路。嗯,这里要小心。
举个例子:
wire sum; // 组合逻辑信号
reg counter; // 时序逻辑信号
assign sum = a + b; // wire用assign驱动
always @(posedge clk) begin
counter <= counter + 1; // reg在always块里赋值
end
3.3 运算符——别怕,都是套路
Verilog的运算符和C语言很像。你如果写过C,上手会很快。我整理了一张表:
| 类别 | 运算符 | 说明 |
|---|---|---|
| 算术 | + - * / % | 加减乘除取模 |
| 逻辑 | && || ! | 与或非(返回1位) |
| 按位 | & | ~ ^ | 按位与、或、非、异或 |
| 关系 | > < >= <= == != | 比较运算 |
| 移位 | << >> | 左移、右移 |
| 拼接 | { } | 把多个信号拼在一起 |
举个例子,拼接运算符特别实用:
wire [3:0] a = 4'b1010;
wire [3:0] b = 4'b0101;
wire [7:0] c = {a, b}; // c = 10100101
你想想看,如果不用拼接,你得写多少行代码?
3.4 assign语句——连续赋值
assign是组合逻辑的标配。它的意思是:只要右边信号变了,左边立刻更新。说白了,就是一根导线。
assign y = a & b | c;
注意,assign的左边必须是wire类型。右边可以是任何表达式。我在项目中经常用assign做数据选择:
assign data_out = sel ? data_a : data_b;
这个三目运算符,其实就是个多路选择器。综合工具会自动生成对应的门电路。
3.5 always块——时序逻辑的核心
always块是Verilog里最灵活、也最容易出错的地方。它的基本格式是:
always @(敏感列表) begin
// 逻辑代码
end
敏感列表决定了什么时候执行块里的代码。常见的写法有:
always @(posedge clk):时钟上升沿触发(时序逻辑)always @(a or b or c):信号变化触发(组合逻辑)always @(*):所有输入信号变化触发(推荐写法)
举个例子,一个D触发器:
always @(posedge clk) begin
q <= d;
end
你看,这就是一个寄存器。时钟上升沿时,把d的值锁存到q里。
3.6 阻塞赋值与非阻塞赋值——最容易踩的坑
这个问题,我当年学的时候也迷糊过。简单说:
- 阻塞赋值(=):顺序执行。上一条赋值完,才执行下一条。适合组合逻辑。
- 非阻塞赋值(<=):并行执行。所有赋值同时发生。适合时序逻辑。
看个例子就明白了:
// 阻塞赋值(组合逻辑)
always @(*) begin
a = b;
c = a; // c得到的是b的新值
end
// 非阻塞赋值(时序逻辑)
always @(posedge clk) begin
a <= b;
c <= a; // c得到的是a的旧值
end
为什么会这样?因为非阻塞赋值是在时钟沿到来时,先计算右边的值,然后统一更新左边。所以c拿到的是a更新前的值。
3.7 知识体系总览
为了让你更直观地理解本章内容,我画了一张图:
这张图把本章的知识点串起来了。你从中心往外看,每个分支都是一个独立的概念,但它们又相互关联。比如,assign只能驱动wire,always块里只能用reg。这些关系搞清楚了,Verilog就算入门了。
好了,这一章就到这里。记住:多写代码,多仿真,多踩坑。经验就是这么积累出来的。