news 2026/9/17 15:50:54

STM32双人五子棋课设全攻略:从时钟树到状态机

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32双人五子棋课设全攻略:从时钟树到状态机

简介:基于STM32的双人五子棋资源,适合嵌入式、自动化、电子信息等专业学生用于课程设计或毕业设计参考,也适合具备一定C语言与单片机基础的学习者进行项目进阶。压缩包共259个文件,以C源文件和H头文件为主,另有工程配置文件、编译中间文件及烧录文件等,整体大小约6.42MB,可直接导入Keil工程查看或重新编译,便于对照学习STM32外设初始化与对弈逻辑,整体目录结构清晰,便于按模块查阅与修改。当前已有1089人学习下载,说明其作为课设样例具有较高参考价值。内容涵盖LCD显示驱动、触摸输入处理、棋盘绘制与胜负判定等核心模块,可帮助理解嵌入式图形界面与状态机设计思路。代码经过完整测试运行稳定,可直接用于答辩演示并便于二次修改调试,也可在此基础上扩展AI对战、计分等功能,满足不同层次学习需求。

1. 双人五子棋为什么是 STM32 课设里的硬骨头

双人五子棋的“双人”意味着同一套设备要交替处理两位玩家的操作,“五子棋”则要求棋盘渲染、落子判定和胜负检测在毫秒级完成。许多人在拿到这类题目时以为只是写个循环判断,真正动手才发现:LCD 画点慢、触摸采样抖、按键消抖没做、状态没有清零,每一步都能让最终效果变成“能编译但没法演示”。这个课设项目虽然代码量不大,却把 GPIO、外部中断、定时器、LCD 驱动、状态机全部串在了一起,适合计算机、自动化、电子信息专业的课设与毕设起步。项目源码能在 STM32F103 平台上直接运行,核心文件包括 lcd.c、stm32f10x_tim.c、stm32f10x_rcc.c 等,下载后打开工程就能看到完整的双人对战流程。完成它之后,你才有能力回答面试里常问的“你的嵌入式代码如何保证实时响应”。

2. 硬件选型与最小系统:LCD 驱动、触摸输入与 RCC 时钟树

2.1 从工程文件反推硬件连接:TOUCH、LCD 与系统时钟

看工程文件列表能发现几个关键点:TOUCH.uvguix.AA是 Keil 工程,TOUCH.axf是编译输出,lcd.c负责显示,stm32f10x_rcc.c负责时钟,stm32f10x_tim.c负责定时,stm32f10x_flash.c则常见于字库或参数存储。主控是 STM32F103 系列,这类芯片在课设里最常见的配置是 72 MHz 主频、64 KB Flash,外设资源足够驱动一块 2.4 英寸 TFT LCD。

虽然工程里没有给出原理图,但按常规课设接线可以反推:LCD 的数据口通常接在 GPIOB 或 GPIOA 的 8 位并口上,控制线 CS、RS、WR、RD 接在剩余 GPIO 上。触摸屏如果是电阻屏,则 X+、X-、Y+、Y- 分别接到 ADC 通道,利用 STM32 内部 ADC 进行坐标采样。文件里没有专门的 touch.c,说明触摸逻辑可能写在主文件或 lcd.c 中,工程结构精简,适合课程设计答辩时逐行讲解。

2.2 时钟树配置:为什么先开 RCC 再碰外设

STM32 任何外设的寄存器操作,第一步都是先使能对应外设的时钟。很多初学者直接写 GPIO 寄存器却发现引脚没反应,原因就是没有调用RCC_APB2PeriphClockCmd。标准外设库的写法是:

void SystemClock_Config(void) { ErrorStatus HSEStartUpStatus; RCC_DeInit(); RCC_HSEConfig(RCC_HSE_ON); // 开启外部高速晶振 HSEStartUpStatus = RCC_WaitForHSEStartUp(); if (HSEStartUpStatus == SUCCESS) { FLASH_PrefetchBufferCmd(FLASH_PrefetchBuffer_Enable); FLASH_SetLatency(FLASH_Latency_2); // 72MHz 时需要 2 个等待周期 RCC_HCLKConfig(RCC_SYSCLK_Div1); RCC_PCLK1Config(RCC_HCLK_Div2); // APB1 不超过 36MHz RCC_PCLK2Config(RCC_HCLK_Div1); // APB2 可到 72MHz RCC_PLLConfig(RCC_PLLSource_HSE_Div1, RCC_PLLMul_9); RCC_PLLCmd(ENABLE); while (RCC_GetFlagStatus(RCC_FLAG_PLLRDY) == RESET); RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK); } }

这段代码把系统时钟从 HSE(外部晶振)锁相到 72 MHz。RCC_PLLMul_9表示 8 MHz 晶振 9 倍频,得到 72 MHz;RCC_PCLK1_Div2是因为 APB1 外设最高只能跑 36 MHz,而 LCD 和 GPIO 挂在 APB2 上,可以吃到 72 MHz。实际调试时如果 LCD 刷新率上不去,先查时钟树,再查 Flash 等待周期,这个顺序不能反。

2.3 按键与触摸两种输入方案的取舍

双人五子棋的输入可以只用按键:一个方向键加一个确认键就能完成落子。触摸屏方案体验更好,但需要处理坐标校准和触摸抖动。工程名里有TOUCH,说明原设计采用触摸或预留了触摸接口。按键方案的 GPIO 配置非常直观:

void KEY_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOE, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入,按键按下为低电平 GPIO_Init(GPIOE, &GPIO_InitStructure); }

GPIO_Mode_IPU上拉输入适合按键接 GND 的电路。这里只用 GPIOE 的低三位接三个按键:一个选位、一个确认、一个悔棋。按键输入比触摸更稳定,但落子效率低。触摸输入需要额外写触摸屏控制器驱动,比如 XPT2046 的 SPI 读写,同时要把触摸坐标映射到 LCD 像素坐标。课设答辩时,评审老师更关心你如何消除触摸抖动、如何做坐标校准,或者按键消抖是否用了定时器扫描,而不是只看能不能画棋。

输入方案优点缺点课设工作量
独立按键电路简单、无需校准交互效率低、需防抖较小
电阻触摸直接点选、体验完整需要 ADC/SPI 驱动、校准麻烦较大
电容触摸无物理磨损需专用芯片、驱动复杂不建议

3. 双人五子棋的核心逻辑:棋盘状态、落子判定与胜负检测

3.1 用二维数组管理棋盘状态

五子棋的逻辑核心是 15×15 或 19×19 的棋盘,STM32F103 的 RAM 只有 20 KB 左右,直接开一个int board[19][19]会浪费空间,更合理的做法是用uint8_t board[15][15],每个格子只存三个值:0 表示空,1 表示黑子,2 表示白子。这样整个棋盘状态只占 225 字节。

#define BOARD_SIZE 15 uint8_t board[BOARD_SIZE][BOARD_SIZE]; void Board_Init(void) { for (uint8_t i = 0; i < BOARD_SIZE; i++) { for (uint8_t j = 0; j < BOARD_SIZE; j++) { board[i][j] = 0; } } }

棋盘索引board[i][j]中的i对应屏幕上的 Y 坐标,j对应 X 坐标。落子时先检查该位置是否为 0,再写入当前玩家的颜色值,同时把屏幕坐标换算成棋盘坐标。这个二维数组是整个程序的状态中枢,所有绘制、胜负判断都围绕它进行。注意清理对局时一定要把整个数组清零,不能只清可见区,否则下一局会出现“幽灵棋子”。

3.2 胜负检测的四方向滑窗算法

五子棋胜负判断最容易写错的是“只检查当前落点”,因为三连、四连方向可能跨越多个格子。正确做法是以刚落的棋子为中心,分别扫描水平、垂直、左上到右下、右上到左下四个方向,只要同色连续棋子数大于等于 5 就判胜。用简单的循环即可,没必要用递归。

