ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32F103上STemWin多页面切换的窗口管理与内存优化实践

STM32F103上STemWin多页面切换的窗口管理与内存优化实践 简介基于STM32F103的STemWin多页面显示工程是一套面向嵌入式开发者与GUI入门学习者的可运行源码。STemWin是意法半导体针对STM32资源受限环境优化的图形库本工程围绕多页面切换这一高频需求提供从LCD初始化、内存分配、窗口创建、控件布局到事件回调的完整工程实现有助于理解嵌入式图形界面的整体运转流程。压缩包为RAR格式共462个文件约12.37MB主要包括153个C头文件、84个C源文件、2个库文件以及Keil工程配置、编译中间文件、hex固件与清理脚本等构成一套可直接打开并烧录验证的完整嵌入式工程。已有417人参与学习。源码内容覆盖了页面管理的关键环节通过创建多个窗口与控件构成独立页面借助栈结构维护页面历史在事件回调中完成界面跳转同时利用控件动态创建和销毁机制缓解内存压力。随包还可见GUIDEMO系列缩放旋转、图标视图、洗衣机界面等演示对系统学习STemWin以及快速落地多界面项目均有直接参考价值。1. 把STemWin多页面当成窗口管理问题而不是画图问题在 STM32F103 上做 STemWinSEGGER emWin 的 ST 授权版多页面显示很多新手一开始就把精力花在绘制每个页面的图形上结果页面能画出来却切不动按钮点了没反应、切页后背景花掉、或者跑一会儿直接进 HardFault。这个标题真正要解决的不是“怎么在屏幕上画东西”而是“在只有 20~64KB SRAM 的 Cortex-M3 上怎么管理多个页面的创建、切换、销毁和重绘”。STemWin 的多页面能力由窗口管理器WM承载核心是窗口回调、WM_ShowWindow/WM_HideWindow这对显隐接口以及内存在GUI_ALLOC堆里的分配策略。本文从移植配置讲起落到一套可复现的源码骨架再把内存排错和真机验证的技巧一起说清楚适合正在用 F103 做完整界面、而不只是跑个 demo 的嵌入式工程师。2. STemWin在STM32F103上的移植与显示驱动关键配置2.1.1 先分清emWin、STemWin和源码包三者的关系STemWin 是 ST 从 SEGGER 获得授权后随 MCU 一起分发的 emWin 版本API 与 emWin 逐函数兼容所以网上搜 emWin 的窗口管理教程结论基本可以直接用在 STemWin 上。区别在于emWin 官方源码包里有GUI/inc、GUI/Lib、GUI/ConvertColor等目录而 STemWin 随 CubeF1 或者旧版 ST 固件库里通常以库文件加Config模板的形式给出。下载到的源码包里真正需要你改的通常只有这几个文件GUIConf.h内存池大小与功能开关、GUIConf.cGUI_X_Config里分配 STemWin 堆、LCDConf_FlexColor.c屏幕尺寸、颜色格式、底层读写函数、GUIDRV_Template.c可选的驱动模板。至于GUI_X_Touch.c和GUI_X_F103.c这类文件不同版本的包名略有出入前者提供触摸坐标采样后者提供GUI_X_Delay和GUI_X_GetTime这两个 OS 相关函数。2.1.2 Keil工程里最小必要文件的组织方式常见做法是把 STemWin 源码的inc头文件目录整个加进 include path同时只把需要的.c文件参与编译。对于 F103我一般这样组织Project/ ├─ STemWin/ │ ├─ inc/ # 全部头文件 │ ├─ Lib/ # STemWin 库文件Keil 下是 .lib │ ├─ Config/ │ │ ├─ GUIConf.h │ │ ├─ GUIConf.c │ │ ├─ LCDConf_FlexColor.c │ │ └─ GUITouchConf.h │ └─ GUI_X/ │ ├─ GUI_X_F103.c # 时钟与延时 │ └─ GUI_X_Touch.c # 触摸采样 ├─ User/ │ ├─ main.c │ ├─ stm32f10x_it.c │ └─ page.c # 多页面源码 └─ Startup/ └─ startup_stm32f10x_hd.s要注意 F103 根据 Flash/RAM 容量分 CL、MD、HD 等型号启动文件必须对应器件startup_stm32f10x_hd.s是 512KB 以上大容量型号用的。如果用的是stm32f103c8t6它属于中容量应使用startup_stm32f10x_md.s。启动文件里Stack_Size和Heap_Size建议至少保持默认值Stack 不要小于 0x400因为 STemWin 窗口回调里如果开了浮点或大局部结构体栈很容易被撑爆。2.1.3 库文件选择与预定义宏Keil 工程中要把 STemWin 的库添加为Library或在 C/C 的 Linker 里Add进去。ST 在 F103 相关的包中提供的库文件名带编译器标识比如 MDK 下以_CM3结尾GCC 场景则是.a后缀。无论选哪个都要在全局 Define 里给出对应配置USE_STDPERIPH_DRIVER STM32F10X_HD其中STM32F10X_HD要与启动文件对应库函数版本的 F103 外设驱动靠这个宏选择寄存器映射范围。另外GUIConf.h中如果启用了GUI_SUPPORT_MEMDEV库内部会维护额外内存结构F103 上不建议开太大的 memdev这点后面第 4 章详述。2.1.4 LCDConf_FlexColor.c 中的像素格式与底层读写STemWin 的显示驱动在 F103 上最常见的组合是FSMC 并口直连 ILI9341/ST7789像素格式用GUICC_565。对应的LCDConf_FlexColor.c里核心是LCD_X_Config和LCD_X_DisplayDriver这两个函数。LCD_X_Config里按下面的方式把驱动和设备链接起来#include GUI.h #include LCD.h #include GUIDRV_FlexColor.h #define XSIZE_PHYS 240 #define YSIZE_PHYS 320 static void LCD_X_Config(void) { GUI_DEVICE *pDevice; CONFIG_FLEXCOLOR Config {0}; GUI_PORT_API PortAPI {0}; pDevice GUI_DEVICE_CreateAndLink(GUIDRV_FLEXCOLOR, GUICC_565, 0, 0); LCD_SetSizeEx(0, XSIZE_PHYS, YSIZE_PHYS); LCD_SetVSizeEx(0, XSIZE_PHYS, YSIZE_PHYS); PortAPI.pfWrite8_A0 LCD_WRITE_A0_REG; /* 写寄存器 */ PortAPI.pfWrite8_A1 LCD_WRITE_A1_DATA; /* 写数据 */ PortAPI.pfWriteM8_A1 LCD_WRITE_MULTI_DATA; /* 连续写多字节 */ GUIDRV_FLEXCOLOR_Config(pDevice, Config); GUIDRV_FLEXCOLOR_SetFunc(pDevice, PortAPI, GUIDRV_FLEXCOLOR_F66708, GUIDRV_FLEXCOLOR_M16C0B8); }F66708是驱动内部对 ILI9341 的配置项表示该款屏以 RGB565 方式刷新。M16C0B8表示显存连续、单个像素两字节、按行写。LCD_SetVSizeEx必须调用STemWin 需要知道虚拟坐标范围否则WM_InvalidateWindow的裁剪矩形可能算错。2.1.5 FSMC 读写函数与总线超时问题底层十六位并口访问直接操作 FSMC 的 bank 地址。F103 的 FSMC 地址映射中Bank1 的 NE1 片选对应0x60000000起始地址习惯上把 A0~A15 地址线接到 LCD 的 RS 引脚#define LCD_REG (*(volatile uint16_t *)0x60000000) #define LCD_RAM (*(volatile uint16_t *)0x60020000) void LCD_WRITE_A0_REG(uint8_t data) { LCD_REG data; } void LCD_WRITE_A1_DATA(uint8_t data) { LCD_RAM data; } void LCD_WRITE_MULTI_DATA(uint8_t *pData, int NumBytes) { uint16_t *p (uint16_t *)pData; while (NumBytes 0) { LCD_RAM *p; NumBytes - 2; } }0x60020000的偏移来自 A0 地址线0x60000000时 A000x60000000 | (1 17)时 A01因为 16 位总线地址按半字对齐A0 对应 CPU 地址的 bit17。这个映射关系不对LCD 会完全没有任何反应。LCD_WRITE_MULTI_DATA里 NumBytes 必须保证偶数STemWin 以 16bpp 刷屏时长度总是偶数但如果自己调用底层接口写字符串就要小心奇数长度。2.1.6 GUIConf.c 内存池与初始化顺序GUIConf.c里GUI_X_Config负责把所有 STemWin 对象的内存集中管理。F103 上常遇到的GUI_ALLOC_ERROR、窗口创建失败、WM_PAINT里访问空指针多半都是这里给的内存太小。#include GUI.h #define GUI_NUMBYTES (12 * 1024) /* F103C8 只能给这么多 */ static U32 aMemory[GUI_NUMBYTES / 4]; void GUI_X_Config(void) { GUI_ALLOC_AssignMemory(aMemory, GUI_NUMBYTES); GUI_ALLOC_SetMemDev(GUI_ALLOC_GetNumBytes() / 4); }GUI_ALLOC_AssignMemory指定 STemWin 动态内存区GUI_ALLOC_SetMemDev设置 memdev 最多占用的字节数。F103C8 只有 20KB SRAM给 STemWin 12KB 后剩下 8KB 给任务栈和库内部使用。GUI_ALLOC_GetNumBytes()返回的是配置的总大小。用GUI_ALLOC_SetMemDev限制 memdev 容量是防止切页时突然申请 150KB 导致崩溃的关键手段。3. STemWin多页面切换WM_ShowWindow与WM_HideWindow的消息驱动实现3.1.1 对话框和顶层窗口的选择STemWin 里做多页面第一反应是用GUI_ExecDialogBox一页一页弹。这个方案在 PC 模拟器上很顺但在 F103 上有个隐患GUI_ExecDialogBox是阻塞式循环执行完当前对话框回调才会返回。连续切换多个页面时主循环里其他任务全被卡死。更稳妥的多页面实现是以WM_CreateWindow创建多个顶层窗口配合WM_ShowWindow和WM_HideWindow在窗口间切换。每个页面有自己的窗口句柄和回调函数STemWin 的窗口管理器保证同一时刻只有可见窗口参与重绘和触摸命中。3.1.2 创建三个页面窗口的最小代码下面是一个三页面主界面、设置页、信息页的骨架这一套代码放在单独的page.c中逻辑与main.c解耦#include GUI.h #include WM.h #include BUTTON.h #define ID_BTN_NEXT 0x10 #define ID_BTN_PREV 0x11 #define ID_BTN_INFO 0x12 #define PAGE_MAIN 0 #define PAGE_SETUP 1 #define PAGE_INFO 2 static WM_HWIN hPage[3]; static void _cbPageMain(WM_MESSAGE *pMsg); static void _cbPageSetup(WM_MESSAGE *pMsg); static void _cbPageInfo(WM_MESSAGE *pMsg); void Page_Init(void) { hPage[PAGE_MAIN] WM_CreateWindow(0, 0, 240, 320, WM_CF_SHOW, _cbPageMain, 0); hPage[PAGE_SETUP] WM_CreateWindow(0, 0, 240, 320, 0, _cbPageSetup, 0); hPage[PAGE_INFO] WM_CreateWindow(0, 0, 240, 320, 0, _cbPageInfo, 0); }WM_CreateWindow参数依次是位置、尺寸、创建标志、回调函数、附加数据。注意只有PAGE_MAIN带WM_CF_SHOW后两页默认隐藏。WM_CF_SHOW让窗口在创建后立刻可见如果三个页面都加了切换之前就会叠在一起底层窗口被上层盖住后还会白白参与裁剪计算所以只让初始页可见。3.1.3 回调函数与WM_NOTIFY_PARENT消息链窗口回调的职责是处理绘制和子控件通知。在按钮页面里WM_NOTIFY_PARENT是 STemWin 向上层窗口报告子控件事件的通道。点击按钮后BUTTON 控件会把通知发给父窗口static void _cbPageMain(WM_MESSAGE *pMsg) { switch (pMsg-MsgId) { case WM_PAINT: GUI_SetBkColor(GUI_WHITE); GUI_Clear(); GUI_SetColor(GUI_BLACK); GUI_SetFont(GUI_Font24_ASCII); GUI_DispStringAt(Main Page, 60, 100); break; case WM_NOTIFY_PARENT: if (pMsg-Data.v WM_NOTIFICATION_CLICKED) { int Id WM_GetId(pMsg-hWinSrc); if (Id ID_BTN_NEXT) { _PageSwitch(PAGE_SETUP); } } break; default: WM_DefaultProc(pMsg); } }pMsg-hWinSrc是发出通知的控件句柄WM_GetId取出创建控件时赋予的 ID。pMsg-Data.v里装的是通知类型WM_NOTIFICATION_CLICKED和WM_NOTIFICATION_RELEASED分别对应按下和抬起。不要在WM_NOTIFICATION_CLICKED里做耗时操作切页动作太快会被下一次触摸事件打断一般切页放在WM_NOTIFICATION_RELEASED里更稳。3.1.4 页面切换函数的状态管理切换函数要处理两个问题目标页显示、其余页隐藏。最简单的是轮询句柄数组static void _PageSwitch(int index) { int i; if (index PAGE_MAIN || index PAGE_INFO) { return; } for (i 0; i 3; i) { if (i index) { WM_ShowWindow(hPage[i]); WM_BringToTop(hPage[i]); } else { WM_HideWindow(hPage[i]); } } }WM_BringToTop把目标窗口移到 z-order 顶部保证它是最后一个被绘制的窗口避免出现隐藏窗口残留像素。WM_HideWindow会把窗口连同子控件一起从可见集合中移除但不会销毁窗口对象所以下次WM_ShowWindow时按钮状态、编辑框内容都还在。3.1.5 在页面上创建按钮控件按钮要挂在页面窗口上作为它的子窗口。用WM_CreateWindowAsChild配合WM_BUTTON_CREATE或者用更高层的BUTTON_Createvoid Page_CreateButtons(void) { BUTTON_Create(150, 280, 70, 30, hPage[PAGE_MAIN], WM_CF_SHOW, ID_BTN_NEXT); BUTTON_SetText(hItem, Next); }BUTTON_Create的最后一个参数是资源 ID这个 ID 会从WM_GetId读出来。按钮文本默认用 ASCII 字体要显示中文需设置包含中文字符的字体如GUI_FontHZ_SimSun_16或外挂 XBF 字库。所有子窗口都跟随父窗口的显隐状态父窗口隐藏时子控件不会收到任何触摸或重绘消息这一点是多页面方案能成立的基础。3.1.6 切页闪烁的根因与WM_CF_MEMDEV的取舍闪烁的本质是旧窗口消失后、新窗口重绘完成前屏幕有一段时间显示的是背景色。F103 上最直接的解决方式是用WM_CF_MEMDEV建窗口把每个页面的绘制结果缓存到内存。但这个方案在 F103 上几乎不可行——一帧 240x320x2 字节就是 150KB流程上必须先把窗口内容画进内存再整体拷贝到显存这对 20KB SRAM 的 F103C8 是天花板以外的需求。因此切页闪烁的常见取舍是提示启用WM_SetCreateFlags(WM_CF_MEMDEV)只对单个页面的局部小窗口生效不要对整屏顶层窗口开 MEMDEV。F103 上更实际的策略是“先隐藏、后显示”并确保新页面 WM_PAINT 里只用一次GUI_Clear完成背景填充避免绘制过程中背景色先露出来。4. STM32F103内存极限下的STemWin页面资源管理4.1.1 一页一窗口还是复用单个窗口上一章的方案是“三个页面三个窗口”但 F103 的资源不总是允许这样。每个顶层窗口在GUI_ALLOC堆里都占用独立的窗口结构体、裁剪区域和控件列表创建 3~4 个页面窗口通常需要 2~4KB 的堆空间。如果工程里还要跑 FreeRTOS 任务栈、Modbus 协议栈或者 FATFSSTemWin 内存池就不可能给到 12KB。另一个常见做法是复用单个窗口只有一个顶层窗口切换时靠重置窗口内容和回调数据。比如用一个current_page变量记录当前页窗口回调的WM_PAINT里根据这个变量画不同界面。这个方案省内存但是WM_NOTIFY_PARENT需要自己区分按钮 ID 对应的页面回调里条件分支会越来越多。对页数与资源建议按下面的表格估算后选择方案内存场景推荐方案代码结构STemWin堆 ≥ 12KB页面 ≤ 4多窗口 Show/Hide每页独立回调消息清晰STemWin堆 8KB~12KB页面 ≤ 6多窗口但动态创建销毁切页时WM_DeleteWindow旧页STemWin堆 8KB单窗口 状态机回调里switch(current_page)单窗口方案里注意GUI_SetBkColor和GUI_Clear要在每次WM_PAINT开头执行因为窗口内容没有任何缓存。4.1.2 动态创建与静态创建的内存差异静态创建初始化时全部建好的优点是运行期没有分配失败的风险缺点是所有窗口同时占内存。动态创建则把窗口对象的生存期缩短切到设置页时才创建设置页离开时WM_DeleteWindow立刻释放。代价是每次创建都要重新设置控件内容和文本而且频繁分配释放会在GUI_ALLOC堆里产生碎片。WM_DeleteWindow会递归删除窗口及所有子控件但它不回卷GUIConf.h里GUI_ALLOC_AssignMemory的释放顺序。碎片多了以后会出现“总剩余内存够但连续区段不够”的情况。因此动态方案里要监控GUI_ALLOC_GetNumFreeBytes()长时间运行后的变化。这个方法放到最后第 5 章讲验证时会给出具体写法。4.1.3 GUI_ALLOC堆水位与峰值统计STemWin 提供了几个查询内存状态的函数可用来在开发阶段打印堆水位#include GUI.h void Mem_PrintStatus(void) { int Total, FreeBytes; Total GUI_ALLOC_GetNumBytes(); FreeBytes GUI_ALLOC_GetNumFreeBytes(); printf(ALLOC Total%d Free%d MaxUsed%d\r\n, Total, FreeBytes, GUI_ALLOC_GetMaxUsed()); }GUI_ALLOC_GetMaxUsed()返回历史最高内存占用这个值能直接告诉你能否把GUI_NUMBYTES调小。GUI_ALLOC_GetNumFreeBytes()是当前剩余值页面切换后在页面上停留几秒等所有延迟重绘执行完再读才是真实水位。不要在WM_PAINT里打印那会使重绘时间拉长内存占用比稳态高很多。4.1.4 位图与字体的内存优化点多页面界面上最常见的非窗口内存开销是位图和字体。F103 上放 240x320 整屏位图是不现实的一张就 150KB。常见做法是小图标用 4bpp/1bpp 压缩位图GUI_DrawBitmap配合LCD_BITMAP结构体显示中文界面用 XBF 外部字体从 SPI Flash 读取不要在内部 SRAM 放二级字库对话框内的 JPEG/PNG 图片用GUI_JPEG_Draw边解码边显示但它要求GUIDRV支持多缓冲F103 上还是建议用 BMP 转换工具生成 C 数组或 XBF。4.1.5 屏幕亮度与背光的电源侧考虑严格说背光控制不属于 STemWin 窗口管理但多页面切换若伴随 PWM 调光页面切换时 PWM 占空比突变会造成肉眼可见的闪屏。通常的做法是把 PWM 更新放到单独的低优先级代码里不要在WM_NOTIFY_PARENT里直接改TIM_SetCompare后立刻刷屏。更不要在每个页面的WM_PAINT里重置背光变量那样页面切换时会看到亮度跳变。5. STemWin多页面在真机上的验证与关键排错细节5.1.1 测量页面切换耗时的硬件计时法把切换耗时量出来比盯着屏幕看流畅度可靠。F103 内核自带 DWT 周期计数器不占用定时器资源#include core_cm3.h static volatile uint32_t tStart; void DWT_Init(void) { CoreDebug-DEMCR | CoreDebug_DEMCR_TRCENA_Msk; DWT-CYCCNT 0; DWT-CTRL | DWT_CTRL_CYCCNTENA_Msk; } void _PageSwitch(int index) { tStart DWT-CYCCNT; /* 原有切换逻辑 */ // ... } void Page_GetSwitchTimeUs(uint32_t *us) { *us (DWT-CYCCNT - tStart) / (SystemCoreClock / 1000000); }切换完成后在目标页的第一个WM_PAINT末尾再读一次DWT-CYCCNT差值除以主频就是消耗的时间。不要把这个调用放在隐藏窗口的WM_PAINT里隐藏窗口不会触发绘制读到的时长会偏小。F103 在 72MHz 下一页 240x320 纯文字界面刷新目标应控制在 30ms 以内超过 100ms 就需要检查 FSMC 时序是否偏慢或GUI_Exec频度不够。5.1.2 切换后屏幕无响应的排查顺序切过去以后页面不刷新或者点按钮没反应按这个顺序查先看新窗口回调是否收到WM_PAINT。在这些代码里加打印。连WM_PAINT都没收到说明窗口还是隐藏状态检查_PageSwitch里WM_BringToTop是否被漏调。收到WM_PAINT但屏幕没变问题在 FSMC 写显存的方向或首地址偏移。触摸点不到按钮则要验证触摸层坐标是否跟屏幕同原点很多电容触摸屏的 Y 轴方向和 LCD 相反这时要用GUI_TOUCH_Calibrate做坐标变换。5.1.3 GUI_ALLOC 碎片与多页面长期运行的稳定性验证多页面长时间切来切去内存碎片会积累。一个有效的验证手法是让设备在 3 个页面间以固定节奏循环切换 1000 次每隔 50 次打印一次GUI_ALLOC_GetNumFreeBytes()。如果剩余内存持续下降说明有窗口被反复创建却没有删除。如果剩余内存在某次切页后突然变少几 KB多半是某个页面创建了大量控件且WM_DeleteWindow没有正确执行。static void _PageStressTest(void) { static int cnt 0; static int page 0; if (cnt % 50 0) { Mem_PrintStatus(); } page (page PAGE_INFO) ? PAGE_MAIN : page 1; _PageSwitch(page); }在main里用软件定时器每 500ms 调一次_PageStressTest跑 10 分钟看打印碎片问题在开发阶段就能暴露。注意这个测试函数只在调试编译里使用发布版本要移除否则会影响正常触摸响应节奏。5.1.4 STemWin页面停留在某页不再刷新的特定场景还有一种情况是切到设置页后编辑框光标还在闪但界面不再响应其他按钮。这通常是因为某个事件处理函数里写了一个长循环等待标志位把 WM 的重绘消息全堵住了。STemWin 的WM_Exec是靠主循环反复调用来驱动消息分发的任何阻塞都会导致整个界面冻结。把耗时操作拆成状态机或者用定时器消息WM_TIMER分段完成是这类问题的标准解法。最后补充一个很隐蔽的坑F103 的启动文件里堆大小设置过小时GUI_ALLOC_AssignMemory的静态数组没问题但malloc族函数会被底层库使用如果你在页面上用了GUI_ALLOC_Alloc之外的标准库内存例如某些字库加载代码里调malloc堆不足会返回 NULL然后 STemWin 收到 NULL 指针后直接崩溃。用工程内搜索把所有的标准库malloc换成GUI_ALLOC_Alloc或者在启动文件里把Heap_Size明确改到 0x800 以上这一步能避免一大类只在真机复现的 HardFault。本文还有配套的精品资源点击获取
返回列表