FPGA 开发入门:Verilog 与逻辑综合
什么是 FPGA?
FPGA(Field-Programmable Gate Array,现场可编程门阵列)是一种可以通过硬件描述语言重新配置内部电路逻辑的芯片。与固定功能的 CPU 或 GPU 不同,FPGA 可以实现你自定义的数字电路,从而在特定任务上获得极高的并行处理能力和低延迟。
FPGA 能做什么?
信号处理、高速通信接口、人工智能加速、电机控制、图像处理……几乎任何需要低延迟、高吞吐或确定性的数字逻辑系统,都可以在 FPGA 上实现。
与 CPU 对比
- CPU:顺序执行指令,适合复杂决策和通用计算。
- FPGA:直接构建硬件电路,并行运行,适合流水线和数据处理管道。
FPGA 架构基础
现代 FPGA 内部主要由可编程逻辑块(Logic Block)、互联资源、I/O 模块和专用硬核(如 Block RAM、DSP)组成。
可编程逻辑块
每一个逻辑块通常包含:
- 查找表(LUT):实现任意布尔函数,可以看作一个小型真值表。例如 4 输入 LUT 能实现任意 4 输入的逻辑组合。
- 触发器(Flip-Flop, FF):用于存储状态,构建时序逻辑,如寄存器和计数器。
FPGA 通过将成千上万个这样的逻辑块连接在一起,实现任意复杂的数字电路。
其他重要资源
- 可编程互连:由开关矩阵和连线组成,负责连接逻辑块和 I/O。
- Block RAM(BRAM):嵌入式存储器,可配置为单端口或双端口 RAM/ROM。
- DSP 块:专用乘法累加器,用于高速信号处理。
- I/O 引脚:支持多种电平和标准,如 LVCMOS、LVDS 等。
FPGA 开发流程
典型的 FPGA 开发遵循 “编写代码 → 仿真 → 综合 → 实现 → 生成比特流 → 上板测试” 的顺序。
- 设计输入:使用 Verilog 或 VHDL 描述电路功能(RTL,寄存器传输级)。
- 功能仿真:用测试平台(testbench)验证逻辑行为是否正确,常用工具如 ModelSim、Vivado Simulator。
- 逻辑综合:将 RTL 代码转换为由逻辑门和触发器组成的网表。
- 布局布线(实现):将网表映射到 FPGA 的物理逻辑块上,并连接。
- 生成比特流:产生最终配置 FPGA 的二进制文件。
- 下载与验证:将比特流烧录到 FPGA,在实际硬件上观察或调试。
Verilog 快速入门
Verilog 是描述数字电路最常用的硬件描述语言之一,语法上与 C 语言有些相似,但思想完全不同——你在描述硬件结构,而不是软件指令。
模块与端口
Verilog 的基本设计单元是模块(module),类似一个芯片的封装,带有输入输出端口。
module and_gate (
input wire a,
input wire b,
output wire y
);
assign y = a & b;
endmodule
数据类型
- wire:表示连线,不能存储值,由连续赋值驱动。
- reg:表示寄存器(变量),在过程块(always)中赋值使用,但综合后不一定真的成为触发器,取决于描述方式。
组合逻辑描述
使用 assign 连续赋值语句,描述纯组合电路:
assign sum = a + b;
assign carry_out = (a & b) | (carry_in & (a ^ b));
或者用 always @(*) 过程块(行为级):
always @(*) begin
if (sel)
out = a;
else
out = b;
end
时序逻辑描述
使用 always @(posedge clk) 描述带时钟的寄存器行为:
reg [7:0] counter;
always @(posedge clk or posedge reset) begin
if (reset)
counter <= 8'd0;
else
counter <= counter + 1'b1;
end
这里使用非阻塞赋值 <=,是时序逻辑推荐写法,能正确模拟寄存器行为。
向量与位选择
wire [7:0] data; // 8 位宽
assign data[3:0] = 4'b1010; // 只赋值低 4 位
逻辑综合是什么?
逻辑综合是将你写的 Verilog/VHDL 代码(RTL 描述)自动转换为由基本门电路(与、或、非)和触发器组成的网表的过程。综合工具(如 Synopsys Design Compiler、Yosys,或 Vivado/Quartus 自带综合器)会进行优化,以满足面积、速度或功耗的要求。
综合如何工作?
- 编译与解析:理解 HDL 代码的语法和语义。
- 逻辑优化:布尔化简、资源共享、常量传播等。
- 工艺映射:将优化后的逻辑映射到目标 FPGA 的 LUT、FF 等具体资源上。
- 网表输出:产生描述各元件连接关系的文件。
关键点:所有可综合的代码都必须能映射到实际硬件上。循环在综合时会被展开,函数调用被内联,递归不可综合(除非静态确定)。
时序约束的必要性
综合工具需要知道设计的时钟频率、输入输出延迟等才能优化。你需要通过 .sdc 或约束文件来告诉工具:
- create_clock:定义时钟周期和占空比。
- set_input_delay / set_output_delay:设置 I/O 时序要求。 不提供约束的综合只是预优化,最终的布局布线必须满足时序约束才能正常工作。
实战:流水灯设计(点亮 LED)
我们从最简单的流水灯实验开始,理解从 Verilog 代码到 FPGA 上运行的完整路径。假设 FPGA 板载 50 MHz 时钟,4 个 LED 低电平点亮,需要设计一个让 LED 每隔约 0.5 秒向左循环移位一次的逻辑。
1. 设计文件:water_light.v
module water_light (
input wire clk, // 50 MHz
input wire rst_n, // 低电平复位
output wire [3:0] led // 低电平点亮
);
// 计数器分频,50M / 25M = 2 Hz? 实际上我们要0.5s = 2 Hz?
// 0.5s 切换一次,对应 2 Hz 频率。50M / 25M = 2 Hz (每 25M 个时钟周期翻转一次)
// 计数范围 0 ~ 24,999,999 (需要25M个状态)
parameter MAX_CNT = 25_000_000 - 1;
reg [24:0] cnt;
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
cnt <= 25'd0;
else if (cnt == MAX_CNT)
cnt <= 25'd0;
else
cnt <= cnt + 1'b1;
end
// 移位寄存器
reg [3:0] led_reg;
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
led_reg <= 4'b1110; // 初始点亮最低位LED
else if (cnt == MAX_CNT)
led_reg <= {led_reg[2:0], led_reg[3]}; // 循环左移
end
assign led = led_reg; // 假设LED低电平亮,但这里为直观输出低有效
// 若LED高电平亮,将reg初始改为0001,并反向输出
endmodule
2. 仿真验证(可选)
编写一个简单 testbench,生成时钟和复位信号,在仿真工具中观察 led 信号是否按要求变化。这一步能极大减少debug时间。
3. 综合与引脚约束
将设计添加到 FPGA 开发软件(Vivado 或 Quartus)中,进行综合。综合后可查看原理图(Schematic),确认生成了计数器和移位寄存器。
然后编写约束文件(如 .xdc 文件),将 clk 分配到板载 50 MHz 时钟引脚,rst_n 分配到一个按钮,led 分配到对应的 LED 引脚,并创建时钟约束:
create_clock -period 20.000 [get_ports clk]
set_input_clock_uncertainty 0.1 [get_clocks]
4. 布局布线及生成比特流
运行实现(Implementation),检查时序报告,确保没有时序违例。生成比特流文件。
5. 下载并观察
连接编程器,将比特流烧录到 FPGA。按下复位键后,LED 应开始流水循环。
进阶提示
时序收敛
如果设计不满足时钟约束,需要分析关键路径,常用手段:
- 添加流水线寄存器(pipeline)。
- 减少组合逻辑级数。
- 优化扇出,复制高负载寄存器。
使用 IP 核
FPGA 厂商提供了大量已验证的 IP 核(FIFO、PLL、以太网控制器等)。初学者可以先用 IP 核搭建系统,再逐步深入设计自己的模块。例如用 PLL 生成任意频率时钟,避免手动分频带来的时序问题。
状态机设计
复杂控制逻辑适合用有限状态机(FSM)描述。推荐使用三段式状态机写法(现态、次态、输出),清晰且易综合。
// 状态编码
localparam IDLE = 2'd0, WORK = 2'd1, DONE = 2'd2;
// 状态跳转(用时序块)
always @(posedge clk) begin
if (rst) state <= IDLE;
else state <= next_state;
end
// 次态逻辑(用组合块)
always @(*) begin
case(state)
IDLE: next_state = start ? WORK : IDLE;
...
endcase
end
// 输出逻辑
always @(posedge clk) begin
case(state)
...
endcase
end
实例化与层次化设计
大设计应拆分为多个模块,顶层只做连线。模块实例化语法:
counter #(.WIDTH(8)) u_counter (
.clk(clk),
.reset(reset),
.count(count_out)
);
参数化使代码复用性大大提高。
总结
FPGA 开发入门的关键在于转变思维:你不再编写顺序执行的程序,而是用硬件描述语言构造并行电路。学习路径建议如下:
- 熟悉 Verilog/Hardware Description Language 基础,特别是组合/时序逻辑描述。
- 理解 FPGA 内部结构(LUT、FF、BRAM)。
- 掌握开发流程:仿真→综合→约束→实现→下载。
- 多动手实践,从流水灯、按键消抖到 UART 通信,循序渐进。
工具链的熟练使用和时序分析能力会在后续项目中自然积累。当你能让一个看似简单的计数器在 FPGA 上稳定工作,并满足时序约束时,就意味着你已经迈入了数字硬件设计的大门。