【STM32 通用】JPEG Codec 硬件编/解码

1. 引言

除了使用外部 TJpgDec 软件解码库之外,ST 官方还提供了硬件 JPEG 编码解码的方式。

2. JPEG Codec 的基本概念

我使用的 STM32H757 内部有一个独立的 JPEG Codec 外设,专门完成 JPEG 压缩与解压缩计算。CPU 不负责核心变换运算,只负责:

  1. 准备输入/输出缓冲
  2. 启动一次编码解码
  3. 在中断回调里继续提供下一块数据或者取走下一块的数据
  4. 处理完成后的业务逻辑(转色、存文件、显示等)

2.1 数据通路

内存输入缓冲
  ↓ 码流或 MCU
MDMA(InFIFO)
  ↓ 送入 JPEG 输入 FIFO
JPEG 外设
  ↓ 硬件编码/解码计算
MDMA(OutFIFO)
  ↓ 从 JPEG 输出 FIFO 取数
内存输出缓冲
  MCU 或码流

2.2 JPEG 编码

内部图像数据转外部 JPEG 文件

摄像头/内存中的 RGB/YUV 图像数据
        ↓
   JPEG 编码器(硬件或软件)
        ↓
   JPEG 码流
        ↓
   写入 SD 卡,变成外部 .jpg 文件

2.3 JPEG 解码

外部 JPEG 文件转 RGB

SD卡/FATFS 里的 JPEG 文件
        ↓ 读取 JPEG 码流
   JPEG 解码器(硬件或软件)
        ↓
   RGB/YUV 像素数据
        ↓
   LCD 显示 / 图像处理

3. 配置 JPEG 硬件编解码流程

3.1 CubeMX 配置

配置 JPEG 硬件编解码参数:
image

开启 JPEG 中断:
image

配置输入和输出的 MDMA:
image

生成代码:
注意:jpeg_utils_conf_template.h 需要改成 jpeg_utils_conf.h
image

3.2 应用代码

代码说明:

数据流如下:
JPEG 压缩数据 -> SDRAM 输入缓冲 -> MDMA -> JPEG Codec
-> YCbCr MCU 缓冲(SDRAM) -> jpeg_utils -> RGB565 缓冲(SDRAM)
-> LTDC 帧缓冲(SDRAM) -> LCD。

JPEG Codec 负责解析码流并输出按 MCU 排列的 YCbCr 数据,
jpeg_utils 根据 JPEG 头中的颜色空间和色度采样方式将其转换为 RGB565。
由于输入、MCU、RGB 和 LTDC 帧缓冲均位于可缓存 SDRAM,
CPU 与 MDMA/LTDC 切换缓冲所有权时必须执行相应的 D-Cache Clean 或 Invalidate 操作。

jpeg_hw_decode 是用户调用的入口,完整流程如下:
1. 检查参数和 SOI 标记
2. 复制码流到 SDRAM,按 32 字节对齐
3. 启动 JPEG Codec 和 MDMA
4. 轮询等待完成或超时(WFI 低功耗等待)
5. 失效 MCU 缓冲 Cache
6. YCbCr → RGB565 转换
7. 逐行复制到 LTDC 帧缓冲显示
回调函数 触发时机 作用
HAL_JPEG_InfoReadyCallback JPEG 头解析完成 保存图片尺寸、颜色空间、色度采样等信息
HAL_JPEG_GetDataCallback 当前输入块被消耗完 向 JPEG Codec 提供下一块码流数据
HAL_JPEG_DataReadyCallback JPEG Codec 输出一块 MCU 数据 累计已解码的 MCU 数据长度
HAL_JPEG_DecodeCpltCallback 整张图片解码完成 置位完成标志,唤醒主循环
HAL_JPEG_ErrorCallback 硬件或 MDMA 出错 置位错误标志,终止解码流程
函数 作用
jpeg_cache_clean CPU 写入 SDRAM 后,将 D-Cache 脏数据写回主存,确保 MDMA/LTDC 能读到最新数据
jpeg_cache_invalidate MDMA 写入完成后,失效 CPU 的 D-Cache,迫使 CPU 从主存重新读取
jpeg_convert_to_rgb565 根据 JPEG 头信息,将 YCbCr MCU 数据转换为 RGB565 像素
jpeg_copy_rgb565_to_lcd 将紧密排列的 RGB565 图像逐行复制到 LTDC 帧缓冲,考虑了 RGB 和 LCD 行宽差异

