近来工作之余,研究了一下APM的源码。
APM源码连接https://pan.baidu.com/s/17Dg1oEJT_fj12DM1BmZWxA
发现源码中有一个简单的任务调度器,不太重要的任务都在Scheduler中完成。为什么需要Scheduler呢??
飞行器姿态信息需要角速度积分,位置信息也需要光流的积分,也就是需要获取精确的时间间隔,对时间敏感。
Scheduler的主要作用是保证主函数运行的时间相同,什么原理呢?
假设我们主循环中有6个任务, 每个任务执行时间为大概20us - 1ms之间,而我们的控制周期要求是5ms,任务1、2、3是必要任务,每次循环都要执行的,比如飞行器的姿态解算,任务4、5是周期任务,任务4每隔4个周期执行一次,任务5每隔10个周期执行一次,任务6是非必要任务比如LED闪烁任务,一般情况下10个周期执行一次。我们可以写成如下伪代码,但是这样十分繁琐,并且修改移植起来十分麻烦,任务调度器就是代替这段代码的,方便移植用的。
int main()
{
uint32_t count = 0;
uint32_t time = 0;
while(1)
{
if(get_now_time_ms() - time > 5) //判断5ms是否过去
{
count ++;
time = get_now_time_ms(); //记录当前时间
Task1(); //任务1 1ms
Task2(); //任务2 1ms
Task3(); //任务3 1ms
if(get_now_time_ms() - time > 1 && count % 4 == 0)
{
Task4(); //任务4 1ms
}
if(get_now_time_ms() - time > 1 && count % 10 == 0)
{
Task5(); //任务5 1ms
if(get_now_time_ms() - time > 1 )
{
Task6(); //任务6 1ms
}
}
}
}
}
Scheduler也可以使代码达到上述代码的效果,换句话来说就是保证控制周期稳定。控制周期稳定有什么用呢? PID! 我们运用PID控制电机时,一般都会默认控制周期不变,从而将PID参数简化为Kp、Ki、Kd,注意使用Ki、Kd的前提是控制周期不变,而控制周期不变就可以Scheduler来解决了。
下面就是一个简单的任务调度器代码, 注意任务调度器需要一个时计器来提供时基。
scheduler.h
#ifndef __SCHEDULER_H
#define __SCHEDULER_H
#include "stdint.h"
typedef struct
{
void (*task_func)(void); //函数指针
uint16_t interval_ticks; //每隔多少周期执行一次
uint16_t max_time_micros; //该函数运行一次的大概时间us
uint32_t last_tick_counter;//上一次函数运行时tick
}sched_task_t;
typedef struct
{
void (* init )(uint8_t num);
void (* tick )(void);
void (* run )(uint32_t time_available);
}sched_t;
extern sched_t sched;
#endif //__SCHEDULER_H
scheduler.c
#include "Scheduler.h"
#include "stdio.h"
/* debug 调试宏定义 */
#define _MY_DEBUG_ 1
#ifdef _MY_DEBUG_
#define MY_DEBUG_PRINT_INFO(...) printf(__VA_ARGS__)
#define MY_DEBUG_PRINT_VAR(X, ...) printf("file: "__FILE__", Line: %d:"X"\r\n",__LINE__,##__VA_ARGS__)
#else
#define MY_DEBUG_PRINT_INFO(...)
#define MY_DEBUG_PRINT_VAR(X, ...)
#endif
/* 获取当前时间 us 注意这里需要换成自己的获取当前时间函数 */
#define GET_NOW_TIME_US systime.get_time_us()
/* 声明时分调度器任务表 */
extern sched_task_t sched_task[];
/* 任务数量 */
static uint8_t num_task;
/**
* @brief 时分任务调度器初始化
*
* @param
*
* @return
*
* @note 获取任务数量
*
* @example
*
* @date 2019/6/29 星期六
*/
void shceduler_init(uint8_t task_num)
{
num_task = task_num;
}
/* shceduler_run执行次数 */
static uint32_t tick_counter = 0;
/**
* @brief 记录执行周期数
*
* @param
*
* @return
*
* @note
*
* @example
*
* @date 2019/6/29 星期六
*/
void shceduler_tick()
{
tick_counter++;
}
/**
* @brief 调度
*
* @param
*
* @return
*
* @note
*
* @example
*
* @date 2019/6/29 星期六
*/
void shceduler_run(uint32_t time_available)
{
uint64_t now;
uint16_t dt;
uint16_t interval_ticks;
for (uint8_t i = 0; i < num_task; i++)
{
now = GET_NOW_TIME_US;//获取当前时间
dt = tick_counter - sched_task[i].last_tick_counter;//计算任务上次运行到现在相隔多少周期
interval_ticks = sched_task[i].interval_ticks; //任务相隔多少周期运行一次
if( dt >= interval_ticks)
{
//调试时使用,任务长时间未运行,说明任务的运行周期太长或者shceduler_run的可运行时间太短
if (dt >= interval_ticks*2)
{
MY_DEBUG_PRINT_VAR("Scheduler slip task[%u] (%u/%u/%u)\n",
(unsigned)i,
(unsigned)dt,
(unsigned)interval_ticks,
(unsigned)sched_task[i].max_time_micros);
}
if (sched_task[i].max_time_micros <= time_available)
{
//运行任务
sched_task[i].task_func();
//更新last_tick_counter
sched_task[i].last_tick_counter = tick_counter;
#ifdef _MY_DEBUG_
uint32_t end_time = GET_NOW_TIME_US - now;
//如果任务运行过后发现时间超出shceduler_run的可运行时间time_available,说明任务的max_time_micros设置小了
if(end_time > time_available)
{
MY_DEBUG_PRINT_VAR("Scheduler slip task[%u] (%u/%u)\n",
(unsigned)i,
(unsigned)end_time,
(unsigned)sched_task[i].max_time_micros);
return;
}
#endif //debug打印
}
}
//更新time_available
time_available -= (GET_NOW_TIME_US - now);
}
//更新tick_counter
sched.tick(); // 该函数作用为tick_counter++
}
sched_t sched =
{
shceduler_init,
shceduler_tick,
shceduler_run,
};
在使用时,调度器需要使用定时器,因此需要先行初始化定时器,确保 GET_NOW_TIME_US 可以使用,在对调度器进行初始化,确定任务数量
// 任务调度结构体,注意重要任务放在前面
sched_task_t sched_task[] =
{
// 函数指针 每隔多少周期执行一次 该函数运行一次的大概时间us 上一次函数运行时tick
{ loop_200hz , 1, 500, 0 },
{ loop_100hz , 2, 4000, 0 },
{ loop_50hz , 4, 100, 0 },
{ loop_20hz , 10, 4000, 0 },
};
/* 初始化时分任务调度器 */
sched.init(sizeof (sched_task) / sizeof (sched_task[0]));
最后在循环中调用调度器即可,调度器会自动对任务函数进行周期分配
/* 记录当前时间差 */
uint32_t current_time;
/* 记录上一次时间 */
uint32_t last_time;
while(1)
{
current_time = systime.get_time_us() - last_time;
if(current_time >= 5000)
{
last_time = systime.get_time_us();
sched.run(5000);
}
}