【STM32 通用】JPEG 软件解码
1. 引言
想要将图片显示到 LCD 屏幕上,就必须对图片进行解码,这里介绍的是 JPEG 软件解码。
本文所用硬件配置:
- STM32H757XIH6
- 正点原子 400 * 800 4.3 寸显示屏
2. TJpgDec 软件解码库


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 做三件事:
- 把图片接入到输入回调
- 把帧缓冲写入接到输出回调
- 按照屏幕大小处理缩放和居中
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);
}
博客导航
本文来自博客园,作者:膝盖中箭卫兵,转载请注明原文链接:https://www.cnblogs.com/Skyrim-sssuuu/p/22959915

浙公网安备 33010602011771号
https://orcid.org/0000-0001-5102-772X