FreeRTOS学习-任务管理(Task管理)(1)

2023-05-16

1. 简介

任务管理(或称进程管理)是所有操作系统内核的最基本组成模块之一,FreeRTOS也不例外。想要了解一个操作系统,不得不理解其任务管理的设计和实现。任务管理的介绍由两篇文章组成,第一篇先介绍了FreeRTOS的任务管理的重要概念和外部特性以及相关联的重要实现,第二篇介绍任务管理实现的细节(关键数据结构和内部函数的实现)。

温馨提示

  1. 由于文章较长,可当作工具文使用,即仅挑选感兴趣的部分阅读;
  2. 为了解释FreeRTOS系统调用的行为,文中难免会涉及一些操作系统原理、ARM体系结构相关的概念,请读者自行查阅资料。当然,若不关心内核实现,可自行跳过。

在FreeRTOS中,可能是为了凸显出其与进程和线程的区别,它使用了任务(Task)这个概念作为它的一个最基础的调度单元。FreeRTOS的任务与进程最大的区别是,它并没有实现内存地址空间隔离,所有的任务都共享相同的内存地址空间。通常,这也是RTOS与普通的通用操作系统的重要差异之处。下面先给出一张图,概述了FreeRTOS中任务管理的设计思路。这里只是先建立一个总体的印象,具体的细节会在后面的小节逐步展开。
FreeRTOS中任务管理的设计思路

2. 任务函数(任务入口)

任务函数(又称任务入口)指的是一个任务实际执行的函数体。它是该任务运行时实际执行的内容。类似于Linux的线程函数,每个任务一定有一个对应的任务函数。

Task函数原型:

// Define in projdefs.h, the prototype to which task functions must conform.  Defined in this file to ensure the type is known before portable.h is included.
typedef void (*TaskFunction_t)(void *);

#define portTASK_FUNCTION_PROTO( vFunction, pvParameters )  void vFunction( void *pvParameters )

这里使用了宏的形式进行定义,目的是为了支持某些体系结构需要增加特定的属性信息。

Task函数定义模板:

void TaskName_Entry(void *pvParameters)
{
    // Do something

    // Task exit
    vTaskDelete(NULL);
}

我们将希望该任务执行的内容在任务函数中实现,并在任务函数退出之前,必须进行任务的自我销毁,否则会使得系统崩溃,详情可参看创建任务。而vTaskDelete()便是完成销毁的接口,这将在删除任务部分介绍。

3. 任务状态

FreeRTOS的任务状态机如下图所示:
FreeRTOS的任务状态机
FreeRTOS在实现任务状态时,每个状态都对应着一个(或多个)任务队列,详情可参看任务队列小节。

下面将分别介绍这几个状态的含义。

3.1. Running State

即任务正在运行,由于目前官方的FreeRTOS发行版不支持多核,因此任何时候,只有一个任务处于Running状态。

3.2. Blocked State

正在等待事件的发生。或者主动进入睡眠。

在FreeRTOS中,等待事件时还可以设定等待时限,此时处于Blocked的任务会被同时放入等待事件的任务队列和Delayed任务队列。

3.3. Suspended State

挂起状态,通常情况不会使用该状态,这是一种不能通过任何外部事件唤醒的状态,只能通过vTaskSuspend()进入,vTaskResume()退出。所以,尽管任务可能无时限等待某个事件的发生,但不能认为此时是Suspended状态。

3.4. Ready State

可以被调度的状态。需要注意的是,FreeRTOS的任务被创建时就是Ready状态。

3.5. Deleted State

任务已经被删除,但TCB和任务栈所占用的内存资源未被回收,类似于Linux的僵尸进程。最终会由系统服务完成资源回收。

4. 创建任务

通常,在使用FreeRTOS时,我们都会在运行时动态创建任务。

动态创建任务的函数原型:需要开启configSUPPORT_DYNAMIC_ALLOCATION = 1

BaseType_t xTaskCreate( TaskFunction_t pxTaskCode,
                        const char * const pcName,
                        const uint16_t usStackDepth,
                        void * const pvParameters,
                        UBaseType_t uxPriority,
                        TaskHandle_t * const pxCreatedTask ) PRIVILEGED_FUNCTION; /*lint !e971 Unqualified char types are allowed for strings and single characters only. */

当创建任务的时候,FreeRTOS会把新创建的任务直接放入到Ready队列。

每当创建一个任务,FreeRTOS会为其分配两片内存区域,一个用于存储任务的数据结构,也即TCB;而另一片则作为该任务的栈。与Linux的进程不同,这两片内存区域是未受到任何保护的,也即可被其他任意任务访问。

需要注意的是,usStackDepth指的是栈的深度,具体实现为可存储的BaseType_t变量的数量,而不是字节数。FreeRTOSv10后,usStackDepth的数据类型由configSTACK_DEPTH_TYPE来表示,以支持更大的栈大小。

pvParameters是为任务函数所用的参数,由任务的创建者传入,最终给到任务入口函数使用。

uxPriority申明了任务的基础优先级。如果开启了MPU功能,则可附加portPRIVILEGE_BIT位用于标识这是一个系统级的任务(或称为系统服务)。

pxCreatedTask用于接收新创建任务的任务句柄。

下面详细介绍一下任务创建的实现(非MPU实现)。因为任务创建的过程涉及到了一些比较重要的概念和行为,因此在介绍实现之前,需要先介绍这几个关键的概念和行为。

关键的概念:

  • 任务的控制块(TCB):记录一个任务的所有元信息,详情可查看进程控制块。
  • 任务的上下文:指一个任务运行时所需要的所有状态信息,例如CPU寄存器(通用寄存器、浮点寄存器等)、FreeRTOS的相关全局信息(临界区深度、FPU标志等)。在任务切出时,这些信息会保存在任务的栈中;在任务切入时,会将信息恢复到对应的寄存器或全局变量中。详情可查看任务切换小节。

关键的行为:

  • 临界区的进入与退出(taskENTER_CRITICAL()/taskENTER_CRITICAL_FROM_ISR()taskEXIT_CRITICAL()/taskEXIT_CRITICAL_FROM_ISR()):临界区的进入和退出是与体系结构强相关的行为。临界区内,屏蔽所有低优先级中断。详情可参看临界区和挂起调度器。
  • 任务栈的初始化(StackType_t *pxPortInitialiseStask( StackType_t *pxTopOfStack, TaskFunction_t pxCode, void *pvParameters )):使用初始信息填充任务的上下文,使得它就像是被调度器切换出去一样。在ARMv7的Port中,该实现需要满足AAPCS。另外,从其具体的实现就可以看出,在ARMv7体系结构中,FreeRTOS的任务是运行在系统模式下的。
    任务栈的初始化
    其中:
  • 初始的CPSR设置为portINITIAL_SPS。这个宏在ARMCA9的port实现为0x1f,具体含义则是:处理器模式为System Mode、使用ARM指令集运行模式、IRQ和FIQ都处于开启状态。
  • 将R14设置为portTASK_RETURN_ADDRESS,通常是NULL。因为任务的初始状态调用栈是空的。

