系列文章目录


前言

使用的是普中A2开发板。

有两个版本:
①STC32G12K128,SPI+DMA驱动WS2812B
②STC8051U34K64,PWM+DMA驱动WS2812B

本文代码对应的是版本①

【单片机】STC32G12K128
【频率】35.0000MHz
【外设】WS2812B彩色点阵屏(8X8)、矩阵按键

效果查看/操作演示:B站搜索“甘腾胜”或“gantengsheng”查看。
源代码下载:B站对应视频的简介有工程文件下载链接。

一、效果展示

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

二、各模块代码

1、WS2812B

h文件

#ifndef __WS2812B_H__
#define __WS2812B_H__

void WS2812B_Clear(void);
void WS2812B_Update(void);
void WS2812B_Init(void);
void WS2812B_SetBuffer(unsigned char X,unsigned char Y,unsigned char R,unsigned char G,unsigned char B);
void WS2812B_HS(unsigned char *Array,unsigned char R,unsigned char G,unsigned char B,unsigned int Offset);
void WS2812B_VS(unsigned char *Array,unsigned char R,unsigned char G,unsigned char B,unsigned int Offset);

#endif

c文件

#include <STC32G.H>

//LED灯珠个数
#define	LED_Quantity	64

//LED灯对应SPI字节数
#define	SPI_Quantity	LED_Quantity*12

/** WB2812B彩屏数据传输顺序:
  * 
  * 本案例使用的点阵屏的数据传输顺序是:从下到上,从左到右
  * 如果是其他情况,则需要修改相关函数
  * 
  *  ^    ^    ^
  *  |    |    |
  *  ^    ^    ^
  *  |    |    |
  *  |    |    |
  *  |    |    |
  */

/** 屏幕显存数组数据存储格式:
  * 
  * 三个字节为一组,三个字节(G、R、B)对应一个像素点
  * 按之字形跟屏幕对应
  * 
  *                                    -------------------> 
  *                                   |                    |
  *                                   |                    |
  *                                   |                    |
  *     WS2812B_Buffer[0][0]          |           WS2812B_Buffer[16][0](G:绿)
  * 	WS2812B_Buffer[0][1]          |           WS2812B_Buffer[16][1](R:红)
  *     WS2812B_Buffer[0][2]          |           WS2812B_Buffer[16][2](B:蓝)
  *             |                     |                    .                    
  *             |                     |                    .                    
  *             v                     |                    .                    
  *     WS2812B_Buffer[1][0]          |                    .
  * 	WS2812B_Buffer[1][1]          ^                    .
  *     WS2812B_Buffer[1][2]          |                    .
  *             .                     |                    .
  *             .                     |                    .
  *             .                     |                    .
  *     WS2812B_Buffer[15][0]         |
  * 	WS2812B_Buffer[15][1]         |
  *     WS2812B_Buffer[15][2]         |
  *             |                     |
  *             |                     |
  *             v                     |
  *              -------------------->
  */

/** 屏幕坐标轴定义:
  * 
  * 左上角为原点(0,0)
  * 横向向右为X轴,取值范围:0~7
  * 纵向向下为Y轴,取值范围:0~7
  * 
  * 	0		X轴		7
  * 	.————————————————>
  *   0 |
  * 	|
  * 	|
  * Y轴	|
  * 	|
  * 	|
  *   7 |
  * 	v
  */

/** 点阵屏显存数组
  * 
  * 用LED_Quantity*3个字节作为WS2812B彩色点阵屏的显示缓存
  * 共有LED_Quantity个灯珠,每个灯珠需要写入24Bit(3个字节)控制显示的颜色
  * WS2812B芯片要求按G、R、B的顺序发送数据,并且每个字节要高位先发
  * 缓存数组中每三个字节为一组,每一组分别对应一个灯珠的G(绿)、R(红)、B(蓝)三原色
  * 想改变屏幕显示,先对此显存数组进行修改
  * 随后调用WS2812B_Update函数
  * 才会将显存数组的数据写入每个灯珠的WS2812B芯片内
  */
unsigned char xdata WS2812B_Buffer[LED_Quantity][3];

/** SPI缓存数组
  * 
  * 使用SPI-MOSI输出直接驱动WS2812B彩屏,通过DMA传输
  * 本例驱动64个灯
  * 本例使用P40输出驱动信号
  * 800KHz码率
  * 数据0(1/4占空比): H=0.3125us,L=0.9375us
  * 数据1(3/4占空比): H=0.9375us,L=0.3125us
  * RESET>50us
  * 
  * WS2812S的标准时序如下:
  * TH+TL=1.25us±150ns,RES>50us
  * T0H=0.25us±150ns=0.10us~0.40us
  * T0L=1.00us±150ns=0.85us~1.15us
  * T1H=1.00us±150ns=0.85us~1.15us
  * T1L=0.25us±150ns=0.10us~0.40us
  * 两个位数据之间的间隔要小于RESET的50us
  * 
  * 用SPI传输,MSB先发,每个字节高4位和低4位分别对应WS2812的一个位数据
  * SPI数据位       B7 B6 B5 B4    B3 B2 B1 B0
  *                 1  0  0  0     1  1  1  0
  *                 WS2812数据0    WS2812数据1 
  * 
  * 参考了国芯技术交流网站(https://www.stcaimcu.com/)梁工的代码
  * 
  * 本案例实测烧录频率从22.1184MHz~35Mhz均能驱动WS2812B
  * 
  */
unsigned char xdata  SPI_Buffer[SPI_Quantity];	//SPI缓存

/**
  * 函    数:更新SPI缓存数组的数据(根据WS2812B缓存数组的数据来更新)
  * 参    数:无
  * 返 回 值:无
  */
void UpdateSPI(void)
{
	unsigned char xdata *px;	//定义一个指向片外扩展RAM的指针
	unsigned int i,j=0;
	unsigned char k;
	unsigned char Temp;	//临时变量

	for(i=0;i<SPI_Quantity;i++)	//先清空SPI数组数据
	{
		SPI_Buffer[i]=0;
	}

	px = &WS2812B_Buffer[0][0];	//获取数组WS2812B_Buffer的首地址

	for(i=0;i<LED_Quantity*3;i++)
	{
		Temp=*(px+i);
		for(k=0;k<4;k++)
		{
			if( Temp & (0x80>>(2*k)) )
			{
				SPI_Buffer[j] = 0xE0;	//数据1(高四位)
			}
			else
			{
				SPI_Buffer[j] = 0x80;	//数据0(高四位)
			}
			if( Temp & (0x80>>(2*k+1)) )
			{
				SPI_Buffer[j] |= 0x0E;	//数据1(低四位)
			}
			else
			{
				SPI_Buffer[j] |= 0x08;	//数据0(低四位)
			}
			j++;
		}
	}
}

/**
  * 函    数:配置SPI
  * 参    数:无
  * 返 回 值:无
  */
