4. 硬件描述语言基础(Verilog):模块、端口、赋值语句、always块、时序逻辑
好,咱们今天聊聊Verilog。说实话,很多初学者一上来就被各种语法吓住了。其实没那么复杂。你把它想象成画电路图,只不过是用文字来描述而已。
我刚开始学的时候,总觉得这玩意儿跟C语言差不多。后来被坑了几次才明白——Verilog是描述硬件,不是写软件。这个观念转不过来,后面会很难受。
4.1 模块与端口——芯片的“黑盒子”
Verilog里最基本的单元叫模块(module)。你可以把它想象成一个芯片的封装。外面的人只看到引脚,里面怎么实现的,那是你自己的事。
核心概念:模块 = 硬件功能的封装。一个模块可以是一个与门,也可以是一个完整的CPU。
看个最简单的例子——一个D触发器:
module d_flip_flop (
input wire clk, // 时钟
input wire rst_n, // 复位,低有效
input wire d, // 数据输入
output reg q // 数据输出
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
q <= 1'b0;
else
q <= d;
end
endmodule
端口有三种方向:input、output、inout。我个人习惯把时钟和复位放在端口列表的最前面,这样一眼就能看到时序控制信号。
小技巧:端口类型声明时,wire是默认的。如果你不写,Verilog默认就是wire。但reg必须显式声明。我建议你养成好习惯,每个端口都写清楚类型。
4.2 赋值语句——两种完全不同的“等号”
这里有个大坑。Verilog里两种赋值方式,阻塞赋值(=)和非阻塞赋值(<=)。它们的行为完全不同。
| 类型 | 符号 | 执行顺序 | 典型用途 |
|---|---|---|---|
| 阻塞赋值 | = | 立即执行,顺序执行 | 组合逻辑(assign、always @(*)) |
| 非阻塞赋值 | <= | 并行执行,块结束后统一更新 | 时序逻辑(always @(posedge clk)) |
我曾经在项目里犯过一个低级错误——在时序逻辑里用了阻塞赋值。结果仿真没问题,上板子就跑飞了。查了两天才发现是赋值方式搞错了。嗯,从那以后我写always块时,第一件事就是确认用的是不是<=。
警告:在同一个always块里,不要混用阻塞和非阻塞赋值。这是铁律。你想想看,一个信号既要立即更新又要延迟更新,硬件怎么可能同时做到?
4.3 always块——硬件的“心脏”
always块是Verilog里最核心的语法。它描述的是什么时候做什么事。
常见的写法有三种:
- 组合逻辑:
always @(*)—— 输入一变,输出马上变 - 时序逻辑:
always @(posedge clk)—— 只在时钟上升沿变化 - 异步复位:
always @(posedge clk or negedge rst_n)—— 时钟或复位变化都触发
我个人的经验是:一个always块只做一件事。别把组合逻辑和时序逻辑混在一起写。虽然语法上允许,但后期维护起来你会想骂人的。
// 好的写法:分开写
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
count <= 4'b0;
else if (en)
count <= count + 1'b1;
end
// 不好的写法:混在一起
always @(posedge clk) begin
if (rst_n == 1'b0)
count <= 4'b0;
else if (en)
count <= count + 1'b1;
// 这里还插了一段组合逻辑...
data_out = count; // 阻塞赋值混进来了!
end
记住:always块里的敏感列表决定了这个块什么时候被“唤醒”。敏感列表写错了,综合出来的电路可能跟你想象的天差地别。
4.4 时序逻辑——让电路“记住”状态
时序逻辑的核心是存储元件——触发器和锁存器。在FPGA里,我们几乎只用D触发器。
为什么?因为D触发器结构简单,时序清晰,不容易出问题。锁存器(latch)在FPGA里是“不受欢迎”的,因为它会导致时序分析困难。
我画了一张图,帮你理解时序逻辑的基本结构:
这张图展示了一个典型的时序逻辑结构。组合逻辑计算“下一状态”,D触发器在时钟沿到来时把“下一状态”锁存为“当前状态”。反馈路径让电路有了“记忆”——这就是状态机的本质。
4.5 实战:写一个4位计数器
说了这么多,咱们来写个实际的东西。一个4位计数器,带使能和异步复位。
module counter_4bit (
input wire clk, // 时钟
input wire rst_n, // 异步复位,低有效
input wire en, // 计数使能
output reg [3:0] count // 计数值
);
// 时序逻辑:在时钟上升沿或复位下降沿触发
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
count <= 4'b0000; // 复位到0
end else if (en) begin
count <= count + 1'b1; // 使能有效时加1
end
// 如果en=0,count保持不变
end
endmodule
这段代码里,我用了非阻塞赋值(<=)。为什么?因为这是时序逻辑。所有赋值在同一个时钟沿“同时”发生,而不是顺序执行。
避坑指南:我曾经在写计数器时,把count <= count + 1'b1写成了count = count + 1'b1。仿真看起来没问题,但综合后电路多了一堆没用的锁存器。记住:时序逻辑用非阻塞,组合逻辑用阻塞。这是铁律。
4.6 总结一下
Verilog其实不复杂。你只要记住三件事:
- 模块是基本单位——一个文件一个模块,端口要写清楚方向
- 赋值方式要分清——时序逻辑用
<=,组合逻辑用=,别混用 - always块要专一——一个块只做一种逻辑,别把组合和时序搅在一起
嗯,这些就是我这些年做FPGA设计总结出来的经验。刚开始可能会觉得别扭,但写多了你就会发现——硬件描述语言,描述的是硬件,不是软件。这个观念一旦建立起来,后面的路就好走了。
最后送你一句话:写Verilog的时候,脑子里要想着电路。每个always块对应一块硬件,每个赋值对应一条连线。想清楚了再下笔,比写完了再改要高效得多。