FreeRTOS task 源码解析

FreeRTOS源码解析——第一章 整体架构 本章主要介绍FreeRTOS的整体框架,给出一个简要的移植示例,文末有示例下载链接。 阅读详情


FreeRTOS 本质上就是有很多的 List 组成,所以学习之前最好要对 FreeRTOS 中的链表要有所了解,可以参考:FreeRTOS 列表 List 源码解析

源码都在 task.c 中

一、基本结构和变量

1、TCB_t

首先来看一下一个任务的结构:

typedef struct tskTaskControlBlock    
{
    volatile StackType_t * pxTopOfStack; /*< 指向任务堆栈中最后放置的项目位置。这必须是TCB结构中的第一个成员,具体原因在后面讲 PendSV 中断的时候会提到 */

    /* MPU 相关,暂时不用管 */
    #if ( portUSING_MPU_WRAPPERS == 1 )
        xMPU_SETTINGS xMPUSettings; /*< The MPU settings are defined as part of the port layer.  THIS MUST BE THE SECOND MEMBER OF THE TCB STRUCT. */
    #endif

    ListItem_t xStateListItem;                  /*< 表示该任务的状态(就绪、阻塞、挂起),不同的状态会挂接在不同的状态链表下 */
    ListItem_t xEventListItem;                  /*< 用于从事件列表中引用任务,会挂接到不同事件链表下 */
    UBaseType_t uxPriority;                     /*< 任务的优先级。0 是最低优先级 */
    StackType_t * pxStack;                      /*< 指向堆栈起始位置,这只是单纯的一个分配空间的地址,可以用来检测堆栈是否溢出 */
    char pcTaskName[ configMAX_TASK_NAME_LEN ]; /*< 任务名,仅用于调试(仅允许用于字符串和单个字符) */

    /* 指向栈尾,可以用来检测堆栈是否溢出 */
    #if ( ( portSTACK_GROWTH > 0 ) || ( configRECORD_STACK_HIGH_ADDRESS == 1 ) )
        StackType_t * pxEndOfStack; /*< Points to the highest valid address for the stack. */
    #endif

    /* 记录临界段的嵌套层数 */
    #if ( portCRITICAL_NESTING_IN_TCB == 1 )
        UBaseType_t uxCriticalNesting; /*< Holds the critical section nesting depth for ports that do not maintain their own count in the port layer. */
    #endif

    /* 跟踪调试用的变量 */
    #if ( configUSE_TRACE_FACILITY == 1 )
        UBaseType_t uxTCBNumber;  /*< 存储一个每次创建TCB时递增的数字。它允许调试器确定何时删除一个任务并重新创建它 */
        UBaseType_t uxTaskNumber; /*< 存储一个专门供第三方跟踪代码使用的数字 */
    #endif

    /* 任务优先级被临时提高时,保存任务原本的优先级 */
    #if ( configUSE_MUTEXES == 1 )
        UBaseType_t uxBasePriority; /*< 最后分配给任务的优先级 - 用于优先级继承机制 */
        UBaseType_t uxMutexesHeld;
    #endif

    /* 任务的一个标签值,可以由用户自定义它的意义,例如可以传入一个函数指针可以用来做 Hook 函数调用 */
    #if ( configUSE_APPLICATION_TASK_TAG == 1 )
        TaskHookFunction_t pxTaskTag;
    #endif

    /* 任务的线程本地存储指针,可以理解为这个任务私有的存储空间 */
    #if ( configNUM_THREAD_LOCAL_STORAGE_POINTERS > 0 )
        void * pvThreadLocalStoragePointers[ configNUM_THREAD_LOCAL_STORAGE_POINTERS ];
    #endif

    /* 运行时间变量 */
    #if ( configGENERATE_RUN_TIME_STATS == 1 )
        uint32_t ulRunTimeCounter; /*< 存储任务在运行状态下所花费的时间 */
    #endif

    /* 支持NEWLIB 的一个变量 */
    #if ( configUSE_NEWLIB_REENTRANT == 1 )

        /* 分配一个特定于此任务的 Newlib reent 结构。 
         * 注意,Newlib 的支持是应广大用户需求而添加的,但并未由 FreeRTOS 的维护者本人使用。
         * FreeRTOS 对于由此产生的 Newlib 操作不承担责任。用户必须熟悉 Newlib,并提供全系统所需的相关实现。
         * 请注意(在撰写时),当前的 Newlib 设计实现了一个需要锁的全系统 malloc()。 */
        struct  _reent xNewLib_reent;
    #endif

    /* 任务通知功能需要用到的变量 */
    #if ( configUSE_TASK_NOTIFICATIONS == 1 )
        volatile uint32_t ulNotifiedValue[ configTASK_NOTIFICATION_ARRAY_ENTRIES ]; /* 任务通知的值 */
        volatile uint8_t ucNotifyState[ configTASK_NOTIFICATION_ARRAY_ENTRIES ];    /* 任务通知的状态 */
    #endif

    /* 用来标记这个任务的栈是不是静态分配的 */
    #if ( tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE != 0 )
        uint8_t ucStaticallyAllocated;              /*< 如果任务是静态分配的,则设置为 pdTRUE,以确保不会尝试释放该内存 */
    #endif

    /* 延时是否被打断 */
    #if ( INCLUDE_xTaskAbortDelay == 1 )
        uint8_t ucDelayAborted;
    #endif

    /* 错误标识 */
    #if ( configUSE_POSIX_ERRNO == 1 )
        int iTaskErrno;
    #endif
} tskTCB;

typedef tskTCB TCB_t;

任务结构体被声明为 TCB_t,也就是 Task Control Block(任务控制块),熟悉这个任务控制块的结构有助于我们对后续源码的理解。

2、状态链表

FreeRTOS 中的任务一共有四种状态分别是运行状态(Running State)、就绪状态(Ready State)、阻塞状态(Blocked State)、挂起状态(Suspended State),其含义可以简单理解为:

  • 运行状态:正在执行的任务。
  • 就绪状态:等待获得执行权的任务。
  • 阻塞状态:直到某些条件达成才会重新进入就绪态等待获得执行权,否则不会执行的任务。
  • 挂起状态:除非被主动恢复,否则永远不会执行。

    这四种状态分别对应着 pxCurrentTCB、pxReadyTasksLists、pxDelayedTaskList、xSuspendedTaskList 这四个变量。除运行状态外,任务处于其它状态时,都是通过将任务 TCB 中的 xStateListItem 挂到相应的链表下来表示的。

因此,FreeRTOS 中任务状态的切换本质上就是把任务项挂接到对应的链表下。

从源码中可以看到 pxReadyTasksLists、pxDelayedTaskList、xSuspendedTaskList 这四个变量的类型是链表数组,每一个下标就表示一个优先级,这样就把同一优先级的多个任务放在了一起,不同优先级是由不同的链表项连接。

进行任务切换的时候,调度器首先选择最高优先级的任务进行切换,而且具有相同优先级的任务会轮流执行。高优先级的任务未执行完低优先级的任务无法执行,因为低优先级无法抢占高优先级。

2.1 pxCurrentTCB

/* 始终指向当前运行的任务 */
PRIVILEGED_DATA TCB_t * volatile pxCurrentTCB = NULL;

当前运行的任务只可能有一个,因此 pxCurrentTCB 只是单个 TCB_t 指针。

2.2 pxReadyTasksLists

#define configMAX_PRIORITIES ( 10 )

/* 由链表组成的数组,每一个成员都是由处于就绪态而又有着相同任务优先级的任务组成的的链表. */
PRIVILEGED_DATA static List_t pxReadyTasksLists[ configMAX_PRIORITIES ]; 

除此之外,还有一个变量 uxTopReadyPriority。其的定义如下

#define tskIDLE_PRIORITY ( ( UBaseType_t ) 0U )

PRIVILEGED_DATA static volatile UBaseType_t uxTopReadyPriority = tskIDLE_PRIORITY;

uxTopReadyPriority 存储的是有任务挂接的最高优先级。pxReadyTasksLists、pxCurrentTCB 和 uxTopReadyPriority 三者之间的关系可由以下的图来表示:

当使用时间片时,pxCurrentTCB 会在有任务挂接的最高优先级链表中遍历,以实现它们对处理器资源的分时共享。

2.3 pxDelayedTaskList

延时链表的作用不仅是用来处理任务的延时,任务的阻塞也是由它进行实现的。

PRIVILEGED_DATA static List_t xDelayedTaskList1;                         /*< 延时任务队列 */
PRIVILEGED_DATA static List_t xDelayedTaskList2;                         /*< 延时任务队列 (使用两个列表:一个用于已溢出当前tick计数的延迟 */
PRIVILEGED_DATA static List_t * volatile pxDelayedTaskList;              /*< 指向当前正在使用的延时任务列表 */
PRIVILEGED_DATA static List_t * volatile pxOverflowDelayedTaskList;      /*< 指向当前正在使用的延时任务列表,用于保存已溢出当前tick计数的任务 */

可以看到这里有两个 xDelayedTaskList:xDelayedTaskList1、xDelayedTaskList1。这是由于 pxDelayedTaskList 要处理和时间相关的信息,所以需要考虑到系统的 systick 溢出的处理。为了解决这一繁琐的问题,FreeRTOS 设计了两个延时链表和两个延时链表指针来处理溢出问题。

如下图,xDelayedTaskList1 和 xDelayedTaskList2 是两个实际链表,其中任务的排列顺序是按退出阻塞时间排序的,也就是链表的第一个成员任务是将最早退出阻塞,而最后一个成员任务是最后退出阻塞的。当系统的 systick 溢出时,pxDelayedTaskList 和pxOverflowDelayedTaskList 指向的链表地址也会随之交换一次,实现对溢出的处理。对于溢出的处理在后面会结合源码分析。以下是四个变量之间的关系:

与延时任务链表变量为 xNextTaskUnblockTime。其定义如下:

/* 存储的是下一个任务进行解除阻塞操作的时间,用来判断在何时进行解除阻塞操作 */
PRIVILEGED_DATA static volatile TickType_t xNextTaskUnblockTime = ( TickType_t ) 0U; 

2.4 xSuspendedTaskList

/*< 已被挂起的任务 */
PRIVILEGED_DATA static List_t xSuspendedTaskList; 

3、任务调度器相关

3.1 xSchedulerRunning

/* 表示任务调度器是否已经运行(挂起的任务调度器也算在运行状态) */
PRIVILEGED_DATA static volatile BaseType_t xSchedulerRunning = pdFALSE;  

