STM32驱动ST7789:基于状态机的物理按键3级菜单设计与实现
1. 项目全貌与方案选型为什么是ST7789加物理按键1.1 从需求倒推硬件选型这个项目源于一个实际需求一块屏幕、三个按键、一套能翻页能设置参数的交互界面。主控用STM32屏幕选了ST7789驱动方案的1.8寸/2.4寸TFT屏交互输入没走触摸而是用物理按键控制。很多人觉得有触摸屏谁还用按键但在工业设备、仪器仪表、桌面小工具这类场景下物理按键的可靠性、盲操作的确定性、成本优势触摸屏比不了。所以物理按键加3级菜单的组合在嵌领域式里非常常见。ST7789这颗驱动IC本质上是一个LCD控制器最大支持240x320分辨率内置GRAM通过SPI接口和STM32通信。它最大优点是性价比高市面上一块240x320的1.8寸屏模组几块钱就能拿到驱动时序不复杂网上的参考代码也一堆。但不是说有代码就能CtrlC CtrlV就跑通实际调起来坑也不少稍后我会专门讲。这个项目适合谁参考刚把STM32基础外设GPIO、SPI、定时器学完想做点有完整交互逻辑的东西的同学或者工作中需要给设备加一个简单的显示设置界面、但没有多余精力去研究复杂GUI库的工程师。裸机跑一个3级菜单状态机加上消息机制就够了。1.2 按键方案对比GPIO扫描、ADC按键、矩阵键盘按键输入看起来简单但设计方案直接关系到硬件成本和代码复杂度我先把常见方案摆出来对比一下。独立GPIO按键一面一个IO直接接高电平或低电平按下时IO翻转。优点是电路简单按键少3个以内时首选。ADC按键电阻分压多个按键通过不同电阻分压共用一个ADC通道通过采样电压判断哪个按键被按下。省IO但波形有个建立时间消抖逻辑要配合ADC采样率按键多了会有阈值重叠风险。矩阵键盘行列交叉扫描适合按键数量多如12键、16键的场景。扫描逻辑需要定时器轮询程序上麻烦些。这个项目只有上/下/确认三个按键我选了独立GPIO方案配置一个外部中断加定时器扫描的混合方式。按键接法上我习惯把按键一端接GND另一端接GPIO内部上拉按下输出低电平。这样按键自然态是高按下是低逻辑上不容易误判。上拉电阻注意打开MCU内部上拉外部不上拉也能用但如果排线长、环境干扰大还是建议外部加一个4.7k到10k的上拉电阻到VCC。1.3 菜单框架选择裸机状态机 vs RTOS vs 图形库菜单架构是一个提前要想清楚的问题。有人项目里原本就有FreeRTOS菜单交互可以做成独立任务有人项目功能简单裸机主循环加状态机就够也有人想直接用LVGL这种图形库。裸机状态机轻量无系统开销适合菜单结构固定、页面数量少的小项目。代码直观出问题好排查。缺点是要自己管理所有页面状态和事件。RTOS消息队列适合菜单多、功能复杂的项目。按键事件挂进队列UI任务接收消息再刷新页面。缺点是工程结构重对资源有限的芯片不友好。LVGL/嵌入式GUI库渲染效果好支持动画、控件但与ST7789的底层对接要额外移植占用的Flash和RAM明显更高。如果只是做个3级设置菜单LVGL其实是杀鸡用牛刀。我这边的取舍是裸机状态机加一个轻量消息模型。3级菜单的页面数量撑死几十个状态机完全管理得过来而且没有调度器不需要考虑优先级反转、任务堆栈这些问题调试简单太多。后面代码结构也是按这个思路写的。2. ST7789屏的驱动与底层渲染2.1 ST7789初始化时序与关键寄存器很多人一上来就复制初始化序列但寄存器是干什么的完全不知道。出问题时抓瞎。这里我刻意梳理一下ST7789初始化的几个关键点。ST7789使用SPI模式初始化流程大致是硬复位RES引脚拉低至少10ms再拉高等待120ms以上时序手册上写最少5ms我习惯等久一点。软件复位发SWRESET命令0x01等待150ms。开睡发SLPOUT0x11退出睡眠模式等待120ms。设置颜色格式COLMOD0x3A设为0x55即16位色RGB565。屏幕方向设置MADCTL0x36控制扫描方向和RGB顺序。显示开DISPON0x29。第5步的MADCTL要根据你板子上的屏幕接线放哪个方向来设定。数值含义分三段行地址顺序、列地址顺序、行列交换。我用一个宏定义来抽象这样换屏幕方向时只改这一处就行。ST7789不是所有模组都一样某些国产屏比如淘宝几十块的裸屏还需要加上PORCTRL、GCTRL等寄存器设置才能不偏色我通常在初始化序列里直接烧进厂家给的参考值实测颜色正常后就不再动。还有个特别容易忽略的事ST7789支持16位的RGB565但SPI是一种字节流协议所以发送一个像素要拆成两个字节高8位在前低8位在后。我在代码里写了两个核心函数LCD_WR_DATA8()发单字节LCD_WR_DATA16()发双字节并做了大小端处理。几乎所有绘制逻辑最后都会落到这两个函数上。static void LCD_WR_DATA8(uint8_t dat) { LCD_CS_LOW(); // 片选拉低 SPI1_SendData(dat); // 发送一个字节 while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_BSY) SET); LCD_CS_HIGH(); // 片选拉高 HAL_Delay(0); } static void LCD_WR_DATA16(uint16_t dat) { LCD_WR_DATA8(dat 8); // 高字节先发 LCD_WR_DATA8(dat 0xFF); // 低字节后发 }2.2 SPI传输优化DMA加硬件片选很多人初版代码是轮询发送一帧满屏刷新要发送240x320x2153600字节的数据如果SPI时钟压到18MHz传输这帧画面也要几十毫秒CPU一直在等SPI发送完成期间什么都干不了。在纯显示界面还能接受但如果同时要做按键检测、传感器采集这种卡死你很难受。建议把SPI发送改成DMA方式。配置SPI发送DMA通道每次要刷屏时CPU把数据地址和长度交给DMA发送完成触发中断CPU继续去干别的。对ST7789这种不带缓存接口的屏来说DMA传输是性价比最好的优化手段。还有一个细节片选信号的处理。如果片选一直拉低不释放SPI总线就被屏占着以后想接个SPI Flash或者别的外设就得打架。每次传完数据把CS拉高代价是多一点GPIO翻转时间换来的是总线共享的自由度。我实测过CS翻转时间大概百纳秒级别在整帧刷屏时间面前可以忽略不计。2.3 点阵字库与中文显示的取舍3级菜单如果是纯英文图标还好办但很多人想显示中文菜单项比如参数设置、系统信息。这就涉及字库问题。常用方案有三类全字库芯片/串行Flash外挂一个W25Q64里面烧GB2312全字库取模函数动态读Flash。优点是字全适合中文菜单多的项目缺点是费一个Flash芯片还要管理文件系统或者地址映射。内部Flash字库用取模工具把需要的汉字提取出来生成数组放到代码里。缺点是每加一个汉字就要重新取模修改麻烦。动态取模PC端生成开发时在PC上生成字模表烧录到内部Flash适合菜单固定、中文数量几十个以内的场景。我这个项目的中文菜单项控制在二十几个汉字以内用的第三种方案把需要的汉字做成16x16点阵字库数组放在一个单独的font.c文件里运行时通过内码索引取模。绘制时每行扫描字节一个汉字的16行就是32字节数据逐行写进GRAM即可。这里有一个我踩过的坑如果你不做内码索引直接按UTF-8字符串显示中文会乱码。STM32裸机环境下编译器默认把字符串按本地编码处理GD32/STM32用的Keil默认是GB2312编码。用VS Code编辑的话保存编码一定要调成GB2312或者干脆用UTF-8写、程序里做UTF-8到GB2312的转换否则显示出来的汉字全是乱的。这个问题排查起来特别耗时但排查逻辑其实很简单先用串口把字符串对应字节打印出来看是不是两个字节一组再对照内码表验证。绘制16x16汉字时还要注意坐标对齐。汉字字模的显示位置要按16像素对齐否则汉字之间出现锯齿或错位。我做了一个LCD_ShowChinese()函数里面强制把x坐标对齐到8的倍数这样显示效果稳定很多。3. 物理按键的消抖与事件机制3.1 硬件消抖电路设计按键按下和释放时机械触点会弹跳波形上就是一串高高低低的抖动信号持续5ms到20ms不等。如果不去抖一次按键可能被识别成好几次菜单画面就会一次跳好几行。硬件层面的基本消抖电路是两个元件一个电阻串联按键一个电容并联在按键两端形成RC低通滤波。典型值电阻10k、电容100nF时间常数约1ms能把高频抖动滤掉大半。如果板子空间有限只放一个100nF电容在按键两端也有明显改善但不能完全消除。我的经验是硬件RC消抖配合软件状态机采样双保险。不要只依赖硬件也不要只依赖软件。硬件滤掉绝大多数毛刺软件负责在后端做最终确认。3.2 软件消抖算法从延时消抖到状态机消抖最常见的软件消抖是检测到IO变化后延时20ms再读一次确认电平稳定就认为是有效触发。代码最简单但问题也很明显延时期间整个系统阻塞主循环卡在那如果一边扫描按键一边刷屏你会发现菜单滚动一顿一顿的。更好的做法是用定时器定时5ms扫描一次按键维护按键状态的软件状态机。每次扫描时读取当前IO电平与上次状态对比连续几次都处于稳定状态才认为按键事件成立。这种思路不阻塞主流程而且还能顺便做长按、连击这些扩展功能。我写了一个简化版四个状态释放稳定、释放抖动、按下抖动、按下稳定。每5ms一个节拍状态转移条件就是按键电平是否与当前稳定状态一致。三个按键我都用这个模型扫描函数里记录两个事件按键按下事件KEY_PRESSED和按键释放事件KEY_RELEASED。菜单逻辑通常在按下事件里触发。KEY_PRESSED事件的定义我建议是从非按下状态稳定地转移到按下状态的那次扫描而不是当前电平是低电平时每次扫描都上报。这一点非常关键否则菜单会连跳。3.3 短按、长按、连击的事件分发做菜单时确认键短按进入子菜单返回键长按回到上级菜单这类交互非常顺手。纯按键扫描代码是做不到的必须在上面的状态机基础上加计时判断。具体做法当按键进入按下稳定状态时记录按下时刻的时间戳用tick计数。当按键释放时用当前时间减去按下时刻得到按压时长。超过1000ms判定为长按否则短按。连击则复杂一些要记录连续短按的间隔超过阈值就视为下一次独立短按。事件分发的架构上我维护了一个环形队列把短按、长按、连击等事件依次塞进去。菜单主循环每次循环从队列里取一个事件交给菜单状态机处理处理完再取下一个。这个模式的好处是按键检测和菜单逻辑之间是解耦的。按键检测的实时性由定时器保证菜单逻辑在一次循环里把事件处理完哪怕处理需要几毫秒也不会影响下一次按键采样。typedef struct { uint8_t key_id; uint8_t event_type; // KEY_EVENT_SHORT_PRESS / KEY_EVENT_LONG_PRESS / KEY_EVENT_REPEAT uint32_t timestamp; } KeyEvent_t; #define KEY_EVENT_QUEUE_SIZE 16 static KeyEvent_t keyEventQueue[KEY_EVENT_QUEUE_SIZE]; static uint8_t keyEventHead 0; static uint8_t keyEventTail 0;按键事件入队和出队都是O(1)操作队列满时丢事件并置一个溢出标志方便调试。这个队列我只开了16个槽位实际很少用超过5个因为菜单处理速度远快于按键产生速度。4. 3级菜单的状态机设计与实现4.1 菜单数据结构的定义菜单结构我推荐用表驱动方式而不是把每个页面用if-else硬写。表驱动的意思就是定义一种统一的结构体每种菜单项是这个结构体数组的元素用数组本身表示菜单树。先定义菜单项结构体typedef struct MenuItem { uint8_t id; // 菜单项唯一ID uint8_t parent_id; // 父菜单项ID0表示根菜单 uint8_t child_count; // 子菜单数量 const struct MenuItem *children; // 子菜单数组指针 void (*on_enter)(void); // 进入菜单时回调 void (*on_execute)(void); // 菜单项被确认执行时的回调 const char *label; // 菜单名称 } MenuItem_t;这种结构相当于一个静态链表只是用数组指针代替了指针连接。好处是菜单树的结构在编译期就确定不占用堆内存没有malloc/free的顾虑。修改菜单项时只要增删数组元素并维护ID关系逻辑代码基本不用动。树形结构在某些场合很有用但我这次用的不是树而是树平面数组混合。每级菜单都是一个独立的MenuItem_t数组父菜单里记录了子数组的指针和长度。这样菜单层级最多3级每级之间的跳转关系清晰代码也直观。用树形链表的话要递归遍历对裸机小项目来说代码不够直白。4.2 状态机转移如何理清菜单树3级菜单的典型状态转移主菜单显示时按下键/上键切换当前高亮项。主菜单某一项被选中且它存在子菜单时按确认键进入二级菜单。二级菜单内按返回键回到主菜单。二级菜单某一项是具体设置项时按确认键进入编辑状态此时按键的功能变成调节值。编辑状态按确认键保存按返回键取消退出。为了实现这套逻辑我定义了一个枚举类型保存当前菜单状态typedef enum { MENU_STATE_MAIN, // 一级菜单浏览 MENU_STATE_SUB, // 二级菜单浏览 MENU_STATE_DETAIL, // 三级菜单浏览 MENU_STATE_EDIT, // 参数编辑 } MenuState_t;同时用一个MenuContext_t结构体记录当前上下文包括当前状态、当前所在的菜单数组、当前选中的索引、当前编辑的值等。菜单循环的函数长这样void Menu_ProcessEvent(KeyEvent_t *evt) { switch (menuCtx.state) { case MENU_STATE_MAIN: Menu_ProcessMainState(evt); break; case MENU_STATE_SUB: Menu_ProcessSubState(evt); break; case MENU_STATE_DETAIL: Menu_ProcessDetailState(evt); break; case MENU_STATE_EDIT: Menu_ProcessEditState(evt); break; default: break; } }每个状态的处理函数里根据事件类型和当前高亮索引做对应的动作。比如MENU_STATE_MAIN里KEY_DOWN就把currentIndex加1再取模然后调Menu_Render()刷新界面。KEY_ENTER则根据currentIndex找到对应菜单项如果它有子菜单就切换到MENU_STATE_SUB并把currentIndex清零。状态机的关键原则是状态转移只发生在明确的条件下并且每次转移都要更新界面不要让界面和状态不同步。我早期吃过亏状态切了但界面没刷用户看到的是上一页菜单按键按了没反应定位半天才发现是状态变量和渲染函数不在同一个函数里调用导致的。4.3 菜单项事件处理与回调函数设计菜单项被确认的时候很多时候不只是跳转还要做一些业务逻辑比如读取一个ADC值并显示或者保存一个参数到Flash。如果把业务逻辑直接写进菜单状态机里状态机会越写越肥最后变成一个大杂烩。我的做法是用函数指针回调。菜单结构体里的on_enter和on_execute就是干这个的。以亮度设置为例进入这个菜单项时on_enter里把当前亮度值读到编辑缓冲区按确认键进入编辑状态时on_execute里保存亮度到EEPROM/Flash。菜单框架本身不知道具体业务是什么只负责调度回调。这样一来新增一个菜单项只需要在数组里加一条记录、写两个回调函数耦合度很低。回调里要注意一件事回调函数执行时间不能太长否则主循环会被阻塞按键队列可能溢出。如果有耗时操作比如Flash写操作要几毫秒尽量拆成开始写入、轮询状态、完成三个小回调或者加一个延时状态机。不要图省事直接HAL_Delay。5. 菜单渲染逻辑怎么画才不乱5.1 渲染与逻辑分离渲染和逻辑分离这个设计理念我强烈建议从一开始就贯彻。具体来说就是状态机只负责这一刻菜单应该处于什么状态渲染函数只负责把当前状态的界面画出来。两者通过菜单上下文字段交流不要互相调用对方的内部细节。因为这两个职责混在一起会出现一种很恶心的bug比如界面刷新到一半按键事件进来直接把状态改了然后界面画出来的内容使用的是新状态但画到一半的残留画面还在显示就花了。尤其是用帧缓冲framebuffer方案时如果逻辑和渲染都操作同一个缓冲区问题更明显。我的渲染接口设计成void Menu_Render(void);这个函数内部先清屏或者只清需要更新的区域然后根据menuCtx.state、currentIndex、currentMenu等数据绘制标题栏、菜单项、高亮条。它不接收任何事件参数也不知道按键发生了什么。事件处理完菜单状态改变下一轮循环无条件调用Menu_Render界面永远追着状态跑。5.2 局部刷新 vs 全屏刷新屏幕不小240x320如果每个按键事件都做全屏清屏再重画闪烁感非常明显而且刷屏耗时大概几百毫秒体验很糟糕。解决办法是评估哪些区域需要更新。菜单界面大体分四块顶部标题栏、中间菜单列表区、底部提示栏、选中高亮条。按键触发菜单项滚动时变化的其实只有菜单列表区和顶部标题栏其他区域数据不变。所以只要把变化区域重绘就行。最常用的局部刷新策略是维护一个脏区域标记。每次按键事件处理后根据事件类型设置哪些区域需要重画。比如KEY_DOWN时只需重画菜单列表区进入子菜单时整屏区域都要重画。渲染函数检查脏区域标记只重画被标记的区域。这个机制代码量不大但对刷新速度的提升非常明显。我实测过整屏清屏加重绘大概需要150ms到200ms局部刷新只要50ms左右肉眼差别非常明显。菜单响应手感好用户才不会觉得机器卡。5.3 滚动列表与选中高亮菜单项数量如果超过单屏显示能力需要滚动。滚动分两种逐项滚动和翻页滚动。逐项滚动时当前选中项始终保持在屏幕内滚动一行画一行翻页滚动是显示固定数量比如8项翻页时整块刷新。实现逐项滚动最常用的技巧是选中项居中策略。假设屏幕能完整显示6个菜单项当前选中第3项那列表区就显示从选中项-2开始的6条。每次上下移动时重新计算列表窗口的起点。这样用户能一直看到选中项在屏幕中间也方便观察上下文菜单项。高亮条直接用反色绘制背景填充高亮颜色文字用黑色。实现上每画一项时判断该项是否等于currentIndex是则先用填充函数把这一行的矩形填成高亮色再在矩形内绘制文字。高亮条的颜色我用的是黄色RGB565值0x07E0其实是绿色0xFFE0黄色0x001F蓝色注意填色和文字颜色要对比强。这里我踩过一个具体的坑高亮条绘制时如果先用填充函数填色再写文字文字的背景色如果不透明文字周围会有一个矩形块盖掉部分高亮色看起来是字周围有个边框。解决办法是绘制文字时用透明背景模式或者让文字函数支持只写前景色不改背景。5.4 页面切换动画效果ST7789刷新速度不差300ms以内能全屏画完。但页面切换如果直接硬切用户总觉得生硬。我给项目加了一个简易的水平滑动效果不是真动画就是分几步把旧页面往左挪、新页面从右进。每步只重画一列利用局部刷新机制。做法很简单把屏幕分成8列30px一列切换时从右到左逐列重绘新内容。虽然每列重绘都比全屏刷新快但连续8次重绘的总时间并不短关键是一帧之间用户看到的是内容在动视觉上的流畅度感知比直接刷一帧好很多。这个动画的代价是切换期间按键事件会被暂时屏蔽防止用户在动画过程中输入否则状态和画面容易打架。动画要不要加看项目定位。如果追求极简、低功耗直接硬切也没问题如果产品要给人有设计感的感受这个动效带来的体验提升很值。6. 实测中遇到的坑与排查实录6.1 花屏的几大元凶第一条最常见的花屏原因SPI时钟频率过高。ST7789的SPI时钟上限一般在60MHz左右但实际模组质量参差不齐有些国产屏到30MHz以上就开始丢位。现象是画面有雪花点、颜色错乱、画面仅仅部分区域正常。排查方法很简单把SPI波特率预分频调大比如从4分频改成8分频看看花屏是否消失。我调试时一般是先跑到最小分频再逐级往上加找到一个稳定点。第二条是电源问题。背光电流较大如果LDO选型余量不足或PCB走线太细背光开启瞬间电压会被拉低LCD驱动芯片逻辑混乱表现是屏幕闪、花屏、甚至白屏。建议背光供电和逻辑供电分开至少保证逻辑电源的纹波在100mV以内。第三条是初始化序列不完整。网上抄的初始化代码可能是给ILI9341用的直接套到ST7789上就会画面偏色、反色、甚至白屏。ST7789和ILI9341的命令集虽然类似但具体寄存器含义不同。我建议以原厂数据手册为准至少把COLMOD、MADCTL、PORCTRL这些关键寄存器确认一遍。6.2 按键双击或失灵最常见的硬件原因按键没有接上拉或者上拉电阻阻值过大。STM32内部上拉电阻大约在30k到50k抗干扰能力一般在电噪声大的环境下容易误触发。外部加10k上拉效果会好很多。软件层面如果不用状态机消抖只靠20ms延时消抖机械翘板寿命老化的按键会出现抖动时间变长的情况20ms不够会出现双击。解决方案就是前面说的状态机消抖把稳定判定次数从2次增加到3次即连续3次扫描都是同一电平才算稳定能有效覆盖大部分老化按键。还有一种隐蔽的失灵按键检测用了外部中断中断服务函数里又没有做消抖导致一次按下触发多次外部中断事件队列被灌满真正的事件被丢弃。这部分我强烈建议外部中断里只置一个标志位具体按键扫描放到定时器中断里做不要直接在外部中断里做业务逻辑。6.3 菜单状态错乱问题定位菜单状态错乱最典型的表现进入二级菜单后按返回回的不是主菜单而是三级菜单或者在编辑状态按确认没有保存就退出。排这类问题我有个习惯把每次状态切换打印出来。串口上输出当前状态、触发事件、目标状态格式类似[MENU] state:SUB event:KEY_DOWN - index:2。有了日志状态机到底在哪一步跳错了一目了然。我发现错乱的高频原因有两个一个是状态切换后的变量没有初始化。比如进入二级菜单后currentIndex没有清零还保留着主菜单的选择结果二级菜单的高亮条落在了一开始那个不可能的位置另一个是菜单数组越界后没有做保护访问了非法地址导致随机现象。解决这类问题的办法主要靠防御性编程每次访问菜单数组前先判断索引是否在有效范围状态切换统一走一个Menu_SetState()函数在该函数里做参数初始化。6.4 Flash和RAM容量焦虑STM32F103C8T664KB Flash20KB RAM是这类项目最常见的配置。3级菜单、ST7789驱动、字库、业务逻辑塞进去不是不行但要注意几点。首先不要用全字库全字库GB2312一个点阵就是200多KB直接超。自建小字库挑菜单里用到的几十个汉字最多几KB完全没压力。其次不要去malloc。裸机项目里malloc容易产生碎片而且一旦堆栈溢出特别难查。所有菜单数组、缓冲都定义为静态全局变量大小编译期就确定RAM占用心里有数。再者如果开了DMA搬运像素数据DMA描述符和缓冲区至少要占几十字节这个也要算进RAM预算。如果RAM紧张可以把帧缓冲去掉直接用边算边画的方式虽然刷新速度慢一点但省一块缓冲区。这就回到项目取舍要快还是要省写代码前先想清楚。我实际编译出来的固件优化等级开-O2Flash占用不到35KBRAM占用不到8KB。如果项目里再塞传感器算法、通信协议这套菜单框架的额外开销确实很可控。这也是我一直愿意用裸机状态机而不是LVGL的原因资源开销一目了然。经验补充这套框架还能怎么扩展坦白说3级菜单只是GUI的入门形态。真正做产品级界面时还会遇到图标菜单、参数曲线显示、多语言切换、实时时钟显示这些问题。用我的这套菜单框架扩展方向其实是现成的想要图标只需在MenuItem里加一个const uint8_t *icon字段渲染时先画图标再画文字。想要多语言可以改成key-value映射菜单项标签存ID而不是字符串渲染时查语言表。想要实时刷新页面可以加一个定时器回调周期性调用某个页面的on_refresh函数比如主界面显示温度曲线的场景。这套框架用函数指针和表驱动模块之间低耦合扩展新增的功能一般只涉及新菜单项定义和对应的回调函数不碰框架本体。从维护角度来说几个月后回头看代码也还能很快找回上下文。如果非要说一个痛点那就是裸机状态机在菜单层级继续加深的时候状态会膨胀。4级、5级菜单之后状态枚举越来越长状态转移条件也开始重复。到那个阶段可以考虑引入一个简单的页面栈数组模拟栈push/pop管理页面而不是继续靠状态枚举硬撑。这个页面栈思路本质上跟嵌入式GUI库的屏幕管理模型已经非常接近了但实现起来又比上RTOS轻量得多。有精力的读者可以考虑往这个方向优化自己的代码。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →