资讯动态

STM32F429上STemWin与FreeRTOS移植及按钮控制LED实战

发布时间:2026/10/26 13:20:28 来源:尧图企业网站定制
简介面向STM32F429 DISC1开发板的嵌入式GUI与RTOS实践工程聚焦STemwin图形库与FreeRTOS在Cortex-M4平台上的协同移植并以GUIbuilder生成两个按钮事件控制LED亮灭适合已具备STM32基础、希望进阶掌握图形界面与实时任务调度的开发者参考。压缩包共530个文件、约18.26MB包含大量h头文件与c源文件另有o、d、crf等编译中间文件及uvproj、sct、hex、map等工程与构建产物便于还原Keil工程结构、核对库文件配置和编译结果。资源已有966人学习下载说明其在同类移植案例中具有一定参考价值。读者可从中了解STemwin的LCD与触摸配置、内存分配策略、FreeRTOS任务创建与调度、任务间同步通信以及GUI事件回调同硬件控制之间的衔接并理解竖屏显示下的布局调整思路。完整工程配置与编译输出还可用于对照中断处理、库文件集成和从GUIbuilder生成代码到集成编译的组织方式适合课程设计、项目复现与排错对照。1. 一块 240×320 的屏加两颗 LED为什么值得把 STemWin 塞进 FreeRTOSSTM32F429I-DISC1 在抽屉里躺了半年180MHz 的 M4、8MB SDRAM、板载 2.4 寸 240×320 的 TFT。我想用 STemWin 做两个按钮控制 LED再把 FreeRTOS 拉进来做任务划分界面直接用 GUIBuilder 拖出来。裸机跑 emWin半小时能出画面一旦加了 RTOSGUI_Init 卡死、按钮点十次只响应六次、刷屏撕裂这些问题全冒出来了。这篇就把这套组合拆开GUI_X 层怎么接 FreeRTOS 的锁和延时、GUIBuilder 生成的窗口代码哪几行必须改、按钮事件怎么落到 GPIO、显存放内部 SRAM 还是外部 SDRAM以及我实际翻车过的五个现场。适合已经能把 LED 点起来、想让板子有个像样交互界面的人。2. 移植 GUI_X 层时基、递归锁和任务优先级这三处定错了后面全是玄学多数人移植 emWin 的顺序是先把画面跑通再考虑 OS。结果是加了 FreeRTOS 之后开始出现“偶尔白屏”“按钮按十次响六次”这类说不清的现象。根因不复杂emWin 的 API 内部有一堆全局状态——当前裁剪区、当前字体、当前前景色、内存池链表——两个任务同时进来必然互相踩。把GUI_OS打开之后emWin 会给每次 API 调用自动加锁代价是你得把GUI_X_Lock/GUI_X_Unlock这两个钩子接到 FreeRTOS 的互斥量上。2.1 三种集成方式的取舍第一种是裸机大循环里调GUI_Delay()最省事但 GUI 一阻塞整个系统都停串口收包、按键扫描全得等。第二种是把 GUI 放进一个独立任务GUI_OS 1让 emWin 自己管锁业务任务通过窗口消息和 GUI 任务交互。第三种是自己在外层包一把大锁所有 GUI 调用前后手动加解锁本质是把 emWin 当成一个不可重入库来用。我一般选第二种。理由是窗口控件WINDOW、BUTTON、TEXT本身不是线程安全的WM_DefaultProc、WM_InvalidateWindow这些调用会改窗口树和无效区域链表靠外层一把粗锁很难覆盖到 emWin 内部的回调路径。用GUI_OS 1之后锁的粒度由 emWin 自己决定虽然多了一点开销但省下来的是排查随机死机的时间。第三种只在一种情况下值得用手上是精简版 emWin不带 GUI_X_OS 相关接口没法开GUI_OS。那就老老实实只让一个任务碰 GUI其它任务一律通过队列传消息别去省那点通信开销。2.2 配置项一次到位GUIConf.h 与 FreeRTOSConfig.h这两份头文件我习惯对着一起改因为 emWin 的内存池和 FreeRTOS 的堆是两套独立的东西混起来最容易出问题。配置项所在文件建议值说明GUI_OSGUIConf.h1打开 emWin 内部锁多任务下必须GUI_WINSUPPORTGUIConf.h1窗口和按钮控件全靠它关掉就没有 WM_ 系列 APIGUI_SUPPORT_TOUCHGUIConf.h1有触摸就开纯按键触发也可以留着GUI_SUPPORT_MEMDEVGUIConf.h1内存设备防闪烁的主力GUI_NUMBYTESGUIConf.h0x1000064KBemWin 自己的内存池窗口、按钮、内存设备都从这里分配GUI_DEFAULT_FONTGUIConf.hGUI_Font8x16240 宽的屏放得下字再大按钮文字就挤了configTOTAL_HEAP_SIZEFreeRTOSConfig.h32×1024任务栈、队列、信号量都从这出configUSE_RECURSIVE_MUTEXESFreeRTOSConfig.h1递归锁要用别漏configTICK_RATE_HZFreeRTOSConfig.h1000让 tick 直接等于毫秒GUI_X_GetTime不用换算configCHECK_FOR_STACK_OVERFLOWFreeRTOSConfig.h2GUI 任务栈溢出是最难查的死机之一GUI_NUMBYTES这块内存不要用malloc分配直接用静态数组并且保证 4 字节对齐/* GUI 内存池静态分配避免和 FreeRTOS 的 heap 抢同一片区域 */ #define GUI_NUMBYTES 0x10000 static U32 aGuiMem[GUI_NUMBYTES / 4]; /* 4 字节对齐由类型保证 */ void GUI_X_Config(void) { GUI_ALLOC_AssignMemory(aGuiMem, GUI_NUMBYTES); GUI_ALLOC_SetAvBlockSize(0x80); /* 小于 128 字节的块合并减少碎片 */ }GUI_ALLOC_SetAvBlockSize这个参数值得单独说一句emWin 的内存池是块式分配按钮文字、窗口对象、内存设备都是小块。如果平均块大小设得太小长时间反复创建销毁窗口会产生大量碎片最后表现为“明明还剩 40KB就是创建不出一个对话框”。我一般在 0x80 到 0x100 之间取。2.3 GUI_X 层几个函数把延时交出去把锁收进来emWin 需要你提供的 OS 抽象接口不多但每一个都不能糊弄。核心是四个初始化、加锁、解锁、延时。工程里新建一个GUI_X_FreeRTOS.c/* GUI_X_FreeRTOS.c —— emWin 的操作系统抽象层 */ #include GUI.h #include FreeRTOS.h #include task.h #include semphr.h static SemaphoreHandle_t xGuiLock NULL; void GUI_X_Init(void) { /* 用递归互斥量同一任务里嵌套调用 GUI 函数不会把自己锁死 */ xGuiLock xSemaphoreCreateRecursiveMutex(); configASSERT(xGuiLock ! NULL); } void GUI_X_InitOS(void) { GUI_X_Init(); } void GUI_X_Lock(void) { xSemaphoreTakeRecursive(xGuiLock, portMAX_DELAY); } void GUI_X_Unlock(void) { xSemaphoreGiveRecursive(xGuiLock); } U32 GUI_X_GetTaskId(void) { return (U32)xTaskGetCurrentTaskHandle(); } int GUI_X_GetTime(void) { return (int)xTaskGetTickCount(); /* tick 1ms 时直接就是毫秒 */ } void GUI_X_Delay(int Period) { TickType_t t pdMS_TO_TICKS(Period); vTaskDelay(t ? t : 1); /* 不足 1 tick 也要让出一次 CPU */ }几个参数和选择的理由说一下。xSemaphoreCreateRecursiveMutex而不是普通互斥量是因为 emWin 内部存在嵌套调用路径比如WM_Exec()里会回调窗口的WM_PAINT而WM_PAINT里你又会调GUI_SetColor同一任务对同一把锁拿两次。用普通互斥量会直接死锁现象是界面画到一半卡住、调试器一看停在xQueueSemaphoreTake。GUI_X_GetTime返回的是毫秒计数emWin 用它算动画、超时、GUI_Delay的实际耗时。如果configTICK_RATE_HZ配成 100那xTaskGetTickCount()的一格就是 10ms这里就必须乘 10否则 emWin 认为时间走得比实际慢十倍WM_CreateTimer全部失准。GUI_X_Delay里那个t ? t : 1是血泪经验。tick 频率低于 1000Hz 时pdMS_TO_TICKS(1)会算成 0vTaskDelay(0)在 FreeRTOS 里意思是“让出一次 CPU 但立即回到就绪队列”GUI 任务会疯狂空转把低优先级任务饿死。注意GUI_StoreKey、GUI_TOUCH_StoreState这类会进 emWin 内部的函数不能在中断里调中断上下文拿不到互斥量。要传按键就在中断里发信号量或队列让任务去调。2.4 GUI 任务与优先级为什么它不该是最高的GUI 任务本身写起来很短但栈大小和优先级这两项定错后面所有问题都会显得莫名其妙。#define GUI_TASK_STACK 768 /* 单位是 word768 word ≈ 3KB */ #define GUI_TASK_PRIO 3 #define LED_TASK_PRIO 2 static void vGuiTask(void *pvParameters) { (void)pvParameters; GUI_Init(); /* 内部会调 LCD_X_Config / LCD_X_Init */ WM_SetCreateFlags(WM_CF_MEMDEV); /* 窗口先画到内存设备防闪烁 */ CreateWindow(); /* GUIBuilder 生成的入口函数 */ for (;;) { GUI_Delay(10); /* GUI_Exec() GUI_X_Delay(10) */ } } void App_TasksCreate(void) { xTaskCreate(vGuiTask, GUI, GUI_TASK_STACK, NULL, GUI_TASK_PRIO, NULL); xTaskCreate(vLedTask, LED, 128, NULL, LED_TASK_PRIO, NULL); }GUI_TASK_STACK给 768 个 word约 3KB。看起来夸张但WM_Exec的重绘路径是递归的窗口嵌套一层就多一份局部变量加上GUI_DispString之类的格式化调用2KB 很容易踩线。我吃过一次亏栈溢出检查开了但没配钩子系统跑二十分钟就 HardFault反汇编看栈指针已经跑到堆区去了。优先级不能给得太高。GUI 任务设成最高优先级之后只要它一直在刷屏低优先级的串口解析、ADC 采集全被饿着表现为“界面很流畅但数据不更新”。放在中间偏上比业务任务高、比硬实时任务低比较合适GUI_Delay(10)在这个位置既能保证刷新率又能把 CPU 让出去。GUI_Delay和GUI_Exec的区别要说清GUI_Exec()只处理当前积累的无效区域处理完立刻返回GUI_Delay(ms)是“执行 延时”的循环体它在延时的这段时间里是释放锁的所以别的任务可以安全地调WM_InvalidateWindow。这也是为什么 GUI 任务的主循环通常就一行GUI_Delay不需要自己拼GUI_ExecvTaskDelay。3. GUIBuilder 出活两个按钮从拖拽到回调只改三处代码GUIBuilder 的价值不在于省代码而在于它生成的_aDialogCreate[]结构是标准写法位置、尺寸、ID 一目了然后面要改布局直接改数组比在代码里翻BUTTON_CreateEx快得多。但它生成的代码有大约三处需要按项目改改完才算能用。3.1 画界面属性怎么填生成的代码才不会返工打开 GUIBuilderFile → New先把窗口尺寸设成 240×320和屏幕一致。这一步别偷懒用默认的 320×240否则跑起来第一眼就是界面错位还得回头查是分辨率问题还是驱动问题。然后从工具箱拖两个 BUTTON 上去位置随意比如 (20, 60) 和 (20, 140)尺寸都给 200×50。接下来是重点在属性栏里改三个字段。属性建议值原因TextLED1/LED2生成的初始化代码里会带上省得后面BUTTON_SetTextId0x300/0x301不要用GUI_ID_OK、GUI_ID_CANCEL这些保留值避免和系统对话框冲突FontGUI_Font8x16200 宽的按钮8x16 字体一行放得下 25 个字符中文要另外加字库ID 这一栏是关键。GUIBuilder 里填的数字会原样出现在生成的_aDialogCreate[]里所以我在自己的头文件里同步定义/* app_ids.h —— 和 GUIBuilder 里填的 Id 一一对应改一处就行 */ #ifndef APP_IDS_H #define APP_IDS_H #define ID_BUTTON_0 0x300 #define ID_BUTTON_1 0x301 #endif保存成WindowDLG.cGUIBuilder 会生成CreateWindow()和_cbDialog()两个函数。这里有个小坑某些版本的 GUIBuilder 会额外生成一个带main()的示例文件加到工程里会跟你的启动文件冲突链接时报_main重复定义。只把对话框那个.c加进来就行。3.2 生成代码的结构读懂_aDialogCreate与WM_NOTIFY_PARENT生成出来的核心是这么一个数组我按实际项目整理了一下/* GUIBuilder 输出略作整理 */ static const GUI_WIDGET_CREATE_INFO _aDialogCreate[] { { WINDOW_CreateIndirect, Main, 0, 0, 0, 240, 320, 0, 0x0, 0 }, { BUTTON_CreateIndirect, LED1, ID_BUTTON_0, 20, 60, 200, 50, 0, 0x0, 0 }, { BUTTON_CreateIndirect, LED2, ID_BUTTON_1, 20, 140, 200, 50, 0, 0x0, 0 }, };字段顺序是创建函数、文字、ID、X、Y、宽、高、标志、扩展指针、额外参数。要挪按钮位置改第 4、5 两个数要改大小改第 6、7 两个数。比在代码里找WM_GetDialogItem拿句柄再WM_SetWindowPos直观得多。回调函数_cbDialog收到的WM_MESSAGE *pMsg里几个字段必须分清pMsg-MsgId是消息类型pMsg-hWin是本窗口句柄pMsg-hWinSrc是消息来源窗口对按钮事件来说就是那个按钮pMsg-Data.v是通知码。刚上手最容易犯的错是把hWin当成按钮句柄用结果WM_GetId拿到的是窗口 IDswitch永远进不去。3.3 回调里只做一件事把按钮事件翻译成 LED 动作改动的第一处是初始化分支第二处是通知分支第三处是删掉 GUIBuilder 默认生成的GUI_ID_OK处理。static void _cbDialog(WM_MESSAGE *pMsg) { int Id, NCode; switch (pMsg-MsgId) { case WM_INIT_DIALOG: /* 只在初始化时设一次属性别放进 WM_PAINT */ WINDOW_SetBkColor(pMsg-hWin, GUI_MAKE_COLOR(0x00303030)); break; case WM_NOTIFY_PARENT: Id WM_GetId(pMsg-hWinSrc); /* 用 hWinSrc不是 hWin */ NCode pMsg-Data.v; if (NCode WM_NOTIFICATION_RELEASED) { switch (Id) { case ID_BUTTON_0: LED_Toggle(0); break; case ID_BUTTON_1: LED_Toggle(1); break; default: break; } } break; default: WM_DefaultProc(pMsg); break; } }为什么只认WM_NOTIFICATION_RELEASED而不是WM_NOTIFICATION_CLICKEDCLICKED在按下那一刻就产生如果手指按住不放或者轻微滑动通知会重复进来LED 就会连续翻转看起来像“按一次翻好几次”。RELEASED是抬起时产生一次交互只来一次。代价是响应有一点延迟但对按钮控制 LED 这种场景完全无所谓。LED_Toggle写成按索引分发的形式比在回调里直接写HAL_GPIO_TogglePin好维护/* 板载两颗 LED绿色 PG13、红色 PG14 */ void LED_Toggle(uint8_t idx) { switch (idx) { case 0: HAL_GPIO_TogglePin(GPIOG, GPIO_PIN_13); break; case 1: HAL_GPIO_TogglePin(GPIOG, GPIO_PIN_14); break; default: break; } }这个函数在 GUI 任务的上下文里执行HAL_GPIO_TogglePin就是几次寄存器写纳秒级返回直接调没问题。但如果按钮背后是写 EEPROM、发串口命令、等传感器应答这类会阻塞的动作就不能这么写了——那会把 GUI 任务卡住界面直接失去响应。这种场景的处理方式放在第 6 章。3.4 窗口创建时机别在 GUI_Init 之前碰任何 WM_ 函数本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