3.2 uxSchedulerSuspended

/* 在调度器挂起期间,上下文切换将被挂起。此外,如果调度器已挂起,中断不得操作 TCB 的 xStateListItem,
 * 或任何可以从 xStateListItem 引用的列表。如果在中断需要挂起调度器时解除阻塞任务,则将任务的事件列表项移入 xPendingReadyList,
 * 以便调度器恢复时内核将任务从待就绪列表移入实际就绪列表。待就绪列表本身只能在临界区中访问 */
PRIVILEGED_DATA static volatile UBaseType_t uxSchedulerSuspended = ( UBaseType_t ) pdFALSE;

uxSchedulerSuspended 的作用是记录任务调度器被挂起的次数,当这个变量为 0(dFALSE)时,任务调度器不被挂起,任务切换正常执行,当这个变量大于 0 时代表任务调度器被挂起的次数。如果执行挂起任务调度器操作该变量值会增加,如果执行恢复任务调度器操作,该变量值会减一,直到它为 0 时才会真正的执行实际的调度器恢复操作,这样可以有效的提高执行效率。

3.3 xPendedTicks

PRIVILEGED_DATA static volatile TickType_t xPendedTicks = ( TickType_t ) 0U;

任务调度器在被挂起期间,系统的时间,仍然是需要增加的。挂起期间漏掉的 systick 数目便会被存储在这个变量中,以用于恢复调度器时补上漏掉的 systick。

3.4 xPendingReadyList

/* 在调度器挂起期间已就绪的任务。调度器恢复时,它们将被移到就绪列表中 */
PRIVILEGED_DATA static List_t xPendingReadyList;                         

这个链表中挂接的是在任务调度器挂起期间解除阻塞条件得到满足的阻塞任务,在任务调度器恢复工作后,这些任务会被移动到就绪链表组中,变为就绪状态。

4、任务删除相关

4.1 xTasksWaitingTermination

/* 已被删除但内存尚未释放的任务 */
PRIVILEGED_DATA static List_t xTasksWaitingTermination; 

当任务自己删除自己时,其是不能立刻自己释放自己所占用的内存等资源的,其需要将自己挂接到 xTasksWaitingTermination 这个链表下,然后让 IdleTask 来回收其所占用的资源。

4.2 uxDeletedTasksWaitingCleanUp

/* 等待IdleTask 处理的自己删除自己的任务的数目 */
PRIVILEGED_DATA static volatile UBaseType_t uxDeletedTasksWaitingCleanUp = ( UBaseType_t ) 0U;  

4.3 xIdleTaskHandle

/* 这个任务句柄指向 IdleTask(任务调度器在启动时便自动创建的空闲任务),用于回收内存等操作 */
PRIVILEGED_DATA static TaskHandle_t xIdleTaskHandle = NULL;                          

TaskHandle_t 本质上是指向任务 TCB 的指针,IdleTask 是任务调度器在启动时便自动创建的空闲任务,用于回收内存等操作,这个任务句柄指向 IdleTask。

5、系统信息相关

5.1 xTickCount

/* 存储systick 的值,用来给系统提供时间信息 */
PRIVILEGED_DATA static volatile TickType_t xTickCount = ( TickType_t ) configINITIAL_TICK_COUNT;  

5.2 xNumOfOverflows

/* 保存了xTickCount 溢出的次数 */
PRIVILEGED_DATA static volatile BaseType_t xNumOfOverflows = ( BaseType_t ) 0;  

5.3 uxTaskNumber

/* 存储当前任务的数目 */
PRIVILEGED_DATA static UBaseType_t uxTaskNumber = ( UBaseType_t ) 0U;  

每创建一个任务,这个值便会增加一次,为每个任务生成一个唯一的序号,供调试工具使用。注意与 uxCurrentNumberOfTasks 区分。

5.4 uxCurrentNumberOfTasks

/* 存储当前任务的数目 */
PRIVILEGED_DATA static volatile UBaseType_t uxCurrentNumberOfTasks = ( UBaseType_t ) 0U;

二、任务的创建和删除

1、任务的创建

FreeRTOS 提供了以下4 种任务创建函数:

  • xTaskCreateStatic():以静态内存分配的方式创建任务,也就是在编译时便要分配好 TCB 等所需要内存。
  • xTaskCreateRestrictedStatic():以静态内存分配的方式创建任务,需要 MPU。
  • xTaskCreate():以动态内存分配方式创建任务,需要提供 portMolloc() 函数的实现,在程序实际运行时分配 TCB 等所需要内存。
  • xTaskCreateRestricted():以动态内存分配方式创建任务,需要 MPU。

这里只讲 xTaskCreate(),其它函数有需要了解的请自行阅读源码。

1.1 xTaskCreate()

创建任务的时候,我们就把它添加到对应就绪链表数组下的对应优先级下的链表的结尾,当我们运行一个任务(同一优先级时)的时候,它会先从链表的最后一项开始运行(因为 pxCurrentTCB 指向它),也就是先运行 3 号任务,然后是 1 号任务,最后是 2 号任务。

BaseType_t xTaskCreate( TaskFunction_t pxTaskCode,  /* 指向任务函数的函数指针 */
                            const char * const pcName, /* 任务的名称 */
                            const configSTACK_DEPTH_TYPE usStackDepth, /* 栈的深度,这里的栈的单位不是byte 而是根据平台的位数决定的,8 位,16 位,32
位分别对应1,2,3,4byte */
                            void * const pvParameters,  /* 传入任务的参数 */
                            UBaseType_t uxPriority,  /* 任务的优先级。数值越大,任务的优先级越高 */
                            TaskHandle_t * const pxCreatedTask ) /* 创建的任务的句柄,本质就是一个指向创建任务TCB 的指针 */
    {
        TCB_t * pxNewTCB;
        BaseType_t xReturn;

        /* 如果堆栈向下增长,则先分配堆栈再分配 TCB,以防止堆栈增长到 TCB 中。
         * 如果堆栈向上增长,则先分配 TCB 再分配堆栈 */
        #if ( portSTACK_GROWTH > 0 )
            {
                /* 栈向上生长 */

                /* 为 TCB 分配空间 */
                pxNewTCB = ( TCB_t * ) pvPortMalloc( sizeof( TCB_t ) );

                if( pxNewTCB != NULL )
                {
                    /* 为栈分配空间 */
                    pxNewTCB->pxStack = ( StackType_t * ) pvPortMalloc( ( ( ( size_t ) usStackDepth ) * sizeof( StackType_t ) ) );

                    if( pxNewTCB->pxStack == NULL )
                    {
                        /* 无法分配堆栈。删除已分配的 TCB */
                        vPortFree( pxNewTCB );
                        pxNewTCB = NULL;
                    }
                }
            }
        #else /* portSTACK_GROWTH */
            {
                /* 栈向下生长 */

                StackType_t * pxStack;

                /* 为正在创建的任务分配堆栈空间 - pvPortMalloc 见 porttable/MemMang/heap_4.c */
                pxStack = pvPortMalloc( ( ( ( size_t ) usStackDepth ) * sizeof( StackType_t ) ) ); /* pvPortMalloc() 返回的所有值至少具有 MCU 堆栈所需的对齐方式,并且此分配是堆栈 */

                if( pxStack != NULL )
                {
                    /* 为 TCB 分配空间 */
                    pxNewTCB = ( TCB_t * ) pvPortMalloc( sizeof( TCB_t ) ); /* pvPortMalloc() 返回的所有值至少具有 MCU 堆栈所需的对齐方式,且 TCB_t 的第一个成员始终是指向任务堆栈的指针 */

                    if( pxNewTCB != NULL )
                    {
                        /* 将堆栈位置存储在 TCB 中 */
                        pxNewTCB->pxStack = pxStack;
                    }
                    else
                    {
                        /* 由于 TCB 未创建,堆栈无法使用。再次释放它 */
                        vPortFree( pxStack );
                    }
                }
                else
                {
                    pxNewTCB = NULL;
                }
            }
        #endif /* portSTACK_GROWTH */

        if( pxNewTCB != NULL )
        {
            #if ( tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE != 0 ) 
                {
                    /* 任务可以静态或动态创建,因此注意此任务是以动态方式创建的,以便稍后删除时参考 */
                    pxNewTCB->ucStaticallyAllocated = tskDYNAMICALLY_ALLOCATED_STACK_AND_TCB;
                }
            #endif /* tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE */

            /** 初始化新创建的任务 **/
            prvInitialiseNewTask( pxTaskCode, pcName, ( uint32_t ) usStackDepth, pvParameters, uxPriority, pxCreatedTask, pxNewTCB, NULL );
            prvAddNewTaskToReadyList( pxNewTCB ); /** 将新创建的任务添加到就绪列表 */
            xReturn = pdPASS;
        }
        else
        {
            xReturn = errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY;
        }

        return xReturn;
    }

代码内容很简单,大致做了这几件事:

  • 初始化栈 - pvPortMalloc、pxNewTCB->pxStack = pxStack;
  • 为任务分配内存空间、填充 TCB 结构体 - pvPortMalloc、prvInitialiseNewTask
  • 将 TCB 加入到就绪列表中,并根据优先级进行任务切换 - prvAddNewTaskToReadyList

1.2 prvInitialiseNewTask

