1. 1. FreeRTOS概述与体验
    1. 1.1. FreeRTOS主要内容
    2. 1.2. 数据类型
    3. 1.3. 变量名
    4. 1.4. 函数名
    5. 1.5. 宏名
  2. 2. 内存管理
    1. 2.1. 五种内存管理方法
      1. 2.1.1. heap_1
      2. 2.1.2. heap_2
        1. 2.1.2.1. Q:这里的最佳匹配是怎么操作的?
      3. 2.1.3. heap_3
        1. 2.1.3.1. Q: heap_3线程安全吗?
      4. 2.1.4. heap_4
        1. 2.1.4.1. Q:这里的首次匹配是怎么操作的?
      5. 2.1.5. heap_5
    2. 2.2. heap相关的函数
      1. 2.2.1. pvPortMalloc/vPortFree
      2. 2.2.2. xPortGetFreeHeapSize
      3. 2.2.3. xPortGetMinimumEverFreeHeapSize
  3. 3. 任务管理
    1. 3.1. 任务创建
    2. 3.2. 任务删除
    3. 3.3. 任务优先级与Tick
    4. 3.4. 任务状态
    5. 3.5. Delay函数
    6. 3.6. 空闲任务及其钩子函数
    7. 3.7. 调度算法
  4. 4. 同步互斥与通信
  5. 5. 队列
    1. 5.1. 特性
    2. 5.2. 队列函数
      1. 5.2.1. 创建队列
      2. 5.2.2. 复位
      3. 5.2.3. 删除
      4. 5.2.4. 写队列
      5. 5.2.5. 读队列
      6. 5.2.6. 查询
      7. 5.2.7. 覆盖/偷看
  6. 6. 信息量
    1. 6.1. 特性
    2. 6.2. 函数
      1. 6.2.1. 创建
      2. 6.2.2. 删除
      3. 6.2.3. give/take
  7. 7. 互斥值
    1. 7.1. 特性
    2. 7.2. 函数
      1. 7.2.1. 创建
      2. 7.2.2. 其他函数
    3. 7.3. 优先级反转
    4. 7.4. 递归锁
      1. 7.4.1. 死锁
      2. 7.4.2. 函数
  8. 8. 事件组
    1. 8.1. 概念
    2. 8.2. 函数
      1. 8.2.1. 创建
      2. 8.2.2. 删除
      3. 8.2.3. 设置事件
      4. 8.2.4. 等待事件
      5. 8.2.5. 同步点
  9. 9. 任务通知
    1. 9.1. 特性
      1. 9.1.1. 优势
      2. 9.1.2. 限制
      3. 9.1.3. 通知状态和通知值
    2. 9.2. 任务通知的使用
      1. 9.2.1. 函数
  10. 10. 软件定时器
    1. 10.1. 特性
    2. 10.2. 软件定时器的上下文
      1. 10.2.1. 守护任务
      2. 10.2.2. 回调函数
    3. 10.3. 函数
      1. 10.3.1. 创建
      2. 10.3.2. 删除
      3. 10.3.3. 启动/停止
    4. 10.4. 复位
    5. 10.5. 修改周期
    6. 10.6. 定时器ID
    7. 10.7. 一般使用配置
  11. 11. 中断管理
    1. 11.1. 特性
    2. 11.2. 两套API函数列表
    3. 11.3. 怎么切换任务
    4. 11.4. 中断与任务间的通信
  12. 12. 资源管理
    1. 12.1. 屏蔽中断
      1. 12.1.1. 任务中屏蔽中断
      2. 12.1.2. 在ISR中屏蔽中断
    2. 12.2. 暂停调度器
  13. 13. 调试方法
    1. 13.1. 调试
      1. 13.1.1. 打印
      2. 13.1.2. 断言
      3. 13.1.3. Trace
      4. 13.1.4. Malloc Hook函数
      5. 13.1.5. 栈溢出Hook函数
    2. 13.2. 优化
      1. 13.2.1. 栈使用情况
      2. 13.2.2. 使用运行时间统计
      3. 13.2.3. 涉及的宏定义头
      4. 13.2.4. 函数说明
  14. 14. 来源

主要学习韦东山FreeRTOS的操作。

FreeRTOS概述与体验

FreeRTOS主要内容

FreeRTOS/Source下的文件 作用
task.c 任务操作
list.c 列表
queue.c 提供队列操作、信号量操作
timer.c 软件定时功能
event_groups.c 通过事件组功能

数据类型

  • TickType_t:
    • FreeRTOS配置了一个周期性的时钟中断:Tick Interrupt
    • 每发生一次中断,中断次数累加,这被称为tick count
    • tick count这个变量的类型就是TickType_t
    • TickType_t可以是16位的,也可以是32位的
    • FreeRTOSConfig.h中定义configUSE_16_BIT_TICKS时,TickType_t就是uint16_t
    • 否则TickType_t就是uint32_t
  • BaseType_t:
    • 这是该架构最高效的数据类型
    • 32位架构中,它就是uint32_t
    • 16位架构中,它就是uint16_t
    • 8位架构中,它就是uint8_t
    • BaseType_t通常用作简单的返回值的类型,还有逻辑值,比如pdTRUE/pdFALSE

变量名

变量名前缀 含义
c char
s int16_t, short
l int32_t, long
x BaseType_t,结构体等
u unsigned
p 指针
uc uint8_t,unsigned char
pc char指针

函数名

函数名前缀 含义
vTaskPrioritySet 返回值类型:void,在task.c中定义
xQueueReceive 返回值类型:BaseType_t,在queue.c中定义
pvTimerGetTimerID 返回值类型:pointer to void,在timer.c中定义

宏名

宏的前缀 在哪个文件定义
port (比如portMAX_DELAY) portable.h或portmacro.h
task (比如taskENTER_CRITICAL()) task.h
pd (比如pdTRUE) projdefs.h
config (比如configUSE_PREEMPTION) FreeRTOSConfig.h
err (比如errQUEUE_FULL) projdefs.h

一般的宏定义

pdTRUE 1
pdFALSE 0
pdPASS 1
pdFAIL 0

内存管理

