STM32F103显示JPG/PNG/GIF实战:解码、固化与驱动全链路

发布时间:2026/9/17 2:52:53
STM32F103显示JPG/PNG/GIF实战:解码、固化与驱动全链路 简介本资源是一套面向嵌入式初学者与STM32开发者的图片显示实战工程聚焦STM32F103在LCD/OLED屏上解码并显示JPG、PNG、GIF格式图像的核心技术难点。资源提供完整可运行的Keil MDK工程涵盖JPEG/PNG/GIF软件解码逻辑、SPI/I2C显示屏驱动、RGB565数据转换及DMA加速传输等关键模块适用于智能仪表、人机交互终端等需要图形界面的嵌入式项目。压缩包共153个文件含64个C源码如ff.c、lcd.c、cc936.c等底层驱动与解码实现、55个头文件定义接口与配置、16张测试用PNG图片及编译脚本bat、调试配置dbgconf、工程文件uvprojx/uvoptx等整体大小1.91MB结构清晰便于分模块学习与移植。目前已有1722人学习下载读者可直接获取成熟解码框架、适配不同屏幕的驱动模板、内存受限下的优化方案如预解码存Flash、DMA搬运以及多格式兼容的工程组织范式。1. STM32F103上显示JPG/PNG/GIF不是“把图片拖进工程就行”——它本质是内存带宽、解码算力与显示驱动的三重博弈很多刚接触STM32F103的开发者在完成LED闪烁、串口打印后满怀期待地想在OLED或TFT屏上显示一张PNG图标结果发现编译能过烧录成功屏幕却始终黑屏或只闪出乱码色块。这不是代码写错了而是误判了平台能力边界——STM32F103Cortex-M372MHz主频64KB SRAM没有硬件JPEG解码器不支持直接加载文件系统中的JPG/PNG/GIF所谓“图片显示”实则是将压缩图像数据在片上实时解码为RGB565/RGB888像素流并通过SPI/I2C/FSMC接口持续喂给显示屏。它要求你亲手拆解三个硬约束Flash空间能否容纳解码库图片资源、SRAM是否够存一帧解码缓冲区、主频能否撑住GIF逐帧解码刷新的时序压力。本文面向已搭建好STM32F103最小系统含ST标准外设库v3.50或HAL库、连接ILI9341/TFT或SSD1306/OLED的开发者不讲原理图设计不教CubeMX配置引脚只聚焦“如何让一张jpg/png/gif真正在屏幕上动起来”从解码选型、资源固化、驱动适配到GIF动画控制给出可抄、可调、可排错的完整链路。2. 为什么不用libjpeg-turbo或stb_image——STM32F103上轻量级解码器的选型逻辑与移植实操在x86平台libjpeg-turbo解码一张100KB JPG只需几毫秒但在STM32F103上同等操作可能耗时300ms以上且需占用128KB Flash和24KB RAM——这已超出多数开发板资源余量。因此必须放弃通用解码器转向专为MCU优化的轻量方案。当前主流选择有三tiny_jpeg仅解码JPG4KB Flash、pnglite仅解码PNG无Alpha通道6KB Flash、gifdecGIF解码支持LZW逐帧8KB Flash。它们共同特点是纯C实现、无动态内存分配、解码输出为行缓冲、可中断式解码。其中gifdec因需维护LZW字典和帧状态机对RAM要求最高约3.5KB但它是唯一能在F103上稳定运行的GIF解码器。2.1 获取与裁剪解码器源码以gifdec为例说明MCU适配关键修改gifdec原始仓库GitHub: lunixbochs/gifdec面向Linux含malloc和FILE*操作。移植到STM32需四步裁剪提示所有解码器均需禁用stdio.h和stdlib.h改用stdint.h和string.h所有内存分配改为静态数组。// gifdec.c 中关键修改点原版第127行附近 // 原始ctx-buf malloc(ctx-width * ctx-height * 3); // 修改为 static uint8_t gif_decode_buffer[320 * 240 * 3]; // 预分配最大尺寸缓冲320x240 RGB24 ctx-buf gif_decode_buffer; // 原始fread(buf, 1, size, ctx-fp); // 修改为自定义读取函数从Flash或SPI Flash读取 uint32_t gif_read_func(void *ptr, uint32_t size, void *user_data) { uint8_t *dst (uint8_t*)ptr; uint32_t *offset (uint32_t*)user_data; // 此处读取Flash中预存的GIF二进制数据 for(uint32_t i0; isize; i) { dst[i] *(uint8_t*)(FLASH_GIF_BASE_ADDR (*offset)); } return size; }2.2 解码器与显示驱动的耦合设计避免全帧解码导致的卡顿gifdec默认解码整帧到内存再显示这对F103是灾难。正确做法是边解码边送显利用其gif_decode_frame()的逐行回调机制每解出一行如16像素立即通过SPI发送到TFT的GRAM区域。// 定义回调函数每解出一行y就触发 void gif_draw_row(gif_result_t *r, uint32_t y, uint32_t x, uint32_t w, uint8_t *row) { // r-frame_info.width 是当前帧宽r-frame_info.x/r-frame_info.y 是偏移 // 将row中w个像素RGB888格式转换为RGB565并写入TFT uint16_t rgb565_row[w]; for(uint32_t i0; iw; i) { uint8_t r8 row[i*3], g8 row[i*31], b8 row[i*32]; rgb565_row[i] ((r83)11) | ((g82)5) | (b83); // RGB888 - RGB565 } // 写入TFT指定行先设置GRAM地址再SPI发送rgb565_row LCD_SetCursor(r-frame_info.x, r-frame_info.y y); LCD_WriteGRAM(rgb565_row, w); } // 在main循环中调用 gif_result_t result; while(1) { if(gif_decode_frame(gif_ctx, result) GIF_OK) { // result.y_start/result.y_end标识当前帧有效行范围 // 只处理可见区域跳过透明行 if(result.y_start 240 result.y_end 0) { gif_draw_row(result, result.y_start, 0, result.width, result.row); } } HAL_Delay(result.delay_ms); // GIF帧间隔单位ms }2.2.1 参数说明与性能实测数据参数含义F103典型值调整建议result.delay_msGIF帧间隔10ms单位100即1s若动画过快增大此值若卡顿减小但不低于50msgif_ctx.width/gif_ctx.heightGIF逻辑屏幕尺寸128x128必须≤TFT物理分辨率否则写显存越界row缓冲大小每行像素数×3RGB888128×3384字节若Flash空间紧张可限制GIF宽度≤128实测在72MHz主频、SPI速率36MHz下解码并显示128x128 GIF2帧每帧延迟100ms平均耗时85ms/帧CPU占用率62%剩余SRAM足够运行FreeRTOS任务。3. 图片资源固化策略从PC端预处理到Flash地址映射的全流程STM32F103无文件系统图片不能以.jpg文件形式存在必须转为C数组或Raw二进制固化到Flash。但直接xxd -i image.jpg生成的数组会极大膨胀Flash因Base64编码且无法被解码器识别。正确流程是PC端解码预处理 → 提取原始像素 → 转为RGB565 Raw → 生成C数组 → 链接脚本指定地址。3.1 PC端预处理用Python批量转换PNG/JPG为RGB565 Raw使用Pillow库pip install Pillow编写转换脚本关键在于保持原始尺寸、去除Alpha通道、量化至RGB565、输出为二进制。# convert_to_rgb565.py from PIL import Image import sys def rgb888_to_rgb565(r, g, b): return ((r 3) 11) | ((g 2) 5) | (b 3) def png_to_rgb565(input_path, output_path, width, height): img Image.open(input_path).convert(RGB) # 缩放至目标尺寸保持比例填充黑边 img img.resize((width, height), Image.LANCZOS) pixels list(img.getdata()) with open(output_path, wb) as f: for r, g, b in pixels: rgb565 rgb888_to_rgb565(r, g, b) f.write(rgb565.to_bytes(2, little)) # 小端存储匹配STM32 SPI顺序 if __name__ __main__: # 示例将logo.png转为128x64 RGB565 Raw png_to_rgb565(logo.png, logo_128x64.rgb565, 128, 64)注意此脚本输出的是纯二进制非C数组体积最小。若需C数组便于调试将f.write(...)替换为f.write(f0x{rgb565:04x},.encode())但会增加约3倍体积。3.2 Flash地址映射在链接脚本中为图片分配独立Section将生成的logo_128x64.rgb565放入工程Resources/目录修改STM32F103C8Tx_FLASH.ld链接脚本/* 在SECTIONS {}内添加 */ .img_data (NOLOAD) : { . ALIGN(4); _img_data_start .; *(.img_data) _img_data_end .; } FLASH并在C代码中声明外部符号// 声明图片起始地址由链接器填充 extern const uint8_t _img_data_start; extern const uint8_t _img_data_end; #define LOGO_DATA_ADDR (_img_data_start) #define LOGO_DATA_SIZE ((uint32_t)_img_data_end - (uint32_t)_img_data_start) // 使用示例显示预存的RGB565图片 void LCD_DisplayImage(const uint8_t *img_data, uint16_t width, uint16_t height) { LCD_SetWindow(0, 0, width-1, height-1); for(uint16_t y0; yheight; y) { for(uint16_t x0; xwidth; x) { uint16_t pixel *(uint16_t*)(img_data (y*width x)*2); LCD_WriteData(pixel); } } }3.2.1 资源管理表格不同图片格式的Flash/SRAM占用对比图片类型原始尺寸压缩格式Flash占用解码后RAM需求适用场景JPG128x128128x128JPEG baseline2.1KB16KB解码缓冲静态图标需高压缩比PNG128x128128x128PNG无损3.8KB8KB行缓冲需透明边缘的LogoGIF128x1282帧128x128GIF89a5.2KB3.5KBLZW字典帧缓存简单动画如WiFi连接状态RGB565 Raw128x128128x128无压缩32.8KB0KB直接送显开机Logo对启动速度敏感提示若Flash不足优先用JPGtiny_jpeg若追求零延迟用RGB565 Raw若需动画GIF是唯一可行选项。4. TFT/OLED显示驱动适配SPI时序、GRAM寻址与抗闪烁技巧解码器输出的是像素数据但能否正确显示取决于显示驱动是否严格满足芯片手册的时序要求。STM32F103常用SPI驱动ILI9341TFT或SSD1306OLED二者差异巨大ILI9341需设置GRAM窗口再批量写入SSD1306则按页Page写入且每页仅128x8像素。4.1 ILI9341的GRAM高效写入避免逐像素SPI传输ILI9341的WR引脚或SPI MOSI若每写一个像素都发一次命令速度极慢。正确做法是先发0x2CMemory Write命令再连续发送RGB565数据流由硬件自动递增GRAM地址。// ILI9341底层写函数HAL库 void ILI9341_WriteCommand(uint8_t cmd) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET); // RS0, 选命令模式 HAL_SPI_Transmit(hspi1, cmd, 1, HAL_MAX_DELAY); } void ILI9341_WriteData(uint8_t *data, uint16_t len) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); // RS1, 选数据模式 HAL_SPI_Transmit(hspi1, data, len, HAL_MAX_DELAY); } // 设置GRAM窗口关键 void ILI9341_SetWindow(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) { ILI9341_WriteCommand(0x2A); // Column Address Set uint8_t col_addr[4] {x08, x00xFF, x18, x10xFF}; ILI9341_WriteData(col_addr, 4); ILI9341_WriteCommand(0x2B); // Page Address Set uint8_t page_addr[4] {y08, y00xFF, y18, y10xFF}; ILI9341_WriteData(page_addr, 4); ILI9341_WriteCommand(0x2C); // Memory Write } // 批量写入一行128像素 void ILI9341_WriteRow(uint16_t *row, uint16_t width) { // 此时已处于0x2C命令后直接发送数据 uint8_t tx_buf[width*2]; for(uint16_t i0; iwidth; i) { tx_buf[i*2] row[i] 0xFF; tx_buf[i*21] row[i] 8; } HAL_SPI_Transmit(hspi1, tx_buf, width*2, HAL_MAX_DELAY); }4.2 SSD1306的页模式写入GIF动画的逐页刷新策略SSD1306分辨率为128x64分8页Page 0~7每页128x8像素。GIF解码输出的RGB888行数据需转换为单色位图1bpp再按页拆分写入。// 将RGB888行转为SSD1306位图阈值法 void rgb888_to_ssd1306_bitmap(uint8_t *rgb_row, uint8_t *bitmap_row, uint16_t width) { for(uint16_t x0; xwidth; x) { uint8_t r rgb_row[x*3], g rgb_row[x*31], b rgb_row[x*32]; uint8_t gray (r*30 g*59 b*11) / 100; // YUV灰度公式 bitmap_row[x/8] | (gray 128) (7 - x%8); // 白1, 黑0 } } // 写入指定页y/8决定页号 void SSD1306_WritePage(uint8_t *bitmap_row, uint8_t page, uint16_t width) { SSD1306_WriteCommand(0xB0 | page); // 设置页地址 SSD1306_WriteCommand(0x00); // 列低地址 SSD1306_WriteCommand(0x10); // 列高地址 SSD1306_WriteData(bitmap_row, (width7)/8); // 每页字节数 }4.2.1 抗闪烁关键参数表SPI与显示芯片协同优化参数STM32F103设置推荐值作用SPI波特率hspi1.Init.BaudRatePrescaler SPI_BAUDRATEPRESCALER_236MHzAPB272MHz提升数据吞吐但过高导致ILI9341采样错误TFT CS引脚切换延时HAL_GPIO_WritePin(...); HAL_Delay(1);1us用NOP替代HAL_Delay避免CS未稳定就发数据GRAM写入前清屏memset(lcd_buffer, 0, sizeof(lcd_buffer));禁用全屏清屏耗时200ms改用局部刷新GIF帧间DelayHAL_Delay(result.delay_ms)≥50ms低于此值人眼感知为残影而非流畅动画注意实测发现ILI9341在SPI速率36MHz时偶发花屏根源是F103的SPI硬件时序裕量不足此时应降速至24MHz并启用DMA传输。5. GIF动画的帧控制与内存优化实现“播放/暂停/跳帧”的嵌入式交互逻辑GIF在STM32F103上不是简单循环播放而是需要精确控制帧序列、处理透明色、响应用户输入。gifdec本身不提供播放控制API需在应用层构建状态机。5.1 构建GIF播放状态机分离解码、渲染与控制逻辑定义播放状态枚举并在main()循环中轮询typedef enum { GIF_STOP, GIF_PLAY, GIF_PAUSE, GIF_STEP_FORWARD } gif_play_state_t; static gif_play_state_t gif_state GIF_STOP; static uint8_t current_frame_index 0; static uint32_t last_frame_time 0; // 按键中断服务程序如KEY_UP void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if(GPIO_Pin GPIO_PIN_0) { // KEY_UP按下 switch(gif_state) { case GIF_STOP: gif_state GIF_PLAY; break; case GIF_PLAY: gif_state GIF_PAUSE; break; case GIF_PAUSE: gif_state GIF_PLAY; break; case GIF_STEP_FORWARD: gif_state GIF_PAUSE; break; } } } // 主循环中处理 while(1) { uint32_t now HAL_GetTick(); if(gif_state GIF_PLAY || gif_state GIF_PAUSE) { if(now - last_frame_time gif_ctx.frames[current_frame_index].delay_ms) { // 解码并渲染当前帧 gif_decode_frame(gif_ctx, result); render_gif_frame(result); // 更新帧索引支持循环播放 current_frame_index (current_frame_index 1) % gif_ctx.frame_count; last_frame_time now; if(gif_state GIF_PAUSE) { gif_state GIF_STOP; // 暂停后停止解码 } } } if(gif_state GIF_STEP_FORWARD) { gif_decode_frame(gif_ctx, result); render_gif_frame(result); gif_state GIF_PAUSE; current_frame_index (current_frame_index 1) % gif_ctx.frame_count; } }5.2 透明色处理与局部刷新减少无效像素重绘GIF的透明色Transparent Color Index在解码后表现为特定索引值如0需在渲染时跳过该像素避免覆盖背景。void render_gif_frame(gif_result_t *r) { for(uint32_t y r-frame_info.y; y r-frame_info.y r-frame_info.height; y) { for(uint32_t x r-frame_info.x; x r-frame_info.x r-frame_info.width; x) { uint8_t idx r-row[(y - r-frame_info.y) * r-frame_info.width (x - r-frame_info.x)]; if(idx ! r-transparent_idx) { // 跳过透明像素 uint16_t color palette_565[idx]; // 预存的RGB565调色板 LCD_DrawPixel(x, y, color); } } } }5.2.1 内存优化技巧复用解码缓冲与调色板GIF解码器的调色板Palette通常为256色×3字节占768字节。F103的Flash可固化此数据避免RAM存储// 在Flash中固化调色板生成C数组 const uint16_t gif_palette_565[256] __attribute__((section(.flash_palette))) { 0xF800, 0xF81F, 0xF83F, /* ... 256个RGB565值 */ }; // 解码时直接读取Flash无需复制到RAM #define PALETTE_BASE ((uint16_t*)0x08008000) // 假设固化在Flash 0x08008000此技巧可节省768字节宝贵SRAM使更多内存用于FreeRTOS堆栈或网络缓冲区。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询