4. 定时器抽象层设计

任务目标

在嵌入式系统中,不同MCU的硬件定时器寄存器操作差异大。本案例将硬件定时器封装为通用定时器接口,支持周期/单次模式配置与回调注册,使上层业务代码不感知具体芯片寄存器,直接复用定时服务。

核心接口设计

/* timer_hal.h */
#ifndef TIMER_HAL_H
#define TIMER_HAL_H

#include <stdint.h>

typedef enum {
    TIMER_MODE_ONESHOT = 0,
    TIMER_MODE_PERIODIC
} timer_mode_t;

typedef void (*timer_callback_t)(void *arg);

typedef struct {
    uint32_t period_us;        /* 定时周期,单位微秒 */
    timer_mode_t mode;
    timer_callback_t callback;
    void *cb_arg;
} timer_cfg_t;

/* 初始化定时器硬件,返回0成功 */
int timer_hal_init(void);

/* 启动定时器,timer_id为0~N-1 */
int timer_hal_start(uint8_t timer_id, const timer_cfg_t *cfg);

/* 停止定时器 */
int timer_hal_stop(uint8_t timer_id);

#endif /* TIMER_HAL_H */

关键完整实现(以STM32 HAL库为底层)

/* timer_hal.c */
#include "timer_hal.h"
#include "stm32f1xx_hal.h"

#define MAX_TIMER_NUM  4

static TIM_HandleTypeDef htim[MAX_TIMER_NUM];
static timer_callback_t cb_table[MAX_TIMER_NUM];
static void *cb_arg_table[MAX_TIMER_NUM];
static uint8_t timer_busy[MAX_TIMER_NUM];

/* 底层定时器时钟使能与GPIO配置(此处仅列出TIM2~TIM5使能) */
static void hw_timer_clock_enable(uint8_t idx)
{
    switch (idx) {
    case 0: __HAL_RCC_TIM2_CLK_ENABLE(); break;
    case 1: __HAL_RCC_TIM3_CLK_ENABLE(); break;
    case 2: __HAL_RCC_TIM4_CLK_ENABLE(); break;
    case 3: __HAL_RCC_TIM5_CLK_ENABLE(); break;
    default: break;
    }
}

static void hw_timer_base_init(uint8_t idx, uint32_t period_us)
{
    /* 假设系统时钟72MHz,预分频72-1,则计数频率1MHz,1 tick = 1us */
    htim[idx].Instance = (TIM_TypeDef *)(TIM2_BASE + idx * 0x400);
    htim[idx].Init.Prescaler = 72 - 1;
    htim[idx].Init.CounterMode = TIM_COUNTERMODE_UP;
    htim[idx].Init.Period = period_us - 1;
    htim[idx].Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
    HAL_TIM_Base_Init(&htim[idx]);
}

int timer_hal_init(void)
{
    for (int i = 0; i < MAX_TIMER_NUM; i++) {
        timer_busy[i] = 0;
        cb_table[i] = 0;
        cb_arg_table[i] = 0;
    }
    return 0;
}

int timer_hal_start(uint8_t timer_id, const timer_cfg_t *cfg)
{
    if (timer_id >= MAX_TIMER_NUM || cfg == 0 || cfg->callback == 0)
        return -1;
    if (timer_busy[timer_id])
        return -2;

    hw_timer_clock_enable(timer_id);
    hw_timer_base_init(timer_id, cfg->period_us);

    cb_table[timer_id] = cfg->callback;
    cb_arg_table[timer_id] = cfg->cb_arg;
    timer_busy[timer_id] = 1;

    if (cfg->mode == TIMER_MODE_ONESHOT)
        htim[timer_id].Init.Period = cfg->period_us - 1;

    HAL_TIM_Base_Start_IT(&htim[timer_id]);
    return 0;
}

int timer_hal_stop(uint8_t timer_id)
{
    if (timer_id >= MAX_TIMER_NUM)
        return -1;
    if (!timer_busy[timer_id])
        return -2;

    HAL_TIM_Base_Stop_IT(&htim[timer_id]);
    timer_busy[timer_id] = 0;
    cb_table[timer_id] = 0;
    return 0;
}

/* 中断回调:由HAL库在中断中调用 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim_ptr)
{
    for (int i = 0; i < MAX_TIMER_NUM; i++) {
        if (htim_ptr->Instance == htim[i].Instance && timer_busy[i]) {
            if (cb_table[i])
                cb_table[i](cb_arg_table[i]);
            /* 单次模式:触发后自动停止 */
            if (htim[i].Init.Period && htim[i].State == HAL_TIM_STATE_READY) {
                /* 此处由HAL库自动处理单次模式停止 */
            }
            break;
        }
    }
}

上层调用示例

/* app_timer_demo.c */
#include "timer_hal.h"
#include <stdio.h>

static void led_blink_cb(void *arg)
{
    (void)arg;
    printf("timer tick\r\n");
}

void app_timer_demo(void)
{
    timer_cfg_t cfg;

    timer_hal_init();

    cfg.period_us = 1000000;   /* 1秒 */
    cfg.mode = TIMER_MODE_PERIODIC;
    cfg.callback = led_blink_cb;
    cfg.cb_arg = 0;

    if (timer_hal_start(0, &cfg) == 0)
        printf("timer0 started\r\n");
}

运行与验证

  1. 依赖:STM32F1系列HAL库,系统时钟配置为72MHz(若时钟不同,需调整Prescaler使计数频率为1MHz)。
  2. 编译:将timer_hal.c加入工程,包含timer_hal.h头文件路径。
  3. 调用:在main函数中调用app_timer_demo(),并确保HAL定时器中断已使能(NVIC配置在HAL_TIM_Base_Start_IT内部完成)。
  4. 通过条件:串口每秒输出一次timer tick,且系统不进入HardFault。将cfg.mode改为TIMER_MODE_ONESHOT后重新编译运行,应只输出一次timer tick,之后定时器停止。

常见问题

  • 定时不准确:检查Prescaler是否与系统时钟匹配。若系统时钟为72MHz,预分频72-1得到1MHz计数时钟,Period = period_us - 1即得到微秒级定时。
  • 中断未触发:确认在HAL_TIM_Base_Start_IT前已调用HAL_NVIC_EnableIRQ(HAL库在Start_IT中自动完成),且定时器全局中断已在stm32f1xx_hal_msp.c中正确配置。
  • 多定时器冲突:本实现固定映射TIM2~TIM5,若芯片资源不足可减少MAX_TIMER_NUM或改用其他定时器实例。