裸机开发告别while(1)大循环:定时器模拟任务调度原理与STM32实战
2026/9/17 10:20:48 网站建设 项目流程

做嵌入式开发的同学应该都经历过这样的阶段:一个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 创建工程时,需要做以下基础配置:

  1. 选择芯片型号 STM32F103C8T6。
  2. 配置时钟树,将 HCLK 设为 72MHz。
  3. 配置一个 LED 引脚为 GPIO_Output,例如 PB0、PB1。
  4. 配置一个按键引脚为 GPIO_Input,例如 PA0。
  5. 配置串口 USART1,用于打印调试信息。
  6. 配置一个基础定时器 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_tickuint32_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的判断方式
中断一打开程序就卡死中断服务函数里调用了阻塞 APIISR 中只做标志位和计数,不处理业务
共享变量偶尔异常中断与主循环共享变量没有声明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。每个任务有自己的任务控制块和独立栈空间,内核根据优先级和状态进行调度。它比定时器模拟任务强大得多,可以解决阻塞等待、任务挂起、事件通知、消息队列、信号量等复杂问题。但它的底层思想,依然是“用定时器产生节拍,在节拍驱动下切换任务”。

因此,建议学习路径是:

  1. 先用超级大循环完成小项目,理解硬件外设的基本使用。
  2. 再用定时器模拟任务重组代码,体会任务抽象带来的可维护性提升。
  3. 遇到任务协作复杂、实时性要求高的场景,再切换到 FreeRTOS或 RT-Thread,学习优先级抢占、消息队列、信号量等机制。

动手改出你的第一个任务调度器,比纠结一百个理论概念都要管用。先用本文的Soft_Task_Register()把现有项目的while(1)重构一下,观察代码结构的变化;再尝试把 LED 周期从 100ms 改成 250ms,或者增加一个串口数据接收任务。只有亲手跑通一遍,才能真正理解定时器模拟任务的边界和优势。

如果这篇文章对你有帮助,可以收藏备用。也欢迎在评论区聊聊你在裸机项目中是怎么组织多任务的,是坚持超级大循环,还是也写了自己的软件定时器调度器。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询