【FreeRTOS源码学习笔记】任务创建

【FreeRTOS源码学习笔记】任务创建

  • 前言
  • 一、创建任务
    • [1.1 定义任务栈](#1.1 定义任务栈)
    • [1.2 定义任务函数](#1.2 定义任务函数)
    • [1.3 定义任务控制块](#1.3 定义任务控制块)
    • [1.4 实现任务函数创建](#1.4 实现任务函数创建)
  • 二、实现就绪列表
    • [2.1 定义就绪列表](#2.1 定义就绪列表)
    • [2.2 就绪列表初始化](#2.2 就绪列表初始化)
    • [2.3 将任务插入就绪列表](#2.3 将任务插入就绪列表)
  • 三、关键设计要点与易错点

前言

在裸机系统中,系统的主体就是 main() 函数里面顺序执行的无限循环,在这个无限循环中,CPU 按照顺序完成各种操作。在多任务系统中,我们根据功能的不同,把整个系统分割成一个个独立的且无法返回的函数,这种函数我们称为任务。


一、创建任务

1.1 定义任务栈

先回想一下,在一个裸机系统中,如果有全局变量,有子函数调用,有中断发生,那么系统在运行时,全局变量放在哪里?子函数调用时,局部变量放在哪里?中断发生时,函数返回地址放在哪里?在裸机系统中,它们统统放在栈中。栈是单片机 RAM 中一段连续的内存空间,栈的大小一般在启动文件或者链接脚本中指定,最后由 C 库函数 __main 进行初始化。

但是,在多任务系统中,每个任务都是独立的、互不干扰的,所以要为每个任务都分配独立的栈空间,这个栈空间通常是一个预先定义好的全局数组,也可以是动态分配的一段内存空间,但它们都存在于 RAM 中。

本章我们要实现两个变量按照一定的频率轮流翻转,每个变量对应一个任务,那么就需要定义两个任务栈。在多任务系统中,有多少个任务就需要定义多少个任务栈。

c 复制代码
1 #define TASK1_STACK_SIZE                         128                
2 StackType_t Task1Stack[TASK1_STACK_SIZE];                      
3 
4 #define TASK2_STACK_SIZE                         128
5 StackType_t Task2Stack[TASK2_STACK_SIZE];
  • 任务栈其实就是一个预先定义好的全局数据,数据类型为 StackType_t,大小由 TASK1_STACK_SIZE 这个宏来定义,默认为 128,单位为字,即 512 字节,这也是 FreeRTOS 推荐的最小的任务栈。

1.2 定义任务函数

任务是一个独立的函数,函数主体无限循环且不能返回。本章我们在 main.c 中定义的两个任务

c 复制代码
1 /* 软件延时 */
2 void delay (uint32_t count)
3 {
4     for (; count!=0; count--);
5 }
6 /* 任务 1 */
7 void Task1_Entry( void *p_arg )                                   
8 {
9     for ( ; ; )
10    {
11        flag1 = 1;
12        delay( 100 );
13        flag1 = 0;
14        delay( 100 );
15    }
16 }
17 
18 /* 任务 2 */
19 void Task2_Entry( void *p_arg )                                   
20 {
21    for ( ; ; )
22    {
23        flag2 = 1;
24        delay( 100 );
25        flag2 = 0;
26        delay( 100 );
27    }
28 }

1.3 定义任务控制块

在裸机系统中,程序的主体是 CPU 按照顺序执行的,CPU 从复位向量开始,一条指令接一条指令地往下跑,谁先谁后完全由代码的书写顺序决定。而在多任务系统中,任务的执行是由系统调度的:多个任务在宏观上"同时"运行,实际上 CPU 在任意时刻只执行其中一个任务,由调度器在任务之间来回切换。既然要切换,系统就必须知道"当前该运行谁""每个任务运行到了哪里""每个任务还剩多少栈空间"等信息,否则切换回来时就无法恢复现场。

为了解决这个问题,FreeRTOS 为每个任务都额外定义了一个任务控制块(Task Control Block,简称 TCB)。这个任务控制块相当于任务的身份证,里面存有任务的所有信息,比如任务的栈顶指针、任务名称、任务的形参、任务的优先级、任务的状态等。有了这个任务控制块之后,以后系统对任务的全部操作都可以通过这个任务控制块来实现------创建任务时填充它,调度任务时读取它,删除任务时释放它。

换句话说,任务栈负责保存任务运行时的"现场"(局部变量、返回地址、寄存器内容),而任务控制块负责保存任务的"身份与状态"(我是谁、我在哪个链表里、我的栈在哪里)。两者配合,才构成一个完整的、可被调度器管理的任务。

定义一个任务控制块需要一个新的数据类型,FreeRTOS 中把它命名为 tskTCB,其精简后的定义如下:

c 复制代码
1 typedef struct tskTaskControlBlock
2 {
3     volatile StackType_t  *pxTopOfStack;       /* 栈顶 */                       (1)
4 
5     ListItem_t xStateListItem;                 /* 任务节点 */                   (2)
6 
7     StackType_t *pxStack;                      /* 任务栈起始地址 */             (3)
8     /* 任务名称,为字符串形式 */
9 
10    char pcTaskName[ configMAX_TASK_NAME_LEN ];                               (4)
11 } tskTCB;
12 typedef tskTCB TCB_t;                                                         (5)

(1):栈顶指针,作为 TCB 的第一个成员。

(2):任务节点,这是一个内置在 TCB 控制块中的链表节点,通过这个节点,可以将任务控制块挂接到各种链表中。这个节点类似晾衣架上的钩子,TCB 就是衣服。

(3):任务栈起始地址。

(4):任务名称,为字符串形式,长度由宏 configMAX_TASK_NAME_LEN 来控制,该宏在 FreeRTOSConfig.h 中定义,默认为 16。

(5):数据类型重定义。

定义任务控制块

c 复制代码
1 /* 定义任务控制块 */
2 TCB_t Task1TCB;
3 TCB_t Task2TCB;

任务节点作用 :任务状态链表节点。相当于一个中间桥梁,当节点挂在那个列表中,任务就相当于在那个列表中,系统调度器执行到该任务就通过这个节点找到这个任务TCB,再找到TCB中存有的任务栈顶指针,进行"恢复现场",从而执行该任务。

为什么不直接存 TCB 地址?

你可能会想,让链表直接存TCB 地址不就完了,干嘛非要存 节点地址再倒推回去?

在 FreeRTOS 的完整 TCB 定义中,其实不止一个 ListItem_t。 xStateListItem(状态节点),通常还有一个 xEventListItem(事件节点)后面会讲到。

同一个任务,可能会同时出现在多个不同的链表里(这是 RTOS 的精髓):

任务 A 正在运行,它在就绪链表里(通过 xStateListItem 挂着)。

同时,任务 A 在等待一个信号量,它又在信号量等待链表里(通过 xEventListItem 挂着)。

如果链表存的是 TCB 整体,那同一个 TCB 根本没法同时挂载到两个不同的链表结构里。而TCB里可以有多个节点去挂在不同的链表结构里。所以每个链表挂载的必须是独立的、轻量级的"节点",然后用"倒推(偏移量计算)"的方式找到本体。

系统怎么通过节点找到任务栈顶指针的?

链表节点是怎么反向找到包含它的父结构体(TCB)的呢?

这就用到了 RTOS 中非常经典的设计:container_of 机制(通过偏移量计算)。

在内存中,tskTCB 结构体的成员是顺序排列的。xStateListItem 在 TCB 内部占据固定的字节偏移量。

当调度器从链表中取出一个节点(比如得到了 xStateListItem 的地址),系统只需要用这个节点的地址 减去 该节点在 TCB 结构体中的偏移量,就能瞬间算出整个 tskTCB 的首地址。

拿到 TCB 首地址后,系统就能访问里面的 pxTopOfStack(栈顶指针),从而恢复该任务的上下文(CPU寄存器等),开始执行它。

为什么找到栈顶指针就找到任务了?

在后面一章任务切换中会进行详细讲解!


1.4 实现任务函数创建

任务的栈、任务的函数实体以及任务的控制块最终需要联系起来才能由系统进行统一调度。那么这个联系的工作就由任务创建函数 xTaskCreateStatic() 来完成。

1. xTaskCreateStatic() 函数

c 复制代码
1 #if( configSUPPORT_STATIC_ALLOCATION == 1 )                                  (1)
2 
3 TaskHandle_t xTaskCreateStatic(TaskFunction_t pxTaskCode,                   (2)
4                                const char * const pcName,                    (3)
5                                const uint32_t ulStackDepth,                  (4)
6                                void * const pvParameters,                    (5)
7                                StackType_t * const puxStackBuffer,           (6)
8                                TCB_t * const pxTaskBuffer )                  (7)
9 {
10    TCB_t *pxNewTCB;
11    TaskHandle_t xReturn;                                                     (8)
12
13    if ( ( pxTaskBuffer != NULL ) && ( puxStackBuffer != NULL ) )
14    {
15        pxNewTCB = ( TCB_t * ) pxTaskBuffer;
16        pxNewTCB->pxStack = ( StackType_t * ) puxStackBuffer;
17
18        /* 创建新的任务 */                                                         (9)
19        prvInitialiseNewTask( pxTaskCode,         /* 任务入口 */
20                              pcName,             /* 任务名称,字符串形式 */
21                              ulStackDepth,       /* 任务栈大小,单位为字 */
22                              pvParameters,       /* 任务形参 */
23                              &xReturn,           /* 任务句柄 */
24                              pxNewTCB);          /* 任务栈起始地址 */
25
26    }
27    else
28    {
29        xReturn = NULL;
30    }
31
32    /* 返回任务句柄,如果任务创建成功,此时 xReturn 应该指向任务控制块 */           (10)
33    return xReturn;
34 }
35
36 #endif/* configSUPPORT_STATIC_ALLOCATION */

(1):在 FreeRTOS 中,任务的创建可采用两种方法,一种是使用动态创建,另一种是使用静态创建。动态创建时,任务控制块和栈的内存是创建任务时动态分配的,任务删除时,内存可以释放。静态创建时,任务控制块和栈的内存需要事先定义好,是静态的内存,任务删除时,内存不能释放。目前我们以静态创建为例来讲解,configSUPPORT_STATIC_ALLOCATION 在 FreeRTOSConfig.h 中定义,我们配置为 1。

(2):任务入口,即任务的函数名称。TaskFunction_t 是在 projdefs.h中重定义的一个数据类型,实际就是空指针

(3):任务名称,为字符串形式,以方便调试。

(4):任务栈大小,单位为字。

(5):任务形参。

(6):任务栈起始地址。

(7):任务控制块指针。

(8):定义一个任务句柄 xReturn,用于指向任务的 TCB。任务句柄的数据类型为 TaskHandle_t,在 task.h 中定义,实际上就是一个空指针。

(10):返回任务句柄,如果任务创建成功,此时 xReturn 应该指向任务控制块,xReturn 作为形参传入 prvInitialiseNewTask() 函数。

TaskHandle_t 定义

c 复制代码
1 /* 任务句柄 */
2 typedef void * TaskHandle_t;

2. prvInitialiseNewTask() 函数

调用 prvInitialiseNewTask() 函数,创建新任务,该函数在 task.c 中实现

c 复制代码
1 static void prvInitialiseNewTask(TaskFunction_t pxTaskCode,                   (1)
2                                const char * const pcName,                     (2)
3                                const uint32_t ulStackDepth,                   (3)
4                                void * const pvParameters,                     (4)
5                                TaskHandle_t * const pxCreatedTask,            (5)
6                                TCB_t *pxNewTCB )                              (6)
7 {
8     StackType_t *pxTopOfStack;
9     UBaseType_t x;
10
11    /* 获取栈顶地址 */                                                              (7)
12    pxTopOfStack = pxNewTCB->pxStack + ( ulStackDepth - ( uint32_t ) 1 );
13    /* 向下做 8 字节对齐 */                                                          (8)
14    pxTopOfStack = ( StackType_t * ) \
15                   ( ( ( uint32_t ) pxTopOfStack ) & ( ~( ( uint32_t ) 0x0007 ) ) );
16
17    /* 将任务名存储在 TCB 中 */                                                     (9)
18    for ( x = ( UBaseType_t ) 0; x < ( UBaseType_t ) configMAX_TASK_NAME_LEN; x++ )
19    {
20        pxNewTCB->pcTaskName[ x ] = pcName[ x ];
21
22        if ( pcName[ x ] == 0x00 )
23        {
24            break;
25        }
26    }
27    /* 任务名的长度不能超过 configMAX_TASK_NAME_LEN */                               (10)
28    pxNewTCB->pcTaskName[ configMAX_TASK_NAME_LEN - 1 ] = '\0';
29
30    /* 初始化 TCB 中的 xStateListItem 节点 */                                        (11)
31    vListInitialiseItem( &( pxNewTCB->xStateListItem ) );
32    /* 设置 xStateListItem 节点的拥有者 */                                           (12)
33    listSET_LIST_ITEM_OWNER( &( pxNewTCB->xStateListItem ), pxNewTCB );
34
35
36    /* 初始化任务栈 */                                                              (13)
37    pxNewTCB->pxTopOfStack = pxPortInitialiseStack( pxTopOfStack,
38                                                    pxTaskCode,
39                                                    pvParameters );
40
41
42    /* 让任务句柄指向任务控制块 */                                                    (14)
43    if ( ( void * ) pxCreatedTask != NULL )
44    {
45        *pxCreatedTask = ( TaskHandle_t ) pxNewTCB;
46    }
47
48 }

( 1 ):任务入口。

( 2 ):任务名称,为字符串形式。

( 3 ):任务栈大小,单位为字。

( 4 ):任务形参。

( 5 ):任务句柄。

( 6 ):任务控制块指针。

( 7 ):获取栈顶地址。为什么这里要减一?可以用数组的方式来理解,数组长度为4,但是合法地址是0-3。

( 8 ):将栈顶指针向下做 8 字节对齐。在 Cortex-M3(Cortex-M4 或 Cortex-M7)内核的单片机中,因为总线宽度是 32 位的,通常只要栈保持 4 字节对齐即可,那么此处为何要为 8 字节?难道有哪些操作是 64 位的?确实有,那就是浮点运算,所以要 8 字节对齐(但是目前还没有涉及浮点运算,此处的设置只是为了让后续能够兼容浮点运行)。如果栈顶指针是 8 字节对齐的,在进行向下 8 字节对齐时,指针不会移动,如果不是 8 字节对齐的,在做向下 8 字节对齐时,就会空出几个字节,比如当 pxTopOfStack 是 33 时,明显不能被 8 整除,进行向下 8 字节对齐就是 32,那么就会空出一个字节不使用。

8字节对齐方法 :通过 & ~0x0007 的位运算,把地址的二进制的最后三位强行清零。这相当于把地址向下取整,抹掉零头,退到最近的、绝对安全的 8 的倍数上(例如 0x200B 变成 0x2008)

为什么 FreeRTOS 要向下做 8 字节对齐?

当发生中断时,CPU 硬件会自动且强制地把 8 个寄存器(R0-R3, R12, LR, PC, xPSR)一口气压入栈中(共 32 字节)。

ARM 架构的死规定:中断压栈的起始地址必须是 8 的倍数。

如果不做对齐:如果栈顶是 0x2004,硬件为了对齐,会偷偷把指针往下挪 4 字节再压栈。这会导致你的栈空间被莫名其妙"吃掉"额外字节,极易引发栈溢出(Stack Overflow)。在某些极端的双字指令下,甚至会直接触发 HardFault(硬件死机)。

( 9 ):将任务名存储在 TCB 中。

( 10 ):任务名的长度不能超过 configMAX_TASK_NAME_LEN,并以 '\0' 结尾。

( 11 ):初始化 TCB 中的 xStateListItem 节点,即初始化该节点所在的链表为空,表示节点还没有插入任何链表。

( 12 ):设置 xStateListItem 节点的拥有者,即拥有这个节点本身的 TCB。

( 13 ):调用 pxPortInitialiseStack() 函数初始化任务栈,并更新栈顶指针,任务第一次运行的环境参数就存在任务栈中。

( 14 ):让任务句柄指向任务控制块。

3. pxPortInitialiseStack() 函数

c 复制代码
1 #define portINITIAL_XPSR          ( 0x01000000 )
2 #define portSTART_ADDRESS_MASK    ( ( StackType_t ) 0xfffffffeUL )
3 
4 static void prvTaskExitError( void )
5 {
6     /* 函数停止在这里 */
7     for (;;);
8 }
9 
10 StackType_t *pxPortInitialiseStack( StackType_t *pxTopOfStack,
11                                    TaskFunction_t pxCode,
12                                    void *pvParameters )
13 {
14    /* 异常发生时,自动加载到 CPU 寄存器的内容 */                                       (1)
15    pxTopOfStack--;
16    *pxTopOfStack = portINITIAL_XPSR;                                                 (2)
17    pxTopOfStack--;
18    *pxTopOfStack = ( ( StackType_t ) pxCode ) & portSTART_ADDRESS_MASK;             (3)
19    pxTopOfStack--;
20    *pxTopOfStack = ( StackType_t ) prvTaskExitError;                                 (4)
21    pxTopOfStack -= 5;  /* r12、r3、r2 和 r1 默认初始化为 0 */
22    *pxTopOfStack = ( StackType_t ) pvParameters;                                     (5)
23 
24    /* 异常发生时,手动加载到 CPU 寄存器的内容 */                                       (6)
25    pxTopOfStack -= 8;
26 
27    /* 返回栈顶指针,此时 pxTopOfStack 指向空闲栈 */                                   (7)
28    return pxTopOfStack;
29 }

(1):压入 xPSR(状态寄存器)。

(2):【重要】 压入任务函数的入口地址(pxCode)。

意义:调度器恢复现场时,CPU 读到这个地址,就会直接跳过去执行你的任务代码。

(3):压入 prvTaskExitError。

意义:这是任务的"退路"。如果任务函数不小心写了 return(正常任务必须有死循环,不能退出),就会跳到这个死循环里报错。

(4):pxTopOfStack -= 5;

意义:一次性把 R12, R3, R2, R1 这 5 个寄存器占位并填 0。直接减 5 比写 5 次自减更高效。

(5):压入 pvParameters。

意义:这是你传进去的任务参数。在 ARM 架构里,函数调用的第一个参数默认放在 R0 寄存器。这样任务第一次启动时,就能顺理成章地拿到你给的参数。

(6):pxTopOfStack -= 8;

意义:给 R4 到 R11 这 8 个寄存器预留空间(此时栈顶指针下移 32 字节)。

(7):返回栈顶指针

这个函数做了什么?

  • 1."压入16个伪造寄存器":这16个寄存器(R0-R12, LR, PC, xPSR 等)构成了一个完整的"CPU快照"。
  • 2.当调度器第一次把这个任务从就绪链表里拿出来,把 >pxTopOfStack 指向的数据"弹(Pop)"出来,填回真正的 CPU 硬件寄存器时。
  • 3."通过寄存器去执行对应函数":因为在这16个寄存器中,代表 PC(程序计数器)的那个位>置,被提前写入了任务函数 pxCode 的地址。CPU 一恢复现场,一看 PC 指着这个函数,立马就跳过去开>始执行了。

为什么自动加载的内容写了代码,手动加载的却没有写?

图里的"自动"和"手动",指的是将来任务运行后,发生中断时,CPU保存现场的动作。而在 pxPortInitialiseStack 这个函数里,我们只是在"伪造"这个动作发生后的内存现场。

  • 自动加载区(xPSR ~ R0):未来发生中断时,ARM 硬件电路会自动把这 8 个寄存器压入栈中。所以在初始化时,这部分数据我们必须一笔一划地"伪造"好。
  • 手动加载区(R11 ~ R4):未来发生中断时,硬件不管这 8 个寄存器,是软件(FreeRTOS 的汇编代码)强行把它们压进去的。所以初始化时,这部分也需要"伪造"。

二、实现就绪列表

2.1 定义就绪列表

任务创建好之后,我们需要把任务添加到就绪列表中,表示任务已经就绪,系统随时可以调度。就绪列表在 task.c 中定义。

c 复制代码
1 /* 任务就绪列表 */
2 List_t pxReadyTasksLists[ configMAX_PRIORITIES ];

就绪列表实际上就是一个 List_t 类型的数组,数组的大小由决定最大任务优先级的宏 configMAX_PRIORITIES 确定,configMAX_PRIORITIES 在 FreeRTOSConfig.h 中默认定义为 5,最大支持 256 个优先级。数组的下标对应任务的优先级,同一优先级的任务统一插入就绪列表的同一条链表中。一个空的就绪列表如图 5-4 所示。

  • 由此可见:它是一个"数组",数组里面的每一个元素,都是一个"独立"的双向循环链表。
  • 为什么要这样设计?
    FreeRTOS 是根据任务的优先级来管理就绪任务的。
    这个数组的下标,就代表优先级。
    比如 configMAX_PRIORITIES 是 32,那这个数组就有 32 个元素。
    如果系统里有 3 个优先级为 1 的任务,这 3 个任务的 TCB 里面的状态节点,就会被依次挂载到 pxReadyTasksLists1 这个双向链表上。
    优先级为 2 的任务,就挂在 pxReadyTasksLists2 上。

2.2 就绪列表初始化

就绪列表在使用前需要先初始化,就绪列表初始化的工作在函数 prvInitialiseTaskLists() 中实现。就绪列表初始化完毕,其示意图如图 5-5 所示。

c 复制代码
1 void prvInitialiseTaskLists( void )
2 {
3     UBaseType_t uxPriority;
4 
5 
6     for ( uxPriority = ( UBaseType_t ) 0U;
7          uxPriority < ( UBaseType_t ) configMAX_PRIORITIES;
8          uxPriority++ )
9     {
10        vListInitialise( &( pxReadyTasksLists[ uxPriority ] ) );
11    }
12 }

有关链表知识点在上一章有详细讲解。

2.3 将任务插入就绪列表

任务控制块里面有一个 xStateListItem 成员,数据类型为 ListItem_t,我们将任务插入就绪列表中,就是通过将任务控制块的 xStateListItem 节点插入就绪列表中来实现的。如果把就绪列表比作晾衣架,任务比作衣服,那么 xStateListItem 就是晾衣架上面的钩子,每个任务都自带晾衣架钩子,就是为了把自己挂在各种链表中。

在本章的实验中,任务创建好之后,紧接着将任务插入就绪列表,具体实现参见下面代码。

c 复制代码
1 /* 初始化与任务相关的列表,如就绪列表 */
2 prvInitialiseTaskLists();
3 
4 Task1_Handle =                                          /* 任务句柄 */
5 xTaskCreateStatic( (TaskFunction_t)Task1_Entry,         /* 任务入口 */
6                  (char *)"Task1",                       /* 任务名称,字符串形式 */
7                  (uint32_t)TASK1_STACK_SIZE ,           /* 任务栈大小,单位为字 */
8                  (void *) NULL,                         /* 任务形参 */
9                  (StackType_t *)Task1Stack,             /* 任务栈起始地址 */
10                 (TCB_t *)&Task1TCB );                  /* 任务控制块 */
11 
12 /* 将任务添加到就绪列表 */
13 vListInsertEnd( &( pxReadyTasksLists[1] ),
14                &( ((TCB_t *)(&Task1TCB))->xStateListItem ) );
15 
16 Task2_Handle =                                          /* 任务句柄 */
17 xTaskCreateStatic( (TaskFunction_t)Task2_Entry,         /* 任务入口 */
18                  (char *)"Task2",                       /* 任务名称,字符串形式 */
19                  (uint32_t)TASK2_STACK_SIZE ,           /* 任务栈大小,单位为字 */
20                  (void *) NULL,                         /* 任务形参 */
21                  (StackType_t *)Task2Stack,             /* 任务栈起始地址 */
22                  (TCB_t *)&Task2TCB );                  /* 任务控制块 */
23 /* 将任务添加到就绪列表 */
24 vListInsertEnd( &( pxReadyTasksLists[2] ),
25                &( ((TCB_t *)(&Task2TCB))->xStateListItem ) );

就绪列表的下标对应的是任务的优先级,但是目前我们的任务还不支持优先级,有关支持多优先级的知识点后面会讲到,所以 Task1 和 Task2 任务在插入就绪列表时,可以任意选择插入的位置。在代码清单 5-15 中,我们选择将 Task1 任务插入就绪列表下标为 1 的链表中,Task2 任务插入就绪列表下标为 2 的链表中,具体示意图如图 5-6 所示。

三、关键设计要点与易错点

  1. pxTopOfStack 必须是 TCB 第一个成员。PendSV 汇编以偏移 0 直接读写它,不经过 C 成员访问。偏移变了,切换就崩。

  2. 栈帧初始化是"伪造异常返回现场"。pxPortInitialiseStack() 在任务栈上按 Cortex-M 异常栈帧格式(R0-R3、R12、LR、PC、xPSR)预置好值,让首次调度与普通恢复走同一条 PendSV 路径,不需要为"第一个任务"写特殊分支。其中 LR 指向 prvTaskExitError,任务函数意外返回时能被诊断到。

  3. 栈深度单位是"字"不是"字节"。usStackDepth=512 在 Cortex-M 上是 2KB,不是 512 字节。

  4. xPSR Thumb 位必须为 1(portINITIAL_XPSR 通常 0x01000000)。漏掉 → 任务创建成功但首次调度即 HardFault。

  5. 任务函数不得返回。返回会跳进 prvTaskExitError,Release 构建禁用断言时表现为"任务卡死、难定位"。

  6. 创建 ≠ 立即运行。调度器未启动不运行;已启动时优先级不高于当前任务也不抢跑。

  7. 静态创建对齐。栈数组需 portBYTE_ALIGNMENT(8 字节)对齐。

相关推荐
Zhao-Jichao1 小时前
AI Prompting for Everyone 课程笔记 - P06 模型提示词对比 AI Model Prompt Comparison
人工智能·笔记·prompt
Answer1st1 小时前
【FPGA学习】IP核(五)
学习
computersciencer1 小时前
贝叶斯入门:由全概率公式引出贝叶斯公式
笔记·数学建模·概率论
叼烟扛炮2 小时前
Linux 学习笔记(四十七):TCP 状态机与 tcpdump 抓包排雷实战
linux·网络·笔记·学习·tcp/ip·tcpdump
蒸蒸yyyyzwd2 小时前
从面经中学到的 5 个知识
笔记
東隅已逝,桑榆非晚2 小时前
红黑树:从概念到实现
数据结构·c++·笔记·学习
ljt27249606613 小时前
Python笔记(十三)--异常
笔记·python
动词ing3 小时前
【学习笔记】动态数码管显示
笔记·学习
陈年老古董3 小时前
ClickHouse 单机搭建记录
hive·笔记·学习·clickhouse·硬件架构