硬件准备
在开始之前,请确保您有以下硬件:

(图片来源网络,侵删)
- 51单片机开发板 (STC89C52RC)
- LCD1602液晶显示屏 (带I2C或并行接口,本代码使用并行接口)
- 4x4矩阵键盘 或 4个独立按键 (用于控制方向和开始/暂停)
- 杜邦线若干
- 面包板 (可选,方便搭建)
硬件连接 (并行接口LCD1602)
我们将使用LCD1602的8位数据模式,连接如下:
| LCD1602 引脚 | 单片机引脚 (P口) | 功能说明 |
|---|---|---|
| VSS (GND) | GND | 电源地 |
| VDD | +5V | 电源正极 |
| V0 (Contrast) | 电位器中间脚 | 对比度调节 |
| RS (Register Select) | P2.5 | 寄存器选择 (1=数据, 0=指令) |
| RW (Read/Write) | GND | 始终写模式 |
| EN (Enable) | P2.6 | 使能信号 |
| D0 | - | 未使用 (4位模式时) |
| D1 | - | 未使用 (4位模式时) |
| D2 | - | 未使用 (4位模式时) |
| D3 | - | 未使用 (4位模式时) |
| D4 | P2.0 | 数据线 4 |
| D5 | P2.1 | 数据线 5 |
| D6 | P2.2 | 数据线 6 |
| D7 | P2.3 | 数据线 7 |
| A (Backlight +) | +5V | 背光正极 |
| K (Backlight -) | GND | 背光负极 |
键盘连接示例 (使用P1口):
| 按键功能 | 连接到单片机引脚 |
|---|---|
| 上 | P1.0 |
| 下 | P1.1 |
| 左 | P1.2 |
| 右 | P1.3 |
| 开始/暂停 | P1.4 |
完整C语言代码
将以下代码保存为 snake.c,并使用Keil C51或其他51开发环境进行编译。
#include <reg52.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
// --- 硬件引脚定义 ---
sbit LCD_RS = P2^5;
sbit LCD_RW = P2^6;
sbit LCD_EN = P2^7;
#define LCD_DATA_PORT P0 // LCD数据总线连接到P0口
// --- 按键定义 (连接到P1口) ---
sbit KEY_UP = P1^0;
sbit KEY_DOWN = P1^1;
sbit KEY_LEFT = P1^2;
sbit KEY_RIGHT = P1^3;
sbit KEY_START = P1^4;
// --- 游戏参数定义 ---
#define LCD_WIDTH 16 // LCD1602每行显示16个字符
#define LCD_HEIGHT 2 // LCD1602共2行
#define SNAKE_MAX_LENGTH 64 // 蛇的最大长度
// 游戏状态
enum GameState {
GAME_STOPPED,
GAME_RUNNING,
GAME_PAUSED,
GAME_OVER
};
// 蛇身节点结构体
typedef struct {
int x;
int y;
} SnakeNode;
// --- 全局变量 ---
SnakeNode snake[SNAKE_MAX_LENGTH]; // 蛇身数组
int snake_length = 3; // 当前蛇的长度
int food_x, food_y; // 食物的位置
int direction = 1; // 蛇的移动方向 (1:右, 2:左, 3:上, 4:下)
enum GameState game_state = GAME_STOPPED;
unsigned int score = 0;
// --- LCD1602 函数库 ---
void LcdDelay(unsigned int ms) {
unsigned int i, j;
for (i = 0; i < ms; i++)
for (j = 0; j < 120; j++);
}
void LcdWriteCmd(unsigned char cmd) {
LCD_RS = 0; // 选择指令寄存器
LCD_RW = 0; // 选择写操作
LCD_DATA_PORT = cmd;
LCD_EN = 1; // 使能信号高电平
LcdDelay(5);
LCD_EN = 0; // 使能信号低电平
LcdDelay(5);
}
void LcdWriteData(unsigned char dat) {
LCD_RS = 1; // 选择数据寄存器
LCD_RW = 0; // 选择写操作
LCD_DATA_PORT = dat;
LCD_EN = 1; // 使能信号高电平
LcdDelay(5);
LCD_EN = 0; // 使能信号低电平
LcdDelay(5);
}
void LcdInit() {
LcdDelay(15);
LcdWriteCmd(0x38); // 设置16x2显示,5x7点阵,8位数据接口
LcdWriteCmd(0x0C); // 显示开,光标关,闪烁关
LcdWriteCmd(0x06); // 读写后指针自动加1
LcdWriteCmd(0x01); // 清屏
LcdDelay(5);
}
void LcdSetCursor(unsigned char x, unsigned char y) {
unsigned char addr;
if (y == 0)
addr = 0x80 + x;
else
addr = 0xC0 + x;
LcdWriteCmd(addr);
}
void LcdShowString(unsigned char x, unsigned char y, unsigned char *str) {
LcdSetCursor(x, y);
while (*str != '\0') {
LcdWriteData(*str++);
}
}
// --- 游戏逻辑函数 ---
void GenerateFood() {
// 简单的随机数生成,基于定时器溢出
food_x = rand() % (LCD_WIDTH - 2) + 1;
food_y = rand() % (LCD_HEIGHT - 2) + 1;
// 确保食物不出现在蛇身上
for (int i = 0; i < snake_length; i++) {
if (snake[i].x == food_x && snake[i].y == food_y) {
GenerateFood(); // 如果冲突,递归生成
return;
}
}
}
void InitGame() {
// 初始化蛇的位置 (从屏幕中央开始)
snake[0].x = LCD_WIDTH / 2;
snake[0].y = LCD_HEIGHT / 2;
snake[1].x = snake[0].x - 1;
snake[1].y = snake[0].y;
snake[2].x = snake[1].x - 1;
snake[2].y = snake[0].y;
snake_length = 3;
direction = 1; // 初始向右
score = 0;
GenerateFood();
game_state = GAME_RUNNING;
}
void UpdateGame() {
if (game_state != GAME_RUNNING) return;
// 移动蛇身
// 1. 保存尾部节点
SnakeNode tail = snake[snake_length - 1];
// 2. 移动身体节点 (除了头部)
for (int i = snake_length - 1; i > 0; i--) {
snake[i] = snake[i - 1];
}
// 3. 根据方向移动头部
switch (direction) {
case 1: snake[0].x++; break; // 右
case 2: snake[0].x--; break; // 左
case 3: snake[0].y--; break; // 上
case 4: snake[0].y++; break; // 下
}
// 4. 检查碰撞
// 4.1 撞墙
if (snake[0].x <= 0 || snake[0].x >= LCD_WIDTH - 1 ||
snake[0].y <= 0 || snake[0].y >= LCD_HEIGHT - 1) {
game_state = GAME_OVER;
return;
}
// 4.2 撞到自己
for (int i = 1; i < snake_length; i++) {
if (snake[0].x == snake[i].x && snake[0].y == snake[i].y) {
game_state = GAME_OVER;
return;
}
}
// 5. 检查是否吃到食物
if (snake[0].x == food_x && snake[0].y == food_y) {
score += 10;
snake_length++; // 蛇变长
if (snake_length >= SNAKE_MAX_LENGTH) {
game_state = GAME_OVER;
} else {
GenerateFood(); // 生成新食物
}
} else {
// 如果没吃到食物,尾部节点消失 (在渲染时处理)
}
}
void RenderGame() {
// 清屏
LcdWriteCmd(0x01);
LcdDelay(5);
// 绘制边界
for (int i = 0; i < LCD_WIDTH; i++) {
LcdSetCursor(i, 0);
LcdWriteData(0xFF); // 0xFF是LCD1602的实心块字符
LcdSetCursor(i, LCD_HEIGHT - 1);
LcdWriteData(0xFF);
}
for (int i = 0; i < LCD_HEIGHT; i++) {
LcdSetCursor(0, i);
LcdWriteData(0xFF);
LcdSetCursor(LCD_WIDTH - 1, i);
LcdWriteData(0xFF);
}
// 绘制蛇
for (int i = 0; i < snake_length; i++) {
LcdSetCursor(snake[i].x, snake[i].y);
// 蛇头用 'O',蛇身用 '='
LcdWriteData((i == 0) ? 'O' : '=');
}
// 绘制食物
LcdSetCursor(food_x, food_y);
LcdWriteData('*');
// 显示分数和状态
char score_str[20];
sprintf(score_str, "Score: %d", score);
LcdShowString(1, 0, score_str);
if (game_state == GAME_STOPPED) {
LcdShowString(4, 1, "Press Start");
} else if (game_state == GAME_PAUSED) {
LcdShowString(5, 1, "Paused");
} else if (game_state == GAME_OVER) {
LcdShowString(4, 1, "Game Over");
}
}
// --- 按键处理函数 ---
void ReadKeys() {
// 检测开始/暂停键
if (KEY_START == 0) {
LcdDelay(10); // 消抖
if (KEY_START == 0) {
if (game_state == GAME_STOPPED || game_state == GAME_OVER) {
InitGame();
} else if (game_state == GAME_RUNNING) {
game_state = GAME_PAUSED;
} else if (game_state == GAME_PAUSED) {
game_state = GAME_RUNNING;
}
while (!KEY_START); // 等待按键释放
}
}
// 检测方向键 (只在游戏运行时响应)
if (game_state == GAME_RUNNING) {
// 防止180度掉头
if (KEY_UP == 0 && direction != 4) {
direction = 3;
LcdDelay(100);
} else if (KEY_DOWN == 0 && direction != 3) {
direction = 4;
LcdDelay(100);
} else if (KEY_LEFT == 0 && direction != 1) {
direction = 2;
LcdDelay(100);
} else if (KEY_RIGHT == 0 && direction != 2) {
direction = 1;
LcdDelay(100);
}
}
}
// --- 主函数 ---
void main() {
// 初始化随机数种子
srand(time(NULL)); // 在51单片机上,time(NULL)可能返回0,但没关系
// 初始化硬件
LcdInit();
// 主循环
while (1) {
ReadKeys(); // 读取按键输入
UpdateGame(); // 更新游戏逻辑
RenderGame(); // 渲染游戏画面
// 控制游戏速度
LcdDelay(200000); // 调整这个值可以改变蛇的移动速度
}
}
代码讲解
1 头文件和引脚定义
#include <reg52.h>: 包含51单片机的特殊功能寄存器定义。sbit LCD_RS = P2^5;: 定义LCD1602的控制引脚,方便后续调用。
2 LCD1602驱动函数
这部分是标准的LCD1602驱动代码,用于初始化、写指令、写数据和显示字符串。