在 xTaskCreate 函数中调用了 prvInitialiseNewTask 函数来填充 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,  /* TCB 地址 */
                                  const MemoryRegion_t * const xRegions ) /* MPU 相关暂时不讨论 */
{
    StackType_t * pxTopOfStack;
    UBaseType_t x;

    /* 如果不需要,避免依赖 memset() */
    #if ( tskSET_NEW_STACKS_TO_KNOWN_VALUE == 1 )
        {
            /* 用已知值填充堆栈以协助调试 */
            ( void ) memset( pxNewTCB->pxStack, ( int ) tskSTACK_FILL_BYTE, ( size_t ) ulStackDepth * sizeof( StackType_t ) );
        }
    #endif /* tskSET_NEW_STACKS_TO_KNOWN_VALUE */

    /* 计算堆栈顶部地址。这取决于堆栈是从高内存向低内存增长(如 80x86)还是相反。
     * portSTACK_GROWTH 用于根据端口的需要使结果为正或负 */
        {
            /* 栈向下生长 */

            pxTopOfStack = &( pxNewTCB->pxStack[ ulStackDepth - ( uint32_t ) 1 ] );
            pxTopOfStack = ( StackType_t * ) ( ( ( portPOINTER_SIZE_TYPE ) pxTopOfStack ) & ( ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) ) );  // 调用入口按 8 字节对齐

            /* 检查计算出的堆栈顶部对齐是否正确 */
            configASSERT( ( ( ( portPOINTER_SIZE_TYPE ) pxTopOfStack & ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) == 0UL ) );
        }

    /* 将任务名称存入 TCB */
    if( pcName != NULL )
    {
        for( x = ( UBaseType_t ) 0; x < ( UBaseType_t ) configMAX_TASK_NAME_LEN; x++ )
        {
            pxNewTCB->pcTaskName[ x ] = pcName[ x ];

            /* 如果字符串短于 configMAX_TASK_NAME_LEN 个字符,则不要复制所有 configMAX_TASK_NAME_LEN,
             * 以防字符串后的内存不可访问(极其不可能) */
            if( pcName[ x ] == ( char ) 0x00 )
            {
                break;
            }
            else
            {
                mtCOVERAGE_TEST_MARKER();
            }
        }

        /* 确保在字符串长度大于或等于 configMAX_TASK_NAME_LEN 的情况下,名称字符串以空字符终止 */
        pxNewTCB->pcTaskName[ configMAX_TASK_NAME_LEN - 1 ] = '\0';
    }
    else
    {
        /* 任务未给定名称,因此确保在读取时有一个空字符终止符 */
        pxNewTCB->pcTaskName[ 0 ] = 0x00;
    }

    /* 这用作数组索引,因此必须确保它不过大。首先移除特权位(如果存在) */
    if( uxPriority >= ( UBaseType_t ) configMAX_PRIORITIES )
    {
        uxPriority = ( UBaseType_t ) configMAX_PRIORITIES - ( UBaseType_t ) 1U;
    }
    else
    {
        mtCOVERAGE_TEST_MARKER();
    }

    pxNewTCB->uxPriority = uxPriority;
    #if ( configUSE_MUTEXES == 1 )
        {
            pxNewTCB->uxBasePriority = uxPriority;
            pxNewTCB->uxMutexesHeld = 0;
        }
    #endif /* configUSE_MUTEXES */

    /** 初始化列表项 - 任务状态列表项和事件列表项 */
    vListInitialiseItem( &( pxNewTCB->xStateListItem ) );
    vListInitialiseItem( &( pxNewTCB->xEventListItem ) );

    /* 将 pxNewTCB 设置为从 ListItem_t 返回的链接。这样我们就可以从列表中的通用项返回到包含的 TCB */
    listSET_LIST_ITEM_OWNER( &( pxNewTCB->xStateListItem ), pxNewTCB );

    /* 事件列表始终按优先级顺序排列 */
    listSET_LIST_ITEM_VALUE( &( pxNewTCB->xEventListItem ), ( TickType_t ) configMAX_PRIORITIES - ( TickType_t ) uxPriority ); 
    listSET_LIST_ITEM_OWNER( &( pxNewTCB->xEventListItem ), pxNewTCB );
    
    #if ( configUSE_TASK_NOTIFICATIONS == 1 )
        {
            memset( ( void * ) &( pxNewTCB->ulNotifiedValue[ 0 ] ), 0x00, sizeof( pxNewTCB->ulNotifiedValue ) );
            memset( ( void * ) &( pxNewTCB->ucNotifyState[ 0 ] ), 0x00, sizeof( pxNewTCB->ucNotifyState ) );
        }
    #endif

	pxNewTCB->pxTopOfStack = pxPortInitialiseStack( pxTopOfStack, pxTaskCode, pvParameters );

    if( pxCreatedTask != NULL )
    {
        /* 以匿名方式传递句柄。该句柄可用于更改已创建任务的优先级、删除已创建的任务等 */
        *pxCreatedTask = ( TaskHandle_t ) pxNewTCB;
    }
    else
    {
        mtCOVERAGE_TEST_MARKER();
    }
}

执行过程大致如下:

  • 将栈值设定为特定值,以用于栈最高使用大小检测等功能
    • ( void ) memset( pxNewTCB->pxStack, ( int ) tskSTACK_FILL_BYTE, ( size_t ) ulStackDepth * sizeof( StackType_t ) );
  • 计算栈顶指针、栈底指针
    • pxTopOfStack = &( pxNewTCB->pxStack[ ulStackDepth - ( uint32_t ) 1 ] );
    • pxTopOfStack = ( StackType_t * ) ( ( ( portPOINTER_SIZE_TYPE ) pxTopOfStack ) & ( ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) ) );
  • 复制任务名、写入优先级等相关 TCB 结构体成员赋初值
    • pxNewTCB->pcTaskName[ x ] = pcName[ x ];
  • 初始化链表项
  • 对栈进行初始化
    • pxPortInitialiseStack

pxPortInitialiseStack 函数会按处理器规则填充任务私有栈的值,将任务的私有栈“伪装”成已经被调度过一次的样子。

StackType_t * pxPortInitialiseStack( StackType_t * pxTopOfStack,
                                     TaskFunction_t pxCode,
                                     void * pvParameters )
{
    /* 模拟上下文切换中断创建的堆栈帧 */

    /* 这里空出一个存储地址是为了符合MCU 进出中断的方式 */
    pxTopOfStack--;

    /* 栈中寄存器 xPSR 被初始为 0x01000000 ,其中 bit24 被置 1,表示使用 Thumb 指令 */
    *pxTopOfStack = portINITIAL_XPSR;                                    /* xPSR */
    pxTopOfStack--;
    /* 将任务函数地址压入栈中程序 PC(R15),当该第一次切换任务时,
     * 硬件的 PC 指针将指向该函数,也就是会从头执行这个任务 */
    *pxTopOfStack = ( ( StackType_t ) pxCode ) & portSTART_ADDRESS_MASK; /* PC */ /* 保证地址对齐 */
    pxTopOfStack--;
    /* 正常任务是死循环,不会使用 LR 进行返回,这里赋为错误处理函数地址,出错时会进入该函数 */
    *pxTopOfStack = ( StackType_t ) prvTaskExitError;                    /* LR */

    /* 跳过 R12 ,R3 ,R2,R1 不用初始化,节省代码空间 */
    pxTopOfStack -= 5;                            /* R12, R3, R2 and R1. */
    *pxTopOfStack = ( StackType_t ) pvParameters; /* R0 */

    /* 使用一种要求每个任务维护自己的 exec 返回值的保存方法 */
    pxTopOfStack--;
    *pxTopOfStack = portINITIAL_EXC_RETURN;

    pxTopOfStack -= 8; /* R11, R10, R9, R8, R7, R6, R5 and R4. */

    return pxTopOfStack;
}

注意看,这里初始化栈的时候,把 LR 的值设为了 prvTaskExitError:

static void prvTaskExitError( void )
{
    /* 实现任务的函数不能退出或尝试返回给调用者,因为没有东西可以返回。
     * 如果任务想要退出,它应该调用 vTaskDelete(NULL)。如果定义了 configASSERT(),
     * 则人为地触发一个 assert(),然后在此处停止,以便应用程序编写者可以捕获错误。
     */
    configASSERT( uxCriticalNesting == ~0UL );
    portDISABLE_INTERRUPTS();  // 进入临界区,禁止中断

    /* 发生错误,进入死循环,会一直停在这里 */
    for( ; ; )
    {
    }
}

也就是一个死循环。所以说,如果我们自己写一个任务处理函数的时候,如果不是死循环的话(且没有经过特殊的处理),最终就会执行到这里,所有的任务都无法再执行,也就是你之前遇到死机的可能的原因之一。

当我们想让任务退出的时候,必须要杀死这个任务,这就会用到下面会将到的 vTaskDelete()。

1.3 prvAddNewTaskToReadyList

在 xTaskCreate 函数中,紧接着调用了 prvAddNewTaskToReadyList 来使任务处于就绪态和任务切换:

static void prvAddNewTaskToReadyList( TCB_t * pxNewTCB )
{
    /* 确保在更新任务列表时,中断不会访问任务列表 */
    taskENTER_CRITICAL();
    {
        uxCurrentNumberOfTasks++; /* 全局变量 - 记录当前任务数 */

        if( pxCurrentTCB == NULL )
        {
            /* 没有其他任务,或者所有其他任务都处于挂起状态 - 将此任务设为当前任务 */
            pxCurrentTCB = pxNewTCB;

            /* 全局变量 - 当前任务数为 1 */
            if( uxCurrentNumberOfTasks == ( UBaseType_t ) 1 )
            {
                /* 这是创建的第一个任务,因此需要进行初步初始化。如果此调用失败,我们将无法恢复,但我们会报告失败 */
                prvInitialiseTaskLists();
            }
            else
            {
                mtCOVERAGE_TEST_MARKER();
            }
        }
        else
        {
            /* 如果调度器尚未运行,那么如果此任务是迄今为止创建的优先级最高的任务,则将该任务设置为当前任务 */
            if( xSchedulerRunning == pdFALSE )
            {
                if( pxCurrentTCB->uxPriority <= pxNewTCB->uxPriority )
                {
                    pxCurrentTCB = pxNewTCB;
                }
                else
                {
                    mtCOVERAGE_TEST_MARKER();
                }
            }
            else
            {
                mtCOVERAGE_TEST_MARKER();
            }
        }

        uxTaskNumber++;

        #if ( configUSE_TRACE_FACILITY == 1 )
            {
                /* 在 TCB 中添加一个计数器,仅用于跟踪 */
                pxNewTCB->uxTCBNumber = uxTaskNumber;
            }
        #endif /* configUSE_TRACE_FACILITY */
        traceTASK_CREATE( pxNewTCB );

        /* 添加到就绪列表中 */
        prvAddTaskToReadyList( pxNewTCB );

        portSETUP_TCB( pxNewTCB );
    }
    taskEXIT_CRITICAL();

    if( xSchedulerRunning != pdFALSE )
    {
        /* 如果创建的任务优先级高于当前任务,则它应该立即运行 */
        if( pxCurrentTCB->uxPriority < pxNewTCB->uxPriority )
        {
            /* 见 port.c vPortGenerateSimulatedInterrupt(),
             * 产生一个模拟中断,以便调度器运行 */
            taskYIELD_IF_USING_PREEMPTION();
        }
        else
        {
            mtCOVERAGE_TEST_MARKER();
        }
    }
    else
    {
        mtCOVERAGE_TEST_MARKER();
    }
}

该函数大致做了如下工作:

  • 记录当前任务数量
    • uxCurrentNumberOfTasks++;
  • 将任务添加到就绪链表中
    • prvAddTaskToReadyList( pxNewTCB );

将任务插入就绪链表中时采用的宏 prvAddTaskToReadyList() 相关代码如下:

/**
 * 将由 pxTCB 表示的任务放置到相应的就绪列表中。它被插入到列表的末尾
 * 按优先级放到对应的链表下
 */
#define prvAddTaskToReadyList( pxTCB )                                                                 \
    traceMOVED_TASK_TO_READY_STATE( pxTCB );                                                           \
    taskRECORD_READY_PRIORITY( ( pxTCB )->uxPriority );                                                \
    vListInsertEnd( &( pxReadyTasksLists[ ( pxTCB )->uxPriority ] ), &( ( pxTCB )->xStateListItem ) ); \
    tracePOST_MOVED_TASK_TO_READY_STATE( pxTCB )


/* 提供了一个 port 优化的版本。调用端口定义的宏,记录最高先级的就绪任务的优先级 */
#define taskRECORD_READY_PRIORITY( uxPriority )    portRECORD_READY_PRIORITY( uxPriority, uxTopReadyPriority )

#define portRECORD_READY_PRIORITY( uxPriority, uxReadyPriorities ) ( uxReadyPriorities ) |= ( 1UL << ( uxPriority ) )

首先通过 taskRECORD_READY_PRIORITY( ( pxTCB )->uxPriority ); 来获取最高优先级的就绪任务的优先级,然后调用 vListInsertEnd( &( pxReadyTasksLists[ ( pxTCB )->uxPriority ] ), &( ( pxTCB )->xStateListItem ) );,根据优先级,将任务放在对应优先级的就绪列表项后面。

  • 根据新加入的优先级判断是否需要进行一次任务切换
    • taskYIELD_IF_USING_PREEMPTION();

该函数本质上就是 port.c 文件中的 vPortGenerateSimulatedInterrupt() 函数,该函数通过产生一个模拟中断来让调度器进行一次任务切换,

至此,xTaskCreate() 的执行过程就结束了,一个任务就此创建好了。

2、任务删除

2.1 vTaskDelete

我们通过调用 vTaskDelete() 函数来删除一个任务,该函数有两个使用场景:

  1. 任务自己删除自己(传入参数为 NULL)
  2. 当前任务删除其它任务(传入任务句柄)
void vTaskDelete( TaskHandle_t xTaskToDelete )
    {
        TCB_t * pxTCB;

        /** 在临界区中操作 */
        taskENTER_CRITICAL();
        {
            /* 获取 TCB,如果为 NULL 则返回当前任务句柄;否则保持不变 */
            pxTCB = prvGetTCBFromHandle( xTaskToDelete );

            /** 将任务从就绪/延迟列表中移除 */
            if( uxListRemove( &( pxTCB->xStateListItem ) ) == ( UBaseType_t ) 0 )
            {
                /** 重新设置最高优先级 */
                taskRESET_READY_PRIORITY( pxTCB->uxPriority );
            }
            else
            {
                mtCOVERAGE_TEST_MARKER();
            }

            /* 任务是否在等待事件 */
            if( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) != NULL )
            {
                ( void ) uxListRemove( &( pxTCB->xEventListItem ) );
            }
            else
            {
                mtCOVERAGE_TEST_MARKER();
            }

            /* 同时增加 uxTaskNumber,以便内核感知的调试器可以检测到任务列表需要重新生成。
             * 这是在 portPRE_TASK_DELETE_HOOK() 之前完成的,因为在 Windows 端口上,该宏不会返回。 */
            uxTaskNumber++;

            if( pxTCB == pxCurrentTCB )
            {
                /* 一个任务正在删除自己。这不能在任务内部完成,因为需要切换到另一个任务。
                 * 将任务放入终止列表中。空闲任务将检查终止列表,并释放调度器为已删除任务的 TCB 和堆栈分配的任何内存。 */
                vListInsertEnd( &xTasksWaitingTermination, &( pxTCB->xStateListItem ) );

                /* 增加 `ucTasksDeleted` 变量,以便空闲任务知道有一个已删除的任务,
                 * 因此应该检查 `xTasksWaitingTermination` 列表。 */
                ++uxDeletedTasksWaitingCleanUp;

                /* 在调用 `portPRE_TASK_DELETE_HOOK()` 之前调用删除钩子,
                 * 因为在 Win32 端口上,`portPRE_TASK_DELETE_HOOK()` 不会返回。 */
                traceTASK_DELETE( pxTCB );

                /* 预删除钩子主要用于 Windows 模拟器,在该模拟器中会执行特定的 Windows 清理操作,
                 * 之后无法从这个任务中让出执行权 - 因此使用 `xYieldPending` 来标记需要进行上下文切换。
                 * 关闭当前正在运行的线程 */
                portPRE_TASK_DELETE_HOOK( pxTCB, &xYieldPending );
            }
            else
            {
                --uxCurrentNumberOfTasks;
                traceTASK_DELETE( pxTCB );

                /* 重置下一个预期的解除阻塞时间,以防它指的是刚刚被删除的任务 */
                prvResetNextTaskUnblockTime();
            }
        }
        taskEXIT_CRITICAL();

        /* 如果任务不是在删除自己,那么在临界区之外调用 `prvDeleteTCB`。如果任务在删除自己,
         * 那么 `prvDeleteTCB` 是从 `prvCheckTasksWaitingTermination` 调用的,
         * 而 `prvCheckTasksWaitingTermination` 又是从空闲任务调用的 */
        if( pxTCB != pxCurrentTCB )
        {
            prvDeleteTCB( pxTCB );
        }

        /* 如果刚刚被删除的任务是当前正在运行的任务,则强制进行重新调度 */
        if( xSchedulerRunning != pdFALSE )
        {
            if( pxTCB == pxCurrentTCB )
            {
                configASSERT( uxSchedulerSuspended == 0 );
                portYIELD_WITHIN_API();  /* 生成一个模拟中断,以便调度器运行 */
            }
            else
            {
                mtCOVERAGE_TEST_MARKER();
            }
        }
    }

我们首先通过如下宏来判断传入的是 NULL 还是任务句柄:

#define prvGetTCBFromHandle(pxHandle) ( ( ( pxHandle ) == NULL ) ? pxCurrentTCB : ( pxHandle ) )

当我们用 vTaskDelete() 来删除其它任务时,所需要进行的工作步骤如下:

  1. 将待删除任务从相关的状态链表中删除,设置相关参数,保证被删除的任务不会再次获得处理器使用权。
    • uxListRemove( &( pxTCB->xStateListItem ) )
  2. 将待删除任务从其相关的事件链表中删除,设置相关参数,保证被删除的任务不会再次获得处理器使用权。
    • uxListRemove( &( pxTCB->xEventListItem ) )
  3. 更改当前任务数目。
    • --uxCurrentNumberOfTasks;
  4. 直接释放内存空间。
    • prvDeleteTCB( pxTCB );
static void prvDeleteTCB( TCB_t * pxTCB )
    {
        /* 这个调用是专门为 TriCore 端口所需的。它必须在 `vPortFree()` 调用之上。
         * 这个调用也被那些希望静态分配和清理 RAM 的端口/演示程序使用。 */
        portCLEAN_UP_TCB( pxTCB );

        /* 任务只能被动态分配- 释放堆栈和 TCB
         * 见 portable/MemMang/heap_4.c */
        vPortFree( pxTCB->pxStack );
        vPortFree( pxTCB );
    }

portCLEAN_UP_TCB 本质上就是 port.c 中的函数 vPortDeleteThread:

void vPortDeleteThread( void *pvTaskToDelete )
{
ThreadState_t *pxThreadState;
uint32_t ulErrorCode;

	/* 消除编译器警告 */
	( void ) ulErrorCode;

	/* 获取线程状态 */
	pxThreadState = ( ThreadState_t * ) ( *( size_t *) pvTaskToDelete );

	/* 检查所指定的 pxThreadState 的 pvThread 句柄是否有效。如果无效,说明线程可能已被关闭 */
	if( pxThreadState->pvThread != NULL )
	{
		/* 等待获取 pvInterruptEventMutex 互斥量,以确保在进行线程删除时,其他可能的线程不会干扰 */
		WaitForSingleObject( pvInterruptEventMutex, INFINITE );

		/* 强制终止指定的线程,并检查返回值是否有误 */
		ulErrorCode = TerminateThread( pxThreadState->pvThread, 0 );
		configASSERT( ulErrorCode );

		/* 关闭 pxThreadState->pvThread 句柄,释放相应资源 */
		ulErrorCode = CloseHandle( pxThreadState->pvThread );
		configASSERT( ulErrorCode );

		/* 释放互斥量 */
		ReleaseMutex( pvInterruptEventMutex );
	}
}
  1. 重新计算下个任务解除阻塞的时间。
    • prvResetNextTaskUnblockTime();
static void prvResetNextTaskUnblockTime( void )
{
    if( listLIST_IS_EMPTY( pxDelayedTaskList ) != pdFALSE )
    {
        /* 新的当前延迟列表为空。将 `xNextTaskUnblockTime` 设置为最大可能值,
         * 以便在延迟列表中有项目之前,`if( xTickCount >= xNextTaskUnblockTime )` 测试极不可能通过。 */
        xNextTaskUnblockTime = portMAX_DELAY;
    }
    else
    {
        /* 新的当前延迟列表不为空,获取延迟列表头部项目的值。这是延迟列表头部任务应从阻塞状态中移除的时间 */
        xNextTaskUnblockTime = listGET_ITEM_VALUE_OF_HEAD_ENTRY( pxDelayedTaskList );
    }
}

// list.h
#define listGET_ITEM_VALUE_OF_HEAD_ENTRY( pxList )        ( ( ( pxList )->xListEnd ).pxNext->xItemValue )

FreeRTOS 系统中所有的阻塞都是由将任务按解除阻塞时间升序挂接到延时任务链表 pxDelayedTaskList 中实现的,因此 prvResetNextTaskUnblockTime() 实际上只是读取 pxDelayedTaskList 下的第一个任务解除阻塞的时间,将其赋值给 xNextTaskUnblockTime 而已,如果 pxDelayedTaskList 为空,那么 xNextTaskUnblockTime 将会被赋值为 portMAX_DELAY。

