FreeRTOS基础知识

什么是 FreeRTOS?

  • 实时操作系统

FreeRTOS 特点?

  • 内核支持抢占式、合作式和时间片调度
  • 支持Corex-M系列
  • 占用存储小,内核4k-9k字节
  • 移植性能好
  • 任务与任务、任务与中断之间可以使用任务通知、消息队列、二值信号量、数值型信
    号量、递归互斥信号量和互斥信号量进行通信和同步
  • 具有优先级继承特性的互斥信号量
  • 任务数量不限、任务优先级不限

任务管理

任务函数

  • C语言实现
  • 必须返回void,并且带有一个void指针参数
  • 每个任务不允许以任何方式从函数内返回
  • 可以创建多个任务,任务之间独立,有自己的栈空间,即使是任务嵌套也是有自己的栈空间
image-20240413152524989

创建任务

  • API

    1
    2
    3
    4
    5
    6
    BaseType_t xTaskCreate(	TaskFunction_t pxTaskCode,
    const char * const pcName,
    const uint16_t usStackDepth,
    void * const pvParameters,
    UBaseType_t uxPriority,
    TaskHandle_t * const pxCreatedTask )
  • 任务注意事项

    任务嵌套时,第一个任务使用开始任务,用于管理后续的业务任务,开始任务执行后,记得删除开始任务,防止多次调用创建多个业务任务导致报错。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    //创建开始任务
    xTaskCreate((TaskFunction_t )start_task, //任务函数
    (const char* )"start_task", //任务名称
    (uint16_t )START_STK_SIZE, //任务堆栈大小
    (void* )NULL, //传递给任务函数的参数
    (UBaseType_t )1, //任务优先级
    (TaskHandle_t* )&StartTask_Handler); //任务句柄
    //开始任务任务函数
    void start_task(void *pvParameters)
    {
    // 使用相同的任务函数,创建不同的任务实例吗,执行不同的操作
    // 注意二者的优先级是一致的
    xTaskCreate((TaskFunction_t )print_task,
    (const char* )"print_task",
    (uint16_t )1000,
    (void* )pcTextForTask1,
    (UBaseType_t )2,
    NULL);

    xTaskCreate((TaskFunction_t )print_task,
    (const char* )"print_task",
    (uint16_t )1000,
    (void* )pcTextForTask2,
    (UBaseType_t )2,
    NULL);
    vTaskDelete(StartTask_Handler); //删除开始任务
    }
    //通用打印字符任务函数,根据传入参数选择打印内容
    void print_task(void *pvParameters)
    {
    char *pcTaskName;
    pcTaskName = (char *)pvParameters;
    while(1)
    {
    printf("%s", pcTaskName);
    delay_us(1);
    }
    }
  • 任务优先级

    低优先级号表示任务的优先级低,优先级号 0 表示最低优先级。有效的优先级号范围从 0 到(configMAX_PRIORITES – 1)。如果被选中的优先级上具有不止一个任务,调度器会让这些任务轮流执行。

  • 任务状态

    调度器总是选择具有最高优先级的可运行任务来执行,如果只有运行态和非运行态两种状态,可能会导致低优先级的任务不会被执行(被饿死)。因此引入事件驱动,一个事件驱动任务只会在事件发生后触发工作(处理),而在事件没有发生时是不能进入运行态的,调度器总是选择所有能够进入运行态的任务中具有最高优先级的任务一个高优先级但
    不能够运行的任务意味着不会被调度器选中,而代之以另一个优先级虽然更低但能够运行的任务。因此,采用事件驱动任务的意义就在于任务可以被创建在许多不同的优先级上,并且最高优先级任务不会把所有的低优先级任务饿死。

    阻塞状态

    任务可以进入阻塞态以等待以下两种不同类型的事件:

    1. 定时(时间相关)事件——这类事件可以是延迟到期或是绝对时间到点。比如
      说某个任务可以进入阻塞态以延迟 10ms。
    2. 同步事件——源于其它任务或中断的事件。比如说,某个任务可以进入阻塞
      态以等待队列中有数据到来。同步事件囊括了所有板级范围内的事件类型。

    挂起状态

    挂起后的任务对于调度器来说是不可见的。

    就绪状态

    如果任务处于非运行状态,但既没有阻塞也没有挂起,则这个任务处于就绪(ready,准备或就绪)状态。处于就绪态的任务能够被运行,但只是“准备(ready)”运行,而当前尚未运行。

