STM32任务调度 – 时间片/Schedule

一、时间片调度概述

任务线程逻辑调度器,采用时间片查询的方式,每个任务没有自己的堆栈,不会进行强行跳转,任何一个任务时间过长都会导致其它任务时间延迟,任务可以是事件驱动型,或者是周期型,或者两者都是,先执行事件驱动,然后是周期运行,系统时间节拍为微秒级别,并且具有任务注册机制;

任务调度逻辑:首先运行调度器执行逻辑之外的任务(最高优先级),再运行调度器执行逻辑之内的任务,遍历全部任务队列更新任务动态优先级,更新静态优先级不在调度程序逻辑之外任务的动态优先级,如果该任务有检查函数(由事件驱动),则增加任务动态优先级,如果该任务无检查函数(无事件驱动),则计算任务周期年龄=(当前时间节拍-任务最后调用的时间节拍)/ 任务执行周期,如果任务周期年龄大于0说明任务的执行周期发生了延迟,增加该任务动态优先级,最后选择动态优先级高的任务执行,添加到目前为止用于检查函数和调度器逻辑的时间,如果用于检查函数和调度器逻辑的时间小于到下一次执行陀螺仪线程的剩余节拍数,则执行该任务,否则不执行。

注:此方式适用于实时控制场景,比如固定频率控制周期。

二、时间片调度时基
1、中断管理

atomic.h(中断屏蔽器):

#pragma once

#include <stdint.h>

/*!

 * ARM BASEPRI操作 – 使用全局内存屏蔽恢复BASEPRI(称为cleanup函数)

 * @param val 优先级

 */

static inline void __basepriRestoreMem(uint8_t *val)

{

    // 设置基本优先级 – 将给定的值赋给基本优先级寄存器

    // 设置为val后,屏蔽所有优先级数值大于等于val的中断和异常

    __set_BASEPRI(*val);

}

/*!

 * 使用全局内存屏障设置BASEPRI_MAX

 * @param prio 优先级

 * @return

 */

static inline uint8_t __basepriSetMemRetVal(uint8_t prio)

{

    // 使用条件设置基本优先级

    //  – 只有当BASEPRI屏蔽被禁用时,才会将给定的值分配给基本优先级寄存器,

    //    或者新值增加BASEPRI优先级级别。

    // 和BASEPRI类似,但有个限制,即后写入的优先级数值要比当前的BASEPRI值小才会起作用,

    // 否则不起作用。影响范围最广,影响CPU内的所有中断源。

    // 事实上BASEPRI_MAX和BASSEPRI是操作同一个寄存器,不过BASEPRI_MAX是一个条件写指令。

    __set_BASEPRI_MAX(prio);

    return 1;

}

/*!

  * 运行带有较高抢占优先级的块(使用BASEPRI_MAX)\

  * 在退出时恢复抢占优先级处理所有的退出路径,实现为for循环

  * 拦截中断和继续Full memory barrier被放置在block的开始处和出口处

 * _unused__属性用于抑制CLang警告

 */

#define ATOMIC_BLOCK(prio) for ( uint8_t \

    __ToDo = __basepriSetMemRetVal(prio); __ToDo ; __ToDo = 0 )

nvic.h(NVIC中断管理):

#pragma once

// NVIC优先级分组配置

#define NVIC_PRIORITY_GROUPING        NVIC_PRIORITYGROUP_4

// 构建NVIC中断优先级 – 抢占优先级,响应优先级

#define NVIC_BUILD_PRIORITY(base,sub) (((((base)<<(4-(7-(NVIC_PRIORITY_GROUPING>>8))))| \

        ((sub)&(0x0f>>(7-(NVIC_PRIORITY_GROUPING>>8)))))<<4)&0xf0)

// 抢占优先级 – 数值为NVIC_BUILD_PRIORITY设置的base数值

#define NVIC_PRIORITY_BASE(prio)      (((prio)>>(4-(7-(NVIC_PRIORITY_GROUPING>>8))))>>4)

// 响应优先级 – 数值为NVIC_BUILD_PRIORITY设置的sub数值

