当前位置: 首页 > article >正文

嵌入式软件定时器原理与实现:从硬件限制到多任务调度

1. 软件定时器从硬件限制到软件自由的桥梁在嵌入式开发里定时器是个绕不开的话题。无论是让LED灯定时闪烁还是需要周期性地采集传感器数据甚至是实现一个简单的按键消抖都离不开定时功能。硬件定时器Timer是芯片提供的外设数量有限比如STM32F103系列高级定时器和通用定时器加起来也就那么几个。当你手头的项目需要十几个甚至几十个独立计时任务时硬件资源立刻捉襟见肘。这时候软件定时器Software Timer就闪亮登场了。简单来说软件定时器就是用代码“模拟”出来的定时器。它本身不是一个物理外设而是通过程序逻辑基于一个基准的硬件时钟源构建出的一套可以管理多个定时任务的机制。它的核心价值在于“以一当百”你可以用一个硬件定时器比如系统滴答定时器SysTick作为“心跳”衍生出成百上千个独立的软件定时器每个都可以设置不同的超时时间执行不同的任务。这彻底打破了硬件资源的瓶颈为复杂的定时调度需求提供了可能。当然天下没有免费的午餐。软件定时器的“软”也带来了其固有的特点它的精度依赖于基准硬件时钟和主循环的扫描频率通常不如硬件定时器中断那样精准和及时同时维护这些定时器状态、检查是否超时都需要消耗CPU的计算资源。因此它通常用于对时间精度要求不是极端苛刻比如毫秒级即可但需要大量、灵活定时任务的场景比如协议解析超时、状态机轮询、非实时性的设备状态更新等。理解软件定时器的原理和实现是嵌入式开发者从“会用库函数”到“理解系统调度”的关键一步。它不仅是RTOS实时操作系统中一个核心组件的基础也是许多复杂裸机程序实现模块化、事件驱动的重要工具。接下来我们就深入其内部看看这个“时间魔术”是如何实现的。2. 软件定时器的核心原理与设计思路2.1 核心思想一个心跳万千计时软件定时器的基本原理可以类比为一个中央时钟和无数个私人闹钟。那个中央时钟就是一个高精度、不间断运行的硬件定时器它每隔一个固定的时间间隔比如1毫秒就产生一次中断我们称之为一个“时钟节拍”Tick。这个节拍是整个系统的时间基准就像秒针的每一次“滴答”。在这个基础上我们维护一个全局的、不断递增的“时间戳”变量比如tick_counter。每次硬件定时器中断发生就在中断服务程序里对这个tick_counter加1。这样tick_counter的值就代表了系统从启动到现在所经过的“节拍”数。现在每个软件定时器都可以被看作是一个独立的“闹钟”。当你启动一个软件定时器时你会为它设定一个“响铃时间”。这个时间不是绝对的真实时间而是基于tick_counter的一个未来值。例如当前tick_counter 1000你需要一个500毫秒后超时的定时器而你的一个节拍是1毫秒那么你就将这个定时器的“到期时间”match_time设置为1000 500 1500。那么如何知道闹钟该响了呢这需要一个“查岗”的过程。在你的主程序循环或者一个专门的低优先级任务中你需要定期地、轮询地检查所有已启动的软件定时器。检查的逻辑非常简单将每个定时器保存的match_time与当前的tick_counter进行比较。一旦发现tick_counter match_time就说明这个定时器到期了该执行它预设的任务了。注意这里使用“大于等于”进行比较而不是“等于”是一个重要的容错设计。因为检查操作可能因为CPU忙于处理其他更高优先级的中断或任务而被延迟导致检查时tick_counter已经略超过了match_time。使用“大于等于”可以确保不会因为微小的延迟而错过定时事件。2.2 两种基本模式单次与周期根据超时后的行为软件定时器通常有两种工作模式单次模式One-Shot定时器到期并执行完预设的回调函数后自动进入停止状态。就像闹钟响完一次就关了需要再次手动设置才会启动。适用于只需要执行一次的动作比如延时开启某个设备、单次数据包发送超时检测等。周期模式Periodic定时器到期并执行回调函数后会自动以相同的间隔重新启动。就像一个每隔固定时间就响一次的循环闹钟。实现原理是在判断定时器到期后不是将其停止而是将其match_time在原有值的基础上再加上一个周期值period。这样下一次检查时当tick_counter再次增长到这个新的match_time时它又会触发。这种模式非常适合需要持续、定期执行的任务如LED心跳灯、传感器定期采集、系统状态监控等。2.3 关键数据结构如何组织这些“闹钟”如何高效地管理和检索这成百上千个“闹钟”是软件定时器设计的核心。这主要涉及到两个层面的数据结构一是单个定时器如何描述二是多个定时器如何组织。单个定时器结构体这定义了每个定时器的“身份证”和“任务清单”。一个典型的定义如下typedef struct { uint8_t state; // 状态停止、运行、超时 uint8_t mode; // 模式单次、周期 uint32_t match_time; // 到期时间基于tick_counter uint32_t period; // 定时周期仅周期模式有效 void (*callback)(void); // 到期后要执行的函数指针 void *arg; // 传递给回调函数的参数 } soft_timer_t;state和mode控制了定时器的生命周期和行为逻辑。match_time是判断是否到期的依据。callback是定时器存在的意义——到期后要做什么。通过函数指针我们可以将任何函数绑定到定时器上实现极高的灵活性。arg允许我们向回调函数传递参数使得同一个回调函数可以处理不同定时器的不同任务。多个定时器的组织方式这决定了定时器的管理效率和资源消耗。主要有两种经典方法数组Array在编译时就创建一个固定大小的定时器数组如soft_timer_t timer_list[MAX_TIMER_NUM];。优点访问速度快O(1)内存地址连续缓存友好实现简单。缺点数量固定不够灵活。设大了浪费内存设小了可能不够用。适用于定时器数量明确且稳定的系统。链表Linked List动态创建定时器节点并通过指针链接起来。优点数量动态可变需要时创建不用时释放内存利用率高。缺点查找、插入、删除需要遍历链表平均O(n)时间开销相对大频繁创建删除容易导致内存碎片。这是像FreeRTOS、Linux等通用系统更常采用的方式因为它提供了最大的灵活性。对于初学者或资源受限、需求明确的裸机系统从数组结构入手是更直观、更稳妥的选择。它能让你更专注于理解定时器本身的逻辑而不是复杂的内存管理。3. 从零实现一个数组式软件定时器理解了原理我们动手实现一个基于数组的、功能完整的软件定时器模块。我们将它分为几个核心部分时钟节拍管理、定时器容器与结构定义、以及核心的操作函数初始化、启动、更新、停止。3.1 建立系统心跳时钟节拍软件定时器需要一个可靠且单调递增的时间基准。我们通常选择一个硬件定时器如SysTick或者一个普通的通用定时器来产生固定频率的中断。// software_timer.c #include “software_timer.h” // 定义时钟节拍计数器使用volatile防止编译器优化 volatile static uint32_t g_tick_count 0; /** * brief 获取当前的系统节拍数 * retval 当前的tick计数值 */ uint32_t get_tick_count(void) { return g_tick_count; } /** * brief 时钟节拍更新函数 * note 该函数必须放在硬件定时器的中断服务程序(ISR)中调用。 * 例如如果硬件定时器配置为1ms中断一次则此函数每1ms被调用一次。 */ void tick_increment(void) { g_tick_count; }在硬件定时器的中断服务函数里我们只需要做一件事调用tick_increment()。// 假设使用STM32的TIM4配置为1ms中断 void TIM4_IRQHandler(void) { if (TIM_GetITStatus(TIM4, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM4, TIM_IT_Update); tick_increment(); // 核心增加全局节拍 } }实操心得g_tick_count必须声明为volatile。因为这个变量会在中断tick_increment中被修改在主循环get_tick_count和定时器检查中被读取。volatile关键字告诉编译器不要对这个变量进行激进的优化比如缓存到寄存器确保每次读取都能拿到内存中最新的值。这是嵌入式编程中处理在中断和主程序间共享变量的一个关键点。3.2 定义定时器容器与状态接下来我们定义软件定时器的结构体和用于存储它们的数组。// software_timer.h #ifndef __SOFTWARE_TIMER_H #define __SOFTWARE_TIMER_H #include stdint.h #include stdbool.h // 定时器状态枚举 typedef enum { TIMER_STATE_STOPPED 0, // 停止状态 TIMER_STATE_RUNNING, // 运行状态 TIMER_STATE_TIMEOUT // 超时状态已到期等待处理或重新装载 } timer_state_t; // 定时器模式枚举 typedef enum { TIMER_MODE_ONE_SHOT 0, // 单次模式 TIMER_MODE_PERIODIC // 周期模式 } timer_mode_t; // 定时器回调函数类型定义 typedef void (*timer_callback_t)(void *arg); // 单个软件定时器结构体 typedef struct { timer_state_t state; // 当前状态 timer_mode_t mode; // 工作模式 uint32_t match_tick; // 到期时间点tick值 uint32_t period_ticks; // 周期时长tick数 timer_callback_t callback; // 超时回调函数指针 void *arg; // 回调函数参数 } soft_timer_t; // 软件定时器模块初始化 void software_timer_init(void); // 启动一个定时器 bool software_timer_start(uint8_t id, timer_mode_t mode, uint32_t delay_ms, timer_callback_t cb, void *arg); // 停止一个定时器 void software_timer_stop(uint8_t id); // 定时器状态更新函数需在主循环中定期调用 void software_timer_update(void); // 获取定时器状态 timer_state_t software_timer_get_state(uint8_t id); #endif /* __SOFTWARE_TIMER_H */// software_timer.c (续) // 定义最大定时器数量 #define MAX_SOFTWARE_TIMERS 10 // 软件定时器对象数组 static soft_timer_t g_timer_list[MAX_SOFTWARE_TIMERS]; /** * brief 软件定时器模块初始化 * note 上电后必须调用一次清零所有定时器状态。 */ void software_timer_init(void) { for (int i 0; i MAX_SOFTWARE_TIMERS; i) { g_timer_list[i].state TIMER_STATE_STOPPED; g_timer_list[i].callback NULL; g_timer_list[i].arg NULL; // 其他字段在start时赋值这里可以不做初始化但保持习惯是好的 } }这里我们创建了一个包含10个定时器的静态数组g_timer_list。初始化函数将所有定时器置于停止状态并将回调函数指针置空。MAX_SOFTWARE_TIMERS可以根据你的具体需求调整。3.3 核心操作函数实现3.3.1 启动定时器设定你的闹钟启动函数是配置一个定时器的入口。它需要指定用哪个定时器ID、什么模式、多久后触发、触发后执行什么函数、以及给这个函数传递什么参数。/** * brief 启动一个软件定时器 * param id: 定时器ID范围 0 ~ (MAX_SOFTWARE_TIMERS-1) * param mode: 定时器模式单次或周期 * param delay_ms: 定时时长单位毫秒 * param cb: 超时回调函数指针 * param arg: 传递给回调函数的参数 * retval true: 启动成功, false: 启动失败ID无效或定时器已在运行 */ bool software_timer_start(uint8_t id, timer_mode_t mode, uint32_t delay_ms, timer_callback_t cb, void *arg) { // 1. 参数检查 if (id MAX_SOFTWARE_TIMERS) { return false; // ID越界 } if (g_timer_list[id].state TIMER_STATE_RUNNING) { // 可选你也可以设计为强制重启这里选择返回错误 return false; // 该定时器正在运行 } if (cb NULL) { // 没有回调函数的定时器虽然可以存在但通常没有意义这里作为错误处理 return false; } // 2. 计算到期时间点 // 注意get_tick_count() 返回的是节拍数我们需要将毫秒转换为节拍数。 // 假设 1 tick 1ms。如果你的tick不是1ms需要做转换。 uint32_t current_tick get_tick_count(); uint32_t delay_ticks delay_ms; // 此处为简化1ms1tick。实际情况需换算。 // 3. 配置定时器结构体 g_timer_list[id].match_tick current_tick delay_ticks; g_timer_list[id].period_ticks delay_ticks; // 周期值等于首次延时值 g_timer_list[id].mode mode; g_timer_list[id].callback cb; g_timer_list[id].arg arg; g_timer_list[id].state TIMER_STATE_RUNNING; return true; }关键点解析match_tick current_tick delay_ticks是软件定时器的灵魂。它把未来的一个绝对时间点以tick为单位记录下来作为触发判决的依据。period_ticks被保存下来是为了在周期模式下计算下一次的触发时间点。3.3.2 更新与检查让闹钟走起来这是软件定时器的“发动机”必须被周期性地调用通常放在主循环while(1)中它负责检查所有运行中的定时器是否到期并执行相应的动作。/** * brief 软件定时器状态更新函数 * note 此函数必须在主循环中尽可能频繁地调用以保证定时精度。 * 调用的时间间隔决定了定时器能分辨的最小时间单位。 */ void software_timer_update(void) { uint32_t current_tick get_tick_count(); for (int i 0; i MAX_SOFTWARE_TIMERS; i) { // 只处理处于运行状态的定时器 if (g_timer_list[i].state ! TIMER_STATE_RUNNING) { continue; } // 检查是否到期当前时间 预设的到期时间 // 注意这里使用“”而非“”是考虑到update函数可能被延迟执行 if (current_tick g_timer_list[i].match_tick) { // 1. 标记为超时状态可选便于外部查询 g_timer_list[i].state TIMER_STATE_TIMEOUT; // 2. 执行回调函数 if (g_timer_list[i].callback ! NULL) { g_timer_list[i].callback(g_timer_list[i].arg); } // 3. 根据模式处理定时器后续状态 if (g_timer_list[i].mode TIMER_MODE_PERIODIC) { // 周期模式重新计算下一次到期时间并恢复运行状态 // 注意不是简单地在原match_tick上加period而是考虑可能的时间漂移 // 使用 while 循环确保 match_tick 严格大于 current_tick防止因回调函数执行时间过长导致连续触发 while (current_tick g_timer_list[i].match_tick) { g_timer_list[i].match_tick g_timer_list[i].period_ticks; } g_timer_list[i].state TIMER_STATE_RUNNING; } else { // 单次模式执行完毕后自动停止 g_timer_list[i].state TIMER_STATE_STOPPED; // 可以在这里选择性地清空调用函数指针和参数避免误用 // g_timer_list[i].callback NULL; // g_timer_list[i].arg NULL; } } } }避坑指南更新函数中的while循环是处理周期定时器时间累积误差的一个小技巧。想象一下如果回调函数执行时间很长或者主循环因故被阻塞了很久导致current_tick已经远超match_tick不止一个周期。简单的match_tick period一次可能仍然小于current_tick这会导致在同一个update调用中这个定时器被误判为再次到期从而“追补”执行多次回调函数这通常不是我们想要的。while循环确保将match_tick增加到大于当前时间从而只触发一次并让后续的定时节奏与系统时钟重新对齐而不是与理论上的绝对时间对齐。这被称为“相对定时”而非“绝对定时”在软件定时器中更为常用和稳健。3.3.3 停止与状态查询这两个函数比较简单但提供了对定时器生命周期的外部控制。/** * brief 停止一个正在运行的软件定时器 * param id: 定时器ID */ void software_timer_stop(uint8_t id) { if (id MAX_SOFTWARE_TIMERS) { return; // 简单处理也可用assert } g_timer_list[id].state TIMER_STATE_STOPPED; } /** * brief 获取指定定时器的当前状态 * param id: 定时器ID * retval 定时器的状态 */ timer_state_t software_timer_get_state(uint8_t id) { if (id MAX_SOFTWARE_TIMERS) { return TIMER_STATE_STOPPED; // 无效ID返回停止状态 } return g_timer_list[id].state; }4. 实战应用三种典型场景测试理论说得再多不如一行代码。我们用一个具体的例子在STM32的裸机环境下测试我们刚刚实现的软件定时器模块。我们将创建三个定时器分别演示单次触发、周期触发以及结合状态查询的用法。4.1 硬件与软件准备假设我们有一个STM32开发板已经配置好了一个USART1用于打印信息波特率115200。一个定时器如TIM4配置为1ms产生一次中断并在其中调用tick_increment()。两个LED灯LED0和LED1对应的GPIO已初始化为推挽输出。4.2 定义定时器ID与回调函数首先我们为要用的定时器分配ID并编写它们到期时需要执行的回调函数。// main.c #include “stm32f10x.h” #include “software_timer.h” #include “stdio.h” // 用于printf // 定义定时器ID方便管理 #define TIMER_PRINT_MSG 0 #define TIMER_LED0_FLASH 1 #define TIMER_LED1_DELAY_ON 2 // LED控制函数假设已实现 extern void LED0_Toggle(void); extern void LED1_On(void); extern void LED1_Off(void); // 回调函数1在串口打印一段信息 void print_message_callback(void *arg) { // arg 被我们用来传递一个字符串 char *msg (char *)arg; printf(“[Timer] %s\\r\\n”, msg); } // 回调函数2翻转LED0状态 void toggle_led0_callback(void *arg) { (void)arg; // 未使用参数消除编译器警告 LED0_Toggle(); } // 回调函数3空函数什么也不做 void nop_callback(void *arg) { (void)arg; // 未使用参数 // intentionally do nothing }4.3 主程序逻辑在主函数中我们初始化所有硬件和软件模块然后启动三个定时器最后在主循环中不断调用更新函数。int main(void) { // 硬件初始化 USART1_Init(115200); TIM4_Init(1); // 初始化1ms定时器 LED_Init(); // 软件定时器模块初始化 software_timer_init(); printf(“System Started. Software Timer Demo Begin.\\r\\n”); // 启动定时器1单次1秒后打印消息 char *hello_msg “Hello from Timer 0 after 1 second!”; software_timer_start(TIMER_PRINT_MSG, TIMER_MODE_ONE_SHOT, 1000, // 1000ms 1s print_message_callback, (void *)hello_msg); // 启动定时器2周期500ms周期让LED0闪烁 software_timer_start(TIMER_LED0_FLASH, TIMER_MODE_PERIODIC, 500, // 500ms周期 toggle_led0_callback, NULL); // 此回调不需要参数 // 启动定时器3单次3秒后其状态会变为TIMEOUT我们在主循环中检测并点亮LED1 software_timer_start(TIMER_LED1_DELAY_ON, TIMER_MODE_ONE_SHOT, 3000, // 3000ms 3s nop_callback, // 回调函数为空 NULL); // 主循环 while (1) { // 核心必须不断调用更新函数以检查并处理到期定时器 software_timer_update(); // 演示通过查询定时器状态来执行任务而非通过回调函数 if (software_timer_get_state(TIMER_LED1_DELAY_ON) TIMER_STATE_TIMEOUT) { LED1_On(); // 任务完成后可以将定时器状态重置为STOPPED避免重复执行 // software_timer_stop(TIMER_LED1_DELAY_ON); } // 这里可以执行其他低优先级任务... // 例如按键扫描、非紧急通信处理等 } }4.4 代码运行逻辑与现象分析系统启动初始化后三个定时器被启动开始计时。主循环software_timer_update()被高频调用可能每几微秒或几十微秒一次取决于循环内其他任务的耗时。定时器0单次打印启动时match_tick current_tick 1000。随着tick_increment()每1ms被中断调用一次current_tick不断增加。大约1秒后在某个update()调用中条件current_tick match_tick成立。状态被设为TIMEOUT并执行print_message_callback串口打印出预设消息。因为是单次模式随后状态被设为STOPPED该定时器生命周期结束。定时器1周期LED闪烁启动逻辑同上period_ticks 500。第一次500ms到期后执行toggle_led0_callbackLED0状态翻转。因为是周期模式代码进入while循环将match_tick增加500直到其大于current_tick然后状态恢复为RUNNING。此后每过500ms都会触发一次翻转LED0开始规律闪烁。定时器2状态查询点亮LED1启动3秒定时回调函数为空。3秒后在update()中它被标记为TIMEOUT。由于回调函数为空没有立即执行任何动作。在主循环的if判断中software_timer_get_state()查询到其状态为TIMEOUT于是执行LED1_On()点亮LED1。这种方式将“定时触发”和“任务执行”解耦适用于那些不希望或不能在回调函数上下文中执行的任务比如任务本身比较耗时或者需要访问某些在主循环上下文中才安全的资源。通过这个测试我们验证了软件定时器的单次触发、周期触发以及状态查询三种基本用法它们覆盖了绝大多数应用场景。5. 进阶探讨与避坑实战指南实现一个能跑起来的软件定时器只是第一步。在实际项目中应用时你会遇到各种边界情况和性能问题。下面分享一些从实际项目中总结出来的经验和避坑点。5.1 精度问题你的定时器到底有多准软件定时器的精度受限于两个主要因素时钟节拍Tick的精度这是基础。如果你的硬件定时器配置为1ms中断一次那么理论上你的软件定时器最小分辨力就是1ms误差也在±1ms以内。如果你需要更高精度比如100us就需要将硬件定时器中断频率提高到10kHz。但这会带来更频繁的中断增加CPU开销。update()函数的调用频率这是关键。即使你的tick是精确的1ms如果主循环被一个耗时很长的任务阻塞了50ms那么update()函数在这50ms内就无法被调用。所有在这期间到期的定时器其回调函数的执行都会被延迟直到update()再次被调用。软件定时器的超时是“被检测到”的而不是“主动发生”的。提升精度的实践建议确保update()调用频率远高于最快定时器的频率。例如你最快的定时器是10ms周期那么最好保证update()的调用间隔在1-2ms以内。将update()放在主循环中尽可能靠前、无阻塞的位置。避免放在可能被长时间阻塞的函数如某些等待式串口接收之后。对于精度要求极高的单个任务仍然要使用硬件定时器中断。软件定时器更适合对实时性要求不苛刻的批量任务管理。5.2 回调函数的设计禁忌回调函数是在update()的上下文中被调用的而update()通常在主循环中运行。这意味着回调函数应尽可能短小精悍。避免在回调函数中进行长时间循环、延时等待或复杂的计算。长时间的回调会阻塞主循环导致其他定时器甚至整个系统的响应性变差。避免在回调函数中调用可能阻塞或耗时很长的系统函数。注意重入问题如果你的回调函数可能被更高优先级的中断打断并且该中断也尝试操作共享资源就需要考虑使用临界区保护或信号量。一个良好的实践是回调函数只做标记或发送消息。例如在一个按键扫描的定时器中回调函数只是设置一个flag_key_scan_request true而实际的按键扫描动作放在主循环中根据这个标志位来执行。这符合“快进快出”的中断/事件处理原则。5.3 定时器ID管理与资源分配我们使用的是静态数组ID需要手动管理。在复杂系统中这容易出错。建议使用枚举或宏集中定义所有ID如上文的TIMER_PRINT_MSG避免在代码中直接使用数字0、1、2。在software_timer_start中增加更严格的状态检查。比如可以设计为允许对已运行的定时器调用start来重新配置重启这有时比先stop再start更方便。实现一个software_timer_find_free()函数用于动态查找一个空闲的定时器ID并返回。这可以简化调用但需要遍历数组有轻微开销。5.4 时间溢出的处理tick_counter和match_tick都是32位无符号整数。以1ms为tick这个计数器大约会在2^32 / 1000 / 3600 / 24 ≈ 49.7天后溢出归零。这会导致一个潜在问题如果在一个溢出点附近设置一个很长的定时计算match_tick current_tick delay可能会发生环绕overflow使得比较逻辑失效。解决方案无符号数自然溢出特性 对于无符号整数a和b即使发生了溢出表达式(a - b) delay在大多数情况下能正确判断a是否在b之后的delay时间内。但更通用的方法是使用“时间差”比较。修改到期判断逻辑// 更健壮的到期判断可处理计数器溢出 static inline bool is_timer_expired(uint32_t current_tick, uint32_t match_tick, uint32_t period_ticks) { // 计算从 match_tick 到 current_tick 经过的“时间差”考虑溢出 // 如果 current_tick - match_tick 的差值以2^32为模小于一个非常大的数如0x7FFFFFFF // 我们可以认为 current_tick “到达或超过了” match_tick。 // 一个简单且足够好的方法是直接使用无符号减法。 // 如果 current_tick match_tick则 (current_tick - match_tick) 是一个正常的小数值。 // 如果 current_tick match_tick由于溢出导致则 (current_tick - match_tick) 会变成一个很大的数高位溢出。 // 因此判断 (current_tick - match_tick) 0x80000000 可以安全地判定是否“到期”。 // 但更简单且正确的做法是判断时间差是否小于定时周期的一半不对于单次定时器这不适用。 // 实际上在嵌入式系统运行49天不重启的情况下对于大多数定时任务秒、分钟级 // 直接使用 current_tick match_tick 在溢出后的一小段时间内会出现误判但很快会恢复正常。 // 一个经典的、安全的比较方法是 return ((int32_t)(current_tick - match_tick)) 0; }这段代码利用了有符号整数的溢出定义。将无符号差转换为有符号数后如果差值为负即发生了溢出且current在match之前则条件为假如果差值为正或零则条件为真。这种方法在标准的补码机器上是可移植的。在我们的update函数中可以将判断条件改为if (is_timer_expired(current_tick, g_timer_list[i].match_tick, 0)) { ... }5.5 与RTOS的软件定时器对比像FreeRTOS这样的RTOS也提供了软件定时器服务。它们通常更强大基于任务实现RTOS的软件定时器通常由一个独立的、低优先级的“守护任务”Daemon Task来管理其回调函数在该任务的上下文中执行而不是在update调用者的上下文中。这更安全但引入了任务切换开销。功能丰富提供API如xTimerCreate,xTimerStart,xTimerStop,xTimerReset等功能更完善。动态创建通常是链表实现可以动态创建和删除。我们实现的这个裸机版本可以看作是一个简化版的、同步的回调在调用者上下文执行软件定时器。它更轻量没有任务调度开销适合在简单的裸机系统或对实时性要求较高的场合使用。理解了这个裸机版本的原理再去学习RTOS的定时器你会觉得豁然开朗。最后软件定时器是一个极其有用的工具它将时间管理抽象化让开发者能更专注于业务逻辑。从简单的延时闪烁LED到复杂的多任务协议调度背后都可能有着它的身影。掌握其原理并亲手实现一个是嵌入式程序员一项非常宝贵的技能。

相关文章:

嵌入式软件定时器原理与实现:从硬件限制到多任务调度

1. 软件定时器:从硬件限制到软件自由的桥梁在嵌入式开发里,定时器是个绕不开的话题。无论是让LED灯定时闪烁,还是需要周期性地采集传感器数据,甚至是实现一个简单的按键消抖,都离不开定时功能。硬件定时器(…...

基于Trinket与NeoPixel的声控LED色彩风琴制作全攻略

1. 项目概述:让声音驱动光效色彩风琴,一个听起来有些复古的名字,在七八十年代的迪斯科舞厅和家庭派对上,它曾是营造氛围的明星。本质上,它就是一个声控灯光系统,能够将音乐的节奏和强度实时转化为绚丽的光影…...

如何通过编译优化与隐私增强实现浏览器性能飞跃:Thorium项目技术深度解析

如何通过编译优化与隐私增强实现浏览器性能飞跃:Thorium项目技术深度解析 【免费下载链接】thorium Chromium fork named after radioactive element No. 90. Source code and Linux releases. Windows/MacOS/ARM builds served in different repos, links are towa…...

Netduino Plus 2硬实时驱动WS2812:托管环境下的纳秒级GPIO控制实战

1. 项目概述:当托管环境遇上纳秒级时序如果你玩过嵌入式开发,尤其是用Arduino驱动过WS2812(也就是Adafruit的NeoPixels),那你肯定知道那套经典的Adafruit_NeoPixel库,几行代码就能让灯带流光溢彩。但当你把…...

RT-Thread实战:基于STM32与软件I2C的IST8310磁力计驱动开发与模块化设计

1. 项目概述与设计思路在RoboMaster这类对实时性和可靠性要求极高的机器人竞赛中,电控系统的稳定与高效是取胜的基石。很多队伍在初期会选择裸机开发,但随着功能模块的增加,任务调度、资源管理、驱动适配等问题会迅速让代码变得臃肿且难以维护…...

别再折腾LaTeX了!用Jupyter Notebook自带功能搞定ipynb转PDF(完美支持中文和公式)

告别复杂工具链:Jupyter Notebook原生方案实现ipynb完美转PDF 在数据分析和学术研究的日常工作中,我们经常需要将Jupyter Notebook(.ipynb文件)转换为PDF格式以便分享或提交报告。传统方法往往依赖pandoc、LaTeX等复杂工具链&…...

Adafruit Fritzing元件库安装与使用指南:提升硬件设计效率

1. 项目概述:为什么你需要Adafruit Fritzing元件库?如果你玩过Arduino或者树莓派,肯定对Adafruit这家公司不陌生。他们出品的各种传感器、显示屏和扩展板,几乎成了开源硬件项目的“标准件”。但每次在Fritzing里画电路图&#xff…...

Tina Linux音频开发指南:从ALSA框架到实战调试

1. 项目概述:为什么我们需要一份音频开发指南?在嵌入式Linux的世界里,音频开发常常被开发者们戏称为“玄学”。我见过太多项目,硬件电路设计得漂漂亮亮,系统也跑得飞快,但一到音频部分就卡壳——要么是播放…...

基于CircuitPython与NeoPixel的智能圣诞树:从硬件搭建到动态灯光算法

1. 项目概述:从零打造一棵会“思考”的圣诞树又到年底了,看着家里那棵年复一年、只会默默发光的传统圣诞树,总觉得少了点“灵魂”。作为一个常年和微控制器、代码打交道的创客,我总琢磨着能不能给节日装饰加点科技感,让…...

让足球经理游戏更真实:NewGAN-Manager 零基础配置全攻略

让足球经理游戏更真实:NewGAN-Manager 零基础配置全攻略 【免费下载链接】NewGAN-Manager A tool to generate and manage xml configs for the Newgen Facepack. 项目地址: https://gitcode.com/gh_mirrors/ne/NewGAN-Manager 还在为足球经理游戏中千篇一律…...

WLED与xLights打造音乐同步LED灯光秀:从硬件连接到创意编排

1. 项目概述:从独立闪烁到交响乐章如果你玩过像NeoPixel这类可单独寻址的LED灯带,肯定体验过那种让灯光随心所欲流动的快感。但不知道你有没有想过,把这些闪烁的光点从简单的循环动画,升级成一场能与音乐节拍精准共舞、充满叙事感…...

基于Arduino与V-USB的红外转USB键盘接收器设计与实现

1. 项目概述:从游戏抢答器到通用输入设备的蜕变几年前,我在一个教育科技展会上看到了那种用于课堂抢答的无线按钮系统,一套动辄上千元的价格让我这个喜欢折腾硬件的玩家直摇头。当时我就在想,这玩意儿的核心不就是个红外发射接收加…...

基于Arduino与V-USB打造低成本红外无线抢答器:从信号解码到HID模拟

1. 项目概述与核心思路拆解如果你是一位老师,或者经常组织一些需要快速抢答的互动活动,肯定对市面上那些动辄上千元的专业无线抢答系统望而却步。它们功能强大,但价格也足够“劝退”。几年前,我在为一所学校的科技节活动寻找低成本…...

别再傻傻重启了!用JRebel给IDEA装上‘秒级热更新’,Spring Boot开发效率翻倍

告别低效重启:用JRebel解锁Spring Boot开发的终极热更新体验 每次修改几行代码就要等待漫长的应用重启?Spring Boot DevTools的热加载功能已经无法满足你对开发效率的极致追求?作为长期奋战在Java开发一线的工程师,我深知这种重复…...

避坑指南:在Ubuntu 22.04上用Anaconda配置Vision-Mamba环境,解决‘bimamba_type‘报错

深度避坑:Ubuntu 22.04下Vision-Mamba环境配置全攻略 在深度学习项目部署过程中,环境配置往往是第一个拦路虎。最近在配置Vision-Mamba环境时,我遇到了几个令人头疼的问题,特别是那个让人摸不着头脑的bimamba_type报错。经过一番折…...

如何快速掌握ComfyUI智能图像分割:面向新手的完整指南

如何快速掌握ComfyUI智能图像分割:面向新手的完整指南 【免费下载链接】comfyui_segment_anything Based on GroundingDino and SAM, use semantic strings to segment any element in an image. The comfyui version of sd-webui-segment-anything. 项目地址: ht…...

【每日一题】排序

📌 写在前面:排序是算法竞赛中最基础也最核心的技能之一。它不仅是快速查找、去重、贪心等算法的前置步骤,更是自定义比较策略、多关键字排序、排序后贪心等高级技巧的基石。本文基于蓝桥杯官方课程与真题,从基础排序到竞赛实战&a…...

备战蓝桥杯国赛【Day 17】

📌 写在前面:今天的4道题全部来自蓝桥杯真题,,核心考点包括:贪心策略排序、自定义比较器、差分思想、前缀和贪心选择。这些题目看似简单,但暗藏陷阱,是检验"代码实现能力"和"思维…...

UP Squared 6000工业级创客板:边缘AIoT开发与部署实战指南

1. 项目概述:UP Squared 6000,一块能“扛事”的工业级创客板在工业自动化和边缘AIoT项目里摸爬滚打这么多年,我经手过不少开发板,从早期的树莓派到各种国产派,再到工业级的工控机。很多时候,我们面临一个尴…...

Boomi 与 Gong 达成合作,将 Revenue AI 引入 Boomi Agentstudio

Gong 的 Revenue AI 现已原生集成至 Boomi Enterprise Platform 面向 AI 时代的数据激活公司 Boomi 今日宣布,与 Revenue AI 领域领导者 Gong 达成合作,将 Gong 捕获的营收信号原生整合至 Boomi Enterprise Platform。通过此次合作,企业可构…...

工业作业火花识别 工业作业安全监测 工业安全火灾识别 火灾烟雾识别

火灾、烟雾及火花检测数据集 数据集概述 本数据集面向计算机视觉目标检测场景构建,聚焦火情风险要素识别,为烟火火花类智能监测模型训练提供标准化图像数据支撑,整体适配深度学习目标检测算法训练、验证与测试流程,可有效支撑安防…...

嵌入式Linux无线AP搭建实战:hostapd与udhcpd配置详解

1. 项目概述:为什么要在嵌入式设备上折腾无线AP?最近在调试一个移动机器人项目,设备上跑的是裁剪过的嵌入式Linux系统。调试过程里最头疼的就是网线——设备满场跑,我得抱着笔记本在后面追,活像在玩现实版的“老鹰捉小…...

终极指南:如何快速免费解决GBK到UTF-8编码转换难题

终极指南:如何快速免费解决GBK到UTF-8编码转换难题 【免费下载链接】GBKtoUTF-8 To transcode text files from GBK to UTF-8 项目地址: https://gitcode.com/gh_mirrors/gb/GBKtoUTF-8 还在为乱码文件而烦恼吗?GBKtoUTF-8是一款专为中文文本编码…...

NVDC充电架构深度解析:智能电源管理如何提升笔记本性能与电池寿命

1. 项目概述:NVDC充电器,一个被低估的“能量管家”如果你是一位经常需要带着笔记本电脑移动办公的资深用户,或者是一位对设备续航和充电效率有极致追求的硬件爱好者,那么“NVDC”这个词,很可能已经或即将进入你的视野。…...

RFSoC玩转跳频通信:从NCO配置到多片同步的实战指南(Zynq UltraScale+ RFSoC Gen 3)

RFSoC跳频通信实战:从NCO配置到多片同步的高级技巧 跳频通信技术在现代无线系统中扮演着关键角色,尤其在抗干扰和频谱感知应用中。Xilinx的Zynq UltraScale RFSoC Gen 3平台凭借其集成的RF数据转换器和灵活的数字信号处理能力,为跳频系统设计…...

Cadence Allegro 16.6 环境设置保姆级教程:从绘图参数到自动保存,新手避坑指南

Cadence Allegro 16.6 环境设置实战指南:从零配置到高效设计 第一次打开Cadence Allegro 16.6时,满屏的菜单选项和参数设置可能会让新手感到无所适从。作为一款专业的PCB设计工具,Allegro提供了高度可定制的工作环境,但这也意味着…...

Perplexity学校信息检索的“黑箱”终于被打开:基于37所样本校实测的响应延迟、召回率与可信度三维评估报告

更多请点击: https://codechina.net 第一章:Perplexity学校信息检索的“黑箱”终于被打开:基于37所样本校实测的响应延迟、召回率与可信度三维评估报告 实测方法论:三维度穿透式评估框架 我们对全国37所高校(含985/2…...

为什么92.7%的临床研究者用错Perplexity药物检索?——2024年真实审计案例暴露的4个致命盲区

更多请点击: https://intelliparadigm.com 第一章:Perplexity药物信息检索的临床价值与审计背景 在精准医疗快速演进的当下,临床决策对实时、可信、上下文感知的药物信息依赖日益加深。Perplexity作为基于推理增强型大语言模型的信息检索系统…...

EPLAN端子图表修改避坑指南:从占位符到动态区域,手把手教你定制专属端子连接图

EPLAN端子图表深度定制指南:从占位符优化到动态布局实战 在电气工程设计领域,EPLAN作为行业标杆软件,其端子图表功能直接影响项目交付的专业度和效率。许多工程师在项目后期常遇到这样的困境:标准端子图表无法满足客户特殊规范要求…...

深入Keil5编译器:解读#1295-D警告背后的C语言函数原型进化史

深入Keil5编译器:解读#1295-D警告背后的C语言函数原型进化史 当你在Keil5环境下打开一个遗留的单片机项目时,那个看似微不足道的#1295-D: Deprecated declaration警告可能正暗示着一段跨越四十年的编程语言进化史。这个关于函数声明的警告不是Keil5的任…...