当前位置:   article > 正文

STM32+OLED屏多级菜单显示(三)_stm32 oled菜单

stm32 oled菜单

        前面两章实现了OLED屏幕显示的基本功能,这一章就做一个多级菜单显示功能

        单片机选择STM32F103C8T6最小系统板,OLED屏选择0.96寸OLED显示器,除了单片机和OLED屏之外,还需要三个按键(下一位键、确认键和返回键),当然一个按键也可以(单击、双击和长击完成),为了提高可玩性这里就只使用一个按键。

        1.1   STM32+OLED屏初始化(一) 

        1.2  STM32+OLED屏显示字符串、汉字、图片(二)

        1.3  STM32+OLED屏多级菜单显示(三)

        1.4  STM32+OLED屏(软件IIC+位带+帧缓冲区)刷新速率优化(四) 

1.多级菜单

        多级菜单是一种用户界面设计,它将信息和选项组织为层次结构,使得用户可以快速找到所需的选项。多级菜单的实现基于两种方案索引法树结构法,索引法阅读性好,扩展性不错,查找性最优,但是比较占用内存,并且一旦选项过多就会造成逻辑混乱

        下面展示使用索引法设计的多级目录(黑色的板子是底板,可以理解为面包板),首先是一级目录:

        其次,二级目录:

        最后,三级目录:

2.typedef用法

        在使用多级界面之前,首先要了解一些基础知识。typedef是C/C++中的一个关键字,用来给一个已存在的数据类型(比如int、float、struct等)取一个新的别名(自定义数据类型)。通过typedef声明的别名,可以像原类型一样被使用,但具有更直观、更易读的含义,从而提高代码可读性和可维护性。typedef的使用格式为:

  1. typedef 原类型 新类型
  2. 举个例子,如:
  3. typedef int MyInt;
  4. MyInt a = 10;//等效int a = 10

        再看这个例子,将一个结构体定义为Point类型的别名,使用时就可以使用Point代替这个结构体。这个例子中的第三行代码创建了一个Point类型的结构体变量p,并为其成员变量赋值,除此之外,还有更多的定义方式,如数组、函数指针、enum等

  1. typedef struct {
  2. int x;
  3. int y;
  4. } Point;
  5. Point p;
  6. p.x = 1;
  7. p.y = 2;

2.函数指针

        简单来说,函数指针是指向函数的指针变量,它存储了函数的入口地址,可以用来调用函数。在C语言中,函数指针的定义格式为:返回值类型 (*函数指针名称)(参数列表)。

        在下面的例子中,定义了一个函数指针func_ptr,它指向同样返回int类型的函数add。将add函数的地址赋给了函数指针func_ptr后,就可以使用func_ptr来调用add函数,和直接使用add(3,5)是等效的

  1. #include <stdio.h>
  2. int add(int a, int b)
  3. {
  4. return a + b;
  5. }
  6. int main()
  7. {
  8. int result;
  9. int (*func_ptr)(int, int); // 定义函数指针
  10. func_ptr = add; // 将函数的地址赋给函数指针
  11. result = func_ptr(3, 5); // 使用函数指针调用函数
  12. printf("%d\n", result); // 输出结果:8
  13. return 0;
  14. }

