🚀 FPGA零基础入门:Verilog语法全攻略
💡 前言
你是否曾经好奇过芯片是如何设计出来的?想要亲手"搭建"一个数字电路吗?
别担心,即使你完全没有硬件基础,这篇教程也会带你一步步走进FPGA的奇妙世界!
🎯 什么是FPGA?为什么要学Verilog?
FPGA简介
FPGA(Field Programmable Gate Array,现场可编程门阵列)是一种可以通过编程来实现各种数字电路功能的芯片。简单来说,它就像"数字电路界的乐高积木",你可以用代码来"搭建"出各种电路。
为什么选择Verilog?
FPGA开发主要有两种语言:VHDL和Verilog。其中:
- Verilog(1995年成为IEEE标准)语法更接近C语言,学习曲线平缓
- VHDL(1987年成为IEEE标准)语法较为严格,更接近Ada语言
Verilog是目前应用最广泛的一种硬件描述语言,如果你有C语言基础,那么学习Verilog会非常轻松!
🔄 Verilog与C语言:你的学习捷径
相似的控制结构
好消息!如果你学过C语言,你会发现Verilog的很多语法都很眼熟:
| 功能 | C语言 | Verilog |
|---|---|---|
| 函数/模块 | sub-function |
module, function, task |
| 条件判断 | if-then-else |
if-then-else |
| 多分支选择 | case |
case |
| 代码块 | { } |
begin ... end |
| 循环 | for, while |
for, while |
| 宏定义 | #define |
``define` |
| 整数类型 | int |
integer |
熟悉的运算符
几乎所有C语言的运算符在Verilog中都能找到对应:
verilog
// 算术运算
a + b; a - b; a * b; a / b; a % b;
// 逻辑运算
&& || !
// 关系运算
> < >= <= == !=
// 位运算
& | ^ ~ << >>
// 三目运算符
condition ? true_value : false_value;
🏗️ Verilog基础语法:从零开始
1️⃣ 模块结构:Verilog的"主函数"
在C语言中,程序从main()函数开始。在Verilog中,**模块(module)**就是基本单元,就像搭积木的一个个部件。
基本结构
verilog
module 模块名 (
// 端口声明(输入输出接口)
input wire a,
input wire b,
output wire c
);
// 逻辑功能实现
assign c = a & b; // c = a AND b
endmodule
实际例子:一个简单的或门和与门
verilog
module logic_gates (
// 输入信号
input wire a,
input wire b,
// 输出信号
output wire or_result, // 或门输出
output wire and_result // 与门输出
);
// 逻辑实现
assign or_result = a | b; // 或运算
assign and_result = a & b; // 与运算
endmodule
2️⃣ 信号类型:电路中的"变量"
📌 输入输出信号
verilog
input // 输入端口
output // 输出端口
inout // 双向端口(既能输入又能输出)
例子:
verilog
input clk; // 单bit输入
input [7:0] data_in; // 8bit输入总线
output [3:0] led; // 4bit输出
📌 内部信号类型
Verilog中有两种主要的信号类型,这是初学者最容易混淆的地方:
🔹 wire型 - "导线"
- 代表物理连线
- 用于组合逻辑(如
assign语句) - 不能存储数值,只是传递信号
verilog
wire connection;
assign connection = a & b; // 组合逻辑,立即响应
🔹 reg型 - "寄存器"
- 可以存储数值
- 用在
always块中 - 虽然叫"reg",但不一定综合成寄存器
verilog
reg counter;
always @(posedge clk) begin
counter <= counter + 1; // 时序逻辑,在时钟上升沿更新
end
⚠️ 新手易错点:
reg类型的名字容易误导人!在always块中被赋值的信号必须声明为reg,但实际综合后可能是组合逻辑电路,并非寄存器。
3️⃣ 数据表示:数字的"方言"
进制表示法
Verilog支持多种进制,格式为:<位宽>'<进制><数值>
verilog
8'd100 // 8位十进制数100
8'b1010 // 8位二进制数1010
8'hFF // 8位十六进制数FF
8'o77 // 8位八进制数77
// 可以用下划线增加可读性
16'b1111_0000_1010_0101 // 等同于 16'hF0A5
特殊值
verilog
x // 不定值(don't care)
z // 高阻态(未连接)
参数定义:硬件中的"常量"
verilog
parameter WIDTH = 8; // 可被外部修改
localparam STATE_IDLE = 2'b00; // 仅本模块使用
reg [WIDTH-1:0] data; // 使用参数定义位宽
4️⃣ 赋值语句:阻塞 vs 非阻塞
这是Verilog最重要也最容易出错的概念!
🔸 阻塞赋值 = (类似C语言)
verilog
always @(*) begin
a = b;
c = a; // c的值是b的值,按顺序执行
end
特点:
- 顺序执行
- 用于组合逻辑
- 像C语言一样"立即生效"
🔸 非阻塞赋值 <= (硬件特有)
verilog
always @(posedge clk) begin
a <= b;
c <= a; // c的值是a的旧值,并行执行
end
特点:
- 并行执行
- 用于时序逻辑
- 所有赋值同时发生(在时钟沿)
💡 记忆口诀:
组合用等号,时序用小于等于!
5️⃣ 条件语句:硬件中的"决策"
if-else语句
verilog
// 简单if
if (a > b)
out <= a;
// if-else
if (a > b)
out <= a;
else
out <= b;
// 多重if-else(优先级编码器)
if (sel == 2'b00)
out <= data0;
else if (sel == 2'b01)
out <= data1;
else if (sel == 2'b10)
out <= data2;
else
out <= data3;
case语句:多路选择器的最佳实践
verilog
always @(*) begin
case (sel)
2'b00: out = data0;
2'b01: out = data1;
2'b10: out = data2;
2'b11: out = data3;
default: out = 8'h00; // 务必加default!
endcase
end
变体:
casez: 将z和?视为"任意值"casex: 将x、z和?都视为"任意值"
6️⃣ 循环语句:代码复用的艺术
for循环:最常用
verilog
integer i;
always @(*) begin
for (i = 0; i < 8; i = i + 1) begin
result[i] = data[i] ^ parity;
end
end
⚠️ 重要提示:
Verilog中的
for循环会被展开成并行硬件,不是像C语言那样逐次执行!
while循环:条件重复
verilog
while (count < 10) begin
sum = sum + data[count];
count = count + 1;
end
repeat循环:固定次数
verilog
repeat (8) begin
result = result + shift_data;
shift_data = shift_data << 1;
end
forever循环:永久执行(仅用于仿真)
verilog
initial begin
forever #10 clk = ~clk; // 每10个时间单位翻转时钟
end
7️⃣ 结构说明语句:程序的"骨架"
initial块:初始化(仅仿真)
verilog
initial begin
clk = 0;
rst = 1;
#100 rst = 0; // 100时间单位后复位解除
end
📝 注意:
initial块只在仿真中执行,不能综合成实际硬件!
always块:硬件行为描述
组合逻辑:
verilog
always @(*) begin // @(*)表示对所有输入敏感
sum = a + b;
end
时序逻辑:
verilog
always @(posedge clk or posedge rst) begin // 时钟上升沿或复位
if (rst)
counter <= 0;
else
counter <= counter + 1;
end
8️⃣ 任务与函数:代码复用
function:返回单一值
verilog
function [7:0] calc_parity;
input [7:0] data;
begin
calc_parity = ^data; // 计算奇偶校验位
end
endfunction
// 调用
assign parity = calc_parity(input_data);
特点:
- 必须有返回值
- 至少有一个输入
- 执行时间为0(组合逻辑)
task:执行复杂操作
verilog
task write_data;
input [7:0] addr;
input [7:0] data;
begin
@(posedge clk);
address <= addr;
write_en <= 1;
data_out <= data;
@(posedge clk);
write_en <= 0;
end
endtask
// 调用
write_data(8'h10, 8'hAB);
特点:
- 可以有多个输入输出
- 可以消耗仿真时间
- 可以调用其他task
🛠️ 系统任务:调试的利器
打印输出
verilog
$display("Counter = %d, Time = %t", counter, $time); // 带换行
$write("Data = %h", data); // 不换行
// 格式控制符
%d // 十进制
%h // 十六进制
%b // 二进制
%t // 时间格式
监控信号
verilog
initial begin
$monitor($time, " clk=%b data=%h", clk, data);
// 每当clk或data变化时自动打印
end
结束仿真
verilog
$finish; // 结束仿真
文件操作
verilog
$readmemh("data.hex", memory); // 从文件读取十六进制数据
$readmemb("data.bin", memory); // 从文件读取二进制数据
🎓 编译预处理:高级技巧
宏定义
verilog
`define WIDTH 8
`define MAX_COUNT 255
reg [`WIDTH-1:0] data;
文件包含
verilog
`include "definitions.v"
时间尺度
verilog
`timescale 1ns/1ps // 时间单位/时间精度
条件编译
verilog
`ifdef SIMULATION
// 仅在仿真时编译
initial $display("Simulation mode");
`else
// 综合时编译
`endif
✨ 完整实例:8位计数器
让我们用一个完整的例子把所有知识串起来:
verilog
`timescale 1ns/1ps
module counter_8bit (
input wire clk, // 时钟信号
input wire rst_n, // 低电平复位
input wire en, // 使能信号
output reg [7:0] count, // 计数值
output wire overflow // 溢出标志
);
// 参数定义
parameter MAX_COUNT = 255;
// 组合逻辑:溢出检测
assign overflow = (count == MAX_COUNT) ? 1'b1 : 1'b0;
// 时序逻辑:计数器
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
count <= 8'd0; // 复位
else if (en) begin
if (count == MAX_COUNT)
count <= 8'd0; // 溢出归零
else
count <= count + 1; // 递增
end
end
endmodule
测试模块(testbench):
verilog
module tb_counter;
reg clk, rst_n, en;
wire [7:0] count;
wire overflow;
// 实例化被测模块
counter_8bit uut (
.clk(clk),
.rst_n(rst_n),
.en(en),
.count(count),
.overflow(overflow)
);
// 生成时钟
initial begin
clk = 0;
forever #5 clk = ~clk; // 10ns周期
end
// 测试激励
initial begin
rst_n = 0;
en = 0;
#20 rst_n = 1;
#10 en = 1;
#3000 $finish;
end
// 监控输出
initial begin
$monitor("Time=%0t count=%d overflow=%b", $time, count, overflow);
end
endmodule
🎯 新手常见错误与避坑指南
❌ 错误1:混用阻塞和非阻塞赋值
verilog
// 错误示范
always @(posedge clk) begin
a = b; // 阻塞赋值
c <= a; // 非阻塞赋值,混用!
end
// 正确写法
always @(posedge clk) begin
a <= b;
c <= a; // 全部使用非阻塞
end
❌ 错误2:敏感列表不完整
verilog
// 错误示范
always @(a) begin
out = a + b; // b没有在敏感列表中!
end
// 正确写法
always @(*) begin // 或 always @(a, b)
out = a + b;
end
❌ 错误3:case语句缺少default
verilog
// 错误示范
always @(*) begin
case (sel)
2'b00: out = a;
2'b01: out = b;
// 缺少其他情况!可能产生锁存器
endcase
end
// 正确写法
always @(*) begin
case (sel)
2'b00: out = a;
2'b01: out = b;
default: out = 8'h00;
endcase
end
📚 学习路线建议
- 第一周: 掌握基本语法、数据类型、模块结构
- 第二周: 理解组合逻辑与时序逻辑的区别
- 第三周: 学习状态机设计(FSM)
- 第四周: 实践小项目(LED流水灯、按键消抖、串口通信等)
🔗 推荐学习资源
- 在线仿真工具: EDA Playground (免费在线Verilog仿真)
- 开发板推荐: FPGA入门开发板(如Basys3、DE10-Lite)
- 经典书籍: 《Verilog HDL数字设计与综合》
📖 学习声明
本文是学习知识星球**「FPGA从入门到精通」**后按个人理解整理的学习笔记,内容可能存在理解不够深入或不够完善之处。
如果你希望获取更系统、更专业的FPGA与数字电路知识,建议:
- 参考《Verilog经典教程》等权威书籍
- 前往CSDN、知识星球、知乎等平台查阅更多高质量文章
- 加入FPGA学习社区与同行交流
笔记整理有限,原创内容无限 🌟
💬 写在最后
FPGA的世界浩瀚无垠,Verilog只是你的第一把钥匙。希望这篇教程能帮你打开数字电路设计的大门,祝学习顺利!有问题欢迎交流讨论~
⭐ 如果觉得有帮助,别忘了点赞收藏哦!