做嵌入式开发的同学应该都经历过这样的阶段:一个while(1)大循环里堆满了 LED 翻转、按键扫描、显示刷新、数据采集,逻辑越加越多,改一个功能可能牵动好几个if分支。想直接上 RTOS 又怕线程同步、内存占用、调度切换引入新问题。那有没有一种比较轻量的方案,既能缓解大循环的混乱,又不用引入完整操作系统?答案往往就是本文要聊的定时器模拟任务。
本文会从嵌入式软件架构演进讲起,重点拆解“定时器模拟任务”的原理、数据结构、完整代码示例和常见坑点。无论你是刚接触单片机开发的学生,还是正在做裸机项目的嵌入式工程师,都可以通过这套思路把代码组织得更清晰、更接近 RTOS 的任务模型。
1. 为什么要关注嵌入式软件设计架构
1.1 裸机项目的复杂度瓶颈
很多初学者写嵌入式程序是从“点亮一个小灯”开始的,代码量不大,一个主循环足够搞定。但随着功能增加,程序会逐渐变成这样:
while (1) { HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin); HAL_Delay(100); if (HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin) == GPIO_PIN_RESET) { // 处理按键逻辑 } // 刷新屏幕 // 处理串口数据 // 采集传感器数据 // ... }这种代码最大的问题在于:所有逻辑混在同一个循环里,周期很难控制。你想让 LED 每 100ms 翻转一次,按键每 10ms 扫描一次,屏幕每 50ms 刷新一次,但HAL_Delay(100)一阻塞,后面的按键扫描、屏幕刷新也变成了“大概 100ms 一次”。如果某段代码执行时间较长,整个系统的实时性都会受到影响。
这种情况不是个例。只要项目功能超过三四个,主循环就会变成一锅粥。此时需要思考的已经不是“怎么加代码”,而是“代码结构怎么组织”。
1.2 什么是定时器模拟任务
定时器模拟任务是嵌入式裸机开发中常用的一种软件设计方法。它的核心思想是:利用一个硬件定时器产生固定间隔的中断,在中断里维护一个“系统节拍”,然后按照节拍把 CPU 时间分配给不同的函数,让这些函数按照各自设定的周期轮流执行。
用一句通俗的话概括:它让单核 MCU 看起来好像同时在运行多个任务。
实际上,MCU 同一时刻只能执行一条指令。定时器模拟任务只是把时间切成一个个小片段,每个任务在自己的时间片里运行,任务之间通过“我该不该运行”的判断来决定谁占用 CPU。这是一种非常典型的协作式多任务调度,也叫时间片轮询的简化版。
1.3 什么时候适合用定时器模拟任务
适合采用定时器模拟任务的场景有这些:
- 项目功能在 3~10 个左右,主循环已经混乱。
- 任务对实时性要求不高,允许毫秒级误差。
- 不想引入 RTOS 的额外 RAM/ROM 开销。
- 希望保留裸机程序的简单性和可控性。
- 正在学习 RTOS,想先理解任务调度的底层原理。
如果任务数量非常多、任务之间有复杂的同步关系、某个任务必须抢占式执行,那就应该考虑完整的 RTOS,比如 FreeRTOS、RT-Thread、uC/OS。定时器模拟任务是裸机与 RTOS 之间的一个“中间态”,也是一个很好的过渡桥梁。
2. 嵌入式软件架构的演进路径
理解定时器模拟任务,最好先看一遍嵌入式软件架构的演进路径。不同架构之间没有绝对的优劣,只有适不适合当前项目。
2.1 前后台架构(超级大循环)
前后台架构是最原始的裸机架构:
- 后台:
while(1)主循环,负责处理业务逻辑。 - 前台:各类中断,负责响应紧急事件。
这种架构的优点是非常简单,没有线程同步问题,代码直来直去。缺点是后台循环无法及时响应紧急任务,而且随着功能增多,代码会越来越难维护。
2.2 定时器中断 + 标志位
当发现主循环轮询的方式响应太慢,就会改进为:在定时器中断里置一个标志位,主循环检测到标志位后执行对应逻辑。
volatile uint8_t flag_100ms = 0; volatile uint8_t flag_10ms = 0; void TIM2_IRQHandler(void) { flag_10ms = 1; if (++counter >= 10) { counter = 0; flag_100ms = 1; } } while (1) { if (flag_10ms) { flag_10ms = 0; Key_Scan(); } if (flag_100ms) { flag_100ms = 0; Led_Update(); } }这种写法比裸循环清晰很多,但标志位一多,代码依然会变得混乱。尤其是不同任务之间的“周期”和“执行时机”是散落着写的,代码复用性不强。
2.3 时间片轮询/软定时器调度
在标志位的基础上继续抽象,把每个任务封装成函数指针,配上一个周期和上次执行时间,就形成了“定时器模拟任务”,也就是本文的核心。这种架构的优点是:
- 任务表结构清晰,添加任务只需要注册一行。
- 任务周期一目了然。
- 主循环只需要调用一个调度函数。
- 是最接近 RTOS 的裸机写法。
2.4 实时操作系统 RTOS
RTOS 提供了真正的任务抽象:任务有自己的栈、优先级、状态机,由内核负责抢占式调度。用户不用关心“下一个该执行谁”,只需要创建任务并设置优先级。代价是需要额外的 RAM、ROM,以及任务同步机制的学习成本。
2.5 架构选型对照
| 架构 | 实时性 | 代码复杂度 | RAM/ROM 开销 | 适合项目规模 |
|---|---|---|---|---|
| 超级大循环 | 差 | 低 | 极低 | 1~2 个简单功能 |
| 定时器中断+标志位 | 中 | 中 | 低 | 3~5 个功能 |
| 定时器模拟任务 | 中 | 中 | 低 | 3~10 个功能 |
| 状态机 | 中 | 中高 | 低 | 状态明确的业务逻辑 |
| RTOS | 高 | 高 | 较高 | 10 个以上任务或强实时场景 |
3. 环境准备与硬件平台
为了让示例可运行、可验证,本文选用最常见的 STM32 平台来演示。版本细节请以你的实际工程为准。
3.1 硬件选型
- 主控:STM32F103C8T6 最小系统板。
- 板载资源:LED(两个)、按键、串口。
- 调试工具:ST-Link 或 J-Link。
STM32F103C8T6 是非常常见的学习型 MCU,资料丰富,用它演示定时器模拟任务很合适。如果你手边只有其他型号,例如 STM32G4、STM32L4 或者 ESP32,思路完全一致,只需要调整定时器时钟和引脚配置。
3.2 软件开发环境
- IDE:STM32CubeIDE 或 Keil MDK。
- 固件库:STM32CubeF1 HAL 库。
- 配置工具:STM32CubeMX。
- 串口工具:任意串口助手。
版本需要根据你的项目实际情况调整,本文示例以常见环境为例,重点演示配置思路。
3.3 CubeMX 基础配置
使用 CubeMX 创建工程时,需要做以下基础配置:
- 选择芯片型号 STM32F103C8T6。
- 配置时钟树,将 HCLK 设为 72MHz。
- 配置一个 LED 引脚为 GPIO_Output,例如 PB0、PB1。
- 配置一个按键引脚为 GPIO_Input,例如 PA0。
- 配置串口 USART1,用于打印调试信息。
- 配置一个基础定时器 TIM2,并使用内部时钟。
这里有一个关键选择点:为什么要用 TIM2,而不是使用 SysTick?
因为 STM32 的 HAL 库默认把 SysTick 用于HAL_Delay()和HAL_GetTick()等时间基准服务。如果直接改写 SysTick 中断,会影响整个 HAL 的延时和超时机制。为了不破坏这些依赖,示例中我们单独启用 TIM2 作为系统节拍源,这样调度器和 HAL 延时互不干扰。
3.4 示例工程结构
├── Core/ │ ├── Inc/ │ │ ├── main.h │ │ ├── soft_timer.h │ │ └── app_task.h │ └── Src/ │ ├── main.c │ ├── soft_timer.c │ ├── app_task.c │ └── stm32f1xx_it.c ├── Drivers/ └── 其他 CubeMX 生成文件soft_timer.c/h是软件调度器模块,app_task.c/h是用户任务模块。调度器与业务逻辑分离,可以方便地复用到其他项目。
4. 定时器模拟任务的核心原理
4.1 系统节拍 Tick
定时器模拟任务的物理基础是一个稳定、周期性的时钟中断。在 STM32F103 的 72MHz 主频下,将 TIM2 配置为 1ms 产生一次更新中断,就得到了一个 1kHz 的“心跳”,这个心跳通常被称为Tick。
每次进入中断,一个全局计数变量加一:
static volatile uint32_t s_tick = 0; void Soft_Tick_Handler(void) { s_tick++; }这个s_tick就是整个调度系统的时间基准。
4.2 任务控制块
为了让调度器知道“有哪些任务、任务多久跑一次、上次是什么时候跑的”,需要用一个结构体来保存任务信息:
typedef struct { void (*func)(void); // 任务函数指针 uint32_t period_ms; // 执行周期 uint32_t last_run; // 上一次执行时刻 uint8_t enabled; // 使能标记 } task_ctrl_t;这种结构体在 RTOS 中有一个对应的概念,叫任务控制块(TCB)。只是 RTOS 的 TCB 还包含栈指针、优先级、状态等信息,而我们这里只保留最基本的信息。
4.3 调度器的工作方式
调度器本身不复杂,它做的事情就是遍历任务表,判断当前时间是否达到任务周期。如果达到,就调用对应的函数:
void Soft_Task_Run(void) { uint32_t now = Soft_GetTick(); for (uint8_t i = 0; i < s_task_num; i++) { if (s_task_table[i].enabled == 0 || s_task_table[i].func == 0) { continue; } if ((now - s_task_table[i].last_run) >= s_task_table[i].period_ms) { s_task_table[i].last_run = now; s_task_table[i].func(); } } }这里有一个非常经典的细节:判断周期用的是now - last_run,而不是now >= last_run + period。原因是s_tick是uint32_t类型,运行足够久后必然会发生回绕。如果用加法,last_run + period一旦溢出,判断就会出错。而用无符号减法,即使now回绕到 0,只要真实时间差小于 2 的 32 次方毫秒,差值依然是正确的。这是嵌入式系统编程中处理计数回绕的常用技巧。
4.4 为什么它看起来像“多任务”
每隔 1ms,TIM2 都会产生一次中断,s_tick加一。每次主循环执行Soft_Task_Run()时,调度器都会检查任务表:
- LED1 任务周期 100ms,所以它每 100 个 Tick 执行一次。
- 按键扫描任务周期 10ms,所以它每 10 个 Tick 执行一次。
- 串口打印任务周期 1000ms,所以它每 1000 个 Tick 执行一次。
任务之间相互独立,新的功能只需要往任务表里注册一个函数,主循环不需要改动。从逻辑上看,这些函数就好像是“并行”在跑。由于 MCU 是单核的,这种并行是假的,所以更准确的称呼是模拟任务或时间片轮询任务。
5. 完整实战:基于 TIM2 的模拟任务调度器
5.1 项目需求
假设我们要在一个 STM32F103 开发板上实现以下几个“任务”:
| 任务 | 周期 | 行为 |
|---|---|---|
| LED1 闪烁 | 100ms | 翻转 PB0 |
| LED2 闪烁 | 300ms | 翻转 PB1 |
| 按键扫描 | 10ms | 检测 PA0,按下时打印事件 |
| 调试信息打印 | 1000ms | 打印当前系统 Tick |
这个需求用裸循环写会非常别扭,用本文的定时器模拟任务写就很清晰。
5.2 配置定时器初始化
首先在 CubeMX 中配置 TIM2:
- Prescaler:71(即 72 分频)
- Counter Period:999(即自动重装值 1000)
- AutoReloadPreload:Enable
- 使能 TIM2 全局中断
在 72MHz 的定时器时钟下,TIM2 的更新频率为:
72MHz / 72 / 1000 = 1kHz,也就是 1ms 产生一次更新中断。
CubeMX 生成的初始化函数核心思路如下:
// 文件:Core/Src/tim.c (核心片段) void MX_TIM2_Init(void) { TIM_ClockConfigTypeDef sClockSourceConfig = {0}; TIM_MasterConfigTypeDef sMasterConfig = {0}; htim2.Instance = TIM2; htim2.Init.Prescaler = 71; htim2.Init.CounterMode = TIM_COUNTERMODE_UP; htim2.Init.Period = 999; htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; htim2.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE; if (HAL_TIM_Base_Init(&htim2) != HAL_OK) { Error_Handler(); } sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL; if (HAL_TIM_ConfigClockSource(&htim2, &sClockSourceConfig) != HAL_OK) { Error_Handler(); } sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET; sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE; if (HAL_TIMEx_MasterConfigSynchronization(&htim2, &sMasterConfig) != HAL_OK) { Error_Handler(); } HAL_NVIC_SetPriority(TIM2_IRQn, 1, 0); HAL_NVIC_EnableIRQ(TIM2_IRQn); }重点注意:如果你的系统时钟不是 72MHz,Prescaler 和 Period 需要重新计算。不要直接照抄初值,否则时间基准会不准。
5.3 编写软件调度器模块
创建soft_timer.h:
// 文件:Core/Inc/soft_timer.h #ifndef SOFT_TIMER_H #define SOFT_TIMER_H #include <stdint.h> #define TASK_MAX_NUM 8 typedef struct { void (*func)(void); // 任务函数指针 uint32_t period_ms; // 执行周期 uint32_t last_run; // 上一次执行时刻 uint8_t enabled; // 使能标记 } task_ctrl_t; void Soft_Task_Init(void); uint8_t Soft_Task_Register(void (*func)(void), uint32_t period_ms); void Soft_Task_Run(void); void Soft_Tick_Handler(void); uint32_t Soft_GetTick(void); #endif创建soft_timer.c:
// 文件:Core/Src/soft_timer.c #include "soft_timer.h" static volatile uint32_t s_tick = 0; static task_ctrl_t s_task_table[TASK_MAX_NUM]; static uint8_t s_task_num = 0; void Soft_Tick_Handler(void) { s_tick++; } uint32_t Soft_GetTick(void) { return s_tick; } void Soft_Task_Init(void) { s_tick = 0; s_task_num = 0; for (uint8_t i = 0; i < TASK_MAX_NUM; i++) { s_task_table[i].func = 0; s_task_table[i].period_ms = 0; s_task_table[i].last_run = 0; s_task_table[i].enabled = 0; } } uint8_t Soft_Task_Register(void (*func)(void), uint32_t period_ms) { if (func == 0 || s_task_num >= TASK_MAX_NUM) { return 0; // 注册失败 } s_task_table[s_task_num].func = func; s_task_table[s_task_num].period_ms = period_ms; s_task_table[s_task_num].last_run = 0; s_task_table[s_task_num].enabled = 1; s_task_num++; return 1; } void Soft_Task_Run(void) { uint32_t now = Soft_GetTick(); for (uint8_t i = 0; i < s_task_num; i++) { if (s_task_table[i].enabled == 0 || s_task_table[i].func == 0) { continue; } if ((now - s_task_table[i].last_run) >= s_task_table[i].period_ms) { s_task_table[i].last_run = now; s_task_table[i].func(); } } }这个模块不依赖任何 HAL 的私有类型,只用了stdint.h,所以复用到 STM32 其他系列甚至其他 MCU 时非常方便。只要底层有一个 1ms 的中断调用Soft_Tick_Handler()即可。
5.4 编写用户任务
创建app_task.c:
// 文件:Core/Src/app_task.c #include "app_task.h" #include "main.h" #include "soft_timer.h" void Led1_Task(void) { HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0); } void Led2_Task(void) { HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_1); } void Key_Scan_Task(void) { if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET) { // 这里只做按键事件标记,真正的业务逻辑放到主循环处理 // 避免在任务里做过多耗时操作 } } void Debug_Print_Task(void) { printf("tick = %lu ms\r\n", (unsigned long)Soft_GetTick()); }任务函数本身非常简单,这是有意识的。在模拟任务架构里,任务函数应当尽快返回,不能出现长时间阻塞。任务的职责是“发现事件”和“提交事件”,真正耗时的事情应该拆分或放到合适的地方。
5.5 定时器中断回调
更新中断由 HAL 库接管,用户只需要重写回调函数。
在stm32f1xx_it.c中,CubeMX 已经生成了:
void TIM2_IRQHandler(void) { HAL_TIM_IRQHandler(&htim2); }在此基础上,我们重写 HAL 的回调函数:
// 可以放在 main.c 或 stm32f1xx_it.c 中 void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM2) { Soft_Tick_Handler(); } }HAL_TIM_PeriodElapsedCallback是 HAL 库的弱函数(weak),用户实现同名的强函数后会覆盖默认版本。HAL_TIM_IRQHandler在检测到更新事件时,会调用这个回调。
5.6 主函数集成
// 文件:Core/Src/main.c #include "main.h" #include "soft_timer.h" #include "app_task.h" TIM_HandleTypeDef htim2; int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_USART1_UART_Init(); MX_TIM2_Init(); // 启动 TIM2 更新中断 HAL_TIM_Base_Start_IT(&htim2); // 初始化软件调度器 Soft_Task_Init(); // 注册任务 Soft_Task_Register(Led1_Task, 100); // 100ms 翻转一次 LED1 Soft_Task_Register(Led2_Task, 300); // 300ms 翻转一次 LED2 Soft_Task_Register(Key_Scan_Task, 10); // 10ms 扫描一次按键 Soft_Task_Register(Debug_Print_Task, 1000); // 1s 打印一次调试信息 while (1) { // 主循环不断驱动调度器 Soft_Task_Run(); } }注意printf的重定向:在 Keil 中通常勾选 MicroLIB,然后实现fputc;在 STM32CubeIDE 中可以用 retarget 的方式重定向到 USART。这块不同编译器差异较大,建议按实际工程配置。
5.7 运行与预期结果
烧录程序后,预期现象:
- PB0 上的 LED 以 100ms 为周期翻转,即 5Hz 闪烁。
- PB1 上的 LED 以 300ms 为周期翻转,约 1.67Hz 闪烁。
- 串口每 1s 输出一次
tick = xxx ms。 - 按键按下时,在键扫描任务中检测到电平变化。
可以看到,不同周期的“任务”在同一块 MCU 上互不干扰地运行。
6. 常见问题与排查思路
在实际使用中,定时器模拟任务可能会遇到以下几类问题。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 任务完全不执行 | 定时器没有启动,或中断回调没写对 | 确认调用HAL_TIM_Base_Start_IT,确认回调函数是否重写 |
| 任务执行周期不准 | 任务函数内部存在HAL_Delay或长耗时操作 | 去掉阻塞延时,把耗时逻辑拆分或改用状态机 |
| 周期越跑越偏 | 使用now + period的方式累加时间 | 改用now - last_run >= period的判断方式 |
| 中断一打开程序就卡死 | 中断服务函数里调用了阻塞 API | ISR 中只做标志位和计数,不处理业务 |
| 共享变量偶尔异常 | 中断与主循环共享变量没有声明volatile | 对跨中断共享的变量添加volatile,必要时临界区保护 |
| 新注册的任务数组越界 | 注册任务数量超过TASK_MAX_NUM | 增加任务数组上限,或在注册接口中做边界判断 |
| 任务执行顺序不符合预期 | 调度器遍历顺序是数组顺序 | 调整注册顺序,或根据优先级重新排序任务表 |
6.1 任务不执行
最常见的原因就是忘了启动定时器。CubeMX 只生成了MX_TIM2_Init(),但不会自动开始产生中断。必须手动调用:
HAL_TIM_Base_Start_IT(&htim2);另外还要检查回调函数是否真的被重写。如果在其他地方也定义了HAL_TIM_PeriodElapsedCallback,会导致链接错误或覆盖。
6.2 任务里使用了 HAL_Delay
这是最容易踩的坑:
void Led1_Task(void) { HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0); HAL_Delay(50); // 错误:会卡住整个调度器 }HAL_Delay是一个阻塞延时函数,它会一直占用 CPU 直到延时结束。如果任务里调用它,其他任务在这 50ms 内都无法执行,调度器的“并行”效果被彻底破坏。
正确的做法是:任务通过周期属性控制节奏,而不是在任务内部使用阻塞延时。LED 闪烁只需要设置周期为 100ms,翻转一次后立即返回,由调度器决定下一次什么时候调用它。
6.3 时间基准的回绕问题
uint32_t的 Tick 最大值大约是 49.7 天。对于长期运行的设备,溢出回绕是必然事件。但只要坚持使用无符号减法判断周期,回绕不会影响功能:
if ((now - last_run) >= period) { /* 正确 */ }不要写:
if (now >= last_run + period) { /* 错误 */ }后者在last_run + period溢出时会得到错误结果。
7. 定时器模拟任务的边界与工程建议
7.1 明确适用边界
定时器模拟任务本质上是协作式调度,它假设每个任务都会“自觉”地快速返回。如果某个任务执行时间太长,或者任务之间出现互相等待,整个系统就会卡住。
适用边界的判断标准很简单:
- 如果所有任务的最长执行时间之和远小于系统节拍,可以放心使用。
- 如果某个任务执行时间已经接近节拍周期,或者多个任务之间需要信号量、消息队列这类同步机制,建议升级到 RTOS。
7.2 ISR 中只做最少量工作
定时器中断里的Soft_Tick_Handler()只增加一个计数值,时间复杂度是 O(1)。这是正确做法。千万不要在定时器中断里直接调用业务任务,比如:
// 错误示例:在中断里执行任务函数 void TIM2_IRQHandler(void) { Led1_Task(); }一旦任务函数内部有复杂逻辑或阻塞延时,中断服务程序就会被拖得很长,影响其他中断的实时响应。
7.3 数据共享与临界区
主循环的任务函数和中断之间会共享变量,典型的例子是s_tick。在 C 语言层面,这个变量必须用volatile修饰,防止编译器把它优化进寄存器而不是内存。
static volatile uint32_t s_tick = 0;如果任务之间需要共享较复杂的结构体或缓冲区,可以考虑短暂关闭中断来保护临界区:
__disable_irq(); // 访问共享数据 __enable_irq();但临界区不宜过长,否则会影响定时器中断的实时性。更优雅的做法是使用“生产者-消费者”模式:中断只负责放数据,任务循环负责取数据。
7.4 任务表中的优先级策略
定时器模拟任务的执行顺序是按照任务表的注册顺序来的。如果你希望高优先级任务先执行,只需把高优先级任务放在数组前面。不过这只是“优先执行”的静态顺序,并不是抢占式调度。要时刻记住:所有任务都必须相互配合,才能保证整个系统的实时性。
7.5 可维护性建议
在实际项目中,建议把调度器模块和业务任务模块分开,职责界限要清晰:
soft_timer.c只提供注册、初始化、运行、Tick 管理四个接口,不包含任何业务逻辑。app_task.c只编写具体的任务函数,不关心调度器如何实现。- 一个任务函数只做一件事,如果任务很复杂,内部可以采用状态机拆分。
这样后续新增功能时,只需要注册一个新任务,不需要改动调度器核心代码。即使以后从裸机迁移到 FreeRTOS,也可以把任务函数直接搬过去,注册方式改为创建 FreeRTOS 任务即可。
8. 延伸思考:从定时器模拟任务到 RTOS
如果你已经完全理解了定时器模拟任务的原理,再去看 RTOS 的任务调度会有一种“豁然开朗”的感觉。
FreeRTOS 的核心也是一个 Tick,通常由 SysTick 产生,频率常见为 1kHz。每个任务有自己的任务控制块和独立栈空间,内核根据优先级和状态进行调度。它比定时器模拟任务强大得多,可以解决阻塞等待、任务挂起、事件通知、消息队列、信号量等复杂问题。但它的底层思想,依然是“用定时器产生节拍,在节拍驱动下切换任务”。
因此,建议学习路径是:
- 先用超级大循环完成小项目,理解硬件外设的基本使用。
- 再用定时器模拟任务重组代码,体会任务抽象带来的可维护性提升。
- 遇到任务协作复杂、实时性要求高的场景,再切换到 FreeRTOS或 RT-Thread,学习优先级抢占、消息队列、信号量等机制。
动手改出你的第一个任务调度器,比纠结一百个理论概念都要管用。先用本文的Soft_Task_Register()把现有项目的while(1)重构一下,观察代码结构的变化;再尝试把 LED 周期从 100ms 改成 250ms,或者增加一个串口数据接收任务。只有亲手跑通一遍,才能真正理解定时器模拟任务的边界和优势。
如果这篇文章对你有帮助,可以收藏备用。也欢迎在评论区聊聊你在裸机项目中是怎么组织多任务的,是坚持超级大循环,还是也写了自己的软件定时器调度器。