news 2026/7/12 3:20:19

软件定时器 |【裸机通用】基于 SysTick 实现轻量可移植 | 弱函数解耦 | 附完整可复用源码

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
软件定时器 |【裸机通用】基于 SysTick 实现轻量可移植 | 弱函数解耦 | 附完整可复用源码

1. 前言:为什么我们需要软件定时器

在嵌入式裸机开发中,硬件定时器资源稀缺是非常普遍的痛点。 一个常规单片机往往只有 2~4 个通用硬件定时器,却要同时承担 PWM 输出、输入捕获、延时、周期性任务调度等多种工作,资源分配捉襟见肘。

软件定时器(Software Timer)就是解决这个问题的最优方案之一:

  • 只占用 1 个硬件时基(通常是 SysTick 滴答定时器),就能虚拟出多路独立定时器
  • 纯 C 实现,零硬件依赖,所有带 SysTick 的 MCU(STM32、TI MSPM0、GD32 等)均可直接移植
  • 轻量无阻塞,适合 LED 闪烁、按键扫描、通信超时检测、状态轮询等绝大多数慢速定时场景

本文从零拆解一套工程级软件定时器的实现思路,包含SysTick 时基封装、定时器数组管理、weak 弱函数解耦三大核心模块,源码可直接复制进项目使用。

2. 核心原理:软件定时器是怎么工作的

2.1 硬件定时器 vs 软件定时器

对比维度硬件定时器软件定时器
实现方式芯片内部硬件电路纯软件逻辑模拟
数量稀缺,固定几个理论上可无限扩展
精度极高,可达 us 级取决于时基,通常 ms 级
CPU 占用几乎不占用中断内需遍历计数,占用少量 CPU
适用场景高精度时序、PWM、捕获周期任务、超时检测、状态轮询

2.2 核心工作机制

软件定时器的本质可以用一句话概括:

一个统一硬件心跳 + 多路独立计数器 + 到期回调执行

  1. 底层由SysTick 1ms 中断提供固定时基,作为全局心跳
  2. 每一路软件定时器维护独立的计数值和周期值
  3. 每进一次 SysTick 中断,所有已启用的定时器计数 + 1
  4. 当计数值达到设定周期时,触发对应回调函数,重置计数,实现循环定时

2.3 整体架构分层

本文实现采用三层架构,职责清晰、耦合度低:

  • 时基层:SysTick 驱动,提供 1ms 心跳、HAL_Delay 延时、统一 Tick 接口
  • 核心层:软件定时器管理,负责定时器注册、停止、计数、到期判断
  • 业务层:用户回调函数,通过弱符号实现驱动与业务解耦

3. 完整源码与逐行解析

3.1 时基层:SysTick 驱动封装

systick.h头文件:对外暴露初始化、获取 Tick、延时函数接口。

#ifndef __SYSTICK__ #define __SYSTICK__ #include "ti_msp_dl_config.h" //这里修改为对应芯片头文件包即可 #define HAL_MAX_DELAY 0xFFFFFFFFU /** * @brief 获取当前系统滴答计数(单位ms) * @retval 32位滴答值 */ uint32_t HAL_GetTick(void); /** * @brief 毫秒级阻塞延时 * @param[in] Delay 延时时长,单位ms */ void HAL_Delay(uint32_t Delay); /** * @brief SysTick中断初始化 */ void systick_init(void); #endif // __SYSTICK__

systick.c源文件:核心是 SysTick 中断服务函数,同时驱动软件定时器心跳。

