FreeRTOS学习记录-ESP32

FreeRTOS学习记录

首先,此篇笔记是针对FreeRTOS的回顾,平台是ESP32系列,部分FreeRTOS基本概念,因为我之前是学过的,如调度什么的我这边并没有笔记。

笔记的目的是,借此学习一下ESP32,且快速回顾一下FreeRTOS,在需要时,可以快速找到对应的概念和API接口。

ESP32使用FreeRTOS与原生FreeRTOS应用程序入口有所不同,

在 ESP-IDF 中使用 FreeRTOS 的用户 永远不应调用 vTaskStartScheduler() 和 vTaskEndScheduler()。相反,ESP-IDF 会自动启动 FreeRTOS。用户必须定义一个 void app_main(void) 函数作为用户应用程序的入口点,并在 ESP-IDF 启动时被自动调用。

  • 通常,用户会从 app_main 中启动应用程序的其他任务。
  • app_main 函数可以在任何时候返回(应用终止前)。
  • app_main 函数由 main 任务调用。

以下FreeRTOS笔记无特殊说明外,默认为单核

任务

创建任务

  • 使用xTaskCreate()创建任务时,任务内存动态分配。
  • 使用xTaskCreateStatic()创建任务时,任务内存静态分配,即由用户提供。

执行任务

  • 只能处于以下任一状态:运行中、就绪、阻塞或挂起。
  • 任务函数通常为无限循环。
  • 任务函数不应返回。

删除任务

  • 使用vTaskDelete()删除任务,若任务句柄为NULL,则会删除当前正在运行的任务
  • 注意事项:
    • 请保证删除任务时,任务是处于已知的状态
      • 比如任务内部,运行完成,且释放了任务内分配的资源,再进行删除,不然会造成内存泄露
      • 删除持有互斥锁的任务,会导致别的任务永久锁死

打印任务信息

  • 使用vTaskList()罗列出所有任务的当前状态,以及堆栈信息
  • 使用uxTaskGetStackHighWaterMark()获取任务栈剩余空间,越接近0越代表接近溢出,可以通过这个值,监测任务函数栈空间是否充足

空闲任务

  • ESP-IDF会隐式创建一个优先级为 0 的空闲任务。当没有其他任务准备运行时,空闲任务运行并有以下作用:

    • 释放已删除任务的内存
    • 执行应用程序的空闲函数

任务看门狗定时器 (TWDT)

  • 任务看门狗定时器 (TWDT) 用于监视特定任务,确保任务在配置的超时时间内执行。

  • TWDT 主要监视每个 CPU 的空闲任务

  • TWDT 是基于定时器组 0 中的硬件看门狗定时器构建的。超时发生时会触发中断。

  • 可以在用户代码中定义函数 esp_task_wdt_isr_user_handler 来接收超时事件,并扩展默认行为。

  • 调用以下函数,用 TWDT 监视任务:

    • esp_task_wdt_init()初始化 TWDT 并订阅空闲任务。
    • esp_task_wdt_add()为其他任务订阅 TWDT。
    • 订阅后,应从任务中调用esp_task_wdt_reset()来喂 TWDT。
    • esp_task_wdt_delete()可以取消之前订阅的任务。
    • esp_task_wdt_deinit取消订阅空闲任务并反初始化 TWDT。
  • 注意事项:

    • 擦除较大的 flash 区域可能会非常耗时,并可能导致任务连续运行,触发 TWDT 超时。以下两种方法可以避免这种情况:

      • 延长看门狗超时时间。
      • 在擦除 flash 区域前,调用esp_task_wdt_init(),增加看门狗超时时间。

消息队列

消息队列就是通过 RTOS 内核提供的服务,任务或中断服务子程序可以将一个消息(注意,FreeRTOS 消息队列传递的是实际数据,并不是数据地址,RTX,uCOS-II 和 uCOS-III 是传递的地址)放入到队列。

同样,一个或者多个任务可以通过 RTOS 内核服务从队列中得到消息。通常,先进入消息队列的消息先传 给任务,也就是说,任务先得到的是最先进入到消息队列的消息,即先进先出的原则(FIFO),FreeRTOS 的消息队列支持 FIFO 和 LIFO 两种数据存取方式。

  • 消息队列和全局变量相比,在FreeRTOS里更具以下优势:

    • 使用消息队列可以让 RTOS 更有效管理任务,而全局变量是无法做到

      1. 比如,任务的超时等待机制,用全局变量则需要用户自己去实现。
    • 消息队列支持FIFO,更有利于数据处理

    • 使用全局数组,还需要处理多任务的访问冲突,而消息队列就处理好了这个问题

    • 消息队列可以有效解决中断与任务之间通信问题

  • 使用消息队列传输数据时有两种方法:

    1. 拷贝:把数据、把变量的值复制进队列里
    2. 引用:把数据、把变量的地址复制进队列里

消息队列--任务之间通信

发送消息

接收消息

任务1

消息队列

任务2

消息队列--中断与任务之间通信

中断处理:发送消息

接收消息

中断触发

消息队列

任务1

  • 注意

    • 在中断发送消息需要使用 xQueueSendFromISR,且不支持超时设置,所以发送前要通过函数 xQueueIsQueueFullFromISR 检测 消息队列是否满
    • 在中断中处理越快越好,防止低于该优先级的异常无法正常响应
    • 最好不要在中断中处理消息队列,只发送
    • 中断服务程序中一定要调用专用于中断的消息队列函数,即以 FromISR 结尾的函数。

