1. 51单片机8x8 LED点阵基础实验
1.1 硬件连接与原理分析
8x8 LED点阵是最基础的显示器件之一,由64个LED按8行8列排列组成。在普中开发板上,我们采用74HC595芯片进行行控制,P0端口直接控制列。这种设计可以大大节省单片机IO口资源,是嵌入式系统中常见的扩展方式。
硬件连接要点:
- 点阵行控制:通过74HC595串行输入,使用P3^4(SER)、P3^5(RCLK)、P3^6(SRCLK)三个引脚
- 点阵列控制:直接使用P0端口
- 特别注意:J24跳线帽必须短接到GND端,这是点阵共阴/共阳配置的关键
点阵显示原理采用动态扫描方式:
- 每次只点亮一行中的一个LED
- 以足够快的速度逐行扫描(通常>60Hz)
- 利用人眼视觉暂留效应形成稳定图像
1.2 基础点控实验代码解析
下面这段代码实现了按下K1按键点亮左上角第一个LED,松开熄灭的功能:
c复制#include "reg51.h"
// 74HC595控制引脚定义
sbit SRCLK=P3^6; // 移位寄存器时钟
sbit RCLK=P3^5; // 存储寄存器时钟
sbit SER=P3^4; // 串行数据输入
// 点阵列控制端口
#define LEDDZ_COL_PORT P0
// 按键定义
sbit KEY1=P3^1;
// 点阵行列数据定义
u8 gled_col[8] = {0x7f, 0xbf, 0xdf, 0xef, 0xf7, 0xfb, 0xfd, 0xfe}; // 列选(低有效)
u8 gled_row[8] = {0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01}; // 行选(高有效)
void hc595_write_data(u8 dat) {
// 595数据写入函数
u8 i;
for(i=0;i<8;i++) {
SER = dat >> 7; // 取最高位
dat <<= 1; // 左移一位
SRCLK = 0; // 产生时钟上升沿
delay_10us(1);
SRCLK = 1;
delay_10us(1);
}
RCLK = 1; // 锁存数据
delay_10us(1);
RCLK = 0;
}
void display_point(u8 on) {
if(on) {
LEDDZ_COL_PORT = gled_col[0]; // 第0列
hc595_write_data(gled_row[0]); // 第0行
delay_10us(100);
}
// 消影处理
hc595_write_data(0x00);
LEDDZ_COL_PORT = 0xff;
}
void main() {
while(1) {
if(KEY1 == 0) { // 按键按下
display_point(1); // 点亮
} else {
display_point(0); // 熄灭
}
}
}
关键点说明:消影处理是LED点阵显示中的重要技术,如果不进行消影,在快速扫描时会出现"串亮"现象。原理是在切换行列时先关闭所有显示,再开启新的显示。
2. 点阵全点扫描实验
2.1 扫描原理与实现
全点扫描是测试点阵每个LED是否正常工作的基础方法。通过双重循环依次点亮每个LED,可以检查点阵的硬件连接是否正确。
扫描顺序通常有两种:
- 逐行扫描:固定列,扫描行
- 逐列扫描:固定行,扫描列
以下是逐点扫描的实现代码:
c复制void display_single_point(u8 row, u8 col) {
LEDDZ_COL_PORT = gled_col[col]; // 选中列
hc595_write_data(gled_row[row]); // 选中行
delay_10us(50000); // 显示延时
// 消影
hc595_write_data(0x00);
LEDDZ_COL_PORT = 0xff;
delay_10us(100);
}
void main() {
u8 row, col;
while(1) {
for(row=0; row<8; row++) {
for(col=0; col<8; col++) {
display_single_point(row, col);
}
}
}
}
2.2 扫描速度优化技巧
扫描速度直接影响显示效果,有几个关键参数需要注意:
-
每个点的显示时间:POINT_DELAY参数
- 太小:亮度不足
- 太大:闪烁明显
- 推荐值:30000-50000(对应约30-50ms)
-
消影时间:通常为显示时间的1/5-1/10
-
扫描周期计算:
- 单点时间 = 显示时间 + 消影时间
- 全点扫描周期 = 单点时间 × 64
- 刷新率 = 1 / 全点扫描周期
实测经验:当刷新率低于60Hz时,人眼会明显感觉到闪烁。因此全点扫描更适合用于硬件检测,实际应用中应采用更高效的扫描方式。
3. 贪吃蛇游戏实现(独立按键版)
3.1 游戏框架设计
贪吃蛇游戏是嵌入式系统中经典的综合性实验,涉及:
- 按键输入处理
- 定时器中断
- 点阵显示控制
- 游戏逻辑实现
游戏主要数据结构:
c复制#define SIZE 8 // 点阵尺寸
#define MAX_LEN 16 // 蛇最大长度
typedef enum { LEFT, RIGHT, UP, DOWN } DIR; // 方向枚举
typedef struct {
u8 x;
u8 y;
} POS; // 位置结构体
POS snake[MAX_LEN]; // 蛇身数组
u8 len; // 当前长度
DIR dir; // 当前方向
POS food; // 食物位置
bit game_over; // 游戏结束标志
3.2 核心代码实现
游戏主循环结构:
c复制void main() {
timer0_init(); // 初始化定时器
game_init(); // 初始化游戏
while(1) {
key = key_scan(); // 扫描按键
if(game_over) {
if(key != KEY_UNPRESS) game_init(); // 重新开始
} else {
// 方向控制
switch(key) {
case KEY1_PRESS: if(dir!=RIGHT) dir=LEFT; break;
case KEY2_PRESS: if(dir!=LEFT) dir=RIGHT; break;
case KEY3_PRESS: if(dir!=DOWN) dir=UP; break;
case KEY4_PRESS: if(dir!=UP) dir=DOWN; break;
}
if(need_move) { // 定时移动
need_move = 0;
snake_move();
}
}
flush_scan(); // 更新显示缓存
led_scan(); // 点阵扫描显示
}
}
定时器中断控制移动速度:
c复制void timer0_init() {
TMOD |= 0x01; // 定时器0模式1
TH0 = (65536-10000)/256; // 10ms中断
TL0 = (65536-10000)%256;
ET0 = 1; // 允许中断
TR0 = 1; // 启动定时器
EA = 1; // 开总中断
}
void timer0_isr() interrupt 1 {
static u16 cnt = 0;
TH0 = (65536-10000)/256;
TL0 = (65536-10000)%256;
if(++cnt >= 30) { // 约300ms移动一次
cnt = 0;
need_move = 1;
}
}
3.3 显示刷新优化
贪吃蛇游戏需要实时刷新显示,采用显示缓存技术可以大幅提高效率:
c复制u8 scan_buf[8]; // 显示缓存
void flush_scan() {
u8 i;
// 清空缓存
for(i=0; i<8; i++) scan_buf[i] = 0;
if(game_over) {
// 显示游戏结束图案(X形)
scan_buf[0]=0x81; scan_buf[1]=0x42;
scan_buf[2]=0x24; scan_buf[3]=0x18;
scan_buf[4]=0x18; scan_buf[5]=0x24;
scan_buf[6]=0x42; scan_buf[7]=0x81;
return;
}
// 绘制蛇身
for(i=0; i<len; i++) {
scan_buf[snake[i].y] |= (1<<(7-snake[i].x));
}
// 绘制食物
scan_buf[food.y] |= (1<<(7-food.x));
}
void led_scan() {
u8 i;
for(i=0; i<8; i++) {
LEDDZ_COL_PORT = col_code[i]; // 选中列
hc595(scan_buf[i]); // 输出行数据
hc595(0x00); // 消影
LEDDZ_COL_PORT = 0xff; // 关闭所有列
}
}
4. 贪吃蛇游戏进阶(矩阵按键版)
4.1 矩阵按键扫描原理
4x4矩阵按键相比独立按键可以节省IO资源,扫描原理是:
- 逐行输出低电平
- 检测列线状态
- 通过行列组合确定按键位置
实现代码:
c复制u8 matrix_key_scan() {
u8 key_val = KEY_UNPRESS;
// 第一行扫描
ROW1=0; ROW2=1; ROW3=1; ROW4=1;
if(COL1==0) { delay_ms(10); while(COL1==0); key_val=S1_PRESS; }
if(COL2==0) { delay_ms(10); while(COL2==0); key_val=S2_PRESS; } // 上
// 其他行类似...
// 第二行扫描
ROW1=1; ROW2=0; ROW3=1; ROW4=1;
if(COL1==0) { delay_ms(10); while(COL1==0); key_val=S5_PRESS; } // 左
if(COL3==0) { delay_ms(10); while(COL3==0); key_val=S7_PRESS; } // 右
// 其他行类似...
return key_val;
}
4.2 游戏控制优化
矩阵按键版主要修改了按键映射部分:
c复制while(1) {
key = matrix_key_scan();
if(game_over) {
if(key!=KEY_UNPRESS) game_init();
} else {
switch(key) {
case S5_PRESS: if(dir!=RIGHT) dir=LEFT; break; // 左
case S7_PRESS: if(dir!=LEFT) dir=RIGHT; break; // 右
case S2_PRESS: if(dir!=DOWN) dir=UP; break; // 上
case S10_PRESS: if(dir!=UP) dir=DOWN; break; // 下
}
if(need_move) {
need_move = 0;
snake_move();
}
}
flush_scan();
led_scan();
}
4.3 常见问题与调试技巧
-
点阵显示闪烁严重
- 检查消影处理是否完善
- 调整扫描速度,确保刷新率>60Hz
- 检查延时函数精度
-
按键响应不灵敏
- 增加按键消抖时间(10-20ms)
- 检查按键扫描频率是否足够
- 确保按键释放检测可靠
-
蛇移动不流畅
- 调整定时器中断周期
- 优化移动算法,避免复杂计算
- 检查碰撞检测逻辑
-
显示残影
- 加强消影处理
- 确保列切换时行数据已清零
- 检查硬件连接是否稳定
-
游戏逻辑错误
- 添加调试输出(可通过串口)
- 分模块测试(先测试按键,再测试显示,最后整合)
- 使用模拟器逐步调试
调试心得:在资源有限的51单片机上开发游戏,最重要的是做好性能优化。我的经验是:(1)将耗时操作放在定时器中断外;(2)使用查表法替代实时计算;(3)合理使用静态变量减少栈开销;(4)显示刷新与逻辑更新分离。
