前言:本篇教程,参考B站,小黑学长,连接最后,请勿搬运,仅供学习
复刻必备
物料清单:面包板+STM32最小系统板+0.96寸oled+杜邦线
本篇教程,会将这种简单方式的多级菜单,容易出错,关键的代码进行拆分讲解,理解为什么这么用,这里的话,oled的驱动程序用的是江科大的模版,链接放在下面,可以下载使用。
资料下载
代码详解
首先按键需求是,两个按键一个用来选择菜单,一个用来判断执行功能。
#include "Key.h"
#include "stm32f10x.h"
#include "Delay.h"
#include "OLED.H"
int Key1_Flag = 0;void Key_Init(void) // 初始化按键引脚
{
GPIO_InitTypeDef GPIO_InitStruct;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 开时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);
// 初始化 GPIOA
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP; // 根据需要设置为合适的模式
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9 | GPIO_Pin_8;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_10MHz;
GPIO_Init(GPIOA, &GPIO_InitStruct);
GPIO_SetBits(GPIOA, GPIO_Pin_8);
GPIO_SetBits(GPIOA, GPIO_Pin_9);
// 初始化 GPIOC
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP; // 设置为推挽输出
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_13; // 仅针对 PC13
GPIO_Init(GPIOC, &GPIO_InitStruct);
// 设置为高电平
GPIO_ResetBits(GPIOC, GPIO_Pin_13);
}
int Key1_Scanf(void) {
static int Key1_Flag = 0; // 这里使用 static 修饰 Key1_Flag 让其保持上次的值,不要每次调用函数都是 0
if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_8) == RESET) {
Delay_ms(1000); // 去抖动
if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_8) == RESET) {
Key1_Flag = (Key1_Flag + 1) % 4; // 更新状态
return Key1_Flag; // 返回当前状态
}
}
return Key1_Flag; // 返回当前状态(未改变)
}
int Key2_Scanf(void)
{
//static int Key2_Flag= 0 ;
if(GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_9) == RESET)
{
Delay_ms(100);
if(GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_9) == RESET)
{
//Key2_Flag= (Key2_Flag+1)%4;//x取值 1 或者 2
//OLED_ShowNum(0, 28, Key1_Flag, 1, OLED_6X8);
return Key1_Flag;
}
}
return 0;
}
代码里面,初始化了两个按键引脚,以及pc13用来控制板载led,通过判断led状态来观察函数执行,这里初始化需要注意的是,引脚设置为 推挽输出,而不是复用推挽输出,前者mcu可以直接控制io引脚到低电平,后者则是mcu片上外设,来控制io引脚,如果配置为后者,大概率直接控制设置电平时没有效果。
Key1_Scanf(void)函数里面,有讲究的地方有两句。
static int Key1_Flag = 0;
在函数里面局部变量使用了,static来修饰,这里选择局部变量是为了节省ram空间,使用static来修饰标志位变量,是为了每次调用这个函数,变量能够保留上次的值,改变它的生命周期,让他一直活着。
int Key1_Flag = 0;
如果改成这个样子,每次调用这个函数,标志位变量都会被赋值为0,函数结束变量声明周期结束,也就死了。
Key1_Flag = (Key1_Flag + 1) % 4; // 更新状态
另外需要注意的是,这句限制了变量的取值范围,在 1 2 3 三个值之间一直循环,这句很美,如果没有static修饰这个变量,实际取值只有 1 没有其他的取值。
Key2_Scanf(void)函数里面,有讲究的地方有两句。
负责执行函数功能的按键,这里需要注意的是返回值是光标的位置值,根据这个值,来判断执行什么功能,这一点很重要,是通过这个值来判断的。
return Key1_Flag;
这里返回 0 是因为函数类型是 int 类型,也就是说必须强制有 int 类型的返回值,也就是说没有进入 if语句 里面也得有返回值。这个 0 就是返回值。
return 0;
然后写完按键之后,开始写主菜单部分,这一部分把显示功能的部分放在while循环外边,然后再while循环里面,一直读按键的返回值,判断按键按下了没有选择的是那个工能,同时读按键2,根据按键2返回的值,来决定执行那个功能。
void menu(void)
{
int menu_flag;
int decide_flag;
int exit_flag = 0; // 用于控制主菜单循环是否退出
OLED_ShowString(30, 10, "开灯", OLED_8X16);
OLED_ShowString(30, 26, "关灯", OLED_8X16);
OLED_ShowString(30, 42, "开灯控制", OLED_8X16);
OLED_Update();
while (!exit_flag) // exit_flag 为 0 时循环继续
{
menu_flag = Key1_Scanf();
decide_flag = Key2_Scanf();
switch(menu_flag)
{
case 1:
OLED_ClearArea(22, 42, 8, 16);
OLED_ClearArea(22, 26, 8, 16);
OLED_ClearArea(22, 10, 8, 16);
OLED_ShowString(22, 10, "*", OLED_8X16);
OLED_Update();
break;
case 2:
OLED_ClearArea(22, 42, 8, 16);
OLED_ClearArea(22, 26, 8, 16);
OLED_ClearArea(22, 10, 8, 16);
OLED_ShowString(22, 26, "*", OLED_8X16);
OLED_Update();
break;
case 3:
OLED_ClearArea(22, 42, 8, 16);
OLED_ClearArea(22, 26, 8, 16);
OLED_ClearArea(22, 10, 8, 16);
OLED_ShowString(22, 42, "*", OLED_8X16);
OLED_Update();
break;
}
if (decide_flag == 1)
{
GPIO_SetBits(GPIOC, GPIO_Pin_13);
}
if (decide_flag == 2)
{
GPIO_ResetBits(GPIOC, GPIO_Pin_13);
}
if (decide_flag == 3)
{
Key1_Flag = 0;
exit_flag = sub_menu(); // 检查子菜单的返回值
}
}
}
需要注意的是,光标问题。
Key1_Flag = 0;
进入子菜单的这行代码,是为了将光标的值清零,如果没有清零光标的值,在进入子菜单还是在第三行。
exit_flag = sub_menu();
这里将子菜单的放在一个函数里面,执行当执行到子菜单的退出功能的时候,会直接跳出函数,exit_flag这个标志位会被赋值为1,这个时候while循环条件会不成立,只有下次再main函数的循环中被调用在能执行。
子菜单部分
int sub_menu(void)
{
int menu_flag;
int decide_flag;
OLED_Clear();
OLED_Update();
Delay_ms(1000);
OLED_ShowString(30, 10, "开灯", OLED_8X16);
OLED_ShowString(30, 26, "关灯", OLED_8X16);
OLED_ShowString(30, 42, "退出", OLED_8X16);
OLED_Update();
while (1)
{
menu_flag = Key1_Scanf();
decide_flag = Key2_Scanf();
switch(menu_flag)
{
case 1:
OLED_ClearArea(22, 42, 8, 16);
OLED_ClearArea(22, 26, 8, 16);
OLED_ClearArea(22, 10, 8, 16);
OLED_ShowString(22, 10, "*", OLED_8X16);
OLED_Update();
break;
case 2:
OLED_ClearArea(22, 42, 8, 16);
OLED_ClearArea(22, 26, 8, 16);
OLED_ClearArea(22, 10, 8, 16);
OLED_ShowString(22, 26, "*", OLED_8X16);
OLED_Update();
break;
case 3:
OLED_ClearArea(22, 42, 8, 16);
OLED_ClearArea(22, 26, 8, 16);
OLED_ClearArea(22, 10, 8, 16);
OLED_ShowString(22, 42, "*", OLED_8X16);
OLED_Update();
break;
}
if (decide_flag == 1)
{
GPIO_SetBits(GPIOC, GPIO_Pin_13);
}
if (decide_flag == 2)
{
GPIO_ResetBits(GPIOC, GPIO_Pin_13);
}
if (decide_flag == 3)
{
OLED_Clear();
OLED_Update();
Key1_Flag = 0;
return 1; // 请求退出主菜单的while循环
}
}
}
这里子菜单其实和主菜单的套路一样,但是需要注意的是在执行子菜单的退出功能的时候,需要将光标清零,也就是下面这句代码。
Key1_Flag = 0;
如果没有这句代码,当光标执行子菜单的退出的时候,退出到主菜单,因为mcu执行速度很快,当时手还在按执行按键,这个时候又进入子菜单了,这个时候就会一直卡在子菜单。
这个就是基本的菜单框架,如果有需要可以根据这个框架去改出来,自己需要的菜单结构。
子菜单最重要的一句
return 1; // 请求退出主菜单的while循环
子菜单功能3,是退出菜单,这里退出直接使用return这个关键字,去终结子菜单while的执行,不让子菜单跑了,让主菜单去跑,如果这里没有 return 这个关键字,直接调用menu(),是不对的,你会在子菜单的基础上,显示主菜单,到时候两个菜单会重叠的。
这里就老老实实把子菜单就掉就行了。
实现效果
WeChat_20241025201928
欢迎指正,希望对你,有所帮助!!!