ESP32上的FreeRTOS学习(3)

看门狗

简介

看门狗其实就是一个定时器,从功能上说它可以让微控制器在程序发生意外(程序进入死循环或跑飞)的时候,能重新回复到系统刚上电状态,以保障系统出问题的时候可以重启一次。说的简单一点,看门狗就是能让程序出问题时能重新启动系统。

ESP32看门狗

ESP32有两个核心,每个核心都有对应的看门狗。当RTOS调度器开始工作后,为了保证至少有一个任务在运行,空闲任务被自动创建,占用最低优先级(0优先级),这就是IDLE任务。
而核心1上还运行着 loopback 任务,包括了 setup() 和 loop()。

  • core0:IDLE (优先级0)
  • core1:IDLE (优先级0)、loopback(优先级1)。

默认情况下只有 core0 的 IDLE任务开启了看门狗。

关闭看门狗

1
2
disableCore0WDT();
disableCore1WDT();

添加看门狗

1
2
esp_err_t esp_task_wdt_add(TaskHandle_t handle);
esp_task_wdt_add(NULL); // NULL代表本任务

喂狗

1
esp_task_wdt_reset();

队列

简介

FreeRTOS中的队列是一种用于实现【任务与任务】,【任务与中断】以及【中断与任务】之间的通信机制。
队列是一种FIFO操作的数据结构,入队操作就是把一个新的元素放进队尾(tail),出队操作就是从队头(front)取出一个元素。FreeRTOS中也支持把一个元素放到队头的操作,这个操作会覆盖之前队头的元素。

创建队列

1
QueueHandle_t My_Queue = xQueueCreate(10, sizeof(int));

入队

1
xQueueSend(My_Queue,&in,10);

当元素成功入队时返回pdPASS。
在队列已经满了的时候。如果需要等待,任务会因为调用这个函数而进入阻塞状态,直到队列非满而能让这个任务写入数据或者指定的阻塞时间过期,才会转变为就绪态。如果参数使用0,则当队列已经满了的时候,此函数立即返回而不阻塞。

出队

1
xQueueReceive(My_Queue,&out,10);

当成功从队列读取到元素时返回pdPASS。

信号量

简介

信号量其实就是队列的一种应用,信号量的各种操作都是在队列的基础上建立起来的。那么既然是在队列的基础上建立的,信号量一定具有和队列相同的属性。因此信号量也是为任务和任务、任务和中断之间通信做准备的,但是信号量一般用来进行资源管理和任务同步。因为信号量是一种共享资源,当它被创建之后,系统中所有任务和中断都能对信号量进行访问。同时也可以进行任务同步,即在一个任务(或中断)中告诉另一个任务它所等待的事件发生了,等到发生任务调度的时候,再切换到相应任务中,执行该事件发生的相关处理。

二进制信号量

二进制信号量看作只有一个项目(item)的队列,因此这个队列只能为空或满(因此称为二进制)。任务和中断使用队列无需关注谁控制队列—只需要知道队列是空还是满。利用这个机制可以在任务和中断之间同步。
轮询的方法会浪费CPU资源并且妨碍其它任务执行。更好的做法是任务的大部分时间处于阻塞状态(允许其它任务执行),直到某些事件发生该任务才执行。可以使用二进制信号量实现这种应用:当任务取信号量时,因为此时尚未发生特定事件,信号量为空,任务会进入阻塞状态;当外设需要维护时,触发一个中断服务例程,该中断服务仅仅给出信号量(向队列写数据)。任务只是取信号,并不需要归还,中断服务只是给信号。

创建二进制信号量

1
2
SemaphoreHandle_t MySema = NULL;  // 创建二进制信号量句柄
MySema = xSemaphoreCreateBinary(); // 创建二进制信号量

发送二进制信号量

1
xSemaphoreGive(MySema);

接收二进制信号量

1
xSemaphoreTake(MySema, timeout); // 接收成功-pdTRUE

