Verilog 不是编程语言:一个嵌入式FPGA工程师的 HDL 入门指南
💡 如果你有 C / Python 的编程基础,刚开始接触 Verilog 时一定会觉得它”长得很像编程语言”——有变量、有 if-else、有 for 循环、甚至还有函数。
但这是一个危险的错觉。
Verilog 不是在”编程”,而是在描述一个电路。你写的每一行代码,都对应着芯片里的一段真实硬件。如果你用”写软件”的思维来写 Verilog,你会掉进无数的坑里——综合出来的电路要么功能错误,要么面积爆炸,要么时序全崩。
这篇文章会帮你完成一次关键的思维转换:从”软件编程思维”切换到”硬件描述思维”。这是学习 FPGA 最难也最重要的一步。
目录
1. 什么是 HDL
HDL(Hardware Description Language,硬件描述语言) 是用文本形式描述数字电路结构和行为的语言。它不是让 CPU “执行”的指令,而是让 EDA 工具”翻译”成电路的图纸。
目前主流的 HDL 有两种:
| 语言 | 特点 | 主流地区 | 最新标准 |
|---|---|---|---|
| Verilog | 语法接近 C,上手快 | 国内、北美、亚洲 | IEEE 1364-2005 |
| VHDL | 语法严谨,类型检查强 | 欧洲、航空航天 | IEEE 1076-2008 |
💬 你可能会问:Verilog 和 VHDL 选哪个?
如果你在国内做 FPGA 开发,选 Verilog——教程多、社区大、招聘要求也以 Verilog 为主。两种语言没有本质优劣,重要的不是语言本身,而是用 HDL 为真实电路建模的思维方式。
2. HDL 和 C 语言的三大本质区别
这是本文最重要的一节。理解了这三个区别,你就抓住了 HDL 的灵魂。
区别一:并发 vs 串行
// C语言:逐行串行执行
a = b + c; // 先执行这行
d = e + f; // 再执行这行
// Verilog:所有语句同时执行
assign a = b + c; // 这两行
assign d = e + f; // 同时发生!
C 语言运行在 CPU 上,CPU 逐条取指令、逐条执行——串行。而 Verilog 描述的是硬件电路,所有模块、所有 assign 语句、所有 always 块都在同时运行。这是硬件的物理特性:电路一旦通电,所有部分同时工作。
区别二:互联 vs 调用
C 语言中的函数通过”调用-返回”通信。Verilog 中的模块通过信号线互联——它们不是”调用关系”,而是”连线关系”。
// 模块实例化 = 连线,不是函数调用
adder u_add (
.a (data_a), // 信号线连接
.b (data_b),
.sum (result)
);
当你”实例化”一个模块时,你并不是在”调用”它,而是在 PCB 上”焊接”了一颗芯片,然后用导线把信号接上去。
区别三:时间 vs 无时间
C 语言没有”时间”的概念——代码执行多快取决于 CPU 频率,程序员通常不关心。但在硬件世界里,时间就是一切:
- 信号从输入到输出有传播延迟
- 寄存器只在时钟边沿采样数据
- 建立时间(Setup)和保持时间(Hold)必须满足
// 时钟边沿驱动——这是硬件世界的心跳
always @(posedge clk) begin
q <= d; // 只在时钟上升沿,q才更新为d的值
end
💡 工程师手记:我从 C 语言转到 Verilog 时,最难适应的就是”并发”。写 C 的时候,我习惯了”上一行执行完才执行下一行”。写 Verilog 时,我还在用这个思维,结果综合出来的电路完全不是我想的样子。直到有一天我对自己说:“忘掉代码,想象电路”——从那以后一切才开始顺畅。
3. Verilog 核心概念速览
基本设计单元:module
Verilog 的一切都围绕 module(模块) 展开。一个 module 就像一颗芯片——有输入引脚、有输出引脚、有内部逻辑。
module led_blink (
input wire clk, // 输入:时钟
input wire rst, // 输入:复位
output reg led // 输出:LED
);
reg [23:0] counter;
always @(posedge clk) begin
if (rst)
counter <= 24'd0;
else
counter <= counter + 1'b1;
end
always @(posedge clk) begin
if (rst)
led <= 1'b0;
else if (counter == 24'd0)
led <= ~led;
end
endmodule
一个 module 由五个部分组成:
| 部分 | 说明 |
|---|---|
| 端口定义 | 模块的输入输出”引脚” |
| I/O 声明 | 每个端口的方向(input / output / inout)和位宽 |
| 参数声明 | 可选,用 parameter 定义可配置的常量 |
| 内部信号 | wire 和 reg 类型的中间信号 |
| 功能定义 | assign、always、模块实例化等 |
参数化设计
通过 parameter 让模块可配置,实现代码复用:
module counter #(
parameter WIDTH = 8 // 默认8位
)(
input wire clk,
input wire rst,
output reg [WIDTH-1:0] count
);
always @(posedge clk) begin
if (rst) count <= {WIDTH{1'b0}};
else count <= count + 1'b1;
end
endmodule
// 实例化时指定参数
counter #(.WIDTH(16)) u_cnt16 (.clk(clk), .rst(rst), .count(cnt));
4. 三种描述方法
Verilog 提供三种描述硬件的方式,它们可以混合使用:
数据流描述(assign)
用 assign 连续赋值,适合描述组合逻辑:
assign y = a & b; // 与门
assign sum = a + b; // 加法器
assign mux = sel ? d1 : d0; // 2选1 MUX
特点:输入变化 → 输出立即跟随变化(有传播延迟),只能给 wire 类型赋值。
行为描述(always / initial)
用 always 块描述时序逻辑和复杂组合逻辑:
// 时序逻辑:时钟边沿触发
always @(posedge clk) begin
if (rst) q <= 1'b0;
else q <= d;
end
// 组合逻辑:敏感列表用 *
always @(*) begin
case (sel)
2'b00: out = in0;
2'b01: out = in1;
2'b10: out = in2;
2'b11: out = in3;
endcase
end
关键区别:
<=(非阻塞赋值):用于时序逻辑(always @(posedge clk))=(阻塞赋值):用于组合逻辑(always @(*))- 绝对不要混用! 这是 Verilog 新手最常犯的错误之一。
结构化描述(实例化)
通过实例化已有模块来搭建更大的系统——就像用现成的芯片搭电路:
// 实例化一个加法器模块
adder u_add (
.a (data_a),
.b (data_b),
.sum (result)
);
核心理解:Verilog 中所有
always块、所有assign语句、所有实例化模块——它们都是并行执行的。书写顺序不影响执行顺序。它们之间通过信号名互相连接。
5. 可综合 vs 不可综合
这是 Verilog 初学者必须搞清楚的一个概念:不是所有 Verilog 语法都能变成电路。
| 类型 | 说明 | 示例 |
|---|---|---|
| 可综合 | 能被综合工具转化为真实电路的语句 | assign、always @(posedge clk)、if-else、case、+、* |
| 不可综合 | 只能在仿真中使用,无法变成电路 | #10(延迟)、initial、$display、$readmemh |
判断法则:如果你自己都想象不出这段代码对应什么样的硬件电路,那它大概率不可综合。
// ✅ 可综合:你能想象出这是一个加法器
assign c = a + b;
// ✅ 可综合:你能想象出这是一个MUX
assign y = (sel) ? d1 : d0;
// ❌ 不可综合:硬件里没有"等10纳秒"这种东西
#10 c = b;
// ❌ 不可综合:除法没有直观的硬件实现(需要算法展开)
assign result = a / b;
不可综合的语法并非没用——它们是 Testbench(仿真测试台) 的核心工具。
initial块用于生成测试激励,#10用于控制仿真时序,$display用于打印调试信息。
6. 总结
| 要点 | 核心内容 |
|---|---|
| HDL 是什么 | 硬件描述语言,描述电路结构,不是编程语言 |
| 与 C 的三大区别 | 并发 vs 串行、互联 vs 调用、有时间 vs 无时间 |
| Verilog 基本单元 | module = 一颗芯片(端口 + 参数 + 信号 + 逻辑) |
| 三种描述方法 | 数据流(assign)、行为(always)、结构化(实例化) |
| 可综合 vs 不可综合 | 能想象出电路 = 可综合;只为仿真服务 = 不可综合 |
给初学者的一句话:学 Verilog 最重要的不是背语法,而是每写一行代码都问自己:这对应什么样的电路? 当你能在脑海中把代码”翻译”成门电路和寄存器时,你就真正入门了。
常见问题
Q1:Verilog 和 SystemVerilog 什么关系?
SystemVerilog 是 Verilog 的超集,在可综合部分增加了
logic类型、interface、enum等便利特性,在验证部分增加了面向对象、随机约束等高级功能。新项目推荐使用 SystemVerilog,但核心的硬件描述思维是一样的。
Q2:为什么 <= 和 = 不能混用?
<=(非阻塞赋值)模拟的是寄存器行为——所有赋值在时钟边沿”同时”生效。=(阻塞赋值)模拟的是组合逻辑——按顺序立即生效。在时序逻辑中用=会导致仿真与实际硬件行为不一致,产生难以排查的 Bug。
Q3:Verilog 的 for 循环能综合吗?
能,但含义完全不同。C 的 for 是”重复执行 N 次”,Verilog 的 for 是”展开成 N 份并行硬件”。
for (i=0; i<8; i=i+1) a[i] = b[i] & c[i];会被综合成 8 个独立的与门,而不是一个与门执行 8 次。
Q4:学 Verilog 之前需要什么基础?
最重要的是数字电路基础:组合逻辑(与/或/非门、MUX、译码器)、时序逻辑(触发器、计数器、状态机)。如果你有 C 语言基础会更容易上手语法,但记住:思维方式要完全重建。
参考资料
- IEEE Std 1364-2005: IEEE Standard for Verilog Hardware Description Language
- Stuart Sutherland,Verilog HDL Quick Reference Guide
- 夏宇闻,《Verilog 数字系统设计教程》(国内经典教材)
- HDLBits 在线练习:hdlbits.01xz.net
系列导航:本文是「FPGA 入门系列」第 6 篇。
如果这篇文章对你有帮助,欢迎点赞、收藏,也欢迎在评论区分享你学 Verilog 时踩过的坑——相信我,你踩的坑别人也会踩。