一、前言
本文的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个),标绿的则为固定的索引号与该索引下需要执行的函数。
typedef struct
{
u8 current; //当前状态索引号
u8 next; //向下一个
u8 enter; //确定
6u8 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情况使用局部刷新,这样可能项目会更加丝滑一点。
本项目中的菜单索引图:
4.2 内部功能实现(简化智能手表)
OLED就是正常的驱动与显示,有能力的读者朋友可以使用高级算法去加速OLED屏幕的刷新率,可以使自己的多级菜单切换起来更丝滑。
唯一需要注意的点就是需要去制作菜单里面的UI图标(注意图片大小是否合适):
如果是黑白图片的话,可以直接使用PCtoLCD2002完美版进行取模:
4.3 KEY按键
KEY按键注意消抖(建议裸机情况下支持连续按动),同时注意自己实际硬件情况去进行编程(电阻是否存在上拉或者下拉)。
4.4 DinoGame实现
谷歌公司最近比较流行的小游戏,笔者之前有文章进行了STM32的成功复刻。博客地址: 谷歌小恐龙在线 — 免费玩谷歌小恐龙 (dino.zone)
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.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=0
IIC_WaitAck();
IIC_WriteByte(0x00); //write command
IIC_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#=0
IIC_WaitAck();
IIC_WriteByte(0x40); //write data
IIC_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 off
OLED_WR_Byte(0x00,OLED_CMD);//---set low column address
OLED_WR_Byte(0x10,OLED_CMD);//---set high column address
OLED_WR_Byte(0x40,OLED_CMD);//--set start line address
OLED_WR_Byte(0xB0,OLED_CMD);//--set page address
OLED_WR_Byte(0x81,OLED_CMD); // contract control
OLED_WR_Byte(0xFF,OLED_CMD);//--128
OLED_WR_Byte(0xA1,OLED_CMD);//set segment remap
OLED_WR_Byte(0xA6,OLED_CMD);//--normal / reverse
OLED_WR_Byte(0xA8,OLED_CMD);//--set multiplex ratio(1 to 64)
OLED_WR_Byte(0x3F,OLED_CMD);//--1/32 duty
OLED_WR_Byte(0xC8,OLED_CMD);//Com scan direction
OLED_WR_Byte(0xD3,OLED_CMD);//-set display offset
OLED_WR_Byte(0x00,OLED_CMD);//
OLED_WR_Byte(0xD5,OLED_CMD);//set osc division
OLED_WR_Byte(0x80,OLED_CMD);//
OLED_WR_Byte(0xD8,OLED_CMD);//set area color mode off
OLED_WR_Byte(0x05,OLED_CMD);//