image-20240414163317855

​ < 任务状态机 >

  • 错误使用优先级代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    /*
    *问题描述:开始任务优先级为1,开启任务调度后,开始任务开始工作,首先创建一个任务优先级为2的任务,cpu将会执行高优先级的任务,导致开始任务后续代码得不到cpu的使用权,从而使任务优先级为2的任务一直执行,导致其它任务饿死
    *解决方法:开始任务优先级设置高一点,使其一直执行,并且在开始任务执行后删除自身,使创建的任务开始执行。如果具有相同的优先级,还要记得使用延时函数进行阻塞。
    */
    int main(void)
    {
    //创建开始任务
    xTaskCreate((TaskFunction_t)start_task, //任务函数
    (const char *)"start_task", //任务名称
    (uint16_t)START_STK_SIZE, //任务堆栈大小
    (void *)NULL, //传递给任务函数的参数
    (UBaseType_t)1, //任务优先级
    (TaskHandle_t *)&StartTask_Handler); //任务句柄

    vTaskStartScheduler(); //开启任务调度
    }

    //开始任务任务函数
    void start_task(void *pvParameters)
    {
    xTaskCreate((TaskFunction_t)print_task,
    (const char *)"print_task",
    (uint16_t)1000,
    (void *)pcTextForTask1,
    (UBaseType_t)2,
    NULL);

    xTaskCreate((TaskFunction_t)print_task,
    (const char *)"print_task",
    (uint16_t)1000,
    (void *)pcTextForTask2,
    (UBaseType_t)3,
    NULL);

    vTaskDelete(StartTask_Handler); //删除开始任务
    }
  • 延时函数

    vTaskDelay()和vTaskDelayUntil():这两个延时函数和自己实现的延时函数不同,这两个延时函数一旦被调用,当前任务会立马进入阻塞状态,而自己写的延时函数(以for循环等形式实现的软件延时)会被当做有效任务而一直执行。

    相对延时是指每次延时都是从任务执行函数vTaskDelay()开始,延时指定的时间结束;
    绝对延时是指每隔指定的时间,执行一次调用vTaskDelayUntil()函数的任务。换句话说:任务以固定的频率执行。

    参考链接:FreeRTOS进阶之系统延时完全解析 / 张生荣 (zhangshengrong.com)

空闲任务

  • 空闲任务拥有最低优先级(优先级 0)以保证其不会妨碍具有更高优先级的应用任务进入运行态

  • 空闲任务的责任是要将分配给已删除任务的内存释放掉

    image-20240415115440911 image-20240415115414443

改变优先级

  • vTaskPrioritySet()用于在调度器启动后改变任何任务的优先级

  • uxTaskPriorityGet() 用于查询一个任务的优先级

删除任务

  • 当一个正在运行的任务调用了vTaskDelete(NULL)删除自己时,只会将任务的从列表(就绪,阻塞,挂起和事件链表)中移除,而任务结构体TCB与栈会交给空闲任务去释放,这样才算完成任务的删除

调度算法

  • 优先级抢占式调度

    image-20240415155457216

  • 协作式调度

    只可能在运行态任务进入阻塞态或是运行态任务显式调用 taskYIELD()时,才会进行上下文切换。任务永远不会被抢占,而具有相同优先级的任务也不会自动共享处理器时间。协作式调度的这作工作方式虽然比较简单,但可能会导致系统响应不够快。

