一、前言
本文的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);// ? ?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); ???} ?} } ? //坐標設置 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 } //關閉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 } //清屏函數,清完屏,整個屏幕是黑色的!和沒點亮一樣!!! 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;t120){x=0;y+=2;} ???j++; ?} }
?
?
6.2 谷歌小恐龍游戲圖形繪制代碼
該部分為整個項目代碼的核心部分之一,任何一個游戲都是需要去繪制和構建游戲的圖形以及模型的。好的游戲往往都具有很好的游戲模型和精美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起始點坐標(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; } ? ? //?繪制隨機出現的仙人掌障礙物 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 谷歌小恐龍的運行控制代碼
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按鍵按下強制退出一次循環 ??{ ???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;?//當前狀態索引號 ????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?:?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 }
七、總結與代碼開源
總結:本項目目前還處于最初代版本,十分簡易,后期筆者將抽時間去精進優化該多級菜單項目。其中,UI界面中的電池與信號目前都還處于貼圖狀態,后期筆者會加上庫侖計測量電池電量等。文章中指出了需要注意的地方與可以改進的點,感興趣的朋友可以彼此交流交流。
編輯:黃飛
?
評論
查看更多