FreeRTOS的内存管理包括静态内存管理动态内存管理

  • 堆:heap,由程序员自己malloc一块空间,用完后free标记为"空闲"
  • 栈:stack,函数调用时局部变量保存在栈中,当前程序环境也是保存在栈中。

五种内存管理方法

文件 优点 缺点
heap_1.c 分配简单,时间确定,没有碎片 只分配,不回收
heap_2.c 动态分配,最佳匹配 有碎片、时间不懂
heap_3.c 调用标准库函数 速度慢、时间不定
heap_4.c 相邻的空闲碎片可合并 时间不定
heap_5.c 在heap_4.c基础上支持分隔的内存块 时间不定

FreeRTOS在创建任务时,需要2个内核对象:task control block(TCB)、stack

heap_1

  • 实现了pvPortMalloc,并没有实现vPortFree

  • 如果程序不需要删除内核对象,可以使用它

heap_2

相较于heap_1

  • Heap_2使用最佳匹配算法(best fit)来分配内存
  • 支持vPortFree

Q:这里的最佳匹配是怎么操作的?

举例,一个5 10 15内存的空间,此时需要存入一个6大小的数据,那么就存放在10里面,变成5 4 15;后续又需要存放一个4大小的数据,就变成5 0 15。

heap_3

  • 使用标准C库里的malloc、free函数

Q: heap_3线程安全吗?

C库里的malloc、free函数并非线程安全的,heap_3中先暂停FreeRTOS的调度器,再去调用这些函数,使用这种方法实现了线程安全

heap_4

  • heap_4使用首次适应算法(first fit)来分配内存。它还会把相邻的空闲内存合并为一个更大的空闲内存,这有助于较少内存的碎片问题。

Q:这里的首次匹配是怎么操作的?

举例,一个10 5 3内存的空间,此时需要存入一个6大小的数据,那么就存放在10里面,变成4 5 3;后续又需要存放一个3大小的数据,就变成1 5 3。

heap_5

  • 相比于heap_4,Heap_5并不局限于管理一个大数组:它可以管理多块、分隔开的内存

heap相关的函数

pvPortMalloc/vPortFree

1
2
void * pvPortMalloc( size_t xWantedSize );	// 分配内存,如果分配内存不成功,则返回值为NULL。
void vPortFree( void * pv ); // 释放内存

作用:分配内存、释放内存

如果分配内存不成功,则返回值为NULL。

xPortGetFreeHeapSize

1
size_t xPortGetFreeHeapSize( void );

作用:当前还有多少空闲内存,heap_3中无法使用。

xPortGetMinimumEverFreeHeapSize

1
size_t xPortGetMinimumEverFreeHeapSize( void );

作用:空闲内存容量的最小值。

注意:只有heap_4、heap_5支持此函数

任务管理

任务状态:就绪态、阻塞态、挂起态、进行态

任务创建

1
2
3
4
5
6
7
8
BaseType_t xTaskCreate( TaskFunction_t pxTaskCode, // 函数指针, 任务函数
const char * const pcName, // 任务的名字,不用太在意
const configSTACK_DEPTH_TYPE usStackDepth, // 栈大小,单位为word,10表示40字节
void * const pvParameters, // 调用任务函数时传入的参数
UBaseType_t uxPriority, // 优先级
TaskHandle_t * const pxCreatedTask ); // 任务句柄, 以后使用它来操作这个任务
// 成功:pdPASS;
// 失败:errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY(失败原因只有内存不足)

同时创建两个程序,同优先级下,先执行最后创建的。

1
2
3
xTaskCreate(vTask1, "Task 1", 1000, NULL, 1, NULL);
xTaskCreate(vTask2, "Task 2", 1000, NULL, 1, NULL);
// 在执行程序时,先执行vTask2,再到Task1,任务本质是一种链表结构。

多个任务可以使用同一个函数,怎么体现它们的差别?

  • 栈不同
  • 创建任务时可以传入不同的参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static const char *pcTextForTask1 = "T1 run\r\n";
static const char *pcTextForTask2 = "T2 run\r\n";

