基于N32G45贪吃蛇小游戏项目移植
本次项目功能为贪吃蛇小游戏。用的外设模块包含:0.96寸OLED屏幕、4个按键、LED程序指示灯。
0.96寸OLED屏幕采用7线SPI接口,通过板载硬件SPI方式驱动。
4个按键采用外部中断EXTI方式驱动,以达到实时检测效果。
LED程序指示灯采用定时器PWM输出方式控制,定时器周期时间为1000ms,其中有效电平时间为500ms。即500ms闪烁一次。
配置USART1,实现串口数据收发,用于代码调试。
-
贪吃蛇游戏运行效果示例
0.96寸OLED驱动
OLED,即有机发光二极管( Organic Light Emitting Diode)。 OLED 由于同时具备自发光,不需背光源、对比度高、厚度薄、视角广、反应速度快、可用于挠曲性面板、使用温度范围广、 构造及制程较简单等优异之特性,被认为是下一代的平面显示器新兴应用技术。
GND 电源地
VCC 电源正( 3~5.5V)
D0 OLED 的 D0 脚,在 SPI 和 IIC 通信中为时钟管脚
D1 OLED 的 D1 脚,在 SPI 和 IIC 通信中为数据管脚
RES OLED 的 RES#脚,用来复位(低电平复位)
DC OLED 的 D/C#E 脚, 数据和命令控制管脚
CS OLED 的 CS#脚,也就是片选管脚
本次选用OLED屏幕为0.96寸,驱动IC为SSD1306,驱动协议为SPI。分辨率为128*64;单色屏幕。采用页面寻址方式。
1.SPI硬件驱动OLED屏幕
SPI是串行外设接口(Serial Peripheral Interface)的缩写,是一种高速的,全双工,同步的通信总线,并且在芯片的管脚上只占用四根线,节约了芯片的管脚,同时为PCB的布局上节省空间,提供方便,正是出于这种简单易用的特性,越来越多的芯片集成了这种通信协议。
SPI:高速同步串行口。是一种标准的四线同步双向串行总线,是串行外围设备接口。是Motorola首先在其MC68HCXX系列处理器上定义的。SPI接口主要应用在 EEPROM,FLASH,实时时钟,AD转换器,还有数字信号处理器和数字信号解码器之间。
该接口一般使用4条线:串行时钟线(SCLK)、主机输入/从机输出数据线MISO、主机输出/从机输入数据线MOSI和低电平有效的从机选择线SS(有的SPI接口芯片带有中断信号线INT、有的SPI接口芯片没有主机输出/从机输入数据线MOSI)。
SPI根据时钟极性(CPOL)和时钟相位(CPHA)的不同,能够产生4时钟时序。时钟极性(CPOL)控制时钟线空闲电平状态,时钟相位(CPHA)用来控制数据采样极性。
- 全双工和单工同步模式
- 支持主模式、从模式和多主模式
- 支持 8bit 或 16bit 数据帧格式
- 数据位顺序可编程
- 硬件或软件片选管理
- 时钟极性和时钟相位可配置
- 发送和接收支持硬件 CRC 计算及校验
- 支持 DMA 传输功能
由于PA5、PA6被按键占用,PB4、PB5被LED占用,所有我们这里开启SPI1完全重映射。将PB2作为片选,PE7为时钟、PE8为主机输入、PE9为主机输出。
注意:这里之所以选择SPI1,是由于SPI1在APB2时钟线上,SPI1最高频率可达36MHZ。根据N32手册第32章可知。
SPI1硬件BUG
根据官方提供的款物手册,在使用SPI1工作在主模式时,并且NSS由软件管理,会存在BUG,解决办法为SPI1_SSOEN置1。
2.SPI硬件配置
static void OLED_GPIO_Init(void)
{
RCC->APB2PCLKEN|=1<<3;
RCC->APB2PCLKEN|=1<<6;
RCC->APB2PCLKEN|=1<<0;
AFIO->RMP_CFG|=1<<0;
AFIO->RMP_CFG3|=1<<18;
GPIOE->PL_CFG&=0x0fffffff;
GPIOE->PL_CFG|=0xB0000000;
GPIOE->PH_CFG&=0xffffff0f;
GPIOE->PH_CFG|=0x000000B0;
GPIOB->PH_CFG&=0xfff0ffff;
GPIOB->PH_CFG|=0x00030000;
GPIOB->PL_CFG&=0xfffff00f;
GPIOB->PL_CFG|=0x00000330;
RCC->APB2PCLKEN|=1<<12;
RCC->APB2PRST|=1<<12;
RCC->APB2PRST&=~(1<<12);
SPI1->CTRL1|=1<<9;
SPI1->CTRL1|=1<<2;
SPI1->CTRL2|=1<<2;
SPI1->CTRL1|=1<<6;
OLED_CS=1;
OLED_RES=1;
}
0.96寸OLED采用的是3线SPI,没有主机输入端口,因此我们底层只需要提供SPI发送函数即可。
static inline void SPI_ReadWriteByte(u8 data_tx)
{
SPI1->DAT=data_tx;
while(!(SPI1->STS&1<<1)){}
}
3.OLED底层画点函数实现
要实现贪吃蛇游戏,就必须实现核心函数:画点函数。但由于OLED默认寻址方式为页面寻址方式,因此要实现画点函数还必须建立一个屏幕缓存,通过先将数据加载到缓存中,然后再刷新到屏幕上。
画点函数实现如下:
/*******************画点函数**********************
**
**形参:u8 x --横坐标0~127
** u8 y --纵坐标0~63
** u8 c --0表示不显示,1表示显示
**OLED_DrawPoint(50,20,u8 c)
**************************************************/
static u8 oled_gram[8][128];//屏幕缓冲区
void OLED_DrawPoint(u8 x,u8 y,u8 c)
{
u8 page=y/8;//y坐标值在第几页
u8 line=y%8;//在当前页的第几行上
if(c)oled_gram[page][x]|=1<<line;
else oled_gram[page][x]&=~(1<<line);
}
外部中断检测按键
中断,是指处理机处理程序运行中出现的紧急事件的整个过程。程序运行过程中,系统外部、系统内部或者现行程序本身若出现紧急事件,处理机立即中止现行程序的运行,自动转入相应的处理程序(中断服务程序),待处理完后,再返回原来的程序运行,这整个过程称为程序中断。当处理机接受中断时,只需暂停一个或几个周期而不执行处理程序的中断,称为简单中断,中断又可分为屏蔽中断和非屏蔽中断两类。
外部中断/事件控制器包含 21 个产生中断/事件触发的边沿检测电路,每条输入线可以独立地配置脉冲或挂起输入类型, 以及上升沿、下降沿或者双边沿 3 种触发事件类型, 也可以独立地被屏蔽。挂起寄存器保持着状态线的中断请求, 可通过在挂起寄存器的对应位写’1’操作,清除中断请求。
- 外部中断特性
- 支持 21 个软件中断/事件请求
- 每条输入线对应的中断/事件都能独立配置触发或屏蔽
- 每条中断线都有独立的状态位
- 支持脉冲或挂起输入类型
- 支持三种触发事件:上升沿、下降沿或双边沿
- 可唤醒退出低功耗模式
EXTI 包含 21 条中断线,其中 16 条来自 I/O 管脚,另 5 条来自内部模块。 要产生中断,必须配置外部中断控制器的 NVIC 中断通道使能相应的中断线。 通过边沿触发配置寄存器 EXTI_RT_CFG 和 EXTI_FT_CFG 选择上升沿、下降沿或双边沿触发事件类型, 并将中断屏蔽寄存器 EXTI_IMASK 的相应位写’1’开放允许中断请求。当外部中断线上检测到预设的边沿触发极性,将产生一个中断请求,对应的挂起位也随之被置’1’。在挂起寄存器的对应位写’1’,将清除该中断请求。
本开发板的按键接口为PA0、PA4、PA5、PA6。因此我们需要开启的外部中断线为中断线0、中断线4、中断线5、中断线6。
对于中断线的开启,则是通过AFIO寄存器的EXTI_CFGx来实现。
1.按键外部中断配置
配置外部中断时,中断线0,中断线4是有单独的中断线,而中断线5、中断线6则是共用中断线5~9。
void EXTI_Init(void)
{
KEY_Init();
RCC->APB2PCLKEN|=1<<0;
AFIO->EXTI_CFG[0]&=~(0xF<<0*4);
EXTI->IMASK|=1<<0;
EXTI->RT_CFG|=1<<0;
EXTI->FT_CFG|=1<<0;
N32_NVIC_SetPriority(EXTI0_IRQn,1,1);
AFIO->EXTI_CFG[1]&=~(0xF<<0*4);
EXTI->IMASK|=1<<4;
EXTI->RT_CFG|=1<<4;
EXTI->FT_CFG|=1<<4;
N32_NVIC_SetPriority(EXTI4_IRQn,1,1);
AFIO->EXTI_CFG[1]&=~(0xF<<1*4);
EXTI->IMASK|=1<<5;
EXTI->RT_CFG|=1<<5;
EXTI->FT_CFG|=1<<5;
AFIO->EXTI_CFG[1]&=~(0xF<<2*4);
EXTI->IMASK|=1<<6;
EXTI->RT_CFG|=1<<6;
EXTI->FT_CFG|=1<<6;
N32_NVIC_SetPriority(EXTI9_5_IRQn,1,1);
}
u8 key_val;
void EXTI0_IRQHandler(void)
{
Delay_Ms(20);
if(WK_UP)
{
key_val=0;
key_val|=1<<0;
}
else
{
}
EXTI->PEND|=1<<0;
}
void EXTI4_IRQHandler(void)
{
Delay_Ms(20);
if(KEY1==0)
{
key_val=0;
key_val|=1<<1;
}
EXTI->PEND|=1<<4;
}
void EXTI9_5_IRQHandler(void)
{
Delay_Ms(20);
if(KEY2==0)
{
key_val=0;
key_val|=1<<2;
}
else if(KEY3==0)
{
key_val=0;
key_val|=1<<3;
}
EXTI->PEND|=0xf<<5;
}
贪吃蛇整体功能实现
利用随机数随机生成食物,通过4个按键控制上下左右方向,检测是否吃到食物,没吃到一个食物蛇身长度加1,蛇碰到墙壁或碰到蛇身则游戏结束。
u8 SnakeGameStart(void)
{
u8 x=10;
u8 y=30;
u8 food_x,food_y;
u8 stat=0;
char buff[20];
OLED_DrawRectangle(0, 0, 127, 63);//绘制矩形
OLED_DrawRectangle(1, 1, 126, 62);//绘制矩形
//创建蛇身1
snake_info[node_cnt].x=x;
snake_info[node_cnt].y=y;
node_cnt++;
//创建蛇身2
snake_info[node_cnt].x=x+5;
snake_info[node_cnt].y=y;
//保存蛇头坐标
x=snake_info[node_cnt].x;
y=snake_info[node_cnt].y;
node_cnt++;
OLED_DrawSnakebody(snake_info,node_cnt,1);//绘制蛇身
//生成实物坐标
food_x=((rand()%115)/5)*5+5;
food_y=((rand()%50)/5)*5+5;
OLED_DrawSnake(food_x,food_y,1);//绘制实物
OLED_Refresh();//更新数据到屏幕
while(1)
{
if(snake_info[node_cnt-1].x>=125 || snake_info[node_cnt-1].y>=60)//判断是否撞墙
{
snprintf(buff,sizeof(buff),"Mark:%d",node_cnt-2);
OLED_DisplayStr(36,8,8,16,buff);//字符串显示
OLED_DisplayStr(10,28,12,24,"Game over");//字符串显示
OLED_Refresh();//更新数据到屏幕
key_val=0;
node_cnt=0;//清空节点
break;//游戏结束
}
if(snake_info[node_cnt-1].x==food_x && snake_info[node_cnt-1].y==food_y)//判断是否吃到食物
{
if(stat&1<<0 || stat&1<<1)//向上或向下时y+5
{
snake_info[node_cnt].x=food_x;
snake_info[node_cnt].y=food_y+5;
}
else //向左或向右时x+5
{
snake_info[node_cnt].x=food_x+5;
snake_info[node_cnt].y=food_y;
}
node_cnt++;
while(1)//保证重新生成的实物不再蛇身上
{
food_x=((rand()%115)/5)*5+5;
if(OLED_GetSnakeFoodX(food_x,node_cnt)==0)break;
}
while(1)
{
food_y=((rand()%50)/5)*5+5;
if(OLED_GetSnakeFoodY(food_y,node_cnt)==0)break;
}
OLED_DrawSnake(food_x,food_y,1);
OLED_Refresh();//更新数据到屏幕
}
switch(key_val)
{
case UP://上
stat&=~(0x3<<2);//清除左右标志
if(stat&1<<1){}//向下时不能直接向上
else
{
stat|=1<<0;//向上
y-=5;
Snake_Move(x,y);
}
break;
case DOWM://下
stat&=~(0x3<<2);//清除左右标志
if(stat&1<<0){}//在向上时不能直接向下
else
{
stat|=1<<1;//向下标志位
y+=5;
Snake_Move(x,y);
}
break;
case LEFT://左
stat&=~(0x3<<0);//清除上下标志
if(stat&1<<3){}//向右时不能直接向左
else
{
stat|=1<<2;//向左
x-=5;
Snake_Move(x,y);
}
break;
case RIGHT://右
stat&=~(0x3<<0);//清除上下标志
if(stat&1<<2){}//向左时不能直接向右
else
{
stat|=1<<3;
x+=5;
Snake_Move(x,y);
}
break;
}
OLED_Refresh();//更新数据到屏幕
Delay_Ms(600);
}
return 0;
}
定时器PWM输出控制LED
PWM是一种对模拟信号电平进行 数字编码的方法。通过高 分辨率计数器的使用,方波的占空比被调制用来对一个具体 模拟信号的电平进行编码。PWM信号仍然是数字的,因为在给定的任何时刻,满幅值的直流供电要么完全有(ON),要么完全无(OFF)。电压或电流源是以一种通(ON)或断(OFF)的重复脉冲序列被加到模拟负载上去的。通的时候即是直流供电被加到负载上的时候,断的时候即是供电被断开的时候。只要带宽足够,任何模拟值都可以使用PWM进行编码。
脉宽调制(PWM,Pulse Width Modulation)是利用微处理器的数字输出来对模拟电路进行控制的一种非常有效的技术,广泛应用在从测量、通信到功率控制与变换的许多领域中 。
PWM的一个优点是从处理器到 被控系统信号都是数字形式的,再进行数模转换。可将噪声影响降到最低(可以跟电脑一样)。噪声只有在强到足以将逻辑1改变为逻辑0或将逻辑0改变为逻辑1时,也才能对数字信号产生影响。
用户可以使用 PWM 模式产生一个信号,其占空比由Mx_CCDATx 寄存器的值决定,其频率由TIMx_AR 寄存器的值决定。 并且取决于 TIMx_CTRL1.CAMSEL 的值, TIM 可以在边沿对齐模式或中央对齐模式下产生 PWM 信号。
用户可以通过设置 TIMx_CCMODx. OCxMD=110 或设置 TIMx_CCMODx.OCxMD=111 来设置 PWM 模式 1 或 PWM 模式 2。 要使能预加载寄存器,用户必须设置相应的 TIMx_CCMODx.OCxPEN。 然后设置 TIMx_CTRL1.ARPEN 自动重装载预加载寄存器。
用户可以通过设置 TIMx_CCEN.CCxP 来设置 OCx 的极性。当 TIM 处于 PWM 模式时, TIMx_CNT 和 TIMx_CCDATx 的值总是相互比较。
只有当更新事件发生时,预加载寄存器才会转移到影子寄存器。 因此,用户必须在计数器开始计数之前通过设置 TIMx_EVTGEN.UDGN 来复位所有寄存器。
关于定时器PWM详细介绍请参考文档:基于N32G45的定时器PWM输出-电子发烧友网 https://www.elecfans.com/d/1948570.html
1.定时器PWM硬件配置
完成定时器基本功能配置,设置周期时间,设置分频系数,配置通道参数,输出PWM。
void TIM3_PWM_Out(u8 chx,u16 psc,u16 ar,u16 ccr)
{
RCC->APB2PCLKEN|=1<<3;
RCC->APB2PCLKEN|=1<<0;
AFIO->RMP_CFG&=~(0x3<<10);
AFIO->RMP_CFG|=2<<10;
AFIO->RMP_CFG&=~(0x7<<24);
AFIO->RMP_CFG|=1<<24;
GPIOB->PL_CFG&=0xFF00FFFF;
GPIOB->PL_CFG|=0x00BB0000;
RCC->APB1PCLKEN|=1<<1;
RCC->APB1PRST|=1<<1;
RCC->APB1PRST&=~(1<<1);
TIM3->CTRL1|=1<<7;
TIM3->PSC=psc-1;
TIM3->AR=ar;
if(chx&0x1)
{
TIM3->CCMOD1&=~(0x3<<0);
TIM3->CCMOD1|=1<<2;
TIM3->CCMOD1|=1<<3;
TIM3->CCMOD1|=0x6<<4;
TIM3->CCDAT1=ccr;
TIM3->CCEN|=1<<0;
}
if(chx&1<<1)
{
TIM3->CCMOD1&=~(0x3<<8);
TIM3->CCMOD1|=1<<10;
TIM3->CCMOD1|=1<<11;
TIM3->CCMOD1|=0x6<<12;
TIM3->CCDAT2=ccr;
TIM3->CCEN|=1<<4;
}
TIM3->CTRL1|=1<<0;
}
整体功能实现
基本外设初始化(LED、外部中断、串口、OLED屏幕)。设置OLED启动界面,通过按键进入贪吃蛇游戏。
int main()
{
LED_Init();
EXTI_Init();
USART_Init(115200);
OLED_Init();
TIM3_PWM_Out(1,7200,10000,5000);
BB:
OLED_ClearGram(0x0);
key_val=0;
OLED_DisplayFont(28,0,24,0);
OLED_DisplayFont(28+24,0,24,1);
OLED_DisplayFont(28+24*2,0,24,2);
OLED_DisplayFont(20,30,16,10);
OLED_DisplayFont(20+16,30,16,11);
OLED_DisplayStr(20+16*2,30,8,16,":WK");
OLED_DisplayFont(20,47,16,4);
OLED_DisplayFont(20+16,47,16,5);
OLED_DisplayStr(20+16*2,47,8,16,":KEY1");
OLED_Refresh();
while(1)
{
if(key_val&0x1)
{
printf("查看帮助\n");
OLED_Refresh2();
OLED_DisplayFont(20,0,16,6);
OLED_DisplayStr(20+16,0,8,16,":KEY2");
OLED_DisplayFont(20,16,16,7);
OLED_DisplayStr(20+16,16,8,16,":KEY1");
OLED_DisplayFont(20,16*2,16,8);
OLED_DisplayStr(20+16,16*2,8,16,":WKUP");
OLED_DisplayFont(20,16*3,16,9);
OLED_DisplayStr(20+16,16*3,8,16,":KEY3");
OLED_Refresh();
Delay_Ms(3000);
key_val=0;
goto BB;
}
else if(key_val&0x2)
{
printf("开始游戏\n");
break;
}
}
key_val=0;
OLED_Refresh2();
srand(1);
AA:
SnakeGameStart();
while(1)
{
if(key_val)
{
OLED_Refresh2();
key_val=0;
goto AA;
}
}
}
*附件:贪吃蛇游戏.rar