3.2.1 配置 SDRAM 缓冲布局

#include "app_hw_jpeg.h"
#include <string.h>
#include <stdio.h>
#include "jpeg.h"
#include "jpeg_utils.h"
#include "ltdc.h"
#include "lcd.h"

/*
 * SDRAM 缓冲布局。地址避开从 0xD0000000 开始的 800 x 480 RGB565
 * LTDC 帧缓冲,各区域之间也不重叠。
 */
#define JPEG_MCU_BUFFER_ADDR 0xD0100000U
#define JPEG_RGB_BUFFER_ADDR 0xD0110000U
#define JPEG_INPUT_BUFFER_ADDR 0xD01E0000U
#define JPEG_MCU_BUFFER_SIZE (32U * 1024U)
#define JPEG_RGB_BUFFER_SIZE (800U * 480U * 2U)

3.2.2 配置缓冲容量

/* JPEG 输入和输出 MDMA 单次可访问的最大缓冲容量。 */
#define JPEG_INPUT_CHUNK_SIZE (32U * 1024U)
#define JPEG_OUTPUT_BUFFER_SIZE (32U * 1024U)
/* JPEG FIFO 阈值请求对应 32 字节 MDMA BufferTransferLength。 */
#define JPEG_MDMA_TRANSFER_UNIT 32U

/* JPEG Codec 通过 MDMA 将解码后的 YCbCr MCU 数据写入该缓冲。 */
#define JPEG_MCU_BUFFER \
  ((uint8_t *)JPEG_MCU_BUFFER_ADDR)

/* jpeg_utils 将 MCU 数据转换为按图片宽度紧密排列的 RGB565 像素。 */
#define JPEG_RGB_BUFFER \
  ((uint8_t *)JPEG_RGB_BUFFER_ADDR)

/* 原始码流复制到该缓冲并按 MDMA 传输单元补齐后交给 JPEG Codec。 */
#define JPEG_INPUT_BUFFER \
  ((uint8_t *)JPEG_INPUT_BUFFER_ADDR)

3.2.3 定义相关变量

/**
 * @brief 一次硬件 JPEG 解码会话的异步状态
 *
 * JPEG 和 MDMA 中断回调通过该结构与同步等待流程交换状态。active 用于
 * 拒绝并发解码,并防止会话结束后到达的回调修改下一次解码状态。
 */
typedef struct
{
  volatile uint8_t active;     // 会话是否仍在进行
  volatile uint8_t info_ready; // JPEG 头解析完成后置位
  volatile uint8_t done;       // 完成解码后置位
  volatile uint8_t error;      // 检测到外设或 MDMA 错误后置位 
  volatile uint32_t in_off;    // 已被 JPEG Codec 消耗的输入字节数 
  uint32_t in_size;            // 按 MDMA 传输单元补齐后的输入长度 
  uint32_t out_size;           // JPEG Codec 实际输出的 MCU 数据字节数
  uint32_t rgb_stride;         // MCU 对齐后的 RGB 缓冲行宽,单位为像素
  JPEG_ConfTypeDef info;       // 图片尺寸、颜色空间和色度采样信息
} jpeg_session_t;

/* HAL JPEG 回调没有用户上下文参数,因此当前实现使用单个全局会话。 */
static jpeg_session_t g_jpeg;
static const uint8_t *g_jpeg_input = NULL;

3.2.4 回调函数逻辑

/**
 * @brief  保存 JPEG Codec 解析得到的图片信息
 * @param  hjpeg JPEG 外设句柄
 * @param  info  JPEG 头中的尺寸、颜色空间和色度采样信息
 *
 * @note 该回调只表示 JPEG 头解析完成,不表示整张图片已经解码完成。
 */
