【STM32 通用】JPEG 软件解码

1. 引言

想要将图片显示到 LCD 屏幕上,就必须对图片进行解码,这里介绍的是 JPEG 软件解码。
本文所用硬件配置:

  • STM32H757XIH6
  • 正点原子 400 * 800 4.3 寸显示屏

2. TJpgDec 软件解码库

【下载地址】

image

image

2.1 调整 tjpgd_utils_conf.h

文件名调整后,代码中的包含关系和 CM7 Makefile 也必须同步修改。tjpgd_utils_conf.h 当前关键配置为:

#define JD_SZBUF       512
#define JD_FORMAT      1
#define JD_USE_SCALE   1
#define JD_TBLCLIP     1
#define JD_FASTDECODE  1

配置含义:

宏 值 作用
JD_SZBUF 512 TJpgDec 流输入缓冲大小
JD_FORMAT 1 输出 RGB565
JD_USE_SCALE 1 支持 1、1/2、1/4、1/8 输出缩放
JD_TBLCLIP 1 使用查表进行颜色限幅
JD_FASTDECODE 1 启用适合 32 位 MCU 的解码优化

其中 JD_FORMAT=1 很重要,因为当前 LTDC 帧缓冲也是 RGB565。这样软件解码后可以直接复制像素,不需要额外的颜色格式转换。

3. JPEG 软件解码的基本概念

JPEG 文件保存的是压缩后的图像数据,不是可以直接交给 LCD 的像素数组。解码器需要依次完成文件标记解析、Huffman 解码、反量化、IDCT、色度上采样和颜色转换,最终产生 RGB 像素。

3.1 JPEG Mark

JPEG 文件由多个 Marker 段组成,常见标记如下:

Marker 名称 作用
FFD8 SOI 图片开始
FFE0~FFEF APPn JFIF、Exif 等应用信息
FFDB DQT 量化表
FFC0 SOF0 Baseline 图片尺寸、分量和采样信息
FFC2 SOF2 Progressive JPEG
FFC4 DHT Huffman 表
FFDA SOS 压缩扫描数据开始
FFD9 EOI 图片结束

解码器在准备阶段会解析这些段,建立量化表和 Huffman 表,并获得图片宽高、颜色分量和采样方式。

3.2 Baseline 与 Progressive

嵌入式轻量解码器通常优先支持 Baseline JPEG:图片数据按一次扫描组织,状态和内存需求相对简单。

Progressive JPEG 会分多次扫描逐步恢复图像细节,浏览器支持良好,但轻量 MCU 解码器未必支持。当前 TJpgDec 遇到不支持的 JPEG 标准时会返回 JDR_FMT3,Progressive JPEG 是常见原因之一。

建议先把测试图片转换为 Baseline JPEG:

ffmpeg -i input.png -pix_fmt yuvj420p -q:v 3 output.jpg

3.3 MCU、YCbCr 颜色格式

JPEG 不按 LCD 的逐像素方式保存图片,而是按 MCU(Minimum Coded Unit)组织数据。每个 MCU 包含一个或多个 8 x 8 数据块,具体数量取决于色度采样方式。

YCbCr是YUV经过缩放和修改的翻版,只是在表示方法上不同。其中Y是指亮度分量,Cb指蓝色色度分量,而Cr指红色色度分量。人眼对视频的Y分量更敏感,因此通过对色度分量进行子采样来减少色度分量后,人眼察觉不到的图像质量的变化。
在YUV 家族中,YCbCr 是在计算机系统中应用最多的成员,其应用领域广泛,JPEG、MPEG均采用此格式。

常见采样格式:

YCbCr采样格式 含义 特点
4:4:4 色度与亮度分辨率相同 质量高、数据量大
4:2:2 水平方向降低色度分辨率 常见折中方案
4:2:0 水平和垂直方向都降低色度分辨率 照片中最常见
灰度 只有亮度分量 无 Cb、Cr

简化后的 Baseline JPEG 解码步骤为:

解析 Marker
  -> Huffman 解码
  -> 反量化
  -> IDCT(逆离散余弦变换)
  -> 恢复 Y/Cb/Cr 分量
  -> 色度上采样
  -> 转换为 RGB
  -> 写入帧缓冲

轻量软件解码器通常一次只保留一个 MCU 的工作数据,因此不必在 RAM 中保存整张解码图片。

3.4 TJpgDec 输出格式

TJpgDec 输出像素格式可直接配置为 RGB565、RGB888、灰度,然后将这些像素格式直接输入到屏幕帧缓冲中。

4. JPEG 软件解码流程

