☰
STM32篮球计时记分器:LCD与矩阵按键的Proteus仿真与Keil实战
2026/10/29 17:33:18 网站建设 项目流程

简介:这份资源面向电子信息、自动化及嵌入式方向的课程设计学习者,提供一套基于STM32的篮球比赛计时记分器完整实现方案,采用HAL库开发,并配套Proteus仿真与Keil工程代码。设计围绕LCD液晶显示本节剩余时间与进攻剩余时间、两队比分,通过矩阵按键实现暂停、继续、复位及一分、两分、三分进球模拟,同时用蜂鸣器与发光二极管完成得分声光提示,适合作为单片机课程设计或嵌入式入门综合练习。压缩包共183个文件,约7.7MB,包含24个c源文件与53个h头文件构成主要逻辑,另有uvprojx、ioc等工程配置,hex、axf、map等编译产物,以及Proteus仿真相关文件,便于直接打开验证。目前已有2625人学习下载,读者可据此掌握矩阵按键扫描、定时器计时、LCD驱动与声光外设联动等关键环节,并对照仿真结果排查代码问题。

1. 从一块 LCD 和一堆矩阵按键说起:这套 STM32 篮球计时记分器到底能跑成什么样

很多人做嵌入式课程设计,卡的不是写代码,而是“我到底要做一个能演示、能答辩、还能讲清楚原理的东西”。这套基于 STM32 的 LCD 篮球计时记分器,恰好踩在这个需求上:Proteus 里能直接跑仿真,Keil 里能编译下载,外设用到 LCD 显示、矩阵按键扫描、定时器计时,逻辑上覆盖了篮球比赛需要的 24 秒进攻倒计时、比赛倒计时、双方比分加减和暂停复位。它适合正在做单片机课程设计、嵌入式实训,或者想找一个“外设组合完整但不至于失控”的 STM32 HAL 库练手项目的人。你拿到手后,最该先确认的不是代码写得多漂亮,而是仿真能不能跑、按键能不能响应、LCD 能不能按预期刷新——这三件事决定了这套资源对你有没有用。

2. 硬件链路怎么搭:LCD、矩阵按键和 STM32 的引脚分配逻辑

2.1 为什么选 LCD 而不是数码管,以及 Proteus 里的元件对应关系

篮球计时记分器最直观的输出就是比分和时间。用数码管也能做,但数码管只能显示数字,没法同时把“主队”“客队”“节次”“24 秒”这些信息分层展示出来。LCD 的优势在于一屏能塞下多组数据,而且 Proteus 里的 LCD 模型和真实 LCD1602、LCD12864 的驱动逻辑基本一致,仿真通过之后,换成实物屏通常只需要改引脚定义和初始化时序。

在 Proteus 里,常见做法是搜LM016L或LCD1602,前者是仿真模型名,后者是实物型号。如果你用的是 LCD12864,Proteus 里对应OLED12864I2C或并口版本,但注意:I2C 版本的仿真模型和真实 I2C 屏在地址上可能不一致,这是后面避坑章节要展开的点。对于这套篮球计时记分器,我一般建议先用 LCD1602 把逻辑跑通,因为 1602 的驱动简单、仿真稳定,等计时和按键都对了,再换 12864 做界面美化。

引脚分配上,HAL 库的典型做法是:

功能STM32 引脚说明
LCD RSPA0寄存器选择,0 指令 1 数据
LCD RWPA1读写选择,仿真里通常接地
LCD ENPA2使能脉冲
LCD D4-D7PA4-PA74 位数据模式,省引脚
矩阵按键行PB0-PB3输出扫描行
矩阵按键列PB4-PB7输入读取列
蜂鸣器PC1324 秒到时提示

这个分配不是固定的,但你要保证矩阵按键的行列不要和 LCD 数据线冲突。Proteus 仿真里引脚冲突不会报错,只会表现为“按键没反应”或“LCD 花屏”,排查起来很费时间。

2.2 矩阵按键扫描:HAL 库下的行扫描法和状态机写法

矩阵按键是这套项目里最容易写崩的部分。原因很简单:很多人用HAL_GPIO_ReadPin直接在主循环里轮询,按下一次比分加了好几分。正确的做法是行扫描加状态机去抖。

