简介这份资源是一套基于STM32的秒表程序工程面向正在学习STM32定时器、按键与LCD显示的单片机初学者及进阶开发者可用于课程实验、自学练手或作为二次开发的基础模板。程序实现0.01秒精度的计时、暂停与清零功能keyup负责开始与继续key1负责暂停key0负责清零整体思路清晰换用其他厂家开发板只需少量修改即可运行。压缩包共140个文件约2.69MB以34个c源文件与36个h头文件为核心配合Keil工程文件、编译中间产物及hex烧录文件覆盖定时器、LCD、RCC等外设驱动模块目录结构完整可直接打开工程编译验证。目前已有2389人学习下载。作者几乎为每个函数都添加了注释读者可据此理解计时逻辑与按键消抖处理并在此基础上扩展硬件功能、优化显示效果适合作为嵌入式入门的参考工程。1. 一块精英板上的秒表为什么值得单独拆一遍手头这块 STM32 精英板在抽屉里躺了大半年直到有人问「能不能做个能暂停、能计次、精度到 10ms 的秒表」我才把它翻出来。秒表看着简单真写起来全是细节定时器怎么配、按键怎么消抖、数码管还是 LCD 怎么刷、暂停后时间怎么保持。这份「精英板秒表」资源就是把这些细节一次性落地的完整工程基于 STM32F103 系列主控配套正点原子、野火两套常见开发板的引脚习惯适合刚学完 GPIO 和定时器、想找一个能跑通又能改的中等体量项目练手的开发者。它不追求花哨界面胜在逻辑闭环完整——计时、暂停、复位、计次四个动作全走通代码结构清晰到可以当模板抄。2. 工程结构与技术选型从时钟树到显示刷新的一条链2.1 目录怎么分层为什么这么分拿到一个 STM32 工程我第一件事不是看 main而是看目录。这份秒表工程按「硬件抽象—业务逻辑—应用入口」三层铺开典型结构如下Stopwatch/ ├── Core/ │ ├── Inc/ // 头文件stopwatch.h、key.h、display.h │ └── Src/ │ ├── main.c // 初始化 主循环调度 │ ├── stopwatch.c // 计时核心状态机 │ ├── key.c // 按键扫描与消抖 │ └── display.c // 显示驱动与刷新 ├── Drivers/ │ ├── CMSIS/ // 内核头文件 │ └── STM32F1xx_HAL_Driver/ // HAL 库 ├── MDK-ARM/ // Keil 工程文件 └── Stopwatch.ioc // CubeMX 配置这种分法的好处是改显示不动计时、换按键不动主循环。很多新手把所有代码堆在 main.c 里改一个按键逻辑要翻三百行血泪经验就是别省这一步。业务层只暴露Stopwatch_Start/Stop/Reset/Lap四个接口底层怎么实现上层不关心后面换 OLED 屏只改 display.c 一个文件。2.2 定时器选型为什么用 TIM 做时基而不是 SysTick秒表的核心是一个稳定时基。SysTick 被 HAL 库的HAL_Delay占着拿来做 10ms 计时会和延时函数打架常见做法是单独开一个通用定时器 TIM3 做 10ms 中断。STM32F103 主频 72MHz预分频和重装载值这样算// TIM3 配置为 10ms 中断一次 // 72MHz / (71991) 10kHz 计数频率 // 10kHz 下计 100 个数 10ms htim3.Init.Prescaler 7199; htim3.Init.Period 99; HAL_TIM_Base_Start_IT(htim3);Prescaler决定计数频率Period决定溢出周期两者相乘再除以主频就是中断间隔。这里 7200×100/72M 10ms正好是秒表最小显示单位。中断里只做一件事给一个 volatile 的毫秒计数器加 10绝不在中断里刷屏或做浮点运算否则主循环会被拖慢。2.3 计时状态机暂停和计次怎么不打架秒表最容易翻车的地方是状态管理。运行中按暂停、暂停中按计次、复位后计次清不清这些组合不画清楚就会出玄学 bug。工程里用一个枚举加一个结构体把状态收拢typedef enum { SW_IDLE, // 复位态显示 00:00.00 SW_RUNNING, // 计时中 SW_PAUSED // 暂停时间冻结 } SW_State; typedef struct { SW_State state; uint32_t elapsed_ms; // 累计毫秒 uint32_t lap_ms[8]; // 最多存 8 次计次 uint8_t lap_count; } Stopwatch;主循环根据state决定按键响应RUNNING 下只认暂停和计次PAUSED 下只认继续和复位。elapsed_ms在定时器中断里累加暂停时状态切走、中断里判断状态再决定加不加这样暂停后时间自然冻结不需要额外保存快照。计次就是把当前elapsed_ms塞进数组超过 8 次覆盖最早一条够用且不占内存。3. 核心模块拆解按键消抖、显示刷新与主循环调度3.1 按键扫描20ms 消抖为什么比延时靠谱机械按键抖动是秒表第一个坑。按下瞬间电平会抖 520ms直接读会一次按下触发好几次。工程里用「定时扫描 状态确认」而不是HAL_Delay(20)死等// 每 10ms 调用一次连续两次读到相同电平才认 uint8_t Key_Scan(void) { static uint8_t last 0xFF; uint8_t now 0; if (HAL_GPIO_ReadPin(KEY_PORT, KEY_PIN) GPIO_PIN_RESET) now KEY_PRESSED; if (now ! last) { // 电平变化先记下不响应 last now; return KEY_NONE; } if (now KEY_PRESSED last KEY_PRESSED) { last 0xFF; // 消费掉这次按下防连发 return KEY_PRESSED; } return KEY_NONE; }逻辑是「变化不响应、稳定才响应、响应后清状态」。last记录上次电平第一次读到按下只更新不返回第二次仍按下才确认天然滤掉抖动。放在 10ms 定时器里调用等于自带 10ms 采样周期比在主循环里HAL_Delay阻塞优雅得多。参数上KEY_PORT/KEY_PIN按板子实际接线改正点原子精英板一般是 PE3/PE4 这类野火板引脚不同改宏定义即可。3.2 显示刷新只刷变化的位别整屏重画显示部分用 4 位数码管或 1602 LCD 都行工程默认走数码管动态扫描。动态扫描的坑是刷新频率太低会闪太高占 CPU。常见做法是 1ms 切一位、4 位一轮 4ms人眼完全看不出闪烁。核心是「时间变了才更新缓冲区」void Display_Update(uint32_t ms) { static uint32_t last_ms 0xFFFFFFFF; if (ms last_ms) return; // 没变不刷省 CPU last_ms ms; uint8_t min ms / 60000; uint8_t sec (ms / 1000) % 60; uint8_t cs (ms / 10) % 100; // 百分秒 Seg_SetDigit(0, min / 10); Seg_SetDigit(1, min % 10); Seg_SetDigit(2, sec / 10); Seg_SetDigit(3, sec % 10); // 百分秒另起一组或复用按实际位数调整 }ms / 60000取分钟、% 60取秒、/ 10 % 100取百分秒这套换算记住就不会错。last_ms做脏检查时间没动就不重画主循环空转时 CPU 占用能降一大截。如果换 OLED把Seg_SetDigit换成字库绘制函数接口不变。3.3 主循环调度一个 while 里怎么排优先级主循环不写业务只做调度。顺序很讲究先扫按键响应要快再刷显示视觉要稳最后处理计次这类低频动作while (1) { uint8_t key Key_Scan(); if (key KEY_PRESSED) { switch (sw.state) { case SW_IDLE: Stopwatch_Start(sw); break; case SW_RUNNING: Stopwatch_Pause(sw); break; case SW_PAUSED: Stopwatch_Resume(sw); break; } } if (Key_LongPress()) Stopwatch_Reset(sw); // 长按复位 Display_Update(sw.elapsed_ms); HAL_Delay(1); // 让出一点时间避免空转占满 }按键处理走状态机分支长按复位单独判断避免短按误触清零。HAL_Delay(1)不是必须但给调度留个节拍配合 10ms 定时器中断整体节奏就稳了。这里的关键认知是中断负责「记时间」主循环负责「读按键、刷显示」两边通过volatile变量通信职责不混。4. 跑起来与改起来编译烧录、参数调整与常见问题排查4.1 从零编译烧录的完整步骤工程用 Keil MDK 打开MDK-ARM/Stopwatch.uvprojx首次编译前确认三件事器件型号选对STM32F103 对应具体容量、调试器选 ST-Link 或 J-Link、时钟配置和板子晶振一致精英板一般 8MHz 外部晶振。编译通过后直接下载复位即可看到数码管从 00:00.00 开始。如果用的是 CubeMX 重新生成注意别覆盖自己写的stopwatch.c生成设置里把用户代码放在USER CODE BEGIN/END之间。4.2 想改精度、改显示、加功能动哪里改精度把 TIM3 的Period从 99 改成 9中断变 1mselapsed_ms每次加 1显示就能到毫秒级但要注意数码管位数够不够。改显示只动display.c把Seg_SetDigit换成 LCD 或 OLED 的绘制接口Display_Update的换算逻辑原样保留。加计次回看lap_ms数组已经存了历史加一个按键在 PAUSED 态循环翻页显示即可不用改计时核心。这三类改动互不牵连正是分层带来的好处。4.3 避坑清单五个真实踩过的坑现象一时间走得比实际快或慢。原因多半是外部晶振没起振HAL 回退到内部 RC主频不准。解决检查SystemClock_Config里时钟源用示波器或 MCO 引脚输出时钟确认 72MHz。现象二按一次键触发两次。消抖逻辑写成了「读到按下就返回」没做二次确认。解决按 3.1 的两级确认改确保响应后清last状态。现象三暂停后时间还在涨。中断里无条件累加elapsed_ms没判断状态。解决中断里加if (sw.state SW_RUNNING)再累加。现象四数码管某一位常亮或全暗。位选和段选引脚定义和实际接线对不上或动态扫描频率太低导致视觉残留。解决对照原理图核对引脚宏扫描周期控制在 4ms 一轮。现象五下载后没反应。多半是启动文件或中断向量表没匹配容量或 BOOT 引脚状态不对。解决确认器件型号、检查 BOOT0 接地、重新全片擦除再下载。5. 二次开发把秒表改成可复用的计时组件这份工程最大的价值不是秒表本身而是那套「定时器时基 状态机 分层显示」的骨架。我一般会把它抽成一个独立组件stopwatch.c/h只依赖一个「获取当前毫秒」的回调不直接绑 TIM3这样换到任何有系统滴答的平台上都能用。改造时把elapsed_ms的累加从定时器中断挪到Stopwatch_Tick(uint32_t delta_ms)接口里主循环或 RTOS 任务周期性调用计时核心就和硬件解耦了。验证方法很直接写一个测试函数手动喂Stopwatch_Tick(10)一百次断言elapsed_ms 1000再模拟 Start→Pause→Resume 序列检查暂停期间 tick 不累加。这种纯逻辑测试不需要上板在 PC 上编译跑一遍就能挡住大部分状态机 bug。参数上建议把最大计次从 8 提到 16 或做成环形缓冲lap_ms用uint32_t存毫秒足够跑 49 天不溢出。改造点原实现建议改法影响范围时基来源绑死 TIM3 中断回调或 Tick 接口注入stopwatch.c显示介质数码管动态扫描抽象 Draw 接口display.c计次容量固定 8 条环形缓冲可配结构体 显示精度10ms1ms改 Period定时器配置从那以后我每次接计时类需求都强制先把状态机枚举和 Tick 接口定下来再写一行显示代码返工率明显下降。这套骨架你拿去改个倒计时、跑表、甚至简单的运动计时都不费劲希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?