下面将详细介绍创建Task的实现。先来看一张相对宏观的活动图:
创建Task

  1. 申请TCB和任务栈所需的内存空间(通过调用pvPortMalloc()接口完成):如果申请内存失败,则直接返回errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY

  2. 初始化TCB的内容和初始化任务栈(static void prvInitialiseNewTask( TaskFunction_t pxTaskCode, const char * const pcName, const uint32_t ulStackDepth, void * const pvParameters, UBaseType_t uxPriority, TaskHandle_t * const pxCreatedTask, TCB_t * pxNewTCB, const MemoryRegion_t * const xRegions )):

    初始化TCB的流程:(初始化任务栈参看前面的介绍[任务栈的初始化])
    初始化TCB的流程

  3. 将新申请的任务加入Ready任务队列(static void prvAddNewTaskToReadyList( TCB_t *pxNewTCB)。如果该任务是第一个申请的任务,则还需要初始化所有任务队列。其活动图如下:
    新申请的任务加入Ready任务队列

(在v10中引入)除了动态创建任务之外,FreeRTOS还支持静态创建方式,函数原型如下:需要开启configSUPPORT_STATIC_ALLOCATION = 1

TaskHandle_t xTaskCreateStatic( TaskFunction_t pxTaskCode,
                                const char * const pcName,
                                const uint32_t ulStackDepth,
                                void * const pvParameters,
                                UBaseType_t uxPriority,
                                StackType_t * const puxStackBuffer,
                                StaticTask_t * const pxTaskBuffer ) PRIVILEGED_FUNCTION; /*lint !e971 Unqualified char types are allowed for strings and single characters only. */

特别地,为了实现任务栈内存的保护机制,FreeRTOS还提供了MPU版本的任务创建。函数原型如下:需要开启portUSING_MPU_WRAPPERS = 1

BaseType_t xTaskCreateRestricted( const TaskParameters_t * const pxTaskDefinition, TaskHandle_t *pxCreatedTask ) PRIVILEGED_FUNCTION;

这里需要用到任务参数结构体(TaskParameters_t),用于申明该任务所使用的MPU内存区。

另外,FreeRTOS还为MPU的任务提供了类似于Linux虚拟内存的方法。函数原型如下:

void vTaskAllocateMPURegions( TaskHandle_t xTask, const MemoryRegion_t * const pxRegions ) PRIVILEGED_FUNCTION;

内存区域结构体参数(pxRegions)申明了需要重新映射的目标内存区域。

5. 任务优先级

创建任务时,需要设置优先级。该优先级还可以在运行时进行更改。FreeRTOS的优先级数值与Linux的相反,即值越大,优先级越高。

在FreeRTOS的调度策略中,总是确保最高优先级的任务被调度,相同优先级则使用时间片轮转调度。

FreeRTOS在实现任务队列时,Ready任务队列是由多个优先级队列构成的,即一个优先级对应一个队列。因此最大优先级应该尽可能的小,即configMAX_PRIORITIES应该尽可能的小,防止内存被过度占用。详情可参看任务队列小节。

此外,为了实现互斥锁中的优先级继承机制,FreeRTOS需要记录任务实际继承的优先级(实际优先级),以及任务的原始优先级(Base优先级)。

在ARMCA9的实现中,使用优先级位图的概念记录了系统当前所有任务的优先级。详情可参看任务管理的私有宏函数。

6. 系统中的特殊任务(或称系统服务)

6.1. Idle Task

Idle任务在开启任务调度器的时候被创建,它的优先级被设置为tskIDLE_PRIORITY,通常是最低优先级。其主要职责如下:

  • 确保总有一个任务在执行;
  • 回收僵尸任务占用的系统资源;
  • 实现系统级低功耗功能。

其函数原型如下:

#define portTASK_FUNCTION_PROTO( prvIdleTask, pvParameters );

它的函数体是一个永无止境的循环。在查看其具体实现之前,先来看看一个用于实现低功耗功能的关键行为:

  • 计算预期的Idle Ticks(static TickType_t prvGetExpectedIdleTime( void )):

    {
        初始化`uxHigherPriorityReadyTasks = pdFALSE`,`uxLeastSignificantBit = 0x01`。
    
        如果当前系统等待调度的最高优先级是否大于Idle任务的优先级(`uxTopReadyPriority > uxLeastSignificantBit`),则设置局部调度Flag`uxHigherPriorityReadyTasks = pdTRUE`。
    
        如果系统当前的任务优先级大于Idle的优先级,或存在与Idle优先级相同的任务,或局部调度Flag不为`pdFALSE`,则表明系统当前不能进入休眠,设置返回值`xReturn = 0`;
        否则,进入休眠的时间为`xReturn = xNextTaskUnblockTime - xTickCount`。
    
        返回预期的Idle时间`xReturn`。
    }
    

下面便来看看在Idle任务中具体做了什么。宏观上,Idle任务的任务函数的活动图如下:
Idle任务的任务函数

  1. 回收处于Deleted状态的任务的资源(prvCheckTasksWaitingTermination()):

  2. (仅开启USE_PREEMPTION && IDLE_SHOULD_YIELD)若存在其他相同优先级的任务(listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ tskIDLE_PRIORITY ] ) ) > 1),主动发起一次调度(taskYIELD()),即触发一次软件中断(SWI 0)。

  3. (仅开启IDLE_HOOK)调用vApplicationIdleHook()

  4. (仅开启TICKLESS_IDLE)进入系统低功耗模式:

    进入系统低功耗模式
    在实现portSUPPESS_TICKS_AND_SLEEP()时,需要确保系统会被外部中断唤醒,否则会导致系统漏掉重要的外部中断信号。
    在FreeRTOSv10后,支持设置Idle任务的名字。

6.1.1. Idle Application Hook

Idle Application Hook是一个钩子函数,为程序员提供了在Idle上下文中调用的钩子,它可以实现以下内容:

  • 执行低优先级、后台、或需要持续处理的函数;
  • 度量空闲的处理能力;
  • 将处理器进入低功耗模式。

因为这个钩子运行在Idle上下文,所以FreeRTOS对其实现有一定的限制:即不能进入Blocked和Suspended状态,防止系统中没有任务可被调度。

6.2. Timer Task

主要职责:实现FreeRTOS的系统级软件Timer。

创建时机:在开启调度器的时候创建。

这个任务还会在后续的FreeRTOS学习系列笔记-软件定时器章节中进行详细介绍。

7. 任务调度

7.1. 调度方法

7.1.1. 调度方法配置

相关配置:

  • configUSE_PREEMPTION
  • configUSE_TIME_SLICING
  • configUSE_TICKLESS_IDLE:用于低功耗模式

具体用法在几种调度方法中介绍。

7.1.2. 几种调度方法

7.1.2.1. Prioritized Pre-emptive Scheduling with Time Slicing(最常用)

配置:configUSE_PREEMPTION = 1 && configUSE_TIME_SLICING = 1

特点:

  • 优先调度高优先级任务;
  • 可抢占;
  • 相同优先级的任务则采用时间片轮转调度。
7.1.2.2. Prioritized Pre-emptive Scheduling (without Time Slicing)

配置:configUSE_PREEMPTION = 1 && configUSE_TIME_SLICING = 0

特点:

  • 优先调度高优先级任务;
  • 可抢占;
  • 相同优先级任务需要等待Running的任务主动放弃CPU才能触发调度。
7.1.2.3. Co-operative Scheduling

配置:configUSE_PREEMPTION = 0 && configUSE_TIME_SLICING = 任意

特点:

  • 优先调度高优先级任务;
  • 非抢占,即需要Runing的任务主动放弃CPU才可触发调度。

7.2. 任务切换

在FreeRTOS中,支持多种触发任务调度的方式:

  • Tick中断周期性触发:顾名思义,每当产生Tick中断时,触发一次任务的调度,以实现时间片轮转。
  • 由某些系统调用而间接引发的调度:某些系统调用会改变任务的状态,比如由Running状态转换为Delayed,那这种情况必然会引发一次任务的调度。
  • 由中断间接引发的调度:中断的产生使得任务被唤醒,进而触发一次调度。详情可参看中断处理流程小节。

无论是哪种触发方式,如果在本次调度中发现需要进行任务切换,那这个任务切换的过程是一致的,所以,先介绍一下任务切换的过程。

在任务切换过程中,最重要的就是需要将当前切出的任务的上下文进行保存(寄存器->栈),并将切入的任务的上下文进行恢复(栈->寄存器)。那么,任务的上下文就是这个过程中最重要的概念。

任务的上下文:指一个任务运行时所需要的所有状态信息,例如CPU寄存器(通用寄存器、浮点寄存器等),FreeRTOS的相关全局信息(临界区深度、FPU标志等)。在ARMv7体系结构中,FreeRTOS的任务上下文在任务栈中的内存分布模型如下(假设:栈的上顶端为0xFFFFFFF0,栈的格式符合AAPCS,且使用了浮点寄存器):

地址内容备注
0xFFFFFFFCSPSR_<p_mode>在切换为系统模式之前的模式的SPSR,即异常返回时的CPSR
0xFFFFFFF8R14_<p_mode>在切换为系统模式之前的模式的LR,即异常的返回地址
0xFFFFFFF4R14
0xFFFFFFF0R12
0xFFFFFFECR11
0xFFFFFFC4R1
0xFFFFFFC0R0
0xFFFFFFBCulCriticalNesting
0xFFFFFFB4D15
0xFFFFFFACD14
0xFFFFFF44D1
0xFFFFFF3CD0
0xFFFFFF34D31
0xFFFFFF2CD30
0xFFFFFEC4D17
0xFFFFFEBCD16
0xFFFFFEB8FPSCR浮点状态和控制寄存器
0xFFFFFEB4ulPortTaskHasFPUContext当前SP指向的位置,也是pxCurrentTCB指向的位置

在这个基础上,理解任务的切换过程便简单多了:

  1. 保存被切出任务的上下文(portSAVE_CONTEXT):按照上下文在栈中的内存分布模式来填充任务栈。

  2. 切换当前系统的任务上下文(vTaskSwitchContext( void )):

    {
        如果调度器被挂起(`uxSchedulerSuspended != pdFALSE`),则设置调度挂起请求标志后直接返回(`xYieldPending = pdTRUE`)。
        否则:
        {
            清空调度挂起标志(`xYieldPending = pdFALSE`)。
            (仅开启GENERATE_RUN_TIME_STATS):
            {
                更新系统总运行时长(`ulTotalRuntime = portGET_RUN_TIME_COUNTER_VALUE()`)。
    
                如果发现当前系统总运行时长大于任务切入时间(`ulTotalRuntime > ulTaskSwitcedInTime`),则更新该任务运行时计时(`pxCurrentTCB->ulRunTimeCounter += ( ulTotalRunTime - ulTaskSwitchedInTime )`)。
    
                更新任务切入时间(`ulTaskSwitchedInTime = ulTotalRunTime`)。
            }
    
            检查任务栈是否存在溢出:即判断栈底是否已经被更改(`taskCHECK_FOR_STACK_OVERFLOW()`)。
        }
        选择一个优先级最高的任务作为被调度任务(`taskSELECT_HIGHEST_PRIORITY_TASK()`):即选取最高优先级队列中的队列首部的任务。并更新系统当前TCB指针(`pxCurrentTCB`)。
    
        (仅开启USE_NEWLIB_REENTRANT)切换NewLib的任务相关信息。
    }
    
  3. 恢复切入任务的上下文(portRESTORE_CONTEXT):

    {
        第一步便是恢复SP:即将`pxCurrentTCB`的值赋给SP。
        如果发现FPU上下文,则恢复FPU寄存器。
        恢复临界区深度`ulCriticalNesting`。
        恢复中断屏蔽系统寄存器:确保为0xFF或`ulMaxAPIPriorityMask`。
        恢复R0-R12,R14。
        恢复PC和CPSR(`RFEIA`)。
    }
    

7.3. 调度器

在FreeRTOS中,调度器只是一个逻辑概念,而不是一个相对独立存在的模块,它的实现与任务管理紧耦合。而调度器的开启则意味着FreeRTOS的任务管理功能开始运行,即FreeRTOS将开始接管CPU。此外,调度器还支持挂起(或称加锁)和恢复操作,下面将分小节介绍。

7.3.1. 开启调度器