创建消息队列

  • 动态分配

    QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength, /* 消息个数 */
    UBaseType_t uxItemSize ); /* 每个消息大小,单位字节 */
  • 静态分配(一般不用这个)

    QueueHandle_t xQueueCreateStatic( UBaseType_t uxQueueLength, /* 消息个数 */
    UBaseType_t uxItemSize, /* 每个消息大小,单位字节 */
    uint8_t *pucQueueStorageBuffer, /* 如果uxItemSize非0,pucQueueStorageBuffer必须指向一个uint8_t数组,此数组大小至少为"uxQueueLength * uxItemSize" */
    StaticQueue_t *pxQueueBuffer ); /* 必须执行一个StaticQueue_t结构体,用来保存队列的数据结构 */

写消息队列

/*
* 等同于xQueueSendToBack,往队列尾部写入数据,如果没有空间,阻塞时间为xTicksToWait
*/
BaseType_t xQueueSend( QueueHandle_t xQueue, /* 消息队列句柄 */
const void * pvItemToQueue, /* 要传递数据地址 */
TickType_t xTicksToWait ); /* 等待消息队列有空间的最大等待时间 */
/*
* 往队列尾部写入数据,如果没有空间,阻塞时间为xTicksToWait
*/
BaseType_t xQueueSendToBack(
QueueHandle_t xQueue,
const void *pvItemToQueue,
TickType_t xTicksToWait
);
/*
* 往队列尾部写入数据,此函数可以在中断函数中使用,不可阻塞
*/
BaseType_t xQueueSendToBackFromISR(
QueueHandle_t xQueue,
const void *pvItemToQueue,
BaseType_t *pxHigherPriorityTaskWoken
);
/*
* 往队列头部写入数据,如果没有空间,阻塞时间为xTicksToWait
*/
BaseType_t xQueueSendToFront(
QueueHandle_t xQueue,
const void *pvItemToQueue,
TickType_t xTicksToWait
);
/*
* 往队列头部写入数据,此函数可以在中断函数中使用,不可阻塞
*/
BaseType_t xQueueSendToFrontFromISR(
QueueHandle_t xQueue,
const void *pvItemToQueue,
BaseType_t *pxHigherPriorityTaskWoken
);

读消息队列

/* 读到一个数据后,队列中该数据会被移除 */
BaseType_t xQueueReceive( QueueHandle_t xQueue, /* 消息队列句柄 */
void * const pvBuffer, /* bufer指针,队列的数据会被复制到这个buffer -*+*/
TickType_t xTicksToWait );/* 等待消息队列有空间的最大等待时间 */
BaseType_t xQueueReceiveFromISR( QueueHandle_t xQueue,
void *pvBuffer,
BaseType_t *pxTaskWoken );

删除消息队列

/* vQueueDelete()只能删除使用动态方法创建的队列,它会释放内存 */
void vQueueDelete( QueueHandle_t xQueue );

查询消息队列

/*
* 返回队列中可用数据的个数
*/
UBaseType_t uxQueueMessagesWaiting( const QueueHandle_t xQueue );
/*
* 返回队列中可用空间的个数
*/
UBaseType_t uxQueueSpacesAvailable( const QueueHandle_t xQueue );

覆盖/窥视

  • 覆盖

    当队列长度为1时(队列长度必须为1才可以使用),可以使用xQueueOverwrite()xQueueOverwriteFromISR()来覆盖数据

    在FreeRTOS中,队列的发送和接收操作是原子的,也就是说,在执行这些操作期间,不会被中断或其他任务打断。这确保了数据的完整性和可靠性。

    而覆盖操作是一种特殊情况,它允许在队列满时替换队列中最早的消息,并添加新的消息。

    为了保持原子性,只有在队列长度为1时,才能保证覆盖操作的一致性。

    如果队列长度大于1,那么在进行覆盖操作时,可能会涉及多个元素的移动和替换。 由于队列操作必须是原子的,这将涉及更复杂的同步和保护机制,以确保数据的一致性。这不仅增加了实现的复杂度,还可能引入竞争条件和死锁等问题。

    /* 覆盖队列
    * xQueue: 写哪个队列
    * pvItemToQueue: 数据地址
    * 返回值: pdTRUE表示成功, pdFALSE表示失败
    */
    BaseType_t xQueueOverwrite(
    QueueHandle_t xQueue,
    const void * pvItemToQueue
    );
    BaseType_t xQueueOverwriteFromISR(
    QueueHandle_t xQueue,
    const void * pvItemToQueue,
    BaseType_t *pxHigherPriorityTaskWoken
    );
  • 窥视

    想让队列中的数据供多方读取,也就是说读取时不要移除数据,要留给后来人。那么可以使用"窥视",也就是xQueuePeek()xQueuePeekFromISR()。这些函数会从队列中复制出数据,但是不移除数据。

    这也意味着,如果队列中没有数据,那么"偷看"时会导致阻塞;一旦队列中有数据,以后每次"偷看"都会成功。

    /* 偷看队列
    * xQueue: 偷看哪个队列
    * pvItemToQueue: 数据地址, 用来保存复制出来的数据
    * xTicksToWait: 没有数据的话阻塞一会
    * 返回值: pdTRUE表示成功, pdFALSE表示失败
    */
    BaseType_t xQueuePeek(
    QueueHandle_t xQueue,
    void * const pvBuffer,
    TickType_t xTicksToWait
    );
    BaseType_t xQueuePeekFromISR(
    QueueHandle_t xQueue,
    void *pvBuffer,
    );