int main( void )
{
prvSetupHardware();

xTaskCreate(vTaskFunction, "Task 1", 1000, (void *)pcTextForTask1, 1, NULL);
xTaskCreate(vTaskFunction, "Task 2", 1000, (void *)pcTextForTask2, 1, NULL);

/* 启动调度器 */
vTaskStartScheduler();

/* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
return 0;
}

任务删除

1
void vTaskDelete( TaskHandle_t xTaskToDelete );

Q:怎么删除任务?

  1. 自杀:vTaskDelete(NULL)
  2. 被杀:别的任务执行vTaskDelete(pvTaskCode),pvTaskCode是自己的句柄
  3. 杀人:执行vTaskDelete(pvTaskCode),pvTaskCode是别的任务的句柄

任务优先级与Tick

高优先级的任务先运行,可选择0 ~ (configMAX_PRIORITIES – 1)

  • FreeRTOS会确保最高优先级的、可运行的任务立马执行
  • 对于相同优先级的可执行任务,轮流执行

对于相同优先级的,通过Tick(滴答)中断实现,但它并不精确

1
2
3
4
vTaskDelay(2);  // 等待2个Tick,假设configTICK_RATE_HZ=100, Tick周期时10ms, 等待20ms

// 还可以使用pdMS_TO_TICKS宏把ms转换为tick
vTaskDelay(pdMS_TO_TICKS(100)); // 等待100ms

可以使用uxTaskPriorityGet来获得任务的优先级

1
UBaseType_t uxTaskPriorityGet( const TaskHandle_t xTask );

使用vTaskPrioritySet 来设置任务的优先级

1
2
void vTaskPrioritySet( TaskHandle_t xTask,				//具体任务
UBaseType_t uxNewPriority ); //新的优先级

任务状态

Delay函数

  • vTaskDelay至少等待指定个数的Tick Interrupt才能变为就绪状态
  • vTaskDelayUntil等待到指定的绝对时刻,才能变为就绪态。
1
2
3
4
5
6
7
8
void vTaskDelay( const TickType_t xTicksToDelay ); /* xTicksToDelay: 等待多少给Tick */

/* pxPreviousWakeTime: 上一次被唤醒的时间
* xTimeIncrement: 要阻塞到(pxPreviousWakeTime + xTimeIncrement)
* 单位都是Tick Count
*/
BaseType_t xTaskDelayUntil( TickType_t * const pxPreviousWakeTime,
const TickType_t xTimeIncrement );

空闲任务及其钩子函数

空闲任务(Idle任务)的作用:释放被删除的任务的内存

在使用vTaskStartScheduler() 函数来创建、启动调度器时,这个函数内部会创建空闲任务:

  • 空闲任务优先级为0:它不能阻碍用户任务运行
  • 空闲任务要么处于就绪态,要么处于运行态,永远不会阻塞

如果使用vTaskDelete() 来删除任务,那么你就要确保空闲任务有机会执行,否则就无法释放被删除任务的内存

可以添加一个空闲任务的钩子函数(Idle Task Hook Functions),空闲任务的循环每执行一次,就会调用一次钩子函数。

钩子函数作用

  • 执行一些低优先级的、后台的、需要连续执行的函数
  • 测量系统的空闲时间:空闲任务能被执行就意味着所有的高优先级任务都停止了,所以测量空闲任务占据的时间,就可以算出处理器占用率。
  • 让系统进入省电模式:空闲任务能被执行就意味着没有重要的事情要做,当然可以进入省电模式了。

钩子函数限制

  • 不能导致空闲任务进入阻塞状态、暂停状态
  • 如果你会使用vTaskDelete() 来删除任务,那么钩子函数要非常高效地执行。如果空闲任务移植卡在钩子函数里的话,它就无法释放内存。

如何使用钩子函数

FreeRTOS\Source\tasks.c中设置configUSE_IDLE_HOOK = 1,实现vApplicationIdleHook函数。

调度算法

配置项 A B C D E
configUSE_PREEMPTION 1 1 1 1 0
configUSE_TIME_SLICING 1 1 0 0 x
configIDLE_SHOULD_YIELD 1 0 1 0 x
说明 常用 很少用 很少用 很少用 几乎不用
  • A:可抢占+时间片轮转+空闲任务让步
  • B:可抢占+时间片轮转+空闲任务不让步
  • C:可抢占+非时间片轮转+空闲任务让步
  • D:可抢占+非时间片轮转+空闲任务不让步
  • E:合作调度

同步互斥与通信

能实现同步、互斥的内核方法有:任务通知(task notification)、队列(queue)、事件组(event group)、信号量(semaphoe)、互斥量(mutex)

  • 队列:
    • 里面可以放任意数据,可以放多个数据
    • 任务、ISR都可以放入数据;任务、ISR都可以从中读出数据
  • 事件组:
    • 一个事件用一bit表示,1表示事件发生了,0表示事件没发生
    • 可以用来表示事件、事件的组合发生了,不能传递数据
    • 有广播效果:事件或事件的组合发生了,等待它的多个任务都会被唤醒
  • 信号量:
    • 核心是"计数值"
    • 任务、ISR释放信号量时让计数值加1
    • 任务、ISR获得信号量时,让计数值减1
  • 任务通知:
    • 核心是任务的TCB里的数值
    • 会被覆盖
    • 发通知给谁?必须指定接收任务
    • 只能由接收任务本身获取该通知
  • 互斥量:
    • 数值只有0或1
    • 谁获得互斥量,就必须由谁释放同一个互斥量

队列

特性

  • 队列可以包含若干个数据:队列中有若干项,这被称为"长度"(length)
  • 创建队列时就要指定长度、数据大小(大小固定)
  • 数据的操作采用先进先出的方法(FIFO,First In First Out):写数据时放到尾部,读数据时从头部读
  • 也可以强制写队列头部:覆盖头部数据

队列传输数据两种方法:

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

多个任务在等待同一个队列的数据。当队列中有数据时,哪个任务会进入就绪态

  • 优先级最高的任务
  • 如果大家的优先级相同,那等待时间最久的任务会进入就绪态

队列函数

创建队列

  • 动态分配内存:xQueueCreate,队列的内存在函数内部动态分配
1
QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength, UBaseType_t uxItemSize );
参数 说明
uxQueueLength 队列长度,最多能存放多少个数据(item)
uxItemSize 每个数据(item)的大小:以字节为单位
返回值 非0:成功,返回句柄,以后使用句柄来操作队列 NULL:失败,因为内存不足
  • 静态分配内存:xQueueCreateStatic,队列的内存要事先分配好
1
2
3
QueueHandle_t xQueueCreateStatic(UBaseType_t uxQueueLength,
UBaseType_t uxItemSize, uint8_t *pucQueueStorageBuffer,
StaticQueue_t *pxQueueBuffer);
参数 说明
uxQueueLength 队列长度,最多能存放多少个数据(item)
uxItemSize 每个数据(item)的大小:以字节为单位
pucQueueStorageBuffer 如果uxItemSize非0,pucQueueStorageBuffer必须指向一个uint8_t数组, 此数组大小至少为"uxQueueLength * uxItemSize"
pxQueueBuffer 必须执行一个StaticQueue_t结构体,用来保存队列的数据结构
返回值 非0:成功,返回句柄,以后使用句柄来操作队列 NULL:失败,因为pxQueueBuffer为NULL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 示例代码
#define QUEUE_LENGTH 10
#define ITEM_SIZE sizeof( uint32_t )

// xQueueBuffer用来保存队列结构体
StaticQueue_t xQueueBuffer;

// ucQueueStorage 用来保存队列的数据
// 大小为:队列长度 * 数据大小
uint8_t ucQueueStorage[QUEUE_LENGTH * ITEM_SIZE];

void vATask( void *pvParameters )
{
QueueHandle_t xQueue1;
// 创建队列: 可以容纳QUEUE_LENGTH个数据,每个数据大小是ITEM_SIZE
xQueue1 = xQueueCreateStatic( QUEUE_LENGTH,
ITEM_SIZE,ucQueueStorage,
&xQueueBuffer);
}

复位

使用过程中可以调用xQueueReset()把队列恢复为初始状态

1
2
3
4
/* pxQueue : 复位哪个队列;
* 返回值: pdPASS(必定成功)
*/
BaseType_t xQueueReset( QueueHandle_t pxQueue);

删除

删除队列的函数为vQueueDelete()只能删除使用动态方法创建的队列,它会释放内存