当任务是自己删除自己时,上述步骤的第4 步将有所变化。当前任务仍在运行中,此时直接释放其占用的内存可能导致运行错误,因此需要等待其退出运行状态时才能安全的对其占用的内存进行释放。此时上述的步骤 4 替换为以下两步:

  • 将待删除任务挂接到待终止任务链表 xTasksWaitingTermination 中
    • vListInsertEnd( &xTasksWaitingTermination, &( pxTCB->xStateListItem ) );
  • 增加删除待清理任务数 uxDeletedTasksWaitingCleanUp
    • ++uxDeletedTasksWaitingCleanUp;

在前面讲创建任务的时候,提到会创建一个空闲任务,空闲任务就会来释放掉这个任务所申请的内存(TCB、栈等),相当于 Linux 下的 init 守护进程。但是空闲任务的优先级是 0,如果就绪列表一直不为空,那空闲任务该如何得到执行?那就是用 vTaskDelay,它会把任务从就绪链表移动到延迟列表,让出 CPU 资源,这样空闲任务就可以得到执行。

void vTaskDelay( const TickType_t xTicksToDelay )
{
    BaseType_t xAlreadyYielded = pdFALSE;

    /* 大于 0 说明需要进行延迟 */
    if( xTicksToDelay > ( TickType_t ) 0U )
    {
        configASSERT( uxSchedulerSuspended == 0 );
        vTaskSuspendAll();  /* 任务 */
        {
            traceTASK_DELAY();

            /* 在调度器暂停时从事件列表中移除的任务,在调度器恢复之前不会进入就绪列表或从阻塞列表中移除。
             * 由于这是当前正在执行的任务,因此它不能存在于事件列表中。
             */
            prvAddCurrentTaskToDelayedList( xTicksToDelay, pdFALSE ); /* 将任务添加到延迟列表 */
        }
        xAlreadyYielded = xTaskResumeAll();  /* 恢复任务 */
    }
    else
    {
        mtCOVERAGE_TEST_MARKER();
    }

    /* Force a reschedule if xTaskResumeAll has not already done so, we may
     * have put ourselves to sleep. */
    if( xAlreadyYielded == pdFALSE )
    {
        portYIELD_WITHIN_API();
    }
    else
    {
        mtCOVERAGE_TEST_MARKER();
    }
}

三、任务切换

任务切换的目的是保证当前具有最高优先级的就绪任务获得处理器的使用权。在进行任务切换时,首先要找到具有最高优先级的就绪任务,如果该任务不是当前正在运行的任务,需要先保存当前运行任务的堆栈,并将具有最高优先级的就绪任务堆栈恢复到处理器的堆栈中进行运行。

1、vTaskSwitchContext

通过 vTaskSwitchContext 可以实现任务上下文切换:

删去了不必要的宏

void vTaskSwitchContext( void )
{
    /* 检查调度器是否被挂起 */
    if( uxSchedulerSuspended != ( UBaseType_t ) pdFALSE )
    {
        /* 调度器已经被挂起,不允许上下文切换 */
        xYieldPending = pdTRUE;
    }
    else
    {
        /* 调度器未被挂起,允许上下文切换 */
        xYieldPending = pdFALSE;
        traceTASK_SWITCHED_OUT();

        /* 进行堆栈溢出检查,确保当前任务没有溢出 */
        taskCHECK_FOR_STACK_OVERFLOW();

        /* 调用函数选择下一个要运行的任务,依据任务的优先级进行调度 */
        taskSELECT_HIGHEST_PRIORITY_TASK();
        traceTASK_SWITCHED_IN();
    }
}

1.1 taskSELECT_HIGHEST_PRIORITY_TASK

调用 taskSELECT_HIGHEST_PRIORITY_TASK 可以根据当前就绪列表中任务的最高优先级 uxTopReadyPriority 获得要运行任务:

	/* 优化后版本 - 寻找拥有最高优先级的就绪任务 
     * 这里不在使用数值大小来表示最高优先级,而是使用每一位表示是否有该优先级的任务处于就
     * 绪态,对于cortex -m3有 32 位,如 0000 0000 0000 0000 0000 0000 0000 0001 表示第0级有就绪态的任务 */
    #define taskSELECT_HIGHEST_PRIORITY_TASK()                                                  \
    {                                                                                           \
        UBaseType_t uxTopPriority;                                                              \
                                                                                                \
        /* 查找包含就绪任务队列中的优先级最高的任务 */                                           \
        portGET_HIGHEST_PRIORITY( uxTopPriority, uxTopReadyPriority );                          \
        configASSERT( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ uxTopPriority ] ) ) > 0 ); \
        listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) );   \
    } /* taskSELECT_HIGHEST_PRIORITY_TASK() */

其中出现的宏定义如下:

/* 获取在就绪优先级位图中最高的优先级 
 * bsr(Bit Scan Reverse,位扫描反向)指令,目的是查找 uxReadyPriorities 中最高有效位(即最高优先级)*/
#define portGET_HIGHEST_PRIORITY( uxTopPriority, uxReadyPriorities )	\
	__asm volatile(	"bsr %1, %0\n\t" 									\
					:"=r"(uxTopPriority) : "rm"(uxReadyPriorities) : "cc" )

#define listCURRENT_LIST_LENGTH( pxList )                 ( ( pxList )->uxNumberOfItems )