队列集合

知道有这个即可,实际很少用到!

队列邮箱

邮箱的概念,其实就是,长度为1的消息队列,使用覆盖函数,之所以是覆盖函数,就是不管队列有没有值,都能及时更新,不会陷入阻塞的情况,发送任务发送数据后,队列就成了邮箱,其他任务都在”订阅“邮箱,使用窥视函数去,只获取值,不删除数据,达到一个发送,多个接收。

这个也是知道概念即可,具体用到的函数也是上述几个。

具体流程如下

使用队列覆盖函数

发送任务

邮箱:长度为1的队列

任务1:使用窥视函数读取

任务2:使用窥视函数读取

任务3:使用窥视函数读取

使用注意

  1. 分辨数据源

    1. 当有多个发送任务,通过同一个队列发出数据,接收任务如何分辨数据来源?数据本身带有"来源"信息,比如写入队列的数据是一个结构体,结构体中的lDataSouceID用来表示数据来源:
    typedef struct {
    ID_t eDataID;
    int32_t lDataValue;
    }Data_t;
    /* 不同的发送任务,先构造好结构体,填入自己的eDataID,再写队列;接收任务读出数据后,根据eDataID就可以知道数据来源了 */
  2. 传输大块数据

    1. 因为FreeRTOS的队列使用拷贝传输,如果是用uint32_t类型,那就拷贝4字节,如果用uint8_t类型,那就拷贝1字节,但是如果要拷贝很大的数据,比如uint8_t data[1000],写队列的时候直接拷贝1000个字节吗?读队列连续读1000个字节??那样效率未免也太低了吧。更为合适的方法是 使用**地址来间接传数据**
      /* 比如 send_task 里面 malloc(1000)字节,把地址通过队列写进去
      在 recive_task 里面 free() 释放空间,使用时要注意,成对出现
      不要未使用就释放内存,导致野指针
      也不要忘记释放内存,导致内存泄漏
      */
      void send_task(void *arg)
      {
      QueueHandle_t QueueHandle1 = (QueueHandle_t)arg;
      BaseType_t status;
      while(1) {
      char *pStrSend = malloc(1000); //分配内存,并拷贝数据(我忽略了这步)
      status = xQueueSend(QueueHandle1, &pStrSend , 5000);
      if (status == pdPASS) {
      printf("send success\n");
      } else {
      printf("send fail\n");
      }
      vTaskDelay(1000 / portTICK_PERIOD_MS);
      }
      }
      void recive_task(void *arg)
      {
      QueueHandle_t QueueHandle1 = (QueueHandle_t)arg;
      BaseType_t status;
      char *pStrRecive;
      while(1) {
      status = xQueueReceive(QueueHandle1, &pStrRecive, 0);
      if (status == pdPASS) {
      // 在这里处理数据,随后并释放空间
      free(pStrRecive);
      printf("pStrRecive:%s\n\n", pStrRecive);
      } else {
      printf("recive fail\n");
      }
      vTaskDelay(1000 / portTICK_PERIOD_MS);
      }
      }

信号量

  • 信号:起通知作用

  • 量:还可以用来表示资源的数量

    • 当"量"没有限制时,它就是"计数型信号量"(Counting Semaphores)
    • 当"量"只有0、1两个取值时,它就是"二进制信号量"(Binary Semaphores)
  • 支持的动作都一样,give--给出资源,take--拿走资源,计数值减1

二进制信号量跟计数型的唯一差别,就是计数值的最大值被限定为1。

信号量的"give"、"take"双方并不需要相同,可以用于生产者-消费者场合:即多个任务产生信号量,多个任务消费信号量

计数型信号量

/* 创建一个计数型信号量,返回它的句柄。
* 此函数内部会分配信号量结构体
* uxMaxCount: 最大计数值
* uxInitialCount: 初始计数值
* 返回值: 返回句柄,非NULL表示成功
*/
SemaphoreHandle_t xSemaphoreCreateCounting(UBaseType_t uxMaxCount, UBaseType_t uxInitialCount);
/* 创建一个计数型信号量,返回它的句柄。
* 此函数无需动态分配内存,所以需要先有一个StaticSemaphore_t结构体,并传入它的指针
* uxMaxCount: 最大计数值
* uxInitialCount: 初始计数值
* pxSemaphoreBuffer: StaticSemaphore_t结构体指针
* 返回值: 返回句柄,非NULL表示成功
*/
SemaphoreHandle_t xSemaphoreCreateCountingStatic( UBaseType_t uxMaxCount,
UBaseType_t uxInitialCount,
StaticSemaphore_t *pxSemaphoreBuffer );

二进制信号量

信号量在创建后是空的状态,在调用take获取之前,需要先give释放一个资源出来

/* 创建一个二进制信号量,返回它的句柄。
* 此函数内部会分配信号量结构体
* 返回值: 返回句柄,非NULL表示成功
*/
SemaphoreHandle_t xSemaphoreCreateBinary( void );
/* 创建一个二进制信号量,返回它的句柄。
* 此函数无需动态分配内存,所以需要先有一个StaticSemaphore_t结构体,并传入它的指针
* 返回值: 返回句柄,非NULL表示成功
*/
SemaphoreHandle_t xSemaphoreCreateBinaryStatic( StaticSemaphore_t *pxSemaphoreBuffer );

