在嵌入式系统开发中,定时器是实现精准时间控制、周期性任务调度、延时操作的核心模块。无论是简单的裸机应用还是复杂的实时操作系统,对定时器的理解和熟练应用都至关重要。本文将系统梳理从硬件定时器到软件定时器的完整知识体系,并提供在STM32、RTOS及Linux等不同环境下的实战配置代码。
定时器分类概览
嵌入式定时器主要分为硬件定时器和软件定时器两大类,各自适用于不同的场景。
分类图表
嵌入式定时器
├── 硬件定时器
│ ├── 系统定时器(SysTick,系统滴答)
│ ├── 通用定时器(TIM,支持PWM/捕获/比较)
│ ├── 实时时钟(RTC)
│ ├── 低功耗定时器(LPTIM)
│ └── 看门狗定时器(WDT)
└── 软件定时器
├── RTOS软件定时器(如FreeRTOS、RT-Thread)
├── 裸机软件定时器(基于硬件定时器实现)
└── Linux系统定时器(如timerfd、alarm)
特性对比
| 定时器类型 |
精度 |
功耗 |
主要功能 |
典型适用场景 |
| SysTick |
高 (1ms级) |
低 |
系统节拍 |
RTOS任务调度、系统延时 |
| 通用TIM |
很高 (ns级) |
中 |
PWM/输入捕获/输出比较 |
电机控制、信号测量 |
| RTC |
低 (秒级) |
极低 |
日历/闹钟 |
时间戳记录、定时唤醒 |
| LPTIM |
中 |
极低 |
低功耗定时 |
电池供电设备、传感器采集 |
| WDT |
低 |
低 |
系统监控 |
故障检测与恢复 |
| RTOS软件定时器 |
中 |
中 |
任务调度 |
周期性任务管理 |
| timerfd |
高 |
中 |
事件驱动定时 |
Linux应用开发 |
硬件定时器详解与配置
SysTick(系统滴答定时器)
特性:
- ARM Cortex-M内核内置的24位递减计数器。
- 独立于外设,不占用额外的定时器资源。
- 主要用于为操作系统(如FreeRTOS、RT-Thread)提供任务调度的基准时钟节拍。
工作原理:
配置重装载值(LOAD) -> 启动定时器 -> 计数器递减 -> 计数到0触发SysTick中断 -> 执行中断处理程序(进行任务调度等)-> 自动重装载,周而复始。
STM32 HAL库配置示例(1ms中断):
#include "stm32f4xx.h"
// SysTick配置(1ms中断)
void SysTick_Config_1ms(void) {
// 假设系统时钟为168MHz
uint32_t sysclk = SystemCoreClock;
uint32_t reload = sysclk / 1000; // 1ms对应的计数值
SysTick->LOAD = reload - 1; // 设置重装载值
SysTick->VAL = 0; // 清空当前计数值
// 使能SysTick,使用系统时钟,并开启中断
SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk |
SysTick_CTRL_TICKINT_Msk |
SysTick_CTRL_ENABLE_Msk;
}
// SysTick中断服务函数
void SysTick_Handler(void) {
// 如果使用了RTOS,调用其系统节拍处理函数
#ifdef USE_RTOS
osSystickHandler();
#endif
// 用户自定义的节拍计数(例如用于HAL_Delay)
static uint32_t tick_count = 0;
tick_count++;
}
// 基于SysTick实现的毫秒延时函数
void delay_ms(uint32_t ms) {
uint32_t start_tick = HAL_GetTick(); // 此函数依赖于SysTick
while ((HAL_GetTick() - start_tick) < ms) {
// 空循环等待
}
}
应用场景:RTOS任务调度节拍、系统延时函数、提供HAL_GetTick()时间基准。
通用定时器(TIM)
特性:
- 功能极其丰富:支持基本定时、PWM生成、输入捕获、输出比较、编码器接口等。
- 精度高:通过预分频配置,可实现纳秒级精度。
- 多通道独立:一个定时器常可支持多路PWM输出或输入捕获。
功能模式:
通用定时器 (TIM)
├── 定时模式:产生周期性中断或单次触发。
├── PWM输出模式:用于电机控制、LED调光、蜂鸣器驱动。
├── 输入捕获模式:精确测量脉冲宽度、频率或边沿检测。
├── 输出比较模式:产生精确的延时或特定波形。
└── 编码器模式:读取正交编码器信号。
PWM输出配置示例(1kHz,50%占空比):
#include "stm32f4xx_hal.h"
TIM_HandleTypeDef htim2;
void TIM2_PWM_Init(void) {
TIM_OC_InitTypeDef sConfigOC = {0};
htim2.Instance = TIM2;
htim2.Init.Prescaler = 168 - 1; // 预分频:168MHz/168 = 1MHz
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
htim2.Init.Period = 1000 - 1; // 自动重载值:1MHz/1000 = 1kHz
htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
HAL_TIM_PWM_Init(&htim2);
// 配置PWM通道1
sConfigOC.OCMode = TIM_OCMODE_PWM1;
sConfigOC.Pulse = 500; // 比较值,占空比 = 500/1000 = 50%
sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
HAL_TIM_PWM_ConfigChannel(&htim2, &sConfigOC, TIM_CHANNEL_1);
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1); // 启动PWM输出
}
// 动态调整PWM占空比
void TIM2_SetDutyCycle(uint16_t duty) {
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, duty);
}
输入捕获配置示例(测量脉冲宽度):
TIM_HandleTypeDef htim3;
volatile uint32_t capture_start = 0, capture_end = 0, pulse_width_us = 0;
void TIM3_InputCapture_Init(void) {
TIM_IC_InitTypeDef sConfigIC = {0};
htim3.Instance = TIM3;
htim3.Init.Prescaler = 168 - 1; // 1MHz计数频率,1个计数值=1us
htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
htim3.Init.Period = 0xFFFFFFFF; // 使用32位计数器
HAL_TIM_IC_Init(&htim3);
// 通道1捕获上升沿
sConfigIC.ICPolarity = TIM_INPUTCHANNELPOLARITY_RISING;
sConfigIC.ICSelection = TIM_ICSELECTION_DIRECTTI;
sConfigIC.ICPrescaler = TIM_ICPSC_DIV1;
sConfigIC.ICFilter = 0;
HAL_TIM_IC_ConfigChannel(&htim3, &sConfigIC, TIM_CHANNEL_1);
// 通道2捕获下降沿
sConfigIC.ICPolarity = TIM_INPUTCHANNELPOLARITY_FALLING;
HAL_TIM_IC_ConfigChannel(&htim3, &sConfigIC, TIM_CHANNEL_2);
HAL_TIM_IC_Start_IT(&htim3, TIM_CHANNEL_1);
HAL_TIM_IC_Start_IT(&htim3, TIM_CHANNEL_2);
}
// 输入捕获中断回调函数
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) {
if (htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1) {
capture_start = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1);
} else if (htim->Channel == HAL_TIM_ACTIVE_CHANNEL_2) {
capture_end = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_2);
// 计算脉冲宽度(微秒),处理计数器溢出
if (capture_end > capture_start) {
pulse_width_us = capture_end - capture_start;
} else {
pulse_width_us = (0xFFFFFFFF - capture_start) + capture_end;
}
}
}
RTC(实时时钟)
特性:
- 提供完整的日历功能(年、月、日、时、分、秒)。
- 功耗极低,通常由纽扣电池在系统掉电后维持运行。
- 支持闹钟和自动唤醒功能。
应用场景:为数据记录添加时间戳、在特定时刻触发任务、将系统从低功耗模式唤醒。
STM32 RTC配置示例:
#include "stm32f4xx_hal.h"
RTC_HandleTypeDef hrtc;
void RTC_Init(void) {
hrtc.Instance = RTC;
hrtc.Init.HourFormat = RTC_HOURFORMAT_24;
hrtc.Init.AsynchPrediv = 127;
hrtc.Init.SynchPrediv = 255;
hrtc.Init.OutPut = RTC_OUTPUT_DISABLE;
HAL_RTC_Init(&hrtc);
}
// 设置时间
void RTC_SetTime(uint8_t hour, uint8_t min, uint8_t sec) {
RTC_TimeTypeDef sTime = {0};
sTime.Hours = hour;
sTime.Minutes = min;
sTime.Seconds = sec;
HAL_RTC_SetTime(&hrtc, &sTime, RTC_FORMAT_BIN);
}
// 设置闹钟(每天10:30触发)
void RTC_SetAlarm(void) {
RTC_AlarmTypeDef sAlarm = {0};
sAlarm.AlarmTime.Hours = 10;
sAlarm.AlarmTime.Minutes = 30;
sAlarm.AlarmTime.Seconds = 0;
sAlarm.AlarmMask = RTC_ALARMMASK_DATEWEEKDAY; // 忽略日期,仅按时间匹配
sAlarm.Alarm = RTC_ALARM_A;
HAL_RTC_SetAlarm_IT(&hrtc, &sAlarm, RTC_FORMAT_BIN);
}
// 闹钟中断回调
void HAL_RTC_AlarmAEventCallback(RTC_HandleTypeDef *hrtc) {
printf("RTC Alarm Triggered!\r\n");
// 执行预定任务
}
LPTIM(低功耗定时器)
特性:
- 设计用于低功耗模式(Stop、Standby),可在核心时钟关闭时运行。
- 由低速内部时钟(LSI)或外部低速晶体(LSE)驱动。
- 是电池供电设备实现周期性唤醒的关键部件。
STM32 LPTIM配置示例(1秒低功耗定时):
#include "stm32l4xx_hal.h"
LPTIM_HandleTypeDef hlptim1;
void LPTIM1_Init(void) {
hlptim1.Instance = LPTIM1;
hlptim1.Init.Clock.Source = LPTIM_CLOCKSOURCE_APBCLOCK_LPOSC; // 使用LSI
hlptim1.Init.Clock.Prescaler = LPTIM_PRESCALER_DIV128; // LSI=32kHz, 分频后250Hz
hlptim1.Init.Trigger.Source = LPTIM_TRIGSOURCE_SOFTWARE;
HAL_LPTIM_Init(&hlptim1);
}
void LPTIM1_Start_Timeout(uint32_t timeout_ms) {
// 计算计数值:250Hz -> 每4ms计数1次
uint32_t count = (timeout_ms / 4);
HAL_LPTIM_TimeOut_Start_IT(&hlptim1, 0xFFFF, count);
}
void HAL_LPTIM_TimeOutCallback(LPTIM_HandleTypeDef *hlptim) {
// 定时器超时,唤醒系统或执行传感器采样
printf("LPTIM Wakeup from low-power mode.\r\n");
}
看门狗定时器(WDT)
特性:
- 用于监控系统运行状态,防止程序跑飞或陷入死循环。
- 需在超时前定期“喂狗”,否则将触发系统复位。
- 分为独立看门狗(IWDG)和窗口看门狗(WWDG)。
STM32 独立看门狗(IWDG)配置:
#include "stm32f4xx_hal.h"
IWDG_HandleTypeDef hiwdg;
void IWDG_Init(void) {
hiwdg.Instance = IWDG;
hiwdg.Init.Prescaler = IWDG_PRESCALER_64; // 预分频
hiwdg.Init.Reload = 625; // 重装载值: (625*64)/32kHz ≈ 1.25秒
HAL_IWDG_Init(&hiwdg);
}
void IWDG_Feed(void) {
HAL_IWDG_Refresh(&hiwdg); // “喂狗”,重置计数器
}
int main(void) {
IWDG_Init(); // 初始化看门狗
while (1) {
// 执行主要任务
do_critical_task();
// 在预期时间内喂狗,证明程序运行正常
IWDG_Feed();
HAL_Delay(500);
}
}
软件定时器原理与应用
RTOS软件定时器
特性:
- 基于RTOS的系统节拍(Tick)实现。
- 支持创建、启动、停止、修改周期、单次或周期性触发。
- 通过回调函数执行定时任务,简化了并发任务的管理。
FreeRTOS软件定时器示例:
#include "FreeRTOS.h"
#include "timers.h"
TimerHandle_t xPeriodicTimer, xOneShotTimer;
void vPeriodicTimerCallback(TimerHandle_t xTimer) {
printf("Periodic timer fired.\r\n");
// 执行周期性任务,如状态上报
}
void vOneShotTimerCallback(TimerHandle_t xTimer) {
printf("One-shot timer fired.\r\n");
// 执行一次性任务,如延时启动
}
void SoftwareTimer_Init(void) {
// 创建周期性定时器,周期1000ms
xPeriodicTimer = xTimerCreate(
"PeriodicTimer",
pdMS_TO_TICKS(1000),
pdTRUE, // 自动重载
(void *)0,
vPeriodicTimerCallback
);
// 创建单次定时器,5000ms后触发
xOneShotTimer = xTimerCreate(
"OneShotTimer",
pdMS_TO_TICKS(5000),
pdFALSE, // 单次触发
(void *)1,
vOneShotTimerCallback
);
if (xPeriodicTimer != NULL) xTimerStart(xPeriodicTimer, 0);
if (xOneShotTimer != NULL) xTimerStart(xOneShotTimer, 0);
}
裸机环境下的软件定时器
实现原理:利用一个硬件定时器(如SysTick)作为时基,通过软件管理多个定时器任务的状态。
#include <stdint.h>
#include <stdbool.h>
#define MAX_SOFT_TIMERS 8
typedef struct {
bool is_active;
bool is_periodic;
uint32_t countdown;
uint32_t reload_value;
void (*callback)(void);
} soft_timer_t;
static soft_timer_t timer_pool[MAX_SOFT_TIMERS];
static uint32_t sys_tick = 0;
// 在SysTick中断(1ms)中调用此函数
void SoftTimer_TickUpdate(void) {
sys_tick++;
for (int i = 0; i < MAX_SOFT_TIMERS; i++) {
if (timer_pool[i].is_active && timer_pool[i].countdown > 0) {
timer_pool[i].countdown--;
if (timer_pool[i].countdown == 0) {
if (timer_pool[i].callback) {
timer_pool[i].callback(); // 执行回调
}
if (timer_pool[i].is_periodic) {
timer_pool[i].countdown = timer_pool[i].reload_value; // 重载
} else {
timer_pool[i].is_active = false; // 单次定时器停止
}
}
}
}
}
// 创建并启动一个软件定时器
int SoftTimer_Start(uint32_t period_ms, bool periodic, void (*cb)(void)) {
for (int i = 0; i < MAX_SOFT_TIMERS; i++) {
if (!timer_pool[i].is_active) {
timer_pool[i].is_active = true;
timer_pool[i].is_periodic = periodic;
timer_pool[i].reload_value = period_ms;
timer_pool[i].countdown = period_ms;
timer_pool[i].callback = cb;
return i; // 返回定时器ID
}
}
return -1; // 创建失败
}
void SoftTimer_Stop(int id) {
if (id >= 0 && id < MAX_SOFT_TIMERS) {
timer_pool[id].is_active = false;
}
}
Linux系统定时器
timerfd(高精度定时器文件描述符)
特性:
- 将定时器抽象为文件描述符,可以方便地集成到
select/poll/epoll I/O多路复用机制中。
- 提供纳秒级的高精度定时。
- 是现代Linux应用中进行定时事件处理的推荐方式。
使用示例:
#include <sys/timerfd.h>
#include <sys/epoll.h>
#include <unistd.h>
#include <stdio.h>
#include <stdint.h>
int main() {
// 1. 创建timerfd
int tfd = timerfd_create(CLOCK_MONOTONIC, 0);
struct itimerspec its = {
.it_value = { .tv_sec = 1, .tv_nsec = 0 }, // 首次超时:1秒后
.it_interval = { .tv_sec = 1, .tv_nsec = 0 } // 后续周期:1秒
};
timerfd_settime(tfd, 0, &its, NULL);
// 2. 使用epoll监听timerfd
int efd = epoll_create1(0);
struct epoll_event ev = { .events = EPOLLIN, .data.fd = tfd };
epoll_ctl(efd, EPOLL_CTL_ADD, tfd, &ev);
struct epoll_event events[5];
while (1) {
int n = epoll_wait(efd, events, 5, -1);
for (int i = 0; i < n; i++) {
if (events[i].data.fd == tfd) {
uint64_t exp_cnt;
read(tfd, &exp_cnt, sizeof(exp_cnt)); // 必须读取,以清空事件
printf("Timer triggered. Count: %lu\n", exp_cnt);
// 执行定时任务...
}
}
}
close(tfd);
close(efd);
return 0;
}
传统的信号定时器 (alarm/setitimer)
特性:通过发送信号(如SIGALRM)来通知定时事件,实现简单但精度和灵活性不如timerfd。
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
void sigalrm_handler(int sig) {
printf("Received SIGALRM.\n");
}
int main() {
signal(SIGALRM, sigalrm_handler);
alarm(5); // 5秒后发送SIGALRM信号
pause(); // 挂起进程等待信号
return 0;
}
如何选择合适的定时器?
选择定时器时,需要综合考虑以下四个维度,可以参考以下决策流程:
- 精度要求:是否需要纳秒/微秒级(通用TIM),还是毫秒级(SysTick,软件定时器),或是秒级(RTC)即可?
- 功能需求:是否需要PWM、输入捕获等特殊功能?
- 运行环境:是在裸机、RTOS还是Linux系统下运行?
- 资源与功耗:可用定时器外设数量是否紧张?是否有严格的低功耗要求?
| 应用场景推荐表: |
应用场景 |
推荐定时器 |
关键理由 |
| RTOS任务调度与延时 |
SysTick |
内核内置,零额外外设消耗 |
| 电机控制、LED调光 |
通用TIM (PWM模式) |
硬件输出,精度高,性能稳定 |
| 测量脉冲频率/宽度 |
通用TIM (输入捕获) |
硬件直接测量,结果精确 |
| 为数据添加时间戳 |
RTC |
提供真实日历时间,掉电不丢失 |
| 低功耗设备周期性唤醒 |
LPTIM |
在深度睡眠模式下仍可工作 |
| RTOS内的周期性任务 |
RTOS软件定时器 |
管理方便,与任务系统集成度高 |
| Linux应用定时任务 |
timerfd |
精度高,可与I/O事件统一处理 |
| 防止系统死机 |
看门狗WDT |
硬件级监控,保障系统可靠性 |
总结
嵌入式系统中的定时器是构建稳定、可靠、高效应用的基础。从硬件的精准控制到软件层面的灵活调度,开发者需要根据具体的精度、功能、环境和功耗约束,在SysTick、通用TIM、RTC、WDT、LPTIM以及各类软件定时器之间做出合理选择。掌握其原理并熟练运用提供的代码框架,将能显著提升嵌入式开发中时间相关功能的实现效率与质量。