前言:本文的OLED多级菜单UI为一个综合性的STM32小项目,使用多传感器OLED显示屏实现智能终端的效果。项目中的多级菜单UI使用了较为常见的结构体索引法去实现功能与功能之间的来回切换,搭配DHT11,RTC,LED,KEY等器件实现高度智能化一体化操作。后期自己打板设计结构,可以衍生为智能手表等小玩意。目前,项目属于裸机状态CPU占用率100%),后期可能会加上RTOS系统。(本项目源码在本文末尾进行开源!

        硬件实物图:

效果图:

        温度计:

         游戏机:

引脚连接:

OLED模块:

VCC --> 3.3V

GND --> GND

SCL --> PB10

SDA --> PB11

DHT11模块:

DATA --> PB9

VCC --> 3.3V

GND --> GND

KEY模块(这部分笔者直接使用了正点原子精英板上的):

KEY0 --> PE4

KEY1 --> PE3

KEY_UP --> PA0

一、多级菜单

随着工业化和自动化的发展,如今基本上所有项目都离不开显示终端。而多级菜单更是终端显示项目中必不可少的组成因素,其实TFT-LCD屏幕上可以借鉴移植很多优秀的开源多级菜单GUI,比如:LVGL),而0.96寸的OLED屏幕上通常需要自己去适配和编程多级菜单。

精美的多级菜单:

网上的普遍采用的多级菜单的方案是基于索引或者结构树,其中,索引法居多索引法的优点:可阅读性好,拓展性也不错,查找的性能差不多是最优,就是有点占用内存空间

说明:本项目的多级菜单也是采用了索引法进行实现。

二、索引法多级菜单实现

网上关于索引法实现多级菜单功能有很多基础教程,笔者就按照本项目中的具体实现代码过程给大家讲解一下索引法实现多级菜单特别说明:本项目直接使用了正点原子的精英板作为核心板,所以读者朋友复现代码还是很简单的。

首先,基于索引法实现多级菜单首要条件是先确定项目中将使用到几个功能按键比如:向前,向后,确定,退出等等)本项目中,笔者使用到了3个按键下一个(next)确定(enter)退出(back)。所以,接下首先定义一个结构体,结构体中一共有5个变量3+2),分别为:当前索引序号(current)向下一个(next)确定(enter)退出(back)当前执行函数(void)。其中,标红的为需要设计的按键(笔者这里有3个),标绿的则为固定的索引号该索引下需要执行的函数

typedef struct
{u8 current;        //当前状态索引号u8 next;       //向下一个u8 enter;         //确定u8 back;        //退出void (*current_operation)(void); //当前状态应该执行的操作
} Menu_table;

接下来就是定义一个数组去决定整个项目菜单的逻辑顺序利用索引号

Menu_table  table[30]=
{{0,0,1,0,(*home)}, //一级界面(主页面) 索引,向下一个,确定,退出{1,2,5,0,(*Temperature)},   //二级界面 温湿度{2,3,6,0,(*Palygame)},    //二级界面 游戏{3,4,7,0,(*Setting)},  //二级界面 设置{4,1,8,0,(*Info)}, //二级界面 信息{5,5,5,1,(*TestTemperature)},      //三级界面:DHT11测量温湿度{6,6,6,2,(*ControlGame)},               //三级界面:谷歌小恐龙Dinogame{7,7,9,3,(*Set)},                                //三级界面:设置普通外设状态 LED{8,8,8,4,(*Information)},             //三级界面:作者和相关项目信息{9,9,7,3,(*LED)},        //LED控制
};

这里解释一下这个数组中各元素的意义,由于我们在前面先定义了Menu_table结构体结构体成员变量分别与数组中元素对应。比如:{0,0,1,0,(*home)},代表了索引号为0按向下键(next)转入索引号为0,按确定键(enter)转入索引号为1,按退出键(back)转入索引号为0,索引号为0时执行home函数

在举一个例子帮助大家理解一下,比如,我们当前程序处在索引号为2(游戏界面),就会执行Playgame函数。此时,如果按下next按键,程序当前索引号就会变为3,并且执行索引号为3时候的Setting函数。如果按下enter按键,程序当前索引号就会变为6,并且执行索引号为6时候的ControlGame函数。如果按下back按键,程序当前索引号就会变为0,并且执行索引号为0时候的home函数

再接下就是按键处理函数

