1. 项目概述
这个OLED多级菜单交互项目特别适合刚接触嵌入式开发的新手练手。我当年在学校实验室第一次用OLED屏做菜单时,折腾了整整两天才搞明白怎么处理按键事件和页面跳转逻辑。现在回头看,其实只要掌握几个关键点,两小时就能实现基础功能。
OLED屏幕在嵌入式领域应用非常广泛,从智能手表到工业控制面板都能看到它的身影。相比LCD屏,OLED具有自发光、高对比度、超薄等优势,特别适合低功耗设备。而菜单系统作为人机交互的核心组件,其实现方式直接影响用户体验。
2. 硬件准备与接线
2.1 所需材料清单
- 0.96寸OLED显示屏(SSD1306驱动)
- STM32开发板(以F103C8T6为例)
- 4个轻触按键
- 杜邦线若干
- 面包板(可选)
提示:OLED屏建议选择I2C接口的,比SPI接口节省IO口。市面上常见的4针OLED(GND、VCC、SCL、SDA)基本都是I2C通信。
2.2 硬件连接示意图
code复制OLED STM32
GND -> GND
VCC -> 3.3V
SCL -> PB6(I2C1_SCL)
SDA -> PB7(I2C1_SDA)
按键1 -> PA0 (上)
按键2 -> PA1 (下)
按键3 -> PA2 (确认)
按键4 -> PA3 (返回)
3. 软件架构设计
3.1 菜单数据结构设计
采用链表结构存储菜单项,每个节点包含:
c复制typedef struct MenuItem {
char title[16]; // 菜单显示文本
MenuItem *parent; // 父级菜单指针
MenuItem *child; // 子菜单指针
MenuItem *next; // 同级下一项指针
void (*action)(void);// 点击执行函数
} MenuItem;
这种结构可以实现无限级菜单嵌套,实测在STM32上即使有上百个菜单项内存占用也不到2KB。
3.2 状态机设计
使用有限状态机(FSM)管理菜单流程:
mermaid复制graph TD
A[初始状态] --> B[主菜单]
B -->|按键下| C[下级菜单]
B -->|按键确认| D[执行功能]
C -->|按键返回| B
实际代码中我用枚举实现了这个状态机:
c复制typedef enum {
MENU_MAIN,
MENU_SUB,
FUNC_EXEC
} MenuState;
4. 核心代码实现
4.1 OLED驱动移植
推荐使用现成的SSD1306驱动库,以u8g2为例:
c复制#include "u8g2.h"
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0);
void oled_init() {
u8g2.begin();
u8g2.setFont(u8g2_font_wqy12_t_gb2312); // 中文字体
u8g2.setDrawColor(1);
}
注意:如果显示乱码,需要检查字体文件是否包含中文。我常用的解决方案是使用自定义的12px点阵字体。
4.2 按键扫描逻辑
采用定时器中断实现按键消抖:
c复制#define KEY_UP GPIO_PIN_0
#define KEY_DOWN GPIO_PIN_1
#define KEY_ENTER GPIO_PIN_2
#define KEY_BACK GPIO_PIN_3
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
static uint8_t key_state[4] = {0};
// 扫描4个按键
for(int i=0; i<4; i++) {
uint16_t pin = (i==0)?KEY_UP:(i==1)?KEY_DOWN:(i==2)?KEY_ENTER:KEY_BACK;
if(HAL_GPIO_ReadPin(GPIOA, pin) == GPIO_PIN_RESET) {
if(key_state[i] < 20) key_state[i]++;
if(key_state[i] == 20) {
key_event(i); // 触发按键事件
}
} else {
key_state[i] = 0;
}
}
}
4.3 菜单渲染优化
采用局部刷新策略提升流畅度:
c复制void menu_refresh(MenuItem *current) {
static MenuItem *last = NULL;
if(current != last) {
u8g2.clearBuffer();
// 绘制标题栏
u8g2.drawUTF8(0,12, current->parent?current->parent->title:"主菜单");
u8g2.drawHLine(0,14,128);
// 绘制菜单项
MenuItem *p = current;
for(int i=0; i<4 && p!=NULL; i++, p=p->next) {
u8g2.drawUTF8(5, 30+i*12, p->title);
if(i == cursor_pos)
u8g2.drawTriangle(0,24+i*12, 3,30+i*12, 0,36+i*12);
}
u8g2.sendBuffer();
last = current;
}
}
5. 实用技巧与避坑指南
5.1 内存优化技巧
在资源有限的MCU上,可以采用这些优化方案:
- 使用
const将菜单文本存入Flash - 用
uint8_t代替int存储菜单索引 - 禁用u8g2的缓冲模式(直接驱动模式)
5.2 常见问题排查
-
显示花屏
- 检查I2C地址是否正确(通常0x3C或0x3D)
- 确认上拉电阻已接(4.7KΩ)
- 降低I2C时钟频率(尝试100kHz)
-
按键不响应
- 确认GPIO模式设置为输入上拉
- 检查消抖时间是否合适(建议20ms)
- 确保中断优先级配置正确
-
菜单卡死
- 检查链表指针是否闭环
- 添加看门狗定时器复位
- 在状态切换处添加日志输出
6. 功能扩展思路
6.1 动态菜单生成
通过回调函数实现动态菜单:
c复制typedef MenuItem* (*GetChildFunc)(MenuItem* parent);
struct MenuItem {
// ...原有字段
GetChildFunc get_child; // 动态获取子菜单
};
6.2 动画过渡效果
在菜单切换时添加滑动动画:
c复制void menu_transition(MenuItem *from, MenuItem *to, int dir) {
for(int i=0; i<128; i+=5) {
u8g2.clearBuffer();
u8g2.setClipWindow(dir?i:0, 0, dir?128-i:i, 64);
draw_menu(from);
u8g2.setClipWindow(dir?0:i, 0, dir?i:128-i, 64);
draw_menu(to);
u8g2.sendBuffer();
}
}
6.3 多语言支持
使用结构体存储多语言文本:
c复制typedef struct {
char *zh;
char *en;
} MultiLangText;
MultiLangText menu_titles[] = {
{"设置", "Settings"},
{"时间", "Time"},
// ...
};
这个项目最让我惊喜的是,当我把这套菜单系统应用到实际产品中后,客户反馈操作体验比之前用的商业方案更流畅。后来我才明白,不是代码有多高明,而是针对硬件特性做了足够多的优化。比如在STM32F103上,局部刷新能让菜单响应速度提升3倍以上。
