本文讲述如何从无到有用C51做一个重力感应的贪吃蛇,包括元件选型、原理图PCB绘制和代码编写。
元件 | 封装 | 数量 |
---|---|---|
89C52 | 双列直插DIP40 | 1 |
MPU6050 | 8引脚(实际只用到4个) | 1 |
8*8LED点阵屏 | 18引脚(2个起固定作用) | 1 |
74HC595 | 贴片SOP16 | 1 |
CH340 | 贴片SOP16 | 1 |
12M晶振 | HC-49S | 2 |
USB母座 | 6脚(USB-A) | 1 |
排阻10k | 0603 8P4R | 4 |
电阻10k | 贴片0603 | 2 |
电阻33R | 贴片0603 | 2 |
电阻47k | 贴片0603 | 1 |
电阻4.7k | 贴片0603 | 1 |
电容22P | 贴片0603 | 3 |
电容33P | 贴片0603 | 2 |
电容103 | 贴片0603 | 1 |
电容104 | 贴片0603 | 2 |
电解电容10u | 插件 | 1 |
电解电容470u | 插件 | 1 |
SS14肖特基二极管 | SMA | 1 |
A1SHB | SOT-23 | 1 |
贴片LED | 0603 | 1 |
轻触开关 | 6*6*5 | 1 |
按钮开关 | 8*8 | 1 |
这个比较简单的,网上有很多资料,不赘述。
拿P1的0和1做IIC通信的时钟线和数据线,配置一下MPU6050的寄存器:
sbit SCL=P1^0;
sbit SDA=P1^1;
#define SMPLRT_DIV 0x19
#define CONFIG 0x1A
#define GYRO_CONFIG 0x1B
#define ACCEL_CONFIG 0x1C
#define ACCEL_XOUT_H 0x3B
#define ACCEL_XOUT_L 0x3C
#define ACCEL_YOUT_H 0x3D
#define ACCEL_YOUT_L 0x3E
#define ACCEL_ZOUT_H 0x3F
#define ACCEL_ZOUT_L 0x40
#define TEMP_OUT_H 0x41
#define TEMP_OUT_L 0x42
#define GYRO_XOUT_H 0x43
#define GYRO_XOUT_L 0x44
#define GYRO_YOUT_H 0x45
#define GYRO_YOUT_L 0x46
#define GYRO_ZOUT_H 0x47
#define GYRO_ZOUT_L 0x48
#define PWR_MGMT_1 0x6B
#define WHO_AM_I 0x75
#define SlaveAddress 0xD0
延时5秒IIC要用,写得精确点:
void Delay5us()
{
_nop_();_nop_();_nop_();_nop_();
_nop_();_nop_();_nop_();_nop_();
_nop_();_nop_();_nop_();_nop_();
_nop_();_nop_();_nop_();_nop_();
_nop_();_nop_();_nop_();_nop_();
_nop_();_nop_();_nop_();_nop_();
}
IIC通信的相关函数:
void I2C_Start()
{
SDA = 1;
SCL = 1;
Delay5us();
SDA = 0;
Delay5us();
SCL = 0;
}
void I2C_Stop()
{
SDA = 0;
SCL = 1;
Delay5us();
SDA = 1;
Delay5us();
}
void I2C_SendACK(bit ack)
{
SDA = ack;
SCL = 1;
Delay5us();
SCL = 0;
Delay5us();
}
bit I2C_RecvACK()
{
SCL = 1;
Delay5us();
CY = SDA;
SCL = 0;
Delay5us();
return CY;
}
void I2C_SendByte(uchar dat)
{
uchar i;
for (i=0; i<8; i++)
{
dat <<= 1;
SDA = CY;
SCL = 1;
Delay5us();
SCL = 0;
Delay5us();
}
I2C_RecvACK();
}
uchar I2C_RecvByte()
{
uchar i;
uchar dat = 0;
SDA = 1;
for (i=0; i<8; i++)
{
dat <<= 1;
SCL = 1;
Delay5us();
dat |= SDA;
SCL = 0;
Delay5us();
}
return dat;
}
void Single_WriteI2C(uchar REG_Address,uchar REG_data)
{
I2C_Start();
I2C_SendByte(SlaveAddress);
I2C_SendByte(REG_Address);
I2C_SendByte(REG_data);
I2C_Stop();
}
uchar Single_ReadI2C(uchar REG_Address)
{
uchar REG_data;
I2C_Start();
I2C_SendByte(SlaveAddress);
I2C_SendByte(REG_Address);
I2C_Start();
I2C_SendByte(SlaveAddress+1);
REG_data=I2C_RecvByte();
I2C_SendACK(1);
I2C_Stop();
return REG_data;
}
MPU6050相关函数:
void InitMPU6050()
{
Single_WriteI2C(PWR_MGMT_1, 0x00);
Single_WriteI2C(SMPLRT_DIV, 0x07);
Single_WriteI2C(CONFIG, 0x06);
Single_WriteI2C(GYRO_CONFIG, 0x18);
Single_WriteI2C(ACCEL_CONFIG, 0x01);
}
int GetData(uchar REG_Address)
{
uchar H,L;
H=Single_ReadI2C(REG_Address);
L=Single_ReadI2C(REG_Address+1);
return (H<<8)+L;
}
定义接口:
sbit SRCLK=P3^6;
sbit RCLK=P3^5;
sbit SER=P3^4;
595串转并函数:
void HC595SendByte(unsigned char dat)
{
unsigned char a;
SRCLK=0;
RCLK=0;
for(a=0;a<8;a++)
{
SER=dat>>7;
dat<<=1;
SRCLK=1;
_nop_();
_nop_();
SRCLK=0;
}
RCLK=1;
_nop_();
_nop_();
RCLK=0;
}
宏定义如下:
#define COMMONPORTS P0
#define UP 0x00
#define DOWN 0x01
#define LEFT 0x02
#define RIGHT 0x03
全局变量如下:
unsigned char code TAB[8] = {0x7f,0xbf,0xdf,0xef,0xf7,0xfb,0xfd,0xfe};//scan 8*8 LED column
unsigned char snake[40] = {0x34}; //initial position
unsigned char food = 0x33;
unsigned char refresh = 0; //determine whether to refresh
unsigned char len = 1; //the length of snake
unsigned char dir = DOWN; // direction
unsigned char map[8] = {0,0,0,0,0,0,0,0}; //scan 8*8 LED row
unsigned char isalive = 1; // determine game over or not
static int ts; // timer
其中TAB是8*8LED点阵屏的列扫描,map是一个长度为8的数组,记录每一列上8个LED的亮灭。
先放个主函数压压惊:
void main()
{
unsigned char tab;
InitMPU6050();
Timer0Init();
while(1){
while(!refresh){
for(tab=0;tab<8;tab++){
HC595SendByte(0x00);
COMMONPORTS = TAB[tab];
HC595SendByte(map[tab]);
}
}
refresh = 0;
get_dir();
snake_move();
if(!isalive){
ET0 = 0;
continue;
}
print_game();
}
}
思路很简单,根本不需要解释。
void tran(unsigned char num,unsigned char *x, unsigned char *y)
{
*x = num >> 4;
*y = (unsigned char)(num << 4) >> 4;
}
这个函数就是用来把一个字节拆成两半,分别代表横坐标和纵坐标。
void Timer0Init()
{
TMOD|=0X01;
TH0=0XFC;
TL0=0X18;
ET0=1;
EA=1;
TR0=1;
}
void Timer0() interrupt 1
{
TH0=0XFC;
TL0=0X18;
ts++;
if(ts==1000)
{
ts=0;
refresh = 1;
}
}
void print_game()
{
unsigned char i;
unsigned char x,y;
memset(map,0,sizeof(unsigned char)*8);
for(i=0;imap[x] += (1<map[x] += (1<
unsigned char generate_food()
{
unsigned char food_,fx,fy;
unsigned char in_snake = 0,i;
int rd;
do {
rd = (unsigned char)(GetData(ACCEL_ZOUT_H));
fx = rd & 0x07;
fy = (rd & 0x38) >> 3;
in_snake = 0;
food_ = ((fx^0) << 4) ^ fy;
for (i = 0; i < len; ++i){
if (food_ == snake[i])
in_snake = 1;
}
} while(in_snake);
return food_;
}
这里本来打算用rand生成随机数的,后来一想,MPU在手里,为何不直接取位数取余做随机数呢,岂不是更好。
void snake_move()
{
unsigned char grow = 0,i,tmp,x_snake_head,y_snake_head;
unsigned char last = snake[0];
tran(snake[0],&x_snake_head,&y_snake_head);
switch (dir){
case UP: y_snake_head++; break;
case DOWN: y_snake_head--; break;
case LEFT: x_snake_head--; break;
case RIGHT: x_snake_head++; break;
}
snake[0] = ((x_snake_head^0) << 4) ^ y_snake_head;
if(x_snake_head>7 || y_snake_head>7)
isalive = 0;
if(snake[0] == food){
grow = 1;
food = generate_food();
}
for(i=1;iif(snake[0] == snake[i])
isalive = 0;
tmp = snake[i];
snake[i] = last;
last = tmp;
}
if(grow){
snake[len] = last;
len++;
}
}
void get_dir()
{
int ax = GetData(ACCEL_XOUT_H);
int ay = GetData(ACCEL_YOUT_H);
if(ax < -8000)
dir = LEFT;
else if(ax > 8000)
dir = RIGHT;
else if(ay < -8000)
dir = UP;
else if(ay > 8000)
dir = DOWN;
}
OK大功告成。
可以下载源码试一试,亲测有效
C51贪吃蛇(MPU6050控制方向,LED点阵显示)