1
void vQueueDelete( QueueHandle_t xQueue );

写队列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* 往队列尾部写入数据,如果没有空间,阻塞时间为xTicksToWait */
BaseType_t xQueueSendToBack(QueueHandle_t xQueue,
const void *pvItemToQueue,
TickType_t xTicksToWait);
/* 等同于xQueueSendToBack,往队列尾部写入数据,如果没有空间,阻塞时间为xTicksToWait */
BaseType_t xQueueSend(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);
参数 说明
xQueue 队列句柄,要写哪个队列
pvItemToQueue 数据指针,这个数据的值会被复制进队列, 复制多大的数据?在创建队列时已经指定了数据大小
xTicksToWait 如果队列满则无法写入新数据,可以让任务进入阻塞状态, xTicksToWait表示阻塞的最大时间(Tick Count)。 如果被设为0,无法写入数据时函数会立刻返回; 如果被设为portMAX_DELAY,则会一直阻塞直到有空间可写
返回值 pdPASS:数据成功写入了队列 errQUEUE_FULL:写入失败,因为队列满了。

读队列

1
2
3
4
5
6
7
8
// 在任务中使用
BaseType_t xQueueReceive( QueueHandle_t xQueue,
void * const pvBuffer,
TickType_t xTicksToWait);
// 在ISR中使用
BaseType_t xQueueReceiveFromISR(QueueHandle_t xQueue,
void *pvBuffer,
BaseType_t *pxTaskWoken);
参数 说明
xQueue 队列句柄,要读哪个队列
pvBuffer bufer指针,队列的数据会被复制到这个buffer 复制多大的数据?在创建队列时已经指定了数据大小
xTicksToWait 果队列空则无法读出数据,可以让任务进入阻塞状态, xTicksToWait表示阻塞的最大时间(Tick Count)。 如果被设为0,无法读出数据时函数会立刻返回; 如果被设为portMAX_DELAY,则会一直阻塞直到有数据可写
返回值 pdPASS:从队列读出数据入 errQUEUE_EMPTY:读取失败,因为队列空了。

查询

可以查询队列中有多少个数据、有多少空余空间

1
2
3
4
5
/*返回队列中可用数据的个数*/
UBaseType_t uxQueueMessagesWaiting( const QueueHandle_t xQueue );

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

覆盖/偷看

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

1
2
3
4
5
6
7
8
9
10
/* 覆盖队列
* 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()

1
2
3
4
5
6
7
8
9
10
11
12
13
/* 偷看队列
* 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,
TickType_t xTicksToWait);

信息量

特性

  • 信号:起通知作用
  • 量:还可以用来表示资源的数量
    • 当"量"只有0、1两个取值时,它就是"二进制信号量"(Binary Semaphores)
    • 当"量"没有限制时,它就是"计数型信号量"(Counting Semaphores)
  • 支持的动作:"give"给出资源,计数值加1;"take"获得资源,计数值减1

函数

创建

创建二进制信号量

1
2
3
4
5
6
7
8
9
10
11
/* 创建一个二进制信号量,返回它的句柄。
* 此函数内部会分配信号量结构体
* 返回值: 返回句柄,非NULL表示成功
*/
SemaphoreHandle_t xSemaphoreCreateBinary(void);

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

创建计数型信号量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* 创建一个计数型信号量,返回它的句柄。
* 此函数内部会分配信号量结构体
* 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 );

删除

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

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

give/take

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 在任务中使用
// xSemaphore信号量句柄,释放哪个信号量
BaseType_t xSemaphoreGive( SemaphoreHandle_t xSemaphore );

// xSemaphore信号量句柄,获取哪个信号量
//如果无法马上获得信号量,阻塞一会。 0:不阻塞,马上返回
// portMAX_DELAY: 一直阻塞直到成功
// 其他值: 阻塞的Tick个数
BaseType_t xSemaphoreTake(SemaphoreHandle_t xSemaphore, TickType_t xTicksToWait);

// 在ISR中使用
// xSemaphore信号量句柄,释放哪个信号量;
// 如果释放信号量导致更高优先级的任务变为了就绪态,则*pxHigherPriorityTaskWoken = pdTRUE
BaseType_t xSemaphoreGiveFromISR(SemaphoreHandle_t xSemaphore,
BaseType_t *pxHigherPriorityTaskWoken);
// xSemaphore信号量句柄,获取哪个信号量
BaseType_t xSemaphoreTakeFromISR(SemaphoreHandle_t xSemaphore,
BaseType_t *pxHigherPriorityTaskWoken);

互斥值

特性

FreeRTOS的互斥锁,并没有在代码上实现谁上锁,就只能由谁开锁,只是约定。

  • 互斥量初始值为1
  • 任务A想访问临界资源,先获得并占有互斥量,然后开始访问
  • 任务B也想访问临界资源,也要先获得互斥量:被别人占有了,于是阻塞
  • 任务A使用完毕,释放互斥量;任务B被唤醒、得到并占有互斥量,然后开始访问临界资源
  • 任务B使用完毕,释放互斥量

函数

要想使用互斥量,需要在配置文件FreeRTOSConfig.h中定义:

1
#define configUSE_MUTEXES 1

创建

1
2
3
4
5
6
7
8
9
/* 创建一个互斥量,返回它的句柄。
* 此函数内部会分配互斥量结构体
* 返回值: 返回句柄,非NULL表示成功 */
SemaphoreHandle_t xSemaphoreCreateMutex( void );

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

其他函数

互斥量不能在ISR中使用,ISR需要快速运行,不能阻塞太久。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* 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);

优先级反转

假设任务A、B都想使用串口,A优先级比B低:

  • 任务A获得了串口的互斥量
  • 任务B也想使用串口,它将会阻塞、等待A释放互斥量
  • 高优先级的任务,被低优先级的任务延迟,这被称为"优先级反转"(priority inversion)

互斥量可以通过"优先级继承",临时提高有锁且低优先级的程序,可以很大程度**解决"优先级反转"**的问题

递归锁

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

死锁

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

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

解决这样问题可以使用递归锁

  • 任务A获得递归锁M后,它还可以多次去获得这个锁
  • "take"了N次,要"give"N次,这个锁才会被释放