#define NVIC_PRIORITY_SUB(prio)       (((prio)&(0x0f>>(7-(NVIC_PRIORITY_GROUPING>>8))))>>4)

// 建立外设NVIC中断优先级(不能用 0)

#define NVIC_PRIO_MAX                 NVIC_BUILD_PRIORITY(0, 1)

2、时间节拍

timebase.h

#pragma once

#include <stdio.h>

// 根据不同芯片包含

#include "stm32f4xx_hal.h"

/*!

  * 时间节拍调用

 */

void tickIncrease(void);

/*!

  * 循环计数器初始化

 */

void cycleCounterInit(void);

/*!

  * 获取当前时间节拍(ISR)

 * @return 当前时间节拍(us)

 */

uint32_t microsISR(void);

/*!

  * 获取当前时间节拍

 * @return 当前时间节拍(us)

 */

uint32_t micros(void);

/*!

  * 获取系统正常运行时间(ms)

 */

uint32_t millis(void);

/*!

  * 微秒延时

 * @param us 延时时间

 */

void delayMicroseconds(uint32_t us);

/*!

  * 毫秒延时

 * @param ms 延时时间

 */

void delay(uint32_t ms);

timebase.c

#include "timebase.h"

#include "atomic.h"

#include "nvic.h"

// 微秒循环计数

static uint32_t usTicks = 0;

// 当前运行时间为1kHz systick定时器

static volatile uint32_t sysTickUptime = 0;

static volatile uint32_t sysTickValStamp = 0;

// 缓存的RCC值->CSR

uint32_t cachedRccCsrValue;

// cpu时钟频率

static uint32_t cpuClockFrequency = 0;

// SysTick等待

static volatile int sysTickPending = 0;

void tickIncrease(void)

{

    // 临界保护 – 不允许被打断

    ATOMIC_BLOCK(NVIC_PRIO_MAX) {

        sysTickUptime++;                    // 毫秒计数

        sysTickValStamp = SysTick->VAL;     // 读取当前计数值

        sysTickPending = 0;                 // 清除挂起

        (void)(SysTick->CTRL);

    }

}

void cycleCounterInit(void)

{

    // 设置中断优先级分组

    HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITY_GROUPING);

    // 配置系统时钟频率

    cpuClockFrequency = HAL_RCC_GetSysClockFreq();

    // 配置微秒循环计数值 – 1us

    usTicks = cpuClockFrequency / 1000000;

}

uint32_t microsISR(void)

{

    register uint32_t ms, pending, cycle_cnt;

    // 临界保护

    ATOMIC_BLOCK(NVIC_PRIO_MAX) {

        cycle_cnt = SysTick->VAL;      // 读取计数值

        // 判断是否有计数至0事件

        if (SysTick->CTRL & SysTick_CTRL_COUNTFLAG_Msk) {

            sysTickPending = 1;        // 更新挂起

            cycle_cnt = SysTick->VAL;  // 再次读取计数值以确保在翻转后读取该值

        }

        ms = sysTickUptime;

        pending = sysTickPending;

    }

    return ((ms + pending) * 1000) + (usTicks * 1000 – cycle_cnt) / usTicks;

}

uint32_t micros(void)

{

    // 一般情况下,变量的值是存储在内存中的,MCU 每次使用数据都要从内存中读取。

    // 如果有一些变量使用非常频繁,从内存中读取就会消耗很多时间。

    // 为了解决这个问题,可以将使用频繁的变量放在MCU的通用寄存器中,

    // 这样使用该变量时就不必访问内存,直接从寄存器中读取,大大提高程序的运行效率。

    // register关键字请求“编译器”将变量放到寄存器里。

    // 寄存器的数量是有限的,通常是把使用最频繁的变量定义为register

    // 信息交换: MCU <– 寄存器 <– 内存

    register uint32_t ms, cycle_cnt;

    // 在中断(中断控制状态寄存器)和提升(非零)BASEPRI上下文中调用microsISR()

    if ((SCB->ICSR & SCB_ICSR_VECTACTIVE_Msk) || (__get_BASEPRI())) {

        return microsISR();

    }

    do {

        ms = sysTickUptime;            // 获取当前ms计数

        cycle_cnt = SysTick->VAL;      // 读取当前计数值

    } while (ms != sysTickUptime || cycle_cnt > sysTickValStamp);

    //                      us        +     (168000 – VAL) / 168

    // 微秒计数 = (毫秒计数 * 1000) + (系统时钟频率 / 1000 – systick计数值) / (系统时钟频率 / 1000000)

    //              (usTicks = 168000000 / 1000000)(systick计数值:倒数168000下为1ms,自动重装初值COUNTFLAG置位并触发中断)

    return (ms * 1000) + (usTicks * 1000 – cycle_cnt) / usTicks;

}