uint8_t  func_index = 0; //主程序此时所在程序的索引值void  Menu_key_set(void)
{if((KEY_Scan(1) == 1) && (func_index != 6))        //屏蔽掉索引6下的情况,适配游戏{ func_index=table[func_index].next;    //按键next按下后的索引号OLED_Clear(); }if((KEY_Scan(1) == 2) && (func_index != 6)){func_index=table[func_index].enter;   //按键enter按下后的索引号OLED_Clear();}if(KEY_Scan(1) == 3){func_index=table[func_index].back;   //按键back按下后的索引号OLED_Clear(); }current_operation_index=table[func_index].current_operation;  //执行当前索引号所对应的功能函数(*current_operation_index)();//执行当前操作函数
}//按键函数
u8 KEY_Scan(u8 mode)
{static u8 key_up=1;if(mode)key_up=1; if(key_up&&(KEY0==0||KEY1==0||WK_UP==1)){HAL_Delay(100);      //消抖key_up=0;if(KEY0==0)return 1;else if(KEY1==0)return 2;else if(WK_UP==1)return 3;}else if(KEY0==1&&KEY1==1&&WK_UP==0)key_up=1; return 0;
}

说明2点:

(1)由于是目前本项目是裸机状态下运行的,所以CPU占用率默认是100%的,所以这里使用按键支持连按时,对于菜单的切换更好些。

(2)可能部分索引号下的执行函数,需要使用到已经定义的3个按键(比如,本项目中的DInogame中)。所以,可以在需要差别化的索引号下去屏蔽原先的按键功能。如下:

  if((KEY_Scan(1) == 1) && (func_index != 6))        //屏蔽掉索引6下的情况,适配游戏{ func_index=table[func_index].next;    //按键next按下后的索引号OLED_Clear(); }if((KEY_Scan(1) == 2) && (func_index != 6))        //屏蔽掉索引6下的情况,适配游戏{func_index=table[func_index].enter;   //按键enter按下后的索引号OLED_Clear();}

(3)笔者这里是使用全屏刷新去切换功能界面,同时,没有启用高级算法去加速显示,所以可能在切换界面的时候效果一般。读者朋友可以试试根据自己的UI情况使用局部刷新,这样可能项目会更加丝滑一点。

本项目中的菜单索引图:

三、此项目内部功能实现(简化智能手表)

3.1 OLED显示

OLED就是正常的驱动与显示,有能力的读者朋友可以使用高级算法去加速OLED屏幕的刷新率,可以使自己的多级菜单切换起来更丝滑。如果对OLED驱动与显示不太熟悉的朋友可以去看看本人另一篇博客:【强烈推荐】基于stm32的OLED各种显示实现(含动态图)_混分巨兽龙某某的博客-CSDN博客_stm32oled显示

唯一需要注意的点就是需要去制作菜单里面的UI图标(注意图片大小是否合适):

如果是黑白图片的话,可以直接使用PCtoLCD2002完美版进行取模:

3.2 KEY按键

KEY按键注意消抖(建议裸机情况下支持连续按动),同时注意自己实际硬件情况去进行编程(电阻是否存在上拉或者下拉)。

3.3 DinoGame实现

谷歌公司最近比较流行的小游戏,笔者之前有文章进行了STM32的成功复刻。博客地址:基于STM32的小游戏——谷歌小恐龙(Chrome Dino Game)_混分巨兽龙某某的博客-CSDN博客_谷歌恐龙

3.4 LED控制和DHT11模块

LED和DHT11模块其实都属于外设控制,这里读者朋友可以根据自己的实际情况去取舍。需要注意的是尽可能适配一下自己多级菜单(外设控制也需要注意一下按键安排,可以参考笔者项目的设计)。

四、CubeMX配置

1、RCC配置外部高速晶振(精度更高)——HSE;

2、SYS配置:Debug设置成Serial Wire否则可能导致芯片自锁);

 3、I2C2配置:这里不直接使用CubeMX的I2C2,使用GPIO模拟(PB10:CLK;PB11:SDA)

4、RTC配置:年月日,时分秒;

5、TIM2配置:由上面可知DHT11的使用需要us级的延迟函数,HAL库自带只有ms的,所以需要自己设计一个定时器;

6、KEY按键配置:PE3,PE4和PA0设置为端口输入(开发板原理图)

7、时钟树配置:

8、文件配置

五、代码

5.1 OLED驱动代码

此部分OLED的基本驱动函数,笔者使用的是I2C驱动的0.96寸OLED屏幕。所以,首先需要使用GPIO模拟I2C通讯。随后,使用I2C通讯去驱动OLED。(此部分代码包含了屏幕驱动与基础显示,如果对OLED显示不太理解的朋友可以去看看上文提到的笔者的另一篇文章)

oled.h:

#ifndef __OLED_H
#define __OLED_H#include "main.h"#define u8 uint8_t
#define u32 uint32_t#define OLED_CMD  0 //写命令
#define OLED_DATA 1 //写数据#define OLED0561_ADD   0x78  // OLED I2C地址
#define COM             0x00  // OLED
#define DAT             0x40  // OLED #define OLED_MODE 0
#define SIZE 8
#define XLevelL     0x00
#define XLevelH     0x10
#define Max_Column  128
#define Max_Row     64
#define Brightness  0xFF
#define X_WIDTH     128
#define Y_WIDTH     64//-----------------OLED IIC GPIO进行模拟----------------#define OLED_SCLK_Clr() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_10, GPIO_PIN_RESET) //GPIO_ResetBits(GPIOB,GPIO_Pin_10)//SCL
#define OLED_SCLK_Set() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_10, GPIO_PIN_SET) //GPIO_SetBits(GPIOB,GPIO_Pin_10)#define OLED_SDIN_Clr() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_11, GPIO_PIN_RESET) // GPIO_ResetBits(GPIOB,GPIO_Pin_11)//SDA
#define OLED_SDIN_Set() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_11, GPIO_PIN_SET) // GPIO_SetBits(GPIOB,GPIO_Pin_11)//I2C GPIO模拟
void IIC_Start();
void IIC_Stop();
void IIC_WaitAck();
void IIC_WriteByte(unsigned char IIC_Byte);
void IIC_WriteCommand(unsigned char IIC_Command);
void IIC_WriteData(unsigned char IIC_Data);
void OLED_WR_Byte(unsigned dat,unsigned cmd);//功能函数
void OLED_Init(void);
void OLED_WR_Byte(unsigned dat,unsigned cmd);void OLED_FillPicture(unsigned char fill_Data);
void OLED_SetPos(unsigned char x, unsigned char y);
void OLED_DisplayOn(void);
void OLED_DisplayOff(void);
void OLED_Clear(void);
void OLED_On(void);
void OLED_ShowChar(u8 x,u8 y,u8 chr,u8 Char_Size);
u32 oled_pow(u8 m,u8 n);
void OLED_ShowNum(u8 x,u8 y,u32 num,u8 len,u8 size2);
void OLED_ShowString(u8 x,u8 y,u8 *chr,u8 Char_Size);#endif