void HAL_JPEG_InfoReadyCallback(JPEG_HandleTypeDef *hjpeg,
                                JPEG_ConfTypeDef *info)
{
  (void)hjpeg;

  if (!g_jpeg.active)
  {
    return;
  }

  g_jpeg.info = *info;
  g_jpeg.info_ready = 1U;
}

/**
 * @brief  在当前输入块被消耗后向 JPEG Codec 提供下一块码流
 * @param  hjpeg JPEG 外设句柄
 * @param  length 当前输入块已消耗的字节数
 *
 * @note 当全部输入已提交时,必须使用长度 0 配置输入缓冲,通知 HAL
 *       不再有后续数据。当前输入已整体复制到连续 SDRAM 缓冲中。
 */
void HAL_JPEG_GetDataCallback(JPEG_HandleTypeDef *hjpeg, uint32_t length)
{
  uint32_t remaining;
  uint32_t next_length;

  if (!g_jpeg.active)
  {
    return;
  }

  /* 防止异常回调长度导致无符号减法下溢和输入地址越界。 */
  if (length > g_jpeg.in_size - g_jpeg.in_off)
  {
    g_jpeg.error = 1U;
    HAL_JPEG_Pause(hjpeg, JPEG_PAUSE_RESUME_INPUT);
    return;
  }

  g_jpeg.in_off += length;
  remaining = g_jpeg.in_size - g_jpeg.in_off;

  /* 长度 0 是 HAL JPEG 表示“输入流已经结束”的约定。 */
  if (remaining == 0U)
  {
    HAL_JPEG_ConfigInputBuffer(
        hjpeg,
        (uint8_t *)g_jpeg_input + g_jpeg.in_off,
        0U);
    return;
  }

  next_length = remaining;
  if (next_length > JPEG_INPUT_CHUNK_SIZE)
  {
    next_length = JPEG_INPUT_CHUNK_SIZE;
  }

  HAL_JPEG_ConfigInputBuffer(
      hjpeg,
      (uint8_t *)g_jpeg_input + g_jpeg.in_off,
      next_length);
}

/**
 * @brief  记录 JPEG Codec 已写入输出缓冲的 MCU 数据长度
 * @param  hjpeg JPEG 外设句柄
 * @param  data  本次输出数据的首地址
 * @param  length 本次输出数据长度(字节数)
 *
 * @note 当前实现要求整张图片的 MCU 数据不超过 32 KiB,因此这里只累计
 *       输出长度,不配置下一输出缓冲。大图需要在此处接入分块或双缓冲。
 */
void HAL_JPEG_DataReadyCallback(JPEG_HandleTypeDef *hjpeg,
                                uint8_t *data, uint32_t length)
{
  (void)hjpeg;
  (void)data;

  if (!g_jpeg.active)
  {
    return;
  }

  /* 检查累计长度,避免后续颜色转换读取固定 MCU 缓冲之外的数据。 */
  if (length > JPEG_OUTPUT_BUFFER_SIZE - g_jpeg.out_size)
  {
    g_jpeg.error = 1U;
    return;
  }

  g_jpeg.out_size += length;
}

/**
 * @brief  标记 JPEG 硬件解码完成
 * @param  hjpeg JPEG 外设句柄
 *
 * @note HAL 已传输完最后一段输出数据后才调用该回调。
 */
void HAL_JPEG_DecodeCpltCallback(JPEG_HandleTypeDef *hjpeg)
{
  (void)hjpeg;

  if (g_jpeg.active)
  {
    g_jpeg.done = 1U;
  }
}

/**
 * @brief  标记 JPEG Codec 或 MDMA 解码错误
 * @param  hjpeg JPEG 外设句柄
 */
void HAL_JPEG_ErrorCallback(JPEG_HandleTypeDef *hjpeg)
{
  (void)hjpeg;

  if (g_jpeg.active)
  {
    g_jpeg.error = 1U;
  }
}

3.2.5 内部辅助功能函数

/**
 * @brief  将指定范围的 D-Cache 脏数据写回主存
 * @param  address 缓存起始地址
 * @param  length  缓存长度(字节数)
 *
 * @note Cortex-M7 D-Cache Line 为 32 字节,因此操作范围向两端扩展到
 *       Cache Line 边界。用于 CPU 写、MDMA 或 LTDC 读的所有权切换。
 */