#define listGET_OWNER_OF_NEXT_ENTRY( pxTCB, pxList )                                           \
    {                                                                                          \
        List_t * const pxConstList = ( pxList );                                               \
        /* Increment the index to the next item and return the item, ensuring */               \
        /* we don't return the marker used at the end of the list.  */                         \
        ( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext;                           \
        if( ( void * ) ( pxConstList )->pxIndex == ( void * ) &( ( pxConstList )->xListEnd ) ) \
        {                                                                                      \
            ( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext;                       \
        }                                                                                      \
        ( pxTCB ) = ( pxConstList )->pxIndex->pvOwner;                                         \
    }

2、进入任务切换的方式

FreeRTOS 进入任务切换的方式有以下两种

  1. 在 xPortSysTickHandler() 中断中进入,也就是在系统 Systick 增加时,根据情况进入任务切换。
  2. 手动调用 portYIELD_WITHIN_API() 或 taskYIELD_IF_USING_PREEMPTION()(在启用抢占模式的情况下其和 portYIELD_WITHIN_API 一样,非抢占模式下,其没有任何作用)直接进行一次任务切换。

2.1 xPortSysTickHandler

xPortSysTickHandler 其实就是 SysTick_Handler,在 FreeRTOSConfig.h 文件中有:

#define vPortSVCHandler SVC_Handler
#define xPortPendSVHandler PendSV_Handler
#define xPortSysTickHandler SysTick_Handler

在 portable/RVDS/ARM_CM4F/port.c 实现了这个函数:

void xPortSysTickHandler( void )
{
    /* SysTick运行在最低的中断优先级,因此当这个中断执行时,
     * 所有中断都必须被取消屏蔽。因此,不需要保存然后恢复中断掩码值,
     * 因为其值已经已知 - 因此使用稍微快一些的 vPortRaiseBASEPRI()函数来
     * 代替 portSET_INTERRUPT_MASK_FROM_ISR()
     */
    vPortRaiseBASEPRI();
    {
        /* 增加滴答数
         * 这里并不是每次进入系统滴答中断都会进行上下文切换,只有有任务从阻塞状态退出
		 * 或者在时间片轮询模式中有相同的优先级的任务,才会进行上下文切换 */
        if( xTaskIncrementTick() != pdFALSE )
        {
            /* 需要进行任务切换。此时,代码将 PendSV 中断设置为待处理,
             * 这样在中断结束后,系统会进行上下文切换 */
            portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
        }
    }

    /* 清除在处理 SysTick 中断时设置的优先级 */
    vPortClearBASEPRIFromISR();
}

xTaskIncrementTick() 函数的主要功能是在在任务调度器工作时修改 Systick 的值,并根据 Systick 值的变化判断是否需要进行一次任务切换动作;在任务调度器被挂起时,其会记录任务调度器挂起期间漏掉的 Systick 数,一旦任务调度器恢复运行,任务调度器会补上漏掉的 Systick 和相应的任务切换动作在任务调度器工作时,当以下两种情况发生时,xTaskIncrementTick() 将返回 pdTRUE,以触发一次 PendSV 中断,以进行任务切换动作:

  1. 当前时刻有任务需要退出阻塞状态
  2. 启用时间片模式,当前优先级下有多个任务,需要共享使用权。

2.2 portYIELD_WITHIN_API

这个 API 在前面讲 vTaskCreate 和 vTaskDelete 的时候已经见过了(可能名称不一样,因为又用 #define 封装了几次),这里列出源码(port.c):

void vPortGenerateSimulatedInterrupt( uint32_t ulInterruptNumber )
{
ThreadState_t *pxThreadState = ( ThreadState_t *) *( ( size_t * ) pxCurrentTCB );  // 获取当前任务的线程状态

	configASSERT( xPortRunning );  // 确保调度器在运行

	/* 判断要生成的中断编号是否在最大中断数范围内,并保证互斥量不为空 */
	if( ( ulInterruptNumber < portMAX_INTERRUPTS ) && ( pvInterruptEventMutex != NULL ) )
	{
		WaitForSingleObject( pvInterruptEventMutex, INFINITE );  /* 等待获取互斥锁 */
		/**************************************************************************************/
		ulPendingInterrupts |= ( 1 << ulInterruptNumber );  /* 设置挂起的中断 */

		/* 模拟的中断现在处于挂起状态,但如果此调用处于临界区中,则不要立即处理它。
		 * 由于等待互斥锁的调用是累积的,因此有可能处于临界区中。
		 * 如果在临界区中,那么当临界区嵌套计数减少到零时,事件将被设置 */
		if( ulCriticalNesting == portNO_CRITICAL_NESTING )
		{
			SetEvent( pvInterruptEvent );

			/* 准备等待一个事件 - 确保事件尚未被信号通知 */
			ResetEvent( pxThreadState->pvYieldEvent );
		}
		/**************************************************************************************/
		ReleaseMutex( pvInterruptEventMutex );  /* 释放互斥锁 */
		if( ulCriticalNesting == portNO_CRITICAL_NESTING )
		{
			/* 有一个中断被挂起,所以确保阻塞以允许它执行。
			 * 在大多数情况下,(模拟的) 中断将在到达下一行之前已经执行
			 * 所以这只是为了确保万无一失 */
			WaitForSingleObject( pxThreadState->pvYieldEvent, INFINITE );
		}
	}
}

2.3 xPortPendSVHandler

前面也看到了,当触发 PendSV 中断的时候,就会调用 xPortPendSVHandler,也就是 PendSV_Handler,下面是它的实现(port.c),通过它我们就可以清楚任务是如何进行上下文切换的。

__asm void xPortPendSVHandler( void )
{
    extern uxCriticalNesting;
    extern pxCurrentTCB;
    extern vTaskSwitchContext;

    /* DMB
       数据存储器隔离。DMB 指令保证仅当所有在它前面的存储器访问操作
       都执行完毕后,才提交(commit)在它后面的存储器访问操作。

       DSB
       数据同步隔离。比 DMB 严格:仅当所有在它前面的存储器访问操作都执行完毕后,
       才执行在它后面的指令(亦即任何指令都要等待存储器访问操作)

       ISB
       指令同步隔离。最严格:它会清洗流水线,以保证所有它前面的指令都执
       行完毕之后,才执行它后面的指令。
    */
    
    /* step1 保存当前任务现场*/
    /* =================================================================*/

/* *INDENT-OFF* */
    PRESERVE8  /* 字节对齐 */

    /* PendSV 中断产生时,硬件自动将xPSR ,PC(R15),LR(R14),R12 ,R3-R0 使用 PSP 压入任务
     * 堆栈中,进入中断后硬件会强制使用MSP 指针,此时LR(R14)的值将会被自动被更新为
     * 特殊的 EXC_RETURN */
    mrs r0, psp   /* 保存进程堆栈指针到R0 */
    isb
    /* Get the location of the current TCB. */
    ldr r3, =pxCurrentTCB    /* 读取当前TCB 块的地址到R3 */
    ldr r2, [ r3 ]           /* 将当前任务栈顶地址放到 R2 中,这也是为什么强调栈顶指针一定得是 TCB 块的第一个成员的原因 */

    /* 不用管 */
    /* Is the task using the FPU context?  If so, push high vfp registers. */
    tst r14, #0x10
    it eq
    vstmdbeq r0!, {s16-s31}

    /* 将 R4 到 R11 通用寄存器的值压入栈保存 */
    stmdb r0!, {r4-r11, r14}  

    /* 将 R0 的值写入以 R2 为地址的内存中,也就是保存当前的栈顶地址到 TCB 的第一个成员,也就是栈顶指针 */
    str r0, [ r2 ]  

    /* 将 R3,R14 临时压栈,这里的 SP 其实使用的是 MSP ,这里进行压栈保护的原因是 bl 指令会自动更改 R14 值用于返回 */
    stmdb sp!, {r0, r3}

    /* 屏蔽 configMAX_SYSCALL_INTERRUPT_PRIORITY 以下优先级的中断 */
    mov r0, # configMAX_SYSCALL_INTERRUPT_PRIORITY
    msr basepri, r0

    /* step2 恢复待切换任务的现场*/
    /* =================================================================*/

    dsb
    isb
    bl vTaskSwitchContext  /* 这里调用 vTaskSwitchContext 函数来获取下一个要执行任务控制块 */

    /* 取消中断屏蔽 */
    mov r0, # 0  
    msr basepri, r0

    /* 将 R0、R3 出栈,这里 R3 相当于是 pxCurrentTCB 内存的值,所以此时 R3 值已经更新为下一个要执行的任务 TCB 地址了 */
    ldmia sp!, {r0, r3}

    /* The first item in pxCurrentTCB is the task top of stack. */
    ldr r1, [ r3 ]
    ldr r0, [ r1 ]  /* 把新任务的栈顶指针放到R0里 */

    /* Pop the core registers. */
    ldmia r0!, {r4-r11, r14}  /* 将新任务的 R4-R11、R14 出栈 */

    /* Is the task using the FPU context?  If so, pop the high vfp registers
     * too. */
    tst r14, # 0x10
    it eq
    vldmiaeq r0!, {s16-s31}

    /* step3 更改PSP 指针值*/
    /* =================================================================*/

    msr psp, r0  /* 将新的栈顶地址放入到进程堆栈指针PSP */
    isb
    #ifdef WORKAROUND_PMU_CM001 /* XMC4000 specific errata */
        #if WORKAROUND_PMU_CM001 == 1
            push { r14 }
            pop { pc }
            nop
        #endif
    #endif

    /* 异常发生时,R14 中保存异常返回标志,包括返回后进入线程模式还是处理器模
     * 式、使用 PSP 堆栈指针还是 MSP 堆栈指针,当调用 bx r14 指令后,硬件会知道要从异常返
     * 回,然后出栈,这个时候堆栈指针 PSP 已经指向了新任务堆栈的正确位置,当新任务的运
     * 行地址被出栈到 PC 寄存器后,新的任务也会被执行 */
    bx r14
/* *INDENT-ON* */
}

注意,在任务中使用的是 PSP,而处理器复位后默认使用的是 MSP 指针。这是因为任务调度器在启动时会调用 prvStartFirstTask() 函数,这个函数也是一段汇编代码,它的主要工作就是复位 MSP,开中断和异常,并且触发一次 SVC 中断,进行第一次任务的加载,其内容如下:

__asm void prvStartFirstTask( void )
{
/* *INDENT-OFF* */
    PRESERVE8

    /* 使用NVIC偏移寄存器来定位堆栈 */
    ldr r0, =0xE000ED08  /* 向量表偏移量寄存器的起始地址存储着 MSP 的初始值 */
    ldr r0, [ r0 ]
    ldr r0, [ r0 ]
    /* 将主堆栈指针(msp)设置回堆栈的起始位置 */
    msr msp, r0  /* 复位MSP */

    /* 清除指示 FPU 正在使用的位,以防在调度器启动之前使用了FPU——
     * 否则会导致在SVC堆栈中为FPU寄存器的延迟保存不必要地留下空间 */
    mov r0, #0
    msr control, r0
    /* 使能全局中断和异常 */
    cpsie i
    cpsie f
    dsb
    isb
    /* 触发 SVC 中断来启动第一个任务 */
    svc 0
    nop
    nop
/* *INDENT-ON* */
}

SVC 异常服务函数里的代码如下:

__asm void vPortSVCHandler( void )
{
/* *INDENT-OFF* */
    PRESERVE8

    /* 获取当前TCB的位置 */
    ldr r3, = pxCurrentTCB
    ldr r1, [ r3 ]
    ldr r0, [ r1 ]
    /* Pop the core registers. */
    ldmia r0 !, {r4-r11,r14}
    msr psp, r0
    isb
    mov r0, # 0
    msr basepri, r0
    bx r14
/* *INDENT-ON* */
}

在最后一步, SVC 异常服务函数修改了 r14 的值,正是修改该值使得处理器在退出中断后运行任务函数时进入线程模式并使用 PSP 栈指针。

四、任务调度器

1、启动

1.1 vTaskStartScheduler()

FreeRTOS 中任务调度器的启动由 vTaskStartScheduler() 函数实现,此函数被调用后,OS 将接手处理器的管理权,它主要有以下几个步骤:

  • 创建空闲任务、定时器任务。
  • 初始化下一次解除阻塞时间,系统 tick 初始值,运行状态等变量。
  • 调用 xPortStartScheduler() 函数启动调度器
void vTaskStartScheduler( void )
{
    BaseType_t xReturn;

    /* 1. 添加空闲任务(设置为最低优先级) */
    /******************************************************/
        {
            /* 空闲任务由正在使用动态分配的RAM创建 */
            xReturn = xTaskCreate( prvIdleTask,
                                   configIDLE_TASK_NAME,
                                   configMINIMAL_STACK_SIZE,
                                   ( void * ) NULL,
                                   portPRIVILEGE_BIT,  /* 实际上是 ( tskIDLE_PRIORITY | portPRIVILEGE_BIT ),但 tskIDLE_PRIORITY 为零 */
                                   &xIdleTaskHandle ); /* MISRA 异常,这是合理的,因为它不是对所有支持的编译器都冗余的显式转换 */
        }

    /* 2. 添加定时器任务 */
    /******************************************************/

    #if ( configUSE_TIMERS == 1 )
        {
            if( xReturn == pdPASS )
            {
                /* 创建定时器任务 */
                xReturn = xTimerCreateTimerTask();
            }
            else
            {
                mtCOVERAGE_TEST_MARKER();
            }
        }
    #endif /* configUSE_TIMERS */

    /* 3. 启动调度器 */
    /******************************************************/

    if( xReturn == pdPASS )
    {
        /* 只有在定义了用户可定义的宏 FREERTOS_TASKS_C_ADDITIONS_INIT 时,
         * 才应调用 freertos_tasks_c_additions_init(),因为这是该函数唯一调用的宏 */
        #ifdef FREERTOS_TASKS_C_ADDITIONS_INIT
            {
                freertos_tasks_c_additions_init();
            }
        #endif

        /* 在此处关闭中断,以确保在调用 xPortStartScheduler() 之前或期间不会发生滴答。
         * 已创建任务的堆栈包含一个中断已开启的状态字,因此当第一个任务开始运行时,中断将自动重新启用 */
        portDISABLE_INTERRUPTS();

        xNextTaskUnblockTime = portMAX_DELAY; /* 初始化下一次解除阻塞时间,因为当前任务是首次使用没有要延迟的任务了,所以设为最大 portMAX_DELAY */
        xSchedulerRunning = pdTRUE;  /* 设置标志任务调度器已启动 */
        xTickCount = ( TickType_t ) configINITIAL_TICK_COUNT;  /* 初始化系统 tick 初始值 */

        /* 如果定义了 configGENERATE_RUN_TIME_STATS,则必须定义以下宏以配置用于生成运行
         * 时间计数器时间基准的定时器/计数器。注意:如果 configGENERATE_RUN_TIME_STATS 设置为 0 并且以下行无法构建,
         * 请确保在您的 FreeRTOSConfig.h 文件中没有定义 portCONFIGURE_TIMER_FOR_RUN_TIME_STATS()。 */
        portCONFIGURE_TIMER_FOR_RUN_TIME_STATS();

        traceTASK_SWITCHED_IN();

        /* 设置定时器滴答是硬件特定的,因此位于可移植接口中 */
        if( xPortStartScheduler() != pdFALSE )
        {
            /* 如果调度器正在运行,该函数将不会返回,因此不应到达此处 */
        }
        else
        {
            /* 只有当任务调用 xTaskEndScheduler() 时才会到达此处 */
        }
    }
    else
    {
        /* 只有当内核无法启动时才会到达此行,因为没有足够的堆内存来创建空闲任务或定时器任务 */
        configASSERT( xReturn != errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY );
    }

    ( void ) xIdleTaskHandle;

    ( void ) uxTopUsedPriority;
}

1.2 xPortStartScheduler

这个函数是与平台相关的,根据 arm-cm3 的移植文件来看(在目录 portable/RVDS/ARM_CM4F 下),它主要的工作是设置上下文切换中断和Systick 中断,启动定时器为系统提供 Systick,最终调用 prvStartFirstTask() (前面已经介绍过)来启动第一个任务。

BaseType_t xPortStartScheduler( void )
{
    /* configMAX_SYSCALL_INTERRUPT_PRIORITY 不能为 0
     * See https://www.FreeRTOS.org/RTOS-Cortex-M3-M4.html */
    configASSERT( configMAX_SYSCALL_INTERRUPT_PRIORITY );

    /* This port can be used on all revisions of the Cortex-M7 core other than
     * the r0p1 parts.  r0p1 parts should use the port from the
     * /source/portable/GCC/ARM_CM7/r0p1 directory. */
    configASSERT( portCPUID != portCORTEX_M7_r0p1_ID );
    configASSERT( portCPUID != portCORTEX_M7_r0p0_ID );

    /* 1. 中断配置 */
    /*************************************************************/

    #if ( configASSERT_DEFINED == 1 )
        {
            volatile uint32_t ulOriginalPriority;  /* 存储原始的中断优先级 */
            /* 指向第一个用户中断优先级寄存器的指针 */
            volatile uint8_t * const pucFirstUserPriorityRegister = ( uint8_t * ) ( portNVIC_IP_REGISTERS_OFFSET_16 + portFIRST_USER_INTERRUPT_NUMBER );
            volatile uint8_t ucMaxPriorityValue;  /* 存储最大优先级值 */

            /* 读取并保存当前的中断优先级寄存器值 */
            ulOriginalPriority = *pucFirstUserPriorityRegister;

            /* 将寄存器设置为最大8位值(0xFF) */
            *pucFirstUserPriorityRegister = portMAX_8_BIT_VALUE;

            /* 读取返回的值以查看有多少位保持不变 */
            ucMaxPriorityValue = *pucFirstUserPriorityRegister;

            /* 确保内核中断优先级设置为最低优先级 */
            configASSERT( ucMaxPriorityValue == ( configKERNEL_INTERRUPT_PRIORITY & ucMaxPriorityValue ) );

            /* 计算可用于系统调用的最大优先级 */
            ucMaxSysCallPriority = configMAX_SYSCALL_INTERRUPT_PRIORITY & ucMaxPriorityValue;

            /* 初始化最大优先级组值  */
            ulMaxPRIGROUPValue = portMAX_PRIGROUP_BITS;

            /* 确定最大优先级的位数 */
            while( ( ucMaxPriorityValue & portTOP_BIT_OF_BYTE ) == portTOP_BIT_OF_BYTE )
            {
                ulMaxPRIGROUPValue--;
                ucMaxPriorityValue <<= ( uint8_t ) 0x01;
            }

            #ifdef __NVIC_PRIO_BITS
                {
                    /* Check the CMSIS configuration that defines the number of
                     * priority bits matches the number of priority bits actually queried
                     * from the hardware. */
                    configASSERT( ( portMAX_PRIGROUP_BITS - ulMaxPRIGROUPValue ) == __NVIC_PRIO_BITS );
                }
            #endif

            #ifdef configPRIO_BITS
                {
                    /* 检查定义优先级位数的 FreeRTOS 配置是否与从硬件实际查询的优先级位数相匹配 */
                    configASSERT( ( portMAX_PRIGROUP_BITS - ulMaxPRIGROUPValue ) == configPRIO_BITS );
                }
            #endif

            /* 将优先级组值移回其在 AIRCR 寄存器内的位置  */
            ulMaxPRIGROUPValue <<= portPRIGROUP_SHIFT;
            ulMaxPRIGROUPValue &= portPRIORITY_GROUP_MASK;

            /* 将中断优先级寄存器恢复到原始值 */
            *pucFirstUserPriorityRegister = ulOriginalPriority;
        }
    #endif /* conifgASSERT_DEFINED */

    /* 2. PendSV 和 SysTick 中断优先级配置 */
    /*************************************************************/

    /* 将 PendSV 和 SysTick 设置为最低优先级的中断 */
    portNVIC_SHPR3_REG |= portNVIC_PENDSV_PRI;
    portNVIC_SHPR3_REG |= portNVIC_SYSTICK_PRI;

    /* 3. 定时器中断配置 */
    /*************************************************************/

    /* 启动生成 tick ISR 的定时器。这里已经禁用了中断 */
    vPortSetupTimerInterrupt();

    /* 初始化临界区嵌套计数,为第一个任务做准备 */
    uxCriticalNesting = 0;

    /* 确保启用VFP - 无论如何都应该启用 */
    prvEnableVFP();

    /* 总是使用延迟保存 */
    *( portFPCCR ) |= portASPEN_AND_LSPEN_BITS;

    /* 4. 启动第一个任务 */
    /*************************************************************/

    /* 开始第一个任务 */
    prvStartFirstTask();

    /* 不应该执行到这里 */
    return 0;
}

2、结束

2.1 vTaskEndScheduler

任务调度器的关闭由 vTaskEndScheduler() 函数实现,此函数调用后 OS 将停止工作。它的实现就非常简单了,只有三行:

void vTaskEndScheduler( void )
{
    /* 停止调度器中断并调用可移植调度器结束例程,以便在需要时可以恢复原始的 ISR。
     * port 层必须确保中断使能位保持在正确的状态 */
    
    portDISABLE_INTERRUPTS();    /* 关闭中断 */
    xSchedulerRunning = pdFALSE; /* 设置标志表示任务调度器已停止 */
    vPortEndScheduler();         /* 停止调度器 */
}
FreeRtos内核源码分析(一)——CPU内核部分 基于STM32F103ZE单片机+keil5编译器分析FreeRtos启动及运行过程,主要涉及CM3的内核知识、FreeRtos启动及上下文切换。 阅读详情

相关推荐

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

任务管理(或称进程管理)是所有操作系统内核的最基本组成模块之一,FreeRTOS也不例外。想要了解一个操作系统,不得不理解其任务管理的设计和实现。任务管理的介绍由两篇文章组成,第一篇先介绍了FreeRTOS的任务管理的重要概念和外部特性以及相关联的重要实现,第二篇介绍任务管理实现的细节(关键数据结构和内部函数的实现)。温馨提示由于文章较长,可当作工具文使用,即仅挑选感兴趣的部分阅读;为了解释FreeRTOS系统调用的行为,文中难免会涉及一些操作系统原理、ARM体系结构相关的概念,请读者自行查阅资料。

Duran.L的专栏 4948

手把手教你FreeRTOS源码解析(一)——内存管理

FreeRTOS中一共有5种内存分配的方法,分别在文件heap_1.c,heap_2.c,heap_3.c,heap_4.c,heap_5.c种。虽然标准C库中的 malloc()和 free()也可以实现动态内存管理,但是它有以下缺陷:1、在小型嵌入式系统种效率不高。2、线程不安全。3、具有不确定性,每次执行的时间不同。4、会导致内存碎片。

weixin_46461874的博客 9057

STM32——FreeRTOS - 移植

本文介绍了FreeRTOS操作系统的学习要点,主要包括:1)如何获取FreeRTOS V10.4.6源码;2)关键源码文件说明(include、portable等文件夹);3)FreeRTOS移植目的;4)系统配置文件解析。文章为STM32开发者提供了FreeRTOS学习的总体框架,包含源码下载地址和核心文件介绍,适合嵌入式系统开发者参考。