计数信号量

二进制信号量可以被认为是长度为1的队列,计数信号量则可以被认为长度大于1的队列。此外,信号量使用者不必关心存储在队列中的数据,只需关心队列是否为空。

创建计数信号量

1
2
3
SemaphoreHandle_t MyCountSema = NULL;  // 创建计数信号量句柄
MyCountSema = xSemaphoreCreateCounting(10, 0); // 创建计数信号量
// 10为计数信号量最大值 0为初始值

发送计数信号量

1
xSemaphoreGive(MyCountSema);  // 计数信号量+1

接收计数信号量

1
xSemaphoreTake(MyCountSema, timeout); // 接收成功-pdTRUE

互斥信号量

互斥信号量其实是特殊的二值信号量,由于其特有的优先级继承机制从而使它更适用于简单互锁,也就是保护临界资源。用作互斥时,信号量创建后可用信号量个数应该是满的,任务在需要使用临界资源时,(临界资源是指任何时刻只能被一个任务访问的资源),先获取互斥信号量,使其变空,这样其他任务需要使用临界资源时就会因为无法获取信号量而进入阻塞,从而保证了临界资源的安全。
在操作系统中,我们使用信号量的很多时候是为了给临界资源建立一个标志,信号量表示了该临界资源被占用情况。这样,当一个任务在访问临界资源的时候,就会先对这个资源信息进行查询,从而在了解资源被占用的情况之后,再做处理,从而使得临界资源得到有效的保护。

创建互斥信号量

1
2
SemaphoreHandle_t xMuteMpu9250 = NULL;  // 创建互斥信号量句柄
xMuteMpu9250 = xSemaphoreCreateMutex(); // 创建互斥信号量

获取信号量

1
xSemaphoreTake(xMuteMpu9250,time_out);

释放信号量

1
xSemaphoreGive(xMuteMpu9250);

注意

互斥量用来保护资源。为了访问资源,任务必须先获取互斥量。任务 A 想获取资源,首先它使用 API 函数 xSemaphoreTake() 获取信号量,成功获取到信号量后,任务 A 就持有了互斥量,可以安全的访问资源。期间任务 B 开始执行,它也想访问资源,任务 B 也要先获得信号量,但是信号量此时是无效的,任务 B 进入阻塞状态。当任务 A 执行完成后,使用 API 函数 xSemaphoreGive() 释放信号量。之后任务 B 解除阻塞,任务 B 使用 API 函数 xSemaphoreTake() 获取并得到信号量,任务 B 可以访问资源。
互斥量与二进制信号量最大的不同是:互斥量具有优先级继承机制。也就是说,如果一个互斥量正在被一个低优先级任务使用,此时一个高优先级企图获取这个互斥量,高优先级任务会因为得不到互斥量而进入阻塞状态,正在使用互斥量的低优先级任务会临时将自己的优先级提升,提升后的优先级与与进入阻塞状态的高优先级任务相同。这个优先级提升的过程叫做优先级继承。
这个机制用于确保高优先级任务进入阻塞状态的时间尽可能短,以及将已经出现的“优先级翻转”影响降低到最小。
互斥量不可以用在中断服务程序中,这是因为:

  1. 互斥量具有优先级继承机制,只有在任务中获取或给出互斥才有意义。
  2. 中断不能因为等待互斥量而阻塞。

全局变量、信号量、队列

这三者都是可以解决任务间通信的问题,不过使用时要注意场景需求,选用恰当的方式实现。
在多任务的操作系统或者大型的工程项目中,尽量不使用过多的全局变量,而是使用信号量和消息队列等方式来进行进程间通信。因为:

  • 信号量和消息队列等可以让 RTOS 内核有效地管理任务,而全局变量无法做到,任务的超时等机制需要用户自己实现。
  • 使用了全局变量就要防止多任务的访问冲突,而使用信号量和消息队列则处理好了这个问题,用户无需担心。