FreeRTOS知识点总结

FreeRTOS知识点总结

1. 移植

  • 添加src文件,添加包含路径
  • 修改FreeRTOSconfig.h
    • 定义中断组:4
    • 宏定义SVC,PendSV,SysTick中断处理函数
    • 根据所需功能,裁剪系统

2. 系统裁剪

对FreeRTOSConfig.h文件进行修改,宏定义打开/关闭,包含打开/关闭

  • 内核相关配置
  • 内存管理配置
  • 钩子函数配置
  • 任务追踪配置
  • 协程配置
  • 软件定时器配置
  • 断言配置
  • 中断配置

3. 调试方法

3.1 串口打印调试

  • 需打开任务追踪配置
#define configUSE_TRACE_FACILITY	1
#define configGENERATE_RUN_TIME_STATS	1
#define configUSE_STATS_FORMATTING_FUNCTIONS	1
#define portCONFIGURE_TIMER_FOR_RUN_TIME_STATS()	初始化定时器函数(),周期为系统时钟的10~20倍
#define portGET_RUN_TIME_COUNTER_VALUE()	设置全局变量,为时间统计功能提供节拍,在定时器中断服务函数中更新
  • 初始化定时器,中断优先级设到最高
  • 使用函数vTaskList()创建一个表格描述每个任务的详细信息
  • 使用函数vTaskGetRunTimeStats()创建一个表格描述每个任务的运行时间和其所占总时间的百分比

函数原型:

void vTaskList( char * pcWriteBuffer );
void vTaskGetRunTimeStats( char *pcWriteBuffer );
  • 调用printf()打印表格

3.2 IAR插件调试

  • 打开任务追踪配置
#define configUSE_TRACE_FACILITY	1
  • 打开IAR-Project-Options
    • Debugger
      • Plugins
        • √ FreeRTOS and OpenRTOS
  • 插件显示表格

3.3 栈溢出检测调试

3.3.1 方法1

在**任务切换**时,检测任务栈指针是否越界,越界则触发栈溢出钩子函数

  • 打开钩子函数配置
#define configCHECK_FOR_STACK_OVERFLOW	1
  • 在函数vApplicationStackOverflowHook()函数中,打印任务名

3.3.2 方法2

在任务创建时,任务栈所有数据初始化为0xa5,在任务切换时,检测末尾16个字节是否都为0xa5

  • 打开钩子函数配置
#define configCHECK_FOR_STACK_OVERFLOW	2 
  • 在函数vApplicationStackOverflowHook()函数中,打印任务名

无论方法1、方法2都是在任务进行切换时执行,存在局限性:

  • 任务执行的过程中出现过栈溢出,但任务切换前栈指针又恢复到了正常水平(方法1)
  • 任务栈末尾的 16 个字节没有用到,即不会被修改,但是任务栈已经溢出了(方法2)
  • 任务栈溢出后,把系统中的重要数据修改了导致系统直接进入Hardfault(方法1、2)

3.3.3 任务栈水位检测函数

函数uxTaskGetStackHighWaterMark (TaskHandle_t xTask),可返回自任务运行以来剩余可用堆栈空间的最小值,即任务运行过程中堆栈最大使用量时还剩余多少空间

  • 打开包含
#define INCLUDE_uxTaskGetStackHighWaterMark	1
  • 如果函数返回0则说明可能发生了任务堆栈溢出

4. 栈设置

  • 创建任务时,设置任务栈大小,单位为word,从FreeRTOSConfig.h文件中定义的堆空间申请
  • 启动文件.s中设置系统栈大小,单位为byte,中断函数与中断嵌套使用系统栈
    • MSP主堆栈指针给系统栈使用,PSP进程堆栈指针给任务栈使用,进入中断函数及中断嵌套中,都是用MSP指针
    • 实际应用中分配,按最坏情况:
      • M3:64字节
      • M4:200字节

5. 中断设置

  • 中断优先级组配置:STM32只使用4位,共5组优先级组使用FreeRTOS时,使用分组4,只有0~15抢占优先级

  • configPRIO_BITS:配置系统中断优先级组寄存器[7:0]使用位数,设为 4

  • configLIBRARY_LOWEST_INTERRUPT_PRIORITY:设置系统最低优先级,设为 15

  • configKERNEL_INTERRUPT_PRIORITY

配置内核中断优先级,由宏 configLIBRARY_LOWEST_INTERRUPT_PRIORITY 左移 8-configPRIO_BITS 位得到,即配置PendSV,SysTick中断优先级为最低

  • configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY:设为 1<=n<=15

设置 FreeRTOS 系统可管理的最大优先级,实际即对 BASEPRI 寄存器进行操作,优先级高于该值的中断,FreeRTOS内核无法调度

  • configMAX_SYSCALL_INTERRUPT_PRIORITY