PinnsiR的博客 128

可能是最全的FreeRTOS源码分析及应用开发系列

可能是最全的FreeRTOS源码分析及应用开发系列 FreeRTOS 是一个可裁剪的小型且免费的 RTOS 系统,尺寸非常小,可运行于微控制器上。其特点包括: – 内核支持抢占式,合作式和时间片调度。 – 提供了一个用于低功耗的 Tickless 模式。 – 系统的组件在创建时可以选择动态或者静态的 RAM。 – FreeRTOS-MPU 支持 Corex-M 系列中的 MPU 单元,如 STM32F429。 – FreeRTOS 系统简单、小巧、易用,通常情况下内核占用 4k-9k 字节的空间。

kunkliu的博客 6696

FreeRTOS解析:TCB_t结构体及重要变量说明(Task-1)

FreeRTOS解析:Task - 1.变量及TCB_t结构体说明 受博客限制,如果您想获得更好的阅读体验,请前往https://github.com/Nrusher/FreeRTOS-Book或者https://gitee.com/nrush/FreeRTOS-Book下载PDF版本阅读,如果您觉得本文不错也可前往star,以示对作者的鼓励。如发现问题欢迎交流。PDF阅读效果展示见:FreeR...

Nrush的博客 1万+

FreeRTOS解析:任务的删除(TASK-2)

任务的删除 受博客限制,如果您想获得更好的阅读体验,请前往https://github.com/Nrusher/FreeRTOS-Book或者https://gitee.com/nrush/FreeRTOS-Book下载PDF版本阅读,如果您觉得本文不错也可前往star,以示对作者的鼓励。如发现问题欢迎交流。 相关博客: FreeRTOS解析:List FreeRTOS解析:TCB_t结构体...

Nrush的博客 1468

FreeRTOS任务管理核心:Task.h头文件源码解析

嵌入式系统开发中,任务调度与资源管理是工程师绕不开的核心难题。FreeRTOS 作为广泛使用的实时操作系统,其任务控制块(TCB)承载着任务状态、优先级和栈信息,而 task.h 正是直面这些机制的公开接口。它通过不透明句柄封装内部结构,既保障数据安全,也向上层API提供稳定的任务创建、删除、延时与同步入口。深入理解任务状态机、链表节点以及临界区保护原理,有助于把握多任务并发和中断管理的本质,在实际工程中快速定位任务卡死、栈溢出或优先级反转问题。本文将沿着 task.h 的类型定义与函数声明,结合嵌入式开发

weixin_33552698的博客 236

FreeRTOS源码解析 -> xTaskCreate()

