1. STM32定时器秒表设计基础刚接触STM32定时器时我完全被那些专业术语搞懵了——ARR、PSC、CNT这些寄存器名字像天书一样。后来发现定时器本质上就是个高级版的沙漏沙子漏完一次就是一个计时周期。STM32的定时器外设就是通过计数时钟脉冲来实现精准定时。以常见的STM32F103C8T6为例它的定时器分为基本、通用和高级三种类型。做秒表项目最适合用通用定时器如TIM2-TIM5因为它们兼具定时和输入输出功能。我最初尝试用基本定时器做秒表结果发现连按键都接不了只能重新开始。定时器工作的核心公式其实很简单定时时间 (ARR1) * (PSC1) / 时钟频率比如要实现1秒定时当系统时钟为72MHz时设置PSC7199分频后得到10KHzARR9999计数10000次这样正好是1秒。我第一次调试时把ARR设成了999结果秒表走得飞快检查半天才发现少写了个0。2. 硬件电路搭建要点2.1 主控芯片选择我用的是STM32F103C8T6最小系统板价格不到10块钱。这款芯片有4个通用定时器完全够用。有次为了省钱买了STM32F030结果发现定时器功能被阉割只能含泪重新下单。2.2 数码管驱动方案共阳数码管和共阴数码管的驱动逻辑是相反的。我一开始没注意代码里写的共阳驱动实际接的却是共阴数码管显示全乱了。后来用万用表二极管档测出引脚极性才解决。静态驱动虽然简单但占用IO口多。两位数码管需要段选8个IO实际7个小数点不用位选2个IO动态扫描更省IO但要注意刷新频率不能太低否则会闪烁。实测刷新率50Hz时人眼就看不出来了。2.3 按键电路设计机械按键必须加消抖处理我最开始直接读取按键状态结果按一次触发好几回。后来用定时器实现20ms延时消抖效果立竿见影if(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_12)0){ Delay_ms(20); while(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_12)0); Delay_ms(20); // 处理按键动作 }3. 定时器中断配置详解3.1 时基单元设置配置定时器就像组装一台机械钟表选择时钟源上发条设置分频齿轮PSC设定表盘刻度ARR具体代码实现void Timer_Init(void){ TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; // 开启TIM2时钟 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); // 1ms定时配置 TIM_TimeBaseStructure.TIM_Period 1000-1; // ARR TIM_TimeBaseStructure.TIM_Prescaler 72-1; // 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); // 启动定时器 TIM_Cmd(TIM2, ENABLE); }3.2 中断优先级配置NVIC配置就像给中断分配VIP通道。秒表项目中定时器中断优先级应该比按键中断高否则计时会不准确void NVIC_Config(void){ NVIC_InitTypeDef NVIC_InitStructure; NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); // 定时器中断 NVIC_InitStructure.NVIC_IRQChannel TIM2_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 0; NVIC_InitStructure.NVIC_IRQChannelSubPriority 1; NVIC_Init(NVIC_InitStructure); // 按键中断优先级较低 NVIC_InitStructure.NVIC_IRQChannel EXTI15_10_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 1; NVIC_Init(NVIC_InitStructure); }4. 数码管显示驱动实现4.1 数字编码方法数码管的每个数字对应一组段选信号。我最初傻傻地写了10个if判断后来改用查表法简洁多了const uint8_t seg_table[] { 0xC0, // 0 0xF9, // 1 0xA4, // 2 0xB0, // 3 0x99, // 4 0x92, // 5 0x82, // 6 0xF8, // 7 0x80, // 8 0x90 // 9 }; void Display_Number(uint8_t num, uint8_t pos){ GPIO_Write(GPIOA, seg_table[num]); // 段选 GPIO_Write(GPIOB, pos0?0xFFFE:0xFFFD); // 位选 }4.2 动态扫描技巧动态显示要注意两点扫描频率要够快50Hz每位显示后要短暂延时我优化后的显示函数void Display_Scan(uint8_t num1, uint8_t num2){ static uint8_t pos 0; if(pos 0){ Display_Number(num1, 0); pos 1; }else{ Display_Number(num2, 1); pos 0; } Delay_us(500); // 保持显示时间 }5. 按键功能逻辑设计5.1 外部中断配置STM32的每个GPIO都可以配置为外部中断源。我把三个按键分别接到PB12、PB13、PB14void Key_Init(void){ EXTI_InitTypeDef EXTI_InitStructure; // PB12-14配置为上拉输入 GPIO_InitStructure.GPIO_Pin GPIO_Pin_12 | GPIO_Pin_13 | GPIO_Pin_14; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU; GPIO_Init(GPIOB, GPIO_InitStructure); // 配置EXTI线路 GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource12); EXTI_InitStructure.EXTI_Line EXTI_Line12; EXTI_InitStructure.EXTI_Mode EXTI_Mode_Interrupt; EXTI_InitStructure.EXTI_Trigger EXTI_Trigger_Falling; EXTI_Init(EXTI_InitStructure); }5.2 状态机实现用状态变量管理秒表状态停止/计时/暂停typedef enum{ STOP, RUNNING, PAUSE }State; State timer_state STOP; void EXTI15_10_IRQHandler(void){ if(EXTI_GetITStatus(EXTI_Line12) SET){ // 复位键 count 0; timer_state STOP; }else if(EXTI_GetITStatus(EXTI_Line13) SET){ // 暂停键 if(timer_state RUNNING) timer_state PAUSE; }else if(EXTI_GetITStatus(EXTI_Line14) SET){ // 开始键 if(timer_state ! RUNNING) timer_state RUNNING; } EXTI_ClearITPendingBit(EXTI_Line12 | EXTI_Line13 | EXTI_Line14); }6. 完整系统整合与调试6.1 主函数逻辑主循环只需要处理显示和状态切换int main(void){ SystemInit(); Timer_Init(); Key_Init(); Display_Init(); while(1){ if(timer_state RUNNING){ Display_Scan(count/10, count%10); }else if(timer_state PAUSE){ Display_Scan(count/10, count%10); }else{ Display_Scan(0, 0); } } }6.2 常见问题排查数码管显示不全检查限流电阻是否合适一般220Ω按键无反应用万用表测量按键按下时电压变化计时不准用示波器检查定时器中断间隔显示闪烁增加动态扫描频率记得第一次调试时秒表走到59秒后就卡住了原来是判断条件写成了if(count 99)应该用if(count 99) count0。这种小bug最折磨人后来我养成了在中断里加LED指示的习惯通过观察LED闪烁就能快速定位问题。