0.96寸oled模块,多级菜单教程,用最简单的方式写出多级菜单,教程详解!!!让嵌入式小白变高手系列教程 一

                前言:本篇教程,参考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

                                欢迎指正,希望对你,有所帮助!!!

       禁止搬运,违者举报,违者必究,感谢理解。

上一篇:为何即使是加密的安全信道,依然要对传输文件进行加密呢(附混合加密方法)


下一篇:【重学 MySQL】七十七、掌握存储过程与存储函数的查看、修改与删除技巧-查看存储过程与存储函数