STM32简易多级菜单(数组查表法)

本文介绍了如何在STM32单片机上使用数组查表方式实现多级菜单的显示,通过定义结构体和数组来管理菜单之间的跳转关系,并结合OLED屏幕和U8g2图形库展示菜单内容。详细解析了菜单结构体的各个字段意义,以及按键操作下的页面切换逻辑。同时展示了实际的代码示例和测试效果。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

单片机开发中,有时会用到屏幕来显示内容,当需要逐级显示内容时,就需要使用多级菜单的形式了。

1 多级菜单

多级菜单的实现,大体分为两种设计思路:

  • 通过双向链表实现
  • 通过数组查表实现

总体思路都是把菜单的各个界面联系起来,可以从上级菜单跳到下级菜单,也可从下级菜单返回上级菜单。

数组查表的方式比较简单,易于理解,本篇就来使用数组查表发在STM32上实现多级菜单的显示。

2 代码实现

2.1 数组查表

首先需要定义一个结构体

typedef struct
{
	uchar current;
	uchar up;//向上翻索引号
	uchar down;//向下翻索引号
	uchar enter;//确认索引号
	void (*current_operation)();
} key_table;
  • current:当前页面的索引号
  • up:按下“向上翻“按钮后要跳转到的页面索引号
  • down:按下“向下翻“按钮后要跳转到的页面索引号
  • enter:按下“确认“按钮后要跳转到的页面索引号
  • current_operation:当前页面的索引号要执行的显示函数,这是一个函数指针

注意:对于菜单显示的操作,用到了3个按键,分别是向下、向下和确认,如果单片机上的IO资源较为紧张,还可以把“向上翻”按钮省去,只通过“向下翻”按钮来实现循环访问,对应的结构体也可以去掉该成员。

然后定义一个表,用来定义各个页面间如何跳转

key_table table[30]=
{
	//第0层
	{0,0,0,1,(*fun_0)},
	
    //第1层
	{1,4,2, 5,(*fun_a1)},
	{2,1,3, 9,(*fun_b1)},
	{3,2,4,13,(*fun_c1)},		
	{4,3,1, 0,(*fun_d1)},
	
    //第2层
	{5,8,6,17,(*fun_a21)},					
	{6,5,7,18,(*fun_a22)},
	{7,6,8,19,(*fun_a23)},						                	
	{8,7,5, 1,(*fun_a24)},
	
	{ 9,12,10,20,(*fun_b21)},					
	{10, 9,11,21,(*fun_b22)},
	{11,10,12,22,(*fun_b23)},						                	
	{12,11, 9, 2,(*fun_b24)},
	
	{13,16,14,23,(*fun_c21)},					
	{14,13,15,24,(*fun_c22)},				                	
	{15,14,16,25,(*fun_c23)},				                	
	{16,15,13, 3,(*fun_c24)},
	
    //第3层
	{17,17,17,5,(*fun_a31)},			                	
	{18,18,18,6,(*fun_a32)},		                	
	{19,19,19,7,(*fun_a33)},
	
	{20,20,20, 9,(*fun_b31)},				                	
	{21,21,21,10,(*fun_b32)},			                	
	{22,22,22,11,(*fun_b33)},
	
	{23,23,23,13,(*fun_c31)},			                	
	{24,24,24,14,(*fun_c32)},			                	
	{25,25,25,15,(*fun_c33)},								
};

这里解释一下该表是如何工作的:

  • 此表,表示了4级菜单的显示关系(注意第0层其实只是一个欢迎界面)
  • 第一层菜单,只有4个选项,因此这里只列了4行(注意最后一个选项用作返回上一级,无实际内容含义)
  • 第二层菜单,就是对第一层菜单中的3个实际的选项进行进一步的介绍,每种介绍又有4个子项(注意最后一个选项也是用作返回上一级,无实际内容含义),因此,这里的第二层菜单列了3x4=12行
  • 第三层菜单,又是对第二层菜单中的子项进行进一步的介绍(3个分类,每类有3个子项),所以第三层菜单列了9行
  • 注意数组中每一行的第1个数组,是索引号,先列举一个实际的例子进行分析:

上图就是一个实际的4级菜单要显示的内容,每个条目前,标记了索引号(0~25),即对应数组在定义的索引号。

比如数组关于第0层和第1层的定义:

//第0层
{0,0,0,1,(*fun_0)},