signed portBASE_TYPE xTaskGenericCreate( pdTASK_CODE pxTaskCode, const signed char * const pcName, unsigned short usStackDepth, void *pvParameters, unsigned portBASE_TYPE uxPriority, xTaskHandle *pxCr

MrJang_的专栏 6057

精读FreeRTOS内核task.h:任务管理、类型定义与源码解析

实时操作系统的任务调度机制是嵌入式开发的核心基础,任务的创建、切换、挂起与恢复都围绕任务控制块展开。深入理解任务状态、优先级、堆栈深度等基础概念,能帮助开发者从根本上掌握RTOS的运行原理,避免因任务配置不当导致的死机、栈溢出等隐蔽问题。在实际工程中,无论是排查调度异常,还是进行内核级性能优化,都需要从任务管理的顶层设计入手。FreeRTOS作为应用最广泛的嵌入式RTOS之一,其任务管理模块的实现极具代表性,而task.h正是揭开这套机制的关键入口。通过精读task.h中的类型定义、状态枚举、API原型与条

weixin_29169899的博客 312

FreeRTOS移植(micronblaze)

1. 实验目的 把FreeRTOS操作系统移植到Xilinx microblaze板子上,运行一个最基本的Task 2. FreeRTOS源码下载 Freertos官网:http://www.freertos.org/ Freertos官网右边栏,点击下载,即为最新版。 下载双击exe,解压后下图中就是源码了, 3. FreeRTOS目录架构 ├─FreeRT...

小白变大白之路 1954

FreeRTOS V10.4.6源码结构与STM32移植全指南

FreeRTOS是一种轻量级实时操作系统内核,广泛应用于STM32等ARM Cortex-M系列微控制器。其核心原理基于抢占式调度、链表管理就绪/延时任务队列,并通过可裁剪的移植层(portable)实现硬件抽象。技术价值在于提供确定性响应、资源隔离与多任务协同能力,同时支持动态内存分配(heap_4)、互斥量、软件定时器等关键机制。典型应用场景包括工业控制、传感器数据采集与多外设协同系统。本文深入解析FreeRTOS V10.4.6源码组织、portable层适配逻辑及STM32平台工程集成要点,覆盖从源

weixin_42515842的博客 124

FreeRTOS学习笔记二【源码文件介绍及命名规范】

Free RTOS学习笔记二【源码文件介绍及命名规范】源码文件介绍源码中的命名规范两个重要数据类型变量名函数名宏名 源码文件介绍 Free RTOS源码中包含了内核、内核配置、各个厂家的微处理器的移植、多种内存管理方案、大量的demo。 下面从使用的角度介绍源码中的文件。 核心共用代码 Free RTOS中实现最基本的任务调度功能只需要两个文件,及task.c和list.c,task用于任务的创...

qq_25370227的博客 1433

因果推断(二):因果推断涉及的ITE、ATE、CATE等效应

因果效应也称为**处理效应(Treatment effect)**是用于评判干预与否的结果差异的指标(处理变量对结果变量的影响)。比如在接受治疗组中,如果这些人不接受治疗,这组人接受治疗和不接受治疗的效应差异是多少。定义为实验组中每个个体的处理效应差异,比如一个个体在接收治疗和不接受治疗的两个处理方式的健康程度得分的差。但是一个人因为不可能。比如我们想探究接受治疗的男性用户和不接受治疗的男性用户的平均效果差异就是CATE。前面说ITE是无法观测的,是因为一个人不能同时接受治疗又不接受治疗,这里关键的就是。

Y1n的博客 3710

1297_FreeRTOS中的idle task实现分析

全部学习汇总: GitHub - GreyZhang/g_FreeRTOS: learning notes about FreeRTOS.这一次来看一下idle task的实现。在很多RTOS中都有idle task,作用估计也是差不多。操作系统在启动的时候必须要至少有一个任务存在,而为此做保障的就是idle task。而idle task通常也会将自己的保障兵的角色发挥到极致,很多不重要的信息都是通过idle task的钩子函数来实现。这里顺便把这个宏定义的信息一起放在这里了,而这个宏的展开效果其实就是定

小灰笔记 1244

STM32实战:5分钟搞定CanFestival协议栈下的CANopen心跳报文配置(附避坑指南)

本文详细介绍了如何在STM32平台上使用CanFestival协议栈快速配置CANopen心跳报文,包括硬件准备、协议栈移植、心跳报文配置及常见问题解决方案。通过实战步骤和避坑指南,帮助开发者高效实现节点状态监控,提升工业控制系统的可靠性。

win55的博客 744

手把手教你在全新服务器内网安装OpenEuler 22.03 LTS SP4+ 安装 openGuass 5.0.0(LTS) 数据库

openEuler是面向数字基础设施的操作系统,支持服务器、云计算、边缘计算、嵌入式等应用场景,支持多样性计算,致力于提供安全、稳定、易用的开源服务器Linux操作系统。

cgqyw的专栏 3736

FreeRTOS-任务通知源码分析

任务通知可用来代替信号量、消息队列、事件标志位,而且使用任务通知的形式效率会更高,它不需要像信号量那样创建队列和操作队列,任务通知的存储变量来自任务控制块中,当宏 configUSE_TASK_NOTIFICATIONS 被定义为1时就可以使用任务通知相关的函数了。任务通知的接口函数同样存在发送和接收,发送的接口函数如下: /* 发送通知,有通知值但不保留接收任务原通知值 */ xTaskNoti...

itdo_just的博客 742

解决银河麒麟V10分辨率问题:xrandr命令详解与常见错误排查

本文详细解析了在银河麒麟V10系统中使用xrandr命令调整屏幕分辨率的方法,包括核心功能解析、常见问题排查及持久化配置技巧。通过实战指南帮助用户解决分辨率缺失、配置失效等典型问题,提升多显示器环境下的工作效率。

weixin_30642561的博客 501

FreeRTOS任务挂起与恢复:从API到源码,附STM32实例

在嵌入式实时操作系统(RTOS)中,任务调度是系统稳定运行的基石,而任务状态管理则是理解调度的核心。任务通常包含就绪、运行、阻塞与挂起等状态,其中挂起与阻塞极易混淆。阻塞是任务等待事件或延时,由内核自动恢复;挂起则是被外部显式冻结,必须通过特定API解除。FreeRTOS作为广泛应用的轻量级RTOS,其任务挂起与恢复机制基于双向链表操作,涉及vTaskSuspend、vTaskResume及中断安全的xTaskResumeFromISR等接口。掌握这些API的正确使用场景,能有效避免任务死锁、恢复失灵等问题

weixin_33461476的博客 165

Ansys Zemax导入CAD文件避坑指南:STL/STEP/IGES格式选择与性能优化

本文深度解析了在Ansys Zemax中导入CAD文件的核心要点与性能优化策略。针对STL、STEP、IGES等主流格式,详细对比了其几何表示、精度与适用场景差异,并提供了基于模型特征的格式选择决策树。文章重点分享了体元、弦公差等高级参数的调优技巧,以及处理复杂装配体和避免精度陷阱的实战指南,旨在帮助光学工程师提升仿真效率与结果可靠性。

weixin_29202687的博客 436

手把手教你解决uniapp中esbuild版本冲突问题(Mac版专属指南)

本文是针对Mac平台uniapp开发者的专属指南,详细解析了运行项目时常见的`Cannot start service`错误,即esbuild的Host版本与binary版本不匹配问题。文章从错误根源入手,提供了从快速应急处理到彻底根治的完整解决方案,包括手动替换二进制文件、锁定依赖版本、清理缓存及优化工具链配置,旨在帮助开发者高效解决版本冲突,恢复并稳定开发环境。

i0j1k2l3m的博客 458

从零构建FreeRTOS工程:任务调度、中断向量表与配置全流程解析

在嵌入式开发中,从裸机程序切换到实时操作系统(RTOS)是一项关键能力跃迁。RTOS的核心价值在于通过任务调度机制,将复杂功能模块拆分为独立任务,由内核按优先级与时间片统一管理CPU资源,从而解决裸机主循环中多模块相互抢占的痛点。要搭建一个可稳定运行的FreeRTOS工程,不仅需要理解任务模型与调度器原理,还需正确配置SysTick、PendSV等异常向量,并精心设计FreeRTOSConfig.h中的堆大小、时钟频率等参数。本文面向准备从裸机开发转入系统开发的工程师,系统讲解最小工程目录结构、源码选型与链

weixin_30225755的博客 173

容器化部署指南——Docker/K8s环境下的OpenClaw实践(附Dockerfile)

通过本文的详细指导,你已经掌握了OpenClaw在Docker和Kubernetes环境中的容器化部署方法。从基础镜像构建到高级部署策略,从开发环境到生产环境,你现在已经具备了完整的容器化部署能力。 容器化技术为OpenClaw提供了灵活、可扩展、可移植的部署方案,能够满足不同规模和需求的应用场景。通过合理使用Docker和Kubernetes,你可以实现高效的部署管理和运维自动化。 在实际应用中,建议根据具体的业务需求选择合适的部署策略,并结合监控、日志、安全等最佳实践,确保系统的稳定性和可靠性。同时

博客 492

FreeRTOS源码分析二:task启动(RISCV架构)

FreeRTOS源码分析一:task启动(RISCV架构)

qq_45226456的博客 1230

【软考】数据库系统工程师备考指南(一)

目录一、数据库系统工程师简介二、考试要求三、考试目标四、考试范围(一)信息系统知识(二)数据库系统设计与管理 一、数据库系统工程师简介 数据库系统工程师考试属于全国计算机技术与软件专业技术资格(水平)考试(简称计算机软件水平资格考试)中的一个中级考试。考试不设学历与资历条件,也不论年龄和专业,考生可根据自己的技术水平选择合适的级别合适的资格,但一次考试只能报考一种资格。考试采用笔试形式,考试实行全国统一大纲、统一试题、统一时间、统一标准、统一证书的考试办法。笔试安排在一天之内。 2022 年数据库系统工程

邓邓子的博客 3859

Data-Structures-and-Algorithm-Analysis-in-Java.pdf

作者: Weiss, Mark A. 出版社: Addison Wesley Data Structures and Algorithm Analysis in Java is an “advanced algorithms” book that fits between traditional CS2 and Algorithms Analysis courses. In the old ACM Curriculum Guidelines, this course was known as CS7. This text is for readers who want to learn good programming and algorithm analysis skills simultaneously so that they can develop such programs with the maximum amo...

FreeRTOS 的源码结构

结构体。

shanstellar的博客 886

假人有限元模型

假人模型,车辆碰撞假人有限元模型,适合做乘员安全的研究者利用

上一篇: BLE 协议之 GATT
Projectsauron
博客等级 码龄4年 2587粉丝 100原创
评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值