uint32_t millis(void)

{

    return sysTickUptime;

}

void delayMicroseconds(uint32_t us)

{

    uint32_t now = micros();

    while (micros() – now < us);

}

void delay(uint32_t ms)

{

    while (ms–)

        delayMicroseconds(1000);

}

RT-Thread嘀嗒定时器中断服务函数(drivers/drv_common.c):

/**

 * This is the timer interrupt service routine.

 *

 */

void SysTick_Handler(void)

{

//    /* enter interrupt */

//    rt_interrupt_enter();

//

//    HAL_IncTick();

//    rt_tick_increase();

//

//    /* leave interrupt */

//    rt_interrupt_leave();

    tickIncrease();

}

如果使用纯时间片调度,需要屏蔽RT时间节拍提供。

3、使用示例
#include <rtthread.h>
#include <rtdevice.h>
#include "timebase.h"

#define DBG_TAG "main"
#define DBG_LVL DBG_LOG
#include <rtdbg.h>

int main(void)
{
    // 初始化循环计数器
    cycleCounterInit();

    while (1) {
        rt_kprintf("status: %d\n", 1);
        delay(1000);
        //rt_thread_mdelay(1000);
    }

    return RT_EOK;
}
三、时间片调度实现
1、任务调度器

schedule.h

#pragma once

#pragma once

#include <stdio.h>

#include <stdbool.h>

#include "timebase.h"

/*!

  * 任务执行周期计算宏

 */

#define TASK_PERIOD_HZ(hz) (1000000 / (hz))

#define TASK_PERIOD_MS(ms) ((ms) * 1000)

#define TASK_PERIOD_US(us) (us)

/*!

  * 任务优先级枚举

 */

typedef enum {

    TASK_PRIORITY_REALTIME = -1,    // 任务将在调度逻辑之外运行

    TASK_PRIORITY_IDLE = 0,         //!< TASK_PRIORITY_IDLE

    TASK_PRIORITY_LOW = 1,          //!< TASK_PRIORITY_LOW

    TASK_PRIORITY_MEDIUM = 3,       //!< TASK_PRIORITY_MEDIUM

    TASK_PRIORITY_MEDIUM_HIGH = 4,  //!< TASK_PRIORITY_MEDIUM_HIGH

    TASK_PRIORITY_HIGH = 5,         //!< TASK_PRIORITY_HIGH

    TASK_PRIORITY_MAX = 255         //!< TASK_PRIORITY_MAX

} taskPriority_e;

/*!

  * 任务结构

 */

typedef int32_t timeDelta_t;              // 时差

typedef uint32_t timeMs_t ;               // 毫秒

typedef uint32_t timeUs_t;                // 微秒

typedef struct {

    // 检查函数指针(事件驱动)

    bool (*checkFunc)(timeUs_t currentTimeUs, timeDelta_t currentDeltaTimeUs);

    // 任务函数指针

    void (*taskFunc) (timeUs_t currentTimeUs);

    // 任务基本信息

    timeDelta_t desiredPeriodUs;               // 执行周期(多久运行一次)

    const int8_t staticPriority;               // 静态优先级

    // 任务调度信息

    uint16_t dynamicPriority;                  // 动态优先级

    uint16_t taskAgeCycles;                    // 任务周期年龄(判断是否错过任务需要运行的时间)

    timeDelta_t taskLatestDeltaTimeUs;         // 任务时差

    timeUs_t lastExecutedAtUs;                 // 任务最后一次调用时间

    timeUs_t lastSignaledAtUs;                 // 检查任务最后一次调用时间

    timeUs_t lastDesiredAt;                    // 任务期望执行的时间

} task_t;