void SPI_Init(void)
{
	HSCLKDIV=0;	//高速时钟不分频
	
	/*
	【SPCTL】SPI控制寄存器,SPI速度控制
	B7        B6        B5        B4        B3        B2        B1B0
	SSIG      SPEN      DORD      MSTR      CPOL      CPHA      SPR[1:0]

	SSIG:SS 引脚功能控制位
	0:SS 引脚确定器件是主机还是从机
	1:忽略 SS 引脚功能,使用 MSTR 确定器件是主机还是从机

	SPEN:SPI 使能控制位
	0:关闭 SPI 功能
	1:使能 SPI 功能

	DORD:SPI 数据位发送/接收的顺序
	0:先发送/接收数据的高位(MSB)
	1:先发送/接收数据的低位(LSB)

	MSTR:器件主/从模式选择位
	设置主机模式:
	若 SSIG=0,则 SS 管脚必须为高电平且设置 MSTR 为 1
	若 SSIG=1,则只需要设置 MSTR 为 1(忽略 SS 管脚的电平)
	设置从机模式:
	若 SSIG=0,则 SS 管脚必须为低电平(与 MSTR 位无关)
	若 SSIG=1,则只需要设置 MSTR 为 0(忽略 SS 管脚的电平)

	CPOL:SPI 时钟极性控制
	0:SCLK 空闲时为低电平,SCLK 的前时钟沿为上升沿,后时钟沿为下降沿
	1:SCLK 空闲时为高电平,SCLK 的前时钟沿为下降沿,后时钟沿为上升沿

	CPHA:SPI 时钟相位控制
	0:数据 SS 管脚为低电平驱动第一位数据并在 SCLK 的后时钟沿改变数据,前时钟沿采样数据(必须 SSIG=0)
	1:数据在 SCLK 的前时钟沿驱动,后时钟沿采样

	SPR[1:0]:SPI 时钟频率选择	

	SPR[1:0] SCLK 频率
	00 SPI输入时钟/4
	01 SPI输入时钟/8
	10 SPI输入时钟/16
	11 SPI输入时钟/2	
	
	*/
	SPCTL=0xD5;

	/*
	SPI_S[1:0]:SPI 功能脚选择位

	SPI_S[1:0]     SS               MOSI        MISO        SCLK
	  00           P1.2/P5.4[1]     P1.3        P1.4        P1.5
	  01           P2.2             P2.3        P2.4        P2.5
	  10           P5.4             P4.0        P4.1        P4.3
	  11           P3.5             P3.4        P3.3        P3.2
	
	有 P1.2 口的型号,此功能在 P1.2 口上,对于无 P1.2 口的型号,此功能在 P5.4 口上
	*/
	//下面四个只能选一个解除注释使用
//	SPI_S1=0;
//	SPI_S0=0;	//对应SPI_S[1:0]为00的情况,即用P13作为MOSI口,用P14作为MISO口
//	P14=0;	//MISO = 0, 目的是让MOSI输出完毕保持低电平

//	SPI_S1=0;
//	SPI_S0=1;	//对应SPI_S[1:0]为01的情况,即用P23作为MOSI口,用P24作为MISO口
//	P24=0;	//MISO = 0, 目的是让MOSI输出完毕保持低电平

	SPI_S1=1;
	SPI_S0=0;	//对应SPI_S[1:0]为10的情况,即用P40作为MOSI口,用P41作为MISO口
	P41=0;	//MISO = 0, 目的是让MOSI输出完毕保持低电平

//	SPI_S1=1;
//	SPI_S0=1;	//对应SPI_S[1:0]为11的情况,即用P34作为MOSI口,用P34作为MISO口
//	P33=0;	//MISO = 0, 目的是让MOSI输出完毕保持低电平
}

/**
  * 函    数:开始传输数据
  * 参    数:无
  * 返 回 值:无
  */
void DMA2SPI_Start(void)
{
	unsigned int Address;	//用来存储地址
	Address=(unsigned int)&SPI_Buffer[0];	//取首地址,强制转换为unsigned int型
	DMA_SPI_TXAH=(unsigned char)(Address>>8);	//发送地址寄存器高字节
	DMA_SPI_TXAL=(unsigned char)Address;	//发送地址寄存器低字节
	DMA_SPI_AMTH=(unsigned char)((SPI_Quantity-1)/256);	//设置传输总字节数 = n+1
	DMA_SPI_AMT=(unsigned char)((SPI_Quantity-1)%256);	//设置传输总字节数 = n+1

	/*
	【DMA_SPI_STA】SPI_DMA 状态寄存器
	-        -        -        -        -        B2        B1        B0
	                                             TXOVW     RXLOSS    SPIIF
	
	SPIIF:SPI_DMA中断请求标志位。
	当SPI_DMA数据交换完成后,硬件自动将SPIIF置1,若使能SPI_DMA中断则进入中断服务程序。标志位需软件清零。

	RXLOSS:SPI_DMA 接收数据丢弃标志位。
	SPI_DMA 操作过程中,当 XRAM 总线过于繁忙,来不及清空 SPI_DMA 的接收 FIFO 导致 SPI_DMA 接收的数据自动丢弃时,
	硬件硬件自动将 RXLOSS 置 1。标志位需软件清零。

	TXOVW:SPI_DMA 数据覆盖标志位。
	SPI_DMA 正在数据传输过程中,主机模式的 SPI 写 SPDAT 寄存器再次触发 SPI 数据传输时,
	会导致数据传输失败,此时硬件硬件自动将 TXOVW 置 1。标志位需软件清零。
	*/
	DMA_SPI_STA=0x00;

	/*
	【DMA_SPI_CFG】SPI_DMA 配置寄存器
	B7        B6        B5        B4        B3B2        B1B0
	SPIIE     ACT_TX    ACT_RX    -         SPIIP[1:0]  SPIPTY[1:0]
	
	SPIIE:SPI_DMA 中断使能控制位
	0:禁止 SPI_DMA 中断
	1:允许 SPI_DMA 中断
	
	ACT_TX:SPI_DMA 发送数据控制位
	0:禁止 SPI_DMA 发送数据。主机模式时,SPI 只发送时钟到 SCLK 端口,但不从 XRAM 读取数据,
	也不向 MOSI 端口上发送数据;从机模式时,SPI 不从 XRAM 读取数据,也不向 MISO 端口上发送数据
	1:允许 SPI_DMA 发送数据。主机模式时,SPI 发送时钟到 SCLK 端口,同时从 XRAM 读取数据,
	并将数据发送到 MOSI 端口;从机模式时,SPI 从 XRAM 读取数据,并将数据发送到 MISO 端口
	
	ACT_RX:SPI_DMA 接收数据控制位
	0:禁止 SPI_DMA 接收数据。主机模式时,SPI 只发送时钟到 SCLK 端口,但不从 MISO 端口读取
	数据,也向 XRAM 写数据;从机模式时,SPI 不从 MOSI 端口读取数据,也不向 XRAM 写数据。
	1:允许 SPI_DMA 接收数据。主机模式时,SPI 发送时钟到 SCLK 端口,同时从 MISO 端口读取数
	据,并将数据写入 XRAM;从机模式时,SPI 从 MOSI 端口读取数据,并写入 XRAM。
	
	SPIIP[1:0]:SPI_DMA 中断优先级控制位
	SPIIP[1:0]       中断优先级
	   00            最低级(0)
	   01            较低级(1)
	   10            较高级(2)
	   11            最高级(3)
	
	
	SPIPTY[1:0]:SPI_DMA 数据总线访问优先级控制位
	SPIPTY [1:0]       总线优先级
	    00             最低级(0)
	    01             较低级(1)
	    10             较高级(2)
	    11             最高级(3)
	*/
	DMA_SPI_CFG=0x40;

	/*
	【DMA_SPI_CFG2】SPI_DMA 配置寄存器 2
	B7        B6        B5        B4        B3        B2        B1B0
	-         -         -         -         -         WRPSS     SSS[1:0]
	
	WRPSS:SPI_DMA 过程中使能 SS 脚控制位
	0:SPI_DMA 传输过程中,不自动控制 SS 脚
	1:SPI_DMA 传输过程中,自动拉低 SS 脚,传输完成后,自动恢复原始状态
	
	SSS[1:0]:SPI_DMA 过程中,自动控制 SS 选择位
	SSS[1:0]     SS 脚
	  00        P1.2/P5.4
	  01        P2.2
	  10        P7.4
	  11        P3.5
	有 P1.2 口的型号,此功能在 P1.2 口上,对于无 P1.2 口的型号,此功能在 P5.4 口上
	*/
	DMA_SPI_CFG2=0x00;

	/*
	【DMA_SPI_CR】SPI_DMA 控制寄存器
	B7        B6        B5        B4        B3        B2        B1        B0
	ENSPI     TRIG_M    TRIG_S    -         -         -         -         CLRFIFO
	
	ENSPI:SPI_DMA 功能使能控制位
	0:禁止 SPI_DMA 功能
	1:允许 SPI_DMA 功能
	
	TRIG_M:SPI_DMA 主机模式触发控制位
	0:写 0 无效
	1:写 1 开始 SPI_DMA 主机模式操作
	
	TRIG_S:SPI_DMA 从机模式触发控制位
	0:写 0 无效
	1:写 1 开始 SPI_DMA 从机模式操作
	
	CLRFIFO:清除 SPI_DMA 接收 FIFO 控制位
	0:写 0 无效
	1:开始 SPI_DMA 操作前,先清空 SPI_DMA 内置的 FIFO
	*/
	DMA_SPI_CR=0xC1;
}


/**
  * 函    数:WS2812B彩屏清空显存数组
  * 参    数:无
  * 返 回 值:无
  * 说    明:需要调用WS2812B_Update函数才能更新到屏幕显示
  */
