[i=s] 本帖最后由 EPTmachine 于 2025-11-8 18:43 编辑 [/i]
@21小跑堂、#申请原创
裸机编程时,常见的方法是在主函数完成外设、系统变量等资源的初始化,使用一个 While(1)循环来接收外设数据、进行数据处理以及发送数据到外设。示例代码结构如下:
int main()
{
Init_Periph();//调用外设初始化函数;
Init_Var();//嗲用系统变量初始化;
while(1)
{
Task1();//调用task1接收外设数据
Task2();//进行数据处理
Task3();//发送数据到外设
}
}
对于while循环中执行的代码,从时间轴上来看,三个任务是顺序执行,而且在每次循环中,三个任务都会调用,三个任务的调用周期频率是一样的。

这种设计简单,适合对于执行周期要求不高的程序。当三个任务的调用周期不同时,程序的执行周期由其中执行周期最短的任务决定,而执行周期长的任务在每个执行周期中需要检查当前周期是否需要执行,会增加额外的代码。而且由于执行周期短,在单个执行周期内,出现任务无法完成的情况时,会出现执行超时的情况。基于以上的限制,需要划分不同的任务,根据其执行周期进行不同周期的调用,这里可以使用时间触发调度的方式对任务的运行进行管理。
1、时间触发调度
时间触发调度是基于一个定时器周期性中断进行任务的调用,当达到任务执行周期后,执行任务函数。任务之间无法抢占,但是可以为每个任务指定执行周期,提高MCU的处理能力。
时间触发调度功能由以下部分组成:
- 调度器数据接口;
- 调度器初始化函数;
- 向调度器增加任务函数
- 调用任务检查函数启动相应的任务函数
1.1 任务调度数据结构
管理任务调度的数据接口如下,包含指向任务函数的指针、任务启动演示以及任务执行周期。还有用于任务数据上限以及限制调度周期的宏。
typedef struct
{
// 任务函数指针
void (*pTask) (void);
// 任务在延时Delay后执行
uint32_t Delay;
// 任务执行周期
uint32_t Period;
} sTask_t;
#define SCH_MAX_TASKS (20)
// 用于监视单个调度周期是否超市
#define SCH_TICK_COUNT_LIMIT (1)
// 任务空指针
#define SCH_NULL_PTR ((void (*) (void)) 0)
1.2 接口函数实现
实现时间触发的调度的函数包括调度器初始化、任务添加、调度器更新以及任务检查函数。
调度器初始化函数将函数指针初始化为空指针。
// 任务管理数组
static sTask_t SCH_tasks_g[SCH_MAX_TASKS];
void SCH_Init_Milliseconds(const uint32_t TICKms)
{
for (uint32_t Task_id = 0; Task_id < SCH_MAX_TASKS; Task_id++)
{
// 初始化任务指针
SCH_tasks_g[Task_id].pTask = SCH_NULL_PTR;
}
}
添加任务函数的相关信息的函数如下,将新的任务添加到任务数组中
void SCH_Add_Task(void (* pTask)(),
const uint32_t DELAY,
const uint32_t PERIOD)
{
uint32_t Task_id = 0;
//寻找空的任务指针,用于存放任务函数指针
while ((SCH_tasks_g[Task_id].pTask != SCH_NULL_PTR)
&& (Task_id < SCH_MAX_TASKS))
{
Task_id++;
}
// 指定任务首次执行的延时以及执行周期
SCH_tasks_g[Task_id].pTask = pTask;
SCH_tasks_g[Task_id].Delay = DELAY + 1;
SCH_tasks_g[Task_id].Period = PERIOD;
}
任务调度函数的实现如下,检查任务是否就绪并调用函数,Systick属于所有CortexM系列都带有的功能,可以用于时间触发源,用于任务的调用。当函数执行完成后,利用MCU从Standby模式唤醒的功能,让MCU进入Stanby模式,从而节省MCU的能耗。
static uint32_t Tick_count_g = 0;
void SysTick_Handler(void)
{
++Tick_count_g;
}
void SCH_Dispatch_Tasks(void)
{
__disable_irq();
uint32_t Update_required = (Tick_count_g > 0); // 检查计时变量
__enable_irq();
while (Update_required)
{
// 遍历任务数组,检查
for (uint32_t Task_id = 0; Task_id < SCH_MAX_TASKS; Task_id++)
{
// Check if there is a task at this location
if (SCH_tasks_g[Task_id].pTask != SCH_NULL_PTR)
{
if (--SCH_tasks_g[Task_id].Delay == 0)
{
(*SCH_tasks_g[Task_id].pTask)(); // 运行计时完成的任务
// 设置任务等待延时,切换到延时状态
SCH_tasks_g[Task_id].Delay = SCH_tasks_g[Task_id].Period;
}
}
}
__disable_irq();
Tick_count_g--;
Update_required = (Tick_count_g > 0); // Check again
__enable_irq();
}
// 切换MCU到Standby模式
__WFI();
}
1.3 示例程序
STM32的NUCLE-C031开发板属于入门级开发板,使用的是Cortex-M0内核,使用时钟触发调度来管理多个任务运行是不错选择。在STM32配套的USART_Printf示例程序中添加以上始终触发调度模块,在SysTick_Handler函数中添加计数变量的更新;
void SysTick_Handler(void)
{
/* USER CODE BEGIN SysTick_IRQn 0 */
Tick_count_g++;
/* USER CODE END SysTick_IRQn 0 */
HAL_IncTick();
/* USER CODE BEGIN SysTick_IRQn 1 */
/* USER CODE END SysTick_IRQn 1 */
}
创建两个任务用于演示,分别通过串口打印信息来表示任务运行。
#include "sch.h"
void task1(void);
void task2(void);
void task1(void)
{
printf("task1 exec\r\n");
}
void task2(void)
{
printf("task2 exec\r\n");
}
在主函数中添加任务调度器的初始化、任务添加以及任务检查模块的调用。
int main(void)
{
SCH_Init_Milliseconds(1);
SCH_Add_Task(task1, 0, 1000); //任务1设定为1s执行1次
SCH_Add_Task(task2, 0, 2000); //任务2设定为2秒执1次
while (1)
{
SCH_Dispatch_Tasks();
}
}
编译并下载程序到开发板,运行程序的效果如下,可以看到task1每执行两次,task2执行1此,符合预期。

2、总结
时间触发的任务调度,实现简单,仅需一个时钟中断即可使用,缺点是在中断频繁时会影响程序的运行周期;