行扫描的逻辑是:把四行依次拉低,读四列,如果某一列为低,说明该行该列的按键被按下。HAL 库下可以这样写:

// 矩阵按键扫描,返回键值 0-15,无按键返回 0xFF uint8_t MatrixKey_Scan(void) { uint8_t row, col, key = 0xFF; for (row = 0; row < 4; row++) { // 当前行拉低,其余行拉高 HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0 << row, GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOB, 0x000F & ~(GPIO_PIN_0 << row), GPIO_PIN_SET); // 读四列 for (col = 0; col < 4; col++) { if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_4 << col) == GPIO_PIN_RESET) { key = row * 4 + col; // 等待按键释放,避免连加 while (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_4 << col) == GPIO_PIN_RESET); return key; } } } return key; }

这段代码的关键参数是GPIO_PIN_0 << row和GPIO_PIN_4 << col,它假设行接 PB0-PB3,列接 PB4-PB7。如果你实际接线是反的,把这两个移位基址换一下就行。while等待释放是简易去抖,实际项目里更稳的做法是加 10ms 延时后再读一次,或者用定时器中断做状态机。仿真里while等待释放通常没问题,但实物上如果按键接触不良,可能会卡死,所以我在实物上会改成带超时的释放检测。

键值映射到功能时,建议单独写一个Key_Handle函数,不要在扫描函数里直接改比分。这样后面改按键布局时不用动扫描逻辑。

3. 计时与记分逻辑怎么落地:24 秒、比赛倒计时和比分更新的代码结构

3.1 用 TIM 定时器做 1 秒基准,而不是 HAL_Delay

篮球计时记分器最核心的状态就是时间。24 秒进攻倒计时和比赛倒计时都需要一个稳定的 1 秒基准。很多人图省事,在主循环里用HAL_Delay(1000)然后减一,结果按键响应变得极慢,因为HAL_Delay是阻塞的。

正确做法是用一个基本定时器,比如 TIM6,配置成 1ms 中断,在中断里累加到 1000 次就是 1 秒:

// TIM6 中断回调,1ms 一次 void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { static uint16_t ms_cnt = 0; if (htim->Instance == TIM6) { ms_cnt++; if (ms_cnt >= 1000) { ms_cnt = 0; if (game_running) { if (match_second > 0) match_second--; if (shot_clock > 0) shot_clock--; if (shot_clock == 0) { // 24 秒到时,蜂鸣器提示 HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET); } } } } }

这里game_running是全局标志,暂停时置 0,计时就停。match_second和shot_clock分别存比赛剩余秒数和 24 秒剩余秒数。注意shot_clock在篮球规则里每次进攻重置为 24,但如果你做的是简化版,可以只在进球后重置。

TIM6 的配置在 CubeMX 里选Activate Clock Source,预分频器设成 71(假设 72MHz 主频),计数周期设成 999,这样就是 1ms 中断一次。这个参数不能错,错了时间会快或慢,答辩时老师一眼就能看出来。

3.2 比分更新和 LCD 刷新:避免整屏重绘导致闪烁

比分更新本身很简单,主队加分就是score_home++,客队加分就是score_away++。但 LCD 刷新有个坑:如果你每次按键都整屏重写,LCD 会闪,仿真里看起来像花屏。

我一般会做局部刷新。LCD1602 只有两行,第一行显示HOME:XX AWAY:XX,第二行显示TIME:XX SHOT:XX。比分变化时只更新比分对应的几个字符位置,时间变化时只更新时间位置。HAL 库下没有现成的局部刷新函数,需要自己封装一个LCD_ShowNum,指定起始坐标和数字位数:

// 在 LCD 指定位置显示固定位数的数字 void LCD_ShowNum(uint8_t x, uint8_t y, uint16_t num, uint8_t len) { uint8_t i; for (i = 0; i < len; i++) { LCD_SetCursor(x + len - 1 - i, y); LCD_WriteData(num % 10 + '0'); num /= 10; } }