3.定义多级菜单数据显示类型

        前面的基础知识讲完了,现在正式开始使用索引法实现多级菜单的第一步,定义多级菜单的数据类型,通过typedef声明的结构体设计界面菜单功能的数据类型,当前索引序号、三个按键、当前执行的函数指针,索引序号表示界面页码,要进入界面就要输入它的索引序号(就像是在宾馆房间编号,要进入房间就要知道它的房间编号),通过按键赋值索引序号达到跳转的目的(给房间编号的前台服务员),最后是执行的函数指针指向要执行的函数(可以理解为通往房间的路径)

  1. typedef struct
  2. {
  3. uint8_t CurrentNum; //当前索引序号:页码
  4. uint8_t Enter; //确认键
  5. uint8_t Next; //下一个
  6. uint8_t Return; //返回键
  7. void (*Current_Operation)(void);//当前操作(函数指针)
  8. }Menu_table_t;

        根据上面自定义的数据类型,定义一个一维数组taskTable[ ],一维数组中的元素的数据类型就是Menu_table_t(就像我定义一个int array[] = { 0 ,},0的数据类型就是int),taskTable的元素中的元素与自定义数据类型里的数据类型统一对应,就像最后一个函数指针元素指向菜单界面函数,简单说明一下,taskTable[0] = {0, 1, 0, 0, Menu_Innterface},

  1. //界面调度表 //假定 int array[] = { 0, };
  2. Menu_table_t taskTable[] =
  3. {
  4. //菜单界面函数 -- 一级界面
  5. {0, 1, 2, 3, Menu_Interface},
  6. };
  7. /**
  8. * @brief 菜单界面函数
  9. * @param 无
  10. * @retval 无
  11. */
  12. void Menu_Interface(void)
  13. {
  14. char buff[50];
  15. RTC_Get_StdTime(RTC_GetCounter());
  16. sprintf(buff,"%0.2d:%0.2d:%0.2d",RTC_CLOCK.hour, RTC_CLOCK.min, RTC_CLOCK.sec);
  17. OLED_ShowString(4, 10, 24, buff);
  18. sprintf(buff,"Date:%0.4d-%0.2d-%0.2d",RTC_CLOCK.year,RTC_CLOCK.mon,RTC_CLOCK.day);
  19. OLED_ShowString(1, 6, 16, buff);
  20. }

        所以,数据类型就是这样对应的,第一个为索引序号,中间三个为按键,最后一个执行函数,将它的首地址赋值给指针函数

4.菜单逻辑顺序表

        通过上述的对应关系,可以制定一张菜单逻辑顺序,通过中间的三个按键将索引序号赋值给任务调度序号,当在一级界面时,按下确认键就通过索引序号把务调度序号切换为1,进入到二级界面的第一个元素了,按下下一位键和返回键

  1. uint8_t taskIndex = 0; //任务调度序号
  2. //任务调度表
  3. Menu_table_t taskTable[] =
  4. {
  5. //菜单界面函数 -- 一级界面
  6. {0, 1, 0, 0, Menu_Interface},
  7. //功能界面函数 -- 二级界面
  8. {1, 4, 2, 0, Function_Interface1},
  9. {2, 5, 3, 0, Function_Interface2},
  10. {3, 6, 1, 0, Function_Interface3},
  11. //功能设置界面函数 -- 三级界面
  12. {4, 4, 4, 1, Function_Interface4},
  13. {5, 5, 5, 2, Function_Interface5},
  14. {6, 6, 6, 3, Function_Interface6},
  15. };

        用三张张图简单的说明一下,第一张图,当前页面为0,按下确认键跳到索引序号为1的元素

 第二张图,当前页面为1,按下确认键跳到索引序号为4的元素

第三张图,当前页面为6,按下返回键跳到索引序号为3的元素 

