ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

STM32F429移植STemWin+FreeRTOS:用GUI Builder实现LED图形控制

STM32F429移植STemWin+FreeRTOS:用GUI Builder实现LED图形控制 简介这是一份基于STM32F429 DISC1开发板的STemwin与FreeRTOS整合移植工程面向具备一定STM32基础、希望进阶嵌入式图形界面与实时系统的开发者。资源以GUIbuilder生成的两个按钮控制LED亮灭为示例直观演示了从界面事件到硬件操作的完整调用流程便于理解事件驱动编程思想。压缩包共530个文件大小18.26MB以h与c源码文件为主还有o、d、crf等编译中间产物以及工程配置文件适合直接打开工程进行编译、烧录与二次开发也可作为模板复用。工程覆盖了STemwin图形库移植、FreeRTOS任务调度、LCD与触摸驱动配置等关键步骤并针对竖屏显示做了布局适配。目前已有960人学习适合对照完整源码梳理移植细节、学习GUI与RTOS协同设计的开发者。 一块ST官方Discovery板不接任何外设只靠板载TFT屏和两颗LED能不能变成一个带图形操作界面的RTOS小型系统这个选题听起来不算特别大但把STemWin、FreeRTOS和GUI Builder这三样凑到一起的时候问题就来了三大模块各自的资料都很多唯独缺少一条把它们串起来、可以直接落地的完整链路。这也就是这篇文章要解决的核心问题。我在STM32F429-DISC1开发板上完整走了一遍移植流程把STemWin图形库跑起来用GUI Builder拖出两个虚拟按键点击后分别控制板上的LED1和LED2。整个过程包含环境搭建、FreeRTOS移植、STemWin内存规划、GUI Builder代码集成这几个关键环节。读完这篇文章你可以直接照搬这套方案到自己的项目里也可以以此为模板把界面换成自己需要的控件做更多图形交互。1. 这块开发板上的硬件资源决定了移植思路很多人拿到F429-DISC1就直接去看STemWin的Demo结果发现官方工程的复杂度远比自己想象中的高原因在于没有先摸清板子特性。我不建议直接打开官方例程就开干而是先花10分钟理清硬件资源这样后面每一步都有据可循。1.1 板载外设与引脚分配STM32F429-DISC1板载的核心是STM32F429ZIT6主频最高180MHz内置192KB SRAM和2MB Flash。这个内存规模在Cortex-M4系列里算是中等偏上但和很多带SDRAM的开发板不同这块板子板上没有外部RAM全部内存就是这192KB所以GUI内存规划必须精打细算。板载硬件方面最有用的几个资源是2.4寸TFT-LCD屏分辨率240x320接口是FSMC并行接口不是SPI也不是LTDC直连RGB。这一点很重要直接决定了STemWin底层驱动的写法。两颗用户LED红色LED在PG14绿色LED在PG13低电平点亮。一个用户按键B1在PA0外部中断方式。板载ST-Link调试器通过USB线连接电脑后既能下载也能串口通信调试很方便。我特别提一下FSMC接口这个点。因为很多F429用户默认会去配置LTDC控制器来驱动RGB屏幕但在DISC1这块板上LCD控制器本身是在屏幕模组里面的MCU只是用FSMC并行总线的形式去“访问”LCD控制器的寄存器而不是直接驱动像素矩阵。这意味着我们不能照搬网上那些F429LTDCSTemWin的教程驱动层必须走FSMC这条路线。1.2 软件架构的任务分配整个系统的软件架构可以拆成三条线第一条是FreeRTOS负责任务调度、时间片管理、信号量同步。GUI相关的操作必须在独立任务中执行不能阻塞其他业务任务。第二条是STemWin负责图形界面绘制、控件消息响应。它本身只是一个库需要底层有一个操作系统对接层比如延时函数、任务锁等这两者之间需要做好适配。第三条是GUI Builder它属于设计工具用来在PC上布局界面生成C代码然后我们把生成的代码集成到工程里控件事件的回调里写LED控制逻辑。架构拆清楚之后移植的顺序也就确定了先移植FreeRTOS再移植STemWin最后用GUI Builder生成界面并完成事件绑定。如果反过来先搞GUI后面再塞RTOS就会出现很多优先级冲突和时钟源冲突的问题。2. FreeRTOS移植这一步的核心不是拷贝源码而是处理时钟与中断FreeRTOS在Cortex-M4平台上的移植已经非常成熟网上一搜一大把教程但百分之八九十的教程都只讲了文件夹怎么加、文件怎么包含真正容易翻车的地方其实在中断和时钟部分。2.1 源码文件的组织方式我的做法是在工程目录下新建一个“FreeRTOS”文件夹里面放三个子目录SourceFreeRTOS核心源码包括tasks.c、queue.c、list.c、timers.c、event_groups.c、croutine.c。如果不用流缓冲只用到任务和消息队列的话tasks.c、queue.c、list.c是必需的timers.c看项目需求。portable再往下一层需要把正点原子或者ST官方例程中适配好Cortex-M4的port.c、portmacro.h拷贝进来路径大概是portable/RVDS/ARM_CM4F。注意带F后缀的这个版本支持FPU寄存器保存F429是有FPU的不能用不带F的版本。include放FreeRTOSConfig.h头文件。Keil工程里把源码和头文件路径整理好预处理宏那里无需额外设置。2.2 FreeRTOSConfig.h的关键参数FreeRTOSConfig.h是移植中的核心我截取几个我自己工程里实际在用的关键配置#define configUSE_PREEMPTION 1 #define configCPU_CLOCK_HZ (SystemCoreClock) #define configTICK_RATE_HZ ((TickType_t)1000) #define configMAX_PRIORITIES (5) #define configMINIMAL_STACK_SIZE ((unsigned short)128) #define configTOTAL_HEAP_SIZE ((size_t)(20 * 1024)) #define configKERNEL_INTERRUPT_PRIORITY (15) #define configMAX_SYSCALL_INTERRUPT_PRIORITY (5) #define configUSE_16_BIT_TICKS 0 #define configUSE_IDLE_HOOK 0 #define configUSE_TICK_HOOK 0 #define configCHECK_FOR_STACK_OVERFLOW 2 #define configUSE_MUTEXES 1 #define configUSE_RECURSIVE_MUTEXES 1 #define configUSE_COUNTING_SEMAPHORES 1 #define configENABLE_FPU 1这里有几个需要留意的细节。configTICK_RATE_HZ设置为1000意味着系统时钟节拍是1ms一次。这个值越高时基精度越高但CPU中断开销也越大。对于LED控制和GUI刷新这种场景1ms完全够用没必要用更高的节拍。configTOTAL_HEAP_SIZE分配了20KB。FreeRTOS的动态内存分配堆任务栈、消息队列、信号量等都会从这里分配。我的实际工程里创建了一个GUI任务、一个LED任务、一个按键检测任务加上各种队列20KB足够如果你后面要加很多任务这里需要适当调大。configCHECK_FOR_STACK_OVERFLOW设为2这个强烈建议打开尤其在线程任务中调用GUI时栈的使用量会比预期更多。设置为2意味着使用栈填充检测方法系统会定期检查任务栈的水位线一旦溢出会调用vApplicationStackOverflowHook回调我们可以在这个回调里设置一个断点来定位问题。优先级配置这里configKERNEL_INTERRUPT_PRIORITY是15configMAX_SYSCALL_INTERRUPT_PRIORITY是5。STM32F4的中断优先级是4位取值0到15数值越小优先级越高。FreeRTOS要求系统节拍中断处于最低优先级所以内核中断优先级必须设为15。而5这个值用于从中断安全API中调用的临界保护它和中断优先级分组设置有关实际使用时我们中断服务程序中调用FromISR结尾的API函数时该中断的抢占优先级必须数值上大于等于5否则容易出问题。2.3 SysTick的归属问题一个典型的移植深坑STM32F429-DISC1默认工程由HAL库驱动HAL库的时基默认是SysTick。FreeRTOS也需要一个时基默认也选SysTick。这两者之间如果不做处理就会出现一个尴尬局面FreeRTOS启动后接管SysTickHAL_Delay就不准了甚至直接卡死。解决办法有几种路径第一种把HAL库的时基切换到其他定时器。在HAL_InitTick函数中把uwTickFreq和SysTick换成TIM6或者TIM7这种基础定时器。修改方法是在stm32f4xx_hal.c中找到HAL_InitTick函数把里面的SysTick_Handler替换成TIM6_IRQHandler同时修改NVIC和定时器配置。这个方案最稳妥HAL_Delay和FreeRTOS互不干扰。第二种用HAL_InitTick的弱函数重写把它重定向到自己的定时器上。工程里保留几个HAL库文件弱函数重定义即可。第三种干脆不用HAL_Delay后续延时全部用FreeRTOS的vTaskDelay或者vTaskDelayUntil。这种方式对改动量最小但很多HAL库内部操作都会调用HAL_GetTick比如某些外设的超时判断所以建议配合第一种方案使用。我在实际工程中直接把HAL时基切换到了TIM6这样HAL库内部的超时判断正常FreeRTOS用SysTick做节拍互不干扰这是最省心的组合。2.4 验证移植是成功还是失败FreeRTOS跑起来之后不要急着往下走先验证一下调度器是否正常工作。最直接的方法是在main函数中创建两个任务一个翻转LED一个翻转另一个LED分别以不同频率运行比如一个200ms翻转一次一个500ms翻转一次。如果LED以预期频率交替闪烁说明任务调度、SysTick中断、上下文切换都正常。如果其中一个任务不运行大概率是栈分配或堆溢出打开硬件断点看vApplicationStackOverflowHook是否被触发。这一步验证的好处在于后续所有问题都可以在已知正常的RTOS基础上排查。3. STemWin移植在192KB内存的约束下点亮LCDSTemWin移植的重点不在于把库文件加进来而在于两件事一是FSMC接口底层驱动的正确配置二是GUI内存池的合理规划。这两件事任何一个做不好屏幕要么白屏要么乱码。3.1 库文件的选择原则STemWin库文件分为带OS版本和不带OS版本。命名规则一般是这样的STemWin_CM4_OS_Keil.lib适配Cortex-M4内部已经实现了信号量、互斥锁等OS相关操作需要在外部提供GUI_X_OS相关接口。STemWin_CM4_NoOS_Keil.lib不带OS功能适合裸机环境。因为在FreeRTOS工程里跑GUI我会选择带OS版本的库。这样STemWin内部的多任务保护机制才能正常工作当多个任务同时访问GUI API时库自身会用互斥锁保证临界区安全。如果选择NoOS版本多任务同时操作GUI时可能出现数据竞争导致界面撕裂或死机。还有一个容易踩的坑是编译器版本。STemWin官方库大多是基于ARMCC V5编译的如果你的Keil MDK选了AC6编译器ARM Compiler V6链接阶段很可能报出library相关错误。我的建议是如果手头没有确认支持AC6的STemWin库第一版移植先用AC5编译器跑通之后想升级再用AC6重新编译一次测试。别在这上面卡太久版本兼容性问题是纯浪费时间。3.2 内存池的精细规划F429-DISC1没有外部SDRAM所以GUI内存只能从内部192KB SRAM中分配。当初我担心的是GUI内存池分配太多FreeRTOS任务就没内存了分配太少界面复杂点就黑屏。实际跑通后发现这个组合的默契点是可以找出来的。先看STemWin自身的配置。// GUIConf.h #define GUI_NUMBYTES (30 * 1024) #define GUI_SUPPORT_MEMDEV 1 #define GUI_SUPPORT_AA 0 #define GUI_SUPPORT_BMP 1 #define GUI_SUPPORT_GIF 1 #define GUI_SUPPORT_PNG 1 #define GUI_SUPPORT_ARGB 0GUI_NUMBYTES设为30KB这是STemWin内部使用的动态内存池。两个按钮的窗口界面包括控件对象、资源表、消息栈等等30KB足够。如果要跑复杂动画或者加载大量位图这里需要加大但F429-DISC1上我建议控制在40KB以内留足FreeRTOS空间。再有一件事很关键就是显存。这个板子的LCD控制器自带GRAM。这一点很多人理解错了以为移植STemWin必须另外分配一块和屏幕分辨率对应的RAM作为显示缓冲区。如果真是那样240x320x2字节RGB565格式就需要150KB192KB的芯片根本不够用。但实际情况是屏上的LCD控制器自带约150KB的GRAMSTemWin只是通过FSMC接口直接把像素数据写到屏控制器的显存里所以MCU侧的RAM压力并不大。这带来的好处是GUI内存池不需要承担显存开销30KB的GUI_NUMBYTES是真正用于控件内存分配的。3.3 FSMC接口上电和LCD控制器初始化STemWin的底层驱动核心是LCDConf.c它要完成三件事初始化FSMC接口初始化LCD控制器告诉STemWin如何读像素、写像素FSMC接口的配置包括地址建立时间、数据建立时间、总线宽度16位、存储块1选择NE1片选。注意F4系列的总线设置需要跟LCD控制器的读写时序匹配不能照抄手册默认值。我的建议是先从ST官方例程或网上的成熟配置拷贝FSMC初始化代码然后再根据实际屏幕的数据手册微调时序。LCD控制器初始化部分比较繁琐不同批次DISC1板子上的LCD控制器型号可能不同比如有的批次是R61579有的是NT35510初始化命令序列有细微差别。我最初在这块儿花了整整一个下午后来发现ST官方例程里有一份写好的“LCD_InitSequence”可以直接用。不建议自己对着数据手册一行行翻译除非你手里的屏型正好没有现成驱动。STemWin通过配置LCD_X_Config来对接底层驱动void LCD_X_Config(void) { GUI_DEVICE_CreateAndLink(GUIDRV_FLEXCOLOR, GUICC_565, 0, 0); LCD_SetSizeEx(0, 240, 320); LCD_SetVSizeEx(0, 240, 320); GUIDRV_FLEXCOLOR_SetBaseAddr(0, 0x60000000); }这段代码的含义是创建一个16位色彩深度的设备宽240、高320FSMC存储区的映射基地址是0x60000000。注意GUIDRV_FLEXCOLOR只是驱动骨架真正读写像素函数需要用GUIDRV_FLEXCOLOR_SetFunc指定但这个函数在不同版本的STemWin中接口差异比较大如果编译报错优先检查函数名是否匹配当前库版本。3.4 GUI与FreeRTOS的时间基准对接STemWin需要两个基本服务延时函数和当前时刻获取函数。这两者都来自GUI_X.c文件。int GUI_X_GetTime(void) { return (int)uwTick; } void GUI_X_Delay(int ms) { vTaskDelay(ms); }为什么用uwTick而不是直接用FreeRTOS的OSGetTickCount因为uwTick是HAL库维护的系统时基单位1ms只要HAL_IncTick函数被周期调用它就一直准。而vTaskDelay是FreeRTOS的阻塞延时把CPU让给其他任务。这里要注意STemWin库内部的很多等待操作会通过GUI_X_GetTime检查超时如果GetTime不走界面就可能卡死在等待循环里。用它对接之后整条时间链路就通了。4. GUI Builder生成界面拖两个按钮生成代码改一个回调GUI Builder是这套开发流程里最让人上手的环节。它的使用逻辑很像VB或者Qt Designer拖控件、改属性、生成代码。但如果你只是会用鼠标拖控件还远远不够生成的代码怎么嵌入到FreeRTOS任务里控件事件怎么绑定自己的业务逻辑才是真正的难点。4.1 图形界面的布局与属性设置打开GUI Builder新建一个Window窗口然后从工具箱里拖两个Button上去。按钮属性的设置最关键是ID和文本按钮0ID设为ID_BUTTON_LED1文本设为“LED1_ON”按钮1ID设为ID_BUTTON_LED2文本设为“LED2_ON”背景色、字体这些属性依个人喜好调整默认白底深色字即可。在布局上我习惯把两个按钮左右排列尺寸设成80x50间距30像素视觉上比较协调。GUI Builder允许直接拖拽调整位置最终会在代码里生成精确坐标。4.2 生成代码的文件结构与作用点击File-Generate CodeGUI Builder会生成三个文件Framewin.c包含Framewin窗口的回调函数、控件创建函数CreateFramewin。Framewin.h头文件声明CreateFramewin函数和控件ID宏。main.c包含MainTask函数的入口Demo这个文件一般不用因为最终要集成到RTOS任务中。把Framewin.c和Framewin.h加入工程后调用CreateFramewin函数就会创建整个窗口和两个按钮。4.3 回调函数中处理按钮点击事件GUI Builder自动生成的回调函数里有一段WM_NOTIFY_PARENT消息处理框架这个框架默认是空的。全部的工作就是填充这个分支。void _cbCallback(WM_MESSAGE *pMsg) { int NCode; int Id; switch (pMsg-MsgId) { case WM_NOTIFY_PARENT: Id WM_GetId(pMsg-hWinSrc); NCode pMsg-Data.v; if (NCode WM_NOTIFICATION_CLICKED) { if (Id ID_BUTTON_LED1) { HAL_GPIO_WritePin(GPIOG, GPIO_PIN_13, GPIO_PIN_RESET); } else if (Id ID_BUTTON_LED2) { HAL_GPIO_WritePin(GPIOG, GPIO_PIN_14, GPIO_PIN_RESET); } } break; default: WM_DefaultProc(pMsg); break; } }WM_NOTIFY_PARENT的机制是当按钮被按下并释放时按钮控件会向它的父窗口发送一条通知消息。父窗口回调里通过WM_GetId拿到发送消息的子控件ID再从pMsg-Data.v中取出通知码WM_NOTIFICATION_CLICKED表示已经完成一次完整点击。低电平点亮LED所以写GPIO_PIN_RESET是点亮。如果你想要更自然的交互体验可以把按钮文本改成“LED1_Toggle”回调中读取当前引脚电平再取反GPIO_PIN_SET改成GPIO_PIN_RESET反之亦然这样每次点击都能翻转一次LED状态。4.4 芯片引脚初始化记得补齐GUI Builder只负责生成界面代码芯片外设初始化必须自己写。GPIO的配置也属于一个容易漏掉的环节。F429-DISC1的LED引脚PG13、PG14配置为输出模式推挽输出速度可以设为低速或中速LED点亮速度并不需要高频翻转。这一段初始化代码放在main函数里在RTOS启动之前调用void LED_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; __HAL_RCC_GPIOG_CLK_ENABLE(); GPIO_InitStruct.Pin GPIO_PIN_13 | GPIO_PIN_14; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOG, GPIO_InitStruct); HAL_GPIO_WritePin(GPIOG, GPIO_PIN_13 | GPIO_PIN_14, GPIO_PIN_SET); }初始状态设为高电平也就是LED熄灭。5. 整合调试从main函数到按钮响应完整走通一遍当所有模块都独立验证通过后最后一步就是组装。组装顺序错了也会出问题比如在主任务里直接调用GUI_Init但RTOS还没启动就可能因为GUI_Init内部等待时间基准导致死等。5.1 main函数的执行顺序我的main函数执行顺序如下HAL初始化系统时钟配置FSMC和LCD控制器初始化GPIO初始化LED引脚创建任务启动调度器vTaskStartScheduler其中第6步vTaskStartScheduler不会返回直到系统关机。所以不要写第7步了写了也执行不到。重要的是STemWin的GUI_Init不能在启动调度器之前调用因为GUI_Init内部会调用GUI_X_Delay或者GUI_X_GetTimeGUI_X_Delay调用vTaskDelay而vTaskDelay必须在调度器启动之后才能生效。把GUI_Init放在任务函数里面就规避了这个问题。void StartGUITask(void *argument) { GUI_Init(); CreateFramewin(); while (1) { GUI_Exec(); vTaskDelay(10); } } void StartLedTask(void *argument) { while (1) { /* 其他业务逻辑比如通过消息队列接收LED控制命令 */ vTaskDelay(100); } }这里GUI_Exec用来处理窗口消息循环和重绘操作必须周期性调用。10ms的延时确保CPU有空余去执行其他任务。如果界面有动画需求可以把延时减到5ms如果对实时性要求不高20ms也够用LCD刷新过程肉眼无差。5.2 任务栈大小的动态调优任务栈大小是一个经常让新手头疼的配置。GUI任务由于涉及STemWin内部递归调用和消息处理栈需求比较大。我自己测试时GUI任务栈从512字2KB起步跑复杂界面出现过栈溢出后来调整到1024字4KB稳定运行。LED任务栈256字1KB就够了。栈溢出的检测方式就是FreeRTOS里configCHECK_FOR_STACK_OVERFLOW配合vApplicationStackOverflowHook回调。溢出触发时停在断点上此时把调用栈拉出来看基本都能定位到是哪个函数吃栈再针对性调整任务栈。这个方法比猜快得多。5.3 点击按钮没反应的排查路径如果你已经烧录进去但点击屏幕上的按钮没有任何反应按以下顺序排查第一步确认GUI任务是否在正常运行。在GUI_Exec前后各翻转一次某个空闲GPIO用示波器观察有没有方波。如果没有方波说明任务没跑起来。第二步确认按钮的回调函数是否收到WM_NOTIFY_PARENT。在回调入口处加断点点击屏幕看有没有进来。没进来的话检查ID是否匹配以及是不是用了WM_NOTIFICATION_CLICKED还是WM_NOTIFICATION_RELEASED。第三步确认GPIO配置正确。用万用表测PG13和PG14的电平。按钮按下后如果回调执行了但LED不亮多半是GPIO时钟没开或者引脚初始化放在任务中被覆盖了。第四步确认FSMC时序是否正常导致画面显示有问题。如果按钮已经画出来了一般FSMC没问题如果画面有花屏检查地址建立时间和数据建立时间的配置。这套排查链路我推荐新手直接照抄按照从上到下的顺序检查不遗漏任何一个环节。6. 移植过程中的几个坑以及对应的处置方式整个流程跑通之后回看过程有四个问题是比较耗费时间的记录在这里给大家做参考。6.1 HAL库时基和FreeRTOS的SysTick冲突这个问题已经在前面重点提过。我的建议非常明确把HAL库的时基迁移到TIM6FreeRTOS独占SysTick。不要试图两个共用SysTick网上有些技巧能实现但交互过于复杂稳定性和可读性都差。6.2 STemWin的库文件与ARMCC编译器版本不匹配STemWin官方提供的静态库很多是基于ARMCC V5生成的。在Keil工程里默认使用AC6编译器时链接阶段常见错误是“Library ... uses personality”或者直接报未定义符号。解决方式第一选择是换成AC5编译器第二选择是寻找对应AC6版本的库文件。别纠结先跑通再说。6.3 GUI Builder生成代码中的字体问题GUI Builder默认使用的字体可能是英文等宽字体中文显示会变成空白或乱码。如果你需要在按钮上显示中文必须先在STemWin工程中添加中文字体支持比如使用XBF字体或者SIF字体在GUI Builder中也要选择对应字体。最省事的方案是按钮文本先用英文中文字体单独再做一轮适配。6.4 FreeRTOS堆空间不足导致GUI_Init后死机GUI_Init内部会申请大量动态内存如果FreeRTOS的堆空间不够系统可能直接在GUI_Init内部触发断言。表现是程序卡死连错误信息都没有。这时候不要急着调大GUI_NUMBYTES而是先看看FreeRTOS的堆剩余情况。我实测下来FreeRTOS堆分配20KB、GUI内存池分配30KB在F429-DISC1上跑两个按钮的界面绰绰有余。最后分享一个实用习惯把GUI_Exec的延时时间改成可配置的宏这样在调试阶段可以快速调整CPU占用率观察有没有影响到其他任务的实时性。比如宏定义GUI_TASK_DELAY_MS为10改小到5或改大到20都只需要改一个地方。这套项目跑起来之后你还能继续扩展比如加触摸输入、加显示波形、加图表控件。底层移植通路已经打通剩下的事情就看你的想象力了。本文还有配套的精品资源点击获取
返回列表