简介:在嵌入式视觉应用开发中,STM32等微控制器因内存与算力有限,直接解码JPEG图片往往面临性能瓶颈。这份JPEG图片STM32解码源程序,面向需要在裸机或Keil MDK环境中实现图片显示的软硬件工程师,提供了一套可移植、可裁剪的完整解码方案。压缩包共2个文件,分别是一个C语言解码源文件与一份Word编解码详解文档,整体体积仅43KB,精简紧凑。C源程序实现了DCT逆变换、反量化、Huffman熵解码等关键环节,运行内存开销小于2.6K,典型条件下解码800×480彩色图片约6秒,适合温控、仪表、门禁等实时性要求不高的场景;同时程序不依赖操作系统,可配合中断与定时器使用。配套文档对JPEG编码流程、数据结构及STM32移植与优化技巧做了完整阐述,便于读者深入理解源码,并结合不同图片尺寸、速度要求灵活定制。已有1027人学习下载,适合嵌入式入门学习者、产品开发者以及需要快速集成图片解码功能的项目团队参考。
1. STM32 为什么需要一份 JPEG 解码源程序
做带屏嵌入式设备的人基本都撞过同一堵墙:图片资源一多,Flash 先爆,RAM 也吃紧。早期方案是把图片转成 C 数组,一张 320×240 的 RGB565 图片就是 150KB Flash,温控面板存个二三十张界面图,芯片容量直接翻三倍。JPEG 图片 STM32 解码源程序解决的就是这个痛点:让 MCU 直接消费 JPEG 字节流,解码过程中只保留几 KB 工作区,边解边把像素喂给 LCD,而不是把整帧图放在内存里。做温控面板、手持仪表、带屏 IoT 设备、甚至低成本串口屏方案的工程师,都值得把这条路径吃透。
很多人一听“STM32 上解 JPEG”就觉得不靠谱,觉得这是 PC 端 libjpeg 才干的事。实际上以目前的 Cortex-M 主频,320×240 的 JPEG 软解在几十毫秒到一两百毫秒级别是可接受的,而且很多界面切换场景并不需要高帧率。真正劝退人的不是 CPU 算力,而是不会选解码源程序、不会写输入输出回调、不知道缓冲区怎么给,最后被花屏和 HardFault 折腾到放弃。这篇文章就把这条路上最关键的选型、移植、内存账和坑一次性讲清楚。
2. JPEG 解码在 STM32 上的真实流程:它是一条流水线
2.1 基线 JPEG 的五个阶段:Huffman、反量化、Z 扫描、IDCT 与色度上采样
JPEG 不是“打开图片”这种黑匣子操作,它在解码器里是一条固定流水线。最常见的是基线 JPEG,文件结构从FF D8的 SOI 标记开始,接着是 DQT 定义量化表、SOF0 定义宽高和采样因子、DHT 定义 Huffman 表,再到 SOS 之后才是真正的压缩数据。解码时压缩数据部分要依次经历五步:先做 Huffman 解码还原出量化后的 DCT 系数,再做反量化,接着按 Z 字扫描把一维系数重排成 8×8 块,然后做逆离散余弦变换得到像素差值,最后做色度上采样和 YCbCr 到 RGB 的颜色转换。
这五步里有三处最容易写错。第一是 Huffman 解码时的位流读取,JPEG 的比特流是高位在前,而且 Huffman 码长可变,一个字节里可能横跨多个码字,稍微移动一位整个画面就花掉。第二是 DC 系数的差分恢复,每个 8×8 块的 DC 系数存的是与上一个块的差值,源程序里通常维护一个last_dc[3]数组,分别对应 Y、Cb、Cr 三个分量。第三是 CbCr 的上采样位置,4:2:0 采样下 CbCr 的分辨率只有 Y 的四分之一,插值时的像素对齐不对,就会出现整片偏色。
理解这条流水线的意义主要在调 bug。比如花屏时你要判断是位流错位还是颜色转换错;偏绿时先怀疑 CbCr 交换而不是去调 LCD 参数。另外,JPEG 解码是按 MCU 块推进的,MCU 也就是最小编码单元,对 4:2:0 来说是 16×16 像素。解码器每解完一个或一行 MCU,就会调用一次输出回调,把这块 RGB 数据交给用户。后面写输出回调时你会反复用到这个概念。
2.2 为什么 STM32 上不能照搬 PC 的解码方式
PC 上解 JPEG 一般是 libjpeg 这类成熟库,它的调用方式是一次性给整张图的内存地址,内部用大堆缓冲处理完整帧,解完之后你再拿到整个位图。这套模型搬到 STM32 上立刻崩:一张 640×480 的 RGB565 位图就要 600KB RAM,很多 MCU 整颗芯片的 SRAM 才 128KB。所以 STM32 上的轻量解码源程序几乎全是“流式”模型,库只保留解码当前 MCU 行所需的状态,解完一行就通过回调交出去,然后缓冲区复用给下一行。
内存之外第二道坎是浮点。PC 端 IDCT 用浮点随便算,Cortex-M 上大部分型号没有硬件浮点单元,纯浮点 IDCT 会吃掉大量 CPU 时间。轻量解码源程序通常采用定点 IDCT,或者对 IDCT 结果做移位缩放,精度略低于 PC 但人眼看不出差异。你在移植时不需要自己重写 IDCT,但要确认源程序里默认用的是定点实现。有些从 Linux 或桌面平台移植过来的代码,里面嵌了双精度浮点表,在 STM32 上编译能过,跑起来慢好几倍。
还有一点是字节序。JPEG 文件内部的大多数字段是大端存储,STM32 默认小端。好的源程序在解析 DQT、SOF0 这些段时已经做了字节序转换,但你如果自己往源程序里加自定义段解析,一定要记得手动(buf[0] << 8) | buf[1],直接用uint16_t*强转读出来的数永远不对。
2.3 软解还是硬件 JPEG 外设:按芯片型号选型
STM32 家族里有一部分型号内置了硬件 JPEG 编解码器,比如 F7 系列和 H7 系列的部分大封装型号。硬件 JPEG 外设支持直接输入 JPEG 码流,输出 YCbCr 数据,速度比软解快一个数量级,而且不占 CPU。同时它也有明显门槛:需要 DMA 配合大一点的缓冲区,输出格式固定为 YCbCr 4:2:2 或 4:4:4,后续还要自己做颜色转换和缩放;驱动代码写起来比软解复杂得多。对绝大多数项目,硬件外设的收益只在频繁解码大图时才体现得出来。
软件解码源程序的适用面就宽多了。STM32F1、F4 这些没有硬件 JPEG 外设的型号只能软解,而且软解代码只有几千行、几个 .c 文件,交叉编译零依赖,一套代码在所有 Cortex-M 上通用。它在解码过程中天然支持输出缩放,比如源图是 320×240,你只需要 160×120 的缩略图,可以按 1/2 或 1/4 缩放直接解出,省内存也省时间。我一般选型就一句话:主频 72MHz 以上、RAM 还有余量的项目优先软解;如果是开机就要秒开好几张大图、CPU 还要同时跑协议栈的项目,才考虑硬件 JPEG 外设。
| 方案 | 适用芯片 | 代码复杂度 | 解码速度 | 内存压力 | 主要成本 |
|---|---|---|---|---|---|
| 软件解码 | 全系 M3/M4/M7 | 低,移植 3 个文件 | 慢,但可接受 | 工作区几 KB | 占 CPU 时间 |
| 硬件 JPEG 外设 | 部分 F7/H7 型号 | 高,需 DMA 和颜色转换 | 快一个数量级 | 需要稍大的 DMA 缓冲 | 驱动调试周期长 |
3. 在 STM32 上跑通 JPEG 解码源程序:最小可复现的代码骨架
3.1 输入回调:解码器只认字节流,不认 SD 卡还是 Flash
轻量 JPEG 解码源程序的外围接口一般只有两个回调,输入回调负责供数,输出回调负责收像素。输入回调的典型签名是:解码器想要ndata字节,你把数据拷到buf,返回实际供给的字节数;当buf为 NULL 时,意思是“跳过这段数据不做拷贝”,主要用在跳过 JPEG 的 APP0、注释段这些解码器不关心的内容。我习惯把所有输入抽象成一个小结构体,无论图片来自 SD 卡、外部 Flash 还是内存数组,都统一走这个接口。
typedef struct { const uint8_t *base_addr; /* JPEG 文件内存镜像的起始地址 */ uint32_t file_size; /* 文件总长度 */ uint32_t read_pos; /* 当前读取偏移 */ } jpeg_stream_t; /* 解码器输入回调:buf 非空时读取数据,buf 为 NULL 时跳过数据 */ uint32_t jpeg_cb_input(JPEG_DEC *dec, uint8_t *buf, uint32_t ndata) { jpeg_stream_t *st = (jpeg_stream_t *)dec->user_data; uint32_t remain = st->file_size - st->read_pos; if (buf == NULL) { /* 跳过场景:只把偏移往前推即可 */ if (ndata > remain) ndata = remain; st->read_pos += ndata; return ndata; } if (ndata > remain) ndata = remain; memcpy(buf, st->base_addr + st->read_pos, ndata); st->read_pos += ndata; return ndata; }这段代码的关键是buf == NULL分支。很多人在移植时报错说解码器反复读取文件却始终不开工,就是没处理跳过逻辑。JPEG 文件头部有很多附加段,解码器解析段结构时常常要“看一眼再跳过”,如果跳过分支持续返回 0,解码器会认为数据流断了,直接返回格式错误。另外remain的计算一定要用文件大小减去当前位置,不要盲目信任源程序会把ndata控制在合法范围内,边界溢出最容易出 HardFault。
如果图片放在 SD 卡而不是内存镜像,思路完全一样:buf == NULL时调用文件系统的 seek 接口向前移动,buf非空时调用 read 接口。唯一要注意的是文件系统读取一般按扇区缓冲,每次ndata可能只有几十字节,频繁 seek 会把 SD 卡速度拖垮,后面踩坑章节会专门讲这个。
3.2 输出回调:把解码出的 MCU 行变成 LCD 上的像素行
输出回调是解码源程序里最核心的落地接口。解码器每完成一个 MCU 或一行 MCU,就把一块像素数据放进bitmap,同时用一个矩形结构体告诉你这块像素在整张图里的坐标范围。你要做的就是把这块数据写到 LCD 的对应位置。绝大多数 SPI 屏或并口屏都支持先设置显示窗口,再连续写像素数据,这正好和回调模型匹配。
/* 输出回调:把解码出来的矩形像素块写入 LCD */ uint32_t jpeg_cb_output(JPEG_DEC *dec, void *bitmap, JRECT *rect) { uint16_t *pixel = (uint16_t *)bitmap; /* RGB565 输出格式 */ int width = rect->right - rect->left + 1; for (int y = rect->top; y <= rect->bottom; y++) { lcd_set_window(rect->left, y, rect->right, y); lcd_write_data((uint8_t *)pixel, width * 2); /* LCD 数据按字节写 */ pixel += width; } return 1; /* 返回 1 继续解码,返回 0 可提前终止 */ }这里的bitmap不是整行像素的数组,而是当前 MCU 行的局部数据,但它的像素顺序和rect坐标是一一对应的,所以按行拷出去即可。width用right - left + 1计算,不要直接用宏定义里的屏幕宽度,因为解码器可能输出的是图片右边缘的不完整块,比如图片宽度不是 16 的整数倍时最后一块会比较窄。
注意lcd_write_data的类型。如果你的 LCD 驱动是按字写入,而这里传的是字节指针,一定要做强制转换并明确长度是width * 2。RGB565 一个像素两个字节,STM32 小端模式下内存里是高字节在后,很多屏控制器恰好要高字节在前,这时候花屏不是解码问题,而是字节序问题,在后面踩坑章节单独说。
3.3 主流程:查信息、定缩放、开解,三个参数决定内存和速度
主流程把输入输出回调串起来。常见的轻量解码源程序的调用方式大同小异:先 open,再取图片信息,设置缩放比,最后执行解码。open 的时候要传入一块工作缓冲区,这个缓冲区供 Huffman 表、反量化表和 MCU 行缓冲复用,是整个移植里最需要认真对待的参数。
/* 工作缓冲区,4 字节对齐,大小按实际提示调整 */ static uint8_t work_buf[4096] __attribute__((aligned(4))); static JPEG_DEC jdec; static JPEG_IMAGE_INFO info; static const uint8_t my_jpeg_file[] = { /* 这里放一张 JPEG 图片的完整字节流,可以在 PC 端转成数组 */ }; int jpeg_display_task(void) { jpeg_stream_t stream = { .base_addr = my_jpeg_file, .file_size = sizeof(my_jpeg_file), .read_pos = 0, }; /* 打开解码器,传入输入/输出回调和用户上下文 */ if (JPEG_Open(&jdec, jpeg_cb_input, jpeg_cb_output, work_buf, sizeof(work_buf), &stream) != 0) { return -1; /* 文件头解析失败或 work_buf 不够 */ } JPEG_GetImageInfo(&jdec, &info); /* 0=原图, 1=1/2, 2=1/4, 3=1/8 缩放解码 */ JPEG_SetScale(&jdec, JPEG_SCALE_HALF); if (JPEG_Decode(&jdec) != 0) { return -2; /* 解码中途出错 */ } return 0; }work_buf的大小和缩放比直接决定 RAM 占用。开大一点不会更快,开小了解码器会在JPEG_Open阶段直接报错,因为 Huffman 表和量化表放不下。我一般做法是先给 4096 字节,如果 open 返回缓冲区不足,再往上加,直到能正常通过。实际项目里可以依据图片最大分辨率算出一个稳定值,后面内存章节会讲怎么算。缩放比则是个性价比极高的参数:从原图直接解出 1/2 大小的图,解码耗时大约降到原来的四分之一,因为每个方向的像素都少了一半。
4. 内存与存储预算:用几 KB 工作区解大图的收支账
4.1 按 MCU 行输出而不是整帧输出,省下的就是几百 KB
很多从 PC 转过来的开发者会下意识找“解码后的完整位图放在哪”,这就是思路没转过来。轻量解码源程序的设计目标就是不保留整帧位图,它内部只维护当前正在解码的若干个 MCU 行,解完一行立即调输出回调,调完这块缓冲区就复用给下一行。对这种模型,RAM 占用和图片总像素数基本无关,只和 MCU 行缓冲大小以及工作区大小有关。
比如同样一张 800×480 的图片,如果按整帧 RGB565 算需要 768KB RAM,大多数 STM32 直接超限。但按 MCU 行输出,解码器内部只需要同时保存 Y、Cb、Cr 各一个 MCU 行的数据,4:2:0 下一个 MCU 行是 16 像素高、800 像素宽,Y 需要 800×16 字节,CbCr 各需要 800×8 字节,加起来不到 25KB。再算上 Huffman 表和量化表的几千字节,总 RAM 占用也就 30KB 级别,这在 STM32F4 上完全可行。
实际项目中我还会把输出直接导向 LCD 的窗口写入,不再额外分配行缓冲。SPI LCD 一次写一个 MCU 行,写完继续解下一个,假如图片需要从下往上显示,只要调整输出回调的写入坐标就行了,内存一分钱不用多花。如果屏本身带帧缓冲,比如 RGB 接口屏加 SDRAM,那可以先把解码输出写到 SDRAM 的帧缓冲里,再统一刷新,但那是另一种内存模型了。
4.2 4:2:0 采样下的隐藏内存结构:CbCr 比想象中更省
JPEG 的色度子采样对内存占用影响很大。最常见的 4:2:0 采样,每 16×16 像素的区域里,Y 分量保存 4 个 8×8 块,Cb 和 Cr 各只保存 1 个 8×8 块。也就是说解码器的中间缓冲面积是 16×16 的 Y 加 8×8 的 Cb 加 8×8 的 Cr,而不是三份同样的 16×16。这意味着大多数情况下内存占用只相当于 YUV444 的一半,移植时可以往小了估。
但也有反例。有些屏幕截图工具会输出 4:4:4 采样或不做子采样的 JPEG,这时每个 8×8 区域里 Y、Cb、Cr 各有一个块,中间缓冲直接翻倍。如果你的图片来源不可控,建议在解码前用图片信息里的采样因子做个判断,遇到 4:4:4 的图就提前把工作区调大,否则解码器走到一半可能输出错误,或者进入输出回调的数据量超出预期。判断方法很简单:SOF0 段里 Y 的采样因子是 2×2,CbCr 是 1×1,就是 4:2:0;CbCr 也是 2×2,就是 4:4:4。
另一个容易忽略的隐藏开销是源程序内部的“缩放缓冲”。设置 1/2 缩放时,解码器通常会先把原始 MCU 解出来,再在内部做水平和垂直抽值,最后输出缩小后的行。这个中间缓冲不会比原始 MCU 更大,所以不会爆内存,但耗时是实打实存在的。1/8 缩放看起来最省输出,实际上内部还是要解完整的 MCU,不要误以为 1/8 缩放能把解码耗时也降到八分之一。
4.3 图片放 SD 卡还是外部 Flash:流式读取与内存镜像两种姿势
图片资源的存放位置影响输入回调的实现方式。放在内存镜像里最简单,把整张 JPEG 图用工具转成 C 数组烧进内部 Flash 或外部 SPI Flash,输入回调直接拷内存。这种方式适合图不多的小项目,一张 320×240 的 JPEG 通常只有十几 KB,内部 Flash 塞得下。缺点是大图或图片数量多了以后,内存镜像会占掉大量 Flash,而且 SD 卡上的图片没法直接这么搞。
放在 SD 卡上则要求输入回调里做文件读取。但 SD 卡读取有几个现实问题。轻量解码源的读取请求往往很碎,一次只要几十个字节,如果每次回调都直接发一条 SPI 或 SDIO 读命令,总时间会非常难看。常规做法是在回调里维护一个扇区缓存,按 512 字节为单位预读,ndata请求命中的时候直接从缓存里拷,只有跨扇区才触发一次新的读命令。
/* 带扇区缓存的输入回调片段 */ uint32_t jpeg_cb_input_sd(JPEG_DEC *dec, uint8_t *buf, uint32_t ndata) { if (buf == NULL) { file_offset += ndata; return ndata; } while (ndata > 0) { uint32_t sector_off = file_offset % 512; uint32_t sector_left = 512 - sector_off; uint32_t copy_len = (ndata < sector_left) ? ndata : sector_left; if (sector_cache_valid != file_offset / 512) { sd_read_sector(file_offset / 512, sector_cache); sector_cache_valid = file_offset / 512; } memcpy(buf, sector_cache + sector_off, copy_len); file_offset += copy_len; buf += copy_len; ndata -= copy_len; } return buf 原请求大小; /* 实际代码中要保存原始 ndata */ }扇区缓存这块要注意file_offset是全局或存在上下文里,不能被解码器的多次回调打断。我踩过一次坑:缓存有效扇区号判断用错了变量,导致每读几个字节就重新读一次扇区,解码一张 20KB 的图耗时从 200ms 涨到两秒。调试时在输入回调里加个计数器,统计调用次数和平均每次请求字节数,如果平均请求不到 32 字节,就说明扇区缓存没起作用。
5. STM32 JPEG 解码源程序踩坑记录:花屏、HardFault、打不开图的五种典型状态
5.1 彩色花屏且有固定规律:先查颜色顺序和字节序
现象是画面能看出轮廓,但颜色像打翻的调色盘,尤其是绿色和紫色大面积错乱,或者在 8×8 方块边界处有明显色差。这种花屏十有八九不是解码器算错,而是数据走出解码器之后的格式问题。原因有两个:一是解码源程序输出的是 RGB888,而你按 RGB565 直接丢给 LCD,每个像素少了低字节导致颜色错乱;二是 RGB565 在内存里的字节顺序和屏控制器期望相反。
解决办法是先在解码器初始化前把输出格式固定下来。大多数轻量源程序提供一个颜色转换宏或模式项,RGB565 和 RGB888 只能选一个,不能靠屏驱动去硬调。确认输出格式后,再看 LCD 驱动写入时的字节顺序,RGB565 的高低位顺序在 SPI 屏上经常需要做一次交换。用纯红、纯绿、纯蓝三张小图测试是最快的方法:纯红图显示成蓝色,直接交换字节序。
5.2 解码到一半进 HardFault:工作缓冲对齐和 DMA 抢内存是两大元凶
现象是解码器已经正常打开,前几张图也显示了,换到某一张图或者解码到某个进度就突然跳进 HardFault。最常见原因是工作缓冲区没有 4 字节对齐。轻量解码源程序内部会把工作区按字对齐拆成多个子块,如果数组起始地址不是 4 的倍数,内部指针运算会访问未对齐地址,在 Cortex-M 上直接触发 HardFault。解决方式是在定义数组时加上__attribute__((aligned(4))),并检查编译器是否把它放到了对的地址。
另一个更隐蔽的原因是 DMA。如果输出回调里用 DMA 向 LCD 推数据,而 DMA 还没传输完成时解码器又调用了下一次输出回调,去写同一块源缓冲区,就会出现一边读一边写的竞争。翻车现场往往是时好时坏,把优化等级换一下才稳定复现。解决方式有两个方向:一是输出回调里等待 DMA 完成标志再返回;二是准备两个行缓冲做乒乓切换,解码器写 A 时 DMA 读 B,下次再交换。
5.3 解码一张小图要好几秒:输入回调被碎片化读取拖死
现象是同一张图在 PC 上秒开,烧进 STM32 后按秒计,而且换优化等级没有明显改善。不要急着怀疑 IDCT 性能,先查输入回调的调用频率。把回调里加个计数,打印出总共调了多少次、平均每次读多少字节。如果调用频次几千次且平均字节数很小,说明你的读取策略有问题,比如 SD 卡每次按单字节读,或者扇区缓存失效。
解决方式已经在上文说过,做扇区级缓冲。对于放在内存镜像里的图片,还有另一个加速技巧:把base_addr指向的 JPEG 数组直接拷贝到 CCM RAM 或紧耦合内存里再解码。内部 Flash 在 Cortex-M 上读取有等待周期,代码和图片数据混在同一个 Flash 总线里,解码器频繁读图片数据会跟指令预取抢带宽。把图片放到更快的内存域,可以让解码速度提升三四成。
5.4 解码器报告格式不支持:渐进式和 CMYK JPEG 打不开
现象是有些图片解码器 open 失败,或者解码到一半返回错误码,而同一张图片换一个图片查看器又能正常打开。原因大概率是图片不是基线 JPEG。JPEG 有两种常见编码流程:基线顺序编码和渐进式编码,很多轻量解码源程序只支持基线。另外商用图库导出的 CMYK JPEG 也会被直接拒绝,因为解码器只认 YCbCr 色彩空间。
判断方法很直接:用十六进制查看 JPEG 文件的 SOF 标记。FF C0是基线 JPEG 的 SOF0,FF C2是渐进式 JPEG 的 SOF2,FF C1是扩展顺序。如果文件头出现FF C2,基本可以认定是渐进式。解决办法是在图片处理环节统一转成基线。各种图片处理软件里找“渐进式”或“Progressive”选项,关掉它再另存,同时确认色彩模型选 RGB 或 YCbCr,不要选 CMYK。
5.5 图能显示但位置不对:坐标映射和显示方向要一起处理
现象是解码正常、颜色正常,但图片在 LCD 上显示时上下颠倒、左右镜像,或者出现错位拼接。原因在于 JPEG 的坐标原点在左上角,而很多 LCD 控制器的扫描方向默认是从右下角开始,或者屏的窗口坐标设置对 x、y 有翻转。这是纯坐标映射问题,不需要动解码器。
解决方式是在输出回调里根据 LCD 的扫描方向做坐标换算。比如屏的扫描方向是自下而上,那么rect->top应该映射到LCD_HEIGHT - rect->bottom - 1。不要每行都做for循环里重新计算,提前把 x 方向偏移和 y 方向偏移算成四个变量,放进一个坐标变换结构体,输出回调里直接套公式。注意有些 LCD 驱动在设置了显示窗口后,继续写像素是自动连续填充的,这时只要保证窗口和像素数据的行顺序一致,就不用手动跳来跳去。
6. 最后的优化技巧:乒乓缓冲推屏加 DWT 计时,让解码耗时可见
解码速度优化里性价比最高的一招,是把输出回调和 LCD 数据传输并行起来。前面说过,DMA 在传输时源缓冲区不能被解码器覆盖,所以很多人的第一版代码是“回调里等 DMA 传完再返回”,这会让解码速度被 LCD 总线速度死死拖住。换成乒乓缓冲后,解码器写line_buf[0]时 DMA 传line_buf[1],下一次两者交换,感觉上就是把 LCD 传输从解码路径里摘出去了。
static uint16_t line_buf[2][320]; /* 两个行缓冲,宽度按屏幕最大值 */ static uint8_t lcd_dma_sel = 0; static volatile uint8_t lcd_dma_done = 1; void lcd_dma_callback(void) { lcd_dma_done = 1; } uint32_t jpeg_cb_output_pingpong(JPEG_DEC *dec, void *bitmap, JRECT *rect) { int width = rect->right - rect->left + 1; int height = rect->bottom - rect->top + 1; /* 等上一次 DMA 结束,再复用当前缓冲 */ while (!lcd_dma_done); memcpy(line_buf[lcd_dma_sel], bitmap, width * height * 2); lcd_set_window(rect->left, rect->top, rect->right, rect->bottom); lcd_dma_start(line_buf[lcd_dma_sel], width * height * 2, lcd_dma_callback); lcd_dma_done = 0; lcd_dma_sel ^= 1; return 1; }这段代码里lcd_dma_done标志是关键,lcd_dma_start传入 DMA 完成回调,回调里置位。因为解码是按顺序推进的,DMA 完成标志天然形成了一个简单的生产者消费者同步。千万别在 DMA 没完成时就返回,否则 DMA 还在读line_buf[0],解码器已经往里面写新数据了。双缓冲的意义不是消除等待,而是把“必须等”的次数减半。
验证优化效果别用眼睛估,用 DWT 周期计数器。Cortex-M3 及以上的内核自带 DWT 单元,可以精确读 CPU 周期数,比 systick 的时间戳精度高得多:
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; DWT->CYCCNT = 0; DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; uint32_t start = DWT->CYCCNT; JPEG_Decode(&jdec); uint32_t ms = (DWT->CYCCNT - start) / (SystemCoreClock / 1000); printf("decode %lu ms\n", ms);DWT 计数在调试模式下会受仿真器停止影响,测出来的数字偏大。正确姿势是脱离调试器、以 release 运行,通过串口打印耗时。我自己的习惯是每次改完解码链路,先记一组 DWT 数据,确认耗时变化来自哪段代码,而不是靠“体感流畅”去判断。这组数据会一路跟着你从方案验证走到量产调优,比任何玄学都有用。
希望这些从选型到落地的踩坑经验能帮你少走一段弯路。
本文还有配套的精品资源,点击获取