give/take 信号量

  • 关于give的函数

    /* 在任务中使用,释放信号量
    * xSemaphore:信号量句柄
    * 返回值: pdTRUE表示成功,
    * 如果二进制信号量的计数值已经是1,再次调用此函数则返回失败;
    * 如果计数型信号量的计数值已经是最大值,再次调用此函数则返回失败
    */
    BaseType_t xSemaphoreGive( SemaphoreHandle_t xSemaphore );
    /* 在ISR中使用,释放信号量
    * xSemaphore:信号量句柄
    * pxHigherPriorityTaskWoken:如果释放信号量导致更高优先级的任务变为了就绪态,
    * 则*pxHigherPriorityTaskWoken = pdTRUE
    * 返回值: pdTRUE表示成功,
    * 如果二进制信号量的计数值已经是1,再次调用此函数则返回失败;
    * 如果计数型信号量的计数值已经是最大值,再次调用此函数则返回失败
    */
    BaseType_t xSemaphoreGiveFromISR(
    SemaphoreHandle_t xSemaphore,
    BaseType_t *pxHigherPriorityTaskWoken
    );
  • 关于take的函数

    /* 在任务中使用,获取信号量
    * xSemaphore:信号量句柄
    * xTicksToWait:阻塞时间,0:不阻塞,马上返回, portMAX_DELAY: 一直阻塞直到成功,
    * 其他值: 阻塞的Tick个数,可以使用pdMS_TO_TICKS()来指定阻塞时间为若干ms
    * 返回值: pdTRUE表示成功
    */
    BaseType_t xSemaphoreTake(
    SemaphoreHandle_t xSemaphore,
    TickType_t xTicksToWait
    );
    /* 在ISR中使用,获取信号量
    * xSemaphore:信号量句柄
    * pxHigherPriorityTaskWoken:如果获取信号量导致更高优先级的任务变为了就绪态,
    * 则*pxHigherPriorityTaskWoken = pdTRUE
    * 返回值: pdTRUE表示成功,
    */
    BaseType_t xSemaphoreTakeFromISR(
    SemaphoreHandle_t xSemaphore,
    BaseType_t *pxHigherPriorityTaskWoken
    );

获取信号量数量

/*
* xSemaphore: 信号量句柄
* 返回值: 信号量个数
*/
uxSemaphoreGetCount( xSemaphore );

删除信号量

对于动态创建的信号量,不再需要它们时,可以删除它们以回收内存。

vSemaphoreDelete可以用来删除二进制信号量、计数型信号量。

/*
* xSemaphore: 信号量句柄,你要删除哪个信号量
*/
void vSemaphoreDelete( SemaphoreHandle_t xSemaphore );

互斥量

互斥量的用途:用来实现互斥访问

值只有 0 和 1

资源互斥的核心在于:谁上锁,就只能由谁开锁 (代码上又并没有实现这点,只是约定成俗,谁上锁谁开锁)

提及两个概念:

  • 对变量的非原子化访问

    修改变量、设置结构体、在16位的机器上写32位的变量,这些操作都是非原子的。也就是它们的操作过程都可能被打断,如果被打断的过程有其他任务来操作这些变量,就可能导致冲突。

  • 函数重入

    “可重入的函数"是指:多个任务同时调用它、任务和中断同时调用它,函数的运行也是安全的。可重入的函数也被称为"线程安全”(thread safe)。 每个任务都维持自己的栈、自己的CPU寄存器,如果一个函数只使用局部变量,那么它就是线程安全的。 函数中一旦使用了全局变量、静态变量、其他外设,它就不是"可重入的",如果改函数正在被调用,就必须阻止其他任务、中断再次调用它。

    任务A访问这些全局变量、函数代码时,独占它,就是上个锁。这些全局变量、函数代码必须被独占地使用,它们被称为临界资源。

互斥量有以下特点需要注意:

  • 刚创建的互斥量可以被成功"take"
  • “take"互斥量成功的任务,被称为"holder”,只能由它"give"互斥量;别的任务"give"不成功
  • 在ISR中不能使用互斥量
  • 互斥量会去“继承”,企图获取互斥量的任务的优先级

创建互斥量

互斥量初始值为1

/* 创建一个互斥量,返回它的句柄。
* 此函数内部会分配互斥量结构体
* 返回值: 返回句柄,非NULL表示成功
*/
SemaphoreHandle_t xSemaphoreCreateMutex( void );
/* 创建一个互斥量,返回它的句柄。
* 此函数无需动态分配内存,所以需要先有一个StaticSemaphore_t结构体,并传入它的指针
* 返回值: 返回句柄,非NULL表示成功
*/
SemaphoreHandle_t xSemaphoreCreateMutexStatic( StaticSemaphore_t *pxMutexBuffer );

删除互斥量

/*
* xSemaphore: 信号量句柄,你要删除哪个信号量, 互斥量也是一种信号量
*/
void vSemaphoreDelete( SemaphoreHandle_t xSemaphore );

获取、释放互斥量

/* 释放 */
BaseType_t xSemaphoreGive( SemaphoreHandle_t xSemaphore );
/* 释放(ISR版本) */
BaseType_t xSemaphoreGiveFromISR(
SemaphoreHandle_t xSemaphore,
BaseType_t *pxHigherPriorityTaskWoken
);
/* 获得 */
BaseType_t xSemaphoreTake(
SemaphoreHandle_t xSemaphore,
TickType_t xTicksToWait
);
/* 获得(ISR版本) */
xSemaphoreGiveFromISR(
SemaphoreHandle_t xSemaphore,
BaseType_t *pxHigherPriorityTaskWoken
);