static void jpeg_cache_clean(const void *address, uint32_t length)
{
  uintptr_t start;
  uintptr_t end;

  start = (uintptr_t)address & ~(uintptr_t)31U;
  end = ((uintptr_t)address + length + 31U) & ~(uintptr_t)31U;

  SCB_CleanDCache_by_Addr((uint32_t *)start,
                          (int32_t)(end - start));
  __DSB();
}

/**
 * @brief  失效指定范围的 D-Cache,使 CPU 后续从主存重新读取数据
 * @param  address 缓存起始地址
 * @param  length  缓存长度(字节数)
 *
 * @note 用于 MDMA 写、CPU 读的所有权切换;操作范围按 32 字节 Cache
 *       Line 对齐,避免 CPU 继续读取 MDMA 写入前的缓存副本。
 */
static void jpeg_cache_invalidate(void *address, uint32_t length)
{
  uintptr_t start;
  uintptr_t end;

  start = (uintptr_t)address & ~(uintptr_t)31U;
  end = ((uintptr_t)address + length + 31U) & ~(uintptr_t)31U;

  SCB_InvalidateDCache_by_Addr((uint32_t *)start,
                               (int32_t)(end - start));
  __DSB();
}

/**
 * @brief  根据 JPEG 头信息将 YCbCr MCU 数据转换为 RGB565
 * @return 0 表示转换成功,-1 表示图片尺寸、颜色格式或 MCU 数量异常
 *
 * @note JPEG_GetDecodeColorConvertFunc() 根据颜色空间和色度采样方式选择
 *       对应转换函数。RGB 输出按图片自身宽度紧密排列,不含 LCD 行偏移。
 */
static int jpeg_convert_to_rgb565(void)
{
  JPEG_ConfTypeDef convert_info;
  JPEG_YCbCrToRGB_Convert_Function convert;
  uint32_t mcu_width;
  uint32_t mcu_height;
  uint32_t image_mcu_count;
  uint32_t converted_bytes;
  uint32_t converted_mcus;

  if (!g_jpeg.info_ready)
  {
    return -1;
  }

  /* 当前 RGB 缓冲和 LCD 均按最大 800 x 480 RGB565 设计。 */
  if ((g_jpeg.info.ImageWidth > 800U) ||
      (g_jpeg.info.ImageHeight > 480U))
  {
    return -1;
  }

  if ((g_jpeg.info.ColorSpace == JPEG_YCBCR_COLORSPACE) &&
      (g_jpeg.info.ChromaSubsampling == JPEG_420_SUBSAMPLING))
  {
    mcu_width = 16U;
    mcu_height = 16U;
  }
  else if ((g_jpeg.info.ColorSpace == JPEG_YCBCR_COLORSPACE) &&
           (g_jpeg.info.ChromaSubsampling == JPEG_422_SUBSAMPLING))
  {
    mcu_width = 16U;
    mcu_height = 8U;
  }
  else
  {
    mcu_width = 8U;
    mcu_height = 8U;
  }

  /*
   * jpeg_utils 按完整 MCU 写入边缘像素,却使用 ImageWidth 作为 RGB 行跨度。
   * 原图宽度不是 MCU 整数倍时,右侧填充像素会串写到下一行。转换阶段
   * 临时使用 MCU 对齐尺寸,为边缘填充像素保留独立空间;显示时再裁剪。
   */
  convert_info = g_jpeg.info;
  convert_info.ImageWidth = (g_jpeg.info.ImageWidth + mcu_width - 1U) &
                            ~(mcu_width - 1U);
  convert_info.ImageHeight = (g_jpeg.info.ImageHeight + mcu_height - 1U) &
                             ~(mcu_height - 1U);

  if ((convert_info.ImageWidth * convert_info.ImageHeight * 2U) >
      JPEG_RGB_BUFFER_SIZE)
  {
    return -1;
  }

  if (JPEG_GetDecodeColorConvertFunc(&convert_info,
                                     &convert,
                                     &image_mcu_count) != HAL_OK)
  {
    return -1;
  }

  g_jpeg.rgb_stride = convert_info.ImageWidth;
  converted_bytes = 0U;

  /* 转换函数只处理完整 MCU,返回值应与 JPEG 头计算的 MCU 总数一致。 */
  converted_mcus = convert(JPEG_MCU_BUFFER,
                           JPEG_RGB_BUFFER,
                           0U,
                           g_jpeg.out_size,
                           &converted_bytes);

  if (converted_mcus != image_mcu_count)
  {
    return -1;
  }

  return 0;
}