x和y是 LCD 坐标,len是显示位数。比如比分最大 99,就传 2。这样每次只写两个字符,不会整屏闪。注意LCD_SetCursor和LCD_WriteData需要你先实现好 4 位数据模式的底层写函数,这部分在正点原子或野火的 LCD1602 例程里都有,直接移植过来改引脚即可。

3.3 按键功能映射:加一、减一、暂停、复位怎么分配

矩阵按键有 16 个键,但篮球计时记分器不需要那么多。我一般这样分配:

键值功能说明
0主队 +1主队得分加一
1主队 +2三分线内
2主队 +3三分线外
3客队 +1客队得分加一
4客队 +2客队两分
5客队 +3客队三分
6开始/暂停切换 game_running
724 秒复位shot_clock = 24
8比赛复位所有比分和时间归零

这个映射不是固定的,但你要保证“开始/暂停”和“复位”不要放在相邻键上,否则手一抖就复位了。仿真里按键是鼠标点的,实物上是手指按的,误触概率不一样。

4. Proteus 仿真和 Keil 联调:从编译到跑通的完整操作链

4.1 Keil 工程配置:HAL 库、时钟和下载选项

拿到代码包后,第一件事不是直接编译,而是确认 Keil 工程里的器件型号和你的 Proteus 仿真模型一致。常见组合是 STM32F103C8T6,Keil 里选STM32F103C8,Proteus 里选STM32F103C8或STM32F103R6。如果型号不一致,仿真可能跑不起来,或者外设行为异常。

HAL 库的工程需要包含这些文件:stm32f1xx_hal.c、stm32f1xx_hal_gpio.c、stm32f1xx_hal_tim.c、stm32f1xx_hal_rcc.c,以及对应的_ex.c回调文件。如果你用 CubeMX 生成,这些会自动包含。如果拿到的是手动搭建的工程,检查main.c里有没有HAL_Init()、SystemClock_Config()和MX_GPIO_Init()、MX_TIM6_Init()。

时钟配置是关键。STM32F103 外部晶振通常 8MHz,经过 PLL 倍频到 72MHz。Proteus 仿真里如果晶振频率设错,定时器时间会不对。在 Proteus 的 STM32 属性里,把Crystal Frequency设成 8MHz,和代码里的HSE_VALUE保持一致。

下载选项里,Keil 的Debug页要选Use Simulator还是ST-Link?如果你只在 Proteus 里跑,Keil 只负责编译出 hex 文件,然后在 Proteus 里加载 hex。所以 Keil 的 Debug 页可以不管,但Output页要勾选Create HEX File。

4.2 Proteus 加载 hex 和仿真运行:几个必须检查的连线点

Proteus 里双击 STM32 模型,在Program File里选 Keil 生成的 hex 文件。然后检查这几个连线:

  • LCD 的 RS、RW、EN、D4-D7 是否和代码里的引脚定义一致
  • 矩阵按键的行列是否接在 PB0-PB7
  • 蜂鸣器是否接在 PC13,并且有驱动电路(仿真里可以直接接,实物上要加三极管)
  • 电源和地是否都接了,Proteus 里不接电源仿真也能跑,但实物上不行

运行仿真后,如果 LCD 亮但没字符,先调对比度。Proteus 的 LCD1602 模型有一个VEE引脚,接一个电位器或者直接接地,对比度不对就看不到字符。如果 LCD 完全不亮,检查VDD和VSS有没有接。

矩阵按键在仿真里是鼠标点击,点一下相当于按下并释放。如果你代码里的while等待释放写得太死,仿真里可能会卡住,因为鼠标点击的释放时机和真实按键不一样。遇到这种情况,把等待释放改成带超时的循环,或者直接用定时器扫描。

4.3 联调顺序:先跑计时,再加按键,最后调显示

我建议的联调顺序是:

  1. 先让 LCD 显示固定字符,确认底层驱动没问题
  2. 再加 TIM6 中断,让一个变量每秒减一,并在 LCD 上显示出来
  3. 然后加矩阵按键扫描,按键改变变量值
  4. 最后把比分、24 秒、比赛时间全部整合进去