void WS2812B_Clear(void)
{
	unsigned int i;
	for(i=0;i<LED_Quantity;i++)
	{
		WS2812B_Buffer[i][0]=0;
		WS2812B_Buffer[i][1]=0;
		WS2812B_Buffer[i][2]=0;
	}
}

/**
  * 函    数:WS2812B彩屏更新屏幕显示
  * 参    数:无
  * 返 回 值:无
  * 说    明:每个灯珠要按G(绿)、R(红)、B(蓝)的顺序发送数据,且高位先发
  */
void WS2812B_Update(void)
{
	while( (DMA_SPI_STA & 0x01) == 0 );	//等待SPI_DMA中断请求标志位置1(即等待传输完成)
	DMA_SPI_STA &= ~0x01;	//软件清零标志位
	UpdateSPI();	//更新SPI缓存数组
	DMA2SPI_Start();	//DMA开始传输数据
}

/**
  * 函    数:WS2812B彩屏初始化
  * 参    数:无
  * 返 回 值:无
  */
void WS2812B_Init(void)
{
	DMA_SPI_STA |= 0x01;	//SPI_DMA中断请求标志位置1,这一步不能少,没有的话则无法开始DMA传输
	SPI_Init();
	WS2812B_Clear();
	WS2812B_Update();
}

/**
  * 函    数:WS2812B彩屏设置一个灯珠的缓存
  * 参    数:X 指定灯珠的横坐标,范围:0~7
  * 参    数:Y 指定灯珠的纵坐标,范围:0~7
  * 参    数:R(Red)红,范围:0~255
  * 参    数:G(Green)绿,范围:0~255
  * 参    数:B(Blue)蓝,范围:0~255
  * 返 回 值:无
  * 说    明:需要调用WS2812B_Update函数才能更新屏幕显示
  */
void WS2812B_SetBuffer(unsigned char X,unsigned char Y,unsigned char R,unsigned char G,unsigned char B)
{
	WS2812B_Buffer[8*X+7-Y][0]=G;	//绿
	WS2812B_Buffer[8*X+7-Y][1]=R;	//红
	WS2812B_Buffer[8*X+7-Y][2]=B;	//蓝
}

/**
  * 函    数:WS2812B彩屏按指定的偏移量显示指定数组的数据(用来控制屏幕左右滚动显示内容)
  * 参    数:Array 数组的地址(指针),数组名就是数组的首地址
  * 参    数:R 红(Red),范围:0~255
  * 参    数:G 绿(Green),范围:0~255
  * 参    数:B 蓝(Blue),范围:0~255
  * 参    数:Offset 偏移量,向左偏移Offset个像素,范围:0~65535
  * 返 回 值:无
  * 说    明:Offset增加则屏幕向左滚动显示,Offset减小则屏幕向右滚动显示
  * 说    明:要求数组数据逐列式取模,高位在下
  * 说    明:HS:Horizontal Scroll,水平滚动
  */
void WS2812B_HS(unsigned char *Array,unsigned char R,unsigned char G,unsigned char B,unsigned int Offset)
{
	unsigned char i,j;
	unsigned char Temp[8];
	Array+=Offset;
	
	for(i=0;i<8;i++)	//将偏移后的数据保存到缓存数组Temp中,一个Bit对应一个灯
	{
		Temp[i]=*(Array+i);
	}
	for(i=0;i<8;i++)	//每个亮点都写入相同的颜色
	{
		for(j=0;j<8;j++)
		{
			if(Temp[i]&(0x01<<j))
			{
				WS2812B_SetBuffer(i,j,R,G,B);
			}
			else
			{
				WS2812B_SetBuffer(i,j,0,0,0);
			}
		}
	}
	WS2812B_Update();
}


/**
  * 函    数:WS2812B彩屏按指定的偏移量显示指定数组的数据(用来控制屏幕上下滚动显示内容)
  * 参    数:Array 数组的地址(指针),数组名就是数组的首地址
  * 参    数:R 红(Red),范围:0~255
  * 参    数:G 绿(Green),范围:0~255
  * 参    数:B 蓝(Blue),范围:0~255
  * 参    数:Offset 偏移量,向左偏移Offset个像素,范围:0~65535
  * 返 回 值:无
  * 说    明:Offset增加则屏幕向上滚动显示,Offset减小则屏幕向下滚动显示
  * 说    明:要求数组数据逐列式取模,高位在下
  * 说    明:VS:Vertical Scroll,竖直滚动
  */
void WS2812B_VS(unsigned char *Array,unsigned char R,unsigned char G,unsigned char B,unsigned int Offset)
{
	unsigned char i,j;
	unsigned char m,n;
	unsigned char Temp[8];
	m=Offset/8;
	n=Offset%8;
	Array+=m*8;
	for(i=0;i<8;i++)	//将偏移后的数据保存到缓存数组Temp中,一个Bit对应一个灯
	{
		Temp[i] = (*(Array+i)>>n) | (*(Array+i+8)<<(8-n));
	}
	for(i=0;i<8;i++)	//每个亮点都写入相同的颜色
	{
		for(j=0;j<8;j++)
		{
			if(Temp[i]&(0x01<<j))
			{
				WS2812B_SetBuffer(i,j,R,G,B);
			}
			else
			{
				WS2812B_SetBuffer(i,j,0,0,0);
			}
		}
	}
	WS2812B_Update();
}

2、矩阵按键

h文件

/*方法来源:B站江协科技的编程技巧(第二期)*/
#ifndef __MATRIXKEY_H__
#define __MATRIXKEY_H__

//各按键对应数组的索引
#define S1	0
#define S2	1
#define S3	2
#define S4	3
#define S5	4
#define S6	5
#define S7	6
#define S8	7
#define S9	8
#define S10	9
#define S11	10
#define S12	11
#define S13	12
#define S14	13
#define S15	14
#define S16	15

//标志位掩码
#define HOLD	0x01	//按住
#define DOWN	0x02	//按下
#define UP		0x04	//松开
#define SINGLE	0x08	//单击
#define DOUBLE	0x10	//双击
#define LONG	0x20	//长按
#define REPEAT	0x40	//重复

void Key_Clear(void);
unsigned char Key(unsigned char n,unsigned char Flag);
void Key_Tick(void);

#endif

c文件

/*方法来源:B站江协科技的编程技巧(第二期)*/
#include <STC32G.H>
#include <INTRINS.H>
#include "MatrixKey.h"

#define KEY_DELAY _nop_()	//按键检测的延时

#define KEY_PRESSED		1	//按键已按下
#define KEY_UNPRESSED	0	//按键未按下

//数值单位:定时器中断函数中相邻两次调用函数Key_Tick的时间间隔
#define KEY_TIME_DOUBLE		0	//双击判定的等待时长
#define KEY_TIME_LONG		2	//长按判定的等待时长
#define KEY_TIME_REPEAT		5	//长按后,重复标志位再次置1的时长
//本案例中,以上数值的单位是:20ms

//引脚配置
#define Row1 P17	//行1
#define Row2 P16	//行2
#define Row3 P15	//行3
#define Row4 P14	//行4
#define Column1 P13	//列1
#define Column2 P47	//列2
#define Column3 P11	//列3
#define Column4 P10	//列4

/** 按键标志数组
  * 
  * 一个字节对应8位,分别为:B7 B6 B5 B4 B3 B2 B1 B0
  * 【B0】1:一直按住,0:未按下
  * 【B1】1:按下瞬间,0:不是按下瞬间
  * 【B2】1:松开瞬间,0:不是松开瞬间
  * 【B3】1:单击,0:不是单击
  * 【B4】1:双击,0:不是双击
  * 【B5】1:长按,0:不是长按
  * 【B6】1:重复,0:未重复
  * 【B7】保留位
  * 其中单击、双击、长按互斥(即这三个对应的标志位最多只能有一个是1)
  * 除B0外,其他标志位在读取后置0
  * xdata:变量保存在片外RAM
  */
unsigned char xdata Key_Flag[16];

/**
  * 函    数:获取按键状态(检测按键是否按下)
  * 参    数:n 按键索引,范围:0~15
  * 返 回 值:按下返回1,未按下返回0
  */
