news 2026/9/12 15:24:38

STM32裸机五子棋:从寄存器到图形交互的完整嵌入式闭环

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32裸机五子棋:从寄存器到图形交互的完整嵌入式闭环

简介:本资源是一套面向嵌入式初学者与高校课程设计者的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.cstm32f10x_tim.cstm32f10x_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.cLCD_Init()函数中,关键步骤如下:

// 1. 配置 GPIO:PB0–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); // 设置 MADCTL:RGB/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(i=0; i<120000; 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_SIZE=15时,第 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_PLAYER2
GAME_WAIT_PLAYER2检测到 KEY2 按下且当前为空位在坐标处绘制白子,切换至GAME_WAIT_PLAYER1
GAME_OVER胜负判定返回 true显示胜者信息,进入 5 秒倒计时,后自动回GAME_IDLE

状态机核心代码位于main.cGame_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 层(CheckWinfor循环 → 内层for),实测编译后函数栈开销为 0 字节(全部优化进寄存器)。Keil uVision5 在Optimize Level 3下对此类循环展开效果极佳。

3.3 按键事件与坐标映射:基于 ADC 采样的模拟按键矩阵(可选扩展)

当前工程使用两个独立按键(KEY1/KEY2)分别代表玩家一、玩家二。但项目预留了ADC1通道支持模拟摇杆输入——通过读取PA0(ADC_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

提示:该脚本删除ObjectsListings目录,强制 Keil 重新编译全部源文件,解决因头文件修改未触发依赖更新导致的“修改不生效”问题。

4.2 必须修改的 3 个工程配置项(否则无法编译通过)

打开 Keil uVision5,右键工程名 →Options for Target,需手动校验以下三项:

配置页签选项名称正确值错误后果
TargetDeviceSTM32F103C8若选错型号(如 F103CB),Flash 算法不匹配,下载失败
OutputName of ExecutableTOUCH.axf若改名,keilkilll.bat中的del *.axf仍可工作,但调试时需同步更新
C/C++DefineUSE_STDPERIPH_DRIVER缺失则stm32f10x.h不启用标准库,编译报RCC_DeInit未定义

此外,C/C++ → Include Paths中必须包含:

.\CMSIS\Include .\STM32F10x_StdPeriph_Driver\inc .\User .\LCD

4.3 典型下载失败场景与串口日志分析法

当点击Load按钮后 Keil 报错No Debugging Interface FoundFlash Download failed — Could not load file,请按以下顺序排查:

4.3.1 检查 SWD 接口物理连接
  • 使用万用表测量SWDIO(PA13)、SWCLK(PA14)对地电压:正常应为 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→ 搜索STM32F1
ARM.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_playergame_state自动切换,手机端仅负责发送坐标,不参与玩家身份管理。

提示:HC-05 默认波特率 9600,需在USART1_Config()中将USART_InitStruct->USART_BaudRate设为9600,并关闭USART_HardwareFlowControl

5.2 棋局记录持久化:利用 STM32 内置 Flash 模拟 EEPROM

stm32f10x_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)连接至PA8(TIM1_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_cycle(0–999),实现无级调光。此功能可绑定长按 KEY1 触发,提升用户体验。


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

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

CentOS7离线部署Python3深度学习环境实战

简介&#xff1a;本资源是一套面向人工智能初学者与进阶学习者的系统性课程资料包&#xff0c;覆盖机器学习基础、深度学习实践及环境配置等核心环节&#xff0c;适用于高校学生、转行从业者及自学爱好者夯实理论与动手能力。压缩包共105个文件&#xff0c;包含27个Jupyter Not…

作者头像 李华
网站建设 2026/9/12 15:24:24

CCKS2017电子病历NER实战:Python实现BIO标注与BiLSTM-CRF

简介&#xff1a;本资源是面向自然语言处理初学者与进阶研究者的CCKS2017中文电子病历命名实体识别完整实践项目&#xff0c;聚焦医疗文本中“一般情况”“出院情况”“病史特点”等关键实体的序列标注任务。项目基于字向量四层双向LSTMCRF架构实现&#xff0c;提供原始数据集&…

作者头像 李华
网站建设 2026/9/12 15:23:12

研究问题被说太宽泛?聚焦的4步清单

被导师或审稿人一句「问题太宽泛」退回&#xff0c;多数人先想到的是往句子里补修饰词&#xff0c;改完仍被判宽泛。标题里所写的4步&#xff0c;在正文中一次展开为四种情形&#xff1a;先判断这次宽泛属于哪种性质、已经牵连到多少下游&#xff0c;再决定该动哪一处。这份清单…

作者头像 李华
网站建设 2026/9/12 15:19:57

Midscene.js 完整指南:如何用视觉AI写出跨平台UI测试的5分钟教程

Midscene.js 完整指南&#xff1a;如何用视觉AI写出跨平台UI测试的5分钟教程 【免费下载链接】midscene GUI Agent for E2E Testing 项目地址: https://gitcode.com/GitHub_Trending/mid/midscene 昨天还能点中的按钮&#xff0c;今天前端一重构就全跑偏了。Midscene.js…

作者头像 李华
网站建设 2026/9/12 15:19:44

AI时代程序员角色分化与技能升级指南

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

作者头像 李华