window10
专业版。Keil5
Proteus
PS
:软件下载地址 Proteus电路仿真及应用(51单片机系列)
├─code # 项目文件
│ ├─bsp # 外设模块封装目录,比如点阵 matrix.c、方向按键 dir_key.c 等
│ ├─output # 编译后的hex文件输出位置
│ ├─project # 存放keil的project目录
│ └─user # main.c 和 pbdata.c共享模块
└─proteus # 仿真画图存放位置
PS
: 关于项目为什么结构为什么这样划分,可以看此视频 51单片机入门学习—以最通俗易懂的语言讲解! 中P25 - P28
关于模块化编程的讲解。
对于方向键我使用的是矩阵键盘的方式完成各个按键的检测(如果不了解矩阵键盘怎么用请看此视频 P10 矩阵键盘)。
dir_key.c
中,检测那个按键按下就修改dir_key.c
中的 dir
这个变量。外部使用这个模块的时候 extern unsigned char dir;
即可引入这个变量来判断此时的方向,具体代码如下:#include "PBDATA.H"
unsigned char dir = 0; // 默认向上
void DirKeyScan()
{
unsigned char temp0 = 0, temp1 = 0, temp2 = 0;
P1 = 0xFC; // 1111 1100
if(P1 != 0xFC) { // 检测矩阵按键那行按下
delay(20);
temp1 = P1;
// 列
P1 = 0xF3; // 1111 0011
if(P1 != 0xF3) { // 检测矩阵按键那列按下
temp2 = P1;
}
}
temp0 = temp1 | temp2;
if(temp0 == 0xFA) { // 1111 1010 上
dir = (dir == 2) ? dir : 0;
} else if(temp0 == 0xF6) { // 1111 0110 下
dir = (dir == 0) ? dir : 2;
} else if(temp0 == 0xF9) { // 1111 1001 左
dir = (dir == 1) ? dir : 3;
} else if(temp0 == 0xF5) { // 1111 0101 右
dir = (dir == 3) ? dir : 1;
}
}
matrix.c
文件当中,具体代码如下:#include "PBDATA.H"
static uchar code indexs[] = {0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80}; // 行位码
static uchar tab[8] = {0}; // 列位码
// 清屏函数
void Matrix_Clear()
{
COLP = 0x00;
}
// 生成列码
void Matrix_GenerateTab(uchar rowIndexs[], uchar colIndexs[], uchar length)
{
uchar i = 0;
// 清掉上次形成的列码
for(i = 0; i < 8; i++) {
tab[i] = 0;
}
//形成列位码
for(i = 0; i < length; i++) {
// rowIndexs[i] 行显示的列追加 tab[rowIndexs[i]];
tab[rowIndexs[i]] += indexs[colIndexs[i]];
}
}
// 使用编码点亮点阵
void Matrix_ShowPointByCode()
{
uchar i = 0;
// 逐行显示
for(i = 0; i < 8; i++) {
Matrix_Clear();
ROWP = ~indexs[i];
COLP = tab[i];
}
}
void Matrix_ShowAll()
{
uchar i = 0;
// 逐行显示
for(i = 0; i < 8; i++) {
Matrix_Clear();
ROWP = ~indexs[i];
COLP = 0xFF;
}
}
main()
函数当中死循环中,这样能最大程度保证点阵屏幕的刷新速度够快,视觉暂留效果达到最佳。main.c
。50ms
检测一次完全够用,且如果跟点刷新放在一条主线上会影响点阵的显示效果,因此我将其放在了定时器0
中,每隔50ms
检测下即可,并未出现什么问题。main.c
。定时器0
中,不过移动频率使用全局变量 speed
控制,也就是隔 speed * 50ms
移动一次,可以自行调整,并且增加后续功能也可以吃的食物越多,调整 speed
变量的值。//1. 使用两个数组存储身体行和列 (uchar 即 unsigned char);
// 如下则表示蛇头到蛇尾分别在0行0列,0行1列,0行2列。
uchar bodyRow[16] = {0, 0, 0};
uchar bodyCol[16] = {0, 1, 2};
// 2. 移动身体 : 将前一个位置的行列赋值给后一个位置即完成蛇身体向头部的一次移动
for(i = snakeBodyLength - 1; i > 0; i--) {
bodyRow[i] = bodyRow[i - 1];
bodyCol[i] = bodyCol[i - 1];
}
// 生成新的蛇头
bodyRow[0] = bodyRow[0] + dirRow[dir];
bodyCol[0] = bodyCol[0] + dirCol[dir];
main.c
。#include "PBDATA.H"
/****************************************
蛇体 和 食物 相关的数据 和 函数
*****************************************/
uchar bodyRow[16], bodyCol[16]; // 蛇身的数组
uchar code dirRow[4] = {-1, 0, 1, 0}; // 上右下左方向X增量
uchar code dirCol[4] = {0, 1, 0, -1}; // 上右下左方向Y增量
extern uchar dir;
uchar maxRow = 8, maxCol = 8; // 行的范围 [0, maxRow), 列范围同理
uchar speed = 5; // 蛇的速度,单位是50ms
uchar snakeBodyLength = 0; // 蛇身体的长度
uchar isDead = 0; // 表示蛇是否死亡
uchar foodRow = 0, foodCol = 0, needCreate = 1; // 食物坐标和食物是否被吃标志
void InitSnake()
{
bodyRow[0] = 7;
bodyCol[0] = 2;
bodyRow[1] = 7;
bodyCol[1] = 1;
snakeBodyLength = 2;
dir = 1;
// 生成蛇打印的内容
Matrix_GenerateTab(bodyRow, bodyCol, snakeBodyLength);
// 设置时间种子
srand(0);
}
void GenerateFood()
{
uchar i = 0;
while(needCreate) {
// 随机生成 Row 和 Col
foodRow = rand() % maxRow;
foodCol = rand() % maxCol;
// 判断食物是否和当前蛇身体冲突
for(i = 0; i < snakeBodyLength; i++) {
if(bodyRow[i] == foodRow && bodyCol[i] == foodCol) {
break;
}
}
if(i == snakeBodyLength) {
needCreate = 0;
bodyRow[snakeBodyLength] = foodRow;
bodyCol[snakeBodyLength] = foodCol;
}
}
}
/****************************************
定时器相关的代码
*****************************************/
uchar count; // time = count * 50ms
void InitTimer()
{
TMOD = 0x01;
// 初始值 : 50ms
TH0 = (65536 - 50000) / 256; // 初始值取高八位
TL0 = (65536 - 50000) % 256; // 初始值取低八位
// 中断开启
ET0 = 1; // 开启定时器0的中断
EA = 1; // 开启总的中断
// 配置TCON
// TR0 : 1, 启动定时器0
TR0 = 1;
}
void TimerIsr() interrupt 1
{
uchar nextHeadRow = 0, nextHeadCol = 0, i = 0;
// 重新装填
// 初始值 : 50ms
TH0 = (65536 - 50000) / 256; // 初始值取高八位
TL0 = (65536 - 50000) % 256; // 初始值取低八位
if(count == speed && !isDead) { // count * 50ms 触发一次
count = 0;
// 生成食物
GenerateFood();
// 预测蛇头
nextHeadRow = bodyRow[0] + dirRow[dir];
nextHeadCol = bodyCol[0] + dirCol[dir];
if(nextHeadRow >= maxRow || nextHeadRow < 0
|| nextHeadCol >= maxCol || nextHeadCol < 0) {
isDead = 1;
return;
}
// 身体撞击
for(i = 0; i < snakeBodyLength; i++) {
if(nextHeadRow == bodyRow[i] && nextHeadCol == bodyCol[i]) {
isDead = 1;
return;
}
}
// 吃到食物与否
if(nextHeadRow == foodRow && nextHeadCol == foodCol) {
snakeBodyLength += 1;
needCreate = 1;
// 再生成新的食物
GenerateFood();
}
// 蛇身体移动
for(i = snakeBodyLength - 1; i > 0; i--) {
bodyRow[i] = bodyRow[i - 1];
bodyCol[i] = bodyCol[i - 1];
}
// 新的蛇头
bodyRow[0] = nextHeadRow;
bodyCol[0] = nextHeadCol;
// 生成需要显示的图形
Matrix_GenerateTab(bodyRow, bodyCol, snakeBodyLength + 1);
}
count++;
// 每50ms进行一次按键扫描
DirKeyScan();
}
void main()
{
uchar i = 0, a = 0;
count = speed;
InitSnake();
InitTimer();
while(1){
if(isDead) {
Matrix_ShowAll();
} else {
// 打印蛇的身体
Matrix_ShowPointByCode();
}
}
}
LCD1602
等模块显示游戏信息,如分数、蛇长度这样。