uint8_t Check_Win(uint8_t row, uint8_t col, uint8_t player) { int8_t dir_row[4] = {0, 1, 1, 1}; int8_t dir_col[4] = {1, 0, 1, -1}; for (uint8_t d = 0; d < 4; d++) { uint8_t count = 1; int8_t nr, nc; // 正方向:从落点向 dir 方向延伸 for (uint8_t step = 1; step < 5; step++) { nr = row + dir_row[d] * step; nc = col + dir_col[d] * step; if (nr >= 0 && nr < BOARD_SIZE && nc >= 0 && nc < BOARD_SIZE && board[nr][nc] == player) { count++; } else { break; } } // 反方向:从落点向相反方向延伸 for (uint8_t step = 1; step < 5; step++) { nr = row - dir_row[d] * step; nc = col - dir_col[d] * step; if (nr >= 0 && nr < BOARD_SIZE && nc >= 0 && nc < BOARD_SIZE && board[nr][nc] == player) { count++; } else { break; } } if (count >= 5) { return 1; } } return 0; }

方向数组dir_rowdir_col分别定义了四个方向的单位向量:水平向右、垂直向下、右下、左下。正反向累加后,只要某个方向同色棋子数达到 5 就返回1。这里有两个容易踩的坑:一是int8_t做边界判断时,nr可能为负,必须同时判断nr >= 0;二是“冲四”和“活四”都算赢,不需要区分,但如果要做高级 AI 才需要细分。count >= 5而不是== 5,是因为五子棋可能出现五连以上,虽然规则上已经赢了。

3.3 LCD 绘制与局部刷新策略

直接全屏重绘棋盘会导致界面闪烁,尤其是 STM32 用 GPIO 并口驱动 TFT 时,16 位色每个像素要写两字节,全屏刷新一次要几十毫秒,手在触摸屏上移动时就会看到残影。常见做法是只在落子时画一个棋子,并保存背景区域,悔棋时恢复该区域。

lcd.c 里通常封装了底层画点函数,上层绘制棋子时要用实心圆方式实现:

void LCD_DrawPiece(uint16_t x, uint16_t y, uint8_t color) { uint8_t radius = 6; // 棋子半径,单位:像素 // 按行列扫描小矩形区域,距离圆心小于半径的画为棋子色 for (int16_t dy = -radius; dy <= radius; dy++) { for (int16_t dx = -radius; dx <= radius; dx++) { if (dx * dx + dy * dy <= radius * radius) { uint16_t px = x + dx; uint16_t py = y + dy; // 超出屏幕边界跳过 if (px >= 0 && px < 240 && py >= 0 && py < 320) { LCD_DrawPoint(px, py, color); } } } } // 棋子上画一个小高光,增加立体感 LCD_DrawPoint(x - 2, y - 2, 0xFFFF); }

这个函数用简单几何公式生成实心圆,避免用浮点三角函数。color参数直接给 LCD 的 16 位 RGB 值,比如黑子0x0000,白子0xFFFF。实际绘制时还应先画棋盘格线和星位,然后根据board数组把棋子画到对应交点。局部刷新时,应该把交叉点的行列转换成屏幕坐标,公式是screen_x = margin + col * cell_sizemargin是棋盘边距,cell_size是交叉点间距,一般设为 13~15 像素。

4. 综合调试:从 “no STM32 target found” 到稳定运行

4.1 仿真器连接与固件下载失败处理

用 Keil 打开 TOUCH.uvguix.AA 工程后,第一件事不是改代码,而是先编译下载。很多人的开发环境第一次连接 STM32 就会报error: no STM32 target found!,这个报错在 ST-Link、J-Link 上都很常见,先排除接线问题再查软件设置。

按优先级检查以下项目:目标板供电是否正常,测量 VDD 引脚是否为 3.3 V;仿真器 SWDIO、SWCLK 是否与芯片正确连接,GND 是否共地;Keil Options 里的 Debug 页是否选择了正确的仿真器和接口,SWD 模式需要选择SW而不是JTAG;目标板复位电路是否正常,有些山寨 ST-Link 在 SWD 速度过快时会连接失败,把Max Clock从 4 MHz 降到 1 MHz 就能识别。

如果是首次烧写,还要确认 BOOT0 跳线接到 GND,BOOT1 任意。BOOT0 接高电平会让芯片进入 ISP 模式,此时仿真器连接会被中断,导致目标查找失败。烧录成功后换成 BOOT0 低电平复位即可运行。

4.2 LCD 花屏与闪烁优化

LCD 花屏几乎都是初始化序列错误或时序不匹配。lcd.c 里的初始化函数包含一段寄存器配置表,不同型号的 LCD 控制器指令不一致。如果你收到的是 ST7789、ILI9341 这类常见控制器,初始化后应能显示纯色背景。花屏时先测单色填充函数,比如全屏红色,如果颜色不对,说明 RGB 顺序反了,需要交换0x00F8000x001F这类颜色宏的字节顺序。

闪烁问题的根源是数据带宽不足。当 STM32 工作在 72 MHz 并把 GPIO 配置为 50 MHz 输出时,并口 LCD 的理论帧率仍然有限。优化手段有三个:减少无效绘制,只在变化区域更新;使用 DMA 传输像素数据,避免 CPU 逐点写;把 LCD 数据引脚放在同一条 GPIO 口的连续 8 位或 16 位上,比如低 8 位用GPIOB->ODR = data一次写入,而不是逐位翻转引脚。如果工程还在用LCD_DrawPoint画整幅棋盘,我一般会在LCD_DrawLine里改用 while 循环连续写,开编译优化-O2也能明显改善。

4.3 触摸误触与按键抖动处理

电阻触摸屏用 ADC 采样坐标时,按下瞬间电压不稳定,很容易出现一个点被识别成相邻两个点。最简单的方法是做“延时去抖”:检测到触摸后延时 10 ms 再读 ADC,连续三次读数偏差小于阈值才认为是有效落子。按键也有类似问题,机械按键的抖动时间约为 5~10 ms,可以在定时器中断里每 5 ms 扫描一次按键,连续两次读到相同电平才确认按下。

uint8_t KEY_Scan_Counters[3] = {0, 0, 0}; uint8_t KEY_Stable_Level[3] = {1, 1, 1}; // 初始为上拉电平 void KEY_Scan_IT(void) { for (uint8_t i = 0; i < 3; i++) { uint8_t level = GPIO_ReadInputDataBit(GPIOE, KeyPins[i]); if (level == KEY_Stable_Level[i]) { KEY_Scan_Counters[i] = 0; // 电平稳定,清计数器 } else { KEY_Scan_Counters[i]++; if (KEY_Scan_Counters[i] >= 2) { // 连续 2 次采样不同,确认变化 KEY_Stable_Level[i] = level; if (level == 0) { // 按下的瞬间才置位标志 KeyPressFlag[i] = 1; } } } } }

这段代码放在定时器中断里,每 5 ms 执行一次。KEY_Scan_Counters记录电平不稳定的次数,连续两次读到不同电平才更新稳定值,相当于 10 ms 消抖。主循环里只检查KeyPressFlag,处理完落子逻辑后清标志。中断驱动的消抖不会阻塞主循环,比delay(10)更优雅,也更容易在答辩时加分。

现象可能原因排查指令
无刷屏RCC 未开 GPIO 时钟检查RCC_APB2PeriphClockCmd
颜色反相RGB 顺序错误交换 LCD 颜色宏高低字节
按键误触无消抖加定时器扫描
触摸偏移未校准记录四个角坐标线性插值

5. 进阶技巧:把双人五子棋改成“单机对战”与状态机重构

5.1 用状态机管理菜单、对局、结束三个状态

双人五子棋课程设计最容易被问到的就是“代码结构怎么组织”。大部分同学的工程是一个 while 死循环,从初始化到落子判断到胜负弹窗全部顺序执行,一旦要加“悔棋”或“重新开始”就要到处改标志位。更合理的做法是用一个有限状态机把系统切分成MENUPLAYINGGAME_OVER三个状态。

typedef enum { STATE_MENU, STATE_PLAYING, STATE_GAME_OVER } GameState; GameState currentState = STATE_MENU; uint8_t currentPlayer = 1; // 1=黑, 2=白 void Game_Update(void) { switch (currentState) { case STATE_MENU: if (KeyPressFlag[0]) { // 按确认进入对局 Board_Init(); LCD_DrawBoard(); currentPlayer = 1; currentState = STATE_PLAYING; } break; case STATE_PLAYING: if (Touch_GetCoordinates(&x, &y)) { if (Board_SetPiece(x, y, currentPlayer)) { LCD_DrawPiece(x, y, currentPlayer == 1 ? BLACK : WHITE); if (Check_Win(row, col, currentPlayer)) { LCD_ShowString(60, 280, "Black Win!"); currentState = STATE_GAME_OVER; } else { currentPlayer = (currentPlayer == 1) ? 2 : 1; } } } break; case STATE_GAME_OVER: if (KeyPressFlag[1]) { // 按悔棋键重新开始 currentState = STATE_MENU; } break; } }

状态机的价值在于每个状态只处理自己的事件,PLAYING状态不会因为菜单按键而误操作,GAME_OVER后棋盘区域不再响应落子。如果需要悔棋,只需要在PLAYING状态记录每一步落子的坐标栈,弹出时清除棋盘数组并刷新该格即可。状态机本身不增加多少代码量,但答辩时老师看到 switch 分支和状态枚举,就认为你有软件工程意识。

5.2 扩展 AI 的思路与资源边界

如果要把双人五子棋升级成“人机对战”,最容易被忽略的是 STM32F103 的资源限制。标准 Alpha-Beta 剪枝算法需要评估函数和搜索树,在 72 MHz 下深度为 4、每层约 200 个候选点的搜索,耗时可能超过 1 秒,用户体验极差。课设改进方向有两种:一是限制 AI 只评估落子点附近的空位,比如只扫描已有棋子周围两步以内的空格,把候选点降到 20 个左右;二是用简单的“打分表”模拟棋型,比如成五得 10000 分、活四得 5000 分、冲四得 1000 分,遍历每个空位计算攻防得分并取最大值。

打分表思路更适合单片机,代码实现也更短。你可以给每个交叉点分别计算黑子和白子四个方向上的连子数,根据连续个数和两端是否被封堵给分,AI 选攻防综合最高的点落子。这个改动不需要引入递归和堆栈,运行时间在毫秒级。做毕设时如果选这个方向,可以在报告里写清楚“放弃全局搜索,采用局部棋型评分”的理由,比硬跑剪枝算法更契合嵌入式实时性要求。

5.3 答辩时如何讲清楚代码

拿到这个项目后,建议不要从main函数第一行开始讲,而是按“硬件初始化 → 时钟树 → 显示 → 输入 → 逻辑”的顺序展开。评审老师通常先看你跑起来的演示,再问三个问题:棋盘坐标怎么映射到 LCD 像素?连续五子怎么判断?按键/触摸抖动怎么处理?

对应到本项目的回答思路:坐标映射用row*cell_size + margin公式,触摸坐标则先做线性校准;胜负判断用四方向滑窗,方向数组体现对称性;抖动处理用定时器扫描或延时去重。把这三个点讲清楚,再展示一下状态机切换的代码片段,基本就能覆盖评分表里的“完成度”“创新性”“答辩表现”各项。最后提示一点:把编译输出的TOUCH.axf和 Keil 工程备份好,现场电脑如果缺少芯片包,直接打开工程可能编译失败,可以提前生成 hex 文件,用 ST-Link Utility 或 ISP 工具烧录演示,避免因为环境问题在答辩现场翻车。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/17 15:50:07

基于MATLAB的有限体积法对流换热数值求解与实现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/17 15:48:35

读OpenSpec 文档后,把 Claude Code 的 Key 换到 TaoToken

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/17 15:48:00

Versal ACAP上运行JupyterLab的底层原理与VD100 AI加速实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/17 15:46:36

STM32工程化开发:VS Code + CMake + GCC构建量产级嵌入式环境

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/17 15:46:05

GraphHopper 路线转向提示多语言翻译机制与本地化贡献指南

GraphHopper 路线转向提示多语言翻译机制与本地化贡献指南 【免费下载链接】graphhopper Open source routing engine for OpenStreetMap. Use it as Java library or standalone web server. 项目地址: https://gitcode.com/GitHub_Trending/gr/graphhopper 导读 Grap…

作者头像 李华