存储介质中的 .jpg
    ↓ 读文件(输入回调)
  JPEG 压缩码流
    ↓ TJpgDec 解码
  RGB565 / RGB888 像素块
    ↓ 写帧缓冲(输出回调)
  SDRAM 帧缓冲
    ↓ 显示控制器读出
  RGB 液晶屏显示

所以后面的代码实现,本质上就是围绕 TJpgDec 做三件事:

  1. 把图片接入到输入回调
  2. 把帧缓冲写入接到输出回调
  3. 按照屏幕大小处理缩放和居中

4.1 确保文件为 JPEG

扩展名不能证明文件格式。把 PNG 文件改名为 image.jpg,文件内容仍然是 PNG,JPEG 解码器必然失败。

JPEG 文件通常以 SOI 标记 FF D8 开始,以 EOI 标记 FF D9 结束。PC 上可以使用以下命令检查:

file image.jpg
xxd -l 16 image.jpg

预期结果应包含 JPEG image data,文件前两个字节应为 ff d8。

当前工程最初的 test.jpg 实际是 PNG 数据,测试代码使用的是转换后的 100 x 100 Baseline JPEG 数组。这类问题应在数据进入 MCU 前排除。

4.2 定义解码过程所需变量

/**
 * @brief JPEG 解码过程的输入状态和显示位置
 *
 * 该结构通过 jd_prepare() 的 dev 参数保存到 JDEC::device,输入和输出
 * 回调因此可以共享同一个会话上下文,而不需要使用全局读写状态。
 */
typedef struct
{
    const uint8_t *data; /* JPEG 压缩数据首地址 */
    size_t size;         /* JPEG 压缩数据总字节数 */
    size_t position;     /* 输入回调当前读取偏移 */
    uint16_t x;          /* 图片左上角在 LCD 中的 X 坐标 */
    uint16_t y;          /* 图片左上角在 LCD 中的 Y 坐标 */
} jpeg_context_t;

4.3 JPEG 字节流输入 TJpgDec 解码库

/**
 * @brief  向 TJpgDec 提供 JPEG 字节流
 * @param  decoder TJpgDec 解码器对象,device 指向 jpeg_context_t
 * @param  buffer  目标缓冲区;为 NULL 时仅跳过 count 字节,不复制数据
 * @param  count   TJpgDec 请求读取或跳过的字节数
 * @return 实际读取或跳过的字节数;到达数据末尾时可能小于 count
 *
 * @note TJpgDec 在解析 APP、COM 等不需要处理的 JPEG 段时,会将 buffer
 *       传为 NULL。即使不执行 memcpy,也必须推进 position,否则解析器
 *       会重复读取同一段数据。
 */
static size_t jpeg_input(JDEC *decoder, uint8_t *buffer, size_t count)
{
    /* jd_prepare() 已将调用者传入的会话上下文保存到 decoder->device。 */
    jpeg_context_t *context = (jpeg_context_t *)decoder->device;
    size_t remaining = context->size - context->position;

    /* 将本次读取限制在数组边界内,避免读取 const 数组之外的数据。 */
    if (count > remaining)
    {
        count = remaining;
    }

    /* buffer == NULL 是 TJpgDec 的“跳过数据”约定。 */
    if (buffer != NULL)
    {
        memcpy(buffer, context->data + context->position, count);
    }

    /* 读取和跳过操作都要更新流位置,并返回实际处理的字节数。 */
    context->position += count;
    return count;
}

4.4 TJpgDec 解码库输出数据至 LTDC 帧缓冲

/**
 * @brief  将 TJpgDec 输出的 RGB565 像素块写入 LTDC 帧缓冲
 * @param  decoder TJpgDec 解码器对象,device 指向 jpeg_context_t
 * @param  bitmap  当前 MCU 解码后的 RGB565 像素数组,按行紧密排列
 * @param  rect    当前输出块在 JPEG 图片中的坐标,四个边界均为闭区间
 * @return 1 表示成功并继续解码,0 表示终止解码
 *
 * @note TJpgDec 按 MCU(Minimum Coded Unit)分块调用该函数,而不是一次
 *       输出整张图片。边缘 MCU 可能比内部 MCU 小,因此每次都必须根据
 *       rect 重新计算宽度和高度。
 */