函数

1
2
3
4
5
6
7
8
9
10
/* 创建一个递归锁,返回它的句柄,此函数内部会分配互斥量结构体 
* 返回值: 返回句柄,非NULL表示成功 */
SemaphoreHandle_t xSemaphoreCreateRecursiveMutex( void );

/* 释放 */
BaseType_t xSemaphoreGiveRecursive( SemaphoreHandle_t xSemaphore );

/* 获得 */
BaseType_t xSemaphoreTakeRecursive(SemaphoreHandle_t xSemaphore,
TickType_t xTicksToWait);

事件组

概念

  • 事件组的每一位表示一个事件
  • 每一位事件的含义由程序员决定,比如:Bit0表示用来串口是否就绪,Bit1表示按键是否被按下
  • 这些位,值为1表示事件发生了,值为0表示事件没发生
  • 一个或多个任务、ISR都可以去写这些位;一个或多个任务、ISR都可以去读这些位
  • 可以等待某一位、某些位中的任意一个,也可以等待多位

事件组用一个整数来表示,高8位留给内核使用,只能用其他的位来表示事件

  • 如果configUSE_16_BIT_TICKS是1,那么这个整数就是16位的,低8位用来表示事件

  • 如果configUSE_16_BIT_TICKS是0,那么这个整数就是32位的,低24位用来表示事件

  • configUSE_16_BIT_TICKS是用来表示Tick Count的,怎么会影响事件组?这只是基于效率来考虑

    • 如果configUSE_16_BIT_TICKS是1,就表示该处理器使用16位更高效,所以事件组也使用16位
    • 如果configUSE_16_BIT_TICKS是0,就表示该处理器使用32位更高效,所以事件组也使用32位

函数

创建

1
2
3
4
5
6
7
8
9
10
11
/* 创建一个事件组,返回它的句柄。
* 此函数内部会分配事件组结构体
* 返回值: 返回句柄,非NULL表示成功
*/
EventGroupHandle_t xEventGroupCreate( void );

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

删除

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

设置事件

  • 在任务中使用xEventGroupSetBits()
  • 在ISR中使用xEventGroupSetBitsFromISR()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* 设置事件组中的位
* 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函数不是直接去设置事件组,而是给一个FreeRTOS后台任务(daemon task)发送队列数据,由这个任务来设置事件组。

等待事件

使用xEventGroupWaitBits来等待事件,可以等待某一位、某些位中的任意一个,也可以等待多位;等到期望的事件后,还可以清除某些位。

1
2
3
4
5
EventBits_t xEventGroupWaitBits( EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToWaitFor,
const BaseType_t xClearOnExit,
const BaseType_t xWaitForAllBits,
TickType_t xTicksToWait );
参数 说明
xEventGroup 等待哪个事件组?
uxBitsToWaitFor 等待哪些位?哪些位要被测试?
xWaitForAllBits 怎么测试?是"AND"还是"OR"? pdTRUE: 等待的位,全部为1; pdFALSE: 等待的位,某一个为1即可
xClearOnExit 函数提出前是否要清除事件? pdTRUE: 清除uxBitsToWaitFor指定的位 pdFALSE: 不清除
xTicksToWait 如果期待的事件未发生,阻塞多久。 可以设置为0:判断后即刻返回; 可设置为portMAX_DELAY:一定等到成功才返回; 可以设置为期望的Tick Count,一般用pdMS_TO_TICKS()把ms转换为Tick Count
返回值 返回的是事件值, 如果期待的事件发生了,返回的是"非阻塞条件成立"时的事件值; 如果是超时退出,返回的是超时时刻的事件值。

同步点

使用xEventGroupSync()函数可以同步多个任务:

1
2
3
4
EventBits_t xEventGroupSync(    EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToSet,
const EventBits_t uxBitsToWaitFor,
TickType_t xTicksToWait );
参数 说明
xEventGroup 哪个事件组
uxBitsToSet 要设置哪些事件,已经完成了哪些事件? 比如0x05(二进制为0101)会导致事件组的bit0,bit2被设置为1
uxBitsToWaitFor 等待那个位、哪些位? 比如0x15(二级制10101),表示要等待bit0,bit2,bit4都为1
xTicksToWait 如果期待的事件未发生,阻塞多久。 可以设置为0:判断后即刻返回; 可设置为portMAX_DELAY:一定等到成功才返回; 可以设置为期望的Tick Count,一般用pdMS_TO_TICKS()把ms转换为Tick Count
返回值 返回的是事件值, 如果期待的事件发生了,返回的是"非阻塞条件成立"时的事件值; 如果是超时退出,返回的是超时时刻的事件值。

任务通知

特性

优势

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

限制

  • 不能发送数据给ISR: ISR并没有任务结构体,所以无法使用任务通知的功能给ISR发送数据。但是ISR可以使用任务通知的功能,发数据给任务。
  • 数据只能给该任务独享: 使用队列、信号量、事件组时,数据保存在这些结构体中,其他任务、ISR都可以访问这些数据。使用任务通知时,数据存放入目标任务中,只有它可以访问这些数据。 在日常工作中,这个限制影响不大。因为很多场合是从多个数据源把数据发给某个任务,而不是把一个数据源的数据发给多个任务。
  • 无法缓冲数据 :使用队列时,假设队列深度为N,那么它可以保持N个数据。 使用任务通知时,任务结构体中只有一个任务通知值,只能保持一个数据。
  • 无法广播给多个任务 :使用事件组可以同时给多个任务发送事件。 使用任务通知,只能发个一个任务。
  • 如果发送受阻,发送方无法进入阻塞状态等待: 假设队列已经满了,使用xQueueSendToBack()给队列发送数据时,任务可以进入阻塞状态等待发送完成。 使用任务通知时,即使对方无法接收数据,发送方也无法阻塞等待,只能即刻返回错误。

通知状态和通知值

每个人物都有一个结构体:TCB,里面存在2个成员:

  • 一个是uint8_t类型,用来表示通知状态
  • 一个是uint32_t类型,用来表示通知
