电赛井字棋无视觉方案:STM32按键矩阵与LED点阵实现
2026/7/21 16:15:29 网站建设 项目流程

对于大一新生来说,电赛的井字棋题目是一个很好的入门项目,它综合了硬件控制、逻辑判断和状态管理。题目要求实现一个无视觉方案的井字棋对弈系统,这意味着不能使用摄像头等视觉传感器来识别棋盘状态,而是需要通过其他传感器(如按键、触摸传感器、磁感应等)来获取落子位置,并通过点阵、LED灯阵或液晶屏来显示棋盘。

无视觉方案的核心在于如何可靠地检测玩家的落子动作,以及如何清晰地向玩家反馈当前棋盘状态。下面将围绕1到6问的实现,提供一个完整的方案,涵盖硬件选型、电路设计、核心逻辑、状态机和调试要点。

1. 理解题目要求和核心挑战

井字棋是一个3x3的棋盘,两名玩家轮流在空格放置自己的棋子(通常用X和O表示),先连成一条直线(横、竖、斜)者获胜。如果棋盘填满仍未分出胜负,则为平局。

无视觉方案需要解决几个关键问题:

  1. 落子检测:如何知道玩家在哪个位置落子。
  2. 棋盘显示:如何让玩家和裁判看清当前棋盘状态。
  3. 游戏逻辑:如何判断胜负、处理平局、管理游戏流程(开始、轮流落子、结束、重置)。
  4. 人机交互:除了基本的落子和显示,可能还需要有开始按钮、重置按钮、胜负提示(如LED灯、蜂鸣器)等。

对于大一新生,建议从最基础的方案入手,先实现功能,再考虑优化。

2. 硬件方案设计:按键矩阵与LED点阵

一个简单可靠的方案是使用按键矩阵作为输入,LED点阵作为输出。

2.1 硬件选型清单

类别器件说明数量参考
主控MCUSTM32F103C8T6 (核心板)资源丰富,资料多,性价比高1
输入器件轻触按键用于9个棋格位置输入9
独立按键用于开始/重置等功能1-2
输出显示8x8 LED点阵屏 (带MAX7219驱动)可显示3x3棋盘,驱动简单1
状态指示LED灯红绿LED,指示当前玩家、胜负状态2-3
声音提示有源蜂鸣器落子提示、胜负提示1
其他电阻、杜邦线、面包板/PCB10kΩ上拉电阻,220Ω限流电阻若干

注意:如果题目对成本或功耗有特殊要求,可以选用更简单的MCU(如STC8系列、ATmega328P)和更简单的显示方案(如9个LED灯直接表示棋盘),但点阵屏方案显示效果更直观专业。

2.2 系统连接示意图

[按键矩阵] ----> [MCU GPIO] <---- [LED点阵屏 (SPI)] [开始按键] ----> [MCU GPIO] <---- [状态LED] [重置按键] ----> [MCU GPIO] <---- [蜂鸣器]

按键连接:最简单的做法是9个棋格按键直接连接到MCU的9个GPIO口,并配置为上拉输入。当按键按下时,GPIO读到低电平。这种接法软件简单,但占用IO口较多。如果IO口紧张,可以排成3x3的矩阵,只需要6个IO口,但扫描程序稍复杂。

LED点阵屏连接:带MAX7219驱动的点阵屏通常通过SPI接口(DIN, CLK, CS)与MCU连接,只需要3个IO口,大大节省了资源。MAX7219芯片内部负责扫描显示,MCU只需发送显示数据即可。

3. 软件逻辑设计与实现

软件是项目的核心,需要清晰地管理游戏状态。

3.1 定义关键数据结构和变量

首先,在代码开头定义核心的变量和数据结构。