unsigned char Key_GetState(unsigned char n)
{
	Row1=1;Row2=1;Row3=1;Row4=1;	//释放所有行
	Column1=1;Column2=1;Column3=1;Column4=1;	//释放所有列

	if(n==S1)
	{
		Row1=0;KEY_DELAY;	//因单片机较快,所以需要延时一下,否则不能正常检测按键
		if(Column1==0)
		{
			return KEY_PRESSED;
		}
	}
	else if(n==S2)
	{
		Row1=0;KEY_DELAY;
		if(Column2==0)
		{
			return KEY_PRESSED;
		}
	}
	else if(n==S3)
	{
		Row1=0;KEY_DELAY;
		if(Column3==0)
		{
			return KEY_PRESSED;
		}
	}
	else if(n==S4)
	{
		Row1=0;KEY_DELAY;
		if(Column4==0)
		{
			return KEY_PRESSED;
		}
	}
	else if(n==S5)
	{
		Row2=0;KEY_DELAY;
		if(Column1==0)
		{
			return KEY_PRESSED;
		}
	}
	else if(n==S6)
	{
		Row2=0;KEY_DELAY;
		if(Column2==0)
		{
			return KEY_PRESSED;
		}
	}
	else if(n==S7)
	{
		Row2=0;KEY_DELAY;
		if(Column3==0)
		{
			return KEY_PRESSED;
		}
	}
	else if(n==S8)
	{
		Row2=0;KEY_DELAY;
		if(Column4==0)
		{
			return KEY_PRESSED;
		}
	}
	else if(n==S9)
	{
		Row3=0;KEY_DELAY;
		if(Column1==0)
		{
			return KEY_PRESSED;
		}
	}
	else if(n==S10)
	{
		Row3=0;KEY_DELAY;
		if(Column2==0)
		{
			return KEY_PRESSED;
		}
	}
	else if(n==S11)
	{
		Row3=0;KEY_DELAY;
		if(Column3==0)
		{
			return KEY_PRESSED;
		}
	}
	else if(n==S12)
	{
		Row3=0;KEY_DELAY;
		if(Column4==0)
		{
			return KEY_PRESSED;
		}
	}
	else if(n==S13)
	{
		Row4=0;KEY_DELAY;
		if(Column1==0)
		{
			return KEY_PRESSED;
		}
	}
	else if(n==S14)
	{
		Row4=0;KEY_DELAY;
		if(Column2==0)
		{
			return KEY_PRESSED;
		}
	}
	else if(n==S15)
	{
		Row4=0;KEY_DELAY;
		if(Column3==0)
		{
			return KEY_PRESSED;
		}
	}
	else if(n==S16)
	{
		Row4=0;KEY_DELAY;
		if(Column4==0)
		{
			return KEY_PRESSED;
		}
	}

	return KEY_UNPRESSED;
}

/**
  * 函    数:清空所有按键的所有标志位
  * 参    数:无
  * 返 回 值:无
  * 说    明:防止切换模式的时候受上一模式所按按键的影响
  */
void Key_Clear(void)
{
	unsigned char i;
	for(i=0;i<16;i++)
	{
		Key_Flag[i]=0;
	}
}
/**
  * 函    数:获取按键标志位的值
  * 参    数:n 按键索引,范围:0~15
  * 参    数:Flag 标志位掩码,用来获取标志的某一位的值为1还是0
  * 返 回 值:1或者0
  */
unsigned char Key(unsigned char n, unsigned char Flag)
{
	if(Key_Flag[n] & Flag)
	{
		if(Flag != HOLD)
		{
			Key_Flag[n] &= ~Flag;
		}
		return 1;
	}
	return 0;
}

/**
  * 函    数:按键检测驱动函数,在定时器中断函数中使用
  * 参    数:无
  * 返 回 值:无
  * 说    明:最后的else的目的是防止S[i]的初值在0~4之外导致检测不了单击、双击、长按、重复
  */
void Key_Tick(void)
{
	static unsigned char xdata i;
	static unsigned char xdata NowState[16];
	static unsigned char xdata LastState[16];
	static unsigned char xdata S[16];
	static unsigned char xdata KeyTime[16];

	for(i=0;i<16;i++)
	{
		if(KeyTime[i]>0)
		{
			KeyTime[i]--;
		}
	}
	
	for(i=0;i<16;i++)
	{
		LastState[i]=NowState[i];
		NowState[i]=Key_GetState(i);
		
		if(NowState[i] == KEY_PRESSED)
		{
			Key_Flag[i] |= HOLD;
		}
		else
		{
			Key_Flag[i] &= ~HOLD;
		}
		
		if(NowState[i] == KEY_PRESSED && LastState[i] == KEY_UNPRESSED)
		{
			Key_Flag[i] |= DOWN;
		}
		
		if(NowState[i] == KEY_UNPRESSED && LastState[i] == KEY_PRESSED)
		{
			Key_Flag[i] |= UP;
		}
		
		if(S[i] == 0)
		{
			if(NowState[i] == KEY_PRESSED)
			{
				KeyTime[i]=KEY_TIME_LONG;
				S[i]=1;
			}
		}
		else if(S[i] == 1)
		{
			if(NowState[i] == KEY_UNPRESSED)
			{
				KeyTime[i]=KEY_TIME_DOUBLE;
				S[i]=2;
			}
			else if(KeyTime[i] == 0)
			{
				KeyTime[i]=KEY_TIME_REPEAT;
				Key_Flag[i] |= LONG;
				S[i]=4;
			}
		}
		else if(S[i]==2)
		{
			if(NowState[i] == KEY_PRESSED)
			{
				Key_Flag[i] |= DOUBLE;
				S[i]=3;
			}
			else if(KeyTime[i] == 0)
			{
				Key_Flag[i] |= SINGLE;
				S[i]=0;
			}
		}
		else if(S[i] == 3)
		{
			if(NowState[i] == KEY_UNPRESSED)
			{
				S[i]=0;
			}
		}
		else if(S[i]==4)
		{
			if(NowState[i] == KEY_UNPRESSED)
			{
				S[i]=0;
			}
			else if(KeyTime[i] == 0)
			{
				KeyTime[i]=KEY_TIME_REPEAT;
				Key_Flag[i] |= REPEAT;
				S[i]=4;
			}
		}
		else
		{
			S[i]=0;
		}
	}
}

3、定时器0

h文件

#ifndef __TIMER0_H__
#define __TIMER0_H__

void Timer0_Init(void);

#endif

c文件

#include <STC32G.H>

/**
  * 函    数:定时器0初始化
  * 参    数:无
  * 返 回 值:无
  */
void Timer0_Init(void)
{	
	AUXR|=0x80;	//定时器时钟1T模式
	TMOD&=0xF0;	//设置定时器模式为16位不自动重装模式
	TMOD|=0x01;	//设置定时器模式为16位不自动重装模式
	TL0=0x48;	//设置定时初值,定时1ms,1T@35.0000MHz
	TH0=0x77;	//设置定时初值,定时1ms,1T@35.0000MHz
	TF0=0;		//清除TF0标志
	TR0=1;		//定时器0开始计时
	ET0=1;		//打开定时器0中断允许
	EA=1;		//打开总中断
	PT0=0;		//设置定时器0的优先级
}

/*定时器中断函数模板
void Timer0_Routine() interrupt 1	//定时器0中断函数
{
	static unsigned int T0Count;	//定义静态变量
	TL0=0x48;	//设置定时初值,定时1ms,1T@35.0000MHz
	TH0=0x77;	//设置定时初值,定时1ms,1T@35.0000MHz
	T0Count++;
	if(T0Count>=1000)
	{
		T0Count=0;
		
	}
}
*/

三、主函数

main.c

/*by甘腾胜@20250711
【效果查看/操作演示】B站搜索“甘腾胜”或“gantengsheng”查看
【单片机】STC32G12K128
【频率】1T@35.0000MHz
【外设】WS2812B彩屏(8X8)、矩阵按键
【接线】
(1)WS2812B彩屏:DIN接P40
(2)矩阵按键:第1行接P17,第2行接P16,第3行接P15,第4行接P14,第1列接P13,第2列接P47,第3列接P11,第4列接P10
【操作说明】
(1)上电后按S16在显示数据传输方向的流水灯和循环滚动显示游戏英文名的界面之间切换,按其他任意按键进入滚动显示难度的英文的界面
(2)滚动显示难度的英文的界面按任意按键进入难度选择界面
(3)难度选择界面按S10向上滚动数字,按S14向下滚动数字,按S16开始游戏
(4)游戏界面,S10:上,S14:下,S13:左,S15:右,按S16:暂停或继续
(5)游戏结束全屏闪烁界面按S16进入滚动显示得分的英文的界面
(6)滚动显示得分的英文的界面可按S16跳过
(7)循环滚动显示得分界面可按S12返回,重新开始游戏
*/

