尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

STM32F103驱动12864点阵LCD多级菜单设计与实现

STM32F103驱动12864点阵LCD多级菜单设计与实现 简介本资源是一份面向嵌入式初学者与中级开发者的STM32人机交互实战项目聚焦STM32F103微控制器驱动12864点阵LCD并实现多级菜单功能解决工业控制、智能家居等场景中图形界面开发与用户交互设计的实际问题。压缩包为RAR格式大小1.64MB包含完整Keil工程源码含LCD底层驱动、SPI通信配置、菜单树形结构管理、按键扫描与消抖逻辑、配套初始化与显示函数模块以及清晰的目录组织结构便于理解软硬件协同流程。已有4001人学习下载资源代码注释详尽支持直接编译烧录运行读者可快速掌握12864显示原理、Cortex-M3外设配置、状态机式菜单导航及人机界面布局优化等核心技能是深入理解嵌入式GUI开发的典型参考范例。 最近在折腾一个用STM32F103驱动12864点阵LCD的多级菜单项目趁着刚整理完源码把这套东西的来龙去脉和踩过的坑一并写出来。12864这种屏在工业设备、仪器仪表、家用小电器里太常见了价格便宜、显示信息量大配上STM32F103这种“国民MCU”基本是低成本人机交互方案里的经典组合。尤其做多级菜单12864比1602能容纳更多信息比OLED大屏又便宜不少我这套代码写完实测下来无论是做温控器、小型仪表还是嵌入式学习拿来改改就能用。这次分享的内容包括硬件怎么选、12864底层驱动怎么写、多级菜单的数据结构和状态机怎么设计最后附上完整的源码工程思路。想直接抄作业的可以照着我的接线表和代码框架搭想搞清楚原理的也能从里面的设计逻辑里学到东西。1. 项目整体设计与硬件选型1.1 为什么是STM32F103 12864这个组合其实是从实际项目需求里长出来的。我之前做一个小型控制器需要显示温度、湿度、工作模式、参数设置等好几层界面又要控制成本。当时对比过几种方案0.96寸OLED显示效果好但尺寸小多级菜单要频繁翻页而且价格比12864贵一倍不止1602字符屏只能显示2行英文数字做菜单层级显示很憋屈3.5寸TFT彩屏效果好但功耗高、驱动复杂、成本高小项目根本没必要12864点阵LCD能显示4行中文每行最多8个汉字带字库的型号写中文尤其方便价格十几块钱还省电STM32F103C8T6这个芯片64KB Flash、20KB RAM主频72MHz跑一个12864加多级菜单的界面逻辑资源绰绰有余而且国产替代型号满天飞几块钱一片量产成本压得很低。整套系统下来核心物料成本可以控制在30块钱以内。1.2 12864屏幕的两种主流方案这里必须先说清楚市场上叫“12864”的屏其实分两大阵营驱动方式完全不同ST7920带中文字库内置GB2312中文字库写汉字直接给编码就行支持8位并行、4位并行、串行三种接口初始化简单底层驱动好写缺点是屏幕刷新慢纯图形模式操作麻烦KS0108不带字库纯图形模式所有字符都要自己取模内部其实是两个64×64的控制器拼成128×64显示中文需要准备字库数组占Flash但绘图能力强刷新快适合做图形界面我这套例程用的是ST7920带字库的方案。原因很简单多级菜单大量涉及中文显示带字库的可以直接调。市面上绝大多数带字库12864屏后面的接口标志是“12864ZW”或类似的型号标识。1.3 硬件接线和接口模式选择ST7920支持三种接口方式通过PSB引脚选择高电平8位并行低电平串行悬空或接电阻4位并行部分型号我实际测试下来8位并行最稳定、速度最快但需要至少11个GPIODB0~DB7共8个再加RS、RW、E。串行模式只需要3个引脚CS、SID、SCLK省引脚但速度慢、时序要求严格。如果做多级菜单这种交互密集型的界面我推荐用8位并行操作直观、不易出bug。以下是完整接线表以STM32F103C8T6最小系统板为例12864引脚功能接STM32引脚VSS电源地GNDVDD电源正3.3V或5V视模块VO对比度调节10K电位器中间脚RSCS数据/命令选择PA0RWSID读/写选择PA1ESCLK使能信号PA2DB0~DB7数据总线PB0~PB7PSB并/串选择接3.3V选并行RST复位接3.3V或MCU引脚LEDA背光正极3.3V串电阻LEDK背光负极GND注意部分模块PSB引脚已经内部上拉不用额外接。但如果模块上PSB悬空默认状态不一定并行必须用万用表确认电平。我遇到过一张屏怎么调都不出字最后发现就是PSB没拉高。另一个关键点是等宽排针对应关系。市面上的12864模块接口排列五花八门有的第1脚是VSS有的第1脚是CS接线前一定要看模块背面的丝印别想当然按“标准顺序”插烧屏不至于但白折腾半天。1.4 软件工程结构工程结构我是这么组织的标准库工程|-- User/ | |-- main.c | |-- stm32f10x_it.c |-- HARDWARE/ | |-- lcd12864.c / lcd12864.h | |-- key.c / key.h | |-- menu.c / menu.h |-- SYSTEM/ | |-- delay.c / delay.h | |-- sys.c / sys.h | |-- usart.c / usart.h注意标准库和HAL库在这项目里都能用我用标准库主要是启动快、代码量小。如果你用的是HAL库底层函数换一下比如GPIO读写就行菜单框架代码完全通用。2. 12864底层驱动核心细节2.1 DDRAM地址映射搞懂它菜单界面就成功一半ST7920的显示数据RAMDDRAM地址不是连续递增的这是新手最容易懵的地方。它总共64×16个字节的DDRAM对中文字符模式扩展指令集和ASCII模式基本指令集有些差异。在中文字符模式下屏幕分4行每行最多8个汉字对应地址如下屏幕行DDRAM起始地址说明第1行0x800x80~0x87第2行0x900x90~0x97第3行0x880x88~0x8F第4行0x980x98~0x9F也就是说要写第3行第1个字符的位置地址是0x88而不是0x88不对——是0x80 0x08 0x88。地址规律是行基地址0x80、0x90、0x88、0x98加上列偏移0~7。这个映射我一开始也搞错过写第2行直接用了0x88结果显示到第3行去了。后来把地址表贴在显示器旁边才顺过来。真正常用的坐标定位函数就是void LCD_SetCursor(uint8_t row, uint8_t col) { uint8_t addr; if (row 0) addr 0x80 col; else if (row 1) addr 0x90 col; else if (row 2) addr 0x88 col; else if (row 3) addr 0x98 col; LCD_WriteCmd(addr); }2.2 写命令和写数据的时序问题ST7920的并行接口有三根控制线RS数据/命令选择、RW读/写选择、E使能。写命令时RS0写数据时RS1RW一直置0我们基本只写屏不读屏。E引脚的操作时序是RS和RW先稳定数据放到DB0~DB7E拉高保持至少一个周期E拉低完成一次写入void LCD_WriteCmd(uint8_t cmd) { LCD_RS_LOW(); LCD_RW_LOW(); LCD_DataPort(cmd); LCD_E_HIGH(); delay_us(2); LCD_E_LOW(); delay_us(2); } void LCD_WriteData(uint8_t dat) { LCD_RS_HIGH(); LCD_RW_LOW(); LCD_DataPort(dat); LCD_E_HIGH(); delay_us(2); LCD_E_LOW(); delay_us(2); }这里有个取舍要不要读忙标志。ST7920执行每条指令需要一定时间大约72us如果连续高速写数据必须判断忙标志BF位否则会丢数据。读忙标志需要RW1从DB7读出。但实际工程里我更倾向于用固定延时等待因为读忙标志本身也要额外时序操作而且STM32F103跑72MHz两条写指令之间加几个微秒延时完全够用代码还简单。经验执行清屏指令0x01耗时较长要延时1.6ms以上初始化或者频繁清屏时别忘了。我最初因为清屏后马上写数据导致第一行偶尔不显示排查好久才发现是等的时间不够。2.3 ST7920初始化序列ST7920的初始化有标准流程必须在代码里严格按顺序执行void LCD_Init(void) { delay_ms(50); // 上电等待 LCD_WriteCmd(0x30); // 基本指令集 delay_ms(5); LCD_WriteCmd(0x30); // 重复设置 delay_ms(5); LCD_WriteCmd(0x30); // 再重复一次 delay_ms(5); LCD_WriteCmd(0x0C); // 显示开光标关闪烁关 delay_ms(2); LCD_WriteCmd(0x01); // 清屏 delay_ms(2); LCD_WriteCmd(0x06); // 光标右移整体画面不移动 }这个序列其实是在测试并行数据线和液晶控制器握手。0x30连续发三次是ST7920手册要求的“唤醒”序列不能省。0x0C是打开显示0x01清屏0x06设置写入后光标自动右移。注意如果要切换中英文模式需要进入扩展指令集即发送0x34在0x30基础上加0x04然后发送0x30选择中文模式。很多屏默认就在中文模式但为了保险我在初始化里加了一句LCD_WriteCmd(0x34); // 进入扩展指令集 LCD_WriteCmd(0x30); // 选择中文字库模式 LCD_WriteCmd(0x36); // 绘图显示开如果需要画图 LCD_WriteCmd(0x30); // 回到基本指令集2.4 显示字符串和自定义符号写字符串的函数核心就是调用写数据接口一个字符一个字符地发。ST7920对ASCII字符按半角处理一个字节一个字符对中文则两个字节一个汉字GB2312编码。void LCD_ShowString(uint8_t row, uint8_t col, const char* str) { LCD_SetCursor(row, col); while (*str) { LCD_WriteData(*str); } }中文显示更需要注意编码问题。如果用的Keil MDK默认编码是GB2312字符串直接写中文是可以的。但如果用其他IDE比如STM32CubeIDE默认UTF-8编码直接把中文字符串发给LCD就会乱码因为ST7920只认GB2312编码。这个坑很隐蔽代码看起来没问题屏幕上全是乱码。解决方法有两个设置IDE编码为GB2312/GBK写一个UTF-8转GB2312的函数转换后再发送另外ST7920内置CGRAM可以自定义8个16×16的点阵图案可以用来做小图标、箭头、进度条符号等。这一步需要进入扩展指令集先写CGRAM地址0x40 offset*32然后连续写32个字节数据void LCD_DrawCustomSymbol(uint8_t index, const uint8_t* data) { LCD_WriteCmd(0x34); // 扩展指令集 LCD_WriteCmd(0x40 index * 32); // CGRAM地址 for (uint8_t i 0; i 32; i) { LCD_WriteData(data[i]); } LCD_WriteCmd(0x30); // 回基本指令集 }写完后在屏幕上显示该自定义符号时直接发送对应的字符码0x00~0x07其中一个。2.5 并行还是串行我的建议虽然前面我推荐并行但如果你的项目引脚紧张串行模式也够用。串行模式下12864只需要三根线CS片选、SID串行数据、SCLK串行时钟连接方式CS → PA3SID ← PA4SCLK ← PA5串行时序比并行复杂一点数据要先发5个字节的同步头部11111ABC然后再分两次发送数据的高4位和低4位。实际效果和并行几乎一样只是每写一字节耗时更长。菜单界面刷新频率不高完全能接受。注意串行模式下RS、RW引脚不参与控制但PSB必须拉低。很多串行模块PSB已经固定接地不需要额外接。3. 多级菜单框架设计这才是本项目核心3.1 菜单数据结构的选型多级菜单本质上是一个树形结构。常见有几种实现方式链表动态分配灵活但占用RAM多STM32F103的20KB RAM勉强够用但不想为了菜单去搞malloc结构体数组索引静态定义查找快代码直观适合单片机嵌套结构体用指针把父子菜单关联起来代码可读性好我最终用的是结构体数组索引的变体方案每个菜单项是一个结构体包含文字、子菜单表指针、选中回调函数等。typedef struct MenuItem { const char* text; // 菜单显示文本 void (*handler)(void); // 确认后执行的函数 const struct Menu* subMenu; // 子菜单指针 } MenuItem; typedef struct Menu { const MenuItem* items; // 菜单项数组 uint8_t count; // 菜单项个数 uint8_t current; // 当前选中项索引 } Menu;然后定义几个菜单extern Menu menu_root; extern Menu menu_set; extern Menu menu_cal; const MenuItem mainItems[] { {温湿度显示, NULL, menu_humi_temp}, {参数设置, NULL, menu_set}, {系统校准, NULL, menu_cal}, {关于本机, ShowAbout, NULL}, }; Menu menu_root { mainItems, 4, 0 }; const MenuItem setItems[] { {温度上限, SetTempMax, NULL}, {温度下限, SetTempMin, NULL}, {报警开关, ToggleAlarm, NULL}, {返回上级, NULL, NULL}, }; Menu menu_set { setItems, 4, 0 };这里两个关键点“返回上级”不靠父指针实现而是作为一个普通菜单项。好处是代码简单——返回也是一次“确认”操作而已。如果想让“返回”键直接返回上级需要维护一个调用栈记录路径代码复杂度会上升不少。我实际做产品时更推荐显式“返回上级”项用户操作逻辑也更清晰。每个菜单项可以关联一个子菜单或者执行函数。如果子菜单非空确认时进入子菜单如果handler非空确认时执行该函数。3.2 菜单状态机的运转逻辑有了数据结构接下来是核心处理逻辑。我用一个简单的状态机管理总共三个状态typedef enum { MENU_STATE_VIEW, // 浏览菜单 MENU_STATE_SETTING, // 参数设置 MENU_STATE_EXECUTE, // 执行动作 } MenuState;按键操作映射到函数上移current (current count - 1) % count下移current (current 1) % count确认进入子菜单或执行handler返回回到父菜单void Menu_HandleKey(uint8_t key) { Menu* cur menu_current; switch (key) { case KEY_UP: if (menu_state MENU_STATE_VIEW) { cur-current (cur-current cur-count - 1) % cur-count; Menu_Refresh(); } break; case KEY_DOWN: if (menu_state MENU_STATE_VIEW) { cur-current (cur-current 1) % cur-count; Menu_Refresh(); } break; case KEY_ENTER: { const MenuItem* item cur-items[cur-current]; if (item-subMenu ! NULL) { menu_current *item-subMenu; // 进入子菜单 menu_state MENU_STATE_VIEW; Menu_Refresh(); } else if (item-handler ! NULL) { item-handler(); Menu_Refresh(); } break; } case KEY_BACK: // 返回父菜单 Menu_GoBack(); Menu_Refresh(); break; } }3.3 返回父菜单的实现这里有多种做法每个菜单结构体里加一个parent指针定义时手动指向上级简单直接用调用栈记录路径进入子菜单时压栈返回时弹栈适合层级很深的场景不维护路径用固定返回当只有一个父级时直接返回根菜单我最后采用的是在MenuItem结构体中增加parent字段。实际定义时因为菜单都是静态的手动指定父菜单指针也花不了多少功夫typedef struct MenuItem { const char* text; void (*handler)(void); struct Menu* subMenu; struct Menu* parent; // 指向父菜单 } MenuItem;每定义一个菜单项就把它的父菜单指好。返回键处理就变成void Menu_GoBack(void) { if (menu_current.items[menu_current.current].parent ! NULL) { menu_current *menu_current.items[menu_current.current].parent; menu_current.current 0; // 回到父菜单第一个项 } }3.4 按键扫描与防抖按键驱动是菜单交互的基础。我用的是定时扫描状态机的方式不用外部中断因为按键机械抖动处理起来更可控。typedef enum { KEY_IDLE, KEY_DOWN, KEY_RELEASE, } KeyState; void Key_Scan(void) { static KeyState state KEY_IDLE; static uint8_t pressed 0; uint8_t key Key_GetLevel(); // 读取GPIO电平 switch (state) { case KEY_IDLE: if (key KEY_PRESSED) { state KEY_DOWN; } break; case KEY_DOWN: if (key KEY_PRESSED) { pressed 1; // 确认按下消除抖动 state KEY_RELEASE; } break; case KEY_RELEASE: if (key KEY_RELEASED) { if (pressed) { Menu_HandleKey(...); pressed 0; } state KEY_IDLE; } break; } }这个扫描函数放在SysTick的中断回调里每10ms调用一次。注意不要在按键扫描的同一时刻去刷新LCD因为LCD刷新比较耗时放在主循环里跑就行。3.5 屏幕绘制与刷新策略12864没有显存缓存每次写字符是直接写DDRAM。菜单刷新我采用“变化时整体重绘”的策略刷新频率低不会出现闪烁。void Menu_Refresh(void) { LCD_Clear(); // 清屏 Menu* cur menu_current; uint8_t showCount (cur-count 4) ? cur-count : 4; uint8_t startY (4 - showCount) / 2; for (uint8_t i 0; i showCount; i) { if (i cur-current) { LCD_ShowString(startY i, 0, ); LCD_ShowString(startY i, 2, cur-items[i].text); } else { LCD_ShowString(startY i, 2, cur-items[i].text); } } }思路很直接光标用“ ”符号标识当前选中项最多只在4行内显示。超过4项的菜单我做了一个简单的滚动处理——维护一个显示偏移量始终让选中项保持在可见范围内但为了省Flash和RAM目前这个例程里菜单项控制在4个以内日常够用。如果菜单层级深、条目多建议加上“当前菜单显示偏移量”字段typedef struct Menu { const MenuItem* items; uint8_t count; uint8_t current; uint8_t pageOffset; // 滚动显示时偏移 } Menu;3.6 参数设置界面的实现多级菜单里经常要设置数值参数温度上限、时间值等。这里不能简单用“确认”结束需要适配数值加减操作。我的做法菜单项在进入“设置模式”后菜单状态切换到最上面定义过的MENU_STATE_SETTING。此时上下键变为加减数值确认键保存并退出返回键取消修改、恢复原值int16_t settingValue 0; int16_t settingTemp 0; const int16_t* settingMin; const int16_t* settingMax; void Menu_EnterSetting(void) { settingTemp settingValue; menu_state MENU_STATE_SETTING; } void Menu_HandleSettingKey(uint8_t key) { switch (key) { case KEY_UP: settingTemp 1; if (settingTemp *settingMax) settingTemp *settingMax; break; case KEY_DOWN: settingTemp - 1; if (settingTemp *settingMin) settingTemp *settingMin; break; case KEY_ENTER: settingValue settingTemp; // 保存 menu_state MENU_STATE_VIEW; break; case KEY_BACK: settingTemp settingValue; // 放弃修改 menu_state MENU_STATE_VIEW; break; } LCD_ShowString(2, 0, 数值: ); LCD_ShowNum(2, 6, settingTemp, 3); }这种方式的优势是复用四个按键不增加额外硬件而且用户操作逻辑清晰——进入设置后上下加减确认保存返回取消。4. 完整源码实现与调试流程4.1 源码文件结构整个项目的源码包括以下核心文件lcd12864.c底层驱动包括引脚初始化、写命令、写数据、清屏、定位、显示字符串、显示数字key.c按键扫描支持矩阵按键或独立按键包含消抖逻辑menu.c菜单数据结构定义、菜单初始化、按键处理、界面刷新、参数设置逻辑main.c主循环调用各模块初始化循环扫描按键并分发到菜单处理模块这里我把菜单逻辑从main函数里完全剥离出来main.c只做三件事int main(void) { SysTick_Init(72); LCD_Init(); Key_Init(); Menu_Init(); LCD_Clear(); Menu_Refresh(); while (1) { uint8_t key Key_Scan(); if (key ! KEY_NONE) { Menu_HandleKey(key); } } }主循环里只做按键扫描和菜单处理。这种架构非常清爽后续要加其他外设直接在while循环里加处理逻辑即可。4.2 菜单示例仪表类应用的三级菜单我测试用的菜单结构如下根菜单 ├── 温湿度显示 → 进入实时显示界面带刷新 ├── 参数设置 │ ├── 温度上限 → 数值加减设置 │ ├── 温度下限 → 数值加减设置 │ ├── 报警使能 → ON/OFF切换 │ └── 返回上级 ├── 系统校准 │ ├── 零点校准 → 执行校准 │ ├── 满度校准 → 执行校准 │ └── 返回上级 └── 关于本机 → 显示版本信息这个结构其实已经覆盖了大多数仪表类产品的菜单需求。如果你要做空气炸锅、温控器、充电桩显示屏之类的产品这个骨架可以直接套用。4.3 编译环境的搭建我用的是Keil MDK5配合ST-Link V2下载器。工程配置要点芯片选择STM32F103C8时钟外部晶振8MHz系统时钟72MHzFlash烧录算法STM32F10x Med-density 128K宏定义USE_STDPERIPH_DRIVER, STM32F10X_MD如果你用的是国产STM32比如GD32F103注意GD32内核时钟和Flash等待周期略有差异但代码完全兼容。下载的时候我在开发中遇到了ST-Link驱动装不上的情况——Win10自动识别驱动失败。解决方案是手动安装ST-Link Utility自带的驱动或者用驱动管理工具强制更新为“STMicroelectronics STLink dongle”驱动。另外用CH340串口下载辅助调试时CH340驱动也可能需要手动安装我在调试串口日志时踩过一次win10的坑插上没反应设备管理器显示感叹号重装CH340驱动后恢复。4.4 串口日志辅助调试12864屏调试有个麻烦屏上显示的内容本身不便于打日志。所以我习惯在开发阶段用USART1输出调试信息比如菜单切换事件、按键值、关键变量变化printf([MENU] enter submenu: %s\r\n, item-text); printf([KEY] key%d state%d\r\n, key, state);在菜单处理函数的入口加一行printf能极大缩短排查时间。比如按了“确认”没反应先看串口日志确认按键值对不对、是哪个菜单项被触发。这个习惯帮我省了至少一半的调试时间。4.5 下载烧录与运行验证烧录流程ST-Link V2连接SWDIO→PA13SWCLK→PA14GND→GND3.3V→3V3Keil里配置Debug选项为ST-Link并勾选“Download to Flash”编译无错误后点LOAD按钮复位后屏幕出现根菜单界面如果出现烧录失败排查顺序ST-Link驱动→连接线序→目标板供电→Boot0跳线如果需要ISP下载。4.6 串行方案源码示例如果你的项目引脚紧张只能分配3个引脚给LCD可以用串行方式。核心函数void LCD_WriteByte_SPI(uint8_t dat) { for (uint8_t i 0; i 8; i) { LCD_SCLK_LOW(); if (dat 0x80) { LCD_SID_HIGH(); } else { LCD_SID_LOW(); } dat 1; LCD_SCLK_HIGH(); } } void LCD_WriteCmd_SPI(uint8_t cmd) { LCD_CS_LOW(); LCD_WriteByte_SPI(0xF8); // 同步头11111 RS0 RW0 0 LCD_WriteByte_SPI(cmd 0xF0); LCD_WriteByte_SPI((cmd 4) 0xF0); LCD_CS_HIGH(); } void LCD_WriteData_SPI(uint8_t dat) { LCD_CS_LOW(); LCD_WriteByte_SPI(0xFA); // 同步头11111 RS1 RW0 0 LCD_WriteByte_SPI(dat 0xF0); LCD_WriteByte_SPI((dat 4) 0xF0); LCD_CS_HIGH(); }注意串行模式下每次发送命令或数据前先发5个1作为同步头然后跟着RS位命令0数据1。之后分两次发送高位字节和低位字节各占4位有效数据。这个协议初次接触比较反直觉调试时用一个逻辑分析仪看波形是最快的。实测下来串行方式驱动带字库12864显示菜单完全没压力。一个菜单界面刷新一次也就几毫秒的事肉眼根本看不出延迟。5. 常见问题与排查技巧实录5.1 屏幕不亮或者对比度异常这是最常遇到的问题通常是三方面原因供电问题st7920屏的VDD一般是5V如果你的屏模块上有稳压电路或者标注支持3.3V可以直接接3.3V否则需要5V供电但数据和控制信号引脚很多是TTL电平3.3V的STM32也能直接驱动实测没问题。如果板子没有5V引脚就从USB供电的VBUS取。对比度VOVO引脚必须接电位器把电压调到合适的负压或者电压偏置。如果直接悬空屏幕要么全黑要么无显示。调节电位器时能看到屏幕底色变化调到刚好无底色但显示字符清晰即可。背光LEDA接电源正极LEDK接地不加限流电阻的话背光太亮也容易烧背光LED。一般串一个几十欧的电阻。5.2 花屏、乱码、显示位置不对如果屏幕能亮但全是方块或者乱码先检查PSB引脚的电平状态。PSB如果悬空默认状态不一定是并行。强制拉高后再试试。如果只有中文乱码英文正常那就是字体编码问题。按前面说的用Keil时检查文件编码是否为GB2312。用其他IDE时把源码文件转成GB2312编码或者加转换函数。如果字符位置偏了检查LCD_SetCursor里的地址映射表是否写对尤其是第2行和第3行的地址0x90和0x88这两个经常有人搞反。5.3 按键无响应或有时失灵排除硬件问题后最常见的是GPIO模式配置错误。我使用内部上拉输入按键一端接GND按下时引脚读到低电平。注意STM32的GPIO配置要选上拉输入GPIO_Mode_IPU而不是浮空输入GPIO_Mode_IN_FLOATING。浮空输入时引脚电平不稳定按键会随机触发。另外如果按键处理在中断里执行注意不要在主循环里和中断里同时调用Menu_HandleKey避免重入问题。5.4 编译下载失败常见的几种找不到targetST-Link连接不良检查SWDIO、SWCLK两根线是否接反目标板供电是否正常Flash Download failedKeil的Flash算法配置不对STM32F103C8应该选“STM32F10x Med-density 128K”RDDI-DAP Error可能是目标板处于低功耗状态或者SWD被禁用。如果代码里调用了GPIO_PinRemapConfig(GPIO_Remap_SWJ_Disable, ENABLE)把SWD引脚禁用那就只能用串口ISP或恢复默认配置5.5 菜单跳转乱了或者进入了奇怪的状态这种问题多半是数据结构定义有问题。排查时重点检查菜单数组元素个数和count是否一致多一个少一个都会导致访问越界子菜单指针是否指向了正确的静态结构体修改菜单后refresh的时候是否更新了current索引常量字符串是否用const修饰防止写入Flash区域崩溃还有一次我遇到的诡异情况是菜单显示正常但按“确认”进去后整个屏乱码。最后定位到是结构体里void (*handler)(void)指针没初始化刚好指向了随机地址。解决办法是所有菜单项定义时把函数指针都显式赋值为NULL处理时判空if (item-handler ! NULL) { item-handler(); }5.6 SysTick延时卡死的现象开发过程中遇到过一次程序跑着跑着卡住反复复位无法解决。用调试器定位发现卡在delay函数里。原因是我在中断服务函数里使用了delay_ms而delay_ms本身依赖SysTick中断优先级又被设成最低导致中断嵌套时SysTick无法触发。解决方案中断服务函数里尽量直接修改标志位延迟等待放到主循环处理如果一定要用延迟用DWT计数器或者简单的for循环延时替代SysTick延时。5.7 菜单显示闪烁如果刷新菜单时能肉眼看到闪动多数是因为清理和重新绘制之间的间隔太长或者背光电流波动。可以尝试以下改善重绘前先关显示0x08绘制完再开显示0x0C虽然会闪但幅度小只刷新发生变化的行而不是整体清屏提高写入效率用连续写数据模式而不是每条命令都判断一次忙标志6. 多级菜单扩展建议让这套代码发挥更大价值这套框架再往下走可以扩展出很多实用的功能6.1 用旋转编码器代替按键很多仪器仪表用旋转编码器EC11来做菜单操作。编码器只需要两个引脚A相、B相配合按键引脚可以识别“旋转按下”操作。STM32的TIM编码器模式可以硬件识别旋转方向代码甚至比按键扫描还简单。原来需要四个按键现在一个编码器全搞定面板更简洁。6.2 菜单参数保存到Flash目前菜单里的参数是RAM变量掉电丢失。实际产品中参数设置完必须断电保存。STM32F103的内置Flash可以模拟EEPROM直接把关键参数写到Flash末尾区域。void SaveParams(void) { FLASH_Unlock(); FLASH_ErasePage(PARAM_ADDR); FLASH_ProgramHalfWord(PARAM_ADDR, settingTempMax); FLASH_ProgramHalfWord(PARAM_ADDR 2, settingTempMin); FLASH_Lock(); } void LoadParams(void) { settingTempMax *(uint16_t*)PARAM_ADDR; settingTempMin *(uint16_t*)(PARAM_ADDR 2); }注意Flash的擦写寿命约1万次产品要合理设计减少频繁保存。另外Flash页面擦除前要确保当前程序不是从该地址运行的——所以存放参数的页要选在程序代码以外的区域。6.3 界面加入图标和进度条ST7920的绘图模式可以画点、画线、画矩形。如果菜单里要做“开机进度条”“信号强度图标”可以用扩展指令集中的绘图命令。但绘图模式下屏幕是纯图形模式不能直接显示中文通常是先画图形再切回文本模式显示文字。6.4 移植到HAL库或者标准库这套代码的菜单逻辑完全不依赖硬件库只要你把LCD_WriteCmd和LCD_WriteData这类底层函数换成对应平台的实现菜单部分就能无缝移植。我用STM32CubeMX生成过HAL库版本只改了底层GPIO操作菜单代码一行没动。6.5 扩展到OLED和TFT如果你以后想换屏这套菜单框架依然有用。OLED的接线和12864兼容性不错只是驱动函数换成OLED的地址映射换成OLED的。TFT触摸屏则复杂一些菜单框架可以保留但绘制层要重写。最后再分享一个实用小技巧调试12864这种带字库屏时准备一个自己做的简单测试函数在LCD_Init之后立刻执行比如发一个“你好”和几个数字。这样不管后面怎么改菜单逻辑一旦屏出问题回退到这一行就能确认是屏的问题还是菜单逻辑的问题。另一个心得是把菜单显示的内容和业务逻辑彻底分开。菜单模块只管“显示什么、选中什么、执行什么”不关心具体业务是怎么实现的。比如“温度上限”菜单项它只要在确认时调用一个SetTempMax函数至于这个函数内部是去操作ADC还是PWM菜单模块一概不关心。这样以后加功能就是往菜单表里加一个条目的事不用动框架代码。这整套代码从底层GPIO配置到菜单状态机我前后跑了大概两个晚上中间踩过编码的坑、地址映射的坑、按键抖动的坑都一一解决了。现在这板子已经跑了一个月24小时连续开机没出过问题稳定性还挺满意。希望对正在搞STM32和12864的朋友有帮助也欢迎大家在评论区交流菜单框架的改进方案。本文还有配套的精品资源点击获取
返回列表