一、前言
本文的OLED多級菜單UI為一個(gè)綜合性的STM32小項(xiàng)目,使用多傳感器與OLED顯示屏實(shí)現(xiàn)智能終端的效果。項(xiàng)目中的多級菜單UI使用了較為常見的結(jié)構(gòu)體索引法去實(shí)現(xiàn)功能與功能之間的來回切換,搭配DHT11,RTC,LED,KEY等器件實(shí)現(xiàn)高度智能化一體化操作。
后期自己打板設(shè)計(jì)結(jié)構(gòu),可以衍生為智能手表等小玩意。目前,項(xiàng)目屬于裸機(jī)狀態(tài)(CPU占用率100%),后期可能會(huì)加上RTOS系統(tǒng)。
二、硬件實(shí)物圖

溫度計(jì):

?

游戲機(jī):

?

三、硬件引腳圖
?
?
OLED模塊: VCC?-->?3.3V GND?-->?GND SCL?-->?PB10 SDA?-->?PB11 DHT11模塊: DATA?-->?PB9 VCC?-->?3.3V GND?-->?GND KEY模塊(這部分筆者直接使用了正點(diǎn)原子精英板上的): KEY0?-->?PE4 KEY1?-->?PE3 KEY_UP?-->?PA0
?
?
四、多級菜單
隨著工業(yè)化和自動(dòng)化的發(fā)展,如今基本上所有項(xiàng)目都離不開顯示終端。而多級菜單更是終端顯示項(xiàng)目中必不可少的組成因素,其實(shí) TFT-LCD 屏幕上可以借鑒移植很多優(yōu)秀的開源多級菜單(GUI,比如:LVGL),而0.96寸的OLED屏幕上通常需要自己去適配和編程多級菜單。
網(wǎng)上的普遍采用的多級菜單的方案是基于索引或者結(jié)構(gòu)樹,其中,索引法居多。索引法的優(yōu)點(diǎn):可閱讀性好,拓展性也不錯(cuò),查找的性能差不多是最優(yōu),就是有點(diǎn)占用內(nèi)存空間。
4.1 索引法多級菜單實(shí)現(xiàn)
網(wǎng)上關(guān)于索引法實(shí)現(xiàn)多級菜單功能有很多基礎(chǔ)教程,筆者就按照本項(xiàng)目中的具體實(shí)現(xiàn)代碼過程給大家講解一下索引法實(shí)現(xiàn)多級菜單。特別說明:本項(xiàng)目直接使用了正點(diǎn)原子的精英板作為核心板,所以讀者朋友復(fù)現(xiàn)代碼還是很簡單的。
首先,基于索引法實(shí)現(xiàn)多級菜單的首要條件是先確定項(xiàng)目中將使用到幾個(gè)功能按鍵(比如:向前,向后,確定,退出等等)本項(xiàng)目中,筆者使用到了3個(gè)按鍵:下一個(gè)(next),確定(enter),退出(back)。所以,接下首先定義一個(gè)結(jié)構(gòu)體,結(jié)構(gòu)體中一共有5個(gè)變量(3+2),分別為:當(dāng)前索引序號(current),向下一個(gè)(next),確定(enter),退出(back),當(dāng)前執(zhí)行函數(shù)(void)。其中,標(biāo)紅的為需要設(shè)計(jì)的按鍵(筆者這里有3個(gè)),標(biāo)綠的則為固定的索引號與該索引下需要執(zhí)行的函數(shù)。
?
?
typedef?struct
{
????u8?current;?????//當(dāng)前狀態(tài)索引號
????u8?next;???//向下一個(gè)
????u8?enter;??????//確定
????6u8?back;???//退出
????void?(*current_operation)(void);?//當(dāng)前狀態(tài)應(yīng)該執(zhí)行的操作
}?Menu_table;
?
?
接下來就是定義一個(gè)數(shù)組去決定整個(gè)項(xiàng)目菜單的邏輯順序(利用索引號)
?
?
Menu_table??table[30]=
{
????{0,0,1,0,(*home)},?//一級界面(主頁面)?索引,向下一個(gè),確定,退出
??
????{1,2,5,0,(*Temperature)},?//二級界面?溫濕度
????{2,3,6,0,(*Palygame)},?//二級界面?游戲
????{3,4,7,0,(*Setting)},?//二級界面?設(shè)置
????{4,1,8,0,(*Info)},?//二級界面?信息
??
????{5,5,5,1,(*TestTemperature)},??//三級界面:DHT11測量溫濕度
????{6,6,6,2,(*ControlGame)},????//三級界面:谷歌小恐龍Dinogame
????{7,7,9,3,(*Set)},????????//三級界面:設(shè)置普通外設(shè)狀態(tài) LED
????{8,8,8,4,(*Information)},????//三級界面:作者和相關(guān)項(xiàng)目信息
?
????{9,9,7,3,(*LED)},??//LED控制
};
?
?
這里解釋一下這個(gè)數(shù)組中各元素的意義,由于我們在前面先定義了Menu_table結(jié)構(gòu)體,結(jié)構(gòu)體成員變量分別與數(shù)組中元素對應(yīng)。比如:{0,0,1,0,(*home)},代表了索引號為0,按向下鍵(next)轉(zhuǎn)入索引號為0,按確定鍵(enter)轉(zhuǎn)入索引號為1,按退出鍵(back)轉(zhuǎn)入索引號為0,索引號為0時(shí)執(zhí)行home函數(shù)。
在舉一個(gè)例子幫助大家理解一下,比如,我們當(dāng)前程序處在索引號為2(游戲界面),就會(huì)執(zhí)行Playgame函數(shù)。此時(shí),如果按下next按鍵,程序當(dāng)前索引號就會(huì)變?yōu)?,并且執(zhí)行索引號為3時(shí)候的Setting函數(shù)。如果按下enter按鍵,程序當(dāng)前索引號就會(huì)變?yōu)?,并且執(zhí)行索引號為6時(shí)候的ControlGame函數(shù)。如果按下back按鍵,程序當(dāng)前索引號就會(huì)變?yōu)?,并且執(zhí)行索引號為0時(shí)候的home函數(shù)。
再接下就是按鍵處理函數(shù):
?
?
uint8_t??func_index?=?0;?//主程序此時(shí)所在程序的索引值
?
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;?//執(zhí)行當(dāng)前索引號所對應(yīng)的功能函數(shù)
??(*current_operation_index)();//執(zhí)行當(dāng)前操作函數(shù)
}
?
?
//按鍵函數(shù)
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點(diǎn):
?
?
(1)由于是目前本項(xiàng)目是裸機(jī)狀態(tài)下運(yùn)行的,所以CPU占用率默認(rèn)是100%的,所以這里使用按鍵支持連按時(shí),對于菜單的切換更好些。
(2)可能部分索引號下的執(zhí)行函數(shù),需要使用到已經(jīng)定義的3個(gè)按鍵(比如,本項(xiàng)目中的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)筆者這里是使用全屏刷新去切換功能界面,同時(shí),沒有啟用高級算法去加速顯示,所以可能在切換界面的時(shí)候效果一般。讀者朋友可以試試根據(jù)自己的UI情況使用局部刷新,這樣可能項(xiàng)目會(huì)更加絲滑一點(diǎn)。
本項(xiàng)目中的菜單索引圖:

4.2 內(nèi)部功能實(shí)現(xiàn)(簡化智能手表)
OLED就是正常的驅(qū)動(dòng)與顯示,有能力的讀者朋友可以使用高級算法去加速OLED屏幕的刷新率,可以使自己的多級菜單切換起來更絲滑。
唯一需要注意的點(diǎn)就是需要去制作菜單里面的UI圖標(biāo)(注意圖片大小是否合適):

如果是黑白圖片的話,可以直接使用PCtoLCD2002完美版進(jìn)行取模:

4.3 KEY按鍵
KEY按鍵注意消抖(建議裸機(jī)情況下支持連續(xù)按動(dòng)),同時(shí)注意自己實(shí)際硬件情況去進(jìn)行編程(電阻是否存在上拉或者下拉)。

4.4 DinoGame實(shí)現(xiàn)

谷歌公司最近比較流行的小游戲,筆者之前有文章進(jìn)行了STM32的成功復(fù)刻。博客地址: 谷歌小恐龍?jiān)诰€ — 免費(fèi)玩谷歌小恐龍 (dino.zone)
4.5 LED控制和DHT11模塊
LED和DHT11模塊其實(shí)都屬于外設(shè)控制,這里讀者朋友可以根據(jù)自己的實(shí)際情況去取舍。需要注意的是盡可能適配一下自己多級菜單(外設(shè)控制也需要注意一下按鍵安排,可以參考筆者項(xiàng)目的設(shè)計(jì))。
五、CubeMX配置
1、RCC配置外部高速晶振(精度更高)——HSE;