(图片来源网络,侵删)
LcdInit(): 初始化LCD,设置显示模式等。LcdWriteCmd(): 向LCD发送指令。LcdWriteData(): 向LCD发送要显示的字符数据。LcdShowString(): 在指定位置显示一个字符串。
3 游戏核心逻辑
SnakeNode结构体: 用来表示蛇的每一个节点的坐标。enum GameState: 定义了游戏的四种状态,让代码逻辑更清晰。InitGame(): 游戏开始时调用,初始化蛇的位置、长度、方向和分数,并生成第一个食物。GenerateFood(): 在屏幕的随机位置生成食物,并确保不会和蛇身重叠。UpdateGame(): 这是游戏的核心。- 移动蛇: 通过循环将蛇身向前移动一位,然后根据
direction变量更新蛇头位置。 - 碰撞检测: 检查蛇头是否撞到墙壁或自己的身体,如果发生碰撞,则将游戏状态设为
GAME_OVER。 - 吃食物: 检查蛇头是否与食物坐标重合,如果重合,增加分数,蛇身长度加一,并调用
GenerateFood()生成新食物。
- 移动蛇: 通过循环将蛇身向前移动一位,然后根据
RenderGame(): 负责将游戏状态绘制到LCD1602上。- 清屏并画边框: 用
0xFF(实心块字符)画出游戏区域的边界。 - 画蛇: 遍历蛇身数组,在对应坐标画上
'O'(蛇头)或(蛇身)。 - 画食物: 在食物坐标画上。
- 显示信息: 在屏幕上显示当前分数和游戏状态(如 "Game Over")。
- 清屏并画边框: 用
4 按键处理
ReadKeys(): 检测按键输入。- 开始/暂停键: 按下后根据当前游戏状态进行切换(
STOPPED->RUNNING,RUNNING->PAUSED,PAUSED->RUNNING)。 - 方向键: 只有在游戏运行状态下才响应,增加了防止180度掉头的逻辑(当前向右走时,不能直接按左键)。
- 开始/暂停键: 按下后根据当前游戏状态进行切换(
5 主函数 main()
srand(time(NULL));: 初始化随机数生成器,让食物的随机位置每次都不同。LcdInit();: 初始化LCD。while(1): 无限循环,是单片机程序的标志。- 在循环中,依次执行
ReadKeys()、UpdateGame()、RenderGame()。 LcdDelay(200000);: 这个延时函数控制了游戏循环的频率,也就是蛇的移动速度,数值越大,蛇移动越慢。
- 在循环中,依次执行
如何编译和烧录
- 创建Keil项目: 打开Keil C51,新建一个项目,并选择你的单片机型号(如STC89C52RC)。
- 添加源文件: 将上面的代码保存为
snake.c,并添加到项目中。 - 配置选项: 在"Options for Target"中,确保晶振频率设置正确(通常是11.0592MHz或12MHz,因为代码中的延时是基于这个频率的)。
- 编译: 点击"Build"按钮,生成
.hex文件。 - 烧录: 使用USB-TTL下载器(如CH340)将
.hex文件烧录到你的单片机中。
可能遇到的问题与调试
- LCD显示乱码或不显示:
- 检查接线: 确保V0引脚连接了电位器用于调节对比度。
- 检查初始化: 确认
LcdInit()函数中的指令是否正确(如0x38)。 - 检查数据/指令选择: 确认
RS、RW、EN引脚的逻辑是否正确。
- 按键无响应:
- 检查按键电路: 按键另一端是否正确连接到GND(通常采用上拉电阻+按键接地的方式)。
- 检查消抖: 代码中已经有简单的延时消抖,如果按键抖动严重,可以增加延时时间。
- 游戏速度过快/过慢:
- 调整
main函数中的LcdDelay(200000)的参数,数值越大,延时越长,速度越慢。
- 调整
- 蛇移动不流畅:
- 这是由
main循环中的延时决定的,如果想实现更平滑的移动,可以考虑使用定时器中断来控制游戏逻辑的更新频率,而main循环可以专注于渲染和按键读取,这样可以实现更精确的时序控制。
- 这是由
希望这份详细的代码和教程能帮助你成功运行单片机贪吃蛇游戏!祝你学习愉快!

(图片来源网络,侵删)