调度器控制是FreeRTOS最基础的功能,要问基础到啥程度?基础到你不能裁剪它,因为裁了后系统就跑不起来了。

开启调度器函数接口:

void vTaskStartScheduler( void ) PRIVILEGED_FUNCTION;

这是一切的开始,开启FreeRTOS的调度器意味着FreeRTOS内核开始接管整个CPU的控制流。它将会开启FreeRTOS的系统服务,包括:

  • Idle Task
  • Timer Task

这个函数不会直接返回。

下面则来看看其实现:宏观的活动图如下:
开启调度器

  1. 创建Idle Task系统服务:这个服务在Idle Task中已介绍,这是为了让OS始终有任务存在。

  2. 创建Timer Task系统服务:这个服务在Timer Task中已介绍,是系统级软件Timer的服务进程。

  3. FreeRTOSv10后引入,调用freertos_tasks_c_additions_init()执行用户自定义初始化

  4. 初始化系统级变量:代码翻译如下

    {
        关闭低优先级中断(`portDISABLE_INTERRUPTS()`)。
        (仅开启USE_NEWLIB_REENTRANT)设置与任务相关的NewLib结构体。
    
        设置下一个任务唤醒时刻为无穷远。(`xNextTaskUnblockTime = portMAX_DELAY`)
        设置调度器的运行状态为Running。(`xSchedulerRunning = pdTRUE`)
        初始化系统的Tick为0。(`xTickCount = 0U`)
    
        (仅开启GENERATE_RUN_TIME_STATS)开启运行时统计定时器。
    }
    
  5. 初始化Tick硬件定时器:因为这个是与平台相关的行为,所以调用平台级接口xPortStartScheduler()。代码翻译如下

    {
        检查平台支持的最大中断优先级数量。
        Assert当前CPU未运行在用户模式。
        Assert中断Binary point寄存器为抢占位。
        屏蔽所有中断。(`portCPU_IRQ_DISABLE()`)。
        初始化Tick硬件Timer:
        {
            中断控制器初始化。
            定时器中断配置,设置FreeRTOS的Tick中断响应函数`FreeRTOS_Tick_Handler()`。
            初始化Tick定时器硬件。
            开启Tick定时器。
            使能Tick定时器中断。
        }
    }
    
  6. 开始执行第一个任务(void vPortRestoreTaskContext( void )):即做一次任务切换的任务切入。可以看到,这个函数是不会返回的。

    {
        切换CPU运行模式为系统模式。
        切入第一个任务(portRESTORE_CONTEXT)。
    }
    

在这个过程中,我们可以看到,在ARMv7中,FreeRTOS是运行在系统模式下的。

在FreeRTOSv10后,支持FREERTOS_TASKS_C_ADDITIONS_INIT宏,如果开启,则会在开启调度器流程的最后调用static void freertos_tasks_c_additions_init( void )实现一些自定义的初始化流程。

停止任务调度器函数接口:

void vTaskEndScheduler( void ) PRIVILEGED_FUNCTION;

这个接口体现了FreeRTOS的超强可操控性,因为它甚至连调度器都可以关闭。它将完成以下主要的行为:

  • 停止Tick中断;
  • 删除所有任务;
  • 清理所有FreeRTOS申请的资源;
  • 跳转到调用vTaskStartScheduler()的位置,也即实现了vTaskStartScheduler()的返回。

需要注意的是,这个退出过程是需要用户自己完成的。在Port layer的vPortEndScheduler()完成最终的退出行为。

7.3.2. 挂起和恢复调度器

在开启了调度器之后,FreeRTOS便能进行正常的任务调度了。但在许多系统调用中,为了防止某些系统全局信息被篡改,要求调度器暂停工作,并在系统调用完成全局信息访问后恢复,因此,为了更好的理解这些系统调用,还需要详细介绍一下调度器的挂起和恢复。

挂起调度器函数接口:

void vTaskSuspendAll( void ) PRIVILEGED_FUNCTION;

个人愚见, 这个名字取得不太好. 乍一看还以为是与vTaskSuspend()类似, 用于控制任务的挂起, 其实不然, 它操作的对象实际上是Scheduler.

它要实现的功能是挂起调度器, 但不会禁止外部中断. 那些自私的任务可以通过调用这个接口实现CPU的长期占用. 需要小心的是, 在挂起调度器后不能调用可能导致上下文切换的接口, 例如: vTaskDelayUntil(), xQueueSend()等.

其具体的实现很简单:

{
    ++uxSchedulerSuspended;
}

从代码可以看出,它仅仅是将表示调度器挂起的标志uxSchedulerSuspended自增1。由于这个标志的类型是BaseType_t,这个行为是原子性的,所以不需要在临界区内执行该操作。这个标志会在许多系统调用中引用,如果发现不为0(pdFALSE),则表示调度器被挂起。

当调度器被挂起时,所有的任务切换请求都会被挂起。另外,在调度器挂起的时候,任何中断都不能操作TCB的xStateListItem、以及xStateListItem指向的队列。而如果在调度器挂起的时候,确实有任务需要被唤醒,那他会被放到PendingReady任务队列,当调度器被唤醒时才真正地被加入到Ready任务队列中。

恢复调度器函数接口:

BaseType_t xTaskResumeAll( void ) PRIVILEGED_FUNCTION;

和上面的那位一样,名字也取得不好。它要完成的是任务调度器的恢复,需要特别注意的是,它并不会把处于Suspended的任务唤醒

它的返回值告诉我们该函数内部是否发生了调度,如果发生了则返回pdTRUE. 这是因为在挂起调度器的这段时间内, 很有可能其他被阻塞的, 更高优先级的任务已经达成了唤醒的条件。

