一、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 应用场景

  1. 逻辑单元优化:适合所有FPGA项目,尤其是逻辑资源紧张的小芯片(比如Cyclone IV、Artix-7的低端型号),或者功能复杂的大项目(比如高速通信、图像处理)。
  2. 存储资源优化:适合需要大量缓存的项目,比如音频处理、视频编解码、高速数据采集,或者存储资源紧张的项目(比如用了小容量FPGA的低成本产品)。

4.2 优缺点

优化方法 优点 缺点
静态计算替代动态计算 逻辑单元占用少,速度快,可靠性高 只能用在结果固定的计算,灵活性低
状态机提取公共判断 逻辑单元占用少,代码简洁,易维护 对复杂状态机的设计能力有一定要求
精准定义存储块大小 存储资源占用少,没有浪费 需要提前精准计算需求,不能随意调整大小
移位寄存器替代小型存储块 节省存储资源,成本低 只适合存少量连续数据,存多了会占逻辑单元

4.3 注意事项

  1. 不要为了优化而优化:比如一个项目逻辑单元和存储资源都很充足,就没必要花时间优化,不然会增加开发成本,甚至引入新的bug。
  2. 优化前先分析资源占用:用FPGA开发工具(比如Quartus、Vivado)的资源报告,先找出是逻辑单元还是存储资源紧张,再针对性优化,不要盲目优化。
  3. 优化后要做验证:优化后的代码一定要做功能仿真和时序仿真,确保功能和原来的代码一致,避免因为优化导致功能错误。

五、文章总结

FPGA开发中的资源优化,本质上是“合理分配资源”,就像整理房间一样,把不需要的东西扔掉(冗余逻辑)、把常用的东西放在方便拿的地方(静态计算)、把大东西拆成小块(精准定义存储)。

本文讲的几种优化方法,都是从实际开发中总结出来的,不需要复杂的理论知识,只要代码写得“细”一点,就能大幅减少资源占用。比如静态计算只要把提前能算的数写对,状态机只要把公共判断提出来,存储块只要精准定义大小,移位寄存器只要用对场景。

最后要提醒的是,优化是一个循序渐进的过程,不要一开始就追求极致优化,先把功能做对,再根据资源报告逐步优化,这样既能保证项目进度,又能达到资源优化的目的。