// 定义棋盘状态:0为空,1为玩家1(X),2为玩家2(O) uint8_t board[3][3] = {0}; // 定义当前轮到哪位玩家落子 (1 或 2) uint8_t currentPlayer = 1; // 定义游戏状态机 typedef enum { GAME_STATE_IDLE, // 空闲,等待开始 GAME_STATE_PLAYING, // 对弈中 GAME_STATE_WIN, // 有玩家获胜 GAME_STATE_DRAW // 平局 } GameState_t; GameState_t gameState = GAME_STATE_IDLE; // 按键引脚定义(以STM32 HAL库为例,假设使用9个独立GPIO) #define KEY_START_PIN GPIO_PIN_0 #define KEY_START_PORT GPIOA // ... 定义其他9个棋格按键的PIN和PORT

3.2 主循环与状态机

主程序的核心是一个状态机,根据不同的gameState执行不同的操作。

int main(void) { // 硬件初始化 HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_SPI1_Init(); // 初始化SPI用于点阵屏 LEDMatrix_Init(); // 初始化点阵屏 Buzzer_Beep(100); // 开机提示音 while (1) { switch (gameState) { case GAME_STATE_IDLE: idle_state_handler(); break; case GAME_STATE_PLAYING: playing_state_handler(); break; case GAME_STATE_WIN: case GAME_STATE_DRAW: game_end_state_handler(); break; } HAL_Delay(50); // 简单的延时,避免过于频繁扫描 } }

3.3 关键函数实现

3.3.1 空闲状态 (GAME_STATE_IDLE)

等待玩家按下开始按钮。

void idle_state_handler(void) { // 在点阵屏上显示欢迎动画或提示"Press Start" LEDMatrix_ShowMessage("START"); if (HAL_GPIO_ReadPin(KEY_START_PORT, KEY_START_PIN) == GPIO_PIN_RESET) { HAL_Delay(20); // 简单消抖 if (HAL_GPIO_ReadPin(KEY_START_PORT, KEY_START_PIN) == GPIO_PIN_RESET) { // 确认按键按下,初始化游戏 game_init(); gameState = GAME_STATE_PLAYING; Buzzer_Beep(50); } while (HAL_GPIO_ReadPin(KEY_START_PORT, KEY_START_PIN) == GPIO_PIN_RESET); // 等待按键释放 } } void game_init(void) { // 清空棋盘 for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { board[i][j] = 0; } } // 随机或指定先手玩家 currentPlayer = 1; // 更新显示 update_display(); }
3.3.2 对弈状态 (GAME_STATE_PLAYING)

这是最核心的状态,循环扫描9个棋格按键。

void playing_state_handler(void) { // 扫描9个按键 for (int row = 0; row < 3; row++) { for (int col = 0; col < 3; col++) { // 获取对应按键的GPIO状态(假设有函数get_key_state) if (get_key_state(row, col) == KEY_PRESSED) { // 处理落子 process_move(row, col); // 处理一次按键后退出扫描,避免连续落子 return; } } } } void process_move(int row, int col) { // 检查该位置是否为空 if (board[row][col] != 0) { // 位置已被占用,可以给个提示音或闪烁提示 Buzzer_Beep(20); return; } // 落子有效 board[row][col] = currentPlayer; Buzzer_Beep(50); // 落子音效 // 检查胜负 if (check_win(currentPlayer)) { gameState = GAME_STATE_WIN; // 闪烁获胜方棋子或显示"WIN" indicate_winner(currentPlayer); return; } // 检查平局 if (check_draw()) { gameState = GAME_STATE_DRAW; // 显示平局提示 indicate_draw(); return; } // 游戏继续,切换玩家 currentPlayer = (currentPlayer == 1) ? 2 : 1; // 更新棋盘显示,可以用不同亮度或符号区分玩家 update_display(); }
3.3.3 胜负判断函数 (check_win)

这是经典的逻辑判断,检查8条线(3横、3竖、2斜)。

uint8_t check_win(uint8_t player) { // 检查行 for (int i = 0; i < 3; i++) { if (board[i][0] == player && board[i][1] == player && board[i][2] == player) { return 1; } } // 检查列 for (int j = 0; j < 3; j++) { if (board[0][j] == player && board[1][j] == player && board[2][j] == player) { return 1; } } // 检查对角线 if (board[0][0] == player && board[1][1] == player && board[2][2] == player) { return 1; } if (board[0][2] == player && board[1][1] == player && board[2][0] == player) { return 1; } return 0; } uint8_t check_draw(void) { for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { if (board[i][j] == 0) { return 0; // 还有空位,不是平局 } } } return 1; // 棋盘已满,平局 }
3.3.4 显示更新函数 (update_display)

将内存中的board数组映射到LED点阵屏的显示缓冲区。

void update_display(void) { uint8_t displayBuffer[8] = {0}; // 8x8点阵的显示缓冲区,每字节代表一行 // 遍历3x3棋盘,在8x8点阵上画出3x3的格子 // 例如,可以用左上角的3x3区域来显示,每个棋格占2x2个点 for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { if (board[i][j] == 1) { // 玩家1,显示"X" // 在对应位置画X,例如在(i*2, j*2)为中心的2x2区域 draw_x(displayBuffer, i*2, j*2); } else if (board[i][j] == 2) { // 玩家2,显示"O" draw_o(displayBuffer, i*2, j*2); } // 为空则什么都不画 } } // 还可以画上棋盘网格线,增强可读性 draw_grid(displayBuffer); // 将缓冲区数据发送到点阵屏 LEDMatrix_SendBuffer(displayBuffer); } // 简单的画X函数示例(在指定坐标画一个2x2的X) void draw_x(uint8_t buf[8], int startRow, int startCol) { buf[startRow] |= (1 << (7 - startCol)) | (1 << (7 - (startCol+1))); buf[startRow+1] |= (1 << (7 - startCol)) | (1 << (7 - (startCol+1))); // 这是一个非常简单的X,实际可以画得更复杂美观 }

4. 进阶优化与功能扩展

实现基本功能后,可以考虑以下优化来提升完成度和用户体验,以应对电赛的评分要求。

4.1 增加人机对战模式

题目要求是双人对战,但增加一个简单AI作为可选模式是很好的亮点。

// 简单的AI:寻找获胜机会,否则随机落子 void ai_make_move(void) { // 1. 检查AI自己是否能一步获胜 for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { if (board[i][j] == 0) { board[i][j] = currentPlayer; // 模拟落子 if (check_win(currentPlayer)) { // 就是这一步了! return; // 棋盘已经修改 } board[i][j] = 0; // 撤销模拟 } } } // 2. 检查是否需要阻挡玩家获胜 uint8_t humanPlayer = (currentPlayer == 1) ? 2 : 1; for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { if (board[i][j] == 0) { board[i][j] = humanPlayer; // 模拟玩家落子 if (check_win(humanPlayer)) { board[i][j] = currentPlayer; // 实际落下AI的棋子进行阻挡 return; } board[i][j] = 0; // 撤销模拟 } } } // 3. 如果中心空着,优先占中心 if (board[1][1] == 0) { board[1][1] = currentPlayer; return; } // 4. 随机选择一个空位落子 int emptyCells[9][2]; int count = 0; for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { if (board[i][j] == 0) { emptyCells[count][0] = i; emptyCells[count][1] = j; count++; } } } if (count > 0) { int choice = rand() % count; board[emptyCells[choice][0]][emptyCells[choice][1]] = currentPlayer; } }

playing_state_handler中,如果当前是AI回合,则调用ai_make_move,然后直接进行胜负判断和切换玩家。

4.2 完善显示与交互

  • 当前玩家提示:用不同颜色的LED灯或点阵屏上的特定位置指示当前轮到谁。
  • 落子动画:落子时,让对应的棋子图案有一个短暂的闪烁或放大效果。
  • 胜负高亮:当有玩家获胜时,让连成一条线的三个棋子闪烁高亮。
  • 音效多样化:为落子、获胜、平局、非法操作设置不同的音调或节奏。

4.3 软件抗干扰处理

  • 按键消抖:上面的代码使用了简单的延时消抖。更可靠的方法是使用状态机或定时器进行软件消抖。
  • 防止按键长按:在等待按键释放的循环中增加超时判断,避免程序卡死。
// 改进的按键检测示例(状态机消抖) typedef enum {KEY_IDLE, KEY_DEBOUNCE, KEY_PRESSED, KEY_RELEASE} KeyState_t; KeyState_t key_check(GPIO_TypeDef* Port, uint16_t Pin, KeyState_t currentState) { switch (currentState) { case KEY_IDLE: if (HAL_GPIO_ReadPin(Port, Pin) == GPIO_PIN_RESET) { currentState = KEY_DEBOUNCE; } break; case KEY_DEBOUNCE: HAL_Delay(10); if (HAL_GPIO_ReadPin(Port, Pin) == GPIO_PIN_RESET) { currentState = KEY_PRESSED; } else { currentState = KEY_IDLE; } break; case KEY_PRESSED: if (HAL_GPIO_ReadPin(Port, Pin) == GPIO_PIN_SET) { currentState = KEY_RELEASE; } break; case KEY_RELEASE: HAL_Delay(10); if (HAL_GPIO_ReadPin(Port, Pin) == GPIO_PIN_SET) { currentState = KEY_IDLE; } else { currentState = KEY_PRESSED; } break; } return currentState; }

5. 常见问题与调试要点

在实现过程中,大一新生最容易遇到以下问题:

问题现象可能原因检查与解决思路
按键无反应1. GPIO模式配置错误(应为上拉输入)
2. 硬件连接虚焊或断路
3. 消抖逻辑过于严格或错误
1. 用万用表测量按键按下前后引脚电平变化。
2. 写一个简单的测试程序,只检测一个按键并点亮一个LED。
点阵屏不显示或乱码1. SPI时序或模式不匹配
2. MAX7219初始化命令错误
3. 数据发送顺序(MSB/LSB)错误
1. 查阅MAX7219数据手册,确认初始化序列。
2. 先尝试发送固定的简单图案(如全亮、第一行亮)来测试。
游戏逻辑混乱(如连续落子)1. 按键扫描和处理逻辑有漏洞,未处理好按键释放
2. 状态机切换条件不清晰
1. 在按键处理函数中加入while循环等待按键释放。
2. 使用调试器或串口打印当前状态和按键值,进行逻辑分析。
胜负判断错误1.check_win函数逻辑错误
2. 棋盘数组board的索引使用错误(行列混淆)
1. 在平局、各种获胜情况下,手动设置board数组,单步调试check_win函数。
程序运行不稳定1. 缺少看门狗
2. 中断冲突
3. 堆栈溢出
1. 开启独立看门狗(IWDG)。
2. 检查是否有不必要的中断,或中断服务函数执行时间过长。

调试建议

  1. 分模块调试:先让按键和LED灯正常工作,再调试点阵屏显示,最后整合游戏逻辑。
  2. 善用串口打印:使用printf通过串口输出变量值、函数执行状态,这是最有效的调试手段之一。
  3. 使用逻辑分析仪:如果条件允许,用逻辑分析仪检查SPI信号,可以快速定位点阵屏的通信问题。

6. 项目总结与扩展方向

通过这个项目,你能够掌握嵌入式系统开发的基本流程:需求分析、硬件选型、电路搭建、软件模块化设计、状态机编程、调试排错。无视觉方案的核心是构建一个可靠的人机交互闭环。

完成基础要求后,可以考虑以下扩展来提升项目竞争力:

  • 更复杂的AI:实现极小化极大算法(Minimax)带Alpha-Beta剪枝,使AI不可战胜。
  • 多种显示模式:增加比分记录、历史对局回顾等功能。
  • 无线通信:通过蓝牙或Wi-Fi实现两个独立设备的联机对弈。
  • 低功耗设计:在空闲状态进入低功耗模式,通过按键中断唤醒。

这个项目很好地融合了硬件和软件,是电子设计竞赛中控制类题目的典型代表。把基础功能做稳定、做流畅,是取得好成绩的关键。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询