2、SYS配置:Debug設(shè)置成Serial Wire(否則可能導(dǎo)致芯片自鎖);

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

4、RTC配置:年月日,時(shí)分秒;

?

5、TIM2配置:由上面可知DHT11的使用需要us級的延遲函數(shù),HAL庫自帶只有ms的,所以需要自己設(shè)計(jì)一個(gè)定時(shí)器;

6、KEY按鍵配置:PE3,PE4和PA0設(shè)置為端口輸入(開發(fā)板原理圖)

7、時(shí)鐘樹配置:

8、文件配置

六、代碼
6.1 OLED驅(qū)動(dòng)代碼
此部分OLED的基本驅(qū)動(dòng)函數(shù),筆者使用的是I2C驅(qū)動(dòng)的0.96寸OLED屏幕。所以,首先需要使用GPIO模擬I2C通訊。隨后,使用I2C通訊去驅(qū)動(dòng)OLED。(此部分代碼包含了屏幕驅(qū)動(dòng)與基礎(chǔ)顯示)
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?//寫數(shù)據(jù) ? #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進(jìn)行模擬---------------- ? #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); ? ? //功能函數(shù) 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);??//這個(gè)延遲很重要
?
?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);//
?
?OLED_WR_Byte(0xD9,OLED_CMD);//Set?Pre-Charge?Period
?OLED_WR_Byte(0xF1,OLED_CMD);//
?
?OLED_WR_Byte(0xDA,OLED_CMD);//set?com?pin?configuartion
?OLED_WR_Byte(0x12,OLED_CMD);//
?
?OLED_WR_Byte(0xDB,OLED_CMD);//set?Vcomh
?OLED_WR_Byte(0x30,OLED_CMD);//
?
?OLED_WR_Byte(0x8D,OLED_CMD);//set?charge?pump?enable
?OLED_WR_Byte(0x14,OLED_CMD);//
?
?OLED_WR_Byte(0xAF,OLED_CMD);//--turn?on?oled?panel
?HAL_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-page1
??OLED_WR_Byte(0x00,0);??//low?column?start?address
??OLED_WR_Byte(0x10,0);??//high?column?start?address
??for(n=0;n<128;n++)
???{
????OLED_WR_Byte(fill_Data,1);
???}
?}
}
?
//坐標(biāo)設(shè)置
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?ON
?OLED_WR_Byte(0XAF,OLED_CMD);??//DISPLAY?ON
}
//關(guān)閉OLED顯示
void?OLED_DisplayOff(void)
{
?OLED_WR_Byte(0X8D,OLED_CMD);??//SET?DCDC命令
?OLED_WR_Byte(0X10,OLED_CMD);??//DCDC?OFF
?OLED_WR_Byte(0XAE,OLED_CMD);??//DISPLAY?OFF
}
//清屏函數(shù),清完屏,整個(gè)屏幕是黑色的!和沒點(diǎn)亮一樣!!!
void?OLED_Clear(void)
{
?u8?i,n;
?for(i=0;i<8;i++)
?{
??OLED_WR_Byte?(0xb0+i,OLED_CMD);????//設(shè)置頁地址(0~7)
??OLED_WR_Byte?(0x00,OLED_CMD);??????//設(shè)置顯示位置—列低地址
??OLED_WR_Byte?(0x10,OLED_CMD);??????//設(shè)置顯示位置—列高地址
??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);????//設(shè)置頁地址(0~7)
??OLED_WR_Byte?(0x00,OLED_CMD);??????//設(shè)置顯示位置—列低地址
??OLED_WR_Byte?(0x10,OLED_CMD);??????//設(shè)置顯示位置—列高地址
??for(n=0;n<128;n++)OLED_WR_Byte(1,OLED_DATA);
?}?//更新顯示
}
//在指定位置顯示一個(gè)字符,包括部分字符
//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函數(shù)
u32?oled_pow(u8?m,u8?n)
{
?u32?result=1;
?while(n--)result*=m;
?return?result;
}
?
//顯示2個(gè)數(shù)字
//x,y?:起點(diǎn)坐標(biāo)
//len?:數(shù)字的位數(shù)
//size:字體大小
//mode:模式?0,填充模式;1,疊加模式
//num:數(shù)值(0~4294967295);
void?OLED_ShowNum(u8?x,u8?y,u32?num,u8?len,u8?size2)
{
?u8?t,temp;
?u8?enshow=0;
?for(t=0;t120){x=0;y+=2;}
???j++;
?}
}
?
?
6.2 谷歌小恐龍游戲圖形繪制代碼
該部分為整個(gè)項(xiàng)目代碼的核心部分之一,任何一個(gè)游戲都是需要去繪制和構(gòu)建游戲的圖形以及模型的。好的游戲往往都具有很好的游戲模型和精美UI,很多3A大作都具備這樣的特性。
dinogame.h:
#ifndef?__DINOGAME_H #define?__DINOGAME_H ? void?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起始點(diǎn)坐標(biāo)(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;y128)?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??127)?break;
??if?(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;
}
?
?
//?繪制隨機(jī)出現(xiàn)的仙人掌障礙物
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??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;?//?32
???byte?=?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);
}
?
?
6.3 谷歌小恐龍的運(yùn)行控制代碼
control.h:
?
?
#ifndef?__CONTROL_H #define?__CONTROL_H ? int?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按鍵按下強(qiáng)制退出一次循環(huán)
??{
???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;
?
?}
?
}
?
?
6.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;?//當(dāng)前狀態(tài)索引號
????u8?next;???//向下一個(gè)
????u8?enter;??//確定
??u8?back;???//退出
????void?(*current_operation)(void);?//當(dāng)前狀態(tài)應(yīng)該執(zhí)行的操作
}?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;??//獲取日期結(jié)構(gòu)體
?
RTC_TimeTypeDef?GetTime;???//獲取時(shí)間結(jié)構(gòu)體
?
?
//UI界面
//主頁
/****************************************************/
//UI庫
?
/****************************************************/
?
void?(*current_operation_index)();??
?
Menu_table??table[30]=
{
????{0,0,1,0,(*home)},?//一級界面(主頁面)?索引,向下一個(gè),確定,退出
??
????{1,2,5,0,(*Temperature)},?//二級界面?溫濕度
????{2,3,6,0,(*Palygame)},?//二級界面?游戲
????{3,4,7,0,(*Setting)},?//二級界面?設(shè)置
????{4,1,8,0,(*Info)},?//二級界面?信息
??
??{5,5,5,1,(*TestTemperature)},??//三級界面:DHT11測量溫濕度
??{6,6,6,2,(*ConrtolGame)},????//三級界面:谷歌小恐龍Dinogame
??{7,7,9,3,(*Set)},????????//三級界面:設(shè)置普通外設(shè)狀態(tài) LED
??{8,8,8,4,(*Information)},????//三級界面:作者和相關(guān)項(xiàng)目信息
?
??{9,9,7,3,(*LED)},??//LED控制
};
?
uint8_t??func_index?=?0;?//主程序此時(shí)所在程序的索引值
?
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;?//執(zhí)行當(dāng)前索引號所對應(yīng)的功能函數(shù)
??(*current_operation_index)();//執(zhí)行當(dāng)前操作函數(shù)
}
?
?
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);
}
?
?
//按鍵函數(shù),不支持連按
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?:?hhss?*/
??OLED_ShowNum(40,0,GetTime.Hours,2,16);????//hour
??OLED_ShowString(57,0,":",16);?
??OLED_ShowNum(66,0,GetTime.Minutes,2,16);???//min
??OLED_ShowString(83,0,":",16);?
??OLED_ShowNum(93,0,GetTime.Seconds,2,16);???//seconds
}
七、總結(jié)與代碼開源
總結(jié):本項(xiàng)目目前還處于最初代版本,十分簡易,后期筆者將抽時(shí)間去精進(jìn)優(yōu)化該多級菜單項(xiàng)目。其中,UI界面中的電池與信號目前都還處于貼圖狀態(tài),后期筆者會(huì)加上庫侖計(jì)測量電池電量等。文章中指出了需要注意的地方與可以改進(jìn)的點(diǎn),感興趣的朋友可以彼此交流交流。
編輯:黃飛
?
電子發(fā)燒友App







評論