第3章:硬件描述语言入门(Verilog)
好,咱们开始聊Verilog。说实话,很多初学者一听到「硬件描述语言」这六个字就头大,觉得这玩意儿跟C语言差不多。嗯,我当年也这么想过,结果第一次写出来的代码,综合器直接给我报了一堆警告——说白了,Verilog不是写软件,是在画电路。
我个人习惯把Verilog当成「用文字画电路图」的工具。你写的每一行代码,最终都会变成实实在在的门电路、触发器和连线。这个思维转变,是入门最关键的一步。
3.1 Verilog基本语法
先看一个最简单的例子。我刚开始带新人时,总让他们先写一个与门:
module and_gate (
input wire a,
input wire b,
output wire y
);
assign y = a & b;
endmodule
看到没?module和endmodule就像一对括号,把整个电路包起来。input和output声明端口方向,wire表示连线型变量。这里有个坑——我曾经见过有人把wire和reg搞混,结果仿真死活不对。
常用的数据类型就两种:
- wire:组合逻辑连线,用assign赋值
- reg:在always块里赋值,可综合成寄存器或组合逻辑
数值表示也很简单:4'b1010表示4位二进制数1010,8'hFF表示8位十六进制数FF。我个人习惯用二进制写测试向量,用十六进制写地址——这样一眼就能看出位宽。
3.2 模块化设计
做项目时,我从来不会把所有逻辑塞进一个模块。你想想看,一个几千行的Verilog文件,出bug了你找都找不到。模块化设计,说白了就是「分而治之」。
举个例子,一个计数器模块:
module counter #(
parameter WIDTH = 8
)(
input wire clk,
input wire rst_n,
input wire en,
output reg [WIDTH-1:0] count
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
count <= 0;
else if (en)
count <= count + 1;
end
endmodule
这里用了parameter定义位宽,调用时可以直接改:
counter #(.WIDTH(16)) u_counter (
.clk (clk),
.rst_n (rst_n),
.en (en),
.count (count)
);
模块化设计还有个好处——复用。我在一个项目中写过一个I2C控制器,后来三个项目都直接拿来用,改改参数就行。省下来的时间,喝杯咖啡不香吗?
3.3 组合逻辑与时序逻辑
这是Verilog里最容易混淆的地方。我见过太多人把组合逻辑和时序逻辑写成一锅粥。
组合逻辑:输出只取决于当前输入。用assign或者always @(*)实现。
// 组合逻辑:多路选择器
assign y = sel ? a : b;
// 或者用always块
always @(*) begin
if (sel)
y = a;
else
y = b;
end
时序逻辑:输出还取决于之前的状态。用always @(posedge clk)实现。
// 时序逻辑:D触发器
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
q <= 0;
else
q <= d;
end
这里有个关键点:组合逻辑用=(阻塞赋值),时序逻辑用<=(非阻塞赋值)。为什么?因为非阻塞赋值能模拟寄存器的并行更新行为。我曾经在面试时问过这个问题,十个人里有八个答不上来。
避坑指南:我曾经在一个项目中,把时序逻辑里的赋值写成了阻塞赋值。结果仿真看起来没问题,但综合后的电路时序全乱了。从那以后,我养成了一个习惯——always块里只要有时钟沿,一律用非阻塞赋值。
3.4 仿真与测试平台搭建
写完了代码,不仿真就敢流片?我反正不敢。测试平台(testbench)就是用来验证你的设计对不对的。
一个最简单的testbench:
`timescale 1ns / 1ps
module tb_counter;
reg clk;
reg rst_n;
reg en;
wire [7:0] count;
// 实例化被测试模块
counter #(.WIDTH(8)) uut (
.clk (clk),
.rst_n (rst_n),
.en (en),
.count (count)
);
// 生成时钟
initial begin
clk = 0;
forever #5 clk = ~clk; // 周期10ns
end
// 测试激励
initial begin
rst_n = 0;
en = 0;
#20;
rst_n = 1;
#10;
en = 1;
#100;
en = 0;
#50;
$finish;
end
// 波形输出
initial begin
$dumpfile("wave.vcd");
$dumpvars(0, tb_counter);
end
endmodule
你看,testbench里没有input和output,因为它是一个独立的仿真环境。时钟用forever生成,激励用initial块控制。我个人习惯在仿真开始时先复位一段时间,等所有信号稳定了再开始操作。
常用的仿真工具有ModelSim、Vivado Simulator、VCS等。我个人推荐初学者先用开源的Icarus Verilog + GTKWave,免费且够用。等做项目了再上商业工具。
最后说一句:仿真通过不代表硬件没问题。我遇到过仿真完美但上板后死机的案例——原因是仿真时没考虑门延迟和布线延迟。所以,仿真只是第一步,后仿和时序分析才是真正见真章的地方。
嗯,这一章的内容就到这儿。下一章咱们聊聊如何用Verilog写一个真正的项目——从需求分析到上板验证,走一遍完整的流程。