FreeRTOS系统配置

  • FreeRTOS 的系统配置文件为 FreeRTOSConfig.h,可通过此文件完成系统配置和裁剪

  • 栈溢出检测方法:

    钩子函数

    vApplicationStackOverflowHook( ( TaskHandle_t ) pxCurrentTCB, pxCurrentTCB->pcTaskName );通过参数pxCurrentTCBpcTaskName ,开发者可以确定是哪个任务堆栈发生溢出,从而进行相应处理。需要注意的是,根据堆栈的溢出程度不同,这些参数本身可能也是不正确的,在这种情况下可以通过访问当前任务控制块pxCurrentTCB来获取堆栈溢出任务信息。

    每个任务都有自己的堆栈

    ​ (1)使用xTaskCreate()创建,则任务堆栈会自动从堆内存上创建;

    ​ (2)使用xTaskCreateStatic()创建,则堆栈由开发者自己确定并提供。

    检测方法

    ​ (1)上下文切换时将会占用较多的堆栈,该方法通过不断检测堆栈指针是否指向有效空间,来确定是否溢出。在堆栈中,portSTACK_GROWTH确定栈增长的方向,指针会从高地址开始指向堆栈的顶部,然后随着堆栈的使用而递减,因此pxCurrentTCB->pxTopOfStack > pxCurrentTCB->pxStack时,才表明发生栈溢出,之后调用钩子函数,通过( TaskHandle_t ) pxCurrentTCB, pxCurrentTCB->pcTaskName参数,可以确定哪个任务出现堆栈溢出。优缺点:检测快,检测不全。

    image-20240413145357135
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    #if( ( configCHECK_FOR_STACK_OVERFLOW == 1 ) && ( portSTACK_GROWTH < 0 ) )
    /* Only the current stack state is to be checked. */
    #define taskCHECK_FOR_STACK_OVERFLOW()
    {
    /* Is the currently saved stack pointer within the stack limit? */
    if( pxCurrentTCB->pxTopOfStack <= pxCurrentTCB->pxStack ) // 从高到低增长
    {
    vApplicationStackOverflowHook( ( TaskHandle_t ) pxCurrentTCB, pxCurrentTCB->pcTaskName );
    }
    }
    #endif /* configCHECK_FOR_STACK_OVERFLOW == 1 */

    or:

    #if( ( configCHECK_FOR_STACK_OVERFLOW == 1 ) && ( portSTACK_GROWTH > 0 ) ) // 从低到高增长
    /* Only the current stack state is to be checked. */
    #define taskCHECK_FOR_STACK_OVERFLOW()
    {
    /* Is the currently saved stack pointer within the stack limit? */
    if( pxCurrentTCB->pxTopOfStack >= pxCurrentTCB->pxEndOfStack )
    {
    vApplicationStackOverflowHook( ( TaskHandle_t ) pxCurrentTCB, pxCurrentTCB->pcTaskName );
    }
    }
    #endif /* configCHECK_FOR_STACK_OVERFLOW == 1 */

    ​ (2)在创建任务的时候会向任务堆栈填充一个已知的标记值,方法二会一直检测堆栈后面的几个 bytes(标记值)是否被改写,如果被改写的话就会调用堆栈溢出钩子函数。堆栈从高到低增长,则检测高地址上的标记值;堆栈从低到高增长,则检测低地址上的标记值。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    #if( ( configCHECK_FOR_STACK_OVERFLOW > 1 ) && ( portSTACK_GROWTH < 0 ) )
    #define taskCHECK_FOR_STACK_OVERFLOW()
    {
    const uint32_t * const pulStack = ( uint32_t * ) pxCurrentTCB->pxStack;
    const uint32_t ulCheckValue = ( uint32_t ) 0xa5a5a5a5;
    if( ( pulStack[ 0 ] != ulCheckValue ) ||
    ( pulStack[ 1 ] != ulCheckValue ) ||
    ( pulStack[ 2 ] != ulCheckValue ) ||
    ( pulStack[ 3 ] != ulCheckValue ) )
    {
    vApplicationStackOverflowHook( ( TaskHandle_t ) pxCurrentTCB, pxCurrentTCB->pcTaskName );
    }
    }
    #endif /* #if( configCHECK_FOR_STACK_OVERFLOW > 1 ) */

    #if( ( configCHECK_FOR_STACK_OVERFLOW > 1 ) && ( portSTACK_GROWTH > 0 ) )
    #define taskCHECK_FOR_STACK_OVERFLOW()
    {
    int8_t *pcEndOfStack = ( int8_t * ) pxCurrentTCB->pxEndOfStack;
    static const uint8_t ucExpectedStackBytes[] = { tskSTACK_FILL_BYTE, tskSTACK_FILL_BYTE, tskSTACK_FILL_BYTE, tskSTACK_FILL_BYTE,
    tskSTACK_FILL_BYTE, tskSTACK_FILL_BYTE, tskSTACK_FILL_BYTE, tskSTACK_FILL_BYTE,
    tskSTACK_FILL_BYTE, tskSTACK_FILL_BYTE, tskSTACK_FILL_BYTE, tskSTACK_FILL_BYTE,
    tskSTACK_FILL_BYTE, tskSTACK_FILL_BYTE, tskSTACK_FILL_BYTE, tskSTACK_FILL_BYTE,
    tskSTACK_FILL_BYTE, tskSTACK_FILL_BYTE, tskSTACK_FILL_BYTE, tskSTACK_FILL_BYTE };
    pcEndOfStack -= sizeof( ucExpectedStackBytes );
    /* Has the extremity of the task stack ever been written over? */
    if( memcmp( ( void * ) pcEndOfStack, ( void * ) ucExpectedStackBytes, \
    sizeof( ucExpectedStackBytes) ) != 0 )
    {
    vApplicationStackOverflowHook( ( TaskHandle_t ) pxCurrentTCB, pxCurrentTCB->pcTaskName );
    }
    }

