简介:这是一份基于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控制器的寄存器,而不是直接驱动像素矩阵。这意味着我们不能照搬网上那些F429+LTDC+STemWin的教程,驱动层必须走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”文件夹,里面放三个子目录:
- Source:FreeRTOS核心源码,包括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是15,configMAX_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启动后接管SysTick,HAL_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内存的约束下点亮LCD
STemWin移植的重点不在于把库文件加进来,而在于两件事:一是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格式)就需要150KB,192KB的芯片根本不够用。但实际情况是,屏上的LCD控制器自带约150KB的GRAM,STemWin只是通过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、高320,FSMC存储区的映射基地址是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和文本:
- 按钮0:ID设为ID_BUTTON_LED1,文本设为“LED1_ON”
- 按钮1:ID设为ID_BUTTON_LED2,文本设为“LED2_ON”
背景色、字体这些属性依个人喜好调整,默认白底深色字即可。
在布局上,我习惯把两个按钮左右排列,尺寸设成80x50,间距30像素,视觉上比较协调。GUI Builder允许直接拖拽调整位置,最终会在代码里生成精确坐标。
4.2 生成代码的文件结构与作用
点击File->Generate Code,GUI 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_GetTime,GUI_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库的时基迁移到TIM6,FreeRTOS独占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都只需要改一个地方。这套项目跑起来之后,你还能继续扩展,比如加触摸输入、加显示波形、加图表控件。底层移植通路已经打通,剩下的事情就看你的想象力了。
本文还有配套的精品资源,点击获取