
简介本资源是一个基于STM32F103微控制器实现的嵌入式五子棋游戏项目面向嵌入式初学者与STM32进阶开发者聚焦触摸交互、SD卡数据存储与实时游戏逻辑开发等典型应用场景。项目完整包含401个文件涵盖50个C源码核心驱动与游戏逻辑、43个头文件外设与GUI接口定义、45个编译中间文件.o/.d及98个HTML文档可能为配套说明或GUI资源另有PNG图像、HEX/AXF可执行镜像及Keil工程文件.uvprojx/.uvoptx整体压缩包7.64MB结构清晰便于分模块学习与调试。已有611人下载学习适合通过实操掌握Cortex-M3底层驱动、SPI-SD卡读写、触摸屏坐标解析、二维棋盘状态管理及简易AI落子判断等关键技术点是理解嵌入式人机交互系统设计的典型范例。1. 一块STM32F103最小系统板如何跑起一个可触摸、能存档的五子棋你手头那块带SPI Flash和4.3寸电阻式触摸屏的STM32F103C8T6开发板不是只能点灯或串口打印——它真能下五子棋。这个项目不依赖PC端渲染所有逻辑在片上运行棋盘坐标映射由ADC采样软件滤波完成胜负判定用四方向线性扫描而非递归回溯存档数据写入SD卡采用FAT16精简实现非FatFS全量移植连AI落子都只用固定深度的启发式评估无Alpha-Beta剪枝。它面向的是嵌入式初学者代码结构清晰中断服务例程ISR与主循环解耦GPIO初始化全部显式配置未用HAL库自动生成功能甚至保留了Keil uVision 5.30工程中.uvprojx.bak和.uvguix.bak等备份文件——说明作者经历过真实调试崩溃。如果你正用标准库V3.5开发又想验证SPI驱动SD卡、触摸校准、二维数组状态机这三个硬核模块这个项目比“点灯流水灯”更贴近真实产品逻辑。2. 触摸屏坐标映射与SD卡FAT16存储的底层实现2.1 电阻式触摸屏的ADC采样与坐标校准STM32F103的ADC1通道10PA0和通道11PA1分别连接X、Y电极通过软件切换PB0/PB1为推挽输出控制X-、Y-接地。每次采样执行四步操作先拉低X-使X电极输出电压读取PA0获取X坐标再拉低Y-读取PA1得Y坐标两次采样后延时10μs消除寄生电容影响最后对连续5次采样值取中位数滤波。关键代码如下// touch.c 中核心采样函数 uint16_t Read_X(void) { GPIO_ResetBits(GPIOB, GPIO_Pin_0); // X- 0 GPIO_SetBits(GPIOB, GPIO_Pin_1); // Y- 1 (高阻) ADC_RegularChannelConfig(ADC1, ADC_Channel_10, 1, ADC_SampleTime_55Cycles5); ADC_SoftwareStartConvCmd(ADC1, ENABLE); while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC)); return ADC_GetConversionValue(ADC1); } // 校准参数存储在Flash第0页0x08000000 typedef struct { int16_t x1, y1; // 左上角物理坐标 int16_t x2, y2; // 右下角物理坐标 } TouchCalib_t; TouchCalib_t calib_param {120, 85, 3720, 2610}; // 实测值非理论最大值注意校准参数必须实测直接使用理论ADC满幅值0~4095会导致触摸偏移。本项目在main.c中预留CALIBRATE_MODE宏短按KEY_UP进入校准界面依次点击屏幕四角生成calib_param。物理坐标需满足x2-x1 3000且y2-y1 2000否则线性映射失真。2.2 SD卡SPI通信与FAT16扇区读写SD卡通过SPI2PB13-SCK, PB14-MISO, PB15-MOSI, PB12-NSS连接初始化流程严格遵循SD 2.0协议先发74个CLK空闲脉冲再发送CMD0复位CMD1查询就绪状态CMD16设置块长度为512字节。关键在于扇区地址计算——FAT16中LBA地址数据区首扇区簇号×每簇扇区数而本项目将游戏存档固定存于LBA 100即第100个512字节扇区// sdio.c 中写入存档的简化逻辑 uint8_t SaveGameRecord(uint8_t player, uint16_t step_count) { uint8_t buffer[512]; buffer[0] player; // 当前玩家1黑/2白 buffer[1] step_count 0xFF; // 步数低字节 buffer[2] (step_count 8) 0xFF; // 步数高字节 buffer[3] CheckWinState(); // 胜负标志0平局/1黑胜/2白胜 // 计算LBA地址跳过MBR(1扇区)FAT1(2扇区)FAT2(2扇区)RootDir(16扇区)21扇区 // 数据区从22扇区开始存档固定用第100扇区LBA100 if(SD_WriteBlock(buffer, 100, 1) SD_OK) return 1; return 0; }2.2.1 FAT16关键参数解析表参数名值说明BPB_BytsPerSec512每扇区字节数SD卡固定值BPB_SecPerClus1每簇扇区数本项目设为1以简化寻址BPB_RsvdSecCnt1保留扇区数MBR所在扇区BPB_NumFATs2FAT表份数冗余备份BPB_RootEntCnt512根目录项数FAT16限制BPB_TotSec1610240总扇区数5MB卡对应值提示若更换SD卡需重算BPB_TotSec16。用diskpart在Windows下查看磁盘容量字节÷512即得该值写入fat16.h中对应宏定义。未修改此值会导致SD_ReadBlock读取超界。3. 五子棋状态机与触摸事件驱动的落子逻辑3.1 棋盘状态二维数组与合法性检查棋盘用uint8_t board[15][15]表示初始全0空位1代表黑子2代表白子。落子前必须验证三重条件坐标在15×15范围内、目标位置为空、当前玩家符合回合制规则current_player全局变量。关键检查函数// game_logic.c uint8_t IsLegalMove(uint8_t x, uint8_t y) { if(x 15 || y 15) return 0; // 超出棋盘 if(board[x][y] ! 0) return 0; // 位置已被占 if(current_player ! (step_count % 2 ? 2 : 1)) return 0; // 非当前玩家回合 return 1; }3.1.1 四方向连珠检测算法详解胜负判定不采用DFS遍历而是对落子点(x,y)向四个方向横、竖、斜、反斜各延伸4格统计同色棋子数量。例如横向检测uint8_t CheckHorizontal(uint8_t x, uint8_t y) { uint8_t count 1; // 自身计1 // 向右检测 for(uint8_t i 1; i 5 xi 15; i) { if(board[xi][y] board[x][y]) count; else break; } // 向左检测 for(uint8_t i 1; i 5 x-i 0; i) { if(board[x-i][y] board[x][y]) count; else break; } return (count 5) ? board[x][y] : 0; // 返回获胜方ID }注意该算法时间复杂度O(1)比全盘扫描快100倍。但需确保board数组索引不越界——xi15和x-i0判断必不可少否则触发HardFault。3.2 触摸中断与主循环协同机制触摸事件通过EXTI9对应PA9触发但实际处理在主循环中完成避免在ISR中执行耗时操作。具体流程EXTI9_IRQHandler中仅置位touch_flag 1主循环检测到flag后调用GetTouchPoint(x,y)获取坐标坐标经MapToBoard(x,y,board_x,board_y)转换为棋盘索引执行IsLegalMove(board_x,board_y)验证合法则board[board_x][board_y] current_player并刷新LCD。// main.c 主循环片段 while(1) { if(touch_flag) { touch_flag 0; if(GetTouchPoint(raw_x, raw_y)) { if(MapToBoard(raw_x, raw_y, board_x, board_y)) { if(IsLegalMove(board_x, board_y)) { board[board_x][board_y] current_player; step_count; current_player (current_player 1) ? 2 : 1; LCD_Refresh(); // 刷新棋盘图像 SaveGameRecord(current_player, step_count); } } } } Delay_ms(10); // 防抖延时 }4. Keil工程配置与标准库V3.5下的编译排错4.1 STM32F103标准库V3.5工程结构解析本项目使用ST官方标准外设库V3.50非HAL库工程目录结构严格遵循规范CMSIS/包含core_cm3.h及启动文件startup_stm32f10x_md.sFWLIB/标准库源码stm32f10x_gpio.c等需在Keil中添加对应.c文件到工程USER/用户代码main.c、game_logic.c、touch.c等OUTPUT/编译输出目录.axf、.hex关键配置项在Options for Target → C/C中Define:USE_STDPERIPH_DRIVER, STM32F10X_MDInclude Paths:.\CMSIS\,.\FWLIB\inc\,.\USER\Optimization:-O1避免内联函数导致调试困难4.1.1 常见编译错误与修复方案错误信息根本原因解决方法error: no stm32 target found!Keil未安装STM32设备支持包运行Keil\UV4\STM32F1xx_DFP.exe安装最新Device Family Packundefined reference to SystemInit启动文件未正确关联检查Options → Target → Startup中Startup file是否为startup_stm32f10x_md.sADC_GetConversionValue undefined未添加stm32f10x_adc.c到工程在Project窗口右键→Add Group→添加FWLIB/src/下对应文件__use_no_semihosting was requested, but _sys_exit was not defined未实现半主机系统调用在main.c中添加__attribute__((used)) int _sys_exit(int x) { while(1); }4.2 ST-Link V2下载失败的硬件级排查当Keil提示Cannot access Memory或No STM32 target found时按顺序检查SWD接线确认SWDIOPA13、SWCLKPA14、GND、3.3V四线连接牢固无虚焊BOOT0状态下载时BOOT0必须接GND运行用户程序模式BOOT1悬空电源纹波用示波器测VDDA引脚纹波应50mV否则ADC采样异常复位电路NRST引脚外接10kΩ上拉100nF电容按下复位键时电压应从3.3V跌至0V再回升。提示若ST-Link Utility识别到设备但Keil无法下载尝试在Options → Debug → Settings → SWD中勾选Connect under reset强制复位后连接。5. 游戏AI的启发式评估与性能优化技巧5.1 简化版AI落子策略实现本项目AI不采用蒙特卡洛树搜索MCTS而是基于三类权重的启发式评估威胁分检测对手是否已形成“活四”两端空立即拦截机会分己方形成“活三”时优先扩展为“活四”中心分棋盘中心区域6≤x≤8且6≤y≤8落子权重×2。核心评估函数// ai.c int8_t EvaluatePosition(uint8_t x, uint8_t y) { int8_t score 0; if(IsThreateningFour(x, y, 1)) score 1000; // 对手活四必须拦截 if(IsMyThree(x, y, 2)) score 500; // 我方活三优先扩展 if(x 6 x 8 y 6 y 8) score 200; // 中心加权 return score; } // 扫描所有空位选择最高分位置 void AIThink(void) { uint8_t best_x 0, best_y 0; int8_t max_score -128; for(uint8_t x 0; x 15; x) { for(uint8_t y 0; y 15; y) { if(board[x][y] 0) { int8_t s EvaluatePosition(x, y); if(s max_score) { max_score s; best_x x; best_y y; } } } } board[best_x][best_y] 2; // AI执白子 }5.2 LCD刷新效率优化局部重绘与双缓冲原始版本每次落子重绘整个15×15棋盘耗时约120ms优化后仅刷新变化区域记录上一次落子坐标last_x,last_y新落子时只重绘以(last_x,last_y)和(new_x,new_y)为中心的3×3区域使用LCD_SetCursor()定位后逐像素写入避免全屏清屏。// lcd_driver.c 局部刷新关键代码 void LCD_RefreshPartial(uint8_t old_x, uint8_t old_y, uint8_t new_x, uint8_t new_y) { // 计算需刷新的矩形区域扩展1格防边缘裁剪 uint8_t min_x (old_x 0) ? old_x-1 : 0; uint8_t max_x (old_x 14) ? old_x1 : 14; uint8_t min_y (old_y 0) ? old_y-1 : 0; uint8_t max_y (old_y 14) ? old_y1 : 14; for(uint8_t x min_x; x max_x; x) { for(uint8_t y min_y; y max_y; y) { LCD_DrawStone(x, y, board[x][y]); // 仅重绘该坐标棋子 } } }5.2.1 双缓冲机制实现表缓冲区类型存储位置更新时机优势前台缓冲区LCDGRAM显存每次LCD_RefreshPartial调用直接显示无延迟后台缓冲区SRAM0x20000000起board[][]更新后异步拷贝避免刷新时数据被修改启用双缓冲需在LCD_Init()中分配2KB内存并在LCD_RefreshPartial末尾添加memcpy(LCDGRAM, back_buffer, 2048)。实测将单次刷新耗时从120ms降至35ms触控响应延迟低于80ms符合人眼感知流畅阈值。技巧若发现AI思考时屏幕卡顿可在AIThink()函数开头插入LCD_Fill(0,0,320,240,WHITE)快速清屏再调用LCD_RefreshPartial局部重绘视觉上更连贯。本文还有配套的精品资源点击获取