#include <STC32G.H>		//51单片机头文件
#include <STDLIB.H>		//随机函数
#include "WS2812B.h"	//WS2812B彩屏
#include "MatrixKey.h"	//矩阵按键
#include "Timer0.h"		//定时器0

/** 屏幕坐标轴定义:
  * 
  * 左上角为原点(0,0)
  * 横向向右为X轴,取值范围:0~7
  * 纵向向下为Y轴,取值范围:0~7
  * 
  * 	0		X轴		7
  * 	.————————————————>
  *   0 |
  * 	|
  * 	|
  * Y轴	|
  * 	|
  * 	|
  *   7 |
  * 	v
  */

/** 游戏模式说明
  * 
  * 0:循环滚动显示游戏英文名“<<SNAKE>>”
  * 1:滚动显示“难度”的英文“DIFFICULTY”
  * 2:难度选择界面(数字的范围是1~5)
  * 3:游戏进行中
  * 4:游戏结束全屏闪烁
  * 5:滚动显示“得分”的英文“SCORE”
  * 6:循环滚动显示得分
  */
unsigned char Mode;
unsigned char LastMode;	//上一次的游戏模式,用来切换模式时清空所有按键的所有标志位
unsigned char Offset;	//偏移量,用来控制英文字母和得分向左滚动显示(切换模式后清零)
unsigned char Offset1;	//偏移量,用来控制难度对应的数字上下滚动显示(切换模式后不清零)
bit OnceFlag=1;	//各模式中(切换为其他模式前)只执行一次的标志,类似于主函数主循环前的那部分,用于该模式的初始化,1:执行,0:不执行
bit FlashFlag;	//闪烁的标志,1:不显示,0:显示
bit PauseFlag;	//游戏时暂停的标志,1:暂停,0:不暂停
bit GameOverFlag;	//游戏结束的标志,1:游戏结束,0:游戏未结束
bit MoveFlag;	//蛇移动的标志,1:移动,0:不移动
unsigned int Score;	//游戏得分,范围:0~65535
unsigned char ScoreLength;	//游戏得分的位数,范围:1~5
/** 蛇身数据数组说明
  * 
  * 8X8点阵屏共64个像素,需要64个字节保存蛇身数据
  * 也可以用两个数组来分别存储横坐标和纵坐标
  * 高四位存储横坐标,范围:0~7(对应1~8列)
  * 低四位存储纵坐标,范围:0~7(对应1~8行)
  * 循环滚动使用数组数据
  * 即蛇头位置用索引为63的数据记录后,如果蛇再移动一次,则移动后用索引为0的数据记录
  * idata:变量保存在片内RAM的间接寻址区
  */
unsigned char xdata SnakeBody[64];
unsigned char NowDirection;	//蛇头现在移动的方向,0:向右,1:向上,2:向左,3:向下
unsigned char LastDirection;	//蛇头上一次移动的方向,0:向右,1:向上,2:向左,3:向下
unsigned char Length;	//蛇的长度,范围:2~64
unsigned char Head;	//蛇头对应数组SnakeBody中的数据的索引,范围:0~63
unsigned char Food;	//保存创造出来的食物的位置,高四位存储列位置,范围:0~7(对应1~8列),低四位存储行位置,范围:0~7(对应1~8行)
bit ScrollFlag;	//字母或数字滚动显示时,切换显示的标志,1:切换,0:不切换
bit ScrollUpFlag;	//难度选择界面,数字向上滚动显示的标志,1:滚动,0:不滚动
bit ScrollDownFlag;	//难度选择界面,数字向下滚动显示的标志,1:滚动,0:不滚动
unsigned char ScrollCount;	//上下滚动的计次
unsigned int T0Count_0;	//定时器0全局计数变量
unsigned char T0Count_1;	//定时器0全局计数变量
unsigned int Duration=1000;	//蛇移动的时间间隔,初始值为1000,单位:ms
unsigned char code MyColor[6][3]={	//预设颜色
15,0,0,	//红色
0,15,0,	//绿色
0,0,15,	//蓝色
8,8,0,	//黄色
8,0,8,	//紫色
0,8,8,	//青色
};
unsigned char xdata SnakeBuffer[8];	//蛇身缓存数组,一个字节对应屏幕一列,1Bit对应一个LED,高位在下
unsigned char FoodColor;	//食物的颜色,范围:0~5
unsigned char SnakeColor;	//上一次的食物的颜色,范围:0~5
bit IsShowWaterLED;	//上电后模式0是否要显示流水灯的标志,1:显示,0:不显示

/** 数组数据说明
  * 
  * 取模要求:逐列式取模,高位在下,亮点为1
  * 我分享的工程文件中有宽6高8的ASCII字符字模数据
  * xdata:变量保存在片外RAM
  * code:数据存储在Flash中
  */
unsigned char xdata ScoreShow[]={	//游戏得分(用于循环滚动显示),得分是多少位数,就显示多少位
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,	// 无显示
0x00,0x00,0x00,0x00,0x00,0x00,	// 得分最多五位数,每一个数字对应6个字节
0x00,0x00,0x00,0x00,0x00,0x00,	// 得分最多五位数,每一个数字对应6个字节
0x00,0x00,0x00,0x00,0x00,0x00,	// 得分最多五位数,每一个数字对应6个字节
0x00,0x00,0x00,0x00,0x00,0x00,	// 得分最多五位数,每一个数字对应6个字节
0x00,0x00,0x00,0x00,0x00,0x00,	// 得分最多五位数,每一个数字对应6个字节
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,	// 无显示
};
unsigned char code Table1[]={	//游戏名称“贪吃蛇”的英文:<<SNAKE>>,宽6高8
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,	// 无显示
0x00,0x08,0x14,0x22,0x49,0x14,0x22,0x41,	// <<	宽8高8(自定义书名号:两个小于号)
0x00,0x46,0x49,0x49,0x49,0x31,	// S	宽6高8
0x00,0x7F,0x04,0x08,0x10,0x7F,	// N
0x00,0x7C,0x12,0x11,0x12,0x7C,	// A
0x00,0x7F,0x08,0x14,0x22,0x41,	// K
0x00,0x7F,0x49,0x49,0x49,0x41,	// E
0x00,0x41,0x22,0x14,0x49,0x22,0x14,0x08,	// >>(自定义书名号:两个大于号)
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,	// 无显示
};
unsigned char code Table2[]={	//“难度”的英文:“DIFFICULTY”,宽6高8
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,	//无显示
0x00,0x7F,0x41,0x41,0x22,0x1C,	// D	宽6高8
0x00,0x00,0x41,0x7F,0x41,0x00,	// I
0x00,0x7F,0x09,0x09,0x09,0x01,	// F
0x00,0x7F,0x09,0x09,0x09,0x01,	// F
0x00,0x00,0x41,0x7F,0x41,0x00,	// I
0x00,0x3E,0x41,0x41,0x41,0x22,	// C
0x00,0x3F,0x40,0x40,0x40,0x3F,	// U
0x00,0x7F,0x40,0x40,0x40,0x40,	// L
0x00,0x01,0x01,0x7F,0x01,0x01,	// T
0x00,0x07,0x08,0x70,0x08,0x07,	// Y
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,	//无显示
0x00,0x00,0x00,0x42,0x7F,0x40,0x00,0x00,	// 1 如果不按按键跳过,则在显示“1”后自动切换到下一个模式
};
unsigned char code Table3[]={	//难度选择界面难度对应的数字:“123451”,宽8高8
0x00,0x00,0x00,0x42,0x7F,0x40,0x00,0x00,	// 1
0x00,0x00,0x42,0x61,0x51,0x49,0x46,0x00,	// 2
0x00,0x00,0x21,0x41,0x45,0x4B,0x31,0x00,	// 3
0x00,0x00,0x18,0x14,0x12,0x7F,0x10,0x00,	// 4
0x00,0x00,0x27,0x45,0x45,0x45,0x39,0x00,	// 5
0x00,0x00,0x00,0x42,0x7F,0x40,0x00,0x00,	// 1
};
unsigned char code Table4[]={	//“得分”的英文:“SCORE”,宽6高8
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,	//无显示
0x00,0x46,0x49,0x49,0x49,0x31,	// S	宽6高8
0x00,0x3E,0x41,0x41,0x41,0x22,	// C
0x00,0x3E,0x41,0x41,0x41,0x3E,	// O
0x00,0x7F,0x09,0x19,0x29,0x46,	// R
0x00,0x7F,0x49,0x49,0x49,0x41,	// E
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,	//无显示
};
unsigned char code Table5[]={	//游戏得分的字模数据,宽6高8
0x00,0x3E,0x51,0x49,0x45,0x3E,	// 0
0x00,0x00,0x42,0x7F,0x40,0x00,	// 1
0x00,0x42,0x61,0x51,0x49,0x46,	// 2
0x00,0x21,0x41,0x45,0x4B,0x31,	// 3
0x00,0x18,0x14,0x12,0x7F,0x10,	// 4
0x00,0x27,0x45,0x45,0x45,0x39,	// 5
0x00,0x3C,0x4A,0x49,0x49,0x30,	// 6
0x00,0x01,0x71,0x09,0x05,0x03,	// 7
0x00,0x36,0x49,0x49,0x49,0x36,	// 8
0x00,0x06,0x49,0x49,0x29,0x1E,	// 9
};

