资讯详情

STM32精英板秒表实战:定时器中断与数码管动态扫描从零到跑通

📅 2026/10/14 17:39:21 | 华诺云谱 👁 阅读
STM32精英板秒表实战:定时器中断与数码管动态扫描从零到跑通
简介这是一份基于STM32的秒表程序源码面向正在学习单片机开发、需要完成计时类课程设计或嵌入式练手的初学者与进阶者。程序最小计数单位为0.01秒支持暂停与清零通过keyup控制开始与继续、key0清零、key1暂停逻辑清晰其他厂家开发板稍作修改即可移植使用。压缩包共140个文件约2.69MB包含36个h头文件、34个c源文件以及编译生成的o、d、crf等中间文件另有sct分散加载、uvprojx工程、hex烧录文件与map映射文件覆盖从源码到可执行固件的完整工程结构。目前已有2388人学习下载。作者对几乎每个函数都做了标注读者可据此理解定时器计数、按键扫描与LCD显示的实现思路并在此基础上扩展硬件功能适合作为STM32计时类项目的参考模板。1. 从“精英板秒表成功.rar”说起一个压缩包背后藏着多少嵌入式入门者的第一次心跳如果你在搜索引擎里敲下“精英板秒表成功.rar”大概率你手里正捏着一块吃灰的开发板电脑里装好了某个版本的 IDE却卡在“怎么让数码管跳起来”这一步。这个压缩包名字本身就带着一股“终于跑通了”的兴奋劲儿——精英板通常指某款基于 Cortex-M3 内核、外设布局对新手友好的开发板而“秒表”几乎是每个嵌入式初学者继流水灯、按键消抖之后第三个必须亲手撸一遍的经典实验。它不复杂但麻雀虽小五脏俱全GPIO 输入输出、定时器中断、数码管动态扫描、按键状态机一个都跑不掉。你需要的不是一份“代码大全”而是一条从新建工程到烧录验证、从现象不对到定位问题的完整路径。这篇文章就按这个压缩包里最可能包含的那套东西把秒表实验从零拆到能复现顺带把那些“明明照着抄却跑不起来”的血泪坑一个个填平。适合刚拿到板子、想用一个小项目把外设串起来的人也适合带学生做课设、需要一份可落地参考的工程师。2. 精英板秒表工程的骨架定时器、数码管与按键怎么拧成一股绳2.1 为什么秒表是定时器中断最典型的练手场景秒表的核心诉求是“每隔固定时间让计数值加一”这天然对应硬件定时器的更新中断。以常见的 STM32F103 系列为例板载晶振通常是 8MHz经过 PLL 倍频到 72MHz 作为系统时钟。如果直接用系统时钟计数1 秒需要 7200 万个脉冲16 位定时器根本装不下。所以标准做法是配置定时器预分频器PSC和自动重装载寄存器ARR把 72MHz 分频到 1kHz 或 100Hz让每次中断间隔为 1ms 或 10ms。1ms 中断一次累计 1000 次就是 1 秒这样秒表的“百分秒”位也能顺带做出来。选 1ms 还是 10ms 取决于你对显示刷新和按键响应的要求1ms 中断太频繁会吃掉 CPU 时间10ms 又会让百分秒跳动不够细腻。我一般会取 1ms 作为时基然后在中断里维护多个计数器变量显示刷新和按键扫描分频处理这样既保证计时精度又不至于让主循环饿死。2.2 数码管动态扫描让四位数码管“同时”亮起来的秘密精英板通常配四位共阳或共阴数码管段选线并联位选线独立。所谓动态扫描就是利用人眼视觉暂留快速轮流点亮每一位只要刷新率高于 50Hz看起来就是同时亮的。具体到代码你需要一个段码表把 0-9 转换成对应的 GPIO 电平组合再通过位选引脚依次选通千位、百位、十位、个位。这里有个容易翻车的地方如果位选和段选操作之间没有加短暂延时或锁存会出现“鬼影”——上一位的段码还没消失下一位已经选通导致数字重叠模糊。常见做法是每次切换位选前先把段选全部拉低共阳则拉高消隐再送新段码再开位选。这个顺序不能乱乱了就是玄学显示。2.3 按键状态机短按启停、长按清零的逻辑怎么写才不打架秒表一般需要两个按键一个控制启动/暂停一个控制复位清零。如果直接在主循环里if(GPIO_ReadInputDataBit(...)0) { ... }不加消抖和状态区分会出现按一下计数值跳好几下、或者长按清零时秒表还在跑。稳妥的做法是做一个 10ms 周期的按键扫描状态机记录按键当前电平、上一次电平、按下持续时间。当检测到下降沿且持续时间超过 20ms 才认定为有效按下如果按下持续超过 1 秒则判定为长按触发清零。启停键只响应短按复位键短按清零、长按也可以清零这样逻辑不冲突。状态机变量建议用static修饰放在独立源文件里避免全局变量满天飞。2.4 最小工程的文件组织与关键配置一个能跑通的秒表工程文件不需要多但分层要清楚。我一般会这样组织/* main.c —— 只负责初始化和主循环调度 */ #include stm32f10x.h #include timer.h #include smg.h #include key.h volatile uint32_t g_ms_tick 0; /* 毫秒计数定时器中断里自增 */ volatile uint8_t g_run_flag 0; /* 秒表运行标志 */ volatile uint32_t g_centi_sec 0; /* 百分秒累计值 */ int main(void) { Timer2_Init_1ms(); /* 配置 TIM2 为 1ms 中断 */ SMG_Init(); /* 数码管 GPIO 初始化 */ Key_Init(); /* 按键 GPIO 初始化 */ while (1) { Key_Scan_10ms(); /* 每 10ms 调用一次内部做分频 */ SMG_Display_Update(); /* 刷新数码管显示 g_centi_sec */ } }/* timer.c —— 定时器中断服务函数只做计数不做耗时操作 */ #include stm32f10x.h extern volatile uint32_t g_ms_tick; extern volatile uint8_t g_run_flag; extern volatile uint32_t g_centi_sec; void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); g_ms_tick; if (g_run_flag) { if (g_ms_tick % 10 0) /* 每 10ms 百分秒加一 */ { g_centi_sec; } } } }上面两段代码里Timer2_Init_1ms()需要你根据实际晶振频率计算 PSC 和 ARR。假设系统时钟 72MHz要得到 1ms 中断则定时器时钟为 72MHz预分频后计数频率应为 1MHz即 PSC71ARR 设为 999则每 1000 个计数溢出一次正好 1ms。中断里只做计数和标志位判断数码管刷新和按键扫描全部放在主循环这是保证系统不卡死的基本原则。g_centi_sec每加 100 就是 1 秒显示时拆成四位千位和百位显示秒十位和个位显示百分秒。3. 从零新建工程到烧录验证每一步都对应一个可检查的现象3.1 新建工程时时钟树和下载选项怎么点打开 IDE 新建工程选好具体芯片型号后第一件事是配置系统时钟。很多新手直接跳过 RCC 配置用默认的 8MHz 内部时钟跑结果定时器算出来的 1ms 实际是 9ms秒表走得比蜗牛还慢。正确做法是在SystemInit()里把时钟源切换到外部晶振再通过 PLL 倍频到 72MHz。如果你用的是标准外设库可以直接调用SystemInit()但要在工程预定义宏里加上USE_STDPERIPH_DRIVER否则库函数链接会报一堆未定义。下载选项里调试器一般选 SWD 模式速度设 1MHz 比较稳太快容易连不上。烧录前记得在“Flash Download”里勾选“Reset and Run”否则每次烧完还要手动按复位容易误判程序没跑起来。3.2 定时器初始化代码逐行拆解void Timer2_Init_1ms(void) { TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; NVIC_InitTypeDef NVIC_InitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); TIM_TimeBaseStructure.TIM_Period 999; /* ARR */ TIM_TimeBaseStructure.TIM_Prescaler 71; /* PSC */ 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); NVIC_InitStructure.NVIC_IRQChannel TIM2_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 0; NVIC_InitStructure.NVIC_IRQChannelSubPriority 1; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure); TIM_Cmd(TIM2, ENABLE); }这段代码里TIM_Prescaler和TIM_Period是决定中断频率的两个关键参数。PSC 把 72MHz 分频成 1MHzARR 决定数到多少溢出。如果你把 PSC 改成 7199ARR 改成 9同样能得到 1ms 中断但计数精度和中断响应特性略有不同一般选 PSC 大一些、ARR 小一些中断频率更稳定。NVIC 优先级设置里抢占优先级和子优先级要根据系统里其他中断来定秒表实验没有其他中断随便设都能跑但养成好习惯时基中断优先级不要设得太低否则会被其他中断打断导致计时不准。3.3 数码管段码表的生成与显示函数/* 共阴数码管段码表0-9a~f 可选 */ const uint8_t smg_code[] { 0x3F, /* 0 */ 0x06, /* 1 */ 0x5B, /* 2 */ 0x4F, /* 3 */ 0x66, /* 4 */ 0x6D, /* 5 */ 0x7D, /* 6 */ 0x07, /* 7 */ 0x7F, /* 8 */ 0x6F /* 9 */ }; void SMG_Display_Update(void) { static uint8_t pos 0; uint32_t value g_centi_sec; uint8_t digit; /* 消隐先关闭所有位选 */ GPIO_ResetBits(GPIOA, GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3); switch (pos) { case 0: digit (value / 1000) % 10; break; /* 秒千位 */ case 1: digit (value / 100) % 10; break; /* 秒百位 */ case 2: digit (value / 10) % 10; break; /* 百分秒十位 */ case 3: digit value % 10; break; /* 百分秒个位 */ default: digit 0; break; } GPIO_Write(GPIOB, smg_code[digit]); /* 送段码 */ GPIO_SetBits(GPIOA, 1 pos); /* 开对应位选 */ pos (pos 1) % 4; }这个函数每调用一次切换一位调用频率要保证四位轮流一遍不超过 20ms否则会看到闪烁。如果你在主循环里调用主循环其他任务耗时太长就会闪。稳妥做法是把它放到定时器中断里每 2ms 调用一次但中断里操作 GPIO 要小心时序别影响其他中断响应。段码表根据你的数码管是共阴还是共阳来定共阳要把 0x3F 取反变成 0xC0这个搞反了就是全亮或全灭别问我是怎么知道的。3.4 烧录后现象自检清单烧录成功后先看电源灯亮不亮再看数码管有没有显示。如果全灭检查位选和段选的 GPIO 时钟是否使能很多人只初始化了 GPIO 但忘了开 RCC_APB2Periph_GPIOA 和 GPIOB。如果显示乱码或缺段用万用表测段选引脚电压对照段码表看哪一位不对。如果秒表不走先确认定时器中断有没有进去——可以在中断里翻转一个空闲 IO用示波器或 LED 看有没有波形。如果按键没反应检查按键引脚是否配置成上拉输入很多板子按键另一端接地不加上拉就是浮空读出来全是玄学。4. 那些年秒表实验里踩过的坑从鬼影到计时漂移4.1 数码管显示鬼影现象、原因与消隐顺序现象数码管上某些位显示的数字周围有淡淡的残影或者相邻位数字重叠。原因位选切换时上一位的段码还没完全关断下一位已经导通导致瞬间两个位同时亮。解决在每次切换位选之前先把段选数据全部清零共阴或置高共阳再送新段码最后开位选。这个顺序不能省省了就是鬼影。另外如果 GPIO 翻转速度太快可以在消隐后加几个空指令__NOP()等待电平稳定。4.2 计时越走越慢中断里干了太多活现象秒表跑几分钟后比手机慢好几秒。原因定时器中断里做了数码管刷新或按键扫描导致中断服务时间过长下一次中断来临时还在处理上一次丢失了中断。解决中断里只做计数和标志位所有耗时操作放主循环。如果主循环任务多可以用一个g_ms_tick全局变量主循环里判断时间差来调度任务而不是在中断里直接调用函数。4.3 按键按一下跳好几下消抖没做对现象按一次启停键秒表启动又暂停反复横跳。原因机械按键抖动持续 5-20ms代码里没有消抖一次按下被识别成多次。解决在 10ms 周期任务里读按键连续两次读到相同电平才确认状态变化。或者用状态机空闲态检测到低电平进入消抖态20ms 后还是低电平才进入按下态。消抖时间不能太长否则长按检测会迟钝。4.4 烧录后没反应启动模式或复位电路在捣鬼现象IDE 显示烧录成功但板子毫无反应。原因启动模式跳线帽插错芯片从 RAM 启动而不是 Flash或者复位电路电容太大上电复位时间不够。解决检查 BOOT0 和 BOOT1 跳线确保 BOOT0 接地。如果板子有复位按键按一下再松开看是否恢复。另外有些下载器需要目标板供电只插下载器不插电源芯片不工作。4.5 百分秒显示跳变不均匀时基和显示刷新不同步现象百分秒位有时跳两下有时停一下。原因显示刷新频率和计时更新频率没有对齐或者主循环里g_centi_sec读取时被中断修改读到了半新半旧的值。解决读取多字节变量时先关中断读完再开中断或者用volatile修饰并在读取时做原子操作。显示刷新频率固定为 2ms 一次计时更新固定 10ms 一次两者用同一个时基分频就不会打架。5. 把秒表跑成产品级精度校准、低功耗与状态保存的进阶手法秒表实验跑通只是起点如果你想让它在实际场景里站住脚还有三件事值得做。第一是精度校准。内部 RC 振荡器温漂大用外部晶振也会有个体差异你可以用一个已知准确的时间源比如手机秒表对比 10 分钟算出误差比例然后在代码里对g_centi_sec的增量做微调——比如每计满 10000 次就多减一次把误差压到每天几秒以内。第二是低功耗。如果秒表用电池供电主循环里可以加__WFI()指令让 CPU 在空闲时休眠定时器中断唤醒这样电流能从十几毫安降到几毫安。第三是状态保存。掉电后秒表值丢失很尴尬可以利用芯片内部的备份寄存器或外挂 EEPROM在每次清零或暂停时写入当前值上电时读回。下面是一个简单的备份寄存器读写示例/* 使用 STM32 备份寄存器保存秒表值需要先使能 PWR 和 BKP 时钟 */ #include stm32f10x.h void BKP_Write_CentiSec(uint32_t value) { RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE); PWR_BackupAccessCmd(ENABLE); BKP_WriteBackupRegister(BKP_DR1, value 0xFFFF); BKP_WriteBackupRegister(BKP_DR2, (value 16) 0xFFFF); } uint32_t BKP_Read_CentiSec(void) { uint32_t value; RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE); PWR_BackupAccessCmd(ENABLE); value BKP_ReadBackupRegister(BKP_DR1); value | (BKP_ReadBackupRegister(BKP_DR2) 16); return value; }这段代码把 32 位百分秒值拆成两个 16 位存进备份寄存器主电源掉电后如果纽扣电池还在数据就不会丢。注意备份寄存器只有在你使能了 PWR 时钟和备份访问后才能写否则写进去全是零。另外备份域复位后数据也会丢所以别指望它当 EEPROM 用只适合短时间掉电保存。最后说一个我自己的习惯每次调通一个外设我都会把关键配置参数和现象记在一个文本文件里比如“TIM2 PSC71 ARR999 实测 1ms 准确”“共阴段码 0x3F 对应数字 0”。下次再新建工程直接翻记录不用重新试错。秒表这个项目虽然小但它是你嵌入式调试方法论的缩影——先让最小现象出现再逐步加功能每加一个就验证一个别一口气写完再烧那样出了问题你连从哪查都不知道。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