多任务系统

image-20240422161025329 image-20240422161112764

队列管理

队列基础知识

  • FreeRTOS 中所有的通信与同步机制都是基于队列实现的

    image-20240418104536691

    ​ <队列读写过程>

队列函数

  • xQueueCreate()

    xQueueCreate()用于创建一个队列,并返回一个 xQueueHandle 句柄以便于对其创建的队列进行引用

  • xQueueSendToBack() 与 xQueueSendToFront()

    xQueueSendToBack()用于将数据发送到队列尾;而 xQueueSendToFront()用于将数据发送到队列首(但切记不要在中断服务例程中调用 xQueueSendToFront() 或xQueueSendToBack(),系统提供中断安全版本的xQueueSendToFrontFromISR()与xQueueSendToBackFromISR()用于在中断服务中实现相同的功能)

  • xQueueReceive()与 xQueuePeek()

    xQueueReceive()用于从队列中接收(读取)数据单元,接收到的单元同时会从队列中删除;xQueuePeek()也是从从队列中接收数据单元,不同的是并不从队列中删出接收到的单元。xQueuePeek()从队列首接收到数据后,不会修改队列中的数据,也不会改变数据在队列中的存储顺序。(中断安全版本的替代 API 函数xQueueReceiveFromISR())

  • uxQueueMessagesWaiting()

    uxQueueMessagesWaiting()用于查询队列中当前有效数据单元个数;(在中断服务中使用其中断安全版本 uxQueueMessagesWaitingFromISR())

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    /* 写任务优先级小于读任务优先级,结果是队列中只要有数据,就会被读取走 */
    static const long writeData01 = 100;
    static const long writeData02 = 200;
    // 队列
    QueueHandle_t myQueue;

    int main(void)
    {
    // 创建队列
    myQueue = xQueueCreate(10, sizeof(long));

    //创建开始任务
    xTaskCreate((TaskFunction_t)start_task, //任务函数
    (const char *)"start_task", //任务名称
    (uint16_t)START_STK_SIZE, //任务堆栈大小
    (void *)NULL, //传递给任务函数的参数
    (UBaseType_t)10, //任务优先级
    (TaskHandle_t *)&StartTask_Handler); //任务句柄

    vTaskStartScheduler(); //开启任务调度
    }

    //开始任务任务函数
    void start_task(void *pvParameters)
    {
    if (myQueue != NULL)
    {
    xTaskCreate((TaskFunction_t)write_queue_task,
    (const char *)"write_queue_task01",
    (uint16_t)1000,
    (void *)writeData01,
    (UBaseType_t)3,
    (TaskHandle_t *)&write_queue_task01); //任务句柄

    xTaskCreate((TaskFunction_t)write_queue_task,
    (const char *)"write_queue_task02",
    (uint16_t)1000,
    (void *)writeData02,
    (UBaseType_t)3,
    (TaskHandle_t *)&write_queue_task02); //任务句柄

    xTaskCreate((TaskFunction_t)read_queue_task,
    (const char *)"read_queue_task",
    (uint16_t)1000,
    (void *)NULL,
    (UBaseType_t)4,
    (TaskHandle_t *)&read_queue_task01); //任务句柄
    }
    vTaskDelete(StartTask_Handler); //删除开始任务
    }

    //写队列任务函数
    void write_queue_task(void *pvParameters)
    {
    long tempWriteData;
    BaseType_t returnState;
    tempWriteData = (long)pvParameters;

    while(1)
    {
    returnState = xQueueSendToBack(myQueue, &tempWriteData, 0);
    if (returnState != pdPASS)
    {
    printf("Write Data errQUEUE_FULL!\r\n");
    }
    }
    }

    //读队列任务函数
    void read_queue_task(void *pvParameters)
    {
    long destReadData;
    BaseType_t returnResult;
    const portTickType xTicksToWait = 100 / portTICK_RATE_MS;
    while(1)
    {
    returnResult = xQueueReceive(myQueue, &destReadData, xTicksToWait);
    if (returnResult != pdPASS)
    {
    printf("Read Data errQUEUE_FULL!\r\n");
    }
    else
    {
    printf("Received: %ld\r\n", destReadData);
    }
    }
    }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    /* 写任务优先级大于读任务优先级,结果是队列中写任务轮流阻塞写入数据,读任务一直工作读取数据*/
    #define mainSENDER_1 1
    #define mainSENDER_2 2
    // 队列
    QueueHandle_t myQueue;
    /* 定义队列传递的结构类型。 */
    typedef struct
    {
    unsigned char ucValue;
    unsigned char ucSource;
    } xData;
    /* 声明两个xData类型的变量,通过队列进行传递 */
    static const xData xStructsToSend[2] =
    {
    { 100, mainSENDER_1 }, /* Used by Sender1. */
    { 200, mainSENDER_2 } /* Used by Sender2. */
    };

    int main(void)
    {
    // 创建队列
    myQueue = xQueueCreate(3, sizeof(xData));

    //创建开始任务
    xTaskCreate((TaskFunction_t)start_task, //任务函数
    (const char *)"start_task", //任务名称
    (uint16_t)START_STK_SIZE, //任务堆栈大小
    (void *)NULL, //传递给任务函数的参数
    (UBaseType_t)10, //任务优先级
    (TaskHandle_t *)&StartTask_Handler); //任务句柄

    vTaskStartScheduler(); //开启任务调度
    }

    //开始任务任务函数
    void start_task(void *pvParameters)
    {
    xTaskCreate((TaskFunction_t)write_queue_task,
    (const char *)"write_queue_task01",
    (uint16_t)1000,
    (void *)&(xStructsToSend[0]),
    (UBaseType_t)3,
    (TaskHandle_t *)&write_queue_task01); //任务句柄

    xTaskCreate((TaskFunction_t)write_queue_task,
    (const char *)"write_queue_task02",
    (uint16_t)1000,
    (void *)&(xStructsToSend[1]),
    (UBaseType_t)3,
    (TaskHandle_t *)&write_queue_task02); //任务句柄

    xTaskCreate((TaskFunction_t)read_queue_task,
    (const char *)"read_queue_task",
    (uint16_t)1000,
    (void *)NULL,
    (UBaseType_t)2,
    (TaskHandle_t *)&read_queue_task01); //任务句柄

    vTaskDelete(StartTask_Handler); //删除开始任务
    }

    //写队列任务函数
    void write_queue_task(void *pvParameters)
    {
    xData* tempWriteData;
    BaseType_t returnState;
    const portTickType xTicksToWait = 100 / portTICK_RATE_MS;
    tempWriteData = pvParameters;

    while(1)
    {
    returnState = xQueueSendToBack(myQueue, tempWriteData, xTicksToWait);
    if (returnState != pdPASS)
    {
    printf("Write Data errQUEUE_FULL!\r\n");
    }
    }
    }

    //读队列任务函数
    void read_queue_task(void *pvParameters)
    {
    xData destReadData;
    BaseType_t returnResult;

    while(1)
    {
    returnResult = xQueueReceive(myQueue, &destReadData, 0);
    if (returnResult != pdPASS)
    {
    printf("Read Data errQUEUE_FULL!\r\n");
    }
    else
    {
    if (destReadData.ucSource == mainSENDER_1)
    {
    printf("Received 1----%d\r\n", destReadData.ucValue);
    }
    else if (destReadData.ucSource == mainSENDER_2)
    {
    printf("Received 2----%d\r\n", destReadData.ucValue);
    }
    else
    {
    printf("ERROR!\r\n");
    }
    }
    }
    }