5.按键切换索引序号

        编写一个按键值获取函数,按键将索引序号赋值给任务调度序号,任务调度表根据任务调度序号执行运行界面,前面有说过,最后一个元素只是指向运行函数的指针(也就是路径),真正的执行需要解引用,也就是最后的执行函数

  1. keyval = Keyval_Scan();
  2. if(keyval == 2) {//双击
  3. taskIndex = taskTable[taskIndex].Enter;//双击表示确认键
  4. OLED_Clear();
  5. }
  6. else if(keyval == 1) {//单击
  7. taskIndex = taskTable[taskIndex].Next;//单击表示下一位键
  8. }
  9. else if(keyval == 10) {//长击
  10. taskIndex = taskTable[taskIndex].Return;//长击表示返回键
  11. OLED_Clear();
  12. }
  13. taskTable[taskIndex].Current_Operation();//执行函数

       按键获取函数,按下按键30ms的按键抖动,2s内松开视为有效单击,0.5s后无按键按下,结算总共按下几次有效单击,单击返回1,双击返回2,三击返回3,长击返回10;用不到三击以上的按键,而且十击与长按都返回10,之后可能会出巨大的事故问题

  1. uint8_t Keyval_Scan(void)
  2. {
  3. static uint16_t key_state, key_time, key_cnt;
  4. uint8_t key_return = 0;
  5. switch(key_state) {
  6. case 0://按键状态0:判断有无按键按下
  7. if(KEY_STATA0) {
  8. key_time = 0;//按键按下时,按键计时器清0
  9. key_state = 1;//按键状态置1
  10. }
  11. break;
  12. case 1://按键状态1:按键消抖
  13. if(KEY_STATA0) {
  14. delay_ms(1);
  15. if(key_time++ >= 30)
  16. key_state = 2;//当按键按下超过30ms时,进入按键单击状态
  17. }
  18. else key_state = 0;//松开按键,视为误触
  19. break;
  20. case 2://按键状态2:单击或长击
  21. if(!KEY_STATA0) {
  22. key_time = 0;
  23. key_cnt++;//按键计数器自增
  24. key_state = 3;//此时按键松开,表示已经单击,进入下个状态
  25. }
  26. else {
  27. delay_ms(1);
  28. if(key_time++ >= 2000) {
  29. key_cnt = 0;
  30. key_return = 10;
  31. key_state = 4;
  32. }
  33. }
  34. break;
  35. case 3://按键状态3:按键多击
  36. if(!KEY_STATA0) {
  37. delay_ms(1);
  38. if(key_time++ >= 500) {//松开按键0.5s无按键按下,返回按键计数器
  39. key_return = key_cnt;
  40. key_cnt = 0;
  41. key_state = 0;
  42. }
  43. }
  44. else {
  45. key_state = 0;
  46. }
  47. break;
  48. case 4://按键状态4:等待长按按键释放
  49. if(!KEY_STATA0) key_state = 0;
  50. break;
  51. }
  52. return key_return;
  53. }

         对应不确定什么时候到的接收,最好使用按键中断,这边使用按键中断与定时器完成单击、双击和长击,中间加了一个互斥锁g_APPTOIN_MUTEX,防止中断切换界面时运行程序出错,常规初始化配置就不写了,直接上中断函数的代码; --  使不使用中断由读者自己选择

  1. uint8_t g_APPTOIN_MUTEX = 1;//程序互斥锁
  2. void EXTI0_IRQHandler(void)
  3. {
  4. static uint8_t Keystate = 0; //静态按键状态 0 -- 松开 1 -- 按下
  5. if(EXTI_GetITStatus(EXTI_Line0) != RESET) {
  6. if(!KEY_STATA0 && Keystate) {
  7. Keystate = 0; //松开按键
  8. TIM_Cmd(TIM2, DISABLE); //关闭定时器
  9. if(g_Time_Count <= 3) {
  10. printf("...keyval, dither time = %d\r\n", g_Time_Count);
  11. }
  12. else if((g_Time_Count > 3) && (g_Time_Count < 200)) {
  13. if(++g_Keyval_Count >= 2) {//双击
  14. taskIndex = taskTable[taskIndex].Enter;//双击表示确认键
  15. OLED_Clear();
  16. printf("double keyval!\r\n");
  17. };
  18. }
  19. if((g_Keyval_Count >= 2) || (g_Keyval_Count == 0)) {
  20. g_Keyval_Count = 0;
  21. g_APPTOIN_MUTEX = 1;//关闭互斥
  22. }
  23. else {
  24. g_Time_Count = 0; //清空定时器溢出次数
  25. TIM_Cmd(TIM2, ENABLE); //启动定时器
  26. }
  27. }
  28. else if(KEY_STATA0 && !Keystate){
  29. Keystate = 1; //按下按键
  30. g_Time_Count = 0; //清空定时器溢出次数
  31. TIM_Cmd(TIM2, ENABLE); //启动定时器
  32. g_APPTOIN_MUTEX = 0; //开启互斥
  33. }
  34. EXTI_ClearITPendingBit(EXTI_Line0); //清除中断标志
  35. }
  36. }
  1. //#include "key.h"
  2. #define KEY_STATA0 !!GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0)
  3. extern uint8_t g_APPTOIN_MUTEX;
  4. void KEY_Init(void);

        定时器也一样,直接上中断函数的代码

  1. uint16_t g_Time_Count;//TIM溢出次数 -- 10ms Tout(溢出时间) = (ARR+1)(PSC+1)/Tclk(时钟分割)
  2. uint8_t g_Keyval_Count;//按键计数器
  3. void TIM2_IRQHandler(void)
  4. {
  5. if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET) {
  6. //进入中断表示定时器计数(CNT)溢出, 自增(10ms溢出一次, 可通过配置ARR, PSC和Tclk自定义溢出时间)
  7. g_Time_Count++;
  8. if((g_Time_Count >= 200) && KEY_STATA0) {
  9. TIM_Cmd(TIM2, DISABLE); //关闭定时器
  10. g_Time_Count = 0; //清空溢出次数
  11. g_Keyval_Count = 0; //清空按键计数
  12. taskIndex = taskTable[taskIndex].Return;//长击表示返回键
  13. OLED_Clear();
  14. printf("long keyval!\n");
  15. g_APPTOIN_MUTEX = 1;
  16. }
  17. else if((g_Time_Count >= 50) && !KEY_STATA0 && (g_Keyval_Count == 1)) {
  18. TIM_Cmd(TIM2, DISABLE); //关闭定时器
  19. g_Time_Count = 0; //清空溢出次数
  20. g_Keyval_Count = 0; //清空按键计数
  21. taskIndex = taskTable[taskIndex].Next;//单击表示下一位键
  22. printf("short keyval!\n");
  23. g_APPTOIN_MUTEX = 1;
  24. }
  25. TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
  26. }
  27. }
  1. //#include "time.h"
  2. extern uint16_t g_Time_Count;
  3. extern uint8_t g_Keyval_Count;
  4. void TIME_Init(uint16_t psc, uint16_t arr);