死锁

死锁又分为两种情况:

  • 互斥死锁

    • 假设有2个互斥量M1、M2,2个任务A、B:

      • A获得了互斥量M1
      • B获得了互斥量M2
      • A还要获得互斥量M2才能运行,结果A阻塞
      • B还要获得互斥量M1才能运行,结果B阻塞
      • A、B都阻塞,再无法释放它们持有的互斥量
      • 死锁发生!
  • 自我死锁

    • 任务A获得了互斥锁M
    • 它调用一个库函数
    • 库函数要去获取同一个互斥锁M,于是它阻塞:任务A休眠,等待任务A来释放互斥锁!
    • 死锁发生!

为了解决上诉死锁问题,又衍生出一种 递归互斥量

递归互斥量

递归互斥量实现了:谁上锁就由谁解锁

递归锁 一般互斥量x
创建 xSemaphoreCreateRecursiveMutex xSemaphoreCreateMutex
获得 xSemaphoreTakeRecursive xSemaphoreTake
释放 xSemaphoreGiveRecursive xSemaphoreGive

假设任务1 需要去对资源A访问,并且资源A需要对资源B进行访问,如果每次访问都加一个普通互斥量,那这对代码维护也十分麻烦,就有了递归互斥量

访问

访问

处理完返回

处理完返回

任务1

资源A

资源B

事件标志组

事件标志组是实现多任务同步的有效机制之一。

事件标志组和全局变量相比,在FreeRTOS里更具以下优势:

  1. 使用事件标志组可以让 RTOS 内核有效地管理任务,而全局变量是无法做到

    1. 比如,任务的超时等机制, 用全局变量则需要用户自己去实现。
  2. 使用了全局变量就要防止多任务的访问冲突,而使用事件标志组则处理好了这个问题。

  3. 使用事件标志组可以有效地解决中断服务程序和任务之间的同步问题。

TickType_t 数据类型可以是 16 位数或者 32 位数

创建事件标志组

/* 创建一个事件组,返回它的句柄。
* 此函数内部会分配事件组结构体
* 返回值: 返回句柄,非NULL表示成功
*/
EventGroupHandle_t xEventGroupCreate( void );
/* 创建一个事件组,返回它的句柄。
* 此函数无需动态分配内存,所以需要先有一个StaticEventGroup_t结构体,并传入它的指针
* 返回值: 返回句柄,非NULL表示成功
*/
EventGroupHandle_t xEventGroupCreateStatic( StaticEventGroup_t * pxEventGroupBuffer );

删除事件组

/*
* xEventGroup: 事件组句柄,你要删除哪个事件组
*/
void vEventGroupDelete( EventGroupHandle_t xEventGroup )

等待事件标志位

EventBits_t xEventGroupWaitBits( EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToWaitFor,
const BaseType_t xClearOnExit,
const BaseType_t xWaitForAllBits,
TickType_t xTicksToWait );

设置事件标志位

  • 使用xEventGroupSetBits(),不可以在中断服务程序中调用此函数

    • 返回当前的事件标志组数值

    • 用户通过函数设置的标志位,并不一定会保留到此函数的返回值中,下面举两种情况:

      1. 调用此函数的过程中,其它高优先级的任务就绪了,并且也修改了事件标志,此函数返回的事件 标志位会发生变化。
      2. 调用此函数的任务是一个低优先级任务,通过此函数设置了事件标志后,让一个等待此事件标志的高优先级任务就绪了,会立即切换到高优先级任务去执行,相应的事件标志位会被函数 xEventGroupWaitBits 清除掉,等从高优先级任务返回到低优先级任务后,函数 xEventGroupSetBits 的返回值已经被修改。
/* 设置事件组中的位
* xEventGroup: 哪个事件组
* uxBitsToSet: 设置哪些位?
* 如果uxBitsToSet的bitX, bitY为1, 那么事件组中的bitX, bitY被设置为1
* 可以用来设置多个位,比如 0x15 就表示设置bit4, bit2, bit0
* 返回值: 返回原来的事件值(没什么意义, 因为很可能已经被其他任务修改了)
*/
EventBits_t xEventGroupSetBits( EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToSet );
/* 设置事件组中的位
* xEventGroup: 哪个事件组
* uxBitsToSet: 设置哪些位?
* 如果uxBitsToSet的bitX, bitY为1, 那么事件组中的bitX, bitY被设置为1
* 可以用来设置多个位,比如 0x15 就表示设置bit4, bit2, bit0
* pxHigherPriorityTaskWoken: 有没有导致更高优先级的任务进入就绪态? pdTRUE-有, pdFALSE-没有
* 返回值: pdPASS-成功, pdFALSE-失败
*/
BaseType_t xEventGroupSetBitsFromISR( EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToSet,
BaseType_t * pxHigherPriorityTaskWoken );

中断中设置事件标志位

  • 使用xEventGroupSetBitsFromISR(),在中断中使用的是这个
  • 设置事件组时,有可能导致多个任务被唤醒,这会带来很大的不确定性。所以xEventGroupSetBitsFromISR函数不是直接去设置事件组,而是给一个FreeRTOS后台任务(daemon task)发送队列数据,由这个任务来设置事件组。