/**
  * 函    数:蛇身缓存数组获取指定Bit的值
  * 参    数:X 指定点的横坐标,范围:-128~127,游戏区域:0~7
  * 参    数:Y 指定点的纵坐标,范围:-128~127,游戏区域:0~7
  * 返 回 值:屏幕区域内返回0或1,超出屏幕区域返回2
  */
unsigned char SnakeBuffer_GetPoint(char X,char Y)
{
	if(X>=0 && X<=7 && Y>=0 && Y<=7)
	{
		if( SnakeBuffer[X] & (0x01<<Y) ) {return 1;}
		else {return 0;}
	}
	else{return 2;}
}

/**
  * 函    数:蛇身缓存数组指定Bit按指定的数值赋值
  * 参    数:X 指定点的横坐标,范围:-128~127,游戏区域:0~7
  * 参    数:Y 指定点的纵坐标,范围:-128~127,游戏区域:0~7
  * 参    数:Value 数值,0:指定Bit置0,非0:指定Bit置1
  * 返 回 值:无
  */
void SnakeBuffer_ControlPoint(char X,char Y,unsigned char Value)
{
	if(Value==0)
	{
		if(X>=0 && X<=7 && Y>=0 && Y<=7){ SnakeBuffer[X] &= ~(0x01<<Y); }
	}
	else
	{
		if(X>=0 && X<=7 && Y>=0 && Y<=7){ SnakeBuffer[X] |= 0x01<<Y; }
	}
}

/**
  * 函    数:在随机位置创造一个食物
  * 参    数:无
  * 返 回 值:食物的位置信息
  * 说    明:高四位存储横坐标,范围:0~7(对应1~8列),低四位存储纵坐标,范围:0~7(对应1~8行)
  */
unsigned char CreateFood(void)
{
	unsigned char Temp;
	unsigned char i,j,m,n;
	m=rand()%8;	//产生一个0~7的随机数
	n=rand()%8;	//产生一个0~7的随机数
	
	//产生一个随机位置,判断该位置是否是蛇身,如果不是蛇身,就返回该位置所对应的数据
	//如果是蛇身的位置,则从该点向周围寻找不是蛇身的空位置
	for(i=0;i<8;i++)
	{
		for(j=0;j<8;j++)
		{
			if( SnakeBuffer_GetPoint((m+i)%8,(n+j)%8) == 0 )
			{
				Temp=(m+i)%8*16+(n+j)%8;
				return Temp;	//找到了空位置就返回
			}
		}
	}
	return Temp;	//正常情况下不会在此处返回
}

/**
  * 函    数:更新蛇的WS2812B的缓存
  * 参    数:无
  * 返 回 值:无
  * 说    明:需要调用WS2812B_Update函数才能更新屏幕显示
  */
void UpdateSnake(void)
{
	unsigned char i,j;
	for(i=0;i<8;i++)
	{
		for(j=0;j<8;j++)
		{
			if( SnakeBuffer_GetPoint(i,j) )
			{
				WS2812B_SetBuffer(i,j,MyColor[SnakeColor][0],MyColor[SnakeColor][1],MyColor[SnakeColor][2]);
			}
			else
			{
				WS2812B_SetBuffer(i,j,0,0,0);
			}
		}
	}
}


/**
  * 函    数:幂函数/指数函数
  * 参    数:X 底
  * 参    数:Y 幂
  * 返 回 值:X的Y次方
  */
unsigned int Pow(unsigned char X,unsigned char Y)
{
	unsigned char i;
	unsigned int Result=1;
	for(i=0;i<Y;i++)
	{
		Result*=X;
	}
	return Result;
}

/**
  * 函    数:主函数(有且仅有一个)
  * 参    数:无
  * 返 回 值:无
  * 说    明:主函数是程序执行的起点,负责执行整个程序的主要逻辑‌
  */