/**
 * @brief  将紧密排列的 RGB565 图像复制到指定 LCD 显示区域
 * @param  x LCD 显示区域的起始 X 坐标
 * @param  y LCD 显示区域的起始 Y 坐标
 * @return 0 表示复制成功,-1 表示目标区域超出 LCD 边界
 *
 * @note RGB 缓冲每行只有 ImageWidth 个像素,而 LTDC 帧缓冲每行有
 *       lcddev.width 个像素,因此必须逐行复制并使用 LCD 宽度计算目标地址。
 */
static int jpeg_copy_rgb565_to_lcd(uint16_t x, uint16_t y)
{
  uint32_t width;
  uint32_t height;
  uint32_t row;

  width = g_jpeg.info.ImageWidth;
  height = g_jpeg.info.ImageHeight;

  if ((x + width > lcddev.width) ||
      (y + height > lcddev.height))
  {
    return -1;
  }

  for (row = 0U; row < height; row++)
  {
    const uint16_t *source;
    uint16_t *destination;

    source = (const uint16_t *)JPEG_RGB_BUFFER +
             row * g_jpeg.rgb_stride;

    destination = (uint16_t *)LTDC_FRAME_BUF_ADDR +
                  (y + row) * lcddev.width +
                  x;

    memcpy(destination,
           source,
           width * sizeof(uint16_t));

    /* CPU 写入可缓存 SDRAM 后执行 Clean,确保 LTDC 能读到最新像素。 */
    jpeg_cache_clean(destination,
                     width * sizeof(uint16_t));
  }

  __DSB();
  return 0;
}

3.2.6 JPEG 解码主功能函数

/**
 * @brief  使用 JPEG Codec 解码 JPEG,并将图像输出到指定 LCD 坐标
 * @param  data JPEG 压缩数据指针
 * @param  size JPEG 压缩数据长度(字节数)
 * @param  x    图片左上角 X 坐标(0 为 LCD 左边界)
 * @param  y    图片左上角 Y 坐标(0 为 LCD 上边界)
 * @return 0 表示解码并显示成功,-1 表示参数、硬件解码、转换或显示失败
 *
 * 执行步骤:
 * 1. 检查参数、JPEG SOI 标记和会话状态;
 * 2. 将码流复制到 SDRAM,并按 32 字节 MDMA 传输单元补齐;
 * 3. 启动 JPEG Codec 和 MDMA,等待完成、错误或超时;
 * 4. 失效 MCU 缓冲 Cache,将 YCbCr MCU 数据转换为 RGB565;
 * 5. 按 LCD 行跨度将 RGB565 图像复制到 LTDC 帧缓冲。
 *
 * @note 当前函数同步等待异步硬件回调,且全局会话只允许一次解码操作。
 */