事件组同步

有一个事情需要多个任务协同,使用xEventGroupSync()函数可以同步多个任务:

  • 可以设置某位、某些位,表示自己做了什么事
  • 可以等待某位、某些位,表示要等等其他任务
  • 期望的时间发生后,xEventGroupSync()才会成功返回。
  • xEventGroupSync成功返回后,会清除事件
EventBits_t xEventGroupSync( EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToSet,
const EventBits_t uxBitsToWaitFor,
TickType_t xTicksToWait );

任务通知

任务通知,简单概括,就是具体通知到哪个任务去运行

我们使用队列、信号量、事件组等等方法时,并不知道对方是谁。使用任务通知时,可以明确指定:通知哪个任务。

使用队列、信号量、事件组时,我们都要事先创建对应的结构体,双方通过中间的结构体通信:

在这里插入图片描述

使用任务通知时,任务结构体TCB中就包含了内部对象,可以直接接收别人发过来的"通知":

在这里插入图片描述

任务通知的特性

  • 优势

    • 效率更高:使用任务通知来发送事件、数据给某个任务时,效率更高。比队列、信号量、事件组都有大的优势。
    • 更节省内存:使用其他方法时都要先创建对应的结构体,使用任务通知时无需额外创建结构体。
  • 限制

    • 不能发送数据给ISR: ISR并没有任务结构体,所以无法使用任务通知的功能给ISR发送数据。但是ISR可以使用任务通知的功能,发数据给任务。

    • 数据只能给该任务独享

      使用队列、信号量、事件组时,数据保存在这些结构体中,其他任务、ISR都可以访问这些数据。使用任务通知时,数据存放入目标任务中,只有它可以访问这些数据。 在日常工作中,这个限制影响不大。因为很多场合是从多个数据源把数据发给某个任务,而不是把一个数据源的数据发给多个任务。

    • 无法缓冲数据

      使用队列时,假设队列深度为N,那么它可以保持N个数据。 使用任务通知时,任务结构体中只有一个任务通知值,只能保持一个数据。 无法广播给多个任务 使用事件组可以同时给多个任务发送事件。 使用任务通知,只能发个一个任务。 如果发送受阻,发送方无法进入阻塞状态等待 假设队列已经满了,使用xQueueSendToBack()给队列发送数据时,任务可以进入阻塞状态等待发送完成。 使用任务通知时,即使对方无法接收数据,发送方也无法阻塞等待,只能即刻返回错误。

通知状态和通知值

每个任务都有一个结构体:TCB(Task Control Block),里面有2个成员:

  • 一个是uint8_t类型,用来表示通知状态
  • 一个是uint32_t类型,用来表示通知值
typedef struct tskTaskControlBlock
{
......
/* configTASK_NOTIFICATION_ARRAY_ENTRIES = 1 */
volatile uint32_t ulNotifiedValue[ configTASK_NOTIFICATION_ARRAY_ENTRIES ];
volatile uint8_t ucNotifyState[ configTASK_NOTIFICATION_ARRAY_ENTRIES ];
......
} tskTCB;

通知状态有3种取值

/* 任务没有在等待通知,也是初始状态 */
#define taskNOT_WAITING_NOTIFICATION ( ( uint8_t ) 0 )
/* 任务在等待通知 */
#define taskWAITING_NOTIFICATION ( ( uint8_t ) 1 )
/* 任务接收到了通知,也被称为pending(有数据了,待处理) */
#define taskNOTIFICATION_RECEIVED ( ( uint8_t ) 2 )

任务通知的使用

使用任务通知,可以实现轻量级的队列(长度为1)、邮箱(覆盖的队列)、计数型信号量、二进制信号量、事件组。

任务通知有2套函数,简化版、专业版,列表如下:

简化版函数的使用比较简单,它实际上也是使用专业版函数实现的
专业版函数支持很多参数,可以实现很多功能

简化版 专业版
发出通知 xTaskNotifyGive vTaskNotifyGiveFromISR xTaskNotify xTaskNotifyFromISR
取出通知 ulTaskNotifyTake xTaskNotifyWait

具体这块内容,使用时再找demo看即可,大概了解使用就好,一般较少使用

软件定时器

  • 软件定时器分为两种状态:

    • 运行(Running、Active):运行态的定时器,当指定时间到达之后,它的回调函数会被调用
    • 冬眠(Dormant):冬眠态的定时器还可以通过句柄来访问它,但是它不再运行,它的回调函数不会被调用
  • 软件定时器工作原理:

    • 首先,FreeRTOS有个Tick中断,软件定时器是基于Tick运行,按照非操作系统的理解,我们是在Tick中断里计数,达到值就调用定时器回调,但是在RTOS里,它不允许在内核、在中断中执行不确定的代码:如果定时器函数很耗时,会影响整个系统。

      所以,FreeRTOS中,不在Tick中断中执行定时器函数。

    • 在FreeRTOS中,有个RTOS守护任务(RTOS Daemon Task),该任务跟普通任务基本一样,不过守护任务的流程只有

      • 处理命令:从命令队列里取出命令、处理
      • 执行定时器的回调函数
        定时器的回调函数是在守护任务中被调用的,守护任务不是专为某个定时器服务的,它还要处理其他定时器。注意如下:
      • 回调函数要尽快实行,不能进入阻塞状态
      • 不要调用会导致阻塞的API函数,比如vTaskDelay()
      • 可以调用xQueueReceive()之类的函数,但是超时时间要设为0:即刻返回,不可阻塞