#include "systick.h" static __IO uint32_t uwTick = 0; static const uint32_t uwTickFreq = 1U; // 声明软件定时器滴答处理函数 extern void softTimer_TickHandler(void); /** * @brief SysTick中断服务函数,1ms触发一次 * @note 软件定时器的心跳源头,必须保证1ms固定周期 */ void SysTick_Handler(void) { uwTick++; softTimer_TickHandler(); // 驱动所有软件定时器计数 } /** * @brief 获取当前系统滴答值 */ uint32_t HAL_GetTick(void) { return uwTick; } /** * @brief 初始化SysTick中断 */ void systick_init(void){ NVIC_ClearPendingIRQ(SysTick_IRQn); NVIC_EnableIRQ(SysTick_IRQn); } /** * @brief 毫秒级阻塞延时 * @note 基于SysTick实现,精度1ms */ void HAL_Delay(uint32_t Delay) { uint32_t tickstart = HAL_GetTick(); uint32_t wait = Delay; if (wait < HAL_MAX_DELAY) { wait += (uint32_t)(uwTickFreq); } while ((HAL_GetTick() - tickstart) < wait) { } }

设计要点:

  • 全局 Tick 变量加__IO修饰,防止编译器优化导致读取异常
  • 软件定时器的 Tick 处理直接放在 SysTick 中断内,保证计数精准
  • 兼容 HAL 库风格的 API 命名,熟悉 STM32 的开发者可以无缝上手

3.2 核心层:软件定时器实现

soft_timer.h头文件:对外 API、弱回调声明。

#ifndef __SOFT_TIMER_H #define __SOFT_TIMER_H #include <stdint.h> #include <stdbool.h> #include <__stddef_null.h> /* 对外API声明 */ void softTimer_init(void); bool softTimer_Register(uint8_t id, uint32_t period_ms, void (*callback)(void)); void softTimer_Stop(uint8_t id); /* 弱回调函数声明(用户可重写) */ void softTimer_0_Callback(void); void softTimer_1_Callback(void); void softTimer_2_Callback(void); void softTimer_3_Callback(void); void softTimer_4_Callback(void); void softTimer_5_Callback(void); void softTimer_6_Callback(void); void softTimer_7_Callback(void); #endif /* __SOFT_TIMER_H */

soft_timer.c源文件:结构体定义和核心逻辑全部实现。

#include "soft_timer.h" #define MAX_SOFT_TIMER 8 // 最大支持8路软件定时器 /** * @brief 软件定时器对象结构体 * @note 每一路定时器独立保存所有状态 */ typedef struct { uint8_t id; // 定时器ID uint8_t enable; // 使能标志:1启用 0停止 uint32_t period_ms; // 定时周期,单位ms uint32_t tick_cnt; // 当前计数值 void (*callback)(void); // 到期回调函数指针 }SoftTimer_TypeDef; // 定时器数组,static限定仅本文件访问,封装性更强 static SoftTimer_TypeDef soft_timer[MAX_SOFT_TIMER] = {0}; /** * @brief 软件定时器模块初始化 * @note 上电调用一次,清空所有定时器状态 */ void softTimer_init(void) { for(uint8_t i = 0; i < MAX_SOFT_TIMER; i++) { soft_timer[i].id = i; soft_timer[i].enable = 0; soft_timer[i].period_ms = 0; soft_timer[i].tick_cnt = 0; soft_timer[i].callback = NULL; } } /** * @brief 注册并启动一个软件定时器,重复调用相当于重新计时 * @param[in] id 定时器ID 0~7 * @param[in] period_ms 定时周期,单位ms,不可为0 * @param[in] callback 到期回调函数,不可为NULL * @retval true成功 false参数非法 */ bool softTimer_Register(uint8_t id, uint32_t period_ms, void (*callback)(void)) { // 参数合法性校验,防止越界和空指针 if(id >= MAX_SOFT_TIMER || period_ms == 0 || callback == NULL) { return false; } // 临时指针缓存地址,减少重复寻址计算 SoftTimer_TypeDef *p_timer = &soft_timer[id]; p_timer->enable = 1; p_timer->tick_cnt = 0; p_timer->period_ms = period_ms; p_timer->callback = callback; return true; } /** * @brief 停止指定ID的定时器,并清零计数 * @param[in] id 目标定时器ID */ void softTimer_Stop(uint8_t id) { if (id < MAX_SOFT_TIMER) { SoftTimer_TypeDef *p_timer = &soft_timer[id]; p_timer->enable = 0; p_timer->tick_cnt = 0; } } /** * @brief 软件定时器滴答处理函数 * @note 必须在SysTick 1ms中断中调用,是整个模块的心脏 */ void softTimer_TickHandler(void) { for( uint8_t i = 0; i < MAX_SOFT_TIMER; i++) { SoftTimer_TypeDef *p_timer = &soft_timer[i]; // 只处理已启用的定时器 if(p_timer->enable) { p_timer->tick_cnt++; // 计数达到周期,执行回调并重置 if(p_timer->tick_cnt >= p_timer->period_ms) { p_timer->callback(); p_timer->tick_cnt = 0; } } } } /** * @brief 定时器0~7 默认弱回调 * @note __attribute__((weak)) 弱符号,用户可在业务层重写覆盖 */ __attribute__((weak)) void softTimer_0_Callback(void) {} __attribute__((weak)) void softTimer_1_Callback(void) {} __attribute__((weak)) void softTimer_2_Callback(void) {} __attribute__((weak)) void softTimer_3_Callback(void) {} __attribute__((weak)) void softTimer_4_Callback(void) {} __attribute__((weak)) void softTimer_5_Callback(void) {} __attribute__((weak)) void softTimer_6_Callback(void) {} __attribute__((weak)) void softTimer_7_Callback(void) {}