1
2
3
4
5
6
7
8
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种取值:

  • taskNOT_WAITING_NOTIFICATION:任务没有在等待通知
  • taskWAITING_NOTIFICATION:任务在等待通知
  • taskNOTIFICATION_RECEIVED:任务接收到了通知,也被称为pending(有数据了,待处理)

任务通知的使用

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

函数

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
// xTaskToNotify和xTaskHandle为任务句柄(创建任务时得到),给哪个任务发通知
BaseType_t xTaskNotifyGive( TaskHandle_t xTaskToNotify );

void vTaskNotifyGiveFromISR( TaskHandle_t xTaskHandle, BaseType_t *pxHigherPriorityTaskWoken );

uint32_t ulTaskNotifyTake( BaseType_t xClearCountOnExit, TickType_t xTicksToWait );

// xTaskNotify 函数功能更强大,可以使用不同参数实现各类功能,比如:

// 让接收任务的通知值加一:这时xTaskNotify()等同于xTaskNotifyGive()
// 设置接收任务的通知值的某一位、某些位,这就是一个轻量级的、更高效的事件组
// 把一个新值写入接收任务的通知值:上一次的通知值被读走后,写入才成功。这就是轻量级的、长度为1的队列
// 用一个新值覆盖接收任务的通知值:无论上一次的通知值是否被读走,覆盖都成功。类似xQueueOverwrite()函数,这就是轻量级的邮箱

BaseType_t xTaskNotify( TaskHandle_t xTaskToNotify, uint32_t ulValue, eNotifyAction eAction );

BaseType_t xTaskNotifyFromISR( TaskHandle_t xTaskToNotify,
uint32_t ulValue,
eNotifyAction eAction,
BaseType_t *pxHigherPriorityTaskWoken );

// 使用xTaskNotifyWait()函数!它比ulTaskNotifyTake()更复杂:
// 可以让任务等待(可以加上超时时间),等到任务状态为"pending"(也就是有数据)
// 还可以在函数进入、退出时,清除通知值的指定位

BaseType_t xTaskNotifyWait( uint32_t ulBitsToClearOnEntry,
uint32_t ulBitsToClearOnExit,
uint32_t *pulNotificationValue,
TickType_t xTicksToWait );

eNotifyAction参数说明

eNotifyAction取值 说明
eNoAction 仅仅是更新通知状态为"pending",未使用ulValue。 这个选项相当于轻量级的、更高效的二进制信号量。
eSetBits 通知值 = 原来的通知值 | ulValue,按位或。 相当于轻量级的、更高效的事件组。
eIncrement 通知值 = 原来的通知值 + 1,未使用ulValue。 相当于轻量级的、更高效的二进制信号量、计数型信号量。 相当于xTaskNotifyGive()函数。
eSetValueWithoutOverwrite 不覆盖。 如果通知状态为"pending"(表示有数据未读), 则此次调用xTaskNotify不做任何事,返回pdFAIL。 如果通知状态不是"pending"(表示没有新数据), 则:通知值 = ulValue。
eSetValueWithOverwrite 覆盖。 无论如何,不管通知状态是否为"pendng", 通知值 = ulValue。

软件定时器

特性

跟闹钟类似,只响一次/每隔多少时间就自动操作

两种状态

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

软件定时器的上下文

守护任务

FreeRTOS中有一个Tick中断,软件定时器基于Tick来运行,但不在Tick中断中执行定时器函数,而是在RTOS Damemon Task中执行,即守护任务

守护任务的优先级:configTIMER_TASK_PRIORITY

定时器命令队列的长度:configTIMER_QUEUE_LENGTH

回调函数

如下是定时器的回调函数原型:

1
void ATimerCallback( TimerHandle_t xTimer );

定时器的回调函数是在守护任务中被调用的,守护任务不是专为某个定时器服务的,它还要处理其他定时器。

因此回调任务不能影响其他人:

  • 回调函数要尽快实行,不能进入阻塞状态
  • 不要调用会导致阻塞的API函数,比如vTaskDelay()
  • 可以调用xQueueReceive()之类的函数,但是超时时间要设为0:即刻返回,不可阻塞

函数

创建

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
/* 使用动态分配内存的方法创建定时器
* 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 );

回调函数的类型是:

1
2
3
void ATimerCallback( TimerHandle_t xTimer );

typedef void (* TimerCallbackFunction_t)( TimerHandle_t xTimer );

删除

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

启动/停止

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

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

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
/* 启动定时器
* 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 );

/* 停止定时器
* 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 );

创建定时器时,设置了它的周期(period)。xTimerStart()函数是用来启动定时器。假设调用xTimerStart()的时刻是tX,定时器的周期是n,那么在tX+n时刻定时器的回调函数被调用。

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

复位

从定时器的状态转换图可以知道,使用xTimerReset()函数可以让定时器的状态从冬眠态转换为运行态,相当于使用xTimerStart()函数。

如果定时器已经处于运行态,使用xTimerReset()函数就相当于重新确定超时时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* 复位定时器
* 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 );

修改周期

从定时器的状态转换图可以知道,使用xTimerChangePeriod()函数,处理能修改它的周期外,还可以让定时器的状态从冬眠态转换为运行态。

修改定时器的周期时,会使用新的周期重新计算它的超时时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* 修改定时器的周期
* 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

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

它的初始值在创建定时器时由xTimerCreate()这类函数传入。

  • 更新ID:使用vTimerSetTimerID()函数
  • 查询ID:查询pvTimerGetTimerID()函数
1
2
3
4
5
6
7
8
9
10
/* 获得定时器的ID
* xTimer: 哪个定时器
* 返回值: 定时器的ID */
void *pvTimerGetTimerID( TimerHandle_t xTimer );

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

一般使用配置

要使用定时器,需要做些准备工作:

1
2
3
4
5
6
7
8
9
10
11
/* 1. 工程中 */
// 添加 timer.c

/* 2. 配置文件FreeRTOSConfig.h中 */
#define configUSE_TIMERS 1 /* 使能定时器 */
#define configTIMER_TASK_PRIORITY 31 /* 守护任务的优先级, 尽可能高一些 */
#define configTIMER_QUEUE_LENGTH 5 /* 命令队列长度 */
#define configTIMER_TASK_STACK_DEPTH 32 /* 守护任务的栈大小 */

/* 3. 源码中 */
#include "timers.h"