这个顺序的好处是,每一步只引入一个新变量。如果一上来就把所有功能打开,出了问题你根本不知道是 LCD 驱动、定时器配置还是按键扫描的锅。仿真里尤其如此,Proteus 的调试信息很少,只能靠二分法定位。

5. 避坑与排查:这套仿真代码最容易翻车的五个地方

5.1 现象:LCD 显示乱码或只显示一排方块

原因:4 位数据模式的初始化时序不对,或者LCD_WriteCmd和LCD_WriteData的 RS 电平搞反了。Proteus 的 LCD 模型对时序比实物更敏感。 解决:检查初始化里有没有先发0x33、0x32切到 4 位模式,再发0x28设置两行显示。RS 在写指令时置 0,写数据时置 1。如果还是乱码,把 EN 脉冲宽度加大到 10us 以上。

5.2 现象:按键按一次,比分加了好几分

原因:没有去抖,或者while等待释放的条件写错了。仿真里鼠标点击的持续时间比真实按键长,更容易连加。 解决:在扫描函数里加 10ms 延时后再读一次,或者用状态机记录按键的“按下”和“释放”两个状态。不要在主循环里直接调用扫描函数并立即改比分,中间加一层按键事件处理。

5.3 现象:24 秒倒计时不走,或者走得特别快

原因:TIM6 的预分频器和计数周期设错,或者HAL_TIM_Base_Start_IT没有调用。Proteus 仿真里如果晶振频率和代码不一致,时间也会偏。 解决:确认SystemClock_Config里 PLL 倍频到 72MHz,TIM6 预分频 71,周期 999。在main里调用HAL_TIM_Base_Start_IT(&htim6)。如果还不对,在中断里翻转一个 GPIO,用示波器看实际中断频率。

5.4 现象:Proteus 仿真跑一会儿就卡死

原因:while等待按键释放没有超时,或者 LCD 忙检测死循环。Proteus 的仿真速度比实物慢,死循环会直接卡住整个仿真。 解决:所有while循环都加超时计数,超过一定次数就强制退出。LCD 忙检测如果读不到忙标志,直接改用延时替代,仿真里延时比忙检测更稳。

5.5 现象:Keil 编译报错undefined symbol HAL_TIM_...

原因:HAL 库的.c文件没有全部加入工程,或者stm32f1xx_hal_conf.h里对应的模块被注释掉了。 解决:检查工程里有没有stm32f1xx_hal_tim.c和stm32f1xx_hal_tim_ex.c,并在stm32f1xx_hal_conf.h里把HAL_TIM_MODULE_ENABLED打开。如果用的是 CubeMX 生成的工程,重新生成一次并选择Copy only necessary library files。

6. 进阶技巧:把仿真代码往实物迁移时,我会先改这三个地方

仿真跑通只是第一步,真正答辩或者交作业时,老师可能会要求实物演示。从 Proteus 往实物迁移,最容易出问题的不是代码逻辑,而是电气特性。我一般会先改这三个地方。

第一,矩阵按键加上拉电阻。Proteus 里可以直接读低电平,实物上如果列线没有上拉,读到的电平是浮空的。STM32 内部有上拉,可以在 CubeMX 里把列线配置成Pull-up,但如果你用的是外部上拉,阻值选 4.7k 到 10k 之间。

第二,LCD 的背光和对比度。实物 LCD1602 的VEE引脚需要接电位器,背光A和K要接限流电阻。仿真里这些都可以省略,实物上省略了要么不亮,要么烧背光。

第三,蜂鸣器驱动。STM32 的 GPIO 输出电流有限,直接接蜂鸣器声音很小,甚至驱动不起来。常见做法是用一个 NPN 三极管,基极串 1k 电阻接 PC13,集电极接蜂鸣器,发射极接地。仿真里可以直接接,实物上必须加。

还有一个习惯:每次改完引脚定义,我都会在main.h里用宏定义统一管理,而不是在代码里到处写GPIO_PIN_0。这样从仿真换到实物时,只改宏定义就行,不用满工程搜引脚号。从那以后我每次做 STM32 课程设计,都强制走一遍“仿真验证逻辑、宏定义隔离引脚、实物只改宏”的流程,省下来的调试时间比写代码的时间还多。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询