简介:STM32F103ZET6单片机STemWin-GIF图片显示实验例程源码,面向使用该型号单片机的嵌入式开发者,演示如何在STemWin图形库中加载与播放GIF动态图片。例程涵盖LCD显示初始化、外设配置、STemWin库初始化等流程,并展示图片尺寸调整、位置控制与循环播放的实现方法,可作为带图形用户界面的智能设备或物联网终端开发参考。压缩包共652个文件,以415个h头文件、181个c源文件为主体,另有汇编、链接脚本与工程配置文件,整体仅5.34MB,结构紧凑,便于阅读与工程移植。已有54人学习使用,适合掌握基础STM32开发、希望进阶STemWin图形界面及嵌入式图像处理技术的开发者。通过这份源码,开发者不仅能掌握GIF图像在嵌入式设备上的显示方法,还能深入理解STemWin图形库的框架与调用逻辑,包括内存管理、图形渲染和资源占用优化等实用技能,为后续开发更具交互性的产品界面打下扎实基础。
1. 一个能放 GIF 动图的 STM32F103ZET6 例程,核心难点在帧管理
做 stm32f103zet6 智能仪表的工程师应该都遇到过:状态页、图标区、开机动画希望动起来,但一放 GIF 就内存溢出或者画面撕裂。这个例程是在 STM32F103ZET6 + STemWin 环境下跑通 GIF 动态显示的一份完整 Keil 工程源码,代码包里既有 uC/OS 的汇编移植文件,也有 STemWin 程序文件与中文编码转换文件。它不只是一个“点屏”实验,而是把文件路径、固化资源、帧缓冲和 GUI 时钟同步都串起来的样例。适合正在做嵌入式 HMI、单片机毕业设计界面或工业控制面板的开发者阅读,尤其适合对 GIF 显示还停留在“取模位图”阶段的人。
2. 源码包工程骨架:先弄懂每个文件再谈 GIF
拿到压缩包后直接搜索 GIF 函数会浪费很多时间。这个例程的核心文件是 Iconviewwin_demo.c,但它不是独立存在的。整个 Keil 工程把 uC/OS 移植层、STemWin 库和 FATFS 相关的编码转换文件放在一起,分开看每个文件都清楚,合在一起很多人会懵。
2.1 解压后这些文件各负责什么
先对照源码包里的文件清单,建立“谁被谁调用”的感觉。
| 文件 | 工程角色 |
|---|---|
| Template.uvguix.Administrator | Keil 工程窗口布局文件,记录调试器视图和断点位置,不参与编译 |
| cpu_a.asm / lib_mem_a.asm / os_cpu_a.asm | uC/OS 在 Cortex-M3 上的汇编移植层,分别是上下文切换、内存拷贝和 OS 调度器底层的汇编入口 |
| keilkilll.bat | 批量清理 Keil 编译产生的 .o、.axf、.crf 临时文件,用来减小打包体积 |
| Iconviewwin_demo.c | 主演示文件,创建图标视窗并触发 GIF 显示逻辑 |
| prechin.c | 中文字符串预处理相关的初始化代码,常见于编码切换或字库选择 |
| cc936.c | 双字节编码转换表,主要服务于 FATFS 读取中文文件名时进行 GBK 与 Unicode 转换 |
其中最容易让人误会的是 cc936.c。很多人以为它和 GIF 无关,但当 GIF 文件是放在 SD 卡上且文件名带中文“开机动画.gif”时,FATFS 在没有这个转换表的情况下会直接返回找不到文件。所以它触发的是文件层错误,不是显示层错误。
2.2 uC/OS 移植文件为什么出现在 GUI 例程里
STemWin 可以裸机跑,但这个例程选择了带 RTOS 的方式。原因很实际:GIF 动画的帧率不高,但每帧的渲染时间不稳定,如果裸机循环里用 GUI_Delay 等同步,按键扫描和串口打印都会被拖累。uC/OS 提供了任务调度,可以让 GUI 刷新任务、数据采集任务和通信任务并行。代价是要多编译三个汇编文件,同时要完成 STemWin 与操作系统的粘连。
STemWin 提供的锁与时基函数在 OS 环境下需要被实现。常见做法是在 GUI_X 层提供互斥信号量和系统时基。以 uC/OS-III 为例,通常会实现下面几个接口:
void GUI_X_InitOS(void) { OS_ERR err; OSSemCreate(&gui_Sem, "GUI lock", 1, &err); } void GUI_X_Lock(void) { OS_ERR err; OSSemPend(&gui_Sem, 0, OS_OPT_PEND_BLOCKING, NULL, &err); } void GUI_X_Unlock(void) { OS_ERR err; OSSemPost(&gui_Sem, OS_OPT_POST_1, &err); }这段代码的作用是让 STemWin 在访问 LCD 控制器前获取一个互斥信号量,防止其他任务同时写显存导致花屏。参数 OS_OPT_POST_1 表示只释放一个信号量,如果同时有多个任务等待,系统会按优先级唤醒其中一个。如果你在工程里找不到 GUI_X 的实现文件,多半是在 STemWin 库的某种配置宏里被切换到了裸机模式。
2.3 keilkilll.bat 的清理逻辑值得抄进自己的工程
这个 bat 文件很小,但非常实用。它的本质是删除 Keil 编译过程中生成的中间文件,保留源码和工程配置。常见的脚本内容长这样:
@echo off del /s /q *.o del /s /q *.d del /s /q *.crf del /s /q *.axf del /s /q *.sct del /s /q *.uvguix.* rmdir /s /q Listings rmdir /s /q Objects echo clean done/s表示递归删除子目录中的文件,/q是安静模式,不逐一询问。*.uvguix.*会匹配 Template.uvguix.Administrator 这类用户界面布局文件,因为不同电脑上的管理员账户名不同,这个文件打包给别人时经常导致打开工程显示异常,直接删掉更省事。看似和 GIF 无关,但如果你在网盘下载的源码包里有这个脚本,一般说明作者用 Keil 5 环境整理过工程。
另外,rmdir /s /q Listings删掉的是工程生成的列表文件,重新编译会自动重建。这样打包出来的源码体积会小很多,发布到网盘或 git 仓库都更合适。
3. STemWin 的 GIF 解码不是开箱即用:间接存储与调色板
在 STemWin 中直接调用一个“显示GIF”的 API 是不存在的。它的设计思路是把 GIF 当作一种动态位图资源,由解码器生成内部对象,再通过帧索引拿到每帧的位图结构。这个机制决定了你可以显示 GIF,但必须理解和控制它的存储策略。
3.1 GIF 解码链路:从文件到 GUI_BITMAP
一个 GIF 文件在 STemWin 里常规的处理路径是这样的:
- 用
GIF_GetImageInfo读取文件头,拿到宽高、帧数、循环次数; - 用
GIF_CreateIndirect把整个 GIF 文件解析为一个内部句柄; - 用
GIF_GetImage按帧索引取出某一位图; - 用
GUI_DrawBitmap把位图画到窗口或者设备上。
关键在第 2 步:GIF_CreateIndirect返回的是一个GUI_HMEM句柄,不是显存地址。之后每取一帧,STemWin 会在内存池里为当前帧的调色板和像素数据分配临时缓冲。所以你的 RAM 大小直接决定能解码多大的 GIF。
下面这段代码用来读取一个已经编译到程序里的 GIF 资源:
#include "GUI.h" #include "GIF.h" GIF_INFO info; const unsigned char acGIF[] = _acBootAnimation; /* 由 bin2c 工具生成的数组 */ void GIF_Demo_ReadInfo(void) { int rc = GIF_GetImageInfo((const void*)acGIF, sizeof(acGIF), &info); if (rc == 0) { printf("Frames=%d Size=%dx%d Loop=%d\r\n", info.NumFrames, info.xSize, info.ySize, info.LoopCount); } }注意GIF_GetImageInfo的返回值:0 表示解析成功,其他值说明数据被截断或非 GIF 格式。这里sizeof(acGIF)传的是数组字节数,不要传成数组元素个数。GIF_INFO结构体里的NumFrames在动图多帧场景下特别关键,它是帧循环结束条件的依据。
3.2 三种素材载入方式选型
GIF 数据存在哪,直接决定例程在 STM32F103ZET6 上能不能跑起来。256 KB 片内 RAM 对裸数据来说很大,对 GIF 解码来说不够看。典型做法分三种:
| 载入方式 | 空间来源 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 转成 C 数组编进固件 | 片内 Flash 512KB | 加载快,无文件系统依赖 | 需要 bin2c,改图要重新编译 | 固定开机动画、Logo |
| 存在 SPI Flash,运行时搬到 RAM | 外扩 Flash | 容量大,可远程更新 | 需要文件系统和地址映射 | 多套动图切换 |
| 存在 SD 卡,FATFS 直接读 | 外部存储 | 随意替换 GIF | 读取速度受 SD 卡和底层影响 | 用户自定义主题 |
对于这个例程,Iconviewwin_demo.c默认示范的是最稳妥的第一种方式。因为 STM32F103ZET6 的 512KB Flash 通常在存储完 GUI 字库后还剩一部分空间,放一个小尺寸 GIF 文件正合适。需要强调:片内 RAM 的 64KB 不一定够同时保存多个完整帧,所以 GIF 的宽高必须控制在 200x200 以内,帧间差异较大的动画甚至要更小。
3.3 调色板与色彩转换
GIF 是索引色,最多 256 种颜色,而 STM32F103ZET6 的 LCD 多半是 RGB565 或 RGB888。STemWin 的内部机制会把 GIF 调色板转换成当前硬件配置的颜色格式,但如果每次取帧都全量转换,开销会非常大。更合理的做法是配合GIF_GetImage传入前一帧位图指针,让解码器做增量更新。
static GUI_BITMAP _prevBmp; void GIF_DrawFrame(GUI_HMEM hGIF, int index) { GUI_BITMAP bm; int rc = GIF_GetImage(hGIF, index, &bm, &_prevBmp); if (rc == 0) { GUI_DrawBitmap(&bm, 0, 0); _prevBmp = bm; } }第四个参数传&_prevBmp表示告诉解码器上一帧长什么样,解码器可以利用 GIF 的帧间压缩特性只更新变化区域,这在 320x240 分辨率下能明显减少总线占用。注意,_prevBmp需要保证在两次调用之间不被其他窗口当普通位图释放,所以通常用静态变量保存。
4. 在 STM32F103ZET6 上复现 GIF 显示:初始化、句柄与回调
前面讲的是原理,接下来是可以在 Keil 工程里照抄的步骤。操作流程分四段:初始化底层、创建 GIF 句柄、用定时器逐帧刷新、处理内存不足场景。
4.1 首先是 LCD 和 STemWin 的初始化
和普通裸机点屏不同,STemWin 环境下的 LCD 初始化要交给底层配置函数。常见的初始化序列如下:
void STemWin_Init(void) { GUI_Init(); /* 初始化 STemWin 内核 */ WM_SetCreateFlags(WM_CF_MEMDEV); /* 窗口重绘时使用内存设备防闪烁 */ GUI_SetColor(GUI_WHITE); GUI_Clear(); }参数说明:WM_SetCreateFlags(WM_CF_MEMDEV)会让每个新建窗口自动带一块内存设备,窗口刷新时先在缓冲区绘制,再一次性刷到 LCD。对 GIF 动画这种高频刷新场景来说,这个开关可以减少撕裂感,但代价是每个窗口多占一块 RAM。如果工程本身很吃内存,建议把参数换成 0,关闭自动内存设备,用窗口的WM_SetCallback在绘制时手动GUI_ClearRect。
初始化之后,还要配置 STemWin 的系统时钟和 OS 时基,否则定时器消息不会触发。在GUI_X_Config里设置GUI_OS_Init或时钟节拍分辨率时,通常把 tick 配置成 1ms,这样WM_RestartTimer的延迟参数能精确到毫秒级。
4.2 用 GIF_CreateIndirect 创建动图句柄
这是整个例程最重要的一步。把 GIF 文件数据交给解码器,生成句柄:
#define GIF_RAW_SIZE (sizeof(_acBootAnimation)) #define GIF_FRAME_MS (80) /* 每帧刷新间隔 */ static GUI_HMEM _hGIF; static int _curFrame; int GIF_Anim_Create(const unsigned char *raw, int len) { _curFrame = 0; _hGIF = GIF_CreateIndirect((const void*)raw, len, 0); return (_hGIF != 0) ? 0 : -1; }第三参数 0 表示从第 0 帧开始解析。返回值是句柄,它在 STemWin 内部是一个整数标识符。如果 GIF 文件过大、内存分配失败,返回值是 0,因此判断条件用_hGIF != 0。GIF_FRAME_MS是帧间隔,80ms 约等于 12.5 FPS,对大多数状态页图标足够,如果想更流畅可以改成 50ms,但要注意 CPU 占用。
创建完成后,在需要清理的地方调用GIF_Delete(_hGIF)释放内部内存池。这里不释放的话,下次创建新 GIF 会看到 GUI_ALLOC 的堆使用量节节升高。
4.3 用窗口回调定时器逐帧刷新
STemWin 的窗口框架里,WM_TIMER是驱动动画的首选方式。在窗口回调函数中处理:
static void _cbGIFAnim(WM_MESSAGE *pMsg) { GIF_INFO info; GUI_BITMAP bm; switch (pMsg->MsgId) { case WM_CREATE: /* 创建后立刻启动一个 80ms 定时器 */ WM_CreateTimer(pMsg->hWin, 0, GIF_FRAME_MS, 0); break; case WM_PAINT: if (GIF_GetImage(_hGIF, _curFrame, &bm, NULL) == 0) { GUI_DrawBitmap(&bm, 0, 0); } break; case WM_TIMER: GIF_GetImageInfo((const void*)_acBootAnimation, sizeof(_acBootAnimation), &info); _curFrame = (_curFrame + 1) % info.NumFrames; WM_RestartTimer(pMsg->Data.v, GIF_FRAME_MS); WM_InvalidateWindow(pMsg->hWin); /* 触发重绘 */ break; default: WM_DefaultProc(pMsg); break; } }这段代码的逻辑是:窗口创建时启动一个定时器,每次定时器到期就更新_curFrame,然后调用WM_InvalidateWindow使窗口重绘,最后WM_RestartTimer重新计时。WM_CreateTimer的第二个参数是定时器 ID,这里用 0;第三个参数是超时毫秒数;第四个参数是窗口句柄的附加参数,一般传 0。
注意WM_PAINT里GIF_GetImage的第四个参数传 NULL,这是为了演示最简单的情况。实际例程中会传_prevBmp做差异刷新,但那样_prevBmp必须与窗口一一对应,稍不留意就会串帧。建议先跑通这种全量绘制的版本,确认没问题后再做增量优化。
4.4 内存预算与裁剪策略
以常见的 240x320 全屏 GIF 为例,每个全帧位图在 RGB565 下是 2403202 = 150 KB,一片 64KB 的片内 RAM 根本装不下。所以要动态显示必须压缩条件:
| GIF 尺寸 | 单帧 RGB565 估算 | STM32F103ZET6 是否可全帧常驻 |
|---|---|---|
| 64x64 | 8 KB | 可以 |
| 96x96 | 18 KB | 可以,但只剩很少堆给 GUI |
| 160x120 | 37.5 KB | 困难,需要关闭其他大型窗口 |
| 240x320 | 150 KB | 不可,必须转为 BPP8 索引或增量刷帧 |
实际操作中有两个调节旋钮:一个是 STemWin 的颜色深度配置,把LCD_BITSPERPIXEL改成 8,会让位图缓冲直接减半,但色彩精度下降;另一个是使用GUI_ALLOC_SetSizeHint调整内存池的大小,给 GIF 句柄预留一段固定堆空间,防止运行中途申请碎片化内存失败。例程源码里如果看不到这两个宏,就去 stm32f103zet6 启动文件的堆大小设置里把 Heap_Size 从默认值调大,比如从 0x200 调到 0x800。
5. 帧率、内存碎片与中文路径:GIF 例程的进阶验证
5.1 用串口验证解码信息
别急着调画面,先在你的 main.c 里加一段串口调试代码,把 GIF_INFO 结构体的关键成员打印出来。能正确打印出 Frames、LoopCount,说明 GIF 源数据没问题;打印不出来,问题在数据长度或对齐上。注意GIF_GetImageInfo传参时不要使用指向 Flash 的const unsigned char *强制转换成void *,emWin 内部会直接读取该地址,因此常量数组必须在编译时对齐为 4 字节边界,否则某些编译器优化下会 HardFault。
5.2 cc936.c 与中文 GIF 文件名
如果你的 GIF 不是编译进固件,而是放在 SD 卡里通过 FATFS 加载,那么 cc936.c 的作用就体现出来了。必须在ffconf.h中设置_CODE_PAGE为 936,并将f_open的文件名参数按 GBK 编码传递。常见错误是在 Windows 上把文件名存成 UTF-8,单片机上读不到,需要转换成 GBK 后再调用。调试方法是用f_printf把文件名的字节打印出来:中文“开”在 GBK 里是 0xBF 0xAA,在 UTF-8 里是 0xE5 0xBC 0x80,一眼就能分辨。
FRESULT Gif_OpenFromSD(const TCHAR *path) { FIL fil; FRESULT rc = f_open(&fil, path, FA_OPEN_EXISTING | FA_READ); if (rc != FR_OK) { return rc; } /* 读出文件长度并分配缓冲区,再把数据交给 GIF_CreateIndirect */ return rc; }参数说明:path必须是 GBK 编码的字符串。如果你用了f_chdir切换目录,路径里的斜杠方向必须是/,不能是 Windows 风格的\。
5.3 用 GUI_ALLOC_GetMaxUsed 定位内存瓶颈
GIF 显示最头疼的问题是“能运行,但一段时间后黑屏”。这往往是 STemWin 内存池碎片化后,GIF_GetImage申请帧缓冲失败。不要猜,直接在定时器消息里调用:
int maxUsed = GUI_ALLOC_GetMaxUsed(); int curUsed = GUI_ALLOC_GetUsed(); printf("GUI alloc: max=%d cur=%d\r\n", maxUsed, curUsed);如果maxUsed一直涨,说明有句柄没有释放,重点检查GUI_DeleteChild或GIF_Delete是否在窗口销毁时被调用。如果maxUsed稳定但GIF_GetImage仍返回错误,说明碎片化严重,把GUI_ALLOC_SetSizeHint的预留块调大即可。
本文还有配套的精品资源,点击获取