中断管理

特性

中断流程,ISR要尽量快

  • 保存现场:Task1被打断,需要先保存Task1的运行环境,比如各类寄存器的值
  • 分辨中断、调用处理函数(这个函数就被称为ISR,interrupt service routine)
  • 恢复现场:继续运行Task1,或者运行其他优先级更高的任务

ISR的优先级高于任务:即使是优先级最低的中断,它的优先级也高于任务。任务只有在没有中断的情况下,才能执行。

Q:为什么要引入两套API函数?

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

  2. ISR调用API函数时,ISR不是"任务",ISR不能进入阻塞状态

  3. 所以,在任务中、在ISR中,这些函数的功能是有差别的

两套API函数列表

类型 在任务中 在ISR中
队列(queue) xQueueSendToBack xQueueSendToBackFromISR
xQueueSendToFront xQueueSendToFrontFromISR
xQueueReceive xQueueReceiveFromISR
xQueueOverwrite xQueueOverwriteFromISR
xQueuePeek xQueuePeekFromISR
信号量(semaphore) xSemaphoreGive xSemaphoreGiveFromISR
xSemaphoreTake xSemaphoreTakeFromISR
事件组(event group) xEventGroupSetBits xEventGroupSetBitsFromISR
xEventGroupGetBits xEventGroupGetBitsFromISR
任务通知(task notification) xTaskNotifyGive vTaskNotifyGiveFromISR
xTaskNotify xTaskNotifyFromISR
软件定时器(software timer) xTimerStart xTimerStartFromISR
xTimerStop xTimerStopFromISR
xTimerReset xTimerResetFromISR
xTimerChangePeriod xTimerChangePeriodFromISR

怎么切换任务

FreeRTOS的ISR函数中,使用两个宏进行任务切换:

1
2
3
portEND_SWITCHING_ISR( xHigherPriorityTaskWoken ); 	//汇编实现

portYIELD_FROM_ISR( xHigherPriorityTaskWoken ); //C语言实现

中断与任务间的通信

队列、信号量、互斥量、事件组、任务通知等等方法,都可使用。

要注意的是,在ISR中使用的函数要有"FromISR"后缀。

资源管理

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

  • 公平竞争:比如使用互斥量

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

    • 中断要跟我抢?我屏蔽中断

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

屏蔽中断

  • 任务中使用:taskENTER_CRITICA()/taskEXIT_CRITICAL()
  • ISR中使用:taskENTER_CRITICAL_FROM_ISR()/taskEXIT_CRITICAL_FROM_ISR()

任务中屏蔽中断

1
2
3
4
5
6
7
8
9
/* 在任务中,当前时刻中断是使能的
* 执行这句代码后,屏蔽中断
*/
taskENTER_CRITICAL();

/* 访问临界资源 */

/* 重新使能中断 */
taskEXIT_CRITICAL();

taskENTER_CRITICA()taskEXIT_CRITICAL()之间:

  • 低优先级的中断被屏蔽了:优先级低于等于configMAX_SYSCALL_INTERRUPT_PRIORITY

  • 高优先级的中断可以产生:优先级高于configMAX_SYSCALL_INTERRUPT_PRIORITY

    • 但是,这些中断ISR里,不允许使用FreeRTOS的API函数
  • 任务调度依赖于中断、依赖于API函数,所以:这两段代码之间,不会有任务调度产生

在ISR中屏蔽中断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void vAnInterruptServiceRoutine( void )
{
/* 用来记录当前中断是否使能 */
UBaseType_t uxSavedInterruptStatus;

/* 在ISR中,当前时刻中断可能是使能的,也可能是禁止的
* 所以要记录当前状态, 后面要恢复为原先的状态
* 执行这句代码后,屏蔽中断 */
uxSavedInterruptStatus = taskENTER_CRITICAL_FROM_ISR();

/* 访问临界资源 */

/* 恢复中断状态 */
taskEXIT_CRITICAL_FROM_ISR( uxSavedInterruptStatus );
/* 现在,当前ISR可以被更高优先级的中断打断了 */
}

taskENTER_CRITICA_FROM_ISR()taskEXIT_CRITICAL_FROM_ISR()之间:

  • 低优先级的中断被屏蔽了:优先级低于、等于configMAX_SYSCALL_INTERRUPT_PRIORITY

  • 高优先级的中断可以产生:优先级高于configMAX_SYSCALL_INTERRUPT_PRIORITY

    • 但是,这些中断ISR里,不允许使用FreeRTOS的API函数
  • 任务调度依赖于中断、依赖于API函数,所以:这两段代码之间,不会有任务调度产生

暂停调度器

如果有别的任务来跟你竞争临界资源,可以把中断关掉:这当然可以禁止别的任务运行,但是这代价太大了。它会影响到中断的处理。

1
2
3
4
5
6
7
/* 暂停调度器 */
void vTaskSuspendAll( void );

/* 恢复调度器
* 返回值: pdTRUE表示在暂定期间有更高优先级的任务就绪了
* 可以不理会这个返回值 */
BaseType_t xTaskResumeAll( void );

示例操作,下面可以递归使用

1
2
3
4
5
vTaskSuspendScheduler();

/* 访问临界资源 */

xTaskResumeScheduler();

调试方法

调试

  • 打印
  • 断言:configASSERT
  • Trace
  • Hook函数(回调函数)

打印

1
int fputc( int ch, FILE *f );

断言

1
#define configASSERT(x)  if (!x) while(1);

Trace