//第1层
{1,4,2, 5,(*fun_a1)},
{2,1,3, 9,(*fun_b1)},
{3,2,4,13,(*fun_c1)},		
{4,3,1, 0,(*fun_d1)},
  • 先看第一行:索引是0,显示欢迎界面;后面的两个0表示此时按“上翻”和“下翻”无效,继续显示欢迎界面;再后面的1表示按下“确认”按钮后,跳转到索引1处(即显示第1级目录,且指向第1级的第1个子项);最后是此索引要显示的具体内容,fun_0就是控制屏幕显示欢迎界面
  • 再看第二行:索引是1,显示第1级目录,且指向第1级的第1个子项(天气);后面的4表示此时按“上翻”跳转到索引4,即显示第1级目录,且指向第1级的第4个子项(Return);再后面的2表示此时按“下翻”跳转到索引2,即显示第1级目录,且指向第1级的第2个子项(音乐);再后面的5表示按下“确认”按钮后,跳转到索引5处(即显示第2级目录,且指向第2级的第1个子项-杭州);最后是此索引要显示的具体内容,fun_a1就是控制屏幕显示第1级目录,且指向第1级的第1个子项(天气)
  • 其它行的含义与之类似

通过分析,不难发现,这些数组在空间上的关系:

对于菜单的最底层,因为没有上翻和下翻的功能需求,因此每行的前3个数字都是当前的索引号:

//第3层
{17,17,17,5,(*fun_a31)},			                	
{18,18,18,6,(*fun_a32)},		                	
{19,19,19,7,(*fun_a33)},

{20,20,20, 9,(*fun_b31)},				                	
{21,21,21,10,(*fun_b32)},			                	
{22,22,22,11,(*fun_b33)},

{23,23,23,13,(*fun_c31)},			                	
{24,24,24,14,(*fun_c32)},			                	
{25,25,25,15,(*fun_c33)},	

2.2 具体的显示函数

对于函数要显示的具体内容,根据自己的实现需要显示即可。

这里我使用的是OLED屏幕,借助U8g2图形库进行内容显示,以下是部分显示示例:

/*********第1层***********/
void fun_a1()   
{	
	u8g2_DrawStr(&u8g2,0,16,">");
	u8g2_DrawStr(&u8g2,16,16,"[1]Weather");
	u8g2_DrawStr(&u8g2,16,32,"[2]Music");
	u8g2_DrawStr(&u8g2,16,48,"[3]Device Info");
	u8g2_DrawStr(&u8g2,16,64,"<--");																					
}

void fun_b1()   
{	
	u8g2_DrawStr(&u8g2,0,32,">");
	u8g2_DrawStr(&u8g2,16,16,"[1]Weather");
	u8g2_DrawStr(&u8g2,16,32,"[2]Music");
	u8g2_DrawStr(&u8g2,16,48,"[3]Device Info");
	u8g2_DrawStr(&u8g2,16,64,"<--");																						
}

void fun_c1()     
{	
	u8g2_DrawStr(&u8g2,0,48,">");
	u8g2_DrawStr(&u8g2,16,16,"[1]Weather");
	u8g2_DrawStr(&u8g2,16,32,"[2]Music");
	u8g2_DrawStr(&u8g2,16,48,"[3]Device Info");
	u8g2_DrawStr(&u8g2,16,64,"<--");																					
}

void fun_d1()     
{	
	u8g2_DrawStr(&u8g2,0,64,">");
	u8g2_DrawStr(&u8g2,16,16,"[1]Weather");
	u8g2_DrawStr(&u8g2,16,32,"[2]Music");
	u8g2_DrawStr(&u8g2,16,48,"[3]Device Info");
	u8g2_DrawStr(&u8g2,16,64,"<--");																							
}

/*********第2层***********/
void fun_a21()     
{	
	u8g2_DrawStr(&u8g2,0,16,">");
	u8g2_DrawStr(&u8g2,16,16,"* HangZhou");
	u8g2_DrawStr(&u8g2,16,32,"* BeiJing");
	u8g2_DrawStr(&u8g2,16,48,"* ShangHai");
	u8g2_DrawStr(&u8g2,16,64,"<--");																						
}
//省略...

2.3 按键切换页面

页面的切换,这里里简单的按钮轮询为例,比如初始显示欢迎界面的状态下,按下不同按键后,通过数组查表,确定要跳转到的索引号,然后根据索引号,通过函数指针执行索引号对应的显示函数,即实现了一次页面切换。

然后,就是在新的页面状态,收到下一个按钮指令,再切换到下一个显示状态。

void (*current_operation_index)(); //定义一个函数指针

//...
while(1)
{
    if((KEY1==0)||(KEY2==0)||(KEY3==0))
    {
        delay_ms(10);//消抖
        if(KEY1==0)
        {
            func_index = table[func_index].up;    //向上翻
            while(!KEY1);//松手检测
        }
        if(KEY2==0)
        {
            func_index = table[func_index].down;    //向下翻
            while(!KEY2);
        }
        if(KEY3==0)
        {
            func_index = table[func_index].enter;    //确认
            while(!KEY3);
        }
    }	

    if (func_index != last_index)
    {
        current_operation_index = table[func_index].current_operation;

        u8g2_ClearBuffer(&u8g2); 
        (*current_operation_index)();//执行当前操作函数
        u8g2_SendBuffer(&u8g2);

        last_index = func_index;
    }
}

3 演示

