ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

STM32+FreeRTOS驱动12864液晶实现多级菜单系统设计

STM32+FreeRTOS驱动12864液晶实现多级菜单系统设计 简介本资源是一套基于STM32与FreeRTOS实现的12864液晶屏多级菜单系统完整工程面向嵌入式初学者及中级开发者解决在资源受限MCU上构建可交互、可扩展用户界面的核心难题。项目涵盖GPIO驱动12864 LCDSPI接口、按键扫描与翻页逻辑、菜单树结构设计及FreeRTOS多任务协同如GUI刷新、按键检测、状态管理适用于智能仪表、工业HMI、教学实验等场景。压缩包含168个文件以75个.h头文件定义菜单结构体、LCD寄存器映射、任务接口和69个.c源文件含stm32f10x系列外设驱动、FreeRTOS内核适配、gui_basic菜单渲染等为主体辅以readme说明、Keil工程配置uvprojx/uvoptx及hex固件整体大小607KB。已有2458人学习下载提供从底层驱动到应用层菜单逻辑的全链路代码结构清晰、注释充分便于理解FreeRTOS任务划分思想与嵌入式GUI开发范式。1. 项目概述一个嵌入式系统的“面子工程”在嵌入式开发里尤其是基于STM32这类MCU的产品人机交互界面HMI往往是决定用户体验好坏的关键一环。一个功能再强大的设备如果用户操作起来菜单混乱、逻辑不清那它的价值也会大打折扣。这次要聊的就是一个非常经典且实用的组合在STM32上利用FreeRTOS实时操作系统驱动一块12864点阵液晶屏实现一个支持按键翻页、层级清晰的多级菜单系统。这个项目压缩包stm32多级菜单实现12864.rar可以说是一个典型的嵌入式UI交互解决方案的缩影。它解决的核心问题是如何在资源有限的单片机RAM、Flash都捉襟见肘上构建一个稳定、流畅、可维护的菜单界面。12864液晶屏通常指128x64像素的点阵屏成本低廉、显示信息量适中是很多工控设备、仪器仪表、消费电子的首选。而FreeRTOS的引入则让菜单响应、按键扫描、屏幕刷新这些任务可以并行不悖互不干扰极大地提升了系统的实时性和可靠性。最终呈现的效果就是用户通过几个简单的按键如上、下、左、右、确认、返回就能像操作智能手机一样在层层嵌套的菜单中自如穿梭完成参数设置、状态查看、功能选择等操作。这个项目适合所有正在或即将使用STM32进行带屏产品开发的工程师无论是学生做课程设计、电子爱好者DIY还是工程师进行产品原型开发都能从中获得一套可以直接移植、深度定形的菜单框架。它不仅教你如何画点画线显示字符更重要的是展示了一种状态机驱动的菜单设计思想这是构建复杂交互逻辑的基石。2. 系统整体设计与架构解析2.1 硬件平台与核心组件选型要实现这个多级菜单系统硬件上离不开三大件主控、显示和输入。主控芯片STM32项目通常基于STM32F1系列如STM32F103C8T6或F4系列。选择它们的原因很直接性价比高、生态完善。F103有足够的性能72MHz Cortex-M3和内存20KB RAM64KB Flash来运行FreeRTOS和菜单逻辑且HAL库或标准库资料浩如烟海。如果菜单图形较复杂或需要更流畅的动画F4系列如STM32F407168MHz Cortex-M4会是更好的选择它拥有更强大的计算能力和更多的内存。显示模块12864 LCD这里的“12864”通常指的是基于ST7920、KS0108或ST7567等控制器的点阵液晶模块。它们大多支持并行8位/4位接口或SPI串行接口。SPI接口因其占用IO少、接线简单而成为首选。以ST7920为例它内部自带中文字库显示汉字非常方便但缺点是刷新速度相对较慢。而像ST7567这类无字库的控制器需要自行取模显示但刷新率和自由度更高。在项目选型时需要根据要显示的内容是否大量中文和刷新率要求来决定。输入设备按键这是人机交互的入口。最经典的方案是矩阵键盘或独立按键。对于菜单操作通常需要6个键上、下、左、右、确认OK、返回ESC。独立按键电路简单但占用IO多矩阵键盘用较少的IO实现更多按键但扫描程序稍复杂。在FreeRTOS下我们可以创建一个独立的“按键扫描任务”周期性地检测按键状态并通过队列Queue或信号量Semaphore将按键事件发送给菜单任务。为什么选用FreeRTOS裸机Bare-metal下实现多级菜单通常需要一个庞大的switch-case状态机耦合度高难以维护。FreeRTOS的引入带来了三大好处任务解耦将屏幕刷新、按键处理、菜单逻辑、业务功能分成独立任务通过任务间通信IPC如队列、事件标志组来协调结构清晰。实时响应高优先级的按键任务可以随时打断低优先级的显示任务确保用户操作无延迟感。系统可扩展性未来很容易在此基础上添加网络通信、数据存储等其他功能任务而无需重写菜单框架。2.2 软件架构与数据流设计整个系统的软件架构可以看作一个“生产者-消费者”模型核心是菜单任务。[按键扫描任务] (生产者) | | 发布按键事件 (队列) V [菜单任务] (核心消费者 处理器) | | 更新显示缓冲区 V [显示刷新任务] (消费者)菜单数据结构设计这是整个系统的灵魂。一个典型的菜单项结构体MenuItem可能包含以下字段typedef struct { uint8_t id; // 菜单项唯一ID const char *name; // 菜单项显示名称 MenuItemType type; // 类型子菜单、执行函数、数值设置等 void (*func)(void); // 若为执行项对应的函数指针 struct MenuItem *parent; // 父菜单指针 struct MenuItem *child; // 子菜单链表头指针 struct MenuItem *next; // 同级下一个菜单项指针 int32_t value; // 若为数值设置项当前值 int32_t min, max, step; // 数值范围与步进 } MenuItem_t;这种链表指针的结构构成了一个非循环图能够完美描述多级菜单的树状关系。每个菜单项都知道自己的父亲、孩子和兄弟通过指针跳转可以轻松实现进入子菜单、返回上级、在同级间切换。显示缓冲与渲染策略为了减少屏幕闪烁和提高刷新效率通常会采用“双缓冲”或“局部刷新”策略。开辟一块内存作为显示缓冲区FrameBuffer大小对应整个屏幕128*64/8 1024字节。菜单任务只更新缓冲区中需要改变的部分如反白的选择条、变化的数值然后通过信号量通知显示任务将整个缓冲区刷到屏幕上。对于12864这种慢速屏全屏刷新频率控制在10-20Hz即可既能保证流畅又不会占用过多CPU。3. 核心模块实现与代码剖析3.1 FreeRTOS任务创建与调度配置首先我们需要一个稳定的FreeRTOS运行环境。以STM32CubeMX配置为例内核配置在FreeRTOSConfig.h中根据芯片资源调整关键参数。#define configTOTAL_HEAP_SIZE ((size_t)10240) // 根据需求调整堆大小 #define configMINIMAL_STACK_SIZE ((uint16_t)128) #define configMAX_PRIORITIES (7) // 优先级数量 #define configUSE_PREEMPTION 1 #define configUSE_TIME_SLICING 1 // 启用时间片轮转使同等优先级任务能分时运行注意堆大小configTOTAL_HEAP_SIZE需要仔细估算。每个任务栈、队列、信号量都从这里面分配。设置太小会导致创建失败太大则浪费内存。建议先设置一个较大值运行后通过xPortGetFreeHeapSize()函数查看剩余堆空间再逐步调整到合适值。任务创建在main.c中创建我们的核心任务。// 按键扫描任务 xTaskCreate(KeyScan_Task, KeyScan, 128, NULL, 6, KeyScanHandle); // 菜单处理任务 xTaskCreate(Menu_Task, Menu, 256, NULL, 4, MenuHandle); // 显示刷新任务 xTaskCreate(Display_Task, Display, 256, NULL, 3, DisplayHandle); // 其他业务任务...优先级设置心得按键扫描任务优先级最高如6确保按键事件能被即时响应。菜单任务次之4负责处理逻辑。显示任务优先级可以设得较低3因为晚几毫秒刷新用户通常感知不到。业务任务如数据采集的优先级根据其紧急程度放在菜单和显示任务之间或之上。通信机制创建// 用于传递按键事件的队列最多存储5个按键消息 QueueHandle_t KeyEventQueue xQueueCreate(5, sizeof(KeyEvent_t)); // 用于通知显示刷新的二进制信号量 SemaphoreHandle_t DisplayUpdateSem xSemaphoreCreateBinary();3.2 菜单引擎的实现状态机与导航逻辑菜单的核心是一个状态机其状态至少包括MENU_IDLE空闲、MENU_BROWSING浏览项、MENU_EDITING编辑数值。Menu_Task的主体是一个无限循环等待按键队列中的事件并根据当前状态进行处理。void Menu_Task(void *argument) { KeyEvent_t key; MenuState_t state MENU_BROWSING; MenuItem_t *current_menu root_menu; // 指向根菜单 MenuItem_t *current_item current_menu-child; // 指向当前选中的项 while(1) { // 等待按键事件最多等待100个Tick if (xQueueReceive(KeyEventQueue, key, pdMS_TO_TICKS(100)) pdTRUE) { switch(state) { case MENU_BROWSING: handle_browsing(current_item, current_menu, key); break; case MENU_EDITING: handle_editing(current_item, key, state); break; } // 无论何种处理最后都标记需要更新显示 xSemaphoreGive(DisplayUpdateSem); } // 这里可以添加一些低优先级的后台菜单逻辑 } }关键函数handle_browsing的逻辑拆解KEY_UP/KEY_DOWN在current_menu指向的当前菜单层级的子项链表中移动current_item指针。如果到达链表头或尾可以实现循环滚动。KEY_OK如果current_item-type MENU_TYPE_SUBMENU则进入子菜单current_menu current_item; current_item current_menu-child;如果current_item-type MENU_TYPE_FUNC则执行对应的函数current_item-func();如果current_item-type MENU_TYPE_VAL则进入编辑状态state MENU_EDITING;KEY_BACK如果current_menu-parent不为空不是根菜单则返回上级current_item current_menu; current_menu current_menu-parent;。这里有个细节返回后最好将current_item指向刚才进入子菜单的那个项用户体验更好。编辑状态handle_editing当编辑一个数值项时KEY_UP/KEY_DOWN用于增减数值并立即更新结构体中的value字段。KEY_OK确认修改并退出编辑状态KEY_BACK取消修改恢复原值并退出。实操心得菜单的“焦点”与“反白”显示在显示部分我们需要根据current_item来高亮显示当前选中的项。通常的做法是在渲染一行菜单文本时判断其是否等于current_item如果是则用反白前景色和背景色互换的方式绘制。在12864上可以通过先画一个实心矩形作为背景再画正常颜色的文字来实现反白效果。3.3 12864液晶驱动与图形界面渲染驱动12864的第一步是初始化这需要严格按照其数据手册的时序来操作。以SPI接口的ST7567为例void LCD_Init(void) { // 硬件复位 LCD_RST_LOW(); HAL_Delay(100); LCD_RST_HIGH(); HAL_Delay(100); // 发送一系列初始化命令 LCD_Write_Cmd(0xAE); // 关闭显示 LCD_Write_Cmd(0x40); // 设置显示起始行 LCD_Write_Cmd(0xA1); // 设置段重映射 LCD_Write_Cmd(0xC8); // 设置COM扫描方向 // ... 更多命令设置对比度、电源模式等 LCD_Write_Cmd(0xAF); // 开启显示 }注意时序LCD_Write_Cmd和LCD_Write_Data函数中在拉高片选CS或使能EN信号前必须插入足够的延时通常是微秒级确保控制器有足够时间读取数据。过快会导致初始化失败或显示乱码。显示缓冲区的操作我们定义一个一维数组uint8_t framebuffer[1024]。对于128x64的屏幕通常被分为8页Page每页8行像素共64行。数组下标[0]到[127]对应Page0的128列[128]到[255]对应Page1以此类推。画点函数就是操作这个缓冲区void FB_SetPixel(uint8_t x, uint8_t y, uint8_t color) { if(x 128 || y 64) return; uint16_t addr (y / 8) * 128 x; uint8_t bit y % 8; if(color) { framebuffer[addr] | (1 bit); // 画白点 } else { framebuffer[addr] ~(1 bit); // 画黑点 } }菜单渲染函数这是连接菜单数据结构和屏幕显示的桥梁。它的职责是根据current_menu和current_item计算出当前屏幕应该显示哪几项菜单文本并将它们绘制到framebuffer中。void Render_Menu(MenuItem_t *menu, MenuItem_t *selected) { // 1. 清空framebuffer中菜单显示区域非全屏清空提高效率 FB_ClearRect(0, 0, 128, 64); // 2. 计算起始显示项。如果selected项不在当前可视区域则调整起始项。 MenuItem_t *start_item ...; // 滚动逻辑计算 // 3. 循环绘制可视范围内的菜单项例如一屏显示4项 uint8_t y_pos 0; MenuItem_t *item start_item; for(int i 0; i 4 item ! NULL; i) { if(item selected) { // 反白绘制选中项 FB_DrawFillRect(0, y_pos, 128, 16); // 画反白背景 FB_DrawString(2, y_pos4, item-name, COLOR_BLACK); // 黑字 } else { // 正常绘制 FB_DrawString(2, y_pos4, item-name, COLOR_WHITE); // 白字 } y_pos 16; // 行高 item item-next; } // 4. 绘制滚动条提示如果菜单项总数大于一屏可显示数 Draw_Scrollbar(...); }最后在Display_Task中等待信号量然后将整个framebuffer通过SPI发送到屏幕。void Display_Task(void *argument) { while(1) { // 等待菜单任务发出的更新信号 if (xSemaphoreTake(DisplayUpdateSem, portMAX_DELAY) pdTRUE) { LCD_Refresh_FrameBuffer(framebuffer); // 将缓冲区数据刷到屏幕 } } }4. 关键难点与性能优化实战4.1 菜单数据结构的动态加载与存储优化在复杂的菜单系统中菜单项可能非常多例如上百条全部存储在RAM中会占用大量宝贵的内存。一个优化策略是将菜单项的文本字符串name存储在Flash只读中只在结构体中保存指针。typedef struct { uint8_t id; const char *name; // 指向Flash中的常量字符串 // ... 其他字段 } MenuItem_t; // 在Flash中定义菜单文本 const char MENU_TEXT_ROOT[] 主菜单; const char MENU_TEXT_SETTING[] 系统设置; const char MENU_TEXT_DATE[] 日期设置; // ... // 定义菜单项字符串指针指向Flash地址 MenuItem_t root_menu {0, MENU_TEXT_ROOT, ...}; MenuItem_t setting_item {1, MENU_TEXT_SETTING, ...};这样每个菜单项节省了字符串长度的RAM。对于更极致的优化如果菜单结构固定甚至可以将整个MenuItem_t数组用const修饰放在Flash中运行时通过指针访问。但要注意此时不能修改结构体中的任何字段如value编辑功能需要额外设计存储方案。动态加载对于菜单层级非常深、项数极多的情况可以考虑“懒加载”。即只初始化当前可见的几级菜单当进入更深层级时再动态创建或从Flash加载该层级的菜单项数据。这需要更复杂的内存管理但在资源极其受限的场合非常有效。4.2 按键消抖与事件处理的最佳实践按键消抖是嵌入式基础但在RTOS中处理有讲究。绝对禁止在任务中使用HAL_Delay()这类阻塞延时这会阻塞整个任务影响系统实时性。推荐做法1在硬件中断中启动软件定时器。// 按键GPIO外部中断回调函数 void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if(GPIO_Pin KEY_Pin) { // 启动一个单次定时器10ms后触发 xTimerStartFromISR(key_debounce_timer, pdFALSE); } } // 定时器回调函数在定时器服务任务中执行 void KeyDebounceTimerCallback(TimerHandle_t xTimer) { // 此时再次读取按键电平如果是稳定按下状态则发送事件 if(HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin) GPIO_PIN_RESET) { KeyEvent_t event {KEY_OK, KEY_EVENT_PRESS}; xQueueSendFromISR(KeyEventQueue, event, NULL); } }推荐做法2在独立的按键扫描任务中使用状态机和非阻塞延时。void KeyScan_Task(void *argument) { TickType_t last_tick xTaskGetTickCount(); KeyState_t key_state KEY_STATE_IDLE; while(1) { vTaskDelayUntil(last_tick, pdMS_TO_TICKS(5)); // 每5ms扫描一次 uint8_t key_raw Read_Key_GPIO(); // 状态机处理消抖 switch(key_state) { case KEY_STATE_IDLE: if(key_raw ! KEY_NONE) { key_state KEY_STATE_DEBOUNCE; debounce_cnt 0; } break; case KEY_STATE_DEBOUNCE: debounce_cnt; if(debounce_cnt 4) { // 连续4次20ms检测到按键 if(key_raw ! KEY_NONE) { // 发送按下事件 Post_Key_Event(key_raw, PRESS); key_state KEY_STATE_PRESSED; } else { key_state KEY_STATE_IDLE; } } break; case KEY_STATE_PRESSED: if(key_raw KEY_NONE) { key_state KEY_STATE_RELEASE_DEBOUNCE; release_cnt 0; } else { // 这里可以实现长按检测计数超过某个值则发送长按事件 } break; // ... 释放消抖状态 } } }第二种方法更常用它将所有按键处理逻辑集中在一个任务中不依赖硬件中断更易于管理和扩展如实现组合键、长按、连按。4.3 屏幕刷新效率与动画效果优化12864液晶的刷新速度是瓶颈。全屏刷新1024字节通过SPI以1MHz速率发送也需要约8ms这期间CPU被大量占用。优化策略如下局部刷新只刷新屏幕上发生变化的区域。例如菜单光标从第2行移到第3行我们只需要重绘第2行和第3行这两行区域对应的缓冲区数据然后只发送这两行数据到屏幕。这需要驱动层支持指定行或区域的刷新函数。差异化刷新率静态内容如标题栏、边框刷新频率可以很低如1Hz动态内容菜单光标、滚动条、编辑中的数值刷新频率高10Hz。这需要将framebuffer分层或分块管理。使用DMA传输如果STM32的SPI支持DMA可以将framebuffer的数据搬运工作交给DMACPU在DMA传输期间可以处理其他任务极大提高效率。配置好DMA传输完成中断在中断中通知显示任务准备下一帧数据。简单的动画效果即使资源有限也可以实现一些提升体验的动画。例如进入子菜单时旧菜单从右向左滑出新菜单从右向左滑入。实现原理是在Render_Menu函数中不直接绘制最终位置而是根据一个“偏移量”绘制。在主循环中每帧改变这个偏移量并触发刷新直到偏移量为0。虽然会带来多帧刷新但简单的滑动效果能让菜单切换显得更流畅。5. 项目移植、调试与问题排查实录5.1 从现有工程到新平台的移植步骤拿到一个类似stm32多级菜单实现12864.rar的工程包要移植到自己的板子上可以按以下步骤进行硬件接口适配屏幕接口确认你的12864屏幕控制器型号ST7920/KS0108/ST7567等和接口并行/SPI/I2C。修改lcd_driver.c/h中的引脚定义、初始化序列和底层读写函数LCD_Write_Cmd,LCD_Write_Data。最可能出错的地方就是初始化序列和时序务必对照新屏幕的数据手册逐一核对命令和延时。按键接口根据你的按键电路独立IO或矩阵重写key_scan.c中的按键读取函数。如果原工程使用中断而你用扫描需要重构这部分代码。FreeRTOS配置检查使用STM32CubeMX重新配置FreeRTOS确保FreeRTOSConfig.h中的堆大小、任务栈深度等参数适合你的芯片。通常需要将原工程的FreeRTOSConfig.h和FreeRTOS相关源文件替换为CubeMX生成的版本但保留其中与硬件无关的修改如钩子函数。菜单内容定制在menu_config.c中找到定义菜单结构的数组或链表。按照你的需求修改菜单项的文字、ID、类型和函数指针。这是最体现项目价值的部分你需要将菜单逻辑与你的实际业务函数如Set_Temperature()Read_Voltage()关联起来。编译与链接解决因路径变化导致的头文件引用错误。注意原工程可能使用了某些编译器特定扩展如__attribute__((section(.xxx)))移植到不同编译器如从Keil到IAR或GCC时需要调整。5.2 常见问题与调试技巧速查表在实际开发中你几乎一定会遇到下面这些问题。这里提供一个快速排查指南现象可能原因排查步骤与解决方案屏幕白屏或乱码1. 电源或背光不正常。2. 初始化序列错误或时序不对。3. 数据/命令选择线RS/A0电平错误。1. 用万用表测量VCC、GND、背光电压。2. 用逻辑分析仪或示波器抓取SPI/I2C波形对照数据手册看初始化命令和数据是否正确发出特别注意命令/数据切换的时序。3. 检查LCD_Write_Cmd和LCD_Write_Data函数中设置RS/A0引脚电平的代码。按键无反应或反应混乱1. 按键扫描任务优先级过低或栈溢出。2. 按键消抖逻辑有bug。3. 按键事件队列满导致新事件丢失。4. GPIO上下拉配置错误。1. 提高按键扫描任务优先级使用FreeRTOS的栈溢出检测钩子函数检查。2. 简化代码先去掉消抖看裸按键值是否能正确读取。3. 在发送队列后检查返回值或增大队列长度。4. 检查硬件电路和CubeMX中GPIO配置上拉/下拉输入。菜单切换卡顿反应慢1. 屏幕全屏刷新太频繁。2. 菜单渲染函数计算过于复杂。3. 有其他高优先级任务长时间阻塞CPU。1. 实现局部刷新并降低显示任务的刷新频率如用vTaskDelay控制为20ms一帧。2. 优化Render_Menu函数避免在循环中进行浮点运算或复杂字符串处理。3. 使用vTaskList()函数查看所有任务的状态和CPU占用优化任务优先级和执行时间。进入深层菜单后系统死机1. 任务栈溢出。2. 菜单链表指针错误导致无限递归或访问非法内存。1. 增大菜单任务和显示任务的栈大小。2. 在菜单导航函数中加入边界检查确保current_item和current_menu不为NULL。使用调试器单步跟踪进入子菜单和返回上级时的指针变化。编辑数值时数值跳动异常1. 按键长按/连按处理逻辑有误。2. 数值的min, max, step边界检查未生效。1. 在按键处理中区分短按步进±1和长按快速连续步进。2. 在handle_editing函数中每次增减value后立即用value MAX(MIN(value, max), min)进行钳制。调试利器SEGGER SystemView。如果你使用的是J-Link调试器强烈建议集成SEGGER SystemView。它可以可视化地展示所有FreeRTOS任务的运行状态、切换时机、中断、队列和信号量操作。当遇到任务调度、响应延迟等复杂问题时SystemView的图形化时间线能让你一眼看清问题所在比如发现显示任务因为等待一个信号量而长时间阻塞从而定位到是菜单任务没有及时发出信号。5.3 功能扩展与项目进阶思路一个基础的多级菜单系统实现后你可以考虑以下方向进行扩展让它更强大、更专业多语言支持将菜单文本从代码中分离存储在外部SPI Flash或SD卡中。定义多个语言包文件运行时根据用户选择加载不同的语言包。菜单结构体中的name字段从一个常量指针变为一个从语言包中查找索引值的函数调用。图标与图形化界面12864屏幕虽然分辨率低但也可以显示简单的图标16x16像素。可以为重要的菜单项配上图标。更进一步可以移植一个轻量级的GUI库如u8g2、LVGL针对嵌入式优化后的版本利用其丰富的控件按钮、滑块、下拉列表来构建更现代化的界面。但这会对RAM和Flash消耗有较大增加。菜单数据掉电保存用户设置的参数数值型菜单项的value需要保存到EEPROM或Flash的特定扇区。可以设计一个“保存设置”的菜单项或在退出编辑状态时自动保存。注意Flash写入寿命和写入前的擦除操作。与业务逻辑深度集成菜单不应只是一个孤立的界面。它应该能实时显示系统状态如“当前温度25.3℃”这需要菜单任务能接收来自其他任务如传感器采集任务的数据更新消息。可以通过全局变量加互斥锁保护、队列或发布-订阅模型来实现。移植和调试这个过程就像在给一个精密的机械手表上弦调校每一个齿轮模块都必须严丝合缝。从最底层的GPIO时序到中间层的RTOS任务调度再到顶层的菜单状态跳转任何一环的疏漏都会导致整个系统行为异常。但一旦调通看着清晰的菜单随着按键的节奏在屏幕上流畅地跳动那种对系统完全掌控的成就感正是嵌入式开发的乐趣所在。这个项目提供的不仅仅是一个可运行的代码包更是一套如何在资源受限环境下构建清晰人机交互逻辑的方法论掌握了它你就能应对更多带屏嵌入式产品的开发挑战。本文还有配套的精品资源点击获取
返回列表