6.界面设计

        使用了三级界面,一级界面显示时间,利用sprintf函数将时间数据写入buff数组中,再显示到OLED屏幕上,完成显示

        二级界面提供功能选项,因为有三个功能选项所以用到三个函数,>>箭头在那个地方就表示选择那个功能,这三个功能分别表示字符串、汉字和图片

    

        三级界面显示内容,效果就不一个一个展示了,直接看后面的操作演示

         最后,显示代码:

  1. //#inlude "oled_show.c"
  2. uint8_t taskIndex = 0; //初始任务
  3. //任务调度表
  4. Menu_table_t taskTable[] =
  5. {
  6. //菜单界面函数 -- 一级界面
  7. {0, 1, 0, 1, Menu_Interface},
  8. //功能界面函数 -- 二级界面
  9. {1, 4, 2, 0, Function_Interface1},
  10. {2, 5, 3, 0, Function_Interface2},
  11. {3, 6, 1, 0, Function_Interface3},
  12. //功能设置界面函数 -- 三级界面
  13. {4, 4, 4, 1, Function_Interface4},
  14. {5, 5, 5, 2, Function_Interface5},
  15. {6, 6, 6, 3, Function_Interface6},
  16. };
  17. /**
  18. * @brief 菜单界面函数
  19. * @param 无
  20. * @retval 无
  21. */
  22. void Menu_Interface(void)
  23. {
  24. char buff[50];
  25. RTC_Get_StdTime(RTC_GetCounter());
  26. sprintf(buff,"%0.2d:%0.2d:%0.2d",RTC_CLOCK.hour, RTC_CLOCK.min, RTC_CLOCK.sec);
  27. OLED_ShowString(4, 10, 24, buff);
  28. sprintf(buff,"Date:%0.4d-%0.2d-%0.2d",RTC_CLOCK.year,RTC_CLOCK.mon,RTC_CLOCK.day);
  29. OLED_ShowString(1, 6, 16, buff);
  30. }
  31. /**
  32. * @brief 功能界面函数
  33. * @param 无
  34. * @retval 无
  35. */
  36. void Function_Interface1(void)
  37. {
  38. OLED_ShowString(0, 0, 16, ">>String");
  39. OLED_ShowString(2, 0, 16, " Chinese");
  40. OLED_ShowString(4, 0, 16, " ImageBMG");
  41. }
  42. void Function_Interface2(void)
  43. {
  44. OLED_ShowString(0, 0, 16, " String");
  45. OLED_ShowString(2, 0, 16, ">>Chinese");
  46. OLED_ShowString(4, 0, 16, " ImageBMG");
  47. }
  48. void Function_Interface3(void)
  49. {
  50. OLED_ShowString(0, 0, 16, " String");
  51. OLED_ShowString(2, 0, 16, " Chinese");
  52. OLED_ShowString(4, 0, 16, ">>ImageBMG");
  53. }
  54. /**
  55. * @brief 功能设置界面函数
  56. * @param 设置有三种状态
  57. * @retval 无
  58. */
  59. void Function_Interface4(void)
  60. {
  61. OLED_ShowString(1, 0, 8, "ABCD");
  62. OLED_ShowString(2, 0, 16, "ABCD");
  63. OLED_ShowString(4, 0, 24, "ABCD");
  64. }
  65. void Function_Interface5(void)
  66. {
  67. OLED_ShowChinese(3,16*2,0);//
  68. OLED_ShowChinese(3,16*3,1);//
  69. OLED_ShowChinese(3,16*4,2);//
  70. OLED_ShowChinese(3,16*5,3);//
  71. OLED_ShowChinese(3,16*6,4);// !
  72. }
  73. void Function_Interface6(void)
  74. {
  75. OLED_ShowImageBMG();
  76. }

