3. Verilog基础语法(上):模块结构、数据类型、assign语句、always块、阻塞与非阻塞赋值
好,咱们正式开始写代码了。
这一章是Verilog的根基。说白了,你后面所有的图像加速模块——从简单的像素操作到复杂的卷积加速器——都得靠这些基础语法搭起来。我见过不少新手一上来就急着写复杂逻辑,结果综合出来一堆莫名其妙的错误。嗯,咱们还是先把地基打牢。
3.1 模块结构——Verilog的“黑盒子”
Verilog里最基本的单元叫模块(module)。你可以把它想象成一个黑盒子:有输入、有输出,里面装着你要实现的逻辑。
一个完整的模块长这样:
module led_controller (
input wire clk, // 时钟信号
input wire rst_n, // 复位信号(低有效)
input wire [7:0] brightness, // 亮度值
output reg [3:0] led // 4位LED输出
);
// 内部逻辑写在这里
endmodule
我个人习惯把端口声明和内部逻辑分开写。端口列表里只写名字,类型和位宽在下面单独声明。这样代码结构更清晰,尤其是模块端口多的时候——我在项目中遇到过一个大模块有50多个端口,全挤在一行里,那叫一个酸爽。
input默认是wire类型,output可以是wire或reg。但为了可读性,我建议都显式写出来。
3.2 数据类型——wire和reg
Verilog里最常用的数据类型就两个:wire 和 reg。别被名字骗了,它们的区别不是“线”和“寄存器”。
- wire:用于组合逻辑。它只是物理连线的抽象,不存储值。你用
assign语句驱动的信号,必须是wire类型。 - reg:用于时序逻辑。它可以在
always块里被赋值,并且能保持上一次的值。注意,reg不一定会被综合成寄存器——如果你在组合逻辑的always块里用reg,它综合出来还是连线。
举个例子:
wire a, b; // 两个连线
wire sum; // 连线
reg [3:0] counter; // 4位寄存器
assign sum = a ^ b; // wire用assign驱动
always @(posedge clk) begin
if (!rst_n)
counter <= 4'd0;
else
counter <= counter + 1'b1;
end
你想想看,如果我把counter声明成wire,在always块里赋值,综合工具会直接报错。为什么?因为wire不能保持状态,它必须被持续驱动。
3.3 assign语句——组合逻辑的“直通车”
assign语句用来描述组合逻辑。它的执行是连续赋值——只要右边表达式里的信号变了,左边立即更新。
说白了,它就像一根导线,输入变了,输出马上跟着变。
assign led[0] = (brightness > 8'd128) ? 1'b1 : 1'b0;
assign sum = a + b;
assign flag = &data; // 按位与,检测data是否全1
我在做图像二值化加速时,就用assign直接算阈值比较:
assign pixel_out = (pixel_in > threshold) ? 8'hFF : 8'h00;
一行搞定,干净利落。assign语句非常适合这种简单的组合逻辑。
3.4 always块——时序逻辑的“心脏”
always块是Verilog里最灵活、也最容易出错的语法。它既可以描述组合逻辑,也可以描述时序逻辑。区别在于敏感列表怎么写。
常见的写法有三种:
| 类型 | 敏感列表 | 示例 |
|---|---|---|
| 时序逻辑 | posedge clk 或 negedge clk |
always @(posedge clk) |
| 组合逻辑 | @(*) 或列出所有输入信号 |
always @(*) |
| 锁存器(不推荐) | 电平敏感 | always @(a or b) |
我个人强烈建议:时序逻辑只用时钟沿触发,组合逻辑用@(*)。别搞什么电平敏感的always块,那玩意儿综合出来大概率是锁存器,你不想在芯片里见到它的。
来看一个典型的时序逻辑例子:
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
counter <= 8'd0;
flag <= 1'b0;
end else begin
if (counter == 8'd255) begin
counter <= 8'd0;
flag <= 1'b1;
end else begin
counter <= counter + 1'b1;
flag <= 1'b0;
end
end
end
注意这里的敏感列表:posedge clk or negedge rst_n。这是标准的异步复位写法。时钟上升沿触发,复位是低有效。
begin...end包起来。如果只有一条语句,可以省略begin...end,但我建议永远都写上——你永远不知道什么时候会加第二条。
3.5 阻塞赋值与非阻塞赋值——FPGA工程师的“分水岭”
这个知识点,是区分新手和老手的关键。我面试FPGA工程师时,必问这个问题。
先看定义:
- 阻塞赋值(=):顺序执行。上一条赋值完成,才执行下一条。
- 非阻塞赋值(<=):并行执行。所有赋值同时计算,在时钟沿统一更新。
听起来简单,但实际用起来很容易翻车。
我给你们看一个经典的反例:
// 错误写法:用阻塞赋值描述移位寄存器
always @(posedge clk) begin
a = b;
c = a;
end
你期望的是:b的值先给a,然后a的值再给c。这样c得到的是b的旧值吗?不是!因为阻塞赋值是顺序执行的,所以c得到的是b的新值。这根本不是移位寄存器的行为。
正确的写法:
// 正确写法:用非阻塞赋值
always @(posedge clk) begin
a <= b;
c <= a;
end
非阻塞赋值会在时钟上升沿同时采样b和a的旧值,然后同时更新a和c。这样c得到的是a的旧值,也就是b上一拍的值。这才是真正的移位寄存器。
我曾经接手过一个项目,前同事在时序逻辑里混用了阻塞和非阻塞赋值。仿真没问题,但上板后数据总是错位。我花了整整两天,一条条语句排查,才发现是赋值方式用错了。从那以后,我写代码前都会先确认:这个always块是时序还是组合?
3.6 知识体系总览
这一章的内容比较多,我画了一张图帮你理清思路:
这张图把这一章的核心知识点串起来了。你写代码时,先想清楚:这个信号是组合逻辑还是时序逻辑?用wire还是reg?用assign还是always?赋值用=还是<=?想清楚了再动笔。
好了,这一章的内容就到这里。记住我今天说的这些,尤其是阻塞和非阻塞赋值的区别——这是你从FPGA新手迈向老手的第一步。
可可精品资料库