核心设计细节讲解:

  1. static 数组封装定时器数组加 static 修饰,仅本文件可访问,避免外部非法篡改内部状态,符合高内聚低耦合的工程规范。

  2. 参数前置校验注册函数先判断 ID 越界、周期为 0、空指针,从源头杜绝数组越界、野指针等致命问题,提升代码健壮性。

  3. 临时指针优化SoftTimer_TypeDef *p_timer = &soft_timer[id];

  • 直接数组下标访问,每一行都要执行「基地址 + 下标 × 结构体大小」的地址计算
  • 临时指针只计算 1 次地址,后续成员访问直接解引用,减少重复运算
  • 在 1ms 高频中断的 TickHandler 中,能有效降低 CPU 开销,中断快进快出

4.弱函数工作原理:

  • 弱符号(Weak Symbol)是链接阶段属性:驱动层提供默认空实现
  • 如果用户在业务代码中定义了同名普通函数(强符号),链接器会自动用强符号覆盖弱符号
  • 用户不重写也不会报错,默认执行空函数,不影响程序运行

优势:驱动与业务完全解耦,新增定时器业务逻辑不需要修改驱动源码,只需要在 main.c 或业务文件中重写回调即可,移植性和可维护性大幅提升,也是 STM32 HAL 库的通用设计思路。

注意:__attribute__((weak))是 GCC/CCS 编译器语法,Keil MDK 可替换为__weak,读者可自行封装跨平台宏提升兼容性。

4. 三步快速上手使用

步骤 1:移植文件到工程

systick.hsystick.csoft_timer.hsoft_timer.c加入工程,确保 SysTick 已配置为 1ms 中断。

步骤 2:初始化与注册

在 main 函数中完成初始化,注册需要的定时器:

#include "systick.h" #include "soft_timer.h" int main(void) { // 系统时钟、外设初始化... systick_init(); softTimer_init(); // 注册定时器0:500ms周期定时 softTimer_Register(0, 500, softTimer_0_Callback); // 注册定时器1:1000ms周期定时 softTimer_Register(1, 1000, softTimer_1_Callback); while(1) { // 主循环业务逻辑 } }

步骤 3:重写回调执行业务

在业务代码中重写对应 ID 的回调函数,推荐只在回调中置标志位,业务逻辑放主循环执行,避免阻塞中断。

// 全局标志位 volatile uint8_t led_flip_flag = 0; volatile uint8_t key_scan_flag = 0; // 重写定时器0回调:500ms翻转LED void softTimer_0_Callback(void) { led_flip_flag = 1; } // 重写定时器1回调:1s按键扫描 void softTimer_1_Callback(void) { key_scan_flag = 1; // 如需单次定时,在这里调用 softTimer_Stop(1); 即可 }