因为恢复调度器的流程涉及到系统下一次唤醒时刻变量的更新,先来看看这个流程:

  • 更新下一次系统唤醒时刻变量(statc void prvResetNextTaskUnblockTime( void )): 代码翻译如下:

    {
        如果切换后新的Delayed任务队列为空(`listLIST_IS_EMPTY( pxDelayedTaskList != pdFALSE`):则设置下一次唤醒时刻为无限大(`xNextTaskUnblockTime = portMAX_DELAY`)。
        否则,即不为空:
        {
            从Delayed任务队列中获取最近需要唤醒的任务(`pxTCB = ( TCB_t * ) listGET_OWNER_OF_HEAD_ENTRY( pxDelayedTaskList )`)。
            并将下一次唤醒时刻设置为这个任务的唤醒时刻(`xNextTaskUnblockTime = listGET_LIST_ITEM_VALUE( &( pxTCB->xStateListItem ) )`)。
    }
    

其具体的实现如下:(代码翻译)

{
    初始化`pxTCB = NULL`。
    初始化`xAlreadyYielded = pdFALSE`。

    Assert `uxSchedulerSuspended != pdTRUE`。

    进入临界区(`taskENTER_CRITICAL()`)。

    减少调度器挂起标志(`--uxSchedulerSuspended`)。
    如果挂起标志递减为0(`uxSchedulerSuspended == pdFALSE`),即调度器挂起深度为0,且当前系统中的任务总数大于0(`uxCurrentNumberOfTasks > 0`):
    {
        循环将PendingReady任务队列中的任务加入到Ready任务队列(`while ( listLIST_IS_EMPTY( &xPendingReadyList ) == pdFALSE`):
        {
            从PendingReady任务队列中取一个任务`pxTCB`。
            将该任务从事件任务队列和Blocked任务队列中删除。
            将该任务加入到Ready任务队列(`prvAddTaskToReadyList( pxTCB )`。

            如果该任务的优先级大于系统当前的任务优先级,则设置调度挂起标志`xYieldPending = pdTRUE`。
        }

        因为在调度器挂起的时候唤醒一个任务时,并没有更新系统下一次唤醒任务的时刻变量,那么现在就需要更新:即如果PendingReady队列不为空(`pxTCB != NULL`),则需要更新下一个唤醒任务的时刻(`prvResetNextTaskUnblockTime()`)。

        处理在调度器挂起时流失的系统Tick:
        {
            为了避免破坏系统变量,初始化变量`uxPendedCounts = uxPendedTicks`。
            循环:如果`uxPendedCounts > 0`:
            {
                递增系统Tick(`xTaskIncrementTick()`),如果返回`pdTRUE`,则表明需要发起调度,则设置`xYieldPending = pdTRUE`。

                `uxPendedCounts--`。
            }
            清空系统调度挂起Tick计数(`uxPendedTicks = 0`)。
        }

        如果存在挂起的调度,即`xYieldPending = pdTRUE`:
        {
            设置`xAlreadyYielded = pdTRUE`。
            (仅开启USE_PREEMPTION)发起一次调度(`taskYIELD_IF_USING_PREEMPTION()`)。
        }
    }

    退出临界区(`taskEXIT_CRITICAL()`)。

    返回`xAlreadyYielded`。
}

从实现可以看出,调度器的挂起和恢复行为是可以嵌套的,只有当嵌套深度为0时,才会真正退出挂起状态。

8. 时间的度量和Tick中断

在任务切换中提到,任务调度可以由Tick中断触发,周期性的tick中断使得调度器周期性执行任务调度。由此可以看出,任务占用CPU的最小时间片是tick中断的周期。

在嵌入式设备中,通常配置Tick中断的周期为1ms,即需配置宏configTICK_RATE_HZ = 1000

Tick硬件定时器是在开启调度器的时候开启的,一旦开启Tick定时器后,便会定期触发Tick中断。而在Tick中断中,便会调用FreeRTOS的Tick中断响应函数FreeRTOS_Tick_Handler()进行处理,这其中会进行任务调度和Tick计数的更新等。

先来看看一些关键的行为:

  • 切换Delayed队列(taskSWITCH_DELAYED_LISTS()):

    {
        切换`pxDelayedTaskList`和`pxOverflowDelayedTaskList`指针。
        增加溢出统计(`xNumOfOverflows++`)。
    
        Tick溢出意味着需要更新下一次唤醒的时刻(`prvResetNextTaskUnblockTime()`)
    }
    

下面便来看看在Tick中断处理中具体做了什么。宏观的活动图如下:
Tick中断处理

  1. 屏蔽低优先级的中断:

    屏蔽CPU的所有中断(`portCPU_IRQ_DISABLE`)。
    
    设置GIC中断优先级屏蔽寄存器。
    
    恢复CPU的所有中断(`portCPU_IRQ_ENABLE`)。
    
  2. 增加系统Tick计数(BaseType_t xTaskIncrementTick()):看似简单的行为,实际上还需要考虑是否需要唤醒处于Blocked状态的任务,Tick溢出处理等。活动图如下:

    在这里插入图片描述

  3. 如果步骤2中触发了调度请求(xTaskIncrementTick() != pdFALSE),则设置系统调度触发标志ulPortYieldRequired = pdTRUE

  4. 恢复中断优先级屏蔽标志(portCLEAR_INTERRUPT_MASK()):

    {
        屏蔽CPU的所有中断(`portCPU_IRQ_DISABLE`)。
    
        恢复GIC中断优先级屏蔽寄存器(`portICCPMR_PRIORITY_MASK_REGISTER = portUNMASK_VALUE`)。
    
        恢复CPU的所有中断(`portCPU_IRQ_ENABLE`)。
    }
    
  5. 清除Tick硬件定时器中断标志位(configCLEAR_TICK_INTERRUPT())。

由于ulPortYieldRequired会在汇编级别的中断响应函数中进行处理,即如果该标志位被置位,则会在汇编级别的中断响应函数中进行任务切换。详情可参看后续笔记的中断处理流程小节。

FreeRTOSv10后,支持从非0开始计数。

9. 任务控制

本小节主要介绍对任务进行控制的API。

9.1. 睡眠操作

睡眠函数原型:需要设置INCLUDE_vTaskDelay = 1

void vTaskDelay( const TickType_t xTicksToDelay ) PRIVILEGED_FUNCTION

使调用该函数的Task睡眠一定的tick。睡眠时,该Task进入Blocked状态,实际的睡眠时长取决于configTICK_RATE_HZ的设置。
该API通常与portTICK_PERIOD_MS()一起使用,它将更易于理解的毫秒计时转换为tick计数。

在介绍具体实现之前,需要先理解内核如何当前任务加入Delayed任务队列(static void prvAddCurrentTaskToDelayedList( TickType_t xTicksToWait, const BaseType_t xCanBlockIndefinitely )):

{
    初始化本地变量`xConstTickCount = xTickCount`。
    (仅开启TaskAbortDelay)清空当前任务的`ucDelayAborted = pdFALSE`。

    将当前任务从Ready任务队列中移除,如果移除任务后该优先级的Ready任务队列为空:
    {
        更新Ready优先级位图`uxTopReadyPriority`,即该优先级从位图中清零。(`portRESET_READY_PRIORITY( pxCurrentTCB->uxPriority, uxTopReadyPriority )`)。
    }

    (仅开启vTaskSuspend)如果`xTicksToWait == portMAX_DELAY`且`xCanBlockIndefinitely != pdFALSE`:
    {
        将该任务插入Suspended队列。
    }
    否则:
    {
        计算唤醒时刻`xTimeToWake = xConstTickCount + xTicksToWait`。
        记录该任务的唤醒时刻(`listSET_LIST_ITEM_VALUE( &( pxCurrentTCB->xStateListItem ), xTimeToWake)`)。

        如果`xTimeToWake`溢出(`xTimeToWake < xConstTickCount`):
        {
            将任务插入OverflowDelayed任务队列。
        }
        如果未溢出:
        {
            将任务插入Delayed任务队列。

            如果该任务的唤醒时刻比系统下一次唤醒时刻早(`xTimeToWake < xNextTaskUnblockTime`),则更新系统下一次唤醒时刻为该任务的唤醒时刻(`xNextTaskUnblockTime = xTimeToWake`)。
        }
    }
}

下面来看看睡眠的具体实现:

{
    初始化`xAlreadyYielded = pdFALSE`。
    如果`xTicksToDelay > 0`:
    {
        挂起调度器(`vTaskSuspendAll()`)。
        将当前的调用者任务加入到Delayed任务队列( `prvAddCurrentTaskToDelayedList( xTicksToDelay, pdFALSE )` )。
        恢复调度器(`xAlreadyYielded = vTaskResumeAll()`)。
    }

    如果`xAlreadyYielded == pdFALSE`,则强制触发一次调度`portYIELD_WITHIN_API()`。
}

可以看到,即使传入的xTicksToDelay = 0,该API也会触发一次调度,因此需要特别注意这个行为是否符合预期。

需要注意的是,该睡眠函数的睡眠时间是相对于调用vTaskDelay()的时刻而言的。因此,该函数并不适合用于实现一些需要高度精确的周期性的操作,因为如果在调用该函数前,该任务被其他任务或中断抢占,那么此时的周期性就不那么精确了。

于是才有了下面的函数原型:需要设置INCLUDE_vTaskDelayUntil = 1

void vTaskDelayUntil( TickType_t * const pxPreviousWakeTime, const TickType_t xTimeIncrement ) PRIVILEGED_FUNCTION;

从参数可以看出与前者的不同,需要注意的是,第一次传入pxPreviousWakeTime时,需要将其设置为当前的TickCount(xTaskGetTickCount()),之后该函数将会维护其值。

我们再来看看这个函数的实现:

{
    初始化`xShouldDelayed = pdFALSE`。
    入参检查:Assert`pxPreviousWakeTime != NULL`,`xTimeIncrement > 0`
    Assert 调度器不处于挂起状态。

    挂起调度器(`vTaskSuspendAll()`)。
    初始化`xConstTickCount = xTickCount`。
    计算唤醒时刻`xTimeToWake = *pxPreviousWakeTime + xTimeIncrement`。
    如果当前系统时钟溢出(`xConstTickCount < *pxPreviousWakeTime`):
    {
        如果唤醒时刻比当前系统Tick晚(`(xTimeToWake < *pxPreviousWakeTime) && (xTimeToWake > xConstTickCount)`),则设置`xShouldDelay = pdTRUE`。
    }
    如果没有溢出:
    {
        如果唤醒时刻比当前系统Tick晚(`(xTimeToWake < *pxPreviousWakeTime) || (xTimeToWake > xConstTickCount)`),则设置`xShouldDelay = pdTRUE`。
    }

    更新上一次唤醒时刻为本次计算的唤醒时刻`*pxPreviousWakeTime = xTimeToWake`。

    如果设置了Delay标志(`xShouldDelay != pdFALSE`):
    {
        将当前任务加入Delayed任务队列(`prvAddCurrentTaskToDelayedList( xTimeToWake - xConstTickCount, pdFALSE )`。
    }

    恢复调度器(`xAlreadyYielded = xTaskResumeAll()`);

    因为调用者已经主动进入睡眠,所以,如果`xAlreadyYielded == pdFALSE`,则强制触发一次调度(`portYIELD_WITHIN_API()`)。
}

从实现中可以看到,这个睡眠是以pxPreviousWakeTime为基准而进行唤醒时刻计算的,因此可以解决上一个API的问题。

如果想中途唤醒睡着的任务怎么办?贴心的FreeRTOS还提供了相应的API:需要设置INCLUDE_vTaskAbortDelay = 1

BaseType_t xTaskAbortDelay( TaskHandle_t xTask ) PRIVILEGED_FUNCTION;

该函数能将任务从睡眠状态唤醒,需要特别说明一下,该函数不仅可以唤醒vTaskDelay()/vTaskDelayUntil()引起的Blocked的任务,还包括由于调用xQueueReceive()ulTaskNotifyTake()而被动休眠的任务。

下面看看其具体实现:

{
    初始化`pxTCB = ( TCB_t * ) xTask`,`xReturn = pdFALSE`。

    参数校验:Assert `pxTCB != NULL`。

    挂起调度器(`vTaskSuspendAll()`)。

    如果该任务处于Block状态(`eTaskGetState( xTask ) == eBlocked`):
    {
        *FreeRTOSv10修正,设置`xReturn = pdPASS`*。

        将任务从当前状态任务队列中删除。

        进入临界区(`taskENTER_CRITICAL()`)。

        如果任务正在等待某个事件(`listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) != NULL`):
        {
            将任务从该事件任务队列中移除。

            设置任务的延迟中止标志(`pxTCB->ucDelayAborted = pdTRUE`)。
        }

        退出临界区(`taskEXIT_CRITICAL()`)。

        将任务加入到Ready队列(`prvAddTaskToReadyList( pxTCB )`)。

        (仅开启USE_PREEMPTION)如果被中止休眠的任务的优先级比当前任务优先级高(`pxTCB->uxPriority > pxCurrentTCB->uxPriority`),则设置调度器请求标志(`xYieldPending = pdTRUE`)。
    }

    恢复调度器(`xTaskResumeAll()`)。

    返回`xReturn`。
}

9.2. 优先级操作

获取任务优先级的函数原型:需要设置INCLUDE_uxTaskPriorityGet = 1

UBaseType_t uxTaskPriorityGet( TaskHandle_t xTask ) PRIVILEGED_FUNCTION;
UBaseType_t uxTaskPriorityGetFromISR( TaskHandle_t xTask ) PRIVILEGED_FUNCTION;

获取指定任务的优先级。用NULL作为实参可表示当前任务。

可以看到,这个系统调用有两个版本,一个带有FromISR后缀,另一个则没有。这是FreeRTOS的一个特性,在使用时FreeRTOS的某些系统调用,使用者需要区分调用者的运行上下文,即是运行在Task上下文还是中断上下文,这是为了防止在中断上下文调用了一些耗时较长的API。而带有FromISR后缀的就是中断上下文中调用的版本。详情可参看在ISR中使用FreeRTOS API。

在实现时,对于任务上下文的版本,需要在临界区获取这个任务的优先级信息,然后返回给调用者;对于中断上下文的版本,需要在屏蔽低优先级中断的情况下获取这个任务的优先级信息,然后返回给调用者。

修改任务的优先级的函数原型:需要配置INCLUDE_vTaskPrioritySet = 1

void vTaskPrioritySet( TaskHandle_t xTask, UBaseType_t uxNewPriority ) PRIVILEGED_FUNCTION;

需要注意的是,如果修改后的任务优先级比当前正在执行的任务的高,那么将会发生一次调度。

下面来看看这个函数的实现细节:

{
    初始化`xYieldRequired = pdFALSE`。
    参数检查:Assert `uxNewPriority < configMAX_PRIORITIES`.

    进入临界区(`taskENTER_CRITICAL()`)。

    (仅开启USE_MUTEXES)本地暂存任务当前的base优先级(`uxCurrentBasePriority = pxTCB->uxBasePriority`)。

    如果新设置的优先级与当前的优先级不相同(`uxCurrentBasePriority != uxNewPriority`):
    {
        如果新的优先级大于当前的优先级:
        {
            如果该任务不是系统当前执行的任务(`pxTCB != pxCurrentTCB`),且新的优先级不小于系统当前执行的任务的优先级,则设置调度标志`xYieldRequired = pdTRUE`。
        }
        否则,即如果不大于:
        {
            如果该任务就是系统当前执行的任务(`pxTCB == pxCurrentTCB`),则表明可能有更高优先级的任务可以被调度,设置调度标志`xYieldRequired = pdTRUE`。
        }

        获取任务当前的实际优先级`uxPriorityUsedOnEntry = pxTCB->uxPriority`。

        (仅开启USE_MUTEXES)更新优先级继承时的任务优先级:
        {
            如果该任务未进行优先级继承(`pxTCB->uxBasePriority == pxTCB->uxPriority`),则更新实际优先级为新的优先级(`pxTCB->uxPriority = uxNewPriority`)。

            更新任务的Base优先级为新优先级(`pxTCB->uxBasePriority = uxNewPriority`)。
        }

        如果任务的event list item value没有被占用(`listGET_LIST_ITEM_VALUE( &( pxTCB->xEventListItem ) ) & taskEVENT_LIST_ITEM_VALUE_IN_USE ) == 0UL`),那么设置该值为`configMAX_PRIORITIES - uxNewPriority`(为了让任务可以在事件任务队列中按照优先级顺序排列)。

        如果该任务正处于某个Ready任务队列(`listIS_CONTAINED_WITHIN( &( pxReadyTasksLists[ uxPriorityUsedOnEntry ] ), &( pxTCB->xStateListItem ) ) != pdFALSE`):
        {
            将该任务从当前这个优先级Ready任务队列中删除,如果此时该优先级队列已经为空,则更新优先级位图(`portRESET_READY_PRIROITY( uxPriorityUsedOnEntry, uxTopReadyPriority )`)。

            将任务添加到新的Ready任务队列(`prvAddTaskToReadyList( pxTCB )`)。
        }

        如果调度标志位被置位(`xYieldRequired != pdTRUE`),则触发一次调度(`taskYIELD_IF_USING_PREEMPTION()`)。
    }

    退出临界区(`taskEXIT_CRITICAL()`)。
}

9.3. 任务状态操作

获取任务当前所处的状态的函数原型:需要设置INCLUDE_eTaskGetState = 1

eTaskState eTaskGetState( TaskHandle_t xTask ) PRIVILEGED_FUNCTION;

返回的是任务状态枚举类型。注意入参不能为NULL

由于在TCB中,并没有直接存储任务当前状态的数据成员,因此需要通过该任务所在的任务队列来判断它所处的状态。具体可查看如下实现:

{
    入参检查:Assert `pxTCB != NULL`。

    如果`pxTCB == pxCurrentTCB`,表示该任务处于Running状态。(`eReturn = eRunning`)。
    否则:
    {
        进入临界区(`taskENTER_CRITICAL()`)。
        获取该任务所在的状态任务队列(`pxStateList = ( List_t * ) listLIST_ITEM_CONTAINER( &( pxTCB->xStateListItem ) )`)。
        退出临界区(`taskEXIT_CRITICAL()`)。

        如果该队列是Delayed任务队列(`( pxStateList == pxDelayedTaskList ) || ( pxStateList == pxOverflowDelayedTaskList )`),则表示任务处于Blocked状态。(`eReturn = eBlocked`)。

        (仅开启vTaskSuspend)若该队列是Suspended任务队列,且任务没有在事件任务队列中,则认为该任务处于Suspended状态(`xReturn = eSuspended`);否则,处于Blocked状态(`eReturn = eBlocked`)。

        (仅开启vTaskDelete)若该队列是Deleted任务队列或任务不在任何队列中,则认为该任务处于Deleted状态。(`xReturn = eDeleted`)。

        否则,则认为任务处于Ready状态(`xReturn = eReady`),所以PendingReady也被任务是Ready。
    }

    返回`xReturn`。
}

9.4. 任务挂起与恢复

挂起任务的函数原型:需要设置INCLUDE_vTaskSuspend = 1

void vTaskSuspend( TaskHandle_t xTaskToSuspend ) PRIVILEGED_FUNCTION;

将指定的任务挂起。被挂起的任务将进入Suspended状态,除了vTaskResume()无人能把它唤醒。

下面看看挂起时FreeRTOS主要做了啥:

{
    进入临界区(`taskENTER_CRITICAL()`)。
    将任务从Ready任务队列中删除,如果此时该队列为空,则需要更新优先级位图。

    如果该任务还处于事件任务队列中,将任务从中删除。

    将任务插入Suspended任务队列中。

    (仅开启USE_TASK_NOTIFICATIONS,*FreeRTOSv10后引入*)若当前任务正在等待某个通知(`pxTCB->ucNotifyState == taskWAITING_NOTIFICATION`),则复位其等待状态为`taskNOT_WAITING_NOTIFICATION`。

    退出临界区(`taskENTER_CRITICAL()`)。

    如果调度器未被挂起(`xSchedulerRunning != pdFALSE`):
    {
        进入临界区(`taskENTER_CRITICAL()`)。
        更新系统下一次唤醒的时刻(`prvResetNextTaskUnblockTime()`)。
        退出临界区(`taskEXIT_CRITICAL()`)。
    }

    如果该任务是正在运行的任务(`pxTCB == pxCurrentTCB`):
    {
        若调度器未被挂起(`xSchedulerRunning != pdFALSE`),则发起一次调度(`portYIELD_WITHIN_API()`)。
        若调度器被挂起:
        {
            若系统中所有的任务都被挂起(`listCURRENT_LIST_LENGTH( &xSuspendedTaskList ) == uxCurrentNumberOfTasks`),则设置系统当前的任务为`NULL`。
            否则,强制切换系统当前运行的任务(`vTaskSwitchContext()`)。
        }
    }
}

唤醒任务的函数原型: 需要设置INCLUDE_vTaskSuspend = 1

void vTaskResume( TashHandle_t xTaskToResume ) PRIVILEGED_FUNCTION;

唤醒被挂起的任务。

先来看看关键的行为:

  • 检查任务是否处于挂起状态(static BaseType_t prvTaskIsTaskSuspended( const TaskHandle_t xTask )):

    {
        初始化`pxTCB = ( TCB_t * ) xTask`
    
        参数校验:Assert `pxTCB != NULL`。
    
        如果任务处于Suspended任务队列(`listIS_CONTAINED_WITHIN( &xSuspendedTaskList, &( pxTCB->xStateListItem ) ) != pdFALSE`),且任务不在PendingReady任务队列(`listIS_CONTAINED_WITHIN( &xPendingReadyList, &( pxTCB->xEventListItem ) ) == pdFALSE`),并且任务不在事件任务队列中(`listIS_CONTAINED_WITHIN( NULL, &( pxTCB->xEventListItem ) ) != pdFALSE`),则返回`pdTRUE`;
        否则返回`pdFALSE`。
    }
    

下面看看它具体做了什么:

{
    入参检查:Assert `xTaskToResume != NULL。`

    若`pxTCB != pxCurrentTCB`:
    {
        进入临界区(`taskENTER_CRITICAL()`)。
        如果任务被挂起(`prvTaskIsTaskSuspended( pxTCB ) != pdFALSE`):
        {
            将任务从Suspended任务队列中删除。
            将任务加入到Ready任务队列中(`prvAddTaskToReadyList( pxTCB )`)。

            如果被唤醒的任务的优先级不低于当前运行任务的优先级,则发起一次调度(`taskYIELD_IF_USING_PREEMPTION()`)。
        }
        退出临界区(`taskEXIT_CRITICAL()`)。
    }
}

可以看出,任务的挂起和恢复并不支持嵌套调用,也就是说,调用一次vTaskResume()便可将其唤醒,不管前面调用了多少次vTaskSuspend()

此外, FreeRTOS还提供了中断上下文版本的唤醒接口:

BaseType_t xTaskResumeFromISR( TaskHandle_t xTaskToResume ) PRIVILEGED_FUNCTION;

该版本的实现与任务上下文中的版本没啥不同. 但需要注意, 不建议使用这个接口来实现Task和中断的同步, 因为中断有可能在任务调用vTaskSuspend()前就触发了, 这样的话这个任务可能就此长眠.

另外, 它的返回值需要特别说明一下: 返回TRUE表示在退出中断处理后需要触发一次任务调度, 也就是说被唤醒的家伙具有比被当前中断打断的任务更高的优先级. 如果返回FALSE就不用触发了.

其实现如下:

{
    初始化`xYieldRequired = pdFALSE`。
    参数检查:Assert `xTaskToResume != NULL`。
    检查中断优先级屏蔽设置是否正确(`portASSERT_IF_INTERRUPT_PRIORITY_INVALID()`)。

    屏蔽低优先级中断(`uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR()`)
    如果该任务处于挂起状态(`prvTaskIsTaskSuspended( pxTCB ) != pdFALSE`):
    {
        如果调度器未被挂起(`uxSchedulerSuspended == pdFALSE`):
        {
            如果该任务的优先级不低于系统当前的任务优先级,设置调度标志`xYieldRequired = pdTRUE`。

            将任务从Suspended队列中删除。
            将任务插入到Ready任务队列中(`prvAddTaskToReadyList( pxTCB )`)。
        }
        否则,即调度器被挂起,将任务加入到PendingReady任务队列(`vListInserEnd( &xPendingReadyList, &( pxTCB->xEventListItem ) )`)。
    }

    恢复低优先级中断屏蔽(`portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus )`)

    返回调度请求标志`xYieldRequired`。
}

9.5. 任务相关的工具接口

获取当前的TickCount函数原型:

TickType_t xTaskGetTickCount( void ) PRIVLEGED_FUNCTION;

这里返回的TickCount是自调用vTaskStartScheduler()开始计算的。另外,该函数还提供了中断上下文的版本:

TickType_t xTaskGetTickCountFromISR( void ) PRIVILEGED_FUNCTION;

设置和获取Application task hook函数的原型:需要配置configUSE_APPLICATION_TASK_TAG = 1

void vTaskSetApplicationTaskTag( TaskHandle_t xTask, TaskHookFunction_t pxHookFunction ) PRIVILEGED_FUNCTION;
TaskHookFunction_t xTaskGetApplicationTaskTag( TaskHandle_t xTask) PRIVILEGED_FUNCTION;

调用Application Task Hook的函数原型:需要配置configUSE_APPLICATION_TASK_TAG = 1

BaseType_t xTaskCallApplicationTaskHook( TaskHandle_t xTask, void *pvParameter ) PRIVILEGED_FUNCTION;

与上面的函数接口一同使用, 调用指定Task的钩子函数。// TODO: 需要进一步研究有啥用。

外部传入的Task私有存储:需要设置configNUM_THREAD_LOCAL_STORAGE_POINTERS > 0

void vTaskSetThreadLocalStoragePointer( TaskHandle_t xTaskToSet, BaseType_t xIndex, void *pvValue) PRIVILEGED_FUNCTION;
void *pvTaskGetThreadLocalStoragePointer( TaskHandle_t xTaskToQuery, BaseType_t xIndex) PRIVILEGED_FUNCTION;

FreeRTOS提供了一个可被外部获取的Task私有存储机制:每个任务包含一组指针(该指针的数量由configNUM_THREAD_LOCAL_STORAGE_POINTERS决定),FreeRTOS内核并不会使用它,而是由外部来决定如何使用它们。// TODO: 非常神奇,不知道有啥用途。

获取Idle Task的句柄的函数原型:需要设置INCLUDE_xTaskGetIdleTaskHandle = 1

TaskHandle_t xTaskGetIdleTaskHandle( void ) PRIVILEGED_FUNCTION;

很显然,这个函数必须在开启了调度器后才能调用,否则压根就找不到Idle Task这玩意儿。

9.6. 调试相关操作

FreeRTOS提供了一些操作系统相关的调试信息获取方式,便于做一些简单的系统监控。

获取当前创建的任务总数的函数原型:

UBaseType_t uxTaskGetNumberOfTasks ( void ) PRIVILEGED_FUNCTION;

需要注意的是,僵尸任务(即被Deleted,但还未被Idle Task回收的任务)也会被统计。

获取任务的名字的函数原型:

char *pcTaskGetName( TaskHandle_t xTaskToQuery ) PRIVILEGED_FUNCTION; /*lint !e971 Unqualified char types are allowed for strings and single characters only. */

通过任务名字查找任务句柄的函数原型:需要设置INCLUDE_xTaskGetHandle = 1

TaskHandle_t xTaskGetHandle( const char *pcNameToQuery ) PRIVILEGED_FUNCTION; /*lint !e971 Unqualified char types are allowed for strings and single characters only. */

需要注意的是,查找字符串是个比较耗时的行为。

获取任务的剩余堆栈空间:需要设置INCLUDE_uxTaskGetStackHighWaterMark = 1

UBaseType_t uxTaskGetStackHighWaterMark( TaskHandle_t xTask ) PRIVILEGED_FUNCTION;

剩余空间的度量单位和vTaskCreate()中的StackDepth一致,都是使用机器字作为单位。

获取任务的详细状态信息:需要设置configUSE_TRACE_FACILITY = 1

void vTaskGetInfo( TaskHandle_t xTask, TaskStatus_t *pxTaskStatus, BaseType_t xGetFreeStackSpace, eTaskState eState) PRIVILEGED_FUNCTION;

pxTaskStatus用于接收状态信息。信息的详情可查看任务状态结构体。

由于获取栈的剩余空间是一个比较耗时的过程,这将会导致系统短时间内不响应其他请求,因此,可通过传入xGetFreeStackSpace = pdFALSE跳过这个过程。

同理,获取当前的任务状态也不是简单的过程,只有当传入eState = eInvalid时才会使能获取当前任务状态过程。

想要一次性获取FreeRTOS当前所有的Task的任务状态信息?了解一下下面这个函数原型:需要设置configUSE_TRACE_FACILITY = 1

UBaseType_t uxTaskGetSystemState( TaskStatus_t * const pxTaskStatusArray, const UBaseType_t uxArraySize, uint32_t * const pulTotalRuntime ) PRIVILEGED_FUNCTION;

需要注意的是,该方法只应该在Debug的时候使用,因为它会将调度器挂起。pxTaskStatusArray用于接收返回的信息,可通过uxTaskGetNumberOfTasks()先获取当前的任务数量,然后申请足够大的空间来存储返回的信息。uxArraySize指该数组中任务状态结构体的个数,而不是字节数。

如果配置了configGENERATE_RUN_TIME_STATS = 1pulTotalRuntime则可获取到开机后的总运行时间。为了实现这个统计功能,还需要外部提供portCONFIGURE_TIMER_FOR_RUN_TIME_STATS()portGET_RUN_TIME_COUNTER_VALUE()的实现,分别用于配置硬件Timer统计时间信息,和获取当前的时间信息。需要注意的是,这个硬件Timer的时钟频率至少得是FreeRTOS Tick的10倍。

如果调用正常,该函数会返回任务状态结构体的数量,正常来说,应该和uxTaskGetNumberOfTasks()的结果是一致的;如果uxArraySize过小,则会返回0.

现如今,大家都喜欢一键式解决方案,上面提供的信息都还不能算是一键式打印系统状态,下面这个才是真正意义上的一键式打印系统状态的函数接口:需要设置configUSE_TRACE_FACILITY = 1configUSE_STATS_FORMATTING_FUNCTIONS = 1

void vTaskList( char * pcWriteBuffer ) PRIVILEGED_FUNCTION;
void vTaskGetRunTimeStats( char * pcWriteBuffer ) PRIVILEGED_FUNCTION;

以上两个函数会直接以字符串的形式打印系统的状态,它在内部调用了uxTaskGetSystemState(),并进行了翻译。通常来说,他们只用于Demo演示,正常情况下,还是建议使用uxTaskGetSystemState()来获取信息。

9.7. 与调度器的实现相关的操作(出于移植的需要而暴露)

以下的接口不能在应用程序代码中使用,因为会导致系统的行为混乱。

增加Tick计数的函数原型:

BaseType_t xTaskIncrementTick( void ) PRIVILEGED_FUNCTION;

该函数会增加tick count,并检查是否有等待超时的Blocked任务需要被唤醒。如果函数返回了非零值,那么说明发生了以下两种情况中的一种:

  • 有一个任务超时了,需要被唤醒。
  • 如果采用时间片调度算法,则说明有一个相同优先级的任务需要被调度了。

详细实现可参看时间的度量和Tick中断部分。

下面介绍与事件队列相关的API:必须在关闭中断的情况下调用

void vTaskPlaceOnEventList( List_t * const pxEventList, const TickType_t xTicksToWait ) PRIVILEGED_FUNCTION;
void vTaskPlaceOnUnorderedEventList( List_t * pxEventList, const TickType_t xItemValue, const TickType_t xTicksToWait ) PRIVILEGED_FUNCTION;

该函数将调用者从Ready队列中移除,并放置到等待事件任务队列(pxEventList)和延迟任务队列中。只有当相应事件发生,或超时时才能被唤醒。

FreeRTOS为队列中的任务提供了两种排序方式的实现:

  • unorder:将链表项的值设置为入参xItemValue的值,并将链表项插入到链表的末尾。用于消息队列的实现。
  • ordered:使用Task的优先级作为链表项的排列依据,因此新插入的Task也将放置到合适的位置。用于事件组的实现。

xTicksToWait指的是Task希望等待的最长时限,单位是Tick Count。可与portTICK_PERIOD_MS一同使用。

需要注意的是,以上两个函数都只支持任务的等待事件的时间是确定的。而FreeRTOS还提供了不确定等待时限的版本:

void vTaskPlaceOnEventListRestricted( List_t * const pxEventList, TickType_t xTicksToWait, const BaseType_t xWaitIndefinitely ) PRIVILEGED_FUNCTION;

xWaitIndefinitelypdTRUE时,则采用不确定时限的等待模式;否则采用确定时限模式。其余行为与vTaskPlaceOnEventList()一致。

有添加就一定会有删除,而从事件任务队列中删除任务的函数原型如下:必须在关闭中断的情况下调用

BaseType_t xTaskRemoveFromEventList( const List_t * const pxEventList ) PRIVILEGED_FUNCTION;
BaseType_t xTaskRemoveFromUnorderedEventList( ListItem_t * pxEventListItem, const TickType_t xItemValue ) PRIVILEGED_FUNCTION;

这两个函数会在指定事件发生,或者等待超时时被调用。他们主要负责将任务从任务事件队列和Blocked任务队列中移除,并放入Ready任务队列中。他们分别与vTaskPlaceOnEventList()vTaskPlaceOnUnorderedEventList()对应。xTaskRemoveFromEventList()则将任务从头部删除,因为它一定是最高优先级的任务;而xTaskRemoveFromUnorderedEventList()会将链表项的值设置为xItemValueFreeRTOSv10后,xTaskRemoveFromUnorderedEventList()修改为void vTaskRemoveFromUnorderedEventList(...)

这些接口的详细实现可参看队列控制和等待和查询事件组部分。

下面要介绍调度器的重要功能,上下文切换的函数原型:

void vTaskSwitchContext( void ) PRIVILEGED_FUNCTION;

它要完成的功能其实很简单,就是找到当前Ready队列中优先级最高的任务,并把当前的TCB指针指向该任务。其实现参看任务切换部分。

复位Task的Event链表项的Item value:

TickType_t uxTaskResetEventItemValue( void ) PRIVILEGED_FUNCTION;

在后续介绍数据结构的章节(链表)中会说明,链表项拥有者和链表项之间是互联的,将这个概念应用到任务队列里面则是:事件任务队列的链表项和对应的任务是互联的,也就是说每个任务自身包含了指向事件任务队列链表项的指针,而该接口可复位链表项的xItemValue,使得它可以被其他其他模块(例如:队列和信号量)使用。该函数将返回复位前的xItemValue值。

获取当前任务句柄的函数原型:

TaskHandle_t xTaskGetCurrentTaskHandle( void ) PRIVILEGED_FUNCTION;

获取当前的Tick信息的函数原型:

void vTaskSetTimeOutState( TimeOut_t * const pxTimeOut) PRIVILEGED_FUNCTION;

超时状态结构体仅用于内部实现。该函数与以下函数配合使用:

BaseType_t xTaskCheckForTimeOut( TimeOut_t * const pxTimeOut, TickType_t * const pxTicksToWait ) PRIVILEGED_FUNCTION;

该函数会与vTaskSetTimeOutState()捕获的时间信息进行比较,查看是否超时。如果超时,则返回pdTRUE;否则返回pdFALSE。该接口用于队列的实现

设置存在挂起的调度请求的函数原型:用于队列的实现。

void vTaskMissedYield( void ) PRIVILEGED_FUNCTION;

该函数可用于避免不必要的taskYIELD()调用。该接口用于队列的实现

获取调度器的状态的函数原型:

BaseType_t xTaskGetSchedulerState( void ) PRIVILEGED_FUNCTION;

返回调度器当前的状态:

  • taskSCHEDULER_RUNNING:正在运行
  • taskSCHEDULER_NOT_STARTED:未启动
  • taskSCHEDULER_SUSPENDED:调度器被挂起

FreeRTOS也支持任务优先级继承的机制,函数原型:

BaseType_t xTaskPriorityInherit( TaskHandle_t const pxMutexHolder ) PRIVILEGED_FUNCTION;

该功能是为互斥锁而设计的。每当有高优先级的任务申请互斥锁,且该锁被低优先级的任务占有时,则会调用该函数进行优先级的继承,即低优先级任务继承高优先级任务的优先级。

在发生过优先级继承的任务释放锁时,还需要将优先级还原。此时则需要调用如下函数接口:

BaseType_t xTaskPriorityDisinherit( TaskHandle_t const pxMutexHolder ) PRIVILEGED_FUNCTION;

这里还存在一个特殊情况:当高优先级的任务等待超时后,它将会放弃申请互斥锁,这时,也需要恢复低优先级任务的优先级。

void vTaskPriorityDisinheritAfterTimeout( TaskHandle_t const pxMutexHolder, UBaseType_t uxHighestPriorityWaitingTask ) PRIVILEGED_FUNCTION;

需要注意的是,因为仍然有可能存在其他高优先级的任务在等待该锁,所以需要将目前处于等待的最高优先级任务的优先级通过uxHighestPriorityWaitingTask传入。

对于互斥锁的实现,FreeRTOS还提供了一个增加互斥锁持有数量的接口:

void *pvTaskIncrementMutexHeldCount( void ) PRIVILEGED_FUNCTION;

该函数会为Task持有的互斥锁数量增1,并且,返回持有该锁的Task的句柄。

这些接口会在资源管理章节中详细介绍。

获取任务编号的函数原型:

UBaseType_t uxTaskGetTaskNumber( TaskHandle_t xTask ) PRIVILEGED_FUNCTION;

该函数返回xTask的uxTCBNumber

上面介绍了getter,下面就介绍setter,设置任务编号的函数原型:

void vTaskSetTaskNumber( TaskHandle_t xTask, const UBaseType_t uxHandle ) PRIVILEGED_FUNCTION;

下面介绍的两个函数原型只在Tickless idle或低功耗模式中使用。

设置需要跳过的Tick数的函数原型:需要设置configUSE_TICKLESS_IDLE = 1

void vTaskStepTick( const TickType_t xTickToJump ) PRIVILEGED_FUNCTION;

当处于Tickless idle模式的idle周期时,tick中断将会被暂停。而Tick count是在tick中断处理函数中更新的,一旦tick中断被暂停,系统的tick计数就不再更新了。因此,为了确保tick计数正常,当系统退出idle周期时,需要手动更新一次tick count。而该函数就是为了实现这个功能。xTickToJump的值应该恰好等于idle的周期。

确认当前是否可进入Sleep模式的函数原型:需要设置configUSE_TICKLESS_IDLE = 1

eSleepModeStatus eTaskConfirmSleepModeStatus( void ) PRIVILEGED_FUNCTION;

FreeRTOS的低功耗机制中,portSUPPRESS_TICKS_AND_SLEEP()提供了进入低功耗的接口。这是一个平台相关的宏,它的语义是让处理器进入等待状态(例如ARM中的WFI)。而该宏只是在调度器挂起的情况下调用的,因此这个过程中有可能被中断处理打断而发生上下文切换。因此,该函数为这个平台宏的实现提供了一个确认是否满足睡眠条件的接口,并且要求该函数在临界区被调用

10. 删除任务

删除任务这个行为很好理解,类似于Linux终止进程的过程。

函数原型:需要配置INCLUDE_vTaskDelete = 1开启功能

void vTaskDelete( TaskHandle_t xTaskToDelete ) PRIVILEGED_FUNCTION;

将指定的任务从内核中删除,这意味着不管它在Ready队列、Blocked队列、Suspended队列或事件队列中,它都会被删除。而Idle Task将负责收回该任务在创建时申请的两片内存。

与Linux不同的是,FreeRTOS的任务没有独立的堆,因此,删除任务前一定要手动释放动态申请的内存

下面来看看它的实现。该函数的活动图如下:

删除任务

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

FreeRTOS学习-任务管理(Task管理)(1) 的相关文章

  • 当任务抛出异常而不等待终结器时如何使应用程序崩溃

    我们在 Net 4 没有可用的异步等待 应用程序中使用任务 有时它们用于启动 Fire and Forget 操作 如下所示 private void Test Task task Task Factory StartNew gt thro
  • 任务与异步任务

    好吧 我一直在试图解决这个问题 我读过一些文章 但没有一篇提供我正在寻找的答案 我的问题是 为什么Task必须返回一个任务async Task不是吗 例如 public override Task TokenEndpoint OAuthTo
  • Task.Yield、Task.Run 和ConfigureAwait(false) 之间有什么区别?

    据我了解 Task Yield如果调用者没有等待该方法 则在方法开始处将强制调用者继续 同时Task Run and ConfigureAwait false both https stackoverflow com questions 1
  • Task.Convert 扩展方法有用还是有隐患?

    我正在为 Google Cloud API 编写客户端库 该库具有相当常见的异步帮助器重载模式 做一些简短的同步工作来设置请求 发出异步请求 以简单的方式转换结果 目前我们正在使用异步方法 但是 就优先级而言 转换await的结果最终会很烦
  • ConcurrentDictionary.GetOrAdd 真的是线程安全的吗?

    我有这段代码 如果该任务是为相同的输入创建的 我想等待正在进行的任务 这是我正在做的事情的最小再现 private static ConcurrentDictionary
  • Task.WhenAll() 和 foreach(任务中的 var task) 有什么区别

    经过几个小时的努力 我在我的应用程序中发现了一个错误 我认为下面的两个函数具有相同的行为 但事实证明它们并非如此 谁能告诉我幕后到底发生了什么 以及为什么他们的行为方式不同 public async Task MyFunction1 IEn
  • 对已知已完成的任务调用 .Result 或await 之间有区别吗? [复制]

    这个问题在这里已经有答案了 以下代码块中是否存在任何功能 性能或死锁风险差异 示例1 await Task WhenAll task1 task2 var result1 await task1 var result2 await task
  • 有可用的 FreeRTOS 解释语言库吗?

    我在一家公司工作 该公司使用 FreeRTOS 为多个设备创建固件 最近 我们对新功能的要求已经超出了我们固件工程师的工作能力 但我们现在也无力雇用任何新人 即使进行微小的更改 也需要固件人员在非常低的级别上进行修改 我一直在为 FreeR
  • C++ freeRTOS任务,非静态成员函数的无效使用

    哪里有问题 void MyClass task void pvParameter while 1 this gt update void MyClass startTask xTaskCreate this gt task Task 204
  • 如何执行任务以避免用户被迫等待响应?

    用户创建新产品后 在我的应用程序中 我执行多项操作 例如更新几个表 统计数据 财务 使用情况 库存等 现在用户必须等待我完成所有步骤 如果很多用户托盘同时执行此操作 则等待时间会更长 这不太好 我的计划是创建一个特殊的 TASK TABLE
  • 在 C++/CLI 中使用 .NET (3.5) 任务并行库

    好吧 我下载了 Reactive Extensions for NET 3 5 以便在 Visual Studio 2008 中通过 c cli 使用它 但所有任务并行库示例都是用 C 编写的 我什至无法弄清楚将简单的 C 语句转换为 C
  • Task.WhenAll 是否在后台线程并行运行任务

    以下2个代码片段的作用相同吗 1 var producer Task Run async gt await bar ReadDataAsync var consumer Task Run async gt await bar WriteDa
  • 异步 ServiceController.WaitForStatus 如何执行?

    So ServiceController WaitForStatus https msdn microsoft com en us library system serviceprocess servicecontroller waitfo
  • 将同步代码包装为异步任务的最佳方法是什么?

    我正在实现一个异步接口方法 返回任务 然而 我的实现必然是同步的 最好的方法是什么 有一些内置的方法可以做到这一点吗 以下是我正在考虑的几个选项 选项 1 Task FromResult return Task FromResult Com
  • 如何限制创建 celery 任务的速度快于消耗速度的脚本?

    我有一个脚本可以生成数百万个 Celery 任务 数据库中每行一个任务 有没有办法限制它 以免它完全淹没芹菜 理想情况下 我想让 Celery 保持忙碌 但我不希望 Celery 队列的长度超过几十个任务 因为这只是浪费内存 特别是因为如果
  • 小型 ARM 微控制器的 RTOS 内核之间的可量化差异 [关闭]

    Closed 这个问题是基于意见的 help closed questions 目前不接受答案 有许多不同的 RTOS 可用于微控制器 我专门寻找支持 ARM Cortex M 处理器的 RTOS 另外 我对闭源解决方案不感兴趣 试图从网站
  • 在 MVC 中从同步调用异步而没有等待时,TPL 任务死锁

    我知道在同步 MVC 方法中调用异步方法 同时使用 Wait 或 Result 等待任务完成时 存在 TPL 死锁陷阱 但我们刚刚在 MVC 应用程序中发现了一个奇怪的行为 同步操作调用异步方法 但由于它是触发器 因此我们从未等待它完成 尽
  • 异步任务、视频缓冲

    我正在尝试理解 C 中的任务 但仍然遇到一些问题 我正在尝试创建一个包含视频的应用程序 主要目的是从文件中读取视频 我使用 Emgu CV 并通过 TCP IP 发送它以在板上进行处理 然后以流 实时 方式返回 首先 我是连续做的 所以 读
  • 在 VSCode 的集成终端中运行任务?

    当我过去运行任务 tasks json 时 它们在 VSCode 的集成终端内运行 但是 在重置我的开发计算机并重新安装所有内容后 我的任务现在在新的 cmd 窗口中运行 当任务因错误而失败时 就会出现问题 在这种情况下 cmd 窗口刚刚关
  • 通过等待任务或访问其 Exception 属性都没有观察到任务的异常

    这些是我的任务 我应该如何修改它们以防止出现此错误 我检查了其他类似的线程 但我正在使用等待并继续 那么这个错误是怎么发生的呢 通过等待任务或访问其 Exception 属性都没有观察到任务的异常 结果 未观察到的异常被终结器线程重新抛出

随机推荐

  • Windows 10 内置linux执行带GUI的应用程序

    1 安装MobaXterm xff0c 并运行 2 打开内置的Linux xff0c 命令执行带GUI的运行程序即可
  • Repo介绍

    目录 1 概要2 工作原理 2 1 项目清单库 repo manifests 2 2 repo脚本库 repo repo 2 3 仓库目录和工作目录 3 使用介绍 3 1 init3 2 sync3 3 upload3 4 download
  • Android 8.0.0-r4源码目录结构详解

    android的移植按如下流程 1 android linux 内核的普通驱动移植 让内核可以在目标平台上运行起来 2 正确挂载文件系统 确保内核启动参数和 android 源代码 system core rootdir 目录下的 init
  • Android8.0.0-r4的编译系统

    一 概述 1 1 编译系统变化 从Android 7 0开始 xff0c android的编译系统发生了变化 xff0c 之前依赖Makefile组织编译系统 xff0c 从7 0开始逐步引入了kati soong optional未正式使
  • [Android Studio]Android Studio 三种添加插件的方式

    何给Android Studio添加插件 添加插件的路径有三种 xff0c 我把他们分类如下 xff1a 点击设置小按钮 点击 xff3b Plugins xff3d 这里展示的是你已经安装的插件 xff0c 我们可以点击插件名称 xff0
  • Gerrit 服务器插件安装-示例插件delete project

    gerrit2 X 中没法直接删除一个项目 xff0c 之前需要手工删除 xff0c 后来社区提供了一个插件delete project来搞定这个事 xff0c 安装方法如下 xff1a 到 gerritforge xff0c 找到对应的
  • Windows平台下载Android源码(整理)

    Google官方下载源码使用的系统Ubuntu系统 xff0c 不过现在我们需要在Windows系统中下载Android源码文件 网站的地址是 xff1a https android googlesource com 里面包括Android
  • Ubuntu 16.04 文件服务器--samba的安装和配置

    Samba是在Linux系统上实现的SMB xff08 Server Messages Block xff0c 信息服务块 xff09 协议的一款免费软件 它实现在局域网内共享文件和打印机 xff0c 是一个客户机 服务器型协议 客户机通过
  • 深入剖析Android音频之AudioTrack

    播放声音能够用MediaPlayer和AudioTrack xff0c 两者都提供了java API供应用开发人员使用 尽管都能够播放声音 但两者还是有非常大的差别的 当中最大的差别是MediaPlayer能够播放多种格式的声音文件 比如M
  • 树莓派4 运行 Tensorflow Lite

    树莓派4 运行 Tensorflow Lite 1 更新树莓派 span class token function sudo span apt update 2 下载安装脚本 span class token function git sp
  • 操作系统进程进行系统调用详细过程

    翻阅很多资料 xff0c 综合了各处所述进程在进行系统调用之后的状态会如何的解答 xff0c 以下是我个人理解 xff0c 欢迎各位读者纠错 PS 特别感谢以下这个帖子 xff0c 看完他们的讨论我才茅塞顿开 xff0c 非常感谢 xff0
  • 解决Ubuntu 找不到ARM64 的源的问题(转)

    Ubuntu 安装了NVIDIA的驱动还有DriveWokrs之后 xff0c 好像把系统添加了arm64的架构 xff0c 因此 xff0c 在源更新的时候 xff0c 也会更新arm64相关的源 xff0c 但是问题在于 xff0c 用
  • asp.net 实现打开文件所在的文件夹, 本地可以打开,发布后点击按钮没有反应的解决办法

    此类情况大概是安全范畴的问题 确定上传文件夹的共享 xff0c iis 以及电脑帐户 xff0c 以及aspnet 等是否有对应的相关权限 1 确认ASPNET 账户属于管理员级别 2 在 服务 里面找到 IIS Admin xff0c 双
  • numpy 和 tensor 的区别

    关系 xff1a 两者共享内存 xff0c 转换方便 xff0c 没有额外的开销 区别 xff1a 1 数据类型上面的区别 xff1a numpy 默认类型是 float64 int32 tensor 默认类型是float32 int64
  • 关于docker无法apt-get update的问题

    在看这篇文章https www jianshu com p 21d66ca6115e 有一个部分是 但是发现自己的 Node 没有ping命令 想着去apt get update 但是出现如下错误 只要在命令签名加上 sudo 就行
  • ubuntu下安装zip unzip

    安装命令 apt get install zip unzip 执行命令常见错误 xff1a 1 unable to locate package 解决办法 xff1a 执行sudo apt get update命令后再执行安装命令就可以了
  • 平衡小车卡尔曼滤波算法

    最近研究STM32的自平衡小车 xff0c 发现有两座必过的大山 xff0c 一为卡尔曼滤波 xff0c 二为PID算法 网上看了很多关于卡尔曼滤波的代码 xff0c 感觉写得真不咋地 一怒之下 xff0c 自己重写 xff0c 不废话 x
  • FreeRTOS学习-前言与FreeRTOS发行版

    1 前言 因为工作的需要 xff0c 学习FreeRTOS已经有一段时间了 接下来一段时间会定期更新本人学习FreeRTOS的系列笔记 系列笔记主要参考了官方的说明手册和FreeRTOS的源代码 其主要思想是先了解FreeRTOS的对外接口
  • FreeRTOS学习-内存管理

    1 动态内存分配与FreeRTOS 从v9 0 0后 xff0c FreeRTOS开始支持内核对象的静态分配方式 xff0c 因此 xff0c 内存管理库可以被裁剪 但在大多数嵌入式应用中 xff0c 堆的使用还是非常常见的 因此 xff0
  • FreeRTOS学习-任务管理(Task管理)(1)

    1 简介 任务管理 xff08 或称进程管理 xff09 是所有操作系统内核的最基本组成模块之一 xff0c FreeRTOS也不例外 想要了解一个操作系统 xff0c 不得不理解其任务管理的设计和实现 任务管理的介绍由两篇文章组成 xff