int jpeg_hw_decode(const uint8_t *data, uint32_t size, uint16_t x, uint16_t y)
{
  uint32_t input_size;
  uint32_t first_input;
  uint32_t start_tick;

  if ((data == NULL) || (size < 4U))
  {
    return -1;
  }

  /* JPEG 文件必须以 SOI 标记 0xFFD8 开始。 */
  if ((data[0] != 0xFFU) || (data[1] != 0xD8U))
  {
    return -1;
  }

  /* 当前 HAL 回调共享全局状态,不允许两个调用者同时解码。 */
  if (g_jpeg.active)
  {
    return -1;
  }

  /*
   * HAL 会把输入长度向下截断到 MDMA BufferTransferLength 的整数倍。
   * 先向上补齐可避免非对齐 JPEG 的末尾 EOI 标记被截掉。
   */
  input_size = (size + JPEG_MDMA_TRANSFER_UNIT - 1U) &
               ~(JPEG_MDMA_TRANSFER_UNIT - 1U);
  if (input_size > JPEG_INPUT_CHUNK_SIZE)
  {
    return -1;
  }

  memset(&g_jpeg, 0, sizeof(g_jpeg));

  /*
   * 输入源可能位于 Flash 或任意 RAM。统一复制到 MDMA 可访问的 SDRAM,
   * 补零不会影响已经由 EOI 标记结束的 JPEG 码流。
   */
  memcpy(JPEG_INPUT_BUFFER, data, size);
  memset(JPEG_INPUT_BUFFER + size, 0, input_size - size);
  jpeg_cache_clean(JPEG_INPUT_BUFFER, input_size);

  g_jpeg_input = JPEG_INPUT_BUFFER;
  g_jpeg.in_size = input_size;
  g_jpeg.active = 1U;

  first_input = input_size;
  if (first_input > JPEG_INPUT_CHUNK_SIZE)
  {
    first_input = JPEG_INPUT_CHUNK_SIZE;
  }

  /*
   * 输出位于 Cacheable SDRAM。
   * 防止已有脏 Cache Line 在 DMA 完成后覆盖 MDMA 写入的数据。
   */
  jpeg_cache_clean(JPEG_MCU_BUFFER,
                   JPEG_OUTPUT_BUFFER_SIZE);
  jpeg_cache_invalidate(JPEG_MCU_BUFFER,
                        JPEG_OUTPUT_BUFFER_SIZE);

  /* HAL_JPEG_Decode_DMA() 非阻塞,后续状态由 JPEG/MDMA 中断回调更新。 */
  if (HAL_JPEG_Decode_DMA(&hjpeg,
                          JPEG_INPUT_BUFFER,
                          first_input,
                          JPEG_MCU_BUFFER,
                          JPEG_OUTPUT_BUFFER_SIZE) != HAL_OK)
  {
    g_jpeg.active = 0U;
    return -1;
  }

  /* 等待中断更新会话状态;WFI 可降低等待期间的 CPU 活动。 */
  start_tick = HAL_GetTick();

  while (!g_jpeg.done &&
         !g_jpeg.error &&
         ((HAL_GetTick() - start_tick) < 5000U))
  {
    __WFI();
  }

  /* 超时不一定设置 HAL 错误码,因此也以 done 标志判断是否真正完成。 */
  if (!g_jpeg.done || g_jpeg.error)
  {
    HAL_JPEG_Abort(&hjpeg);
    g_jpeg.active = 0U;

    printf("CM7: HW JPEG failed, error=0x%08lx\n",
           HAL_JPEG_GetError(&hjpeg));
    return -1;
  }

  /* MDMA 已写完 MCU 缓冲,CPU 读取前丢弃对应的旧 Cache Line。 */
  jpeg_cache_invalidate(JPEG_MCU_BUFFER,
                        g_jpeg.out_size);

  if (jpeg_convert_to_rgb565() != 0)
  {
    g_jpeg.active = 0U;
    return -1;
  }

  if (jpeg_copy_rgb565_to_lcd(x, y) != 0)
  {
    g_jpeg.active = 0U;
    return -1;
  }

  printf("CM7: HW JPEG %lux%lu, MCU=%lu bytes\n",
         g_jpeg.info.ImageWidth,
         g_jpeg.info.ImageHeight,
         g_jpeg.out_size);

  g_jpeg.active = 0U;
  return 0;
}

3.2.7 调用方式

JPEG_InitColorTables();
if (jpeg_hw_decode(jpeg_test_data, sizeof(jpeg_test_data), 0U, 0U) != 0)
{
    printf("CM7: Hardware JPEG decode failed\n");
}

博客导航

博客导航

posted @ 2026-09-21 22:49  膝盖中箭卫兵  阅读(20)  评论(0)    收藏  举报
ORCID iD icon https://orcid.org/0000-0001-5102-772X