创建软件定时器

/* 使用动态分配内存的方法创建定时器
* pcTimerName:定时器名字, 用处不大, 尽在调试时用到
* xTimerPeriodInTicks: 周期, 以Tick为单位
* uxAutoReload: 类型, pdTRUE表示自动加载, pdFALSE表示一次性
* pvTimerID: 回调函数可以使用此参数, 比如分辨是哪个定时器
* pxCallbackFunction: 回调函数
* 返回值: 成功则返回TimerHandle_t, 否则返回NULL
*/
TimerHandle_t xTimerCreate( const char * const pcTimerName,
const TickType_t xTimerPeriodInTicks,
const UBaseType_t uxAutoReload,
void * const pvTimerID,
TimerCallbackFunction_t pxCallbackFunction );
/* 使用静态分配内存的方法创建定时器
* pcTimerName:定时器名字, 用处不大, 尽在调试时用到
* xTimerPeriodInTicks: 周期, 以Tick为单位
* uxAutoReload: 类型, pdTRUE表示自动加载, pdFALSE表示一次性
* pvTimerID: 回调函数可以使用此参数, 比如分辨是哪个定时器
* pxCallbackFunction: 回调函数
* pxTimerBuffer: 传入一个StaticTimer_t结构体, 将在上面构造定时器
* 返回值: 成功则返回TimerHandle_t, 否则返回NULL
*/
TimerHandle_t xTimerCreateStatic(const char * const pcTimerName,
TickType_t xTimerPeriodInTicks,
UBaseType_t uxAutoReload,
void * pvTimerID,
TimerCallbackFunction_t pxCallbackFunction,
StaticTimer_t *pxTimerBuffer );

回调函数类型

void ATimerCallback( TimerHandle_t xTimer );
typedef void (* TimerCallbackFunction_t)( TimerHandle_t xTimer );

删除软件定时器

/* 删除定时器
* xTimer: 要删除哪个定时器
* xTicksToWait: 超时时间
* 返回值: pdFAIL表示"删除命令"在xTicksToWait个Tick内无法写入队列
* pdPASS表示成功
*/
BaseType_t xTimerDelete( TimerHandle_t xTimer, TickType_t xTicksToWait );

启动、暂停、复位

  • 启动

    启动定时器就是设置它的状态为运行态(Running、Active)

    这些函数的xTicksToWait表示的是,把命令写入命令队列的超时时间。命令队列可能已经满了,无法马上把命令写入队列里,可以等待一会。

    xTicksToWait不是定时器本身的超时时间,不是定时器本身的"周期"。

    如果定时器已经被启动,但是它的函数尚未被执行,再次执行xTimerStart()函数相当于执行xTimerReset(),重新设定它的启动时间。

    /* 启动定时器
    * xTimer: 哪个定时器
    * xTicksToWait: 超时时间
    * 返回值: pdFAIL表示"启动命令"在xTicksToWait个Tick内无法写入队列
    * pdPASS表示成功
    */
    BaseType_t xTimerStart( TimerHandle_t xTimer, TickType_t xTicksToWait );
    /* 启动定时器(ISR版本)
    * xTimer: 哪个定时器
    * pxHigherPriorityTaskWoken: 向队列发出命令使得守护任务被唤醒,
    * 如果守护任务的优先级比当前任务的高,
    * 则"*pxHigherPriorityTaskWoken = pdTRUE",
    * 表示需要进行任务调度
    * 返回值: pdFAIL表示"启动命令"无法写入队列
    * pdPASS表示成功
    */
    BaseType_t xTimerStartFromISR( TimerHandle_t xTimer,
    BaseType_t *pxHigherPriorityTaskWoken );
  • 暂停

    停止定时器就是设置它的状态为冬眠(Dormant),让它不能运行

    /* 停止定时器
    * xTimer: 哪个定时器
    * xTicksToWait: 超时时间
    * 返回值: pdFAIL表示"停止命令"在xTicksToWait个Tick内无法写入队列
    * pdPASS表示成功
    */
    BaseType_t xTimerStop( TimerHandle_t xTimer, TickType_t xTicksToWait );
    /* 停止定时器(ISR版本)
    * xTimer: 哪个定时器
    * pxHigherPriorityTaskWoken: 向队列发出命令使得守护任务被唤醒,
    * 如果守护任务的优先级比当前任务的高,
    * 则"*pxHigherPriorityTaskWoken = pdTRUE",
    * 表示需要进行任务调度
    * 返回值: pdFAIL表示"停止命令"无法写入队列
    * pdPASS表示成功
    */
    BaseType_t xTimerStopFromISR( TimerHandle_t xTimer,
    BaseType_t *pxHigherPriorityTaskWoken );
  • 复位

    xTimerReset()函数

    • 让定时器的状态从冬眠态转换为运行态,相当于使用xTimerStart()函数
    • 如果定时器已经处于运行态,使用xTimerReset()函数就相当于重新确定超时时间。
    /* 复位定时器
    * xTimer: 哪个定时器
    * xTicksToWait: 超时时间
    * 返回值: pdFAIL表示"复位命令"在xTicksToWait个Tick内无法写入队列
    * pdPASS表示成功
    */
    BaseType_t xTimerReset( TimerHandle_t xTimer, TickType_t xTicksToWait );
    /* 复位定时器(ISR版本)
    * xTimer: 哪个定时器
    * pxHigherPriorityTaskWoken: 向队列发出命令使得守护任务被唤醒,
    * 如果守护任务的优先级比当前任务的高,
    * 则"*pxHigherPriorityTaskWoken = pdTRUE",
    * 表示需要进行任务调度
    * 返回值: pdFAIL表示"停止命令"无法写入队列
    * pdPASS表示成功
    */
    BaseType_t xTimerResetFromISR( TimerHandle_t xTimer,
    BaseType_t *pxHigherPriorityTaskWoken );

