一、FPGA开发中资源浪费的常见痛点
做FPGA开发的人大多有过这样的经历:辛辛苦苦写完代码,综合布线后突然弹出个警告——逻辑单元(LE)或者存储块(BRAM)占用超了,要么得换贵的芯片,要么得删功能改需求,特别闹心。其实很多时候资源浪费不是功能复杂导致的,而是代码写得不够“省”,就像家里本来能住下的房子,被乱堆的杂物占满了空间,明明整理一下就能腾出位置。
先搞懂两个核心资源的作用:逻辑单元是FPGA的“积木块”,用来做加减乘除、逻辑判断这些计算和控制,相当于电脑的CPU;存储资源是FPGA的“临时仓库”,用来存中间数据、缓存信号,相当于电脑的内存。这俩资源占满了,整个项目就跑不起来,所以优化的核心就是让这俩资源“物尽其用”。
二、逻辑单元(LE)的优化方法
逻辑单元浪费的主要原因是代码里做了没必要的计算、用了冗余的逻辑判断,或者没利用好FPGA的专用硬件资源。下面讲两个最实用的优化方法,每个都带完整示例。
2.1 用“静态计算”替代“动态计算”
啥意思呢?就是把开发过程中就能确定结果的计算,提前算好写进代码,别让FPGA在运行时再算。比如一个模块需要用到“256的平方根”,如果代码里写assign sqrt_256 = $sqrt(256),FPGA综合的时候会自动把它优化成固定值16,这个没问题;但如果是assign sqrt_x = $sqrt(x),x是输入变量,那FPGA就得专门做一个平方根的计算电路,占用好几个逻辑单元。
再举个更贴近实际开发的例子:假设我们要做一个UART(串口)模块,波特率是9600,系统时钟是50MHz,需要计算“波特率分频系数”——也就是每多少个系统时钟周期,产生一个波特率周期。分频系数的计算公式是:系统时钟频率 ÷ 波特率。这个值在开发时就能确定,完全不需要FPGA运行时计算。
先看没优化的代码(技术栈:Verilog):
module uart_tx (
input clk, // 50MHz系统时钟
input rst_n, // 复位信号
input [7:0] data, // 要发送的8位数据
input tx_en, // 发送使能
output reg tx // 串口发送引脚
);
// 未优化:动态计算分频系数,占用逻辑单元
localparam SYS_CLK = 50000000; // 系统时钟50MHz
localparam BAUD = 9600; // 波特率9600
reg [15:0] cnt; // 分频计数器
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
cnt <= 0;
tx <= 1'b1;
end else if (tx_en) begin
// 这里的(SYS_CLK / BAUD)是动态计算,FPGA会生成除法电路
if (cnt < (SYS_CLK / BAUD)) begin
cnt <= cnt + 1;
end else begin
cnt <= 0;
// 后续发送逻辑...
end
end
end
endmodule
这段代码里,(SYS_CLK / BAUD)虽然是常数,但很多初学者会直接写在判断条件里,FPGA综合的时候会识别吗?大部分时候会,但如果是更复杂的组合,比如(SYS_CLK / BAUD + 1) * 2,有些综合工具就会生成除法电路,占用逻辑单元。
优化后的代码(技术栈:Verilog):
module uart_tx (
input clk, // 50MHz系统时钟
input rst_n, // 复位信号
input [7:0] data, // 要发送的8位数据
input tx_en, // 发送使能
output reg tx // 串口发送引脚
);
// 优化:提前计算好分频系数,用localparam定义
localparam SYS_CLK = 50000000; // 系统时钟50MHz
localparam BAUD = 9600; // 波特率9600
localparam DIV = SYS_CLK / BAUD; // 提前算好:50000000 ÷ 9600 = 5208(取整)
reg [15:0] cnt; // 分频计数器
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
cnt <= 0;
tx <= 1'b1;
end else if (tx_en) begin
// 直接用提前算好的DIV,没有动态计算
if (cnt < DIV) begin
cnt <= cnt + 1;
end else begin
cnt <= 0;
// 后续发送逻辑...
end
end
end
endmodule
优化后的代码,DIV是开发时就确定的常数,FPGA综合的时候不会生成除法电路,直接用这个固定值,至少能省掉5-10个逻辑单元,小项目可能不明显,但大项目里累积下来效果很显著。
2.2 用状态机替代冗余逻辑判断
状态机是FPGA开发里用来控制流程的常用工具,比如一个设备的“待机、工作、故障”状态,或者一个通信模块的“空闲、发送、接收”状态。很多初学者写状态机的时候,会在每个状态里写一堆重复的判断条件,导致逻辑单元浪费。
举个例子:假设我们要做一个LED流水灯模块,要求是“待机时LED全灭,工作时LED从左到右依次点亮,故障时LED全闪”。没优化的代码可能会这样写:
module led_flow (
input clk, // 50MHz系统时钟
input rst_n, // 复位信号
input [1:0] state, // 外部输入的状态:00=待机,01=工作,10=故障
output reg [3:0] led // 4位LED输出,0亮1灭
);
// 未优化:每个状态都重复判断时钟分频,逻辑冗余
reg [24:0] cnt; // 用来做1Hz的分频,让LED变化慢一点
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
cnt <= 0;
led <= 4'b1111; // 初始全灭
end else begin
cnt <= cnt + 1;
// 待机状态:全灭
if (state == 2'b00) begin
led <= 4'b1111;
// 工作状态:流水灯
end else if (state == 2'b01) begin
if (cnt == 25'd24999999) begin // 每1秒切换一次
cnt <= 0;
led <= {led[2:0], led[3]}; // 左移一位
end
// 故障状态:全闪
end else if (state == 2'b10) begin
if (cnt == 25'd24999999) begin // 每1秒切换一次
cnt <= 0;
led <= ~led; // 取反,实现闪烁
end
end
end
end
endmodule
这段代码的问题是,工作状态和故障状态都有“每1秒切换一次”的判断条件,重复写了两次,FPGA综合的时候会生成两个一模一样的分频判断电路,占用双倍的逻辑单元。
优化后的状态机代码(技术栈:Verilog):
module led_flow (
input clk, // 50MHz系统时钟
input rst_n, // 复位信号
input [1:0] state, // 外部输入的状态:00=待机,01=工作,10=故障
output reg [3:0] led // 4位LED输出,0亮1灭
);
// 优化:把公共的判断条件提出来,只做一次判断
reg [24:0] cnt; // 1Hz分频计数器
reg tick; // 用来标记“每1秒到了”的信号
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
cnt <= 0;
tick <= 1'b0;
led <= 4'b1111;
end else begin
// 公共的1秒判断,只做一次
if (cnt == 25'd24999999) begin
cnt <= 0;
tick <= 1'b1; // 到1秒时标记为1
end else begin
cnt <= cnt + 1;
tick <= 1'b0; // 没到1秒时标记为0
end
// 不同状态只处理各自的逻辑,不用重复判断
case (state)
2'b00: led <= 4'b1111; // 待机全灭
2'b01: begin // 工作状态
if (tick) begin // 到1秒时才切换流水灯
led <= {led[2:0], led[3]};
end
end
2'b10: begin // 故障状态
if (tick) begin // 到1秒时才切换闪烁
led <= ~led;
end
end
default: led <= 4'b1111;
endcase
end
end
endmodule
优化后的代码,把“每1秒切换一次”的判断做成了一个公共的tick信号,所有状态都用这个信号,只做一次判断,省掉了一个分频电路,逻辑单元占用减少了一半左右。
三、存储资源(BRAM)的优化方法
存储资源浪费的主要原因是缓存的大小没算准、用了没必要的缓存,或者没利用好FPGA的专用存储结构。比如一个模块需要存100个数据,却定义了能存1000个的存储块,相当于租了100平的房子只放10平的东西,剩下的都浪费了。
3.1 精准定义存储块的大小
很多初学者写存储块的时候,会习惯性地用大一点的宽度和深度,觉得“万一不够用呢”,结果导致存储资源浪费。比如一个模块需要存“0-99”的整数,只需要7位宽度(因为2^6=64<100,2^7=128>100),深度是100就够了,但如果写成“reg [15:0] mem [0:1023]”,那这个存储块的大小就是16位×1024=16384位,而实际只需要7位×100=700位,浪费了20多倍的存储资源。
举个实际的例子:假设我们要做一个波形发生器,需要存一个正弦波的采样点,采样点是0-255(8位),总共128个采样点。
没优化的代码(技术栈:Verilog):
module wave_gen (
input clk, // 50MHz系统时钟
input rst_n, // 复位信号
input [6:0] addr, // 地址,0-127
output reg [7:0] wave_data // 输出的波形数据
);
// 未优化:存储块定义过大,浪费存储资源
reg [15:0] sin_mem [0:1023]; // 16位宽度,1024深度,实际只用8位、128深度
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
wave_data <= 8'b0;
end else begin
// 只用到低8位,高8位完全浪费
wave_data <= sin_mem[addr][7:0];
end
end
// 初始化正弦波数据,只给前128个地址赋值
initial begin
sin_mem[0] = 16'd128;
sin_mem[1] = 16'd134;
sin_mem[2] = 16'd140;
// ... 一直到sin_mem[127]
// 后面的128-1023地址都没用到,完全浪费
end
endmodule
这段代码的存储块大小是16位×1024,实际只需要8位×128,浪费了(16×1024 - 8×128)/(8×128) = 15倍的存储资源。
优化后的代码(技术栈:Verilog):
module wave_gen (
input clk, // 50MHz系统时钟
input rst_n, // 复位信号
input [6:0] addr, // 地址,0-127(因为2^7=128,足够覆盖0-127)
output reg [7:0] wave_data // 输出的波形数据
);
// 优化:精准定义存储块的宽度和深度
reg [7:0] sin_mem [0:127]; // 8位宽度,128深度,刚好满足需求
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
wave_data <= 8'b0;
end else begin
wave_data <= sin_mem[addr]; // 直接用整个宽度,没有浪费
end
end
// 初始化正弦波数据,刚好覆盖所有地址
initial begin
sin_mem[0] = 8'd128;
sin_mem[1] = 8'd134;
sin_mem[2] = 8'd140;
// ... 一直到sin_mem[127],没有多余的地址
end
endmodule
优化后的存储块大小刚好是需求的大小,没有任何浪费,存储资源占用减少了15倍,非常可观。
3.2 用“移位寄存器”替代“小型存储块”
如果需要存的是连续的几个数据,比如存最近3个输入的信号,用来做滤波,很多人会用存储块,但其实用移位寄存器更省资源。移位寄存器的原理是“每来一个时钟,所有数据往后移一位,新数据放到第一位”,相当于一个小型的“队列”,但它占用的是逻辑单元,而逻辑单元的成本通常比存储资源低,尤其是小型的移位寄存器,比用存储块划算。
举个例子:假设我们要做一个简单的滤波模块,需要存最近3个输入的8位数据,然后求平均值作为输出。
没优化的代码(技术栈:Verilog):
module avg_filter (
input clk, // 50MHz系统时钟
input rst_n, // 复位信号
input [7:0] din, // 输入的8位数据
output reg [7:0] avg // 输出的平均值
);
// 未优化:用存储块存3个数据,占用存储资源
reg [7:0] mem [0:2]; // 8位宽度,3深度的存储块
reg [1:0] cnt; // 用来记录当前写的地址
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
cnt <= 2'b00;
avg <= 8'b0;
end else begin
// 把新数据写到存储块里
mem[cnt] <= din;
// 地址加1,到2后回到0
cnt <= (cnt == 2'b10) ? 2'b00 : cnt + 1;
// 求3个数据的平均值
avg <= (mem[0] + mem[1] + mem[2]) / 3;
end
end
endmodule
这段代码用存储块存3个数据,虽然存储块很小,但FPGA的存储资源是按“块”分配的,比如一个最小的BRAM块是1024位,哪怕你只需要24位(3×8),也得占用一整个BRAM块,相当于浪费了97%的存储资源。
优化后的代码(技术栈:Verilog):
module avg_filter (
input clk, // 50MHz系统时钟
input rst_n, // 复位信号
input [7:0] din, // 输入的8位数据
output reg [7:0] avg // 输出的平均值
);
// 优化:用移位寄存器存3个数据,占用逻辑单元
reg [7:0] d0, d1, d2; // d0是最新的数据,d1是次新,d2是最旧
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
d0 <= 8'b0;
d1 <= 8'b0;
d2 <= 8'b0;
avg <= 8'b0;
end else begin
// 移位:所有数据往后移一位,新数据放到d0
d2 <= d1;
d1 <= d0;
d0 <= din;
// 求3个数据的平均值
avg <= (d0 + d1 + d2) / 3;
end
end
endmodule
优化后的代码用3个寄存器存数据,占用的是逻辑单元,而不是宝贵的存储资源,因为逻辑单元的数量通常比存储资源多,而且这种小型的移位寄存器只会占用20-30个逻辑单元,比占用一整个BRAM块划算很多。
四、优化方法的应用场景、优缺点与注意事项
4.1 应用场景
- 逻辑单元优化:适合所有FPGA项目,尤其是逻辑资源紧张的小芯片(比如Cyclone IV、Artix-7的低端型号),或者功能复杂的大项目(比如高速通信、图像处理)。
- 存储资源优化:适合需要大量缓存的项目,比如音频处理、视频编解码、高速数据采集,或者存储资源紧张的项目(比如用了小容量FPGA的低成本产品)。
4.2 优缺点
| 优化方法 | 优点 | 缺点 |
|---|---|---|
| 静态计算替代动态计算 | 逻辑单元占用少,速度快,可靠性高 | 只能用在结果固定的计算,灵活性低 |
| 状态机提取公共判断 | 逻辑单元占用少,代码简洁,易维护 | 对复杂状态机的设计能力有一定要求 |
| 精准定义存储块大小 | 存储资源占用少,没有浪费 | 需要提前精准计算需求,不能随意调整大小 |
| 移位寄存器替代小型存储块 | 节省存储资源,成本低 | 只适合存少量连续数据,存多了会占逻辑单元 |
4.3 注意事项
- 不要为了优化而优化:比如一个项目逻辑单元和存储资源都很充足,就没必要花时间优化,不然会增加开发成本,甚至引入新的bug。
- 优化前先分析资源占用:用FPGA开发工具(比如Quartus、Vivado)的资源报告,先找出是逻辑单元还是存储资源紧张,再针对性优化,不要盲目优化。
- 优化后要做验证:优化后的代码一定要做功能仿真和时序仿真,确保功能和原来的代码一致,避免因为优化导致功能错误。
五、文章总结
FPGA开发中的资源优化,本质上是“合理分配资源”,就像整理房间一样,把不需要的东西扔掉(冗余逻辑)、把常用的东西放在方便拿的地方(静态计算)、把大东西拆成小块(精准定义存储)。
本文讲的几种优化方法,都是从实际开发中总结出来的,不需要复杂的理论知识,只要代码写得“细”一点,就能大幅减少资源占用。比如静态计算只要把提前能算的数写对,状态机只要把公共判断提出来,存储块只要精准定义大小,移位寄存器只要用对场景。
最后要提醒的是,优化是一个循序渐进的过程,不要一开始就追求极致优化,先把功能做对,再根据资源报告逐步优化,这样既能保证项目进度,又能达到资源优化的目的。
评论
围绕“Verilog资源优化方法:减少FPGA开发中逻辑单元与存储资源的占用”参与讨论