2. Verilog基础语法:模块结构、数据类型、运算符、assign语句、always块、阻塞与非阻塞赋值
好,咱们正式开始啃Verilog这块硬骨头。说实话,很多初学者觉得Verilog就是写代码,跟C语言差不多。嗯,这个想法很危险。我在FPGA这行摸爬滚打十几年,见过太多把Verilog当C写的工程师,最后综合出来的电路自己都看不懂。
Verilog本质上是描述硬件的语言。你写的每一行代码,最终都会变成实实在在的门电路、触发器和连线。所以,咱们得用硬件工程师的思维去理解它。
核心思想:Verilog不是"执行"代码,而是"描述"电路。这个观念转不过来,后面会踩很多坑。
2.1 模块结构——FPGA设计的基本单元
模块(module)是Verilog最基本的构建块。一个完整的FPGA设计,就是由大大小小的模块拼装起来的。我个人习惯把模块想象成一个"黑盒子"——它有输入、有输出,内部干些什么活,外面不用管。
来看一个最简单的模块结构:
module counter (
input wire clk, // 时钟信号
input wire rst_n, // 复位信号,低电平有效
output reg [7:0] count // 8位计数器输出
);
// 内部逻辑写在这里
endmodule
模块的骨架就三部分:端口声明、内部逻辑、结束标志。端口声明里,我建议你养成好习惯——每个端口都写清楚是input还是output,是wire还是reg。我在项目中遇到过有人偷懒不写数据类型,结果综合时报了一堆warning,查了半天才发现是端口类型不匹配。
我的习惯:所有输入端口都用wire,所有输出端口如果是在always块里赋值就用reg,否则用wire。这样不容易出错。
2.2 数据类型——wire和reg的区别
Verilog里最常用的数据类型就两个:wire和reg。很多新手搞不清它们的区别,其实说白了很简单:
- wire:相当于一根导线。它只能被连续赋值(assign)驱动,不能存储值。
- reg:相当于一个寄存器。它可以在always块里被赋值,能保持上一次的值。
但要注意一个坑:reg不一定会被综合成寄存器!如果你在组合逻辑的always块里用reg,它最终只是根导线。我曾经在面试时问过这个问题,十个有八个答错。
看个例子:
wire a, b; // 两根导线
wire sum; // 导线
reg carry; // 寄存器(但未必是触发器)
// 连续赋值:只能用wire
assign sum = a ^ b;
// 组合逻辑always块:reg在这里只是导线
always @(*) begin
carry = a & b;
end
注意:reg在组合逻辑always块里不会生成触发器。只有当时钟沿触发的always块里,reg才会被综合成真正的寄存器。
2.3 运算符——跟C语言差不多,但有坑
Verilog的运算符大部分跟C语言一样,但有几个需要特别注意:
| 运算符类型 | 符号 | 说明 |
|---|---|---|
| 算术运算符 | + - * / % | 乘除法综合出来很耗资源,慎用 |
| 位运算符 | & | ~ ^ | 按位操作,常用 |
| 逻辑运算符 | && || ! | 返回1位布尔值 |
| 关系运算符 | > < >= <= | 注意<=也是非阻塞赋值 |
| 拼接运算符 | { } | 非常实用,比如{a, b} |
| 条件运算符 | ? : | 类似C的三目运算符 |
这里我要特别提一下除法。你想想看,除法在FPGA里有多贵?一个32位除法器综合出来可能要几百个LUT。我在做交易所行情解析时,遇到需要除法的场景,都尽量用移位或者查表代替。实在避不开,就用IP核。
2.4 assign语句——连续赋值
assign语句用来描述组合逻辑。它的意思是"左边的信号一直等于右边的表达式"。说白了,就是一根导线,只不过这根导线可以带点逻辑。
// 简单的组合逻辑
assign sum = a + b;
assign full = (count == 8'hFF) ? 1'b1 : 1'b0;
// 多位的拼接
assign {carry, result} = {a, b} + {c, d};
assign的左边必须是wire类型。我见过有人试图在assign左边放reg,编译直接报错。嗯,这个错误我也犯过,刚学的时候。
实用技巧:简单的组合逻辑用assign,复杂的用always块。assign写多了代码会变得很散,不好维护。
2.5 always块——时序逻辑和组合逻辑的容器
always块是Verilog里最强大的结构。它有两种主要用法:
组合逻辑always块:
always @(*) begin
// 敏感列表用*,表示所有输入信号变化都触发
case (sel)
2'b00: out = a;
2'b01: out = b;
2'b10: out = c;
2'b11: out = d;
endcase
end
时序逻辑always块:
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
count <= 8'h00;
else if (en)
count <= count + 1'b1;
end
这里有个关键点:组合逻辑的敏感列表用@(*),时序逻辑用时钟沿和复位沿。我曾经在项目中看到有人把组合逻辑的敏感列表写成@(a or b or c),结果漏了一个信号,仿真时死活不对。从那以后,我组合逻辑一律用@(*),省心。
2.6 阻塞与非阻塞赋值——最容易翻车的地方
好,重点来了。阻塞赋值(=)和非阻塞赋值(<=)的区别,是Verilog初学者最容易搞混的。我当年也在这上面栽过跟头。
阻塞赋值(=): 顺序执行,前面的赋值会立即影响后面的语句。用在组合逻辑always块里。
非阻塞赋值(<=): 并行执行,所有赋值在always块结束时同时更新。用时序逻辑always块里。
看个对比:
// 阻塞赋值——组合逻辑
always @(*) begin
temp = a & b;
out = temp | c; // 这里用的是更新后的temp
end
// 非阻塞赋值——时序逻辑
always @(posedge clk) begin
temp <= a & b;
out <= temp | c; // 这里用的是更新前的temp!
end
为什么会这样?因为非阻塞赋值在时钟沿来临时,先计算所有右边的值,然后统一赋给左边。所以第二个例子中,out用的是temp的旧值。
黄金法则:
- 组合逻辑always块用阻塞赋值(=)
- 时序逻辑always块用非阻塞赋值(<=)
- 绝对不要混用!
我曾经在调试一个交易所的UDP协议解析模块时,发现数据总是错位。查了两天,最后发现是一个always块里同时用了阻塞和非阻塞赋值。综合出来的电路跟仿真完全不一样。嗯,从那以后我写代码都会多看一眼赋值符号。
知识体系总览
下面这张图是我自己整理的Verilog基础语法知识结构,你可以把它当作学习路线图:
这张图把五个核心知识点串起来了。你学的时候可以对照着看,哪个环节薄弱就重点补哪个。
我的学习建议:别急着写复杂代码。先把模块结构、数据类型、阻塞非阻塞这三个基础打牢。这三个搞明白了,后面学状态机、FIFO什么的就顺了。
好了,这一章的内容就到这儿。记住,Verilog不是写代码,是画电路。你每写一行,都要想想它综合出来是什么样子。养成这个习惯,你离FPGA高手就不远了。
公众号:蓝海资料掘金营,微信deep3321