trace宏 描述
traceTASK_INCREMENT_TICK(xTickCount) 当tick计数自增之前此宏函数被调用。参数xTickCount当前的Tick值,它还没有增加。
traceTASK_SWITCHED_OUT() vTaskSwitchContext中,把当前任务切换出去之前调用此宏函数。
traceTASK_SWITCHED_IN() vTaskSwitchContext中,新的任务已经被切换进来了,就调用此函数。
traceBLOCKING_ON_QUEUE_RECEIVE(pxQueue) 当正在执行的当前任务因为试图去读取一个空的队列、信号或者互斥量而进入阻塞状态时,此函数会被立即调用。参数pxQueue保存的是试图读取的目标队列、信号或者互斥量的句柄,传递给此宏函数。
traceBLOCKING_ON_QUEUE_SEND(pxQueue) 当正在执行的当前任务因为试图往一个已经写满的队列或者信号或者互斥量而进入了阻塞状态时,此函数会被立即调用。参数pxQueue保存的是试图写入的目标队列、信号或者互斥量的句柄,传递给此宏函数。
traceQUEUE_SEND(pxQueue) 当一个队列或者信号发送成功时,此宏函数会在内核函数xQueueSend(),xQueueSendToFront(),xQueueSendToBack(),以及所有的信号give函数中被调用,参数pxQueue是要发送的目标队列或信号的句柄,传递给此宏函数。
traceQUEUE_SEND_FAILED(pxQueue) 当一个队列或者信号发送失败时,此宏函数会在内核函数xQueueSend(),xQueueSendToFront(),xQueueSendToBack(),以及所有的信号give函数中被调用,参数pxQueue是要发送的目标队列或信号的句柄,传递给此宏函数。
traceQUEUE_RECEIVE(pxQueue) 当读取一个队列或者接收信号成功时,此宏函数会在内核函数xQueueReceive()以及所有的信号take函数中被调用,参数pxQueue是要接收的目标队列或信号的句柄,传递给此宏函数。
traceQUEUE_RECEIVE_FAILED(pxQueue) 当读取一个队列或者接收信号失败时,此宏函数会在内核函数xQueueReceive()以及所有的信号take函数中被调用,参数pxQueue是要接收的目标队列或信号的句柄,传递给此宏函数。
traceQUEUE_SEND_FROM_ISR(pxQueue) 当在中断中发送一个队列成功时,此函数会在xQueueSendFromISR()中被调用。参数pxQueue是要发送的目标队列的句柄。
traceQUEUE_SEND_FROM_ISR_FAILED(pxQueue) 当在中断中发送一个队列失败时,此函数会在xQueueSendFromISR()中被调用。参数pxQueue是要发送的目标队列的句柄。
traceQUEUE_RECEIVE_FROM_ISR(pxQueue) 当在中断中读取一个队列成功时,此函数会在xQueueReceiveFromISR()中被调用。参数pxQueue是要发送的目标队列的句柄。
traceQUEUE_RECEIVE_FROM_ISR_FAILED(pxQueue) 当在中断中读取一个队列失败时,此函数会在xQueueReceiveFromISR()中被调用。参数pxQueue是要发送的目标队列的句柄。
traceTASK_DELAY_UNTIL() 当一个任务因为调用了vTaskDelayUntil()进入了阻塞状态的前一刻此宏函数会在vTaskDelayUntil()中被立即调用。
traceTASK_DELAY() 当一个任务因为调用了vTaskDelay()进入了阻塞状态的前一刻此宏函数会在vTaskDelay中被立即调用。

Malloc Hook函数

编程时,一般的逻辑错误都容易解决。难以处理的是内存越界、栈溢出等。

内存越界经常发生在堆的使用过程总:堆,就是使用malloc得到的内存。

并没有很好的方法检测内存越界,但是可以提供一些回调函数:

1
void vApplicationMallocFailedHook( void );

栈溢出Hook函数

在切换任务(vTaskSwitchContext)时调用taskCHECK_FOR_STACK_OVERFLOW来检测栈是否溢出,如果溢出会调用:

1
void vApplicationStackOverflowHook( TaskHandle_t xTask, char * pcTaskName );

怎么判断栈溢出?

方法1:

  • 当前任务被切换出去之前,它的整个运行现场都被保存在栈里,这时很可能就是它对栈的使用到达了峰值。
  • 这方法很高效,但是并不精确
  • 比如:任务在运行过程中调用了函数A大量地使用了栈,调用完函数A后才被调度。

方法2:

  • 创建任务时,它的栈被填入固定的值,比如:0xa5
  • 检测栈里最后16字节的数据,如果不是0xa5的话表示栈即将、或者已经被用完了
  • 没有方法1快速,但是也足够快
  • 能捕获几乎所有的栈溢出
  • 为什么是几乎所有?可能有些函数使用栈时,非常凑巧地把栈设置为0xa5:几乎不可能

优化

栈使用情况

在创建任务时分配了栈,可以填入固定的数值比如0xa5,以后可以使用以下函数查看"栈的高水位",也就是还有多少空余的栈空间:

1
UBaseType_t uxTaskGetStackHighWaterMark( TaskHandle_t xTask );

原理:从栈底往栈顶逐个字节地判断,它们的值持续是0xa5就表示它是空闲的。

使用运行时间统计

涉及的宏定义头

1
2
3
#define configGENERATE_RUN_TIME_STATS 1
#define configUSE_TRACE_FACILITY 1
#define configUSE_STATS_FORMATTING_FUNCTIONS 1

函数说明

  • uxTaskGetSystemState:获得任务的统计信息
1
2
3
UBaseType_t uxTaskGetSystemState( TaskStatus_t * const pxTaskStatusArray,
const UBaseType_t uxArraySize,
uint32_t * const pulTotalRunTime );
参数 描述
pxTaskStatusArray 指向一个TaskStatus_t结构体数组,用来保存任务的统计信息。 有多少个任务?可以用uxTaskGetNumberOfTasks()来获得。
uxArraySize 数组大小、数组项个数,必须大于或等于uxTaskGetNumberOfTasks()
pulTotalRunTime 用来保存当前总的运行时间(更快的定时器),可以传入NULL
返回值 传入的pxTaskStatusArray数组,被设置了几个数组项。 注意:如果传入的uxArraySize小于uxTaskGetNumberOfTasks(),返回值就是0
  • vTaskList :获得任务的统计信息,形式为可读的字符串。注意,pcWriteBuffer必须足够大。
1
void vTaskList( signed char *pcWriteBuffer );

  • vTaskGetRunTimeStats:获得任务的运行信息,形式为可读的字符串。注意,pcWriteBuffer必须足够大。
1
void vTaskGetRunTimeStats( signed char *pcWriteBuffer );

来源

第一章 FreeRTOS概述与体验 | 百问网

百问网《FreeRTOS入门与工程实践-基于STM32F103》教程-基于DShanMCU-103(STM32F103) | 百问网