5. 避坑指南:

  1. 回调函数禁止耗时操作TickHandler 运行在中断上下文,回调里不能写 HAL_Delay、串口打印、大量运算,否则会阻塞 SysTick 中断,导致所有定时器精度下降,甚至系统崩溃。 正确做法:回调只置标志位,业务逻辑放主循环。

  2. 定时器数量并非越多越好每增加一路定时器,中断内就多一次判断和计数。8 路以内对系统几乎无影响,超过 32 路建议优化为时间轮算法,降低遍历开销。

  3. 弱函数的常见错误

  • ❌ 头文件中加 weak 修饰:导致每个包含头文件的.c 都生成弱符号,多重定义
  • ❌ 用户重写也加 weak:两个弱符号,链接器随机选择,逻辑异常
  • ✅ 正确做法:weak 只加在.c 的默认实现上,用户重写不加任何修

4.精度误差说明软件定时器最小精度为 1ms,且存在 ±1tick 的误差,不适合高精度时序场景;us 级精准时序请使用硬件定时器。

5. 思考: 如果想让颗粒度变成 秒(s)级, 只需把RTC配置为1s中断周期,并把软件定时器TickHandler放入RTC中断回调函数即可

6. 总结

这套软件定时器是裸机开发中性价比极高的通用组件,代码量小、移植简单、解耦规范,能够解决绝大多数项目中硬件定时器不足的问题。 从 SysTick 时基到弱回调设计,完整遵循了嵌入式驱动的工程化规范,建议收藏备用,新项目直接复制复用即可。

如果有优化建议或使用问题,欢迎在评论区交流讨论。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/12 3:20:18

如何用Anime.js打造流畅动画:从基础到高级的完整指南

如何用Anime.js打造流畅动画&#xff1a;从基础到高级的完整指南 【免费下载链接】anime JavaScript animation engine 项目地址: https://gitcode.com/GitHub_Trending/an/anime Anime.js是一款功能强大的JavaScript动画引擎&#xff0c;专为现代Web开发设计&#xff0…

作者头像 李华
网站建设 2026/7/12 3:19:20

C++ Lambda表达式:从语法到实战,掌握现代C++核心编程范式

1. 项目概述&#xff1a;为什么我们需要Lambda表达式&#xff1f;干了这么多年C&#xff0c;从C98/03一路走到C17/20&#xff0c;我越来越觉得Lambda表达式是每个C开发者必须熟练掌握的“瑞士军刀”。它不仅仅是一个语法糖&#xff0c;更是一种编程范式的转变。回想早期写STL算…

作者头像 李华
网站建设 2026/7/12 3:19:20

闻汛而动 向险而行!猎鹰特卫救援队驰援广西抗洪救灾

近期&#xff0c;受台风“美莎克”与季风气流叠加影响&#xff0c;广西横州市遭遇持续性强降雨&#xff0c;引发山洪、城市内涝等严重洪涝灾害。辖区多处村庄被淹、道路损毁&#xff0c;部分区域断水断电&#xff0c;群众安置压力大、生活物资紧缺&#xff0c;灾后防疫和家园重…

作者头像 李华
网站建设 2026/7/12 3:17:53

SEO 是什么意思?

在前端开发中&#xff0c;我们经常听到 "HTML 的 lang 属性有助于 SEO" 这样的说法。SEO 到底是什么意思&#xff1f;lang 属性又是如何对 SEO 产生积极影响的&#xff1f;本文将为你详细解答。一、SEO 是什么&#xff1f;SEO 是 Search Engine Optimization&#xf…

作者头像 李华
网站建设 2026/7/12 3:17:49

AI 率高会影响毕业吗?怎么在送检前降到安全线

AI 率高会影响毕业吗&#xff1f;怎么在送检前降到安全线 你现在大概是刚查完初稿&#xff0c;看到 AI 率那个数字整个人凉了半截。可能是 60%&#xff0c;也可能是 90% 以上&#xff0c;红彤彤一片&#xff0c;然后脑子里第一个念头就是&#xff1a;这会不会直接卡住我毕业&a…

作者头像 李华