
简介本资源是一套面向嵌入式初学者与高校课程设计者的STM32实战项目——双人五子棋系统适用于毕业设计、课程设计、工程实训及学科竞赛等实践场景。项目基于STM32F103系列单片机开发已通过完整功能测试支持直接烧录运行配套硬件连接方案含面包板替代PCB的简化接线指导大幅降低入门门槛。压缩包共259个文件主体为74个.h头文件与71个.c源码文件涵盖LCD显示、触摸屏驱动、定时器控制、Flash存储及RCC时钟配置等核心模块另有编译输出文件.o/.axf/.hex、Keil工程配置.uvprojx/.uvoptx、调试脚本.bat及说明文档.md结构规范便于理解代码逻辑与工程组织。目前已有202人学习下载资源提供完整可复现的软硬协同方案、清晰的模块划分与作者一对一答疑支持是嵌入式系统开发与交互类项目落地的优质参考范例。1. 这不是玩具棋盘而是一套可复现的嵌入式人机交互最小闭环系统你手头那块 STM32F103C8T6 开发板接上一块 40pin 并行接口 LCD比如 ILI9341 或 ST7735 驱动的 2.4 寸屏再连两个独立按键——这套硬件组合就能跑起一个真正可交互、有状态、带图形界面的双人五子棋。它不依赖 PC 上位机不调用操作系统 GUI 库所有逻辑在裸机环境下完成按键扫描消抖、LCD 像素级绘图、棋盘坐标映射、胜负判定算法、状态机切换、Flash 持久化对局记录。这不是 Keil 工程模板的简单拼接而是将嵌入式开发中「外设驱动—状态管理—人机反馈」三者咬合得严丝合缝的典型范例。适合课程设计快速交付、毕设功能模块复用、嵌入式初学者理解“从寄存器到游戏”的完整链路也适合已有经验者拆解其 LCD 显示优化策略与中断响应时序控制逻辑。2. 基于 STM32F10x 标准外设库的硬件抽象层构建与 LCD 图形驱动实现2.1 为什么选用标准外设库而非 HAL 库关键在于时序可控性与资源确定性本项目采用stm32f10x_rcc.c、stm32f10x_tim.c、stm32f10x_flash.c等标准外设库文件而非 STM32CubeMX 生成的 HAL 库。根本原因在于五子棋 UI 对 LCD 刷新帧率和按键响应延迟极为敏感。HAL 库中大量使用动态内存分配与回调注册机制在裸机环境下易引入不可预测的中断延迟而标准库直接操作寄存器所有函数执行周期可静态估算。例如RCC_DeInit()执行固定 12 个周期TIM_SetCounter(TIM2, 0)是单条汇编指令。在 72MHz 主频下一次完整的 16×16 棋盘重绘含清屏网格线落子点耗时约 83ms完全满足人眼无闪烁要求。若改用 HAL仅HAL_GPIO_WritePin()的封装开销就可能增加 15%20% 执行时间导致连续按键被漏判。提示工程中lcd.c并非直接调用GPIO_SetBits()而是封装了LCD_WR_DATA()和LCD_WR_CMD()两个底层函数它们通过宏定义绑定到具体 GPIO 端口如GPIOB与引脚如GPIO_Pin_0GPIO_Pin_15确保编译期地址固化避免运行时查表跳转。2.2 40pin LCD 接口的物理连接与初始化时序解析本项目适配的是 16 位并行数据总线 控制信号的 LCD 模组常见于 2.4/2.8 寸 TFT 屏。其核心引脚定义如下LCD 引脚STM32 引脚功能说明D0–D15PB0–PB1516 位数据总线需配置为推挽输出RSPA0寄存器选择高电平写数据低电平写命令RWPA1读/写选择本项目固定为写模式拉低ENPA2使能信号下降沿锁存数据RESETPA3复位信号低电平有效持续 ≥10ms初始化代码位于lcd.c的LCD_Init()函数中关键步骤如下// 1. 配置 GPIOPB0–PB15 为推挽输出PA0–PA3 为推挽输出 GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; // 初始化数据总线 PB0–PB15 GPIO_InitStructure.GPIO_Pin GPIO_Pin_All; GPIO_Init(GPIOB, GPIO_InitStructure); // 初始化控制线 PA0–PA3 GPIO_InitStructure.GPIO_Pin GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3; GPIO_Init(GPIOA, GPIO_InitStructure); // 2. 发送硬件复位脉冲PA3 GPIO_ResetBits(GPIOA, GPIO_Pin_3); Delay_ms(15); GPIO_SetBits(GPIOA, GPIO_Pin_3); Delay_ms(15); // 3. 发送初始化序列以 ILI9341 为例 LCD_WriteCommand(0x01); // 软件复位 Delay_ms(150); LCD_WriteCommand(0x11); // 退出睡眠 Delay_ms(120); LCD_WriteCommand(0x36); // 设置 MADCTLRGB/BGR 顺序、扫描方向 LCD_WriteData(0x48); // 0x48 旋转 0° BGR 模式 LCD_WriteCommand(0x3A); // 设置像素格式16bit/pixel LCD_WriteData(0x55); LCD_WriteCommand(0x29); // 开启显示2.2.1 为什么Delay_ms()不能用 SysTick——裸机延时的精度陷阱项目中所有延时均采用for循环空转实现见delay.c而非 SysTick 中断。原因在于LCD 初始化序列中多个指令间要求严格微秒级等待如0x11后需 ≥120ms而 SysTick 默认 1ms 中断周期在高频操作中会引入上下文切换开销且裸机环境下中断优先级配置不当易导致延时漂移。实测表明在SystemCoreClock 72000000下for(i0; i120000; i)可稳定提供 120ms±0.3ms 延时误差远低于 LCD 规格书允许的 ±5% 容差。2.3 棋盘坐标系到 LCD 像素坐标的双向映射算法五子棋逻辑运行在抽象的 15×15 棋盘上坐标范围0≤x≤14, 0≤y≤14但 LCD 显示需转换为物理像素如 240×320 分辨率。项目采用线性映射 边界偏移策略棋盘左上角起点像素坐标(X0, Y0) (40, 20)单格宽度/高度GRID_W 14,GRID_H 14棋子中心像素坐标px X0 x * GRID_W GRID_W/2,py Y0 y * GRID_H GRID_H/2该映射被封装为宏#define BOARD_X0 40 #define BOARD_Y0 20 #define GRID_SIZE 14 #define GET_PX(x) (BOARD_X0 (x) * GRID_SIZE GRID_SIZE/2) #define GET_PY(y) (BOARD_Y0 (y) * GRID_SIZE GRID_SIZE/2)注意GRID_SIZE设为 14 而非 15是为了在 15×15 棋盘下留出 1 像素间隙避免线条粘连。实测发现当GRID_SIZE15时第 14 行网格线会超出 LCD 可视区域右边界导致右侧棋子显示不全。2.4 LCD 显示中文的底层实现字模提取与显存搬运优化项目在棋盘上方显示“玩家一”、“玩家二”、“胜者黑方”等中文提示未使用外部字库芯片而是将 GB2312 编码的 16×16 点阵字模硬编码进 Flash。关键在于lcd.c中的LCD_DisplayCNChar()函数void LCD_DisplayCNChar(uint16_t x, uint16_t y, const uint8_t *font_data) { uint16_t i, j; uint8_t pixel; for (i 0; i 16; i) { // 行循环 LCD_SetCursor(x, y i); // 设置光标到第 i 行起始列 for (j 0; j 2; j) { // 每行 2 字节16bit pixel font_data[i * 2 j]; LCD_WR_DATA((uint16_t)pixel 8); // 高字节先送ILI9341 模式 } } }2.4.1 字模数据如何生成——GB2312 到 C 数组的自动化流程实际开发中我们使用PCtoLCD2002工具加载 GB2312 字库输入“玩家一”三字导出 C 语言数组。每个汉字对应 32 字节16 行 × 2 字节/行按行优先存储。例如“玩”字的前 4 字节为0x00, 0x00, 0x00, 0x00顶部空白第 10 行数据为0xF8, 0x07表示该行中间 11 个像素点亮。该数组被声明为const并置于FLASH段避免占用宝贵的 SRAM。3. 双人博弈状态机设计与胜负判定算法的嵌入式落地3.1 基于有限状态机FSM的游戏流程控制整个游戏逻辑不使用while(1)轮询主循环而是由TIM2定时中断10ms 周期驱动状态迁移。状态定义如下状态枚举值触发条件主要行为GAME_IDLE系统上电或复位清空棋盘数组显示初始界面等待按键GAME_WAIT_PLAYER1检测到 KEY1 按下且当前为空位在坐标处绘制黑子切换至GAME_WAIT_PLAYER2GAME_WAIT_PLAYER2检测到 KEY2 按下且当前为空位在坐标处绘制白子切换至GAME_WAIT_PLAYER1GAME_OVER胜负判定返回 true显示胜者信息进入 5 秒倒计时后自动回GAME_IDLE状态机核心代码位于main.c的Game_StateMachine()函数中其结构为switch (game_state) { case GAME_IDLE: LCD_Clear(Black); DrawBoard(); // 绘制棋盘网格 game_state GAME_WAIT_PLAYER1; break; case GAME_WAIT_PLAYER1: if (Key_Scan(KEY1) KEY_ON) { if (CheckValidPos(cur_x, cur_y)) { SetChess(cur_x, cur_y, BLACK); DrawChess(cur_x, cur_y, BLACK); if (CheckWin(cur_x, cur_y, BLACK)) { game_state GAME_OVER; winner BLACK; } else { game_state GAME_WAIT_PLAYER2; } } } break; // ... 其他状态分支 }提示Key_Scan()函数内部已集成 20ms 消抖通过 TIM2 中断计数实现避免机械按键抖动导致误触发。若直接使用GPIO_ReadInputDataBit()轮询需额外添加软件延时会阻塞状态机响应。3.2 五子连珠判定算法空间复杂度 O(1) 的嵌入式友好实现传统二维数组遍历检查横、竖、两斜共 4 个方向在 15×15 棋盘上最坏需 15×15×4×5 4500 次内存访问。本项目采用增量式判定法每次落子后仅检查以该点为中心的 4 个方向上是否形成 5 子连线且只扫描该方向上连续同色棋子数量。// 检查 (x,y) 位置落子 color 后是否获胜 uint8_t CheckWin(uint8_t x, uint8_t y, uint8_t color) { int8_t dx[4] {1, 0, 1, 1}; // 四方向横、竖、主对角、副对角 int8_t dy[4] {0, 1, 1, -1}; uint8_t i, count; for (i 0; i 4; i) { count 1; // 当前落子本身 // 正向延伸 for (int8_t tx x dx[i], ty y dy[i]; tx 0 tx 15 ty 0 ty 15 chess_board[ty][tx] color; tx dx[i], ty dy[i]) { count; } // 反向延伸 for (int8_t tx x - dx[i], ty y - dy[i]; tx 0 tx 15 ty 0 ty 15 chess_board[ty][tx] color; tx - dx[i], ty - dy[i]) { count; } if (count 5) return 1; } return 0; }3.2.1 为什么不用递归或动态规划——栈空间与执行效率的硬约束STM32F103C8T6 的 SRAM 仅 20KB其中用户可用约 16KB。若采用递归搜索每层调用至少消耗 12 字节栈帧返回地址参数5 层深度即 60 字节。而本算法全程使用寄存器变量与局部循环最大栈深度恒为 3 层CheckWin→for循环 → 内层for实测编译后函数栈开销为 0 字节全部优化进寄存器。Keil uVision5 在Optimize Level 3下对此类循环展开效果极佳。3.3 按键事件与坐标映射基于 ADC 采样的模拟按键矩阵可选扩展当前工程使用两个独立按键KEY1/KEY2分别代表玩家一、玩家二。但项目预留了ADC1通道支持模拟摇杆输入——通过读取PA0ADC_IN0电压值映射为棋盘坐标uint16_t adc_val Get_ADC_Value(ADC_Channel_0); uint8_t x (adc_val * 15) / 4096; // 将 0–4095 映射到 0–14 uint8_t y (Get_ADC_Value(ADC_Channel_1) * 15) / 4096;此方案需外接电位器或摇杆模块虽未在默认工程启用但stm32f10x_adc.c已完成初始化配置只需取消#if 0注释即可启用。相比纯数字按键它支持更自然的“移动光标确认”操作模式是向复杂人机交互演进的关键接口。4. Keil MDK 工程配置细节与常见编译/下载故障排查4.1 工程文件结构解析与关键配置项定位解压后的.zip包中TOUCH.uvguix.AA是 UVision5 的.uvprojx工程文件TOUCH.axf是已编译的可执行镜像可用于快速验证。keilkilll.bat是清理临时文件的批处理脚本内容为echo off del /f /q *.axf del /f /q *.tra del /f /q *.o del /f /q *.dep del /f /q *.crf del /f /q *.lnp del /f /q *.plg del /f /q *.htm del /f /q *.lst del /f /q *.mpf del /f /q *.mpj del /f /q *.sct del /f /q *.build_log.htm rmdir /s /q Objects rmdir /s /q Listings echo Clean completed. pause提示该脚本删除Objects和Listings目录强制 Keil 重新编译全部源文件解决因头文件修改未触发依赖更新导致的“修改不生效”问题。4.2 必须修改的 3 个工程配置项否则无法编译通过打开 Keil uVision5右键工程名 →Options for Target需手动校验以下三项配置页签选项名称正确值错误后果TargetDeviceSTM32F103C8若选错型号如 F103CBFlash 算法不匹配下载失败OutputName of ExecutableTOUCH.axf若改名keilkilll.bat中的del *.axf仍可工作但调试时需同步更新C/CDefineUSE_STDPERIPH_DRIVER缺失则stm32f10x.h不启用标准库编译报RCC_DeInit未定义此外C/C → Include Paths中必须包含.\CMSIS\Include .\STM32F10x_StdPeriph_Driver\inc .\User .\LCD4.3 典型下载失败场景与串口日志分析法当点击Load按钮后 Keil 报错No Debugging Interface Found或Flash Download failed — Could not load file请按以下顺序排查4.3.1 检查 SWD 接口物理连接使用万用表测量SWDIOPA13、SWCLKPA14对地电压正常应为 3.3V。若为 0V检查开发板BOOT0是否短接到 GND必须为 0 才能进入 Flash 下载模式。4.3.2 查看Debug → Serial Wire Viewer中的 ITM 输出若启用在main.c开头加入#include core_cm3.h #define ITM_STIMULUS_PORT0 (*((volatile unsigned int*)(0xE0000000))) #define ITM_TRACE_EN (*((volatile unsigned int*)(0xE0000E00))) void ITM_SendChar(uint32_t ch) { while (!(ITM_TRACE_EN 1)); ITM_STIMULUS_PORT0 ch; }然后在main()中调用ITM_SendChar(S);若串口监视器收到S证明 SWD 通信链路正常问题出在 Flash 算法或目标芯片识别。4.3.3 Keil Pack 安装验证表Pack 名称版本号安装状态检查路径Keil.STM32F1xx_DFP.2.3.0.pack必须安装Project → Manage → Pack Installer→ 搜索STM32F1ARM.CMSIS.5.9.0.pack建议安装否则core_cm3.h可能缺失若 Pack 未安装Keil 会报cmsis_armcc.h: No such file or directory。此时需访问 Keil 官网 Pack 页面 下载对应.pack文件双击安装。5. 基于现有框架的 3 种实用扩展方向与代码注入点5.1 添加蓝牙串口透传模块HC-05实现手机端遥控在保留原有双人本地对战基础上接入 HC-05 模块TX→PA10, RX→PA9利用USART1实现 AT 指令配置与数据透传。扩展要点修改usart.c启用USART_IT_RXNE中断接收手机发送的坐标指令如1,3表示第 1 行第 3 列在USART1_IRQHandler()中解析字符串调用SetChess(x, y, current_player)current_player由game_state自动切换手机端仅负责发送坐标不参与玩家身份管理。提示HC-05 默认波特率 9600需在USART1_Config()中将USART_InitStruct-USART_BaudRate设为9600并关闭USART_HardwareFlowControl。5.2 棋局记录持久化利用 STM32 内置 Flash 模拟 EEPROMstm32f10x_flash.c已提供FLASH_ProgramWord()和FLASH_ErasePage()接口。可定义一个__attribute__((at(0x0800F000)))的全局结构体存储最近 5 局胜负结果typedef struct { uint8_t player1_wins; uint8_t player2_wins; uint32_t last_game_time; // 时间戳单位秒 } GameRecord_T; GameRecord_T game_record __attribute__((at(0x0800F000)));在GAME_OVER状态退出前调用FLASH_Unlock()→FLASH_ErasePage(0x0800F000)→FLASH_ProgramWord()写入更新值。注意Flash 编程前必须擦除整页1KB且寿命约 10000 次因此仅用于低频记录。5.3 LCD 亮度动态调节通过 PWM 控制背光 LED 电流若 LCD 模组背光引脚通常标为LED_A/LED_K连接至PA8TIM1_CH1可启用高级定时器 PWM 输出TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE); TIM_TimeBaseStructure.TIM_Period 999; // 1kHz PWM 频率72MHz/72000 TIM_TimeBaseStructure.TIM_Prescaler 71; TIM_TimeBaseStructure.TIM_ClockDivision 0; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM1, TIM_TimeBaseStructure); TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse 500; // 初始占空比 50% TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC1Init(TIM1, TIM_OCInitStructure); TIM_Cmd(TIM1, ENABLE); TIM_CtrlPWMOutputs(TIM1, ENABLE);随后通过TIM_SetCompare1(TIM1, duty_cycle)动态调整duty_cycle0–999实现无级调光。此功能可绑定长按 KEY1 触发提升用户体验。本文还有配套的精品资源点击获取