修改周期

/* 修改定时器的周期
* xTimer: 哪个定时器
* xNewPeriod: 新周期
* xTicksToWait: 超时时间, 命令写入队列的超时时间
* 返回值: pdFAIL表示"修改周期命令"在xTicksToWait个Tick内无法写入队列
* pdPASS表示成功
*/
BaseType_t xTimerChangePeriod( TimerHandle_t xTimer,
TickType_t xNewPeriod,
TickType_t xTicksToWait );
/* 修改定时器的周期
* xTimer: 哪个定时器
* xNewPeriod: 新周期
* pxHigherPriorityTaskWoken: 向队列发出命令使得守护任务被唤醒,
* 如果守护任务的优先级比当前任务的高,
* 则"*pxHigherPriorityTaskWoken = pdTRUE",
* 表示需要进行任务调度
* 返回值: pdFAIL表示"修改周期命令"在xTicksToWait个Tick内无法写入队列
* pdPASS表示成功
*/
BaseType_t xTimerChangePeriodFromISR( TimerHandle_t xTimer,
TickType_t xNewPeriod,
BaseType_t *pxHigherPriorityTaskWoken );

定时器ID

  • 用途:

    • 可以用来标记定时器,表示自己是什么定时器
    • 可以用来保存参数,给回调函数使用
  • 接口类型:

    • 更新ID:使用vTimerSetTimerID()函数

    • 查询ID:查询pvTimerGetTimerID()函数

      这两个函数不涉及命令队列,它们是直接操作定时器结构体

/* 获得定时器的ID
* xTimer: 哪个定时器
* 返回值: 定时器的ID
*/
void *pvTimerGetTimerID( TimerHandle_t xTimer );
/* 设置定时器的ID
* xTimer: 哪个定时器
* pvNewID: 新ID
* 返回值: 无
*/
void vTimerSetTimerID( TimerHandle_t xTimer, void *pvNewID );

中断处理

FreeRTOS中很多API函数都有两套:一套在任务中使用,另一套在ISR中使用。后者的函数名含有"FromISR"后缀。

为什么要引入两套API函数?(在任务中、在ISR中,这些函数的功能是有差别的)

  • 很多API函数会导致任务计入阻塞状态:

    • 运行这个函数的任务进入阻塞状态
    • 比如写队列时,如果队列已满,可以进入阻塞状态等待一会
  • ISR调用API函数时,ISR不是"任务",ISR不能进入阻塞状态

如果使用一套函数的话,则需要在函数内部进行判断,这样大量增加复杂代码,会更难以测试,并且不同平台内部框架也不一样,这也大大加大了代码的复杂度。

中断的延迟处理

在中断中处理内容,尽量要快,这是因为:

  • 其他低优先级的中断无法被处理:实时性无法保证。
  • 用户任务无法被执行:系统显得很卡顿。
  • 如果运行中断嵌套,这会更复杂,ISR越快执行约有助于中断嵌套。

如果这个硬件中断的处理,就是非常耗费时间呢?对于这类中断的处理就要分为2部分:

  • ISR:尽快做些清理、记录工作,然后触发某个任务
  • 任务:更复杂的事情放在任务中处理

资源管理

临界资源

要独占式地访问临界资源,有3种方法:

  • 公平竞争:比如使用互斥量,谁先获得互斥量谁就访问临界资源,这部分内容前面讲过。

  • 谁要跟我抢,我就灭掉谁:

    • 中断要跟我抢?我屏蔽中断
    • 其他任务要跟我抢?我禁止调度器,不运行任务切换

在任务中屏蔽中断

/* 在任务中,当前时刻中断是使能的
* 执行这句代码后,屏蔽中断
*/
taskENTER_CRITICAL();
/* 访问临界资源 */
/* 重新使能中断 */
taskEXIT_CRITICAL();

在ISR中屏蔽中断

void vAnInterruptServiceRoutine( void )
{
/* 用来记录当前中断是否使能 */
UBaseType_t uxSavedInterruptStatus;
/* 在ISR中,当前时刻中断可能是使能的,也可能是禁止的
* 所以要记录当前状态, 后面要恢复为原先的状态
* 执行这句代码后,屏蔽中断
*/
uxSavedInterruptStatus = taskENTER_CRITICAL_FROM_ISR();
/* 访问临界资源 */
/* 恢复中断状态 */
taskEXIT_CRITICAL_FROM_ISR( uxSavedInterruptStatus );
/* 现在,当前ISR可以被更高优先级的中断打断了 */
}

暂停调度器

/* 暂停调度器 */
void vTaskSuspendAll( void );
/* 恢复调度器
* 返回值: pdTRUE表示在暂定期间有更高优先级的任务就绪了
* 可以不理会这个返回值
*/
BaseType_t xTaskResumeAll( void );
posted @   能跑就行_NPJX  阅读(757)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律
点击右上角即可分享
微信分享提示