大数据量队列传输

  • 如果队列存储的数据单元尺寸较大,利用队列来传递数据的指针而不是对数据本身在队列上一字节一字节地拷贝进或拷贝出。传递指针无论是在处理速度上还是内存空间利用上都更有效。

  • 指针操作时注意

    (1)当任务间通过指针共享内存时,应该从根本上保证所不会有任意两个任务同时修改共享内存中的数据,或是以其它行为方式使得共享内存数据无效或产生一致性问题;共享内存在其指针发送到队列之前,其内容只允许被发送任务访问;共享内存指针从队列中被读出之后,其内容亦只允许被接收任务访问。

    (2)如果指针指向的内存空间是动态分配的,只应该有一个任务负责对其进行内存释放。当这段内存空间被释放之后,就不应该有任何一个任务再访问这段空间。

    (3)切忌用指针访问任务栈上分配的空间。因为当栈帧发生改变后,栈上的数据将不再有效。

中断管理

M3中断

  • Cotex-M3 的 NVIC 最多支持 240 个 IRQ(中断请求)、1 个不可屏蔽中断(NMI)、1 个 Systick(滴答定时器)定时器中断和多个系统异常

  • FreeRTOS 的中断配置没有处理亚优先级这种情况,所以只能配置为组 4,16 个优先级

    (1)中断优先级与任务优先级的区别

    • 二者没有任何关系,不论中断优先级别是多少,中断优先级总是高于任务优先级,中断来了就开始执行中断服务程序
    • 通常情况下,中断优先级数值越小,则优先级别越高;但是任务优先级越小,表明优先级别越低
  • configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY(FreeRTOS可以管理的最大的优先级),可以自由设置,设置为 5也就是高于 5 的优先级(优先级数小于 5)不归 FreeRTOS 管理!

  • configKERNEL_INTERRUPT_PRIORITY(最小优先级)16 个优先级,0-15,不同MCU不同,根据芯片架构选择

    image-20240422160631917