oled.c:

#include "oled.h"
#include "asc.h"    //字库(可以自己制作)
#include "main.h"/********************GPIO 模拟I2C*******************/
//注意:这里没有直接使用HAL库中的模拟I2C
/**********************************************
//IIC Start
**********************************************/
void IIC_Start()
{OLED_SCLK_Set() ;OLED_SDIN_Set();OLED_SDIN_Clr();OLED_SCLK_Clr();
}/**********************************************
//IIC Stop
**********************************************/
void IIC_Stop()
{OLED_SCLK_Set() ;OLED_SDIN_Clr();OLED_SDIN_Set();}void IIC_WaitAck()
{OLED_SCLK_Set() ;OLED_SCLK_Clr();
}
/**********************************************
// IIC Write byte
**********************************************/void IIC_WriteByte(unsigned char IIC_Byte)
{unsigned char i;unsigned char m,da;da=IIC_Byte;OLED_SCLK_Clr();for(i=0;i<8;i++){m=da;// OLED_SCLK_Clr();m=m&0x80;if(m==0x80){OLED_SDIN_Set();}else OLED_SDIN_Clr();da=da<<1;OLED_SCLK_Set();OLED_SCLK_Clr();}}
/**********************************************
// IIC Write Command
**********************************************/
void IIC_WriteCommand(unsigned char IIC_Command)
{IIC_Start();IIC_WriteByte(0x78);            //Slave address,SA0=0IIC_WaitAck();IIC_WriteByte(0x00);            //write commandIIC_WaitAck();IIC_WriteByte(IIC_Command);IIC_WaitAck();IIC_Stop();
}
/**********************************************
// IIC Write Data
**********************************************/
void IIC_WriteData(unsigned char IIC_Data)
{IIC_Start();IIC_WriteByte(0x78);           //D/C#=0; R/W#=0IIC_WaitAck();IIC_WriteByte(0x40);          //write dataIIC_WaitAck();IIC_WriteByte(IIC_Data);IIC_WaitAck();IIC_Stop();
}void OLED_WR_Byte(unsigned dat,unsigned cmd)
{if(cmd){IIC_WriteData(dat);}else{IIC_WriteCommand(dat);}
}void OLED_Init(void)
{HAL_Delay(100);        //这个延迟很重要OLED_WR_Byte(0xAE,OLED_CMD);//--display offOLED_WR_Byte(0x00,OLED_CMD);//---set low column addressOLED_WR_Byte(0x10,OLED_CMD);//---set high column addressOLED_WR_Byte(0x40,OLED_CMD);//--set start line addressOLED_WR_Byte(0xB0,OLED_CMD);//--set page addressOLED_WR_Byte(0x81,OLED_CMD); // contract controlOLED_WR_Byte(0xFF,OLED_CMD);//--128OLED_WR_Byte(0xA1,OLED_CMD);//set segment remapOLED_WR_Byte(0xA6,OLED_CMD);//--normal / reverseOLED_WR_Byte(0xA8,OLED_CMD);//--set multiplex ratio(1 to 64)OLED_WR_Byte(0x3F,OLED_CMD);//--1/32 dutyOLED_WR_Byte(0xC8,OLED_CMD);//Com scan directionOLED_WR_Byte(0xD3,OLED_CMD);//-set display offsetOLED_WR_Byte(0x00,OLED_CMD);//OLED_WR_Byte(0xD5,OLED_CMD);//set osc divisionOLED_WR_Byte(0x80,OLED_CMD);//OLED_WR_Byte(0xD8,OLED_CMD);//set area color mode offOLED_WR_Byte(0x05,OLED_CMD);//OLED_WR_Byte(0xD9,OLED_CMD);//Set Pre-Charge PeriodOLED_WR_Byte(0xF1,OLED_CMD);//OLED_WR_Byte(0xDA,OLED_CMD);//set com pin configuartionOLED_WR_Byte(0x12,OLED_CMD);//OLED_WR_Byte(0xDB,OLED_CMD);//set VcomhOLED_WR_Byte(0x30,OLED_CMD);//OLED_WR_Byte(0x8D,OLED_CMD);//set charge pump enableOLED_WR_Byte(0x14,OLED_CMD);//OLED_WR_Byte(0xAF,OLED_CMD);//--turn on oled panelHAL_Delay(100);    OLED_FillPicture(0x0);}/********************************************
// OLED_FillPicture
********************************************/
void OLED_FillPicture(unsigned char fill_Data)
{unsigned char m,n;for(m=0;m<8;m++){OLED_WR_Byte(0xb0+m,0);      //page0-page1OLED_WR_Byte(0x00,0);      //low column start addressOLED_WR_Byte(0x10,0);     //high column start addressfor(n=0;n<128;n++){OLED_WR_Byte(fill_Data,1);}}
}//坐标设置
void OLED_SetPos(unsigned char x, unsigned char y)
{   OLED_WR_Byte(0xb0+y,OLED_CMD);OLED_WR_Byte(((x&0xf0)>>4)|0x10,OLED_CMD);OLED_WR_Byte((x&0x0f),OLED_CMD);
}
//开启OLED显示
void OLED_DisplayOn(void)
{OLED_WR_Byte(0X8D,OLED_CMD);  //SET DCDC命令OLED_WR_Byte(0X14,OLED_CMD);  //DCDC ONOLED_WR_Byte(0XAF,OLED_CMD);  //DISPLAY ON
}
//关闭OLED显示
void OLED_DisplayOff(void)
{OLED_WR_Byte(0X8D,OLED_CMD);  //SET DCDC命令OLED_WR_Byte(0X10,OLED_CMD);  //DCDC OFFOLED_WR_Byte(0XAE,OLED_CMD);  //DISPLAY OFF
}
//清屏函数,清完屏,整个屏幕是黑色的!和没点亮一样!!!
void OLED_Clear(void)
{u8 i,n;for(i=0;i<8;i++){OLED_WR_Byte (0xb0+i,OLED_CMD);    //设置页地址(0~7)OLED_WR_Byte (0x00,OLED_CMD);      //设置显示位置—列低地址OLED_WR_Byte (0x10,OLED_CMD);      //设置显示位置—列高地址for(n=0;n<128;n++)OLED_WR_Byte(0,OLED_DATA);} //更新显示
}
void OLED_On(void)
{u8 i,n;for(i=0;i<8;i++){OLED_WR_Byte (0xb0+i,OLED_CMD);    //设置页地址(0~7)OLED_WR_Byte (0x00,OLED_CMD);      //设置显示位置—列低地址OLED_WR_Byte (0x10,OLED_CMD);      //设置显示位置—列高地址for(n=0;n<128;n++)OLED_WR_Byte(1,OLED_DATA);} //更新显示
}
//在指定位置显示一个字符,包括部分字符
//x:0~127
//y:0~63
//mode:0,反白显示;1,正常显示
//size:选择字体 16/12
void OLED_ShowChar(u8 x,u8 y,u8 chr,u8 Char_Size)
{unsigned char c=0,i=0;c=chr-' ';//得到偏移后的值if(x>Max_Column-1){x=0;y=y+2;}if(Char_Size ==16){OLED_SetPos(x,y);for(i=0;i<8;i++)OLED_WR_Byte(F8X16[c*16+i],OLED_DATA);OLED_SetPos(x,y+1);for(i=0;i<8;i++)OLED_WR_Byte(F8X16[c*16+i+8],OLED_DATA);}else {OLED_SetPos(x,y);for(i=0;i<6;i++)OLED_WR_Byte(F6x8[c][i],OLED_DATA);}
}//m^n函数
u32 oled_pow(u8 m,u8 n)
{u32 result=1;while(n--)result*=m;return result;
}//显示2个数字
//x,y :起点坐标
//len :数字的位数
//size:字体大小
//mode:模式   0,填充模式;1,叠加模式
//num:数值(0~4294967295);
void OLED_ShowNum(u8 x,u8 y,u32 num,u8 len,u8 size2)
{u8 t,temp;u8 enshow=0;for(t=0;t<len;t++){temp=(num/oled_pow(10,len-t-1))%10;if(enshow==0&&t<(len-1)){if(temp==0){
//              OLED_ShowChar(x+(size2/2)*t,y,' ',size2);OLED_ShowChar(x+(size2/2)*t,y,'0',size2);continue;}else enshow=1;}OLED_ShowChar(x+(size2/2)*t,y,temp+'0',size2);}
}//显示一个字符号串
void OLED_ShowString(u8 x,u8 y,u8 *chr,u8 Char_Size)
{unsigned char j=0;while (chr[j]!='\0'){        OLED_ShowChar(x,y,chr[j],Char_Size);x+=8;if(x>120){x=0;y+=2;}j++;}
}

5.2 谷歌小恐龙游戏图形绘制代码

该部分为整个项目代码的核心部分之,任何一个游戏都是需要去绘制构建游戏的图形以及模型的。好的游戏往往都具有很好的游戏模型精美UI,很多3A大作都具备这样的特性。

dinogame.h:

#ifndef __DINOGAME_H
#define __DINOGAME_Hvoid OLED_DrawBMP(unsigned char x0, unsigned char y0,unsigned char x1, unsigned char y1,unsigned char BMP[]);
void OLED_DrawBMPFast(const unsigned char BMP[]);
void oled_drawbmp_block_clear(int bx, int by, int clear_size);
void OLED_DrawGround();
void OLED_DrawCloud();
void OLED_DrawDino();
void OLED_DrawCactus();
int OLED_DrawCactusRandom(unsigned char ver, unsigned char reset);
int OLED_DrawDinoJump(char reset);
void OLED_DrawRestart();
void OLED_DrawCover();#endif

dinogame.c代码:

#include "oled.h"
#include "oledfont.h"
#include "stdlib.h"/***********功能描述:显示显示BMP图片128×64起始点坐标(x,y),x的范围0~127,y为页的范围0~7*****************/
void OLED_DrawBMP(unsigned char x0, unsigned char y0,unsigned char x1, unsigned char y1,unsigned char BMP[])
{unsigned int j=0;unsigned char x,y;if(y1%8==0) y=y1/8;else y=y1/8+1;for(y=y0;y<y1;y++){OLED_SetPos(x0,y);for(x=x0;x<x1;x++){OLED_WR_Byte(BMP[j++],OLED_DATA);}}
}// 快速绘制图像
void OLED_DrawBMPFast(const unsigned char BMP[])
{unsigned int j = 0;unsigned char x, y;for (y = 0; y < 8; y++){OLED_SetPos(0, y);IIC_Start();IIC_WriteByte(0x78);IIC_WaitAck();IIC_WriteByte(0x40);IIC_WaitAck();for (x = 0; x < 128; x++){IIC_WriteByte(BMP[j++]);IIC_WaitAck();}IIC_Stop();}
}void oled_drawbmp_block_clear(int bx, int by, int clear_size)
{unsigned int i;OLED_SetPos(bx, by);IIC_Start();IIC_WriteByte(0x78);IIC_WaitAck();IIC_WriteByte(0x40);IIC_WaitAck();for (i = 0; i < clear_size; i++){if (bx + i>128) break;IIC_WriteByte(0x0);IIC_WaitAck();}IIC_Stop();
}void OLED_DrawGround()
{static unsigned int pos = 0;unsigned char speed = 5;unsigned int ground_length = sizeof(GROUND);unsigned char x;OLED_SetPos(0, 7);IIC_Start();IIC_WriteByte(0x78);IIC_WaitAck();IIC_WriteByte(0x40);IIC_WaitAck();for (x = 0; x < 128; x++){IIC_WriteByte(GROUND[(x+pos)%ground_length]);IIC_WaitAck();}IIC_Stop();pos = pos + speed;//if(pos>ground_length) pos=0;
}// 绘制云朵
void OLED_DrawCloud()
{static int pos = 128;static char height=0;char speed = 3;unsigned int i=0;int x;int start_x = 0;int length = sizeof(CLOUD);unsigned char byte;//if (pos + length <= -speed) pos = 128;if (pos + length <= -speed){pos = 128;height = rand()%3;}if(pos < 0){start_x = -pos;OLED_SetPos(0, 1+height);}else{OLED_SetPos(pos, 1+height);}IIC_Start();IIC_WriteByte(0x78);IIC_WaitAck();IIC_WriteByte(0x40);IIC_WaitAck();for (x = start_x; x < length + speed; x++){if (pos + x > 127) break;if (x < length) byte = CLOUD[x];else byte = 0x0;IIC_WriteByte(byte);IIC_WaitAck();}IIC_Stop();pos = pos - speed;
}// 绘制小恐龙
void OLED_DrawDino()
{static unsigned char dino_dir = 0;unsigned int j=0;unsigned char x, y;unsigned char byte;dino_dir++;dino_dir = dino_dir%2;for(y=0; y<2; y++){OLED_SetPos(16, 6+y);IIC_Start();IIC_WriteByte(0x78);IIC_WaitAck();IIC_WriteByte(0x40);IIC_WaitAck();for (x = 0; x < 16; x++){j = y*16 + x;byte = DINO[dino_dir][j];IIC_WriteByte(byte);IIC_WaitAck();}IIC_Stop();}
}// 绘制仙人掌障碍物
void OLED_DrawCactus()
{char speed = 5;static int pos = 128;int start_x = 0;int length = sizeof(CACTUS_2)/2;unsigned int j=0;unsigned char x, y;unsigned char byte;if (pos + length <= 0){oled_drawbmp_block_clear(0, 6, speed);pos = 128;}for(y=0; y<2; y++){if(pos < 0){start_x = -pos;OLED_SetPos(0, 6+y);}else{OLED_SetPos(pos, 6+y);}IIC_Start();IIC_WriteByte(0x78);IIC_WaitAck();IIC_WriteByte(0x40);IIC_WaitAck();for (x = start_x; x < length; x++){if (pos + x > 127) break;j = y*length + x;byte = CACTUS_2[j];IIC_WriteByte(byte);IIC_WaitAck();}IIC_Stop();}oled_drawbmp_block_clear(pos + length, 6, speed); // 清除残影pos = pos - speed;
}// 绘制随机出现的仙人掌障碍物
int OLED_DrawCactusRandom(unsigned char ver, unsigned char reset)
{char speed = 5;static int pos = 128;int start_x = 0;int length = 0;unsigned int i=0, j=0;unsigned char x, y;unsigned char byte;if (reset == 1){pos = 128;oled_drawbmp_block_clear(0, 6, speed);return 128;}if (ver == 0) length = 8; //sizeof(CACTUS_1) / 2;else if (ver == 1) length = 16; //sizeof(CACTUS_2) / 2;else if (ver == 2 || ver == 3) length = 24;for(y=0; y<2; y++){if(pos < 0){start_x = -pos;OLED_SetPos(0, 6+y);}else{OLED_SetPos(pos, 6+y);}IIC_Start();IIC_WriteByte(0x78);IIC_WaitAck();IIC_WriteByte(0x40);IIC_WaitAck();for (x = start_x; x < length; x++){if (pos + x > 127) break;j = y*length + x;if (ver == 0) byte = CACTUS_1[j];else if (ver == 1) byte = CACTUS_2[j];else if(ver == 2) byte = CACTUS_3[j];else byte = CACTUS_4[j];IIC_WriteByte(byte);IIC_WaitAck();}IIC_Stop();}oled_drawbmp_block_clear(pos + length, 6, speed);pos = pos - speed;return pos + speed;
}// 绘制跳跃小恐龙
int OLED_DrawDinoJump(char reset)
{char speed_arr[] = {1, 1, 3, 3, 4, 4, 5, 6, 7};static char speed_idx = sizeof(speed_arr)-1;static int height = 0;static char dir = 0;//char speed = 4;unsigned int j=0;unsigned char x, y;char offset = 0;unsigned char byte;if(reset == 1){height = 0;dir = 0;speed_idx = sizeof(speed_arr)-1;return 0;}if (dir==0){height += speed_arr[speed_idx];speed_idx --;if (speed_idx<0) speed_idx = 0;}if (dir==1){height -= speed_arr[speed_idx];speed_idx ++;if (speed_idx>sizeof(speed_arr)-1) speed_idx = sizeof(speed_arr)-1;}if(height >= 31){dir = 1;height = 31;}if(height <= 0){dir = 0;height = 0;}if(height <= 7) offset = 0;else if(height <= 15) offset = 1;else if(height <= 23) offset = 2;else if(height <= 31) offset = 3;else offset = 4;for(y=0; y<3; y++) // 4{OLED_SetPos(16, 5- offset + y);IIC_Start();IIC_WriteByte(0x78);IIC_WaitAck();IIC_WriteByte(0x40);IIC_WaitAck();for (x = 0; x < 16; x++) // 32{j = y*16 + x; // 32byte = DINO_JUMP[height%8][j];IIC_WriteByte(byte);IIC_WaitAck();}IIC_Stop();}if (dir == 0) oled_drawbmp_block_clear(16, 8- offset, 16);if (dir == 1) oled_drawbmp_block_clear(16, 4- offset, 16);return height;
}// 绘制重启
void OLED_DrawRestart()
{unsigned int j=0;unsigned char x, y;unsigned char byte;//OLED_SetPos(0, 0);for (y = 2; y < 5; y++){OLED_SetPos(52, y);IIC_Start();IIC_WriteByte(0x78);IIC_WaitAck();IIC_WriteByte(0x40);IIC_WaitAck();for (x = 0; x < 24; x++){byte = RESTART[j++];IIC_WriteByte(byte);IIC_WaitAck();}IIC_Stop();}OLED_ShowString(10, 3, "GAME", 16);OLED_ShowString(86, 3, "OVER", 16);
}
// 绘制封面
void OLED_DrawCover()
{OLED_DrawBMPFast(COVER);
}

5.3 谷歌小恐龙的运行控制代码

control.h:

#ifndef __CONTROL_H
#define __CONTROL_Hint get_key();
void Game_control();#endif

control.c:

#include "control.h"
#include "oled.h"
#include "dinogame.h"
#include "stdlib.h"unsigned char key_num = 0;
unsigned char cactus_category = 0;
unsigned char cactus_length = 8;
unsigned int score = 0;
unsigned int highest_score = 0;
int height = 0;
int cactus_pos = 128;
unsigned char cur_speed = 30;
char failed = 0;
char reset = 0;int get_key()
{if(HAL_GPIO_ReadPin(GPIOE, GPIO_PIN_4)==0){HAL_Delay(10);            //延迟if(HAL_GPIO_ReadPin(GPIOE, GPIO_PIN_4)==0){return 2;}}if(HAL_GPIO_ReadPin(GPIOE, GPIO_PIN_3)==0){HAL_Delay(10);            //延迟if(HAL_GPIO_ReadPin(GPIOE, GPIO_PIN_3)==0){return 1;}}if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0)==1){HAL_Delay(10);            //延迟if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0)==1){return 3;}}return 0;
}void Game_control()
{while(1){if(get_key() == 3)        //wk_up按键按下强制退出一次循环{break;}if (failed == 1){OLED_DrawRestart();key_num = get_key();if (key_num == 2){if(score > highest_score) highest_score = score;score = 0;failed = 0;height = 0;reset = 1;OLED_DrawDinoJump(reset);OLED_DrawCactusRandom(cactus_category, reset);OLED_Clear();}continue;}score ++;if (height <= 0) key_num = get_key();OLED_DrawGround();OLED_DrawCloud();if (height>0 || key_num == 1) height = OLED_DrawDinoJump(reset);else OLED_DrawDino();cactus_pos = OLED_DrawCactusRandom(cactus_category, reset);if(cactus_category == 0) cactus_length = 8;else if(cactus_category == 1) cactus_length = 16;else cactus_length = 24;if (cactus_pos + cactus_length < 0){cactus_category = rand()%4;OLED_DrawCactusRandom(cactus_category, 1);}if ((height < 16) && ( (cactus_pos>=16 && cactus_pos <=32) || (cactus_pos + cactus_length>=16 && cactus_pos + cactus_length <=32))){failed = 1;}OLED_ShowString(35, 0, "HI:", 12);OLED_ShowNum(58, 0, highest_score, 5, 12);OLED_ShowNum(98, 0, score, 5, 12);reset = 0;cur_speed = score/20;if (cur_speed > 29) cur_speed = 29;HAL_Delay(30 - cur_speed);
//      HAL_Delay(500);key_num = 0;}}

5.4 多级菜单核心代码:

menu.h:

#ifndef __MENU_H
#define __MENU_H#include "main.h"
#define  u8 unsigned char//按键定义
#define KEY0 HAL_GPIO_ReadPin(GPIOE, GPIO_PIN_4)        //低电平有效     KEY0
#define KEY1 HAL_GPIO_ReadPin(GPIOE, GPIO_PIN_3)        //低电平有效
#define WK_UP HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0)       //高电平有效typedef struct
{u8 current;    //当前状态索引号u8 next;       //向下一个u8 enter;     //确定u8 back;        //退出void (*current_operation)(void); //当前状态应该执行的操作
} Menu_table;//界面UI
void home();
void Temperature();
void Palygame();
void Setting();
void Info();void  Menu_key_set(void);
u8 KEY_Scan(u8 mode);void TestTemperature();
void ConrtolGame();
void Set();
void Information();void LED();
void RTC_display();#endif

menu.c:

#include "menu.h"
#include "oled.h"
#include "gpio.h"
#include "dinogame.h"
#include "control.h"
#include "DHT11.h"
#include "rtc.h"RTC_DateTypeDef GetData;  //获取日期结构体RTC_TimeTypeDef GetTime;   //获取时间结构体//UI界面
//主页
/****************************************************/
//UI库/****************************************************/void (*current_operation_index)();    Menu_table  table[30]=
{{0,0,1,0,(*home)}, //一级界面(主页面) 索引,向下一个,确定,退出{1,2,5,0,(*Temperature)},   //二级界面 温湿度{2,3,6,0,(*Palygame)},    //二级界面 游戏{3,4,7,0,(*Setting)},  //二级界面 设置{4,1,8,0,(*Info)}, //二级界面 信息{5,5,5,1,(*TestTemperature)},      //三级界面:DHT11测量温湿度{6,6,6,2,(*ConrtolGame)},               //三级界面:谷歌小恐龙Dinogame{7,7,9,3,(*Set)},                                //三级界面:设置普通外设状态 LED{8,8,8,4,(*Information)},             //三级界面:作者和相关项目信息{9,9,7,3,(*LED)},        //LED控制
};uint8_t  func_index = 0;  //主程序此时所在程序的索引值void  Menu_key_set(void)
{if((KEY_Scan(1) == 1) && (func_index != 6)){ func_index=table[func_index].next;    //按键next按下后的索引号OLED_Clear(); }if((KEY_Scan(1) == 2) && (func_index != 6)){func_index=table[func_index].enter;   //按键enter按下后的索引号OLED_Clear();}if(KEY_Scan(1) == 3){func_index=table[func_index].back;   //按键back按下后的索引号OLED_Clear(); }current_operation_index=table[func_index].current_operation;  //执行当前索引号所对应的功能函数(*current_operation_index)();//执行当前操作函数
}void home()
{RTC_display();OLED_DrawBMP(0,0,20,3,signal_BMP);OLED_DrawBMP(20,0,36,2,gImage_bulethouch);OLED_DrawBMP(112,0,128,2,gImage_engery);OLED_DrawBMP(4,6,20,8,gImage_yes);OLED_DrawBMP(12,4,28,6,gImage_left);OLED_DrawBMP(40,2,88,8,gImage_home);OLED_DrawBMP(99,4,115,6,gImage_right);OLED_DrawBMP(107,6,123,8,gImage_back);
}void Temperature()
{RTC_display();OLED_DrawBMP(0,0,20,3,signal_BMP);OLED_DrawBMP(20,0,36,2,gImage_bulethouch);OLED_DrawBMP(112,0,128,2,gImage_engery);OLED_DrawBMP(4,6,20,8,gImage_yes);OLED_DrawBMP(12,4,28,6,gImage_left);OLED_DrawBMP(40,2,88,8,gImage_temp);OLED_DrawBMP(99,4,115,6,gImage_right);OLED_DrawBMP(107,6,123,8,gImage_back);
}void Palygame()
{RTC_display();OLED_DrawBMP(0,0,20,3,signal_BMP);OLED_DrawBMP(20,0,36,2,gImage_bulethouch);OLED_DrawBMP(112,0,128,2,gImage_engery);OLED_DrawBMP(4,6,20,8,gImage_yes);OLED_DrawBMP(12,4,28,6,gImage_left);OLED_DrawBMP(40,2,88,8,gImage_playgame);OLED_DrawBMP(99,4,115,6,gImage_right);OLED_DrawBMP(107,6,123,8,gImage_back);
}void Setting()
{RTC_display();OLED_DrawBMP(0,0,20,3,signal_BMP);OLED_DrawBMP(20,0,36,2,gImage_bulethouch);OLED_DrawBMP(112,0,128,2,gImage_engery);OLED_DrawBMP(4,6,20,8,gImage_yes);OLED_DrawBMP(12,4,28,6,gImage_left);OLED_DrawBMP(40,2,88,8,gImage_setting);OLED_DrawBMP(99,4,115,6,gImage_right);OLED_DrawBMP(107,6,123,8,gImage_back);
}void Info()
{RTC_display();OLED_DrawBMP(0,0,20,3,signal_BMP);OLED_DrawBMP(20,0,36,2,gImage_bulethouch);OLED_DrawBMP(112,0,128,2,gImage_engery);OLED_DrawBMP(4,6,20,8,gImage_yes);OLED_DrawBMP(12,4,28,6,gImage_left);OLED_DrawBMP(40,2,88,8,gImage_info);OLED_DrawBMP(99,4,115,6,gImage_right);OLED_DrawBMP(107,6,123,8,gImage_back);
}//按键函数,不支持连按
u8 KEY_Scan(u8 mode)
{static u8 key_up=1;if(mode)key_up=1; if(key_up&&(KEY0==0||KEY1==0||WK_UP==1)){HAL_Delay(100);      //消抖key_up=0;if(KEY0==0)return 1;else if(KEY1==0)return 2;else if(WK_UP==1)return 3;}else if(KEY0==1&&KEY1==1&&WK_UP==0)key_up=1; return 0;
}void TestTemperature()
{DHT11();
}void ConrtolGame()
{Game_control();
}void Set()
{OLED_ShowString(0,0,"Peripherals: Lights",16);OLED_ShowString(0,2,"Status: Closed",16);HAL_GPIO_WritePin(GPIOB, GPIO_PIN_5, GPIO_PIN_SET);
}void Information()
{OLED_ShowString(0,0,"Author:Sneak",16);OLED_ShowString(0,2,"Date:2022/8/23",16);OLED_ShowString(0,4,"Lab: Multi-level menu",16);
}void LED()
{OLED_ShowString(0,0,"Peripherals: Lights",16);OLED_ShowString(0,2,"Status: Open",16);HAL_GPIO_WritePin(GPIOB, GPIO_PIN_5, GPIO_PIN_RESET);
}void RTC_display()             //RTC????
{/* Get the RTC current Time */HAL_RTC_GetTime(&hrtc, &GetTime, RTC_FORMAT_BIN);/* Get the RTC current Date */HAL_RTC_GetDate(&hrtc, &GetData, RTC_FORMAT_BIN);/* Display date Format : yy/mm/dd *//* Display time Format : hh:mm:ss */OLED_ShowNum(40,0,GetTime.Hours,2,16);               //hourOLED_ShowString(57,0,":",16); OLED_ShowNum(66,0,GetTime.Minutes,2,16);            //minOLED_ShowString(83,0,":",16);  OLED_ShowNum(93,0,GetTime.Seconds,2,16);            //seconds
}

六、项目演示

多级菜单(简化版智能手表)

总结与代码开源

        总结:本项目目前还处于最初代版本,十分简易,后期笔者将抽时间去精进优化该多级菜单项目。其中,UI界面中的电池与信号目前都还处于贴图状态,后期笔者会加上库仑计测量电池电量等。文章中指出了需要注意的地方与可以改进的点,感兴趣的朋友可以彼此交流交流。(积分够的朋友可以支持一下,如果不够的话,点个关注,评论区留下邮箱,笔者看到会尽快发送项目代码。

代码地址:基于STM32的OLED多级菜单项目(简化版智能手表)-嵌入式文档类资源-CSDN文库

基于STM32的OLED多级菜单GUI实现(简化版智能手表)相关推荐

  1. 基于STM32的OLED显示

    基于STM32的OLED显示 一.什么是OLED 二.了解SPI SPI协议概括 同步串行口 三.学号姓名的显示 1.汉字点阵获取 代码修改 3.硬件连接 4.烧录编译结果 总结 参考文献 一.什么是 ...

  2. 基于STM32实现OLED滚动显示

    基于STM32实现OLED滚动显示 滚动方式 水平滚动 水平加垂直滚动 一.文字取模 二.代码修改 三.硬件连接加结果 总结 参考文献 滚动方式 水平滚动 OLED_WR_Byte(0x2E,OLED ...

  3. 基于STM32进行OLED显示

    目录 一.SPI接口简介 1.SPI的简介 2. SPI 接口4条线通信: 3.SPI的特征 4.接口框图 5.SPI整体的通讯过程 二.OLED的原理 1.OLED模块参数 2.该模块的特点: 3. ...

  4. 基于STM32的OLED播放Bad apple

    基于stm32的oled显示badapple 有屏幕的地方就有bad apple 原理 将视频用opencv转换成图片,之后进行图片取模,转化为16进制,使用oled图片显示函数显示. 硬件所需 st ...

  5. 基于stm32的OLED显示屏显示

    文章目录 一.内容 二.关于SPI 三.关于OLED 四.功能实现 1)实验准备 2)代码准备 1.显示学号和姓名 2.显示AHT20的温度和湿度 3.滑动显示长字符 3)结果展示 五.参考内容 六. ...

  6. 基于STM32的OLED屏显实例

    文章标题 一.SPI协议 1.什么是SPI 2.SPI物理层 3.SPI协议层 4.SPI特点 5.通讯引脚 二.OLED屏显原理 1.简介 2.OLED屏显原理 三.OLED屏显实例 1.显示自己的 ...

  7. 基于STM32完成OLED显屏功能

    基于STM32和ISP协议了解OLED屏显和汉字点阵原理完成各项实例 一.SPI协议 二.OLED显示屏介绍 三.汉字点阵编码原理 四.OLED显示屏显示实例 (1).OLED显示自己的学号和姓名 ( ...

  8. 基于STM32下OLED显示实现文字取模软件配置

    在做OLED显示实验时,文字很难编辑出对应的点,从网上找到了这个文字转换软件,以下是软件的配置项,由于是STM32所以自定义格式选择C51格式, 取模方式:列行式 取模走向:逆向 输出数制:16进制 ...

  9. 基于vue+element实现多级菜单动态生成

    使用element的NavMenu组件+vue的组件递归实现动态生成菜单. 首先创建对象记录菜单的标题.链接.子集等信息. menus: [{title: "首页",icon: & ...

最新文章

  1. 服务器开机修改grub,修改 grub
  2. universal image loader在listview/gridview中滚动时重复加载图片的问题及解决方法
  3. 使用.NetCore 控制台演示 熔断 降级(polly)
  4. pytorch将label转为one hot形式
  5. sqlserver2017 +SSMS+ VS2017+SSDT 安装要点及相关组件下载地址
  6. 「小程序JAVA实战」小程序的留言和评价功能(70)
  7. java设置子线程优先级_Java 实例 - 线程优先级设置
  8. vim设置tab宽度为4_vim编辑器VimScript插件开发系列一「定制Vim 3」
  9. 解决2010版办公软件打不开从系统中导出的Excel表格
  10. 当vue遇到pwa--vue+pwa移动端适配解决方案模板案例
  11. AD9371官网汇总
  12. FreeMarker生成word文档
  13. 庸者挣扎泥潭,高手从不恋战
  14. 上海高一计算机奥赛,上海物理奥赛金牌“大神”爱番剧和高达,已保送清华大学姚班...
  15. 通过有用的反馈改善测试人员与开发人员的关系
  16. kafka干货(五):kakfka的python客户端----Confluent-kafka
  17. 基于SSM框架的图书馆借阅管理系统
  18. Python Matplotlib画图基础介绍
  19. matlab图注的上标和下标,关于matlab中特殊字符, 上标和下标
  20. VR数字沙盘高度还原未来房屋实

热门文章

  1. Linux 系统查询处理器架构
  2. 微信小程序扫描二维码、小程序码进入的开发测试
  3. 频域与时域滤波 (一)
  4. c语言百文百鸡问题答案,算法的举例(。。。
  5. response概述
  6. [转载]Linux SWAP 交换分区配置说明
  7. 啥是单点登录及单点登录原理
  8. 刷题记录:牛客NC16122郊区春游
  9. 量化择时之移动平均线初探
  10. 2021年江苏省淮安高考成绩查询,2021年江苏淮安高考时间:6月7日至9日