void main()
{
	unsigned char i,j;	//For循环用到的临时变量

	EAXFR=1; //使能访问XFR(扩展RAM区域特殊功能寄存器)
	CKCON=0; //设置外部数据总线速度为最快
    WTST=0;	//设置程序代码等待参数,赋值为0可将CPU执行程序的速度设置为最快

	//全部IO口设置为准双向口
	P0M1=0;P0M0=0;
	P1M1=0;P1M0=0;
	P2M1=0;P2M0=0;
	P3M1=0;P3M0=0;
	P4M1=0;P4M0=0;
	P5M1=0;P5M0=0;
	P6M1=0;P6M0=0;
	P7M1=0;P7M0=0;

	P25=0;	//防止开发板上的蜂鸣器发出声音
	Timer0_Init();  //定时器0初始化
	WS2812B_Init();	//WS2812B彩屏初始化
	Key_Clear();	//进主循环前清空所有按键的所有标志位

	while(1)
	{
		/*按键处理*/
		if(Mode != LastMode)	//如果模式发生改变
		{
			Key_Clear();	//每次切换模式都要清空所有按键的所有标志位,防止本模式置1的标志位影响下一模式
			LastMode=Mode;
		}
		if(Mode==0)	//如果是循环滚动显示游戏英文名的界面
		{
			if(Key(S16,DOWN))	//如果按下S16
			{
				srand(TL0);	//每次按键按下都用定时器0的低8位做种子,从而产生真随机数
				IsShowWaterLED=!IsShowWaterLED;	//置反是否要显示流水灯的标志
				OnceFlag=1;	//切换模式前只执行一次的标志置1
			}
			for(i=0;i<15;i++)
			{
				if(Key(i,DOWN))	//如果按下S1~S15
				{
					srand(TL0);	//每次按键按下都用定时器0的低8位做种子,从而产生真随机数
					Mode=1;	//切换到模式1
					OnceFlag=1;	//切换模式前只执行一次的标志置1
					break;	//退出循环
				}
			}
		}
		else if(Mode==1)	//如果是滚动显示难度的英文的界面
		{
			for(i=0;i<16;i++)
			{
				if(Key(i,DOWN))	//如果有任意按键按下
				{
					srand(TL0);	//每次按键按下都用定时器0的低8位做种子,从而产生真随机数
					Mode=2;	//切换到模式1
					OnceFlag=1;	//切换模式前只执行一次的标志置1
					break;	//退出循环
				}
			}
		}
		else if(Mode==2)	//如果是难度选择界面
		{
			if(Key(S10,DOWN))	//如果按下S10
			{
				srand(TL0);
				ScrollUpFlag=1;	//数字向上滚动的标志置1
			}
			if(Key(S14,DOWN))	//如果按下S14
			{
				srand(TL0);
				ScrollDownFlag=1;	//数字向下滚动的标志置1
			}
			if(Key(S16,DOWN))	//如果按下S16
			{
				srand(TL0);
				Mode=3;	//切换到模式3
				OnceFlag=1;
			}
		}
		else if(Mode==3)	//如果是游戏进行中
		{
			if(Key(S16,DOWN))	//如果按下S16
			{
				srand(TL0);
				PauseFlag=!PauseFlag;	//切换:暂停/继续
				if(PauseFlag==0)	//由暂停切换为继续时,显示食物
				{
					T0Count_1=0;
					FlashFlag=0;
				}
			}
			if(PauseFlag==0)	//如果不是暂停
			{
				if( (Key(S15,DOWN) || Key(S15,HOLD)) && LastDirection!=2 )	//如果按下或按住S15,且原来的方向不是向左
				{
					srand(TL0);
					NowDirection=0;	//则方向蛇头方向改为向右
				}
				if( (Key(S10,DOWN) || Key(S10,HOLD)) && LastDirection!=3 )	//如果按下或按住S10,且原来的方向不是向下
				{
					srand(TL0);
					NowDirection=1;	//则方向蛇头方向改为向上
				}
				if( (Key(S13,DOWN) || Key(S13,HOLD)) && LastDirection!=0 )	//如果按下或按住S13,且原来的方向不是向右
				{
					srand(TL0);
					NowDirection=2;	//则方向蛇头方向改为向左
				}
				if( (Key(S14,DOWN) || Key(S14,HOLD)) && LastDirection!=1 )	//如果按下或按住S14,且原来的方向不是向上
				{
					srand(TL0);
					NowDirection=3;	//则方向蛇头方向改为向下
				}
			}
		}
		else if(Mode==4)	//如果是游戏结束全屏闪烁的界面
		{
			if(Key(S16,DOWN))	//如果按下S16
			{
				srand(TL0);
				Mode=5;
				OnceFlag=1;
			}
		}
		else if(Mode==5)	//如果是滚动显示得分的英文“SCORE”的界面
		{
			if(Key(S16,DOWN))	//如果按下S16
			{
				srand(TL0);
				Mode=6;
				OnceFlag=1;
			}
		}
		else if(Mode==6)	//如果是循环滚动显示游戏得分的界面
		{
			if(Key(S12,DOWN))	//如果按下S12
			{
				srand(TL0);
				Mode=2;	//返回难度选择界面
				OnceFlag=1;
			}
		}

		/*游戏处理*/
		if(Mode==0)	//循环滚动显示游戏英文名
		{
			if(OnceFlag)	//切换到其他模式前,此if中的代码只执行1次
			{
				OnceFlag=0;	//只执行一次的标志置0
				Offset=0;	//滚动显示的偏移量清零
			}
			if(IsShowWaterLED)	//如果要显示流水灯
			{
				if(ScrollFlag)	//如果滚动显示的标志ScrollFlag为真(非零即真)
				{	//则按数据传输顺序显示流水灯
					ScrollFlag=0;	//滚动显示的标志ScrollFlag置0
					WS2812B_Clear();
					WS2812B_SetBuffer((Offset+5)%64/8,7-(Offset+5)%64%8,31,0,0);	//红色
					WS2812B_SetBuffer((Offset+4)%64/8,7-(Offset+4)%64%8,0,31,0);	//绿色
					WS2812B_SetBuffer((Offset+3)%64/8,7-(Offset+3)%64%8,0,0,31);	//蓝色
					WS2812B_SetBuffer((Offset+2)%64/8,7-(Offset+2)%64%8,16,16,0);	//黄色
					WS2812B_SetBuffer((Offset+1)%64/8,7-(Offset+1)%64%8,16,0,16);	//紫色
					WS2812B_SetBuffer((Offset+0)%64/8,7-(Offset+0)%64%8,0,16,16);	//青色
					WS2812B_Update();	//更新显示
					Offset++;
					Offset%=64;
				}
			}
			else	//如果不显示流水灯,则循环滚动显示游戏英文名
			{
				if(ScrollFlag)	//如果滚动显示的标志ScrollFlag为真(非零即真)
				{
					ScrollFlag=0;	//滚动显示的标志ScrollFlag置0
					WS2812B_HS(Table1,8,0,8,Offset);	//滚动显示,向左,紫色
					Offset++;	//每次向左移动一个像素
					Offset%=54;	//越界清零,循环滚动显示
				}
			}
		}
		else if(Mode==1)	//滚动显示难度的英文
		{
			if(OnceFlag)
			{
				OnceFlag=0;
				Offset=0;
			}
			if(ScrollFlag && Offset<=76)	//只向左滚动显示一次,不循环滚动显示
			{
				ScrollFlag=0;
				WS2812B_HS(Table2,8,8,0,Offset);	//滚动显示,向左
				Offset++;
			}
			else if(Offset>76)	//显示数字“1”之后,自动切换到难度选择界面
			{
				Mode=2;
				OnceFlag=1;
			}
		}
		else if(Mode==2)	//难度选择界面
		{
			if(OnceFlag)
			{
				OnceFlag=0;
				WS2812B_HS(Table3,8,8,0,Offset1);
			}
			if(ScrollFlag && ScrollUpFlag)	//如果滚动标志为1,且向上滚动的标志也为1
			{
				ScrollFlag=0;	//滚动的标志置0
				Offset1++;	//向上移动一个像素
				Offset1%=40;	//越界清零,总共5个数字,每个数字的高度是8,所以是5*8=40
				WS2812B_VS(Table3,8,8,0,Offset1);	//滚动显示,向上
				ScrollCount++;	//滚动显示的计数自增
				if(ScrollCount==8)	//移动了8个像素后停止移动
				{
					ScrollCount=0;
					ScrollUpFlag=0;
					Offset1=(Offset1/8)*8;	//防止移动到一半的时候按下“上”或“下”按键导致数字没有在点阵屏中间,Offset1的值必须是8的整数倍
					switch(Offset1/8)
					{
						case 0:Duration=1000;break;	//难度1,1000ms移动1次
						case 1:Duration= 500;break;	//难度2, 500ms移动1次
						case 2:Duration= 330;break;	//难度3, 330ms移动1次
						case 3:Duration= 250;break;	//难度4, 250ms移动1次
						case 4:Duration= 200;break;	//难度5, 200ms移动1次
						default:break;
					}
				}
			}
			if(ScrollFlag && ScrollDownFlag)	//如果滚动标志为1,且向下滚动的标志也为1
			{
				ScrollFlag=0;
				if(Offset1==0){Offset1=40;}
				Offset1--;
				WS2812B_VS(Table3,8,8,0,Offset1);	//滚动显示,向下
				ScrollCount++;
				if(ScrollCount==8)
				{
					ScrollCount=0;
					ScrollDownFlag=0;
					Offset1=(Offset1/8)*8;
					switch(Offset1/8)
					{
						case 0:Duration=1000;break;	//难度1,1000ms移动1次
						case 1:Duration= 500;break;	//难度2, 500ms移动1次
						case 2:Duration= 330;break;	//难度3, 330ms移动1次
						case 3:Duration= 250;break;	//难度4, 250ms移动1次
						case 4:Duration= 200;break;	//难度5, 200ms移动1次
						default:break;
					}
				}
			}
		}
		else if(Mode==3)	//游戏进行中
		{
			if(OnceFlag)
			{	//游戏初始化
				OnceFlag=0;
				WS2812B_Clear();	//清屏
				for(i=0;i<8;i++){SnakeBuffer[i]=0;}	//清空蛇身缓存数组
				Score=2;	//得分重置
				PauseFlag=0;	//暂停的标志置0
				GameOverFlag=0;	//游戏结束的标志置0
				NowDirection=0;	//蛇头默认向右移动
				LastDirection=0;	//蛇头默认向右移动
				Length=2;	//蛇的初始长度为2
				Head=1;	//初始时蛇头对应数组SnakeBody中的索引为1的数据
				SnakeBody[1]=2*16+1;	//数组SnakeBody中写入蛇身初始的两个数据,开始时蛇头在3列2行
				SnakeBody[0]=1*16+1;	//数组SnakeBody中写入蛇身初始的两个数据,开始时蛇尾在2列2行
				SnakeBuffer_ControlPoint(2,1,1);	//蛇身缓存数组对应位置1
				SnakeBuffer_ControlPoint(1,1,1);	//蛇身缓存数组对应位置1
				SnakeColor=rand()%6;	//随机产生一个蛇身的颜色
				FoodColor=rand()%6;	//随机产生一个食物的颜色
				while(FoodColor==SnakeColor)	//食物的颜色和蛇的颜色要不一样
				{
					FoodColor=rand()%6+1;
				}
				UpdateSnake();	//更新蛇的WS2812B的缓存
				WS2812B_SetBuffer(Food/16,Food%16,MyColor[FoodColor][0],MyColor[FoodColor][1],MyColor[FoodColor][2]);	//更新食物的WS2812B的缓存
				WS2812B_Update();	//更新屏幕显示
				T0Count_0=0;	//定时器0全局计数变量清零
				T0Count_1=0;	//定时器0全局计数变量清零
				FlashFlag=0;	//开始游戏时,食物是显示的
				MoveFlag=0;	//蛇移动的标志置0
			}
			if(GameOverFlag==0)	//如果游戏未结束
			{
				if(PauseFlag==0)	//如果不是暂停
				{
					if(MoveFlag)	//如果蛇移动的标志为真
					{
						MoveFlag=0;	//蛇移动的标志置0

						if(NowDirection==0)	//如果向右移动
						{
							if(SnakeBody[Head]/16==7)	//移动前判断一下移动后是否撞墙,如果是,则游戏结束,游戏结束的标志置1
							{
								GameOverFlag=1;
							}
							else	//如果没撞墙,计算出准新蛇头的位置,方便后面判断有没有撞到蛇身或吃到食物
							{
								SnakeBody[(Head+1)%64]=SnakeBody[Head]+16;	//准新蛇头的数据在现在蛇头数据的基础上,高四位加1
							}
						}
						else if(NowDirection==1)	//如果向上移动
						{
							if(SnakeBody[Head]%16==0)
							{
								GameOverFlag=1;
							}
							else
							{
								SnakeBody[(Head+1)%64]=SnakeBody[Head]-1;
							}
						}
						else if(NowDirection==2)	//如果向左移动
						{
							if(SnakeBody[Head]/16==0)
							{
								GameOverFlag=1;
							}
							else
							{
								SnakeBody[(Head+1)%64]=SnakeBody[Head]-16;
							}
						}
						else if(NowDirection==3)	//如果向下移动
						{
							if(SnakeBody[Head]%16==7)
							{
								GameOverFlag=1;
							}
							else
							{
								SnakeBody[(Head+1)%64]=SnakeBody[Head]+1;
							}
						}

						Head++;	//蛇头对应数组SnakeBody的索引加1
						Head%=64;	//防止越界,即索引为63后再加1,则变成0,循环利用数组数据

						if(GameOverFlag==0)	//如果没撞墙
						{
							if(SnakeBody[Head]==Food)	//如果蛇头移动后的位置是食物
							{
								Length++;	//蛇的长度加1
								Score++;	//分数加1
								SnakeBuffer_ControlPoint(Food/16,Food%16,1);	//食物不再闪烁,变成了蛇头
								SnakeColor=FoodColor;	//吃完食物后,蛇的颜色变成跟食物的颜色一样
								while(FoodColor==SnakeColor)	//重新产生一个跟蛇颜色不一样的食物颜色
								{
									FoodColor=rand()%6;
								}
								if(Length<64)	//如果蛇的长度没有达到最大值64
								{
									Food=CreateFood();	//重新创造一个食物
								}
								else	//如果蛇的长度达到了最大值64
								{
									GameOverFlag=1;	//游戏结束
								}
								T0Count_1=0;	//定时器0全局计数变量清零
								FlashFlag=0;	//创造出新的食物时,食物是显示的
							}
							else if( SnakeBuffer_GetPoint(SnakeBody[Head]/16,SnakeBody[Head]%16)==1 && 
								SnakeBody[Head]!=SnakeBody[(Head+64-Length)%64] )	//如果蛇头移动后的位置是蛇身,且不是蛇尾(蛇不会撞到自己尾巴)
							{
								GameOverFlag=1;	//游戏结束
							}
							else	//如果蛇移动后没撞墙,也没吃到食物,也不是撞到蛇身
							{
								SnakeBuffer_ControlPoint(SnakeBody[(Head+64-Length)%64]/16,SnakeBody[(Head+64-Length)%64]%16,0);	//清除移动前的蛇尾的显示
								SnakeBuffer_ControlPoint(SnakeBody[Head]/16,SnakeBody[Head]%16,1);	//显示移动后的蛇头
							}
						}
						LastDirection=NowDirection;	//更新方向变量
					}
					UpdateSnake();	//更新蛇的WS2812B的缓存
					if(FlashFlag)	//游戏时食物闪烁
					{
						WS2812B_SetBuffer(Food/16,Food%16,0,0,0);	//屏幕不显示食物
					}
					else
					{
						WS2812B_SetBuffer(Food/16,Food%16,MyColor[FoodColor][0],MyColor[FoodColor][1],MyColor[FoodColor][2]);	//屏幕显示食物
					}
				}
				else	//如果是暂停状态
				{
					WS2812B_SetBuffer(Food/16,Food%16,MyColor[FoodColor][0],MyColor[FoodColor][1],MyColor[FoodColor][2]);	//屏幕显示食物
				}
			}
			else	//如果游戏结束
			{
				Mode=4;	//切换到模式4
			}
			WS2812B_Update();	//更新屏幕显示
		}
		else if(Mode==4)	//游戏结束全屏闪烁
		{
			if(FlashFlag)	//屏幕不显示
			{
				WS2812B_Clear();	//清空WS2812B缓存
				WS2812B_Update();	//更新屏幕显示
			}
			else	//屏幕显示
			{
				UpdateSnake();	//更新蛇的WS2812B的缓存
				if(Length<64)
				{
					WS2812B_SetBuffer(Food/16,Food%16,MyColor[FoodColor][0],MyColor[FoodColor][1],MyColor[FoodColor][2]);	//屏幕显示食物
				}
				WS2812B_Update();	//更新屏幕显示
			}
		}
		else if(Mode==5)	//滚动显示得分的英文“SCORE”
		{
			if(OnceFlag)
			{
				OnceFlag=0;
				Offset=0;
			}
			if(ScrollFlag && Offset<=38)	//只滚动显示一次英文
			{
				ScrollFlag=0;
				WS2812B_HS(Table4,0,8,8,Offset);	//滚动显示,向左
				Offset++;
			}
			else if(Offset>38) //滚动结束后,自动切换到循环滚动显示得分的模式
			{
				Mode=6;
				OnceFlag=1;
			}	
		}
		else if(Mode==6)	//循环滚动显示游戏得分
		{
			if(OnceFlag)
			{
				OnceFlag=0;
				Offset=0;
				for(i=0;i<46;i++){ScoreShow[i]=0;}	//清空分数显示的缓存数组的数据
				if(Score>=10000){ScoreLength=5;}//判断得分是多少位数
				else if(Score>=1000){ScoreLength=4;}
				else if(Score>=100){ScoreLength=3;}
				else if(Score>=10){ScoreLength=2;}
				else{ScoreLength=1;}
				for(j=0;j<ScoreLength;j++)//将得分的数字的字模写入数组ScoreShow中
				{
					for(i=0;i<6;i++)
					{
						ScoreShow[8+6*j+i]=Table5[(Score/Pow(10,ScoreLength-1-j)%10)*6+i];
					}
				}
			}
			if(ScrollFlag)
			{
				ScrollFlag=0;
				WS2812B_HS(ScoreShow,8,8,8,Offset);	//滚动显示,向左
				Offset++;
				Offset%=8+ScoreLength*6;	//循环滚动显示
			}
		}
	}
}

