
1. 项目概述为什么需要定时器级联在嵌入式开发尤其是基于STM32这类资源丰富的MCU进行项目设计时我们常常会遇到一个看似简单却令人头疼的需求需要一个超长的定时周期。比如你想做一个每隔1小时才触发一次的事件或者需要精确测量一个长达数分钟的脉冲宽度。STM32的单个定时器即使将预分频器PSC设置到最大计数周期ARR也拉到极限其定时长度依然是有限的。以常见的72MHz系统时钟为例一个16位定时器最大也只能定时到约0.9秒65535 * 65536 / 72e6 ≈ 0.9s。对于24小时制的时钟、长时间数据记录、低功耗唤醒等场景这显然不够用。这时“定时器级联”就成了一种非常经典且高效的解决方案。它的核心思想很简单用一个定时器的溢出事件去驱动另一个定时器的计数时钟。形象地说就像我们生活中的水表小齿轮转一圈一个定时器溢出推动大齿轮走一格另一个定时器计数一次。通过这种“齿轮传动”的方式我们可以将定时周期成倍地、几乎无限地延长。我最近在一个环境监测设备中就用了这个技术。设备需要每5分钟采集一次传感器数据并上传。如果用一个定时器做5分钟中断对系统时钟精度和功耗都是挑战。而使用TIM2和TIM3级联我让TIM3每1秒溢出一次作为TIM2的时钟TIM2计数300次就是5分钟。这样TIM2的中断频率极低大大降低了CPU干预开销整个定时逻辑清晰又稳定。接下来我就把这个从原理到代码实现的完整过程拆解给你看。2. 核心思路与方案选型2.1 级联的几种模式与选择在STM32中实现定时器级联主要有三种硬件方式每种都有其适用场景2.1.1 内部触发连接ITRx这是最常用、最推荐的级联方式。STM32的定时器内部有专门的触发输入线路ITR0, ITR1, ITR2, ITR3可以将一个定时器的更新事件UEV作为另一个定时器的内部触发源。这种方式完全在芯片内部完成信号连接不占用任何外部GPIO引脚稳定可靠是大多数应用的首选。不同定时器之间的ITR连接关系是固定的需要查阅芯片参考手册的“定时器内部触发连接”表格。2.1.2 使用外部时钟模式1这种方式是将主定时器的某个输出通道如OC1REF即PWM的参考信号通过GPIO输出然后通过另一组GPIO连接到从定时器的外部输入通道如TI1或TI2并将从定时器配置为外部时钟模式1。这种方法更灵活可以跨越不直接支持内部触发的定时器组合但代价是占用了两个GPIO引脚并且引入了信号在PCB上传输的延迟和潜在干扰。2.1.3 使用外部时钟模式2 ETR引脚将主定时器的更新事件映射到某个GPIO如MCO输出然后连接到从定时器的外部触发ETR引脚从定时器配置为外部时钟模式2。这种方式最为复杂通常用于特殊场景或验证概念日常项目极少使用。避坑指南对于绝大多数需要长定时的应用无脑选择内部触发连接ITRx模式。它简单、高效、不占资源。只有在需要跨越特定定时器组如高级定时器与通用定时器间某些无法直接ITR连接的组合时才去考虑外部时钟模式。2.1.4 软件级联除了硬件级联还有一种“软件级联”的思路在一个定时器中断服务函数里对一个全局变量进行累加当累加值达到预设值时执行真正的长周期任务。这种方法实现简单但缺点明显中断频率依然很高取决于主定时器的周期增加了CPU中断负载并且如果中断被意外屏蔽或丢失会导致整个长定时出错。因此在追求低功耗和高可靠性的场合硬件级联是更优解。基于以上分析本项目将采用内部触发连接ITRx方式实现TIM3作为主定时器提供基础时基TIM2作为从定时器进行长周期计数的级联。2.2 定时器资源规划与时钟树考量选定模式后我们需要规划具体的定时器资源。以STM32F103C8T6这款经典芯片为例TIM232位通用定时器ARR最大值约42.9亿是作为长周期从定时器的绝佳选择。TIM316位通用定时器我们将用它产生一个合适的基础时间比如1ms或1s作为TIM2的时钟源。接下来是关键的时钟树配置。我们的目标是让TIM3的更新事件触发TIM2计数一次。因此TIM3的时钟需要使能APB1总线时钟TIM2、3、4挂在APB1上。如果APB1预分频器不为1定时器时钟会倍频。例如系统时钟72MHzAPB1预分频器为236MHz则定时器实际时钟是72MHz。这一点在计算定时参数时必须清楚。TIM2的时钟在级联模式下TIM2的时钟源不再是内部的APB1时钟而是来自TIM3的触发信号。因此TIM2的时钟频率等于TIM3的更新事件频率。参数计算示例 假设我们需要一个精确的1小时3600秒定时。步骤1让TIM3产生1秒的更新事件。系统时钟72MHzTIM3时钟72MHz。设定TIM3_PSC 7199 TIM3_ARR 9999。则TIM3周期 (71991)*(99991) / 72e6 1.0000秒。步骤2TIM2被配置为每收到一次TIM3的触发就计数一次。要计数3600次只需设置TIM2_ARR 3599因为从0开始计数。TIM2的PSC在从模式下发通常设置为0不分频。最终效果TIM3每秒“滴答”一次TIM2每3600次“滴答”即3600秒产生一次更新中断。3. 硬件设计与核心配置解析3.1 原理图与引脚分析由于我们采用内部触发ITRx方式无需任何外部电路连接。这是该方案最大的优点之一。所有的级联信号都在STM32芯片内部通过专用路径传输避免了PCB布局布线引入的噪声和延迟问题也节省了宝贵的GPIO资源用于其他功能。你唯一需要确保的是在原理图和PCB上为MCU提供稳定、干净的电源和地并为振荡器如果使用外部晶振做好匹配电路。定时器级联对时钟精度要求较高一个稳定的时钟源是精确长定时的基石。如果项目对时间精度要求极高如ppm级别建议使用外部温补晶振或时钟模块。3.2 CubeMX图形化配置详解对于新手或追求开发效率的开发者使用STM32CubeMX进行初始化配置是极佳的选择。以下是关键步骤的截图与说明时钟配置Clock Configuration 将HCLK设置为72MHz根据你的晶振。确认APB1 Timer Clocks为72MHz显示为x2因为APB1 prescaler2定时器时钟2倍频。TIM3配置主定时器在“Pinout Configuration”标签页中找到TIM3。Clock Source选择Internal Clock。Parameter SettingsPrescaler (PSC - 16 bits value)7199Counter ModeUpCounter Period (AutoReload Register - 16 bits value)9999auto-reload preloadEnable推荐避免更新时产生毛刺此时下方会显示Update event (Hz) 1 即更新频率为1Hz。NVIC Settings不要使能TIM3的全局中断因为TIM3仅作为时钟源我们只需要它的更新事件不需要CPU中断。TIM2配置从定时器找到TIM2。Clock Source选择Internal Clock。注意这里选择内部时钟只是初始化步骤后面配置触发源后会覆盖。Parameter SettingsPrescaler (PSC - 16 bits value)0Counter ModeUpCounter Period (AutoReload Register - 32 bits value)3599 对应3600秒auto-reload preloadEnableTrigger SourceSlave Mode选择Combined Reset Trigger Mode Selection为Trigger Mode。Trigger Source选择ITR1。这是最关键的一步对于STM32F1TIM3的更新事件通常映射到ITR1具体需查数据手册不同系列映射不同。NVIC Settings务必使能TIM2的全局中断。这是我们最终需要的中断。配置完成后生成代码。CubeMX会自动生成正确的初始化代码将TIM3的更新事件连接到TIM2的触发输入并将TIM2配置为从模式。3.3 寄存器级直接配置剖析如果你习惯直接操作寄存器或者想深入理解底层机制以下是核心寄存器的配置逻辑。我们假设系统时钟已配置为72MHz。// 1. 开启时钟如果尚未开启 RCC-APB1ENR | RCC_APB1ENR_TIM2EN | RCC_APB1ENR_TIM3EN; // 2. 配置主定时器 TIM3 (1秒周期) TIM3-PSC 7199; // 预分频器 7200分频 72MHz / 7200 10kHz TIM3-ARR 9999; // 自动重装载值计数10000次 10kHz / 10000 1Hz TIM3-CR1 | TIM_CR1_ARPE; // 使能ARR预装载 TIM3-EGR | TIM_EGR_UG; // 产生更新事件加载PSC和ARR TIM3-CR1 | TIM_CR1_CEN; // 启动TIM3计数 // 3. 配置从定时器 TIM2 (被TIM3触发) TIM2-PSC 0; // 从模式对触发信号不分频 TIM2-ARR 3599; // 计数3600次触发中断 TIM2-CR1 | TIM_CR1_ARPE; // 使能ARR预装载 // 4. 配置TIM2为从模式 - 触发模式 // SMCR寄存器 SMS110 (触发模式) TS001 (选择ITR1作为触发源) TIM2-SMCR ~(TIM_SMCR_SMS_Msk | TIM_SMCR_TS_Msk); // 清零相关位 TIM2-SMCR | (6 TIM_SMCR_SMS_Pos); // SMS 110 触发模式 TIM2-SMCR | (1 TIM_SMCR_TS_Pos); // TS 001 ITR1 // 5. 使能TIM2更新中断 TIM2-DIER | TIM_DIER_UIE; // 使能更新中断 NVIC_EnableIRQ(TIM2_IRQn); // 使能NVIC中断 NVIC_SetPriority(TIM2_IRQn, 1); // 设置中断优先级 // 6. 启动TIM2的从模式 TIM2-SMCR | TIM_SMCR_SMS; // 确保SMS位已设置实际上步骤4已设置 // 更准确的是在触发模式下一旦使能了触发源并设置了SMS从定时器就等待触发 // 通常直接使能计数器即可但触发模式下CEN位可能由硬件控制。 // 更标准的做法是在配置完所有参数后执行 TIM2-CR1 | TIM_CR1_CEN; // 在触发模式下CEN位可能被硬件自动置位或忽略但写上更安全。关键寄存器解读TIMx_SMCR从模式控制寄存器这是级联的灵魂。SMS[2:0]位域选择从模式110代表触发模式。TS[2:0]位域选择触发输入源001对应ITR1即来自TIM3的触发信号。TIMx_CR1控制寄存器1ARPE位使能自动重装载预装载寄存器可以避免在修改ARR时产生错误的中间值确保定时周期的平稳过渡。TIMx_EGR事件生成寄存器写UG位可以手动产生一个更新事件用于在初始化后立即加载PSC和ARR的值使得第一个定时周期就是准确的。4. 软件实现与代码实战4.1 工程结构与初始化流程一个健壮的工程应该模块清晰。建议创建独立的定时器驱动文件timer_cascade.c/h。timer_cascade.h头文件定义#ifndef __TIMER_CASCADE_H #define __TIMER_CASCADE_H #include stm32f1xx_hal.h // 根据你的系列修改 // 长定时回调函数类型定义 typedef void (*LongTimer_Callback_t)(void); void TimerCascade_Init(void); void TimerCascade_Start(uint32_t period_seconds); void TimerCascade_Stop(void); void TimerCascade_SetCallback(LongTimer_Callback_t callback); // 提供给中断服务函数调用 void TIM2_IRQHandler_Callback(void); #endiftimer_cascade.c初始化函数static LongTimer_Callback_t user_callback NULL; void TimerCascade_Init(void) { // 1. 初始化主定时器 TIM3 (1Hz更新事件) __HAL_RCC_TIM3_CLK_ENABLE(); TIM_HandleTypeDef htim3; htim3.Instance TIM3; htim3.Init.Prescaler 7199; htim3.Init.CounterMode TIM_COUNTERMODE_UP; htim3.Init.Period 9999; htim3.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; htim3.Init.AutoReloadPreload TIM_AUTORELOAD_PRELOAD_ENABLE; HAL_TIM_Base_Init(htim3); HAL_TIM_Base_Start(htim3); // 启动TIM3但不开启中断 // 2. 初始化从定时器 TIM2 __HAL_RCC_TIM2_CLK_ENABLE(); TIM_HandleTypeDef htim2; htim2.Instance TIM2; htim2.Init.Prescaler 0; htim2.Init.CounterMode TIM_COUNTERMODE_UP; htim2.Init.Period 0; // 初始化为0后续动态设置 htim2.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; htim2.Init.AutoReloadPreload TIM_AUTORELOAD_PRELOAD_ENABLE; HAL_TIM_Base_Init(htim2); // 3. 配置TIM2为从模式 - 触发模式触发源为ITR1 (TIM3) TIM_SlaveConfigTypeDef sSlaveConfig {0}; sSlaveConfig.SlaveMode TIM_SLAVEMODE_TRIGGER; sSlaveConfig.InputTrigger TIM_TS_ITR1; HAL_TIM_SlaveConfigSynchro(htim2, sSlaveConfig); // 4. 使能TIM2更新中断 HAL_TIM_Base_Start_IT(htim2); // 这个函数会启动计数器并开启中断 // 注意在HAL库中配置为从模式后HAL_TIM_Base_Start_IT()的行为是合适的。 } void TimerCascade_Start(uint32_t period_seconds) { if (period_seconds 0 || period_seconds 0xFFFFFFFF) return; // 简单校验 // TIM2的ARR设置为 period_seconds - 1 __HAL_TIM_SET_AUTORELOAD(htim2, period_seconds - 1); __HAL_TIM_SET_COUNTER(htim2, 0); // 计数器清零 // 由于TIM2已配置为从模式并由TIM3触发这里无需再启动但确保中断使能 __HAL_TIM_ENABLE_IT(htim2, TIM_IT_UPDATE); }4.2 中断服务函数与回调机制中断服务函数ISR应该尽可能短小精悍只做标志位设置或简单计数复杂的任务放到主循环或通过回调函数处理。在stm32f1xx_it.c中修改或确保存在TIM2中断向量void TIM2_IRQHandler(void) { HAL_TIM_IRQHandler(htim2); // 调用HAL库的公共处理函数 }在timer_cascade.c中实现回调分发// 重写HAL库的弱定义回调函数 void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM2) { // 用户自定义的长定时任务 if (user_callback ! NULL) { user_callback(); } // 可以在这里添加一些调试信息如翻转一个LED // HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin); } // 注意TIM3的中断不会被使能所以不会进入这里 } void TimerCascade_SetCallback(LongTimer_Callback_t callback) { user_callback callback; } // 提供给外部调用的中断处理函数如果不用HAL回调 void TIM2_IRQHandler_Callback(void) { if (__HAL_TIM_GET_FLAG(htim2, TIM_FLAG_UPDATE) ! RESET) { if (__HAL_TIM_GET_IT_SOURCE(htim2, TIM_IT_UPDATE) ! RESET) { __HAL_TIM_CLEAR_IT(htim2, TIM_IT_UPDATE); // 用户任务 if (user_callback ! NULL) { user_callback(); } } } }在主函数main.c中的应用示例#include timer_cascade.h void MyOneHourTask(void) { // 这里是需要每1小时执行一次的任务 printf([OneHourTask] System tick: %lu\r\n, HAL_GetTick()); // 例如读取所有传感器数据并上传 // Sensor_ReadAll(); // Cloud_UploadData(); } int main(void) { HAL_Init(); SystemClock_Config(); // 初始化UART等外设... printf(System Booted.\r\n); TimerCascade_Init(); // 初始化级联定时器 TimerCascade_SetCallback(MyOneHourTask); // 设置回调函数 TimerCascade_Start(3600); // 启动1小时3600秒定时 while (1) { // 主循环处理其他任务 HAL_Delay(1000); // 可以在这里添加看门狗喂狗、低功耗模式管理等 } }4.3 动态调整定时周期与状态管理在实际项目中定时周期可能需要动态改变。例如设备在白天每5分钟采集一次夜晚每30分钟采集一次。static TIM_HandleTypeDef htim2; // 假设htim2在模块内为全局变量 void TimerCascade_ChangePeriod(uint32_t new_period_seconds) { // 方法1直接修改ARR适用于当前周期结束后生效 // __HAL_TIM_SET_AUTORELOAD(htim2, new_period_seconds - 1); // 方法2更安全的方式停止计数修改参数重启 __HAL_TIM_DISABLE(htim2); // 禁用TIM2 __HAL_TIM_SET_AUTORELOAD(htim2, new_period_seconds - 1); __HAL_TIM_SET_COUNTER(htim2, 0); // 重置计数器 __HAL_TIM_CLEAR_FLAG(htim2, TIM_FLAG_UPDATE); // 清除可能挂起的更新标志 __HAL_TIM_ENABLE(htim2); // 重新使能 // 注意TIM2作为从定时器其计数使能受TIM3触发控制这里禁用/使能的是TIM2本身是否响应触发和中断。 } // 获取当前剩余时间近似值 uint32_t TimerCascade_GetRemainingTicks(void) { uint32_t arr __HAL_TIM_GET_AUTORELOAD(htim2); uint32_t cnt __HAL_TIM_GET_COUNTER(htim2); if (cnt arr) { // 防止溢出后读取的异常情况 return 0; } return (arr - cnt); // 返回还需要多少个TIM3触发 }重要心得动态修改ARR时如果ARPE位使能了新值会写入预装载寄存器只在下次更新事件时才生效这是安全的。如果想立即生效可以在修改ARR后手动产生一个更新事件__HAL_TIM_GENERATE_EVENT(htim2, TIM_EVENTSOURCE_UPDATE)但这会重置计数器导致当前周期被截断需要根据业务逻辑谨慎使用。5. 调试技巧与常见问题排查即使原理和代码都清晰调试阶段也可能遇到各种“妖孽”问题。下面是我踩过坑后总结的排查清单。5.1 调试手段与工具使用逻辑分析仪/示波器这是最直观的工具。你可以测量TIM3的某个通道配置为PWM输出模式与更新事件同步和TIM2的中断服务函数里翻转的GPIO引脚。操作将TIM3的CH1设置为PWM模式TIM3-CCR1设置为一个很小的值这样每次更新事件时会产生一个极短的脉冲。用逻辑分析仪的一个通道抓这个脉冲另一个通道抓在TIM2中断里翻转的LED引脚。你会看到每3600个TIM3脉冲才会出现一个TIM2中断翻转信号。STM32 CubeMonitor或SEGGER SystemView这些实时监控工具可以可视化中断的发生和定时器的状态对于理解级联定时器的工作时序非常有帮助。串口打印最朴素但有效。在TIM2和TIM3的中断服务函数里分别打印不同的信息。如果只有TIM2的中断信息按长周期出现而TIM3没有信息因为你没开它的中断说明级联成功。5.2 常见问题速查表问题现象可能原因排查步骤与解决方案TIM2完全不进中断1. NVIC中断未使能。2. TIM2的从模式配置错误SMS/TS。3. TIM3没有正确运行或没有更新事件。4. TIM2的更新中断未使能UIE。1. 检查NVIC_EnableIRQ(TIM2_IRQn)是否调用。2. 单步调试查看TIM2-SMCR寄存器确认SMS110 (触发模式)TS值正确指向TIM3的ITRx。3. 检查TIM3的CNT寄存器是否在增加或将其CHx设为PWM输出用示波器测量。4. 检查TIM2-DIER寄存器的UIE位是否为1。TIM2中断频率不对太快或太慢1. TIM3的定时周期计算错误。2. TIM2的ARR值设置错误。3. 时钟树配置错误定时器实际时钟非预期。1. 重新计算TIM3的PSC和ARR。公式定时周期 (PSC1)*(ARR1) / TimerClock。2. 确认TIM2的ARR是否为目标秒数 - 1。3. 检查RCC配置确认APB1/APB2预分频器定时器时钟可能被倍频。定时精度差误差随时间累积1. 系统时钟源HSI/HSE精度不够。2. 中断处理函数耗时过长影响了定时器计数级联模式下影响较小但仍有。3. 其他高优先级中断长时间阻塞。1. 对精度要求高请使用外部晶振HSE。2. 优化中断服务函数只做必要操作标志位处理放主循环。3. 合理配置中断优先级确保定时器中断不会被意外阻塞。动态修改ARR后定时行为异常1. 修改ARR时未考虑ARPE预装载机制。2. 在中断中修改ARR未处理临界区问题。1. 如果使能了ARPE新ARR值在下一次更新事件生效这是正常的。如需立即生效且重置周期可手动产生更新事件UG。2. 在修改ARR/CNT等关键寄存器时最好先暂停定时器__HAL_TIM_DISABLE修改后再恢复。进入调试模式后定时器停止这是仿真器的默认行为。在IDE如Keil的调试配置中找到“Debug”设置将相关定时器的“Core Peripherals”或“Timerx”从“Stop”改为“Run”。5.3 进阶级联链与更长的定时如果需要实现数天甚至数月的定时可以构建三级甚至更多级级联链。例如TIM3 (1秒) - TIM2 (60秒) - TIM4 (60分钟)。原理完全相同只需将第二级的从定时器TIM2的更新事件作为第三级定时器TIM4的触发源。配置要点TIM2需要同时作为TIM3的“从”和TIM4的“主”。配置TIM2为从模式受TIM3触发同时使能TIM2的更新事件输出在CubeMX中或通过设置TIM2-CR2寄存器的MMS位为010即更新事件作为触发输出。TIM4配置为从模式触发源选择对应TIM2的ITRx。代码片段示意// TIM2 配置既是TIM3的从又是TIM4的主 // ... TIM2作为从模式受TIM3触发的配置同上 ... // 同时使能TIM2的更新事件作为主模式输出 TIM2-CR2 | (2 TIM_CR2_MMS_Pos); // MMS010, 更新事件作为TRGO输出 // TIM4 配置作为TIM2的从 TIM4-SMCR | (6 TIM_SMCR_SMS_Pos); // 触发模式 TIM4-SMCR | (X TIM_SMCR_TS_Pos); // TS X X对应TIM2的ITR编号查手册6. 项目实战构建一个简易的长周期任务调度器理论最终要服务于实践。让我们利用这个级联定时器构建一个简易的、可扩展的长周期任务调度器。这个调度器可以管理多个以秒、分、小时为单位的周期性任务。6.1 调度器数据结构设计// task_scheduler.h typedef struct { uint32_t task_id; uint32_t interval_seconds; // 任务执行间隔秒 uint32_t last_execute_ticks; // 上次执行时的“总滴答数” void (*task_func)(void); // 任务函数指针 uint8_t enabled; // 任务使能标志 } LongPeriodTask_t; void TaskScheduler_Init(void); void TaskScheduler_AddTask(LongPeriodTask_t *task); void TaskScheduler_Run(void); // 在主循环中调用 uint32_t TaskScheduler_GetTotalTicks(void); // 获取系统运行的总秒数6.2 基于级联定时器的滴答源我们将级联定时器作为这个调度器的“心脏”它每1秒产生一个中断在中断里对一个全局的“秒计数器”加一。// task_scheduler.c static volatile uint32_t s_system_second_tick 0; // 系统秒计数器 static LongPeriodTask_t *s_task_list[MAX_TASKS] {NULL}; static uint8_t s_task_count 0; // TIM2的中断回调函数 void TIM2_OneSecond_Callback(void) { s_system_second_tick; // 秒计数器加1 // 注意这里不要进行复杂的任务检查只做标记。 } uint32_t TaskScheduler_GetTotalTicks(void) { return s_system_second_tick; }6.3 任务注册与主循环调度在主循环中不断检查每个注册的任务判断是否到达执行时间。void TaskScheduler_AddTask(LongPeriodTask_t *task) { if (s_task_count MAX_TASKS) return; task-last_execute_ticks s_system_second_tick; // 设置初始上次执行时间 s_task_list[s_task_count] task; } void TaskScheduler_Run(void) { uint32_t current_ticks s_system_second_tick; for (int i 0; i s_task_count; i) { LongPeriodTask_t *task s_task_list[i]; if (task-enabled) { // 检查是否到达执行时间 if ((current_ticks - task-last_execute_ticks) task-interval_seconds) { task-task_func(); // 执行任务 task-last_execute_ticks current_ticks; // 更新执行时间 } } } }6.4 应用示例多速率数据采集// 定义任务 void Task_ReadTemperature(void) { /* 每10秒读一次温度 */ } void Task_UploadData(void) { /* 每5分钟上传一次数据 */ } void Task_SystemSelfCheck(void) { /* 每1小时进行一次自检 */ } LongPeriodTask_t temp_task {1, 10, 0, Task_ReadTemperature, 1}; LongPeriodTask_t upload_task {2, 300, 0, Task_UploadData, 1}; // 300秒5分钟 LongPeriodTask_t check_task {3, 3600, 0, Task_SystemSelfCheck, 1}; int main(void) { // ... 初始化 ... TimerCascade_Init(); TimerCascade_SetCallback(TIM2_OneSecond_Callback); // 连接1秒滴答 TimerCascade_Start(1); // 启动1秒定时 TaskScheduler_Init(); TaskScheduler_AddTask(temp_task); TaskScheduler_AddTask(upload_task); TaskScheduler_AddTask(check_task); while (1) { TaskScheduler_Run(); // 主循环调度任务 HAL_Delay(10); // 短暂延时防止CPU空转 // 其他低优先级任务... } }这个简易调度器将级联定时器的稳定长定时能力与灵活的软件调度结合了起来。级联定时器保证了“秒”这个基础时间单位的精确性而软件调度器则在此之上实现了任意间隔的任务管理并且添加、删除、修改任务都非常方便中断服务函数依然保持轻量。这种“硬件精确计时软件灵活调度”的模式在需要管理多个不同周期后台任务的嵌入式系统中非常实用。