STM32秒表工程实战:定时器时基、按键消抖与显示刷新全解析
简介这是一份基于STM32的秒表程序源码面向正在学习单片机定时器、中断与按键处理的嵌入式初学者及进阶开发者可直接在正点原子精英板等开发板上运行。程序实现0.01秒精度的计时、暂停与清零功能keyup负责启动与继续key1控制暂停key0完成清零逻辑清晰其他厂商开发板稍作修改即可移植。压缩包共140个文件约2.69MB包含34个c源文件、36个h头文件以及o、d、crf等编译中间文件另有sct、uvprojx、hex、map等工程与烧录相关文件覆盖STM32标准外设库的定时器、LCD、Flash、RCC等模块。目前已有2388人学习下载。作者几乎为每个函数添加了注释读者可借此理解秒表计时框架、按键扫描与显示刷新流程并在此基础上扩展硬件功能或优化程序结构适合作为STM32入门练手与课程设计的参考。1. 从一块吃灰的精英板说起为什么我要拆这个秒表工程手头有几块 STM32 开发板平时跑例程、点灯、调串口玩完就扔在抽屉里。直到有个做实训的朋友问我能不能用最熟悉的板子做一个能计时的秒表要求按键启停、数码管或 LCD 显示、精度到 10ms最好还能记录分段成绩。我第一反应是这需求太朴素了但翻了一圈网上的资料要么是纯寄存器版本看得头疼要么是 HAL 库工程缺文件跑不起来。后来拿到这份「精英板秒表成功」的压缩包解压后是一个完整的 STM32 工程基于常见的精英板硬件用定时器做时基、按键做交互、显示模块做输出编译下载后直接能跑。它解决的不是什么高深算法问题而是把「定时器中断 按键消抖 显示刷新」这条最基础也最容易翻车的链路完整串了起来。适合刚上手 STM32、想找一个能跑通又能改的实战工程的人也适合拿它当模板去改造成计时器、频率计、PWM 测量之类的项目。2. 秒表工程的骨架定时器时基、按键输入与显示刷新怎么串拿到一个能跑的工程最忌讳上来就改代码。先花十分钟把它的运行逻辑理清楚后面改参数、加功能才不会把整个工程改崩。这个秒表工程的核心其实就三块一个定时器负责产生固定时间基准一组按键负责启停和复位一个显示模块负责把计数值刷到屏幕上。三块之间通过中断和标志位解耦主循环只做显示刷新和状态判断。2.1 定时器选型与 10ms 时基的配置逻辑秒表精度的下限由定时器中断周期决定。工程里用的是通用定时器配置成 10ms 中断一次也就是每秒钟进 100 次中断。为什么选 10ms 而不是 1ms因为 1ms 中断对显示刷新和按键扫描来说太频繁CPU 大部分时间都在进出中断主循环反而被拖慢10ms 既能保证百分秒位的显示需求又给主循环留出足够的执行时间。常见做法是用定时器的预分频器把系统时钟降到一个合适的计数频率再用自动重装载寄存器设定溢出周期。// 以 72MHz 系统时钟为例配置 10ms 定时中断 // 预分频 7200-1计数频率 72MHz / 7200 10kHz // 自动重装载 100-1溢出周期 100 / 10kHz 10ms TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_TimeBaseStructure.TIM_Prescaler 7200 - 1; TIM_TimeBaseStructure.TIM_Period 100 - 1; TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, TIM_TimeBaseStructure); TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); TIM_Cmd(TIM2, ENABLE);这段代码里TIM_Prescaler决定计数频率TIM_Period决定溢出周期两者配合才能得到想要的 10ms。很多人只改TIM_Period忘了改预分频结果中断周期差了几十倍秒表跑起来像快进。中断服务函数里只做一件事给一个全局变量累加并置位一个更新标志。volatile uint32_t g_ms_tick 0; volatile uint8_t g_update_flag 0; void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); g_ms_tick 10; // 每次中断代表 10ms g_update_flag 1; // 通知主循环刷新显示 } }g_ms_tick是毫秒累加器g_update_flag是给主循环的信号。中断里不做显示、不做按键判断只更新计数和标志这是保证计时准确的关键。如果中断里塞了太多逻辑中断执行时间超过 10ms计时就会丢步秒表越跑越慢。2.2 按键启停与消抖状态机比延时更靠谱秒表至少需要两个按键一个启停一个复位。工程里用的是独立按键接 GPIO按下为低电平。新手最容易踩的坑是在按键中断里直接delay_ms(20)消抖结果整个系统被阻塞显示卡顿、计时不准。更稳的做法是用状态机在主循环里轮询或者用定时器中断做周期扫描。// 按键状态机每 10ms 调用一次 typedef enum { KEY_IDLE, KEY_DEBOUNCE, KEY_PRESSED, KEY_RELEASE } key_state_t; key_state_t key_state KEY_IDLE; uint8_t key_press_flag 0; void key_scan(void) { switch (key_state) { case KEY_IDLE: if (KEY_READ() 0) key_state KEY_DEBOUNCE; break; case KEY_DEBOUNCE: if (KEY_READ() 0) { key_state KEY_PRESSED; key_press_flag 1; } else key_state KEY_IDLE; break; case KEY_PRESSED: if (KEY_READ() 1) key_state KEY_RELEASE; break; case KEY_RELEASE: key_state KEY_IDLE; break; } }这个状态机每 10ms 跑一次刚好和定时器中断周期对齐。KEY_DEBOUNCE状态相当于等了一个周期再确认等效于 10ms 消抖但不会阻塞任何东西。key_press_flag置位后由主循环消费消费完清零。这样按键响应和计时互不干扰启停操作不会影响g_ms_tick的累加。2.3 显示刷新别在中断里刷屏显示模块常见的是 LCD 或数码管不管哪种刷新都需要时间。如果放在定时器中断里刷新中断执行时间会被拉长计时精度直接崩掉。工程里的做法是主循环检测g_update_flag置位时才刷新一次显示。int main(void) { uint32_t last_tick 0; uint8_t running 0; timer_init(); key_init(); display_init(); while (1) { key_scan(); if (key_press_flag) { key_press_flag 0; running !running; // 启停切换 } if (running g_update_flag) { g_update_flag 0; display_show_time(g_ms_tick); // 只刷显示不碰计时 } } }主循环里running控制计时是否推进g_update_flag控制显示是否刷新。注意g_ms_tick在中断里累加主循环只读不写所以不需要加锁。如果要在主循环里复位计时直接写g_ms_tick 0即可但最好在running 0时操作避免和中断里的累加产生竞争。3. 把工程跑起来编译环境、下载配置与首次上电验证代码逻辑理清楚之后下一步是让它真的在板子上跑起来。这一步翻车最多不是编译报错就是下载后没反应。下面按顺序把环境配置、编译选项、下载设置和上电验证过一遍。3.1 编译环境搭建与工程文件结构确认这份工程是标准的外设库工程解压后能看到启动文件、外设驱动、用户代码和工程文件。常见做法是用 Keil MDK 或 IAR 打开如果用的是 GCC 工具链需要自己补 Makefile。打开工程前先确认三件事器件型号是否和手头板子一致、晶振频率是否匹配、下载器配置是否正确。检查项常见值不匹配的后果器件型号STM32F103 系列编译通过但下载后不运行外部晶振8MHz系统时钟不对定时器周期全错下载器ST-Link / J-Link无法识别目标芯片调试接口SWD占用引脚少推荐器件型号在工程选项的 Device 页签里选晶振频率在system_stm32f10x.c或时钟配置函数里改。如果板子用的是 12MHz 晶振而工程按 8MHz 配置系统时钟会变成 108MHz 而不是 72MHz定时器中断周期跟着变秒表走得飞快。3.2 下载配置与上电后第一眼该看什么下载配置里最关键的是调试接口选择和复位方式。SWD 模式只需要两根线比 JTAG 省引脚。下载前勾选「Reset and Run」这样烧录完自动运行不用手动按复位。# 如果用的是 OpenOCD GCC 工具链下载命令大致如下 openocd -f interface/stlink.cfg -f target/stm32f1x.cfg \ -c program build/stopwatch.elf verify reset exit这段命令做三件事连接下载器、烧录 ELF 文件、校验并复位运行。verify会回读芯片内容做比对防止烧录不完整。烧录成功后板子上电第一眼应该看到显示模块亮起显示00:00.00或类似格式。按下启停键数字开始跳动再按一次数字停住按复位键归零。如果上电后屏幕全黑先查背光引脚和供电再查初始化代码里显示模块的片选和复位时序。3.3 用示波器或逻辑分析仪验证时基精度显示能跑不代表计时准。要确认 10ms 时基是否准确最直接的办法是拿示波器或逻辑分析仪抓一个 GPIO 翻转信号。在定时器中断里加一行翻转代码测出实际中断周期。void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); GPIO_WriteBit(GPIOA, GPIO_Pin_0, \ (BitAction)(1 - GPIO_ReadOutputDataBit(GPIOA, GPIO_Pin_0))); g_ms_tick 10; g_update_flag 1; } }抓到的方波周期如果是 10ms说明时基准确如果偏差超过 1%回去检查时钟配置和预分频值。没有示波器的话可以用另一个定时器做输入捕获或者用串口每隔一秒打印一次g_ms_tick看累加值是否和实际时间对得上。串口打印会引入额外开销只适合粗测。4. 避坑与排查秒表工程里最容易翻车的五个地方这个工程本身不复杂但正因为简单很多人改着改着就出问题。下面五条是我在拆解和复现过程中实际遇到或见别人踩过的坑按现象、原因、解决三段式写清楚。4.1 计时越跑越慢或越跑越快现象秒表跑一段时间后和手机秒表对比偏差越来越大。原因通常是中断里执行了耗时操作比如刷显示、做浮点运算、调用printf导致中断响应被推迟累加值丢步。解决中断里只做计数和置标志所有耗时操作挪到主循环。另外检查系统时钟配置晶振频率和工程设置不一致会直接改变定时器周期。4.2 按键按一下跳好几下现象按一次启停键秒表状态来回切换好几次。原因是按键机械抖动被多次识别或者状态机里KEY_PRESSED状态没有正确过渡到KEY_RELEASE。解决确认状态机每 10ms 调用一次KEY_DEBOUNCE状态只等一个周期就确认。如果还跳把消抖周期加到 20ms 或 30ms但不要用delay阻塞。4.3 下载后没反应但编译零错误现象Keil 编译通过下载也提示成功板子就是不动。原因可能是启动文件选错、器件型号不匹配、或者下载后没有复位运行。解决检查工程选项里的 Device 是否和板子一致启动文件是否对应容量型号下载配置里勾选「Reset and Run」。如果用的是 GCC确认链接脚本里的 Flash 起始地址和容量正确。4.4 显示数字闪烁或残影现象数码管或 LCD 上的数字一直闪或者旧数字没清干净。原因是刷新频率太低或者没有做消隐。解决把显示刷新放在主循环里保证每 10ms 到 20ms 刷一次。数码管动态扫描的话段选和位选之间加短延时但别用长延时。LCD 的话检查是否每次刷新前清了区域。4.5 复位后计时不清零现象按复位键显示归零了但再按启停键数字从之前的值继续累加。原因是复位只清了显示变量没清g_ms_tick。解决复位操作里同时写g_ms_tick 0和g_update_flag 1并且确保复位时running 0否则中断里的累加会和清零操作打架。5. 从秒表到分段计时一个可复用的计时框架改法把基础秒表跑通之后这个工程最大的价值在于它提供了一个可复用的计时框架。定时器时基、按键状态机、显示刷新这三块拆开来看每一块都能单独拎出来用到别的项目里。我一般会在这个框架上做两件事一是加分段计时二是把计数值通过串口输出方便做数据记录。分段计时的思路是再加一个按键按下时把当前g_ms_tick存到一个数组里显示模块轮流显示总时间和分段值。数组长度按需定常见做法是存 8 到 16 个分段。#define SEG_MAX 8 uint32_t seg_time[SEG_MAX]; uint8_t seg_count 0; void seg_record(void) { if (seg_count SEG_MAX) { seg_time[seg_count] g_ms_tick; } }seg_time存的是绝对时间戳显示分段成绩时做差值即可。seg_count防止数组越界满了之后要么忽略要么覆盖最旧的值看需求定。串口输出的话在主循环里判断g_update_flag每隔一定周期打印一次当前计数值。if (g_update_flag) { g_update_flag 0; printf(time: %lu ms\r\n, g_ms_tick); }printf重定向到串口需要实现fputc这个在标准库工程里很常见。注意printf本身耗时别放在中断里也别每 10ms 打一次改成 100ms 或 500ms 打一次减少对主循环的影响。验证改法是否成功最直接的办法是拿手机秒表做对照启动后同时计时跑 60 秒看偏差是否在可接受范围内。分段计时的话手动按几次分段键检查显示的分段值是否递增且差值合理。串口输出可以用串口助手看时间戳是否连续。从那以后我每次拿到一个能跑的工程都强制先跑一遍「改一个参数、看一个现象」的流程确认自己真的理解了它的运行链路再动手加功能。这个秒表工程不大但把定时器、中断、按键、显示这几样东西的配合关系讲清楚了拿它当起点去改计时器、频率计或者 PWM 测量心里都有底。希望帮到你。本文还有配套的精品资源点击获取