static int jpeg_output(JDEC *decoder, void *bitmap, JRECT *rect)
{
    jpeg_context_t *context = (jpeg_context_t *)decoder->device;
    const uint16_t *source = (const uint16_t *)bitmap;
    /* JRECT 的 right 和 bottom 属于有效区域,因此尺寸计算需要加 1。 */
    uint16_t width = rect->right - rect->left + 1U;
    uint16_t height = rect->bottom - rect->top + 1U;
    uint16_t row;

    /* 将图片偏移叠加到 MCU 坐标后检查屏幕边界,防止破坏帧缓冲。 */
    if ((context->x + rect->right >= lcddev.width) ||
        (context->y + rect->bottom >= lcddev.height))
    {
        return 0;
    }

    for (row = 0U; row < height; row++)
    {
        /*
         * LTDC 帧缓冲采用 RGB565,每像素 16 位,按 lcddev.width 个像素
         * 作为一行。源像素块没有行间空隙,而目标帧缓冲通常比块更宽,
         * 因此逐行复制,避免覆盖图片区域右侧的其他像素。
         */
        uint16_t *destination = (uint16_t *)LTDC_FRAME_BUF_ADDR +
                                ((context->y + rect->top + row) * lcddev.width) +
                                context->x + rect->left;
        uintptr_t cache_start;
        uintptr_t cache_end;

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

        /*
         * SDRAM 在 MPU 中被配置为可缓存,CPU 的 memcpy 可能只更新 D-Cache,
         * 而 LTDC 直接从 SDRAM 取数。Cortex-M7 的 D-Cache 行为 32 字节,
         * 所以清理地址和长度都向 Cache Line 边界扩展,确保 LTDC 能看到
         * 本行刚写入的全部像素。
         */
        cache_start = (uintptr_t)destination & ~(uintptr_t)31U;
        cache_end = ((uintptr_t)destination + width * sizeof(uint16_t) + 31U) &
                    ~(uintptr_t)31U;
        SCB_CleanDCache_by_Addr((uint32_t *)cache_start,
                                (int32_t)(cache_end - cache_start));
    }

    return 1;
}

4.5 JPEG 应用接口

将需要显示的图片数据指针和图片字节数大小传入此函数,调用后即可进行显示在 LCD 中央。

/**
 * @brief  解码内置测试 JPEG,并将其居中显示在 LCD 上
 * @param  jpeg_data  JPEG 图片数据指针
 * @param  jpeg_data_size JPEG 图片数据大小(字节数)
 * @return None
 *
 * 执行步骤:
 * 1. 初始化内存输入上下文;
 * 2. 调用 jd_prepare() 解析 JPEG 头并建立解码表;
 * 3. 检查原图尺寸是否超出 LCD;
 * 4. 计算居中坐标并调用 jd_decomp() 输出 RGB565 像素。
 *
 * @note 当前 scale 参数为 0,表示按原始尺寸解码。TJpgDec 支持 0~3,
 *       分别对应 1、1/2、1/4、1/8 尺寸。
 */
void app_jpeg_run(uint8_t *jpeg_data, unsigned int jpeg_data_size)
{
    /* JDEC 保存本次解码状态,只需在函数执行期间保持有效。 */
    JDEC decoder;
    JRESULT result;
    /* 输入数据位于 Flash;显示坐标在解析出图片尺寸后再计算。 */
    jpeg_context_t context = {
        .data = jpeg_data,
        .size = jpeg_data_size,
        .position = 0U,
        .x = 0U,
        .y = 0U,
    };

    /* 解析 JPEG 文件头,并从 jpeg_work_pool 中分配解码器内部数据。 */
    result = jd_prepare(&decoder, jpeg_input, jpeg_work_pool,
                        sizeof(jpeg_work_pool), &context);
    if (result != JDR_OK)
    {
        printf("CM7: JPEG prepare failed, result=%d\n", (int)result);
        return;
    }

    printf("CM7: JPEG image %ux%u, data=%u bytes\n",
           decoder.width, decoder.height, (unsigned int)context.size);

    /* 本测试不启用缩放,原图必须能够完整放入当前 LCD 逻辑分辨率。 */
    if ((decoder.width > lcddev.width) || (decoder.height > lcddev.height))
    {
        printf("CM7: JPEG image is larger than LCD\n");
        return;
    }

    /* 计算左上角坐标,使图片在 LCD 水平和垂直方向居中。 */
    context.x = (lcddev.width - decoder.width) / 2U;
    context.y = (lcddev.height - decoder.height) / 2U;

    /* scale=0 表示不缩放;解码器将重复调用 jpeg_output() 输出 MCU。 */
    result = jd_decomp(&decoder, jpeg_output, 0U);

    /* 确保所有显式内存操作在打印完成状态前已经结束。 */
    __DSB();

    printf("CM7: JPEG decode result=%d\n", (int)result);
}

博客导航

博客导航

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