文章目录
- 一、 前言
- 谷歌小恐龙
- 二、硬件
- 三、软件
- 3.1 摇杆开关
- 3.2 OLED屏幕
- 四、展示
- 五、总结
一、 前言
最近有看到别人在OLED屏幕上玩小恐龙,所幸查阅下资料,并下好源码。可惜他的源码的主控是STM32F103ZET6,用的是STM32CubeIDE,采用的是HAL库编写。我目前手头上并没有该主控的开发板,只能自己手动移植到STM32F103C8T6上,觉得用按键来操作小恐龙跳跃不是很舒服,所以自己改用摇杆开关用遥控向上来进行跳跃,更顺手些。
谷歌小恐龙
谷歌小恐龙是指谷歌 Chrome 浏览器中的一个小游戏,通常出现在没有网络连接时的页面(称为“无网页游戏”或“离线游戏”)。这款游戏名为“恐龙跳跃”(T-Rex Runner),玩家需要控制一只恐龙跳过障碍物,随着游戏时间的推移,速度会逐渐加快,挑战也会变得更大。
谷歌小恐龙游戏在线玩
二、硬件
老样子,用面板上搭建一个简单的实验环境。
共需要一块最小系统板,一个0.96寸的OLED显示屏(IIC驱动)和一个摇杆开关。
硬件相对比较简单,很容易实现,感兴趣的可以试下。
引脚连接:(如需更改,直接修改对应.h文件里面的宏定义)
摇杆开关X输出:PB0
摇杆开关Y输出:PB1
摇杆开关按键:PB10
OLED显示屏SCL:PA0
OLED显示屏SDA:PA1
对摇杆开关不了解的可以看下摇杆开关的简单应用
对OLED不了解的可以看下0.96寸OLED(IIC接口)显示屏的图像显示应用
三、软件
3.1 摇杆开关
这里的ADCX和ADCY的引脚要是ADC引脚,还要看一下具体的DMA通道。至于下面那个按键就是摇杆开关的SW开关引脚了,任意引脚即可。
/* Defines ------------------------------------------------------------------*/
#define Rocker_GPIO_RCC RCC_APB2Periph_GPIOB
#define Rocker_GPIO_Port GPIOB
#define ADCX_Pin GPIO_Pin_0
#define ADCY_Pin GPIO_Pin_1
#define Key_GPIO_RCC RCC_APB2Periph_GPIOB
#define Key_GPIO_Port GPIOB
#define Key_Pin GPIO_Pin_10//根据实际的引脚修改
/* Variables Define ---------------------------------------------------------*/
uint16_t conv_val[4] = {0};
float LeftTwoAxisKey_X = 0;
float LeftTwoAxisKey_Y = 0;
/* Function prototypes ------------------------------------------------------*/
/*******************************************************************************
* 函数名:User_Rocker_Init
* 描述 :摇杆开关相关变量初始化
* 输入 :void
* 输出 :void
* 调用 :初始化
* 备注 :
*******************************************************************************/
void User_Rocker_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
DMA_InitTypeDef DMA_InitStructure;
ADC_InitTypeDef ADC_InitStructure;
RCC_APB2PeriphClockCmd(Rocker_GPIO_RCC, ENABLE);
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
GPIO_InitStructure.GPIO_Pin = ADCX_Pin;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(Rocker_GPIO_Port, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = ADCY_Pin;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(Rocker_GPIO_Port, &GPIO_InitStructure);
DMA_DeInit(DMA1_Channel1); // 将DMA的通道1寄存器重设为缺省值
DMA_InitStructure.DMA_PeripheralBaseAddr = (u32)(&(ADC1->DR)); // 外设基址为:ADC 数据寄存器地址
DMA_InitStructure.DMA_MemoryBaseAddr = (u32)conv_val; // 存储器地址
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; // 数据源来自外设
// 缓冲区大小,应该等于数据目的地的大小
DMA_InitStructure.DMA_BufferSize = 4;
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; // 外设寄存器只有一个,地址不用递增
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; // 存储器地址递增
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; // 外设数据大小为半字,即两个字节
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; // 内存数据大小也为半字,跟外设数据大小相同
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; // 循环传输模式
DMA_InitStructure.DMA_Priority = DMA_Priority_High; // DMA 传输通道优先级为高,当使用一个DMA通道时,优先级设置不影响
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; // 禁止存储器到存储器模式,因为是从外设到存储器
DMA_Init(DMA1_Channel1, &DMA_InitStructure); // 初始化DMA
DMA_Cmd(DMA1_Channel1, ENABLE); // 使能 DMA 通道
ADC_DeInit(ADC1); // ADC1复位
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
ADC_InitStructure.ADC_ScanConvMode = ENABLE;
ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
ADC_InitStructure.ADC_NbrOfChannel = 2;
ADC_Init(ADC1, &ADC_InitStructure);
RCC_ADCCLKConfig(RCC_PCLK2_Div8);
// 配置ADC 通道的转换顺序和采样时间
ADC_RegularChannelConfig(ADC1, ADC_Channel_8, 1, ADC_SampleTime_55Cycles5);
ADC_RegularChannelConfig(ADC1, ADC_Channel_9, 2, ADC_SampleTime_55Cycles5);
ADC_DMACmd(ADC1, ENABLE);
ADC_Cmd(ADC1, ENABLE); // 使能adc1
ADC_ResetCalibration(ADC1); // 复位ADC1校准
while (ADC_GetResetCalibrationStatus(ADC1))
; // 等待ADC1复位校准结束
ADC_StartCalibration(ADC1); // 开启ADC1校准
while (ADC_GetCalibrationStatus(ADC1))
; // 等待ADC1校准结束
ADC_SoftwareStartConvCmd(ADC1, ENABLE);
}
/*******************************************************************************
* 函数名:Key_Init
* 描述 :按键初始化
* 输入 :void
* 输出 :void
* 调用 :初始化
* 备注 :
*******************************************************************************/
void User_Key_Init(void)
{
/*定义一个GPIO_InitTypeDef类型的结构体*/
GPIO_InitTypeDef GPIO_InitStructure;
/*开启LED相关的GPIO外设时钟*/
RCC_APB2PeriphClockCmd(Key_GPIO_RCC, ENABLE);
/*设置引脚模式为上拉输入*/
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
/*选择要控制的GPIO引脚*/
GPIO_InitStructure.GPIO_Pin = Key_Pin;
/*调用库函数,初始化GPIO*/
GPIO_Init(Key_GPIO_Port, &GPIO_InitStructure);
}
/*******************************************************************************
* 函数名:Key_IN_Read
* 描述 :Key输入模式读取引脚状态
* 输入 :void
* 输出 :uint8_t
* 调用 :初始化
* 备注 :
*******************************************************************************/
uint8_t Key_IN_Read(void)
{
return GPIO_ReadInputDataBit(Key_GPIO_Port, Key_Pin);
}
/*******************************************************************************
* 函数名:Rocker_Scan
* 描述 :摇杆开关扫描
* 输入 :void
* 输出 :void
* 调用 :内部调用
* 备注 :
*******************************************************************************/
int Rocker_Scan(void)
{
LeftTwoAxisKey_X = conv_val[0] * 3.3 / 4096;
LeftTwoAxisKey_Y = conv_val[1] * 3.3 / 4096;
// printf("LeftTwoAxisKey_X=%f LeftTwoAxisKey_Y=%f\r\n",LeftTwoAxisKey_X,LeftTwoAxisKey_Y);
/*********************判断摇杆开关方向****************************/
if ((LeftTwoAxisKey_X > 1) && (LeftTwoAxisKey_Y < 1))
{
// printf("Up \r\n");
return 1;
}
if ((LeftTwoAxisKey_X > 1) && (LeftTwoAxisKey_Y > 2))
{
// printf("Down \r\n");
}
/*********************判断摇杆按键是否按下****************************/
if (Key_IN_Read())
{
delay_syms(2);
if (Key_IN_Read())
{
return 2;
}
}
return 0;
}
3.2 OLED屏幕
#define IIC_GPIO_Port GPIOA
#define SDA_Pin GPIO_Pin_1
#define SCL_Pin GPIO_Pin_0//根据实际的引脚修改
#define OLED_SCLK_Clr() GPIO_ResetBits(IIC_GPIO_Port,SCL_Pin)//SCL
#define OLED_SCLK_Set() GPIO_SetBits(IIC_GPIO_Port,SCL_Pin)
#define OLED_SDIN_Clr() GPIO_ResetBits(IIC_GPIO_Port,SDA_Pin)//SDA
#define OLED_SDIN_Set() GPIO_SetBits(IIC_GPIO_Port,SDA_Pin)
//初始化SSD1306
void OLED_Init(void)
{
IIC_Init();//IIC引脚初始化
delay_syms(500);
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
delay_syms(500);
OLED_FillPicture(0x0);
}
//清屏函数,清完屏,整个屏幕是黑色的!和没点亮一样!!!
void OLED_Clear(void)
{
uint8_t 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_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_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();
}
}
// 绘制跳跃小恐龙
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;
}
// 绘制随机出现的仙人掌障碍物
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;
}
// 绘制云朵
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;
}
四、展示
大概是下面这样子,如果觉得速度有点慢或者快,修改cur_speed 的值,也就是修改score/XX,改XX的大小。
cur_speed = score/20;
if (cur_speed > 29) cur_speed = 29;
delay_syms(30 - cur_speed);
屏闪的问题是手机相机拍摄的问题。实物是没问题的。
基于STM32和OLED的小恐龙游戏项目(摇杆开关控制)
五、总结
今天主要讲了基于STM32的摇杆开关控制小恐龙游戏,主要将原有的按键方式修改为摇杆控制。
感谢你的观看!