由宏 configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 左移 8-configPRIO_BITS位得到

图片

6. 列表

  • FreeRTOS列表:双向循环链表,初始化时,通过列表位迷你列表项作为末尾哨兵节点,将整个链表连接起来

  • 列表结构体:

    • 列表完整性检查项1
    • 列表中列表项个数
    • 列表索引项
    • 列表最后一个列表项(迷你列表项)
    • 列表完整性检查2
  • 列表项结构体:

    • 列表完整性检查项1
    • 列表项值,用于顺序排列(通常为:醒来时间,优先级排序
    • 前一项
    • 后一项
    • 拥有列表项的内核对象,存放TCB
    • 列表项归属,存放列表地址
    • 列表完整性检查项2
  • 迷你列表项:

    • 列表完整性检查项1
    • 列表项值,设为portMAX_DELAY
    • 前一项
    • 后一项
  • 列表初始化:

    • 列表索引项指向末尾迷你列表项
    • 列表末尾项值设为portMAX_DELAY
    • 列表项末尾迷你列表项首尾相连
    • 列表项个数为0
  • 列表项初始化:

    • 初始化列表项归属为NULL,表示不属于任何列表
  • 插入(从前往后,升序插入):

    • 获取列表项值,有序插入
    • 从列表末尾迷你项开始遍历,直到获取到列表项值不大于要插入列表项的最后一个节点
    • 插入列表项
    • 列表项归属设为插入的列表地址
    • 列表项个数++
  • 末尾插入(无序插入):

    • 获取列表当前索引

    • 将要插入节点插入当前索引列表项前面

    • 列表项归属设为插入的列表地址

    • 列表项个数++

  • 删除:

    • 删除连接
    • 如果删除节点为列表的索引项,则将前一项作为索引项
    • 将删除节点归属列表设为NULL
    • 列表项个数–
  • 遍历:(用于从多个同优先级的就绪任务中查找下一个要运行的任务)

    • 列表索引项指向当前索引项下一个节点
    • 如果下一个节点为列表末尾迷你项,则再跳至下一个
  • FreeRTOS存在的列表:

    • pxReadyTasksLists[ configMAX_PRIORITIES ]:无序插入

      就绪列表数组,每个元素为不同优先级的列表,共有configMAX_PRIORITIES个元素

    • xDelayedTaskList1:延时列表1,有序插入

    • xDelayedTaskList2:延时列表2,有序插入

    • pxDelayedTaskList:指向延时列表的指针

    • pxOverflowDelayedTaskList:指向滴答定时器溢出后延时列表的指针

      维护两个延时列表,为了处理系统滴答定时器溢出问题

    • xPendingReadyList,无序插入

      挂起就绪列表,任务进入就绪态,但调度器未开启的列表

    • xTasksWaitingTermination,无序插入

      等待空闲任务删除的任务列表

    • xSuspendedTaskList,无序插入

      挂起列表

  • FreeRTOS任务控制块中的列表项:

    • 状态列表项:存储任务阻塞时间信息
    • 事件列表项:存储任务优先级信息

7. 调度原理

  • 两条原则

    • 高优先级抢占低优先级任务,系统永远执行最高优先级的任务
    • 同等优先级的任务轮转调度
  • 调度:切换不同任务让cpu运行。1.执行系统调用时进行切换;2.滴答定时器中断服务函数中切换

    • 挂起PendSV来启动中断,再PendSV中断服务函数中进行任务切换

    • PendSV中断服务函数:

      __asm void xPortPendSVHandler( void )
      {
          保存上一个任务的上下文(到任务堆栈中);
          调用 vTaskSwitchContext() 查找下一个要运行任务;
          加载要运行的任务的上下文信息(开始运行);
      }
      
    • vTaskSwitchContext()函数:

      void vTaskSwitchContext( void )
      {
          if (调度器挂起)
          {
              将 xYieldPending 置为 pdTrue 表示需要切换;
          }
          else (未挂起)
          {
      		(计算任务运行时间,调试用);
      		(检测栈溢出,调试用);
      		调用 taskSELECT_HIGHEST_PRIORITY_TASK(),查找处于就绪态的最高优先级任务;
          }
      }
      
  • 抢占式调度

    • 查找处于就绪态最高优先级任务 taskSELECT_HIGHEST_PRIORITY_TASK()

      taskSELECT_HIGHEST_PRIORITY_TASK()
      {
          通过计算前导零方式,得到处于就绪态的最高优先级;
          在该优先级任务列表中利用 listGET_OWNER_OF_NEXT_ENTRY(),获取列表索引的下一个节点;
          取出该节点设为当前任务 pxCurrentTCB;
      }
      
    • 计算前导零方式:

      维护全局变量uxTopReadyPriority,每个bit代表一个优先级某个优先级存在就绪任务时,将相应bit置1

      • clz( uxTopReadyPriority ) 用于计算前导0的个数
      • uxTopPriority=(31-__clz( uxTopReadyPriority ))获取当前就绪的最高优先级
  • 时间片调度

    • Round-robin 调度算法
  • 维护的全局变量

    • pxCurrentTCB:当前运行任务的控制块
      • 在任务切换函数中,直接将下一个需要运行的任务控制块赋给该值
    • uxTopReadyPriority:当前就绪的最高优先级标志
      • 32bit 无符号整数
      • 每一个位标记一个优先级
      • 某优先级有就绪任务,则会将该变量的相应bit置1
    • xYieldPending
      • bool类型
      • 表示调度器恢复后需要进行任务切换标志

8. 时间管理

  • 系统滴答定时器中断服务函数 xPortSysTickHandler()

    void xPortSysTickHandler( void )
    {
        关闭全局中断;
        if (调用xTaskIncrementTick()==pdTrue)
        {
            则挂起PendSV,进行一次切换;
        }
        关闭全局中断;
    }
    
  • 系统时钟节拍函数 xTaskIncrementTick():

    维护全局变量 xTickCount,

    维护全局变量 uxPendedTicks: 如果任务调度器被挂起,则更新uxPendedTicks值,当恢复调度时,xTaskResumeAll() 会调用uxPendedTicks次本函数,恢复 xTickCount 的值

    BaseType_t xTaskIncrementTick( void )
    {
        if (调度器未挂起)
        {
            xTickCount++;
            if (xTickCount==0,发生溢出)
            {
                延时列表进行交换;
            }
            if ((当前时刻) >= (下一个任务取消阻塞的时刻))
            {
                for(;;)
                {  
                    //一直循环直到延时列表为空 或 延时列表下个任务未到唤醒时刻
                    if (延时列表为空)
                    {
                    	设置下一个任务取消阻塞的时刻为portMAX_DELAY;
                        break;
                    }
                	else (延时列表不为空)
                    {
                        找到延时列表索引项的下一个节点任务;
                        if ((该节点任务的醒来时刻) 大于 (当前时刻))
                        {
                            未到唤醒点,更新下一个任务取消阻塞的时刻;
                            break;
                        }
                        从延时列表中移除该任务;
                        if (任务在等待事件)
                            从事件的等待列表中删除任务;
                        
                        //抢占式调度
                        将任务加入相应的优先级就绪列表;
                        
                        if ((唤醒任务的优先级)>= (当前任务优先级))
                        {
                            返回值设为 pdTrue;//需要切换
                        }
                    }
                } 
            }
            
            //时间片轮转调度
            if (当前任务所在优先级列表下存在其他任务)
            {
                返回值设为 pdTrue;//需要切换
            }
            
            执行节拍钩子函数;
        }
        else (调度器被挂起)
        {
            uxPendedTicks++;
            
            执行节拍钩子函数;
            
            if(xYieldPending == True)
            {
                返回值设为 pdTrue;//需要切换
            }
        }
    }
    
  • 维护的全局变量

    • xTickCount:系统时钟节拍
    • uxPendedTicks:调度器挂起时,调用 xTaskIncrementTick() 函数的次数
      • 当调度器恢复时,将调用 uxPendedTicks 次 xTaskIncrementTick() 函数以恢复调度
    • xNextTaskUnblockTime:下一个任务解除阻塞的时刻

9. 启动分析

  • 调度器开启函数vTaskStartScheduler()

    void vTaskStartScheduler( void )
    {
        以最低优先级(静态/动态)创建空闲任务(如果使能静态分配内存,优先以静态方式创建);
        
        if(使能 软件定时器)
            创建软件定时器任务;
        
        关闭全局中断;
        
        //设置下一个任务取消阻塞的时刻为 portMAX_DELAY;
        xNextTaskUnblockTime=portMAX_DELAY;
        
        //设置调度器标志位 
        xSchedulerRunning = pdTRUE;
        
        //初始化系统节拍
        xTickCount = ( TickType_t ) 0U;
        
        if(使能 时间统计功能)
            //需要自实现用于初始化定时器
            portCONFIGURE_TIMER_FOR_RUN_TIME_STATS();
        
        //初始化设置硬件(滴答定时器/FPU单元/PendSV中断)
    	xPortStartScheduler();
    }
    
  • xPortStartScheduler()

    BaseType_t xPortStartScheduler( void )
    {
        计算优先级相关寄存器;
        设定PendSV与滴答定时器中断为最低优先级;
        
        //开启滴答定时器 
        vPortSetupTimerInterrupt();
        
        //初始化临界区嵌套计数
        uxCriticalNesting = 0;
        
        //启动第一个任务
        prvStartFirstTask() 
    }
    
  • vPortSetupTimerInterrupt()

    void vPortSetupTimerInterrupt( void )
    {
    #if(使能 低功耗模式)
        {
            计算相关变量;
        }
    #endif    
        暂停并清零滴答定时器;
        
        //设定滴答定时器中断频率 configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ
        portNVIC_SYSTICK_LOAD_REG = ( configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ ) - 1UL;
        开启滴答定时器;
    }
    
  • prvStartFirstTask()

    __asm void prvStartFirstTask( void )
    {
        复位MSP主堆栈指针(中断函数、中断嵌套使用);
        
        开启PRIMASK, FAULTMASK中断;
        
        //只此一次 SVC调用
        调用SVC指令,触发中断;
    }
    
  • SVC中断服务函数 vPortSVCHandler()

    __asm void vPortSVCHandler( void )
    {
        获取 pxCurrentTCB 的任务指针 到 r3;
        获取任务指针值 到 r1;
        获取任务堆栈指针 到 r0;
        手动出栈 r4-r11,恢复现场;
        将任务堆栈指针 赋给 进程堆栈指针PSP;
        打开全局中断;
        调转开始执行;
    }
    

10. 内核函数分析

  • 任务创建函数 xTaskCreate()

    xTaskHandler_t xTaskCreate()
    {
        为任务堆栈和任务控制块TCB申请内存;
        //初始化TCB中的各个字段
        prvInitialiseNewTask() ;
        //将任务加入到就绪列表中
        prvAddNewTaskToReadyList();
    }
    
  • 任务初始化函数 prvInitialiseNewTask()

    • (如果使能堆栈溢出检测或追踪,将任务堆栈全部赋值为 0xa5)
    • 计算堆栈栈顶 pxTopOfStack
    • 初始化任务名
    • 初始化任务优先级
    • (如果 使用互斥信号量,初始化相应字段)
    • 初始化状态列表项与事件列表项
    • (如果有使用其他资源,初始化相应字段)
    • 调用 pxPortInitialiseStack() 函数初始化任务堆栈
    • 生成任务句柄,指针指向任务控制块
  • 任务堆栈初始化函数 pxPortInitialiseStack()

    • 按照 xPSR、 R15(PC)、 R14(LR)、 R12、 R3~R0、 R11~R14 保存到任务堆栈中
  • 添加新任务到就序列表函数 prvAddNewTaskToReadyList()

    • 进入临界区
    • 现存任务数++
    • 如果 当前任务 为 NULL
      • 将 新任务 设为 当前任务
      • 如果 现存任务数 为 1
        • 初始化任务列表
    • 当前任务 不为 NULL
      • 如果 调度器未运行
        • 如果 当前任务优先级 <= 新任务优先级
          • 将 新任务 设为 当前任务
    • 总任务数++
    • (如果使能任务追踪,给任务控制块相应字段赋予 总任务数,用作编号)
    • 将当前任务加入就绪表
    • 离开临界区
    • 如果 调度器正在运行
      • 如果 当前任务优先级 < 新任务优先级
        • 标记需要进行一次任务切换(挂起PendSV中断)
  • 添加任务到就绪表宏 prvAddTaskToReadyList()

    • uxTopReadyPriority 记录任务优先级
    • 调用 vListInsertEnd() 无序插入至对应优先级就绪表,列表项为任务的状态列表项
  • 任务删除 xTaskDelete()

    • 进入临界区
    • 获取任务控制块
    • 从就绪表中删除任务列表项
    • 处理优先级记录uxTopReadyPriority
    • 如果 任务在等待事件
      • 从事件的等待列表中删除
    • 总任务数++,重新生成任务列表
    • 如果 要删除的任务 是 当前任务
      • 将任务 加入 等待删除列表(等待空闲任务释放任务内存)
      • 增加 ucTasksDeleted 变量
      • 调用任务删除钩子函数(需自实现)
    • 不是当前任务
      • 现存任务数–
      • 调用 prvDeleteTCB() 释放任务内存
      • 重新计算下个任务解除阻塞时刻
    • 离开临界区
    • 如果调度器正在运行
      • 如果 删除的任务 是当前任务
        • 挂起PendSV中断进行任务切换
  • 任务挂起

    • 进入临界区
    • 获取任务控制块
    • 从列表中删除任务列表项
    • 处理优先级记录uxTopReadyPriority
    • 如果 任务在等待事件
      • 从事件的等待列表中删除
    • 将任务加入挂起列表中,无序插入
    • 如果任务等待通知
      • 将任务控制块等待通知字段设为不等待
    • 离开临界区
    • 如果调度器正在运行
      • 进入临界区
      • 重新计算下个任务解除阻塞时刻
      • 离开临界区
    • 如果 挂起的任务是当前任务

    • 挂起任务不是当前任务

  • 任务恢复

11. 队列

12. 信号量

13. 软件定时器

14. 事件组

15. 任务通知

你可能感兴趣的:(单片机,stm32,嵌入式硬件)