一、前言
本文的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屏幕上通常需要自己去适配和编程多级菜单。
网上的普遍采用的多级菜单的方案是基于索引或者结构树,其中,索引法居多。索引法的优点:可阅读性好,拓展性也不错,查找的性能差不多是最优,就是有点占用内存空间。
4.1 索引法多级菜单实现
网上关于索引法实现多级菜单功能有很多基础教程,笔者就按照本项目中的具体实现代码过程给大家讲解一下索引法实现多级菜单。特别说明:本项目直接使用了正点原子的精英板作为核心板,所以读者朋友复现代码还是很简单的。
首先,基于索引法实现多级菜单的首要条件是先确定项目中将使用到几个功能按键(比如:向前,向后,确定,退出等等)本项目中,笔者使用到了3个按键:下一个(next),确定(enter),退出(back)。所以,接下首先定义一个结构体,结构体中一共有5个变量(3+2),分别为:当前索引序号(current),向下一个(next),确定(enter),退出(back),当前执行函数(void)。其中,标红的为需要设计的按键(笔者这里有3个),标绿的则为固定的索引号与该索引下需要执行的函数。
typedefstruct
{
u8current;//当前状态索引号
u8next;//向下一个
u8enter;//确定
6u8back;//退出
void(*current_operation)(void);//当前状态应该执行的操作
}Menu_table;
接下来就是定义一个数组去决定整个项目菜单的逻辑顺序(利用索引号)
Menu_tabletable[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_tfunc_index=0;//主程序此时所在程序的索引值
voidMenu_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)();//执行当前操作函数
}
//按键函数
u8KEY_Scan(u8mode)
{
staticu8key_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)return1;
elseif(KEY1==0)return2;
elseif(WK_UP==1)return3;
}elseif(KEY0==1&&KEY1==1&&WK_UP==0)key_up=1;
return0;
}
说明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情况使用局部刷新,这样可能项目会更加丝滑一点。
本项目中的菜单索引图:
435e37ac-9550-11ed-bfe3-dac502259ad0.png
4.2 内部功能实现(简化智能手表)
OLED就是正常的驱动与显示,有能力的读者朋友可以使用高级算法去加速OLED屏幕的刷新率,可以使自己的多级菜单切换起来更丝滑。
唯一需要注意的点就是需要去制作菜单里面的UI图标(注意图片大小是否合适):
如果是黑白图片的话,可以直接使用PCtoLCD2002完美版进行取模:
4.3 KEY按键
KEY按键注意消抖(建议裸机情况下支持连续按动),同时注意自己实际硬件情况去进行编程(电阻是否存在上拉或者下拉)。
4.4 DinoGame实现
谷歌公司最近比较流行的小游戏,笔者之前有文章进行了STM32的成功复刻。博客地址:基于STM32的小游戏——谷歌小恐龙(Chrome Dino Game)_混分巨兽龙某某的博客-CSDN博客_谷歌恐龙
4.5 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、文件配置
六、代码
6.1 OLED驱动代码
此部分OLED的基本驱动函数,笔者使用的是I2C驱动的0.96寸OLED屏幕。所以,首先需要使用GPIO模拟I2C通讯。随后,使用I2C通讯去驱动OLED。(此部分代码包含了屏幕驱动与基础显示,如果对OLED显示不太理解的朋友可以去看看上文提到的笔者的另一篇文章)
oled.h:
#ifndef__OLED_H
#define__OLED_H
#include"main.h"
#defineu8uint8_t
#defineu32uint32_t
#defineOLED_CMD0//写命令
#defineOLED_DATA1//写数据
#defineOLED0561_ADD0x78//OLEDI2C地址
#defineCOM0x00//OLED
#defineDAT0x40//OLED
#defineOLED_MODE0
#defineSIZE8
#defineXLevelL0x00
#defineXLevelH0x10
#defineMax_Column128
#defineMax_Row64
#defineBrightness0xFF
#defineX_WIDTH128
#defineY_WIDTH64
//-----------------OLEDIICGPIO进行模拟----------------
#defineOLED_SCLK_Clr()HAL_GPIO_WritePin(GPIOB,GPIO_PIN_10,GPIO_PIN_RESET)//GPIO_ResetBits(GPIOB,GPIO_Pin_10)//SCL
#defineOLED_SCLK_Set()HAL_GPIO_WritePin(GPIOB,GPIO_PIN_10,GPIO_PIN_SET)//GPIO_SetBits(GPIOB,GPIO_Pin_10)
#defineOLED_SDIN_Clr()HAL_GPIO_WritePin(GPIOB,GPIO_PIN_11,GPIO_PIN_RESET)//GPIO_ResetBits(GPIOB,GPIO_Pin_11)//SDA
#defineOLED_SDIN_Set()HAL_GPIO_WritePin(GPIOB,GPIO_PIN_11,GPIO_PIN_SET)//GPIO_SetBits(GPIOB,GPIO_Pin_11)
//I2CGPIO模拟
voidIIC_Start();
voidIIC_Stop();
voidIIC_WaitAck();
voidIIC_WriteByte(unsignedcharIIC_Byte);
voidIIC_WriteCommand(unsignedcharIIC_Command);
voidIIC_WriteData(unsignedcharIIC_Data);
voidOLED_WR_Byte(unsigneddat,unsignedcmd);
//功能函数
voidOLED_Init(void);
voidOLED_WR_Byte(unsigneddat,unsignedcmd);
voidOLED_FillPicture(unsignedcharfill_Data);
voidOLED_SetPos(unsignedcharx,unsignedchary);
voidOLED_DisplayOn(void);
voidOLED_DisplayOff(void);
voidOLED_Clear(void);
voidOLED_On(void);
voidOLED_ShowChar(u8x,u8y,u8chr,u8Char_Size);
u32oled_pow(u8m,u8n);
voidOLED_ShowNum(u8x,u8y,u32num,u8len,u8size2);
voidOLED_ShowString(u8x,u8y,u8*chr,u8Char_Size);
#endif
oled.c:
#include"oled.h"
#include"asc.h"//字库(可以自己制作)
#include"main.h"
/********************GPIO模拟I2C*******************/
//注意:这里没有直接使用HAL库中的模拟I2C
/**********************************************
//IICStart
**********************************************/
voidIIC_Start()
{
OLED_SCLK_Set();
OLED_SDIN_Set();
OLED_SDIN_Clr();
OLED_SCLK_Clr();
}
/**********************************************
//IICStop
**********************************************/
voidIIC_Stop()
{
OLED_SCLK_Set();
OLED_SDIN_Clr();
OLED_SDIN_Set();
}
voidIIC_WaitAck()
{
OLED_SCLK_Set();
OLED_SCLK_Clr();
}
/**********************************************
//IICWritebyte
**********************************************/
voidIIC_WriteByte(unsignedcharIIC_Byte)
{
unsignedchari;
unsignedcharm,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();}
elseOLED_SDIN_Clr();
da=da<<1;
OLED_SCLK_Set();
OLED_SCLK_Clr();
}
}
/**********************************************
//IICWriteCommand
**********************************************/
voidIIC_WriteCommand(unsignedcharIIC_Command)
{
IIC_Start();
IIC_WriteByte(0x78);//Slaveaddress,SA0=0
IIC_WaitAck();
IIC_WriteByte(0x00);//writecommand
IIC_WaitAck();
IIC_WriteByte(IIC_Command);
IIC_WaitAck();
IIC_Stop();
}
/**********************************************
//IICWriteData
**********************************************/
voidIIC_WriteData(unsignedcharIIC_Data)
{
IIC_Start();
IIC_WriteByte(0x78);//D/C#=0;R/W#=0
IIC_WaitAck();
IIC_WriteByte(0x40);//writedata
IIC_WaitAck();
IIC_WriteByte(IIC_Data);
IIC_WaitAck();
IIC_Stop();
}
voidOLED_WR_Byte(unsigneddat,unsignedcmd)
{
if(cmd)
{
IIC_WriteData(dat);
}
else
{
IIC_WriteCommand(dat);
}
}
voidOLED_Init(void)
{
HAL_Delay(100);//这个延迟很重要
OLED_WR_Byte(0xAE,OLED_CMD);//--displayoff
OLED_WR_Byte(0x00,OLED_CMD);//---setlowcolumnaddress
OLED_WR_Byte(0x10,OLED_CMD);//---sethighcolumnaddress
OLED_WR_Byte(0x40,OLED_CMD);//--setstartlineaddress
OLED_WR_Byte(0xB0,OLED_CMD);//--setpageaddress
OLED_WR_Byte(0x81,OLED_CMD);//contractcontrol
OLED_WR_Byte(0xFF,OLED_CMD);//--128
OLED_WR_Byte(0xA1,OLED_CMD);//setsegmentremap
OLED_WR_Byte(0xA6,OLED_CMD);//--normal/reverse
OLED_WR_Byte(0xA8,OLED_CMD);//--setmultiplexratio(1to64)
OLED_WR_Byte(0x3F,OLED_CMD);//--1/32duty
OLED_WR_Byte(0xC8,OLED_CMD);//Comscandirection
OLED_WR_Byte(0xD3,OLED_CMD);//-setdisplayoffset
OLED_WR_Byte(0x00,OLED_CMD);//
OLED_WR_Byte(0xD5,OLED_CMD);//setoscdivision
OLED_WR_Byte(0x80,OLED_CMD);//
OLED_WR_Byte(0xD8,OLED_CMD);//setareacolormodeoff
OLED_WR_Byte(0x05,OLED_CMD);//
OLED_WR_Byte(0xD9,OLED_CMD);//SetPre-ChargePeriod
OLED_WR_Byte(0xF1,OLED_CMD);//
OLED_WR_Byte(0xDA,OLED_CMD);//setcompinconfiguartion
OLED_WR_Byte(0x12,OLED_CMD);//
OLED_WR_Byte(0xDB,OLED_CMD);//setVcomh
OLED_WR_Byte(0x30,OLED_CMD);//
OLED_WR_Byte(0x8D,OLED_CMD);//setchargepumpenable
OLED_WR_Byte(0x14,OLED_CMD);//
OLED_WR_Byte(0xAF,OLED_CMD);//--turnonoledpanel
HAL_Delay(100);
OLED_FillPicture(0x0);
}
/********************************************
//OLED_FillPicture
********************************************/
voidOLED_FillPicture(unsignedcharfill_Data)
{
unsignedcharm,n;
for(m=0;m<8;m++)
{
OLED_WR_Byte(0xb0+m,0);//page0-page1
OLED_WR_Byte(0x00,0);//lowcolumnstartaddress
OLED_WR_Byte(0x10,0);//highcolumnstartaddress
for(n=0;n<128;n++)
{
OLED_WR_Byte(fill_Data,1);
}
}
}
//坐标设置
voidOLED_SetPos(unsignedcharx,unsignedchary)
{OLED_WR_Byte(0xb0+y,OLED_CMD);
OLED_WR_Byte(((x&0xf0)>>4)|0x10,OLED_CMD);
OLED_WR_Byte((x&0x0f),OLED_CMD);
}
//开启OLED显示
voidOLED_DisplayOn(void)
{
OLED_WR_Byte(0X8D,OLED_CMD);//SETDCDC命令
OLED_WR_Byte(0X14,OLED_CMD);//DCDCON
OLED_WR_Byte(0XAF,OLED_CMD);//DISPLAYON
}
//关闭OLED显示
voidOLED_DisplayOff(void)
{
OLED_WR_Byte(0X8D,OLED_CMD);//SETDCDC命令
OLED_WR_Byte(0X10,OLED_CMD);//DCDCOFF
OLED_WR_Byte(0XAE,OLED_CMD);//DISPLAYOFF
}
//清屏函数,清完屏,整个屏幕是黑色的!和没点亮一样!!!
voidOLED_Clear(void)
{
u8i,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);
}//更新显示
}
voidOLED_On(void)
{
u8i,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
voidOLED_ShowChar(u8x,u8y,u8chr,u8Char_Size)
{
unsignedcharc=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);