/**
  * 函    数:定时器0中断函数
  * 参    数:无
  * 返 回 值:无
  */
void Timer0_Routine() interrupt 1
{
	static unsigned char T0Count0,T0Count1;	//定义计时器静态计数变量
	TL0=0x48;	//设置定时初值,定时1ms,1T@35.0000MHz
	TH0=0x77;	//设置定时初值,定时1ms,1T@35.0000MHz
	T0Count0++;
	T0Count1++;
	if(PauseFlag==0){T0Count_0++;}	//不暂停时才计数
	T0Count_1++;
	if(T0Count0>=20)	//每隔20ms检测一次按键
	{
		T0Count0=0;
		Key_Tick();
	}
	if(T0Count1>=100)	//每隔100ms滚动显示一次字母或数字
	{
		T0Count1=0;
		ScrollFlag=1;
	}
	if(T0Count_0>=Duration)	//每隔 Duration ms 蛇移动一次
	{
		T0Count_0=0;
		MoveFlag=1;
	}
	if(T0Count_1>=250)	//每隔250ms置反闪烁的标志FlashFlag
	{
		T0Count_1=0;
		FlashFlag=!FlashFlag;
	}
}

总结

之前做过8X8彩屏的贪吃蛇,之前直接用IO口模拟时序驱动WS2812B,现在改成用SPI+DMA或PWM+DMA来驱动,解放了CPU。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