7.主函数

         在main.c文件中,不使用中断,cnt是为了OLED屏幕刷新慢一点,不影响按键而设计的

  1. //#include "main.c"
  2. int main(void)
  3. {
  4. uint8_t keyval = 0, cnt = 0;
  5. KEY_Init();
  6. OLED_Init();
  7. Usart_Init();
  8. MyRTC_Init();
  9. printf("多级界面\r\n");
  10. while (1) {
  11. keyval = Keyval_Scan();
  12. if(keyval == 2) {
  13. printf("double\r\n");
  14. taskIndex = taskTable[taskIndex].Enter;//双击表示确认键
  15. OLED_Clear();
  16. }
  17. else if(keyval == 1) {
  18. printf("press\r\n");
  19. taskIndex = taskTable[taskIndex].Next;//单击表示下一位键
  20. }
  21. else if(keyval == 10) {
  22. printf("long\r\n");
  23. taskIndex = taskTable[taskIndex].Return;//长击表示返回键
  24. OLED_Clear();
  25. }
  26. if(cnt++ >= 200) {
  27. cnt = 0;
  28. taskTable[taskIndex].Current_Operation();//执行函数
  29. }
  30. }
  31. }

        在main.c文件中,使用中断,只需要一个互斥锁和一个执行函数就可以了,按键按下时,互斥锁开启,不执行执行函数,和上面的cnt用法相似

  1. //#include "main.c"
  2. int main(void)
  3. {
  4. KEY_Init();
  5. TIME_Init(72, 10000);
  6. OLED_Init();
  7. Usart_Init();
  8. MyRTC_Init();
  9. printf("多级界面\r\n");
  10. while (1) {
  11. if(g_APPTOIN_MUTEX) {
  12. taskTable[taskIndex].Current_Operation();//执行函数
  13. }
  14. }
  15. }

操作演示:

WeChat_20231205093715

源码分享:链接:https://pan.baidu.com/s/1CujKS8I7-eok3y9y3CxkUA?pwd=457e 
提取码:457e

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/weixin_40725706/article/detail/405496
推荐阅读
相关标签
  

闽ICP备14008679号