共计 2393 个字符,预计需要花费 6 分钟才能阅读完成。
为什么需要硬件加速 BMP 压缩?
在嵌入式图像采集系统中(比如工业摄像头或无人机图传),BMP 格式因结构简单而常被采用,但未经压缩的 BMP 会占用大量存储和带宽。用 CPU 做实时压缩时,即使是 ARM Cortex-A72 在 800MHz 下处理 1080P 图像也需要 50ms 以上,而 GPU 方案虽然吞吐量高,但功耗和成本对嵌入式设备不友好。
FPGA 的并行计算特性恰好能解决这个问题:通过定制化流水线,我们可以把哈夫曼编码的三大步骤(频率统计、树构建、编码输出)拆解成同步运行的硬件模块。实测表明,Xilinx Artix- 7 芯片在 100MHz 时钟下就能实现 5 倍于 CPU 的吞吐量,而功耗仅增加 2W。
硬件架构拆解
1. 频率统计的滑动窗口设计
传统软件需要遍历整个图像数据才能统计像素频率,这会导致 FPGA 设计出现长流水线延迟。我们的解决方案是采用 32 像素宽的滑动窗口:
parameter WIN_SIZE = 32;
reg [7:0] pixel_window[WIN_SIZE-1:0];
reg [15:0] histogram[255:0];
always @(posedge clk) begin
// 滑动窗口更新
for (int i=0; i<WIN_SIZE-1; i++)
pixel_window[i] <= pixel_window[i+1];
pixel_window[WIN_SIZE-1] <= new_pixel;
// 并行统计
for (int j=0; j<WIN_SIZE; j++)
histogram[pixel_window[j]] <= histogram[pixel_window[j]] + 1;
end
这个设计的关键点在于:
– 用阻塞 RAM(BRAM)实现 histogram 数组,避免使用分散的 LUTRAM
– 窗口更新和统计在同一个时钟周期完成
– 通过 WIN_SIZE 参数可适配不同性能需求
2. 哈夫曼树构建的硬件优化
软件常用递归方式建树,但这在硬件中会导致不可预测的延迟。我们改用最小堆(Min-Heap)实现:
- 将频率统计结果存入双端口 BRAM(端口 A 用于读取,端口 B 用于写入)
- 设计一个比较器阵列,每次同时比较 8 组节点找出最小值
- 合并节点时,新生成的父节点直接写入 BRAM 空闲区域
资源节省技巧:
– 用 16bit 存储频率值(0-65535 足够表示 1080P 图像的像素频次)
– 树节点指针用 9bit(支持最多 512 个节点)
– 通过 BRAM 的字节写使能信号实现部分写入
3. 并行编码输出电路

(示波器实际抓取的编码波形,显示编码位流与 valid 信号对齐)
编码阶段最易出现时序违例(信号无法在时钟周期内稳定),解决方法包括:
– 对码表 ROM 添加两级流水寄存器
– 使用独热码(One-Hot)编码状态机
– 关键路径插入流水线:
// 原组合逻辑
assign code_out = current_table[pixel];
// 改进后
always @(posedge clk) begin
stage1_pixel <= pixel;
stage2_code <= current_table[stage1_pixel];
code_out <= stage2_code;
end
实战数据:Xilinx Vivado 综合报告
在 Artix-7 xc7a100tcsg324- 1 器件上的资源占用:
| 模块 | LUT | FF | BRAM | DSP |
|---|---|---|---|---|
| 频率统计 | 423 | 587 | 2 | 0 |
| 哈夫曼树构建 | 1124 | 892 | 4 | 0 |
| 编码输出 | 687 | 453 | 1 | 0 |
| AXI-Stream 接口 | 156 | 231 | 0 | 0 |
时序收敛技巧:
1. 对跨时钟域信号(如 AXI 接口)设置 set_false_path
2. 用 Tcl 命令 create_clock -period 10.000 -name clk 明确约束主时钟
3. 对 BRAM 输出寄存器添加set_multicycle_path 2
完整 AXI-Stream 接口示例
module huffman_axi_wrapper #(parameter TDATA_WIDTH = 8)(
input wire clk,
input wire reset_n,
// AXI-Stream 输入
input wire [TDATA_WIDTH-1:0] s_axis_tdata,
input wire s_axis_tvalid,
output wire s_axis_tready,
// AXI-Stream 输出
output wire [31:0] m_axis_tdata,
output wire m_axis_tvalid,
input wire m_axis_tready
);
// 状态机定义(安全恢复设计)typedef enum {
IDLE,
RECV_HEADER,
PROCESSING,
SEND_CODE
} state_t;
(* fsm_encoding = "one_hot" *)
reg [3:0] current_state, next_state;
// 状态机复位保护
always @(posedge clk or negedge reset_n) begin
if (!reset_n) current_state <= IDLE;
else current_state <= next_state;
end
// 其余接口逻辑...
endmodule
进阶思考方向
- 动态哈夫曼表更新:可以增加 BRAM 的读写仲裁逻辑,在帧间隙期间更新码表
- 高色深支持:将统计模块的位宽改为
parameter WIDTH=16,并扩展比较器位数 - Zynq MPSoC 协同:用 DMA 将图像数据直接推送到 FPGA,通过中断通知 PS 端读取压缩结果
实测效果
用 SD 卡存入 100 张 1280×720 的 BMP 图片进行测试:
– 软件压缩(ARM Cortex-A53):平均耗时 248ms/ 张
– 本 FPGA 方案:平均耗时 46ms/ 张
– 功耗监测显示 FPGA 方案仅增加 1.8W 动态功耗
对于需要长时间工作的电池设备,这种硬件加速方案能显著提升续航能力。下次可以试试把设计移植到 Lattice 的低功耗 FPGA 上,应该还能进一步优化能效比。
