基于FPGA的BMP压缩哈夫曼编码器:从原理到硬件实现

1次阅读
没有评论

共计 2393 个字符,预计需要花费 6 分钟才能阅读完成。

image.webp

为什么需要硬件加速 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)实现:

  1. 将频率统计结果存入双端口 BRAM(端口 A 用于读取,端口 B 用于写入)
  2. 设计一个比较器阵列,每次同时比较 8 组节点找出最小值
  3. 合并节点时,新生成的父节点直接写入 BRAM 空闲区域

资源节省技巧:
– 用 16bit 存储频率值(0-65535 足够表示 1080P 图像的像素频次)
– 树节点指针用 9bit(支持最多 512 个节点)
– 通过 BRAM 的字节写使能信号实现部分写入

3. 并行编码输出电路

基于 FPGA 的 BMP 压缩哈夫曼编码器:从原理到硬件实现
(示波器实际抓取的编码波形,显示编码位流与 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

进阶思考方向

  1. 动态哈夫曼表更新:可以增加 BRAM 的读写仲裁逻辑,在帧间隙期间更新码表
  2. 高色深支持:将统计模块的位宽改为parameter WIDTH=16,并扩展比较器位数
  3. Zynq MPSoC 协同:用 DMA 将图像数据直接推送到 FPGA,通过中断通知 PS 端读取压缩结果

实测效果

用 SD 卡存入 100 张 1280×720 的 BMP 图片进行测试:
– 软件压缩(ARM Cortex-A53):平均耗时 248ms/ 张
– 本 FPGA 方案:平均耗时 46ms/ 张
– 功耗监测显示 FPGA 方案仅增加 1.8W 动态功耗

对于需要长时间工作的电池设备,这种硬件加速方案能显著提升续航能力。下次可以试试把设计移植到 Lattice 的低功耗 FPGA 上,应该还能进一步优化能效比。

正文完
 0
评论(没有评论)