/*!

  * 任务信息结构

 */

typedef struct {

    // 任务队列

    task_t* taskQueue;

    // 任务队列数量

    uint8_t taskQueueSize;

} taskInfo_t;

/*!

  * 任务注册宏

  * 注册格式:检查任务函数,任务函数,执行周期,静态优先级

 */

#define DEFINE_TASK(checkFuncParam, taskFuncParam, desiredPeriodParam, staticPriorityParam) {  \

    .checkFunc = checkFuncParam, \

    .taskFunc = taskFuncParam, \

    .desiredPeriodUs = desiredPeriodParam, \

    .staticPriority = staticPriorityParam \

}

#define TASK_SIZE(task)  sizeof(task)/sizeof(task_t)

/*!

  * 获取时差

 * @param a 时间1

 * @param b 时间2

 * @return 时差

 */

static inline timeDelta_t cmpTimeUs(timeUs_t a, timeUs_t b)

{

    return (timeDelta_t)(a – b);

}

/*!

  * 任务调度器初始化

 * @param taskInfo 任务信息

 * @param tasks 任务结构

 * @param tasksSize 任务数量

 */

void schedulerInit(taskInfo_t *taskInfo, task_t *tasks, uint8_t tasksSize);

/*!

  * 任务调度

 * @param taskInfo 任务信息

 */

void scheduler(taskInfo_t *taskInfo);

schedule.c

#include "schedule.h"

void schedulerInit(taskInfo_t *taskInfo, task_t *tasks, uint8_t tasksSize)

{

    taskInfo->taskQueue = tasks;

    taskInfo->taskQueueSize = tasksSize;

}

inline static timeUs_t getPeriodCalculationBasis(const task_t* task)

{

    // 返回该任务最后一次调用的时间节拍

    return task->lastExecutedAtUs;

}

static timeUs_t schedulerExecuteTask(task_t *selectedTask, timeUs_t currentTimeUs)

{

    // 执行任务

    if (selectedTask) {

        // 获取任务增量时间 = 当前时间节拍 – 最后一次调用时间节拍

        selectedTask->taskLatestDeltaTimeUs = cmpTimeUs(currentTimeUs, selectedTask->lastExecutedAtUs);

        // 将当前的时间节拍作为该任务下一次最后一次调用的时间

        selectedTask->lastExecutedAtUs = currentTimeUs;

        // 最后期望执行的时间 += (当前时间节拍 – 最后期望执行的时间) /  执行周期

        selectedTask->lastDesiredAt += (cmpTimeUs(currentTimeUs, selectedTask->lastDesiredAt) /

            selectedTask->desiredPeriodUs) * selectedTask->desiredPeriodUs;

        // 动态优先级 = 0

        selectedTask->dynamicPriority = 0;

        // 任务执行阶段

        selectedTask->taskFunc(currentTimeUs);

    }

    return micros() – currentTimeUs;

}

void scheduler(taskInfo_t *taskInfo)

{

    // 获取当前时间节拍

    timeUs_t currentTimeUs = micros();

    // 任务选择(任务结构信息)

    task_t *selectedTask = NULL;

    // 选择任务动态优先级

    uint16_t selectedTaskDynamicPriority = 0;

    // —————————-1.更新任务动态优先级 – 遍历全部任务队列

    for(int i = 0;i < taskInfo->taskQueueSize;i++) {

        // 获取任务信息

        task_t *task = &taskInfo->taskQueue[i];

        // 更新任务动态优先级

        // ——-(1)该任务有检查函数(由事件驱动)

        if (task->checkFunc) {

            // 记录调用检查函数之前的时间节拍

            const timeUs_t currentTimeBeforeCheckFuncCallUs = currentTimeUs;

            // 如果该任务的动态优先级大于0,则增加事件驱动任务的动态优先级

            if (task->dynamicPriority > 0) {

                // 更新任务周期年龄 = ((当前时间节拍 – 任务最后一次调用时间)/ 任务执行周期 )+ 1

                task->taskAgeCycles = 1 + ((currentTimeUs – task->lastSignaledAtUs) / task->desiredPeriodUs);

                // 增加该任务动态优先级 =(静态优先级 * 任务周期年龄)+ 1

                task->dynamicPriority = 1 + task->staticPriority * task->taskAgeCycles;

            }

            // 执行检查函数 – 如果发生了事件则再次增加该任务动态优先级

            else if (task->checkFunc(currentTimeBeforeCheckFuncCallUs,

                cmpTimeUs(currentTimeBeforeCheckFuncCallUs, task->lastExecutedAtUs))) {

                // 该任务最后一次调用时间 = 当前时间节拍

                task->lastSignaledAtUs = currentTimeBeforeCheckFuncCallUs;

                // 该任务迫切需要得到运行 – 任务周期年龄置为 1

                task->taskAgeCycles = 1;

                // 增加该任务动态优先级 = 任务动态优先级 + 1

                task->dynamicPriority = 1 + task->staticPriority;

            } else {

                task->taskAgeCycles = 0;

            }

        }

        // —————————-2)该任务无检查函数(无事件驱动)

        else {

            // 计算任务周期年龄 = (当前时间节拍 – 任务最后调用的时间节拍)/ 任务执行周期

            task->taskAgeCycles = ((currentTimeUs – getPeriodCalculationBasis(task)) / task->desiredPeriodUs);

            // 如果任务周期年龄大于0说明任务的执行周期发生了延迟

            if (task->taskAgeCycles > 0) {

                // 增加该任务动态优先级 = (静态优先级 * 任务周期年龄)+ 1

                task->dynamicPriority = 1 + task->staticPriority * task->taskAgeCycles;

            }

        }

        // —————————-3)选择动态优先级高的任务

        if (task->dynamicPriority > selectedTaskDynamicPriority) {

            // 选定任务动态优先级

            selectedTaskDynamicPriority = task->dynamicPriority;

            // 调度该任务

            selectedTask = task;

        }

    }

    // —————————-2.执行动态优先级最高任务

    if (selectedTask) {

        schedulerExecuteTask(selectedTask, currentTimeUs);

    }

}

2、使用示例
#include <rtthread.h>
#include <rtdevice.h>
#include "schedule.h"

#define DBG_TAG "main"
#define DBG_LVL DBG_LOG
#include <rtdbg.h>

void Task_20ms(timeUs_t currentTimeUs) {
    rt_kprintf("Task_20ms.\n");
}

void Task_100ms(timeUs_t currentTimeUs) {
    rt_kprintf("Task_100ms.\n");
}

void Task_500ms(timeUs_t currentTimeUs) {
    rt_kprintf("Task_500ms.\n");
}

task_t xxxTask[] = {
    DEFINE_TASK(NULL, Task_20ms, TASK_PERIOD_MS(20), TASK_PRIORITY_MEDIUM_HIGH),
    DEFINE_TASK(NULL, Task_100ms, TASK_PERIOD_MS(100), TASK_PRIORITY_LOW),
    DEFINE_TASK(NULL, Task_500ms, TASK_PERIOD_MS(500), TASK_PRIORITY_HIGH),
};
taskInfo_t xxxTaskInfo;

int main(void)
{
    // 初始化循环计数器
    cycleCounterInit();
    // 初始化任务调度器
    schedulerInit(&xxxTaskInfo, &xxxTask, TASK_SIZE(xxxTask));

    while (1) {
        scheduler(&xxxTaskInfo);
    }

    return RT_EOK;
}

作者:Xiangfu DING

物联沃分享整理
物联沃-IOTWORD物联网 » STM32任务调度 – 时间片/Schedule

发表回复