简介:基于STM32F4(原子探索者)的五子棋对战平台,主要面向嵌入式系统学习者与游戏开发爱好者,完整实现触摸下子、人机对战、人人对战、悔棋以及音量开关等实用功能,且工程结构清晰,便于在不同开发板或平台上参考移植。压缩包共274个文件,大小5.44MB,核心代码由55个C源文件和50个头文件组成,涵盖LCD显示、触摸交互、游戏逻辑、AI对战算法等关键模块;包内另附编译生成的axf、hex固件、工程配置文件、文档说明以及界面预览图,可直接烧录体验,也能对照阅读代码理解实现细节。目前已有2166人学习下载。工程代码注释明确、模块划分合理,适合希望通过实际项目掌握STM32外设驱动、游戏程序设计或人机博弈算法的开发者,可在阅读后快速提取所需模块并迁移至自己的项目中。 做嵌入式这些年,经常有人问我:“想做个有点技术含量、能拿得出手,又不至于烂大街的STM32项目,做什么好?” 我一般会推荐一个方向——游戏类应用。而这里面,五子棋对战平台是最恰到好处的一个。它不像贪吃蛇那样光是逻辑堆砌,也不像智能小车那样依赖机械结构,它把显示、输入、算法、状态机、甚至通信都揉在了一起,难度曲线非常平滑,特别适合作为毕业设计或者求职简历上的实战项目。
这篇文章我会完整拆解一个“基于STM32的五子棋对战平台”该怎么做,从硬件选型、电路设计、软件架构、AI算法,到环境搭建和调试排坑,一条龙讲清楚。项目本身不复杂,但深入去挖,能挖出很多底层和算法的东西。不管是刚学完51准备进阶的学生,还是想做个完整项目给自己加分的嵌入式爱好者,这篇文章都值得你从头看到尾。
1. 项目整体设计与方案选型
很多新手拿到这个题目第一反应是:这不就是一块屏幕加几个按键吗?确实,功能上就是这么回事。但“做出来”和“做好”之间隔着一层选型的功力。
1.1 主控芯片怎么选:F103C8T6为什么是黄金起点
以五子棋的数据规模和运算量来看,几乎任何一款STM32都跑得动,但要选一个“性价比、学习成本、资料丰富度”三者平衡的型号,我首选STM32F103C8T6。
这颗芯片是Cortex-M3内核,主频72MHz,Flash 64KB,SRAM 20KB。五子棋15×15棋盘用 uint8_t 二维数组存储,也就225字节;加上LCD显存、AI搜索的临时数组,20KB SRAM绰绰有余。72MHz的主频做贪心评分AI是毫秒级响应,做3层极小化极大搜索也不会有明显的卡顿感。
如果后续想扩展功能,比如加语音播报(需要音频解码)、加触摸屏(需要FSMC总线)、或者上FreeRTOS做多任务,F103C8T6依然可以战一战,只是RAM会有点吃紧。我的建议是:
- 预算有限、入门为主:F103C8T6最小系统板,10块钱左右;
- 想玩得更从容、后续扩展空间大:STM32F407ZET6,主频168MHz,RAM大得多,还能用FSMC直驱大屏。
但五子棋这个项目,F103C8T6是完全够用的。别一上来就追高配,把精力花在算法和代码结构上,收获更大。
1.2 显示方案怎么选:OLED还是TFT-LCD
显示方案直接决定了项目的观感和用户体验。我见过有人用0.96寸OLED做五子棋,屏幕上一格格非常小,棋子只能用像素点表示,玩起来费眼睛。也有人用12864这种字符屏做,压根画不出来棋子的感觉。
推荐两种方案:
| 方案 | 屏幕 | 接口 | 优点 | 缺点 |
|---|---|---|---|---|
| 入门方案 | OLED SSD1306 0.96寸 | I2C/SPI | 便宜、驱动简单、库多 | 屏幕小,视觉效果差 |
| 推荐方案 | TFT-LCD ILI9341 2.8寸/3.5寸 | SPI/FSMC | 尺寸大、颜色丰富、棋盘清晰 | 接线较多,初始化稍复杂 |
TFT-LCD里,ILI9341是最经典的控制器,网上驱动代码一抓一大把,2.8寸款用4线SPI就能驱动,占用的GPIO引脚不多,画一个15×15的棋盘绰绰有余。驱动库方面,可以用逐飞、正点原子的LCD驱动,或者找一份SPI版ILI9341驱动移植,代码量不大,移植也不是难事。
我建议你直接买带触摸的3.5寸版本,虽然触摸在这个项目中不是必需的,但后续如果你想把“按键落子”升级成“触摸落子”,硬件上就不用再折腾了。
1.3 输入方案的取舍:独立按键还是矩阵键盘
落子需要输入坐标或者移动光标,这里有两个方向:
独立按键方案:4个方向键 + 1个确认键 + 1个返回键,共6个GPIO。优点:按键定义清晰、代码好写、不容易误触。缺点是按键数量多,接线有点乱。
矩阵键盘方案:4×4矩阵键盘,使用8个IO口扫描16个键。可以设计成“数字坐标输入”,比如按“A”代表横坐标,按“5”代表纵坐标,再按“确认”落子。也可以做方向键映射。优点:锻炼扫描算法、看起来专业。缺点是代码稍微复杂,需要处理消抖和组合按键。
两种方案我都在实际项目中试过。如果是毕设,我强烈推荐矩阵键盘——答辩时“矩阵键盘扫描原理”是一个很好的加分点,能讲的东西多。而且4×4键盘在项目展示时视觉上也更有“平台感”。
需要注意的是,不管选哪种方案,尽量避开已经占用的特殊功能引脚。STM32F103C8T6的PA13/PA14/PA15、PB3/PB4默认是SWD和JTAG引脚,如果做IO使用,需要额外配置或会导致无法下载程序。这是个经常踩的坑,后面还会细说。
1.4 系统架构:裸机还是RTOS
五子棋这个项目,裸机大循环完全够用,我建议入门者不要为了“显得高级”而强行上FreeRTOS。裸机代码的可读性更高,调试也更直观。
裸机架构的主循环大概长这样:
int main(void) { SystemInit(); Delay_Init(); LCD_Init(); KEY_Init(); Game_Init(); while (1) { Key_Scan(); // 扫描按键,设置事件标志 Game_Process(); // 根据事件更新游戏状态 Display_Update(); // 刷新LCD画面 } }这个结构简单清晰,三个环节各自独立。按键扫描可以放在主循环里,也可以放到定时器中断里(比如10ms一次),但注意中断里只做“记录按键事件”,真正处理逻辑放到主循环,避免中断服务函数里跑复杂逻辑导致的卡顿和误触发。
如果你后续想优化,可以把显示刷新也拆出去,用DMA传输LCD数据,主循环只负责逻辑运算。不过对于当前项目,裸机就足够,重点还是要放在算法上。
2. 硬件电路设计与核心模块详解
一个完整的五子棋对战平台,硬件上包括最小系统、显示模块、输入模块,以及一些补充电路。这个部分我拆开讲清楚每个电路的原理和关键参数。
2.1 STM32最小系统与电源电路
STM32F103C8T6最小系统看起来简单,但有些细节影响很大:
- 供电:一般USB的5V进来,先经过AMS1117-3.3降压到3.3V。稳压芯片前后各加一个10μF和0.1μF滤波电容。电感布局上尽量靠近芯片电源引脚,别拉长走线。
- 8MHz晶振电路:这是最容易出问题的地方。晶振两端要接两个负载电容,典型值20pF左右。
- 复位电路:NRST引脚接一个10kΩ上拉电阻到3.3V,再接一个0.1μF电容到GND。按下复位键时把引脚拉低,实现复位。
- BOOT0引脚:一定要接一个10kΩ下拉电阻到GND,让芯片从主Flash启动。如果悬空,可能偶尔启动到系统存储器(变成ISP模式),现象是程序不跑、下载后无反应。
关于晶振电容,很多人直接抄电路图,不知道为什么要接20pF。这个值其实有计算公式:
负载电容CL ≈ (C1 × C2) / (C1 + C2) + Cstray
其中Cstray是PCB走线和引脚寄生电容,大约5~10pF。如果目标负载电容CL是20pF,那么两个外接电容分别取22pF左右,算下来(22×22)/(22+22)+6 ≈ 17pF,差不多在合理范围内。选电容时不用过分纠结,10~22pF都能让晶振正常起振,关键是板子布线时晶振要靠近MCU引脚,走线短且不要有长距离平行线。
2.2 TFT-LCD显示模块接线与驱动要点
ILI9341的2.8寸屏,SPI模式,引脚不算多:
| LCD引脚 | 连接到 STM32 | 说明 |
|---|---|---|
| VCC | 3.3V | 电源 |
| GND | GND | 地 |
| CS | PB12 | 片选,低有效 |
| RESET | PB13 | 复位,低有效 |
| DC/RS | PB14 | 数据/命令选择 |
| SCLK | PB13 | SPI时钟(SPI2 SCK) |
| MOSI/SDA | PB15 | SPI数据(SPI2 MOSI) |
| LED | 3.3V串电阻 | 背光,串联一个10Ω~100Ω限流 |
这里SCLK和MOSI的组合,我推荐直接使用STM32硬件SPI2,而不是GPIO模拟。硬件SPI速率可以达到9MHz,刷屏流畅。注意SPI模式要设置成SPI_MODE0(CPOL=0,CPHA=0),这是大多数LCD控制器的要求。
初始化时序上有个容易忽略的点:LCD的RESET引脚需要在初始化时拉低至少10ms再拉高,然后延时等待控制器内部复位完成。很多人LCD白屏,复位时序不对是常见原因之一。
2.3 4×4矩阵键盘的原理与接线
矩阵键盘节省IO的原理很简单:4根行线作为输出,4根列线作为输入,每个按键位于行列交叉点。
扫描逻辑如下:
- 把4根行线全部拉高,然后逐行拉低(比如先拉低第1行,其他行高);
- 读取4根列线的电平,哪一列是低电平,就说明第1行和该列交叉点的按键被按下;
- 同一个时刻只有一行被拉低,所以行与行之间不会互相干扰;
- 配合10~20ms延时做软件消抖。
STM32的GPIO初始化为开漏输出拉高,列线初始化为浮空输入(内部上拉),这样可以省略外部上拉电阻。代码示意:
// 第1行拉低,其他行拉高 GPIO_WriteBit(GPIOA, GPIO_Pin_0, Bit_SET); // 行1 GPIO_WriteBit(GPIOA, GPIO_Pin_1, Bit_RESET); // 行2 低 GPIO_WriteBit(GPIOA, GPIO_Pin_2, Bit_SET); // 行3 GPIO_WriteBit(GPIOA, GPIO_Pin_3, Bit_SET); // 行4 // 读取列电平 uint8_t col = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7);有了矩阵键盘之后,可以设计一个简单直观的下棋输入方式:第一键输入横坐标(A~O),第二键输入纵坐标(0~14),第三键按“确认”落子,按“返回”重新输入。缺点是需要三次按键,不如方向键直觉。我最终采用的方向键思路是:用2×2矩阵键盘映射成“上/下/左/右”,加两个独立确认/返回。这种混搭方案兼顾了代码复杂度与交互体验。
2.4 电源与供电注意事项
整个平台的功耗不高,LCD带背光亮起时电流也就几十毫安到一百多毫安,USB供电完全没问题。有一点要提醒:如果用电脑USB口供电,有些老电脑的USB口电压不太稳,LCD背光会闪烁,程序运行也可能随机复位,可以在USB电源输入端并联一个470μF电解电容,能显著改善稳定性。
3. 软件框架与核心算法
说实话,硬件接线只是体力活,真正的技术含量主要体现在软件架构和AI对弈算法上。这个部分也是写论文或者面试时最值得展开讲的。
3.1 数据结构与状态机设计
先用最简单且可读性高的数据结构来存储棋盘状态:
#define BOARD_SIZE 15 uint8_t board[BOARD_SIZE][BOARD_SIZE]; // 0: 空; 1: 黑棋; 2: 白棋 uint8_t current_player; // 0: 玩家 1: AI uint8_t game_state; // 0: 对局中 1: 分出胜负 2: 平局游戏流程设计成状态机:
- STATE_MENU:主菜单,可选“双人对战”“人机对战”
- STATE_PLAYING:对局中,接收输入并落子
- STATE_END:结束画面,显示胜方,可按任意键返回菜单
状态机的好处是代码逻辑清晰,不容易出现“下完棋还能继续落子”这种bug。我用一个 switch 语句处理状态流转,配合按键事件结构体:
typedef struct { uint8_t key_code; uint8_t key_pressed; } KeyEvent;按键扫描函数只负责填充这个结构体,主循环读它,按键和逻辑完全解耦,后面改输入方式(比如换成触摸屏)都不用动游戏逻辑。
3.2 胜负判定:落子后的四个方向检查
五子棋的胜负判定并不复杂,因为只需要在每次落子后检查当前落点周围是否有五连,不必全盘扫描。高效且清晰的写法是:从落子点出发,向正反两个方向累计相同棋子数量。
static int CheckDirection(int x, int y, int dx, int dy, uint8_t piece) { int count = 1; for (int i = 1; i < 5; i++) { int nx = x + dx * i; int ny = y + dy * i; if (nx < 0 || nx >= BOARD_SIZE || ny < 0 || ny >= BOARD_SIZE) break; if (board[nx][ny] != piece) break; count++; } for (int i = 1; i < 5; i++) { int nx = x - dx * i; int ny = y - dy * i; if (nx < 0 || nx >= BOARD_SIZE || ny < 0 || ny >= BOARD_SIZE) break; if (board[nx][ny] != piece) break; count++; } return count; }四个方向分别是:(1,0) 横、(0,1) 纵、(1,1) 斜向下、(1,-1) 斜向上。只要其中任一条连子数量大于等于5,就判胜利。注意边界检查要放在访问数组之前,否则数组越界可能把程序搞挂。
3.3 人机AI:从贪心评分到极小化极大
人机对战是这个平台的核心亮点,也是拉开档次的地方。
入门版:贪心评分法
给每个空位打分,分值依靠该位置形成的棋型:
- 五连:100000分
- 活四:50000分
- 冲四:8000分
- 活三:20000分
- 眠三:3000分
- 活二:1000分
AI计算时,分别站在自己角度和对手角度给所有空位打分,然后选择综合分最高的点落子:
for (每1个空位 p) { ai_score = Evaluate(p, AI_PIECE); player_score = Evaluate(p, PLAYER_PIECE); total_score = ai_score * 1.2 + player_score; if (total_score > best_score) { best_pos = p; } }这个“1.2”是防守偏向系数,没有绝对标准,但建议大于1.0,这样AI在“进攻”和“防守”之间稍微偏重进攻,棋风更有压迫感。贪心评分法的优点是计算量极小,STM32F103跑起来完全无压力,缺点是只有一层视野,遇到复杂局面容易中招。
进阶版:极小化极大 + α-β剪枝
如果想让AI真正会“思考”,就要用搜索树。每次AI落子,假设玩家也会做出最优应对,构建出一棵博弈树,然后通过负极大值(Negamax)或者极小化极大来计算最佳落子。
伪代码:
int Minimax(int depth, int alpha, int beta, uint8_t currentPlayer) { if (depth == 0 || IsTerminalNode()) return Evaluate(); for (int i = 0; i < candidate_count; i++) { PlacePiece(candidate[i], currentPlayer); int score = -Minimax(depth - 1, -beta, -alpha, opponent); RemovePiece(candidate[i]); if (score >= beta) return beta; if (score > alpha) alpha = score; } return alpha; }这里最关键的是候选点的选择,不能遍历全部225个空位,计算量会爆炸。一个常见的优化是:只考虑“已有棋子周边2格范围内”的空位,候选点一般能降到30个以内,搜索深度设为3层,在72MHz的F103上大约需要几百毫秒,体验可以接受。
3.4 显示刷新与界面设计
界面设计直接影响用户体验。我的做法是:
- 15×15棋盘用两重循环画网格线,每隔4格画一条稍粗的线,方便定位;
- 棋子的绘制是实心圆,黑棋用黑色填充,白棋用白色填充再加一个1像素的黑色描边,这样在LCD上视觉效果更清晰;
- 当前选中的交叉点用一个红色小方框(或圆圈)标记,方便键盘操作定位;
- 顶部留一条状态栏,显示当前回合、玩家信息和提示。
LCD刷新是个容易拖低流畅度的地方。我的经验是:不需要每帧重画整个棋盘,正常情况下只有“一个棋子”和“光标位置”发生变化,所以只把这些区域设置为脏矩形,局部更新即可。这样刷屏延迟会从几十毫秒降到几毫秒,操作体验完全不同。
4. 开发环境搭建与程序部署调试
这个项目开发中,很多零基础的同学卡在最开始:环境装不好、工程建不对、程序下载不进去。这个章节我从头梳理。
4.1 Keil5安装与芯片包部署
STM32开发最常用的IDE是Keil MDK5(也可以选STM32CubeIDE,但Keil在国内资料多,调试方便,我推荐新手用Keil)。
安装后重点提醒:MDK5的芯片支持不再内置,必须额外安装STM32F1系列芯片包。很多人的MDK装了却发现“没有STM32F103C8”的可选项,就是因为缺这一步。
正确做法:
- 打开Keil,点击 Pack Installer 图标;
- 搜索“STM32F1xx”,下载对应芯片支持包;
- 网络不好的时候,去官网手动下载 Keil.STM32F1xx_DFP.pack,双击也能安装。
装好芯片包后,新建工程时选 STM32F103C8 即可。
4.2 标准库新建工程:目录结构和编译配置
STM32标准库虽然“退出历史舞台”,但它的代码清晰度远胜HAL库,而且网上教程最多。新建工程时,我建议这样组织目录:
Project/ ├── CORE/ // 启动文件、core_cm3.h ├── FWLib/ // STM32标准外设库 ├── HARDWARE/ // LCD、KEY、LED等硬件驱动 ├── SYSTEM/ // 延时、串口、中断 ├── USER/ // main.c、stm32f10x_it.c └── OBJ/ // 编译输出配置要点:
- C/C++选项卡里,Define填
USE_STDPERIPH_DRIVER, STM32F10X_MD; - Include Paths把上面每个目录都加进去,漏一个就报头文件找不到;
- Debug选项卡选择ST-Link Debugger,Settings里确认能读到芯片ID;
- Utilities选项卡勾选“Flash Download”,烧录算法添加 STM32F10x Med-density Flash(F103C8是中等容量,别选错了)。
4.3 下载失败:error: no stm32 target found! 的排查实录
这个报错我相信很多新手都被劝退过,完整的报错是:
error: no stm32 target found! if your product embeds debug authentication, please这个提示虽然带debug authentication(新芯片默认启用的一种安全保护),但F103C8T6通常不是这个原因。排查顺序如下:
- 接线检查:SWD接口只需要3根线——SWDIO、SWCLK、GND。杜邦线超过20cm或接触不良都会导致找不到目标。直连ST-Link尽量缩短线长。
- 供电确认:目标板必须先上电。有些ST-Link的3.3V输出可以给板子供电,但电流有限,带LCD时候容易电压跌落;建议外接USB供电,ST-Link只接数据线。
- SWD引脚被复用:最常见的原因!如果板子上一次烧录的程序把PA13/PA14(SWDIO/SWCLK)配置成了普通IO,下一次就连不上。解决方法是按住复位键,点下载,然后立刻松开复位。有些场景还要按久一点,全片擦除后再烧录正常的程序。
- BOOT0被拉高:BOOT0为1时芯片进入系统存储器模式,不会执行用户代码,也可能提示找不到目标。检查BOOT0跳线帽是否在0端。
如果以上都排查完还不行,用STM32 ST-LINK Utility做一次“Full Chip Erase”,能解决绝大部分顽固问题。
4.4 程序烧录后不运行的常见原因
烧录成功后板子没反应,我的排查经验是:
- 启动文件选错:F103C8T6是中等容量(64KB Flash),必须用 startup_stm32f10x_md.s;如果用成高容量 hd 的,程序大概率跑飞。很多直接从网上抄工程的同学都栽在这。
- 时钟配置错误:系统初始化时,要经过PLL把8MHz倍频到72MHz。如果晶振没焊接好或电容不匹配,PLL锁不住,程序会卡在时钟初始化死循环。
- GPIO初始化遗漏:某个外设没开RCC时钟,程序执行到GPIO配置时表现很奇怪。新手最容易漏的(不要笑,我见过好多次)是LCD复位引脚没初始化为输出,导致复位悬空,LCD白屏。
- 数组越界或栈溢出:程序跑着跑着死机,多半是某个索引越界把数据写坏了。可以把HardFault_Handler里加一个断点,直接定位在哪行触发。
5. 常见问题与排查技巧实录
5.1 LCD白屏,但程序在跑
这个是出现频率最高的问题。我的排查顺序:
- 先确认背光亮不亮。不亮——查LED引脚和限流电阻;
- 亮但纯白——查硬件复位时序,复位引脚必须拉低再拉高;
- 初始化序列对不对——ILI9341的初始化配置寄存器序列必须完整,有些屏幕用编译器优化选项不同会导致延时不准,初始化失败。可以把SPI速度降到1MHz试试;
- SPI模式对不对——ILI9341习惯SPI Mode0,个别屏支持Mode2,看规格书或驱动代码说明。
5.2 按键抖动导致的误触发
矩阵键盘消抖,最稳妥的做法是用定时器中断固定周期扫描,比如10ms扫一次,连续两次检测结果一致才确认按键触发。不要在主循环里用while等按键释放的方式,那样会阻塞游戏逻辑。
另外,按键接的GPIO要配置输入上拉,否则浮空状态下容易误触发。内部弱上拉几十千欧,通常够用。
5.3 延时函数卡死
“delay卡死”这个坑,最常见的两个原因:
- 用了SysTick延时,但没有正确初始化SysTick或配置中断。标准库默认的Delay_Init里设置SysTick时钟源和重载值,如果漏了,会停在那里死等。
- 优化级别过高,比如-O3把延时循环里的空转优化掉了,导致延时时间变成0。解决方法是把延时函数里的变量加
volatile修饰,或者干脆用SysTick。
5.4 ST-Link虚拟串口出现黄色感叹号
设备管理器里ST-Link的Virtual COM Port有感叹号,本质不是ST-Link坏了,而是驱动没装好。Windows 10/11下,ST-Link驱动有时候会在系统更新后被替换成通用驱动,重新安装STSW-LINK009或者直接用STM32CubeProgrammer自带的驱动即可。另外,有些USB线只支持充电不支持数据传输,插上去会反复断连,果断换线。
5.5 两台STM32联机对战怎么扩展
如果你想在这个平台基础上加一个“双机联机对战”的功能,最简单是UART串口互联:
// 发送落子坐标 uint8_t buf[4] = {0xAA, x, y, 0x55}; HAL_UART_Transmit(&huart1, buf, 4, 100);接收方校验帧头0xAA、帧尾0x55,再把坐标写入本地棋盘。这种自定义帧协议不复杂,但需要处理粘包和丢帧,是练通信协议的一个好题目。我在推荐这个扩展时,都会强调:别一上来就上完整协议栈,先定好帧格式,用串口助手两边互发调通了,再接入真实数据。
5.6 程序跑飞的通用排查手段
开发过程中我习惯在HardFault_Handler里加上按键复位(比如无限循环等待复位),然后用调试器的寄存器窗口查看PC指针位置,直接定位到跳飞行的代码行。如果没有调试器,也可以在关键函数入口做个LED翻转,看哪个环节没跑到,二分定位。
6. 写在最后的一点经验
说实话,五子棋对战平台这个项目,硬件部分两天就能搭完,真正耗时间的是AI算法调优和一堆莫名其妙的小bug。我做这个项目的感受是:别急着追求AI棋力多强,先把“一台能稳定运行、交互顺手、代码结构清晰”的平台做出来,AI再从贪心评分升级到搜索算法,每一步都能拿出可演示的成果,做汇总材料时也更有层次感。
最后分享一个小编程技巧:棋盘坐标(横坐标)用A~O而不是0~14来表示,这样按键输入的数字和字母能对应上,且界面不会拥挤。如果你打算做毕业设计,还可以把“AI搜索深度的动态调整”做成一个特色功能——玩家选择不同难度时,AI自动在“深度1层的贪心”和“深度3层的搜索”之间切换。这样一个看似普通的五子棋平台,在技术点的深度和丰富度上就比大多数同学的项目高出一个段位了。
本文还有配套的精品资源,点击获取