4. 定时器中断实现秒表

任务目标

使用STM32定时器中断实现精确计时秒表,通过串口周期性输出计时读数(时:分:秒.毫秒)。计时精度由定时器硬件保证,不依赖主循环软件延时。

硬件与依赖

  • STM32F103系列开发板(本文以STM32F103C8T6为例)
  • USART1:PA9(TX)、PA10(RX),波特率115200
  • TIM2:72MHz时钟,配置为1ms中断周期
  • 使用标准外设库(SPL)或HAL库均可,本文给出寄存器级实现以展示核心原理

核心实现

定时器配置为向上计数模式,自动重装载值设为7199(72MHz/7200=10kHz),再通过软件分频每10次中断产生1ms基准。秒表变量在中断服务函数中累加,主循环中通过串口打印当前读数。

// 秒表数据结构
typedef struct {
    uint32_t ms;      // 毫秒计数
    uint8_t second;
    uint8_t minute;
    uint8_t hour;
} Stopwatch_t;

volatile Stopwatch_t sw = {0, 0, 0, 0};
volatile uint8_t ms_tick = 0;  // 1ms标志

// TIM2中断服务函数
void TIM2_IRQHandler(void) {
    if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) {
        TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
        
        static uint8_t sub_count = 0;
        sub_count++;
        if (sub_count >= 10) {  // 10次100us中断 = 1ms
            sub_count = 0;
            ms_tick = 1;
        }
    }
}

// 在main中调用,每1ms更新一次秒表
void Stopwatch_Update(void) {
    if (ms_tick) {
        ms_tick = 0;
        sw.ms++;
        if (sw.ms >= 1000) {
            sw.ms = 0;
            sw.second++;
            if (sw.second >= 60) {
                sw.second = 0;
                sw.minute++;
                if (sw.minute >= 60) {
                    sw.minute = 0;
                    sw.hour++;
                    if (sw.hour >= 24) sw.hour = 0;
                }
            }
        }
    }
}

// 串口打印秒表读数
void Stopwatch_Print(void) {
    char buf[32];
    sprintf(buf, "%02d:%02d:%02d.%03lu\r\n", 
            sw.hour, sw.minute, sw.second, (unsigned long)sw.ms);
    USART_SendString(USART1, buf);
}

// 主函数
int main(void) {
    // 系统时钟72MHz、GPIO、USART1初始化(略,标准配置)
    USART1_Init(115200);
    
    // TIM2初始化:72MHz/7200 = 10kHz,即100us中断一次
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
    TIM_TimeBaseInitTypeDef TIM_InitStruct;
    TIM_InitStruct.TIM_Prescaler = 71;        // 72MHz/72 = 1MHz
    TIM_InitStruct.TIM_CounterMode = TIM_CounterMode_Up;
    TIM_InitStruct.TIM_Period = 99;           // 1MHz/100 = 10kHz
    TIM_InitStruct.TIM_ClockDivision = TIM_CKD_DIV1;
    TIM_TimeBaseInit(TIM2, &TIM_InitStruct);
    
    NVIC_InitTypeDef NVIC_InitStruct;
    NVIC_InitStruct.NVIC_IRQChannel = TIM2_IRQn;
    NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 0;
    NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0;
    NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStruct);
    
    TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
    TIM_Cmd(TIM2, ENABLE);
    
    uint32_t last_print = 0;
    while (1) {
        Stopwatch_Update();
        
        // 每500ms打印一次
        if (sw.ms - last_print >= 500 || sw.ms < last_print) {
            last_print = sw.ms;
            Stopwatch_Print();
        }
    }
}

运行与验证

  1. 将代码编译下载至开发板,连接串口至PC(PA9→RX,PA10→TX,共地)
  2. 打开串口调试助手,设置115200-8-N-1
  3. 复位开发板,观察串口输出

预期输出示例(每500ms更新一次):

00:00:00.000
00:00:00.500
00:00:01.000
00:00:01.500
...

通过判定标准:

  • 串口持续输出递增的秒表读数,格式正确
  • 读数间隔均匀,用秒表对照1分钟内误差不超过±0.5秒
  • 毫秒位按0、500、000、500规律交替(因500ms打印间隔)

关键说明

  • 定时器分频参数:72MHz主频经71分频得1MHz,再计数100次得10kHz中断频率(100μs周期),软件10次累加得1ms基准
  • 若使用HAL库,将TIM2配置为1ms周期中断,在回调中直接递增sw.ms即可,无需软件分频
  • 打印间隔500ms仅为便于观察,可修改条件实现更频繁或更稀疏的输出
  • 秒表最大计时24小时后自动归零,如需更长计时可扩展hour为uint32_t