测试效果如下:
https://www.bilibili.com/video/BV1r5411R7eA?share_source=copy_web

4 总结

本篇介绍了一种简易的多级菜单的显示方法,本质是通过数组查表,实现各级菜单的各个页面(状态)的切换(跳转),并在STM32上编程实现,通过OLED屏幕,以及借助U8g2图形库,测试了多级菜单的显示功能。

### STM32 OLED 菜单实现教程 #### 实现概述 在嵌入式开发领域,STM32作为一款高性能的微控制器,常用于驱动OLED显示屏并实现复杂的交互功能。通过结合硬件接口和软件逻辑,可以轻松构建多级菜单系统。以下是基于STM32和OLED显示屏的菜单实现方案及其示例代码。 --- #### 硬件连接配置 为了实现OLED多级菜单,需正确连接STM32与OLED显示屏及相关外设(如旋转编码器)。具体接线方式如下: | **组件** | **引脚定义** | |-----------------|----------------------| | OLED显示屏 | `GND` → 地 | | | `VCC` → 3.3V | | | `SCL` → PB8 (I2C) | | | `SDA` → PB9 (I2C) | | 旋转编码器模块 | `SW` → PB11 | | | `DT` → PB10 | | | `CLK` → PB1 | 上述硬件连接参考了实际应用中的典型设计[^5]。 --- #### 多级菜单的核心原理 多级菜单的本质在于状态机的设计。每级菜单对应一种状态,用户输入(如按键或旋钮)触发状态间的切换。以下为两种常见的实现方: 1. **数组查表** 使用数组存储各菜单项的内容及对应的子菜单入口。这种方简单易懂,适合初学者快速上手[^2]。 2. **结构体索引** 定义结构体表示菜单节点,包含当前菜单名称、子菜单指针及其他属性。该方灵活性更高,尤其适用于复杂界面需求[^3]。 --- #### 示例代码:基于数组查表多级菜单 ```c #include "stm32f1xx_hal.h" #include "oled.h" // 定义菜单内容 const char *menu_main[] = {"设置", "查询", "退出"}; const char *menu_setting[] = {"亮度", "对比度", "返回"}; uint8_t menu_level = 0; // 当前菜单级别 uint8_t selected_item = 0; // 当前选中项 void display_menu(const char **menu, uint8_t count) { for (int i = 0; i < count; i++) { if (i == selected_item) { OLED_ShowString(0, i * 10, "> ", FONT_16X16); } else { OLED_ShowString(0, i * 10, " ", FONT_16X16); } OLED_ShowString(20, i * 10, menu[i], FONT_16X16); } } void update_display() { switch (menu_level) { case 0: display_menu(menu_main, sizeof(menu_main)/sizeof(char*)); break; case 1: display_menu(menu_setting, sizeof(menu_setting)/sizeof(char*)); break; } } int main(void) { HAL_Init(); SystemClock_Config(); OLED_Init(); // 初始化OLED屏幕 OLED_Clear(); // 清屏 while (1) { update_display(); // 模拟按钮事件处理 if (HAL_GPIO_ReadPin(BUTTON_PIN)) { // 上移 selected_item--; if (selected_item < 0) selected_item = 0; } if (HAL_GPIO_ReadPin(SELECT_BUTTON_PIN)) { // 进入选项 if (menu_level == 0 && selected_item == 2) { break; // 返回上级或退出 } else { menu_level++; // 切换至下一级菜单 } } } } ``` 上述代码展示了如何通过简单的数组管理两级菜单,并动态更新显示内容。 --- #### 示例代码:基于结构体索引多级菜单 ```c typedef struct MenuItem { const char *name; // 菜单项名称 void (*action)(void); // 动作回调函数 } MenuItem; MenuItem setting_items[] = { {"亮度调节", brightness_adjust}, {"对比度调节", contrast_adjust}, {"返回", back_to_main} }; MenuItem main_items[] = { {"设置", enter_settings}, {"查询", query_data}, {"退出", exit_system} }; MenuItem *current_menu = main_items; void show_current_menu(MenuItem *items, int count) { for (int i = 0; i < count; ++i) { OLED_ShowString(0, i * 10, items[i].name, FONT_16X16); } } int main(void) { HAL_Init(); SystemClock_Config(); OLED_Init(); OLED_Clear(); while (1) { show_current_menu(current_menu, sizeof(main_items)/sizeof(MenuItem)); if (button_pressed()) { current_menu[selected_index].action(); } } } ``` 此代码片段采用结构体封装菜单数据,便于扩展更多功能。 --- #### 注意事项 1. 在初始化阶段务必确认GPIO引脚配置无误,避免因错误映射导致通信失败[^4]。 2. 若遇到花屏现象,可能是时序参数未匹配目标器件规格,建议查阅官方资料调整相关寄存器设置。 3. 字符串长度应适配OLED分辨率,超出部分可能导致截断或错位。 ---
评论 58
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值