FreeRTOS 任务

发布时间 2023-05-23 15:18:31作者: wxk1213

使用RTOS时,一个实时任务可以作为一个独立的任务,任何一个时间点只有一个任务运行,具体由RTOS调度器决定。

RTOS调度器的职责是确保当一个任务开始执行的时候上下文环境与上一次推出的时候相同,每个任务都有堆栈,任务切换的时候将上下文保存在堆栈中。

任务特性:

1、简单

2、没有使用限制

3、支持抢占

4、支持优先级

5、每个任务都拥有堆栈导致RAM的使用量增大

6、如果使用抢占的话必须仔细的考虑重入的问题

 

(代码73)

 

 

列表是FreeRTOS中的一个数据结构,用来跟踪任务。

typedef struct xLIST
{
    listFIRST_LIST_INTEGRITY_CHECK_VALUE (1)
    configLIST_VOLATILE UBaseType_t uxNumberOfItems; (2)
    ListItem_t * configLIST_VOLATILE pxIndex; (3)
    MiniListItem_t xListEnd; (4)
    listSECOND_LIST_INTEGRITY_CHECK_VALUE (5)
} List_t;

(1) 和 (5) 、 这 两 个 都 是 用 来 检 查 列 表 完 整 性 的 , 需 要 将 宏
configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES 设置为 1,开启以后会向这两个地方分别添加一个变量 xListIntegrityValue1 和 xListIntegrityValue2,在初始化列表的时候会这两个变量中写入一个特殊的值,默认不开启这个功能。
(2)、uxNumberOfItems 用来记录列表中列表项的数量。
(3)、pxIndex 用来记录当前列表项索引号,用于遍历列表。
(4)、列表中最后一个列表项,用来表示列表结束

 

列表项就是存放在列表中的项目,FreeRTOS 提供了两种列表项:列表项和迷你列表项

struct xLIST_ITEM
{
    listFIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE (1)
    configLIST_VOLATILE TickType_t xItemValue; (2)
    struct xLIST_ITEM * configLIST_VOLATILE pxNext; (3)
    struct xLIST_ITEM * configLIST_VOLATILE pxPrevious; (4)
    void * pvOwner; (5)
    void * configLIST_VOLATILE pvContainer; (6)
    listSECOND_LIST_ITEM_INTEGRITY_CHECK_VALUE (7)
};
typedef struct xLIST_ITEM ListItem_t;

 (1)和(7)、用法和列表一样,用来检查列表项完整性的。

(2)、xItemValue 为列表项值。
(3)、pxNext 指向下一个列表项。
(4)、pxPrevious 指向前一个列表项,和 pxNext 配合起来实现类似双向链表的功能。
(5)、pvOwner 记录此链表项归谁拥有,通常是任务控制块。
(6)、pvContainer 用来记录此列表项归哪个列表。

 

 

 任务调度器

void vTaskStartScheduler( void )
{
    BaseType_t xReturn;
    xReturn = xTaskCreate( prvIdleTask, (1)
                            "IDLE", configMINIMAL_STACK_SIZE,
                            ( void * ) NULL,
                            ( tskIDLE_PRIORITY | portPRIVILEGE_BIT ),
                            &xIdleTaskHandle );
    #if ( configUSE_TIMERS == 1 ) //使用软件定时器使能
    {
        if( xReturn == pdPASS )
        {
            xReturn = xTimerCreateTimerTask(); (2)
        }
        else
        {
            mtCOVERAGE_TEST_MARKER();
        }
    }
    #endif /* configUSE_TIMERS */
    if( xReturn == pdPASS ) //空闲任务和定时器任务创建成功。
    {
        portDISABLE_INTERRUPTS(); (3)
        #if ( configUSE_NEWLIB_REENTRANT == 1 ) //使能 NEWLIB
        {
            _impure_ptr = &( pxCurrentTCB->xNewLib_reent );
        }
        #endif /* configUSE_NEWLIB_REENTRANT */
        xNextTaskUnblockTime = portMAX_DELAY;
        xSchedulerRunning = pdTRUE; (4)
        xTickCount = ( TickType_t ) 0U;
        portCONFIGURE_TIMER_FOR_RUN_TIME_STATS(); (5)
        if( xPortStartScheduler() != pdFALSE ) (6)
        {
            //如果调度器启动成功的话就不会运行到这里,函数不会有返回值的
        }
        else
        {
        //不会运行到这里,除非调用函数 xTaskEndScheduler()。
        }
    }
    else
    {
        //程序运行到这里只能说明一点,那就是系统内核没有启动成功,导致的原因是在创建
        //空闲任务或者定时器任务的时候没有足够的内存。
        configASSERT( xReturn != errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY );
    }
    //防止编译器报错,比如宏 INCLUDE_xTaskGetIdleTaskHandle 定义为 0 的话编译器就会提
    //示 xIdleTaskHandle 未使用。
    ( void ) xIdleTaskHandle;
}

(1)、创建空闲任务,如果使用静态内存的话使用函数 xTaskCreateStatic()来创建空闲任务,优先级为 tskIDLE_PRIORITY,宏 tskIDLE_PRIORITY 为 0,也就是说空闲任务的优先级为最低。
(2)、如果使用软件定时器的话还需要通过函数 xTimerCreateTimerTask()来创建定时器服务任务。
(3)、关闭中断,在 SVC 中断服务函数 vPortSVCHandler()中会打开中断。
(4)、变量 xSchedulerRunning 设置为 pdTRUE,表示调度器开始运行。
(5)、当宏 configGENERATE_RUN_TIME_STATS 为 1 的时候说明使能时间统计功能,此时
需要用户实现宏 portCONFIGURE_TIMER_FOR_RUN_TIME_STATS,此宏用来配置一个定时器/计数器。
(6)、调用函数 xPortStartScheduler()来初始化跟调度器启动有关的硬件,比如滴答定时器、FPU 单元和 PendSV 中断等等。

 

内核相关硬件初始化函数分析

FreeRTOS 系统时钟是由滴答定时器来提供的,而且任务切换也会用到PendSV 中断,这些硬件的初始化由函数 xPortStartScheduler()来完成,缩减后的函数代码如下: 

BaseType_t xPortStartScheduler( void )
{
    /******************************************************************/
    /****************此处省略一大堆的条件编译代码**********************/
    /*****************************************************************/
    portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI; (1)
    portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI; (2)
    vPortSetupTimerInterrupt(); (3)
    uxCriticalNesting = 0; (4)
    prvStartFirstTask(); (5)
    //代码正常执行的话是不会到这里的!
    return 0;
}

 (1)、设置 PendSV 的中断优先级,为最低优先级。
(2)、设置滴答定时器的中断优先级,为最低优先级。
(3)、调用函数 vPortSetupTimerInterrupt()来设置滴答定时器的定时周期,并且使能滴答定时
器的中断,函数比较简单,大家自行查阅分析。
(4)、初始化临界区嵌套计数器。
(5)、调用函数 prvStartFirstTask()开启第一个任务。

 

 

上下文切换被触发的场合可以是:

  执行一个系统调用

  系统滴答定时器(SyeTick)中断

 

  1. SVC中断(软件中断):SVC中断是一种软件中断,通常用于任务之间的通信。在FreeRTOS中,SVC中断可以打断最高优先级的任务。然而,任务永远不能被SVC中断打断。在SVC中断处理函数中,可以使用FreeRTOS提供的API函数,但需要注意的是,一些API函数在ISR内部是无效的,例如将任务置于阻塞状态。
  2. ISR中断(硬件中断):ISR中断是一种硬件中断,通常用于处理外部硬件事件,如输入输出、定时器到期等。在FreeRTOS中,ISR中断可以打断任何正在运行的任务。与SVC中断不同,ISR中断处理函数中可以使用独立的中断安全API函数。这些API函数可以在ISR内部执行,不会将任务置于阻塞状态。

 

  1. SVC中断(系统调用):SVC中断是一种软件中断,由SVC指令触发调用。它在FreeRTOS中用于在任务调度中开启第一个任务。SVC中断处理函数中可以使用FreeRTOS提供的API函数,但在ISR内部一些API函数可能会失效。
  2. PendSV中断(可悬起的系统调用):与SVC相关的是PendSV中断。PendSV中断是一种可悬起的系统调用,意味着它可以延迟响应,直到其他高优先级的中断处理完成。在FreeRTOS中, PendSV中断用于任务切换。当需要发生任务切换时,如果当前正在执行一个中断,则等待该中断执行完毕后,再进行任务切换。PendSV中断处理函数中可以使用独立的中断安全API函数。

 

(p144图)

(1) 任务 A 呼叫 SVC 来请求任务切换(例如,等待某些工作完成)
(2) OS 接收到请求,做好上下文切换的准备,并且 pend 一个 PendSV 异常。
(3) 当 CPU 退出 SVC 后,它立即进入 PendSV,从而执行上下文切换。
(4) 当 PendSV 执行完毕后,将返回到任务 B,同时进入线程模式。
(5) 发生了一个中断,并且中断服务程序开始执行
(6) 在 ISR 执行过程中,发生 SysTick 异常,并且抢占了该 ISR。
(7) OS 执行必要的操作,然后 pend 起 PendSV 异常以作好上下文切换的准备。
(8) 当 SysTick 退出后,回到先前被抢占的 ISR 中, ISR 继续执行
(9) ISR 执行完毕并退出后, PendSV 服务例程开始执行,并且在里面执行上下文切换。
(10) 当 PendSV 执行完毕后,回到任务 A,同时系统再次进入线程模式。 

 

 

在FreeRTOS中,有3个状态寄存器,分别是:

  1. xPSR(程序状态寄存器):记录ALU标志(0标志,进位标志,负数标志,溢出标志),执行状态以及当前正服务的中断号。当任务被中断时,xPSR将保存任务被中断时的状态,以便在中断处理完成后,恢复任务执行状态。
  2. PRIMASK(中断屏蔽寄存器):用于选择性地阻塞或激活异常/中断。如果PRIMASK的对应位为1,则相应的异常/中断将被禁止;如果为0,则相应的异常/中断将被允许。
  3. BASEPRI(基本优先级寄存器):用于设置优先级基线。当异常/中断发生时,处理器会根据当前的BASEPRI值来确定是否抢占正在执行的任务。如果某个异常/中断的优先级高于当前的BASEPRI,则该异常/中断将被抢占执行;否则,该异常/中断将被阻塞。

总之,FreeRTOS中的状态寄存器主要用于保存任务被中断时的状态以及控制异常/中断的执行。这些寄存器可以帮助任务在中断处理过程中正确地保存和恢复状态,确保系统稳定运行。

 

PendSV中断服务函数

__asm void xPortPendSVHandler( void )
{
    extern uxCriticalNesting;
    extern pxCurrentTCB;
    extern vTaskSwitchContext;
    PRESERVE8
    mrs r0, psp (1)
    //读取进程栈指针,保存在寄存器R0里面
        
    isb
    ldr r3, =pxCurrentTCB (2)
    ldr r2, [r3] (3)
    //获取当前任务的任务控制块,并将任务控制块的地址保存在寄存器R2里面
        
    stmdb r0!, {r4-r11, r14} (4)
    //保存r4~r11和R14这几个寄存器的值
    
    str r0, [r2] (5)
    //将寄存器R0的值写入寄存器R2所保存的地址中去,也就是将新的栈顶保存在任务控制块的第一个字段中。
    //此时的寄存器R0保存着最新的堆栈栈顶指针值,所以要将这个最新的栈顶指针写入到当前任务的任务控制块的
    //第一个字段,而经过(2)和(3)已经获取到了任务控制块,并将任务控制块的首地址写入到了寄存器R2中。
        
    stmdb sp!, {r3,r14} (6)
    //将寄存器R3和R14的值临时压栈,寄存器R3中保存了当前任务的任务控制块,而接下来要调用vTaskSwitchContext(),
    //为了防止R3和R14的值被改写,所以这里临时将R3和R14的值先压栈。
    
    mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY (7)
    msr basepri, r0 (8)
    //关闭终端,进入临界区
        
    dsb
    isb
    bl vTaskSwitchContext (9)
    //调用函数vTaskSwitchContext(),此函数用来获取下一个要执行的任务,并将pxCurrentTCB更新为这个要运行的任务
        
    mov r0, #0 (10)    
    msr basepri, r0 (11)
    //打开中断,退出临界区
        
    ldmia sp!, {r3,r14} (12)
    //刚刚保存的寄存器R3和R14的值出栈,恢复寄存器R和R14的值。注意,经过(12),此时pxCurrentTCB的值已经改变
    //所以读取R3所保存的地址处的数据就会发现其值改变,成为了下一个要运行的任务的任务控制块。
    
    ldr r1, [r3] (13)
    ldr r0, [r1] (14)
    //获取新的要运行的任务的任务堆栈栈顶,并将栈顶保存在寄存器R0中。
        
    ldmia r0!, {r4-r11} (15)
    //R4~R11,R14出栈,也就是即将运行的任务现场。
    
    msr psp, r0 (16)
    //更新进程栈指针PSP的值。
        
    isb
    bx r14 (17)
    //执行此行代码以后硬件自动恢复寄存器 R0~R3、R12、LR、PC 和 xPSR 的值,确定异常返回以后应该进入处理器
    //模式还是进程模式,使用主栈指针(MSP)还是进程栈指针(PSP)。很明显这里会进入进程模式,并且使用进程栈指
    //针(PSP),寄存器 PC 值会被恢复为即将运行的任务的任务函数,新的任务开始运行!至此,任务切换成功。
        
    nop
}