RTOS基础
9-13~9-20
实时操作系统(Real-time operating system, RTOS),又称即时操作系统,它会按照排序运行、管理系统资源,并为开发应用程序提供一致的基础。
实时操作系统与一般的操作系统相比,最大的特色就是“实时性”,如果有一个任务需要执行,实时操作系统会马上(在较短时间内)执行该任务,不会有较长的延时。这种特性保证了各个任务的及时执行。
RTOS是一种通用的任务管理框架,用于控制任务的运行和任务之间的交互,保证事件得到实时处理。
RTOS的三要素:实时性 + 操作系统 + 嵌入式
FreeRTOS资料下载
https://sourceforge.net/projects/freertos/files/FreeRTOS/
裸机移植到stm32
https://blog.youkuaiyun.com/qq_61672347/article/details/125529482?spm=1001.2014.3001.5502
文档结构(stm32f103)
RTOS核心功能
内存分配
第一个任务
新建任务并运行
实现
void task2(void *pvParameters){
printf("%s\n", "task2");
//删除任务
vTaskDelete(NULL);
}
void Task1Function(void * param)
{
BaseType_t ret;
while (1)
{
printf("%s\n", param);
//创建任务2
ret = xTaskCreate(task2, "task2", 100, NULL, 2, NULL);
if(ret != pdPASS){
printf("task2 failed\n");
}
// 如果不休眠的话, Idle任务无法得到执行
// Idel任务会清理任务2使用的内存
// 如果不休眠则Idle任务无法执行, 最后内存耗尽
//相对延时
vTaskDelay(100);
}
}
int main( void )
{
TaskHandle_t xHandleTask1;
const char *taskTest1 = "task1";
#ifdef DEBUG
debug();
#endif
prvSetupHardware();
printf("Hello, world!\r\n");
//创建任务
xTaskCreate(Task1Function, "Task1", 100, (void *)taskTest1, 1, &xHandleTask1);
//xTaskCreate(Task2Function, "Task2", 100, NULL, 1, NULL);
//xTaskCreateStatic(Task3Function, "Task3", 100, NULL, 1, xTask3Stack, &xTask3TCB);
//启动调度器
vTaskStartScheduler();
/* Will only get here if there was not enough heap space to create the
idle task. */
return 0;
}
用图表示
tick值
优先级
同种优先级与不同优先级
任务1、任务2:优先级相同,都是1
任务3:优先级最高,是2
修改优先级
void task2(void *pvParameters){
printf("%s\n", "task2");
vTaskDelete(NULL);
}
void Task1Function(void * param)
{
BaseType_t ret;
while (1)
{
printf("%s\n", param);
ret = xTaskCreate(task2, "task2", 100, NULL, 2, NULL);
if(ret != pdPASS){
printf("task2 failed\n");
}
// 如果不休眠的话, Idle任务无法得到执行
// Idel任务会清理任务2使用的内存
// 如果不休眠则Idle任务无法执行, 最后内存耗尽
//等待100个Tick,假设configTICK_RATE_HZ=100, Tick周期时10ms, 等待1000ms
vTaskDelay(100);
}
}
TaskHandle_t xHandleTask3;
void Task2Function(void * param)
{
UBaseType_t uxPriority;
//得到自己的优先级
uxPriority = uxTaskPriorityGet(NULL);
while (1)
{
printf("%s\n", "task2 run");
//提升任务三优先级,任务三立即执行
vTaskPrioritySet(xHandleTask3, (uxPriority+1));
}
}
void Task3Function(void * param)
{
UBaseType_t uxPriority;
//得到自己的优先级
uxPriority = uxTaskPriorityGet(NULL);
while (1)
{
printf("%s\n", "task3 run");
vTaskPrioritySet(xHandleTask3, (uxPriority-2));
}
}
int main( void )
{
TaskHandle_t xHandleTask1;
const char *taskTest1 = "task1";
#ifdef DEBUG
debug();
#endif
prvSetupHardware();
printf("Hello, world!\r\n");
xTaskCreate(Task2Function, "Task2", 100, NULL, 2, NULL);
xTaskCreate(Task3Function, "Task3", 100, NULL, 1, &xHandleTask3);
/* Start the scheduler. */
vTaskStartScheduler();
/* Will only get here if there was not enough heap space to create the
idle task. */
return 0;
}
状态
运行态与非运行态
非运行态状态转换:
延时函数
- vTaskDelay:至少等待指定个数的Tick Interrupt才能变为就绪状态
- vTaskDelayUntil:等待到指定的绝对时刻,才能变为就绪态。
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 );
/* 任务中 第一次调用函数,需要将
* 初始化进任务的循环体的中, 在以后的运行中, 会被自动更新
*/
想演示500ms,则可以使用:
vTaskDelay(500/portTICK_RATE_MS);
宏定义
#ifndef pdMS_TO_TICKS
#define pdMS_TO_TICKS( xTimeInMs ) ( ( TickType_t ) ( ( ( TickType_t ) ( xTimeInMs ) * ( TickType_t ) configTICK_RATE_HZ ) / ( TickType_t ) 1000U ) )
#endif
可以直接写成,也是500ms
vTaskDelay(pdMS_TO_TICKS(500));
函数使用与验证
int flag;
void Task1Function(void * param)
{
//宏pdMS_TO_TICKS()可用于将以毫秒为单位指定的时间转换为以ticks为单位指定的时间
const TickType_t xDelay50ms = pdMS_TO_TICKS(50UL);
TickType_t xLastWakeTime;
int i;
while (1)
{
flag = 1;
for(i = 0; i < 4; i++){
printf("%s\n", "task1 run");
}
#if 0
vTaskDelay(xDelay50ms);
#else
vTaskDelayUntil(&xLastWakeTime, xDelay50ms);
#endif
}
}
void Task2Function(void * param)
{
while (1)
{
flag = 0;
printf("%s\n", "task2 run");
}
}
int main( void )
{
#ifdef DEBUG
debug();
#endif
prvSetupHardware();
printf("Hello, world!\r\n");
xTaskCreate(Task1Function, "Task1", 100, NULL, 2, NULL);
xTaskCreate(Task2Function, "Task2", 100, NULL, 1, NULL);
/* Start the scheduler. */
vTaskStartScheduler();
/* Will only get here if there was not enough heap space to create the
idle task. */
return 0;
}
使用逻辑分析仪,对flag变化
逻辑分析仪使用方法
flag变化
空闲任务与钩子函数
空闲任务简介
为什么必须要有空闲任务?一个良好的程序,它的任务都是事件驱动的:平时大部
分时间处于阻塞状态。有可能我们自己创建的所有任务都无法执行,但是调度器必须能找到一个可以运
行的任务:所以,我们要提供空闲任务。在使用 vTaskStartScheduler() 函数来创建、启动调度器
时,这个函数内部会创建空闲任务:
- 空闲任务优先级为0:它不能阻碍用户任务运行
- 空闲任务要么处于就绪态,要么处于运行态,永远不会阻塞
空闲任务的优先级为0,这以为着一旦某个用户的任务变为就绪态,那么空闲任务马上被切换出去,让
这个用户任务运行。在这种情况下,我们说用户任务"抢占"(pre-empt)了空闲任务,这是由调度器实现
的。
要注意的是:如果使用 vTaskDelete() 来删除任务,那么你就要确保空闲任务有机会执行,否则就无
法释放被删除任务的内存。
如何在空闲任务中使用钩子函数
钩子函数会被空闲任务每循环一次就自动调用一次。
通常空闲任务钩子函数被用于:
- 执行低优先级,后台或需要不停处理的功能代码。
- 测试出系统处理裕量(空闲任务只会在所有其它任务都不运行时才有机会执行,所以测量出空闲任务占用的处理时间就可以清楚的知道系统有多少富余的处理时间)。
- 将处理器配置到低功耗模式——提供一种自动省电方法,使得在没有任何应用功能
需要处理的时候,系统自动进入省电模式。
空闲任务钩子函数必须遵从以下规则:
- 绝不能阻塞或挂起。空闲任务只会在其它任务都不运行时才会被执行(除非有应用任务共享空闲任务优先级)。以任何方式阻塞空闲任务都可能导致没有任务能够进入运行态!
- 如果应用程序用到了 vTaskDelete() API 函数,则空闲钩子函数必须能够尽快返回。因为在任务被删除后,空闲任务负责回收内核资源。如果空闲任务一直运行在钩子函数中,则无法进行回收工作。
创建一个与空闲任务优先级相同的任务。
创建一个任务是最好的解决方法,但是这种方法会消耗更多的 RAM。
要使用空闲任务钩子函数首先要在 FreeRTOSConfig.h 中将宏 configUSE_IDLE_HOOK 改 为 1,然后编写空闲任务钩子函数 vApplicationIdleHook()。通常在空闲任务钩子函数中将处理器设置为低功耗模式来节省电能。这种低功耗的实现方法称之为通用低功耗模式。
调度算法
调度算法的行为主要体现在两方面:高优先级的任务先运行、同优先级的就绪态任务如何被选中。调度
算法要确保同优先级的就绪态任务,能"轮流"运行,策略是"轮转调度"(Round Robin Scheduling)。轮
转调度并不保证任务的运行时间是公平分配的,我们还可以细化时间的分配方法。
从3个角度统一理解多种调度算法:
可否抢占?高优先级的任务能否优先执行(配置项: configUSE_PREEMPTION)
可以:被称作"可抢占调度"(Pre-emptive),高优先级的就绪任务马上执行,下面再细化。
不可以:不能抢就只能协商了,被称作"合作调度模式"(Co-operative Scheduling)
当前任务执行时,更高优先级的任务就绪了也不能马上运行,只能等待当前任务主动让
出CPU资源。
其他同优先级的任务也只能等待:更高优先级的任务都不能抢占,平级的更应该老实点
可抢占的前提下,同优先级的任务是否轮流执行(配置项:configUSE_TIME_SLICING)
轮流执行:被称为"时间片轮转"(Time Slicing),同优先级的任务轮流执行,你执行一个时间
片、我再执行一个时间片
不轮流执行:英文为"without Time Slicing",当前任务会一直执行,直到主动放弃、或者被
高优先级任务抢占
在"可抢占"+"时间片轮转"的前提下,进一步细化:空闲任务是否让步于用户任务(配置项:
configIDLE_SHOULD_YIELD)
空闲任务低人一等,每执行一次循环,就看看是否主动让位给用户任务
空闲任务跟用户任务一样,大家轮流执行,没有谁更特殊
示例
Task1、Task2的优先级都是0,跟空闲任务一样,Task3优先级最高为2。程序
里定义了4个全局变量,当某个的任务执行时,对应的变量就被设为1,可以通过Keil的逻辑分析仪查看
任务切换情况
int flagIdleTaskrun = 0; // 空闲任务运行时flagIdleTaskrun=1
int flagTask1run = 0; // 任务1运行时flagTask1run=1
int flagTask2run = 0; // 任务2运行时flagTask2run=1
int flagTask3run = 0; // 任务3运行时flagTask3run=1
TickType_t xDelay10ms = pdMS_TO_TICKS(10);
void Task1Function(void * param)
{
while (1)
{
flagIdleTaskrun = 0;
flagTask1run = 1 ;
flagTask2run = 0;
flagTask3run = 0;
}
}
void Task2Function(void * param)
{
while (1)
{
flagIdleTaskrun = 0;
flagTask1run = 0;
flagTask2run = 1;
flagTask3run = 0;
}
}
void Task3Function(void * param)
{
while (1)
{
flagIdleTaskrun = 0;
flagTask1run = 0;
flagTask2run = 0;
flagTask3run = 1;
printf("T3\r\n");
vTaskDelay(xDelay10ms);
}
}
//空闲任务的钩子函数
void vApplicationIdleHook(void)
{
flagIdleTaskrun = 1;
flagTask1run = 0;
flagTask2run = 0;
flagTask3run = 0;
/* 故意加入打印让flagIdleTaskrun变为1的时间维持长一点 */
printf("Id\r\n");
}
int main( void )
{
#ifdef DEBUG
debug();
#endif
prvSetupHardware();
printf("Hello, world!\r\n");
xTaskCreate(Task1Function, "Task 1", 1000, NULL, 0, NULL);
xTaskCreate(Task2Function, "Task 2", 1000, NULL, 0, NULL);
xTaskCreate(Task3Function, "Task 3", 1000, NULL, 2, NULL);
/* Start the scheduler. */
vTaskStartScheduler();
/* Will only get here if there was not enough heap space to create the
idle task. */
return 0;
}
时间片不轮转:高优先级任务就绪时会引起任务切换,高优先级任务不再运行时也会引起任务切
换。可以看到任务3就绪后可以马上执行,它运行完毕后导致任务切换。其他时间没有任务切换,
可以看到任务1、任务2都运行了很长时间
时间片轮转:在Tick中断中会引起任务切换
时间片不轮转:高优先级任务就绪时会引起任务切换,高优先级任务不再运行时也会引起任务切
换。可以看到任务3就绪后可以马上执行,它运行完毕后导致任务切换。其他时间没有任务切换,
可以看到任务1、任务2都运行了很长时间
可以自己定义这些宏,如果不定义freertos.h文件中有预编译创建宏,都为1
#define configUSE_TIME_SLICING 1
#define configIDLE_SHOULD_YIELD 1
#define configUSE_PREEMPTION 1
同步与互斥
能实现同步、互斥的内核方法有:任务通知(task notification)、队列(queue)、事件组(event group)、
信号量(semaphoe)、互斥量(mutex)。
它们都有类似的操作方法:获取/释放、阻塞/唤醒、超时
图解各方式
队列
传输数据有两种方法
- 拷贝
- 引用
使用拷贝值的方法
- 局部变量的值可以发送到队列中,后续即使函数退出、局部变量被回收,也不会影响队列中的数据
- 无需分配buffer来保存数据,队列中有buffer
- 局部变量可以马上再次使用
- 发送任务、接收任务解耦:接收任务不需要知道这数据是谁的、也不需要发送任务来释放数据
- 如果数据实在太大,你还是可以使用队列传输它的地址
- 队列的空间有FreeRTOS内核分配,无需任务操心
- 对于有内存保护功能的系统,如果队列使用引用方法,也就是使用地址,必须确保双方任务对这个
地址都有访问权限。使用拷贝方法时,则无此限制:内核有足够的权限,把数据复制进队列、再把
数据复制出队列
队列函数
1.创建队列
QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength, UBaseType_t uxItemSize );
参数 说明
uxQueueLength 队列长度,最多能存放多少个数据(item)
uxItemSize 每个数据(item)的大小:以字节为单位
返回值 非0:成功,返回句柄,以后使用句柄来操作队列
NULL:失败,因为内存不足
QueueHandle_t xQueueCreateStatic(
UBaseType_t uxQueueLength,
UBaseType_t uxItemSize,
uint8_t *pucQueueStorageBuffer,
St
参数 说明
uxQueueLength 队列长度,最多能存放多少个数据(item)
uxItemSize 每个数据(item)的大小:以字节为单位
pucQueueStorageBuffer
如果uxItemSize非0,pucQueueStorageBuffer必须指向一个
uint8_t数组,
此数组大小至少为"uxQueueLength * uxItemSize"
pxQueueBuffer 必须执行一个StaticQueue_t结构体,用来保存队列的数据结构
返回值 非0:成功,返回句柄,以后使用句柄来操作队列
NULL:失败,因为pxQueueBuffer为NULL
2.复位
/* pxQueue : 复位哪个队列;
* 返回值: pdPASS(必定成功)
*/
BaseType_t xQueueReset( QueueHandle_t pxQueue);
3.删除
void vQueueDelete( QueueHandle_t xQueue );
4.写队列
/* 等同于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
);
参数类型说明
- xQueue 队列句柄,要写哪个队列
- pvItemToQueue数据指针,这个数据的值会被复制进队列,复制多大的数据?在创建队列时已经指定了数据大小
- xTicksToWait
如果队列满则无法写入新数据,可以让任务进入阻塞状态,xTicksToWait表示阻塞的最大时间(Tick Count)。如果被设为0,无法写入数据时函数会立刻返回;如果被设为portMAX_DELAY,则会一直阻塞直到有空间可写 - 返回值 pdPASS:数据成功写入了队列。errQUEUE_FULL:写入失败,因为队列满了。
5.读队列
BaseType_t xQueueReceive( QueueHandle_t xQueue,
void * const pvBuffer,
TickType_t xTicksToWait );
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:读取失败,因为队列空了。
6.查询
/*
* 返回队列中可用数据的个数
*/
UBaseType_t uxQueueMessagesWaiting( const QueueHandle_t xQueue );
/*
* 返回队列中可用空间的个数
*/
UBaseType_t uxQueueSpacesAvailable( const QueueHandle_t xQueue );
7.覆盖与偷看
如果想让队列中的数据供多方读取,也就是说读取时不要移除数据,要留给后来人。那么可以使用"窥
视",也就是 xQueuePeek() 或 xQueuePeekFromISR() 。这些函数会从队列中复制出数据,但是不移除
数据。这也意味着,如果队列中没有数据,那么"偷看"时会导致阻塞;一旦队列中有数据,以后每次"偷
看"都会成功。
/* 覆盖队列
* 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,
);
示例
QueueHandle_t xQueue;
static void vSendTask(void * param)
{
int32_t lvalueToSend;
BaseType_t xStatus;
lvalueToSend = (int32_t)param;
while (1)
{
/* 写队列
* xQueue: 写哪个队列
* &lvalueToSend: 写什么数据? 传入数据的地址, 会从这个地址把数据复制进队列
* 0: 不阻塞, 如果队列满的话, 写入失败, 立刻返回
*/
xStatus = xQueueSend(xQueue, &lvalueToSend, 0);
if(xStatus != pdPASS){
printf("%s\n", "could not send");
}
}
}
static void vReceiveTask(void * param)
{
int32_t lReceive;
BaseType_t xStatus;
const TickType_t wait = pdMS_TO_TICKS(100);
while (1)
{
/* 读队列
* xQueue: 读哪个队列
* &lReceive: 读到的数据复制到这个地址
* wait: 如果队列为空, 阻塞一会
*/
xStatus = xQueueReceive(xQueue, &lReceive, wait);
if(xStatus == pdPASS)
printf("Received = %d\n", lReceive);
else
printf("%s\n", "could not receive");
}
}
int main( void )
{
#ifdef DEBUG
debug();
#endif
prvSetupHardware();
//创建队列,长度为5,大小为4字节
xQueue = xQueueCreate(5, sizeof(int32_t));
if(xQueue != NULL){
xTaskCreate(vSendTask, "send1", 100, (void *)100, 1, NULL);
xTaskCreate(vSendTask, "send2", 100, (void *)200, 1, NULL);
xTaskCreate(vReceiveTask, "receive", 100, NULL, 2, NULL);
vTaskStartScheduler();
}
printf("Hello, world!\r\n");
/* Will only get here if there was not enough heap space to create the
idle task. */
return 0;
}
当有多个发送任务,通过同一个队列发出数据,接收任务如何分辨数据来源?数据本身带有"来源"信
息,比如写入队列的数据是一个结构体,结构体中的lDataSouceID用来表示数据来源
QueueHandle_t xQueue;
//定义枚举与结构体
typedef enum {
CAN,
HMI
}ID_t;
typedef struct {
ID_t eDataID;
int32_t lDataValue;
}Data_t;
//结构体数据
static const Data_t data[2] = {
{CAN, 100},{HMI, 200}
};
static void vSendTask(void * param)
{
BaseType_t xStatus;
const TickType_t wait = pdMS_TO_TICKS(100);
while (1)
{
/* 写队列
* xQueue: 写哪个队列
* ¶m: 写什么数据? 传入数据的地址, 会从这个地址把数据复制进队列
* wait: 队列满,阻塞一会
*/
xStatus = xQueueSend(xQueue, param, wait);
if(xStatus != pdPASS){
printf("%s\n", "could not send");
}
}
}
static void vReceiveTask(void * param)
{
BaseType_t xStatus;
Data_t lReceive;
const TickType_t wait = pdMS_TO_TICKS(100);
while (1)
{
/* 读队列
* xQueue: 读哪个队列
* &lReceive: 读到的数据复制到这个地址
* wait: 如果队列为空, 阻塞一会
*/
xStatus = xQueueReceive(xQueue, &lReceive, wait);
if(xStatus == pdPASS){
if(lReceive.eDataID == CAN)
printf("CAN : %d\n", lReceive.lDataValue);
else
printf("HMI : %d\n", lReceive.lDataValue);
}
else
printf("%s\n", "could not receive");
}
}
int main( void )
{
#ifdef DEBUG
debug();
#endif
prvSetupHardware();
//创建队列,长度为5,
xQueue = xQueueCreate(5, sizeof(Data_t));
if(xQueue != NULL){
xTaskCreate(vSendTask, "CAN", 100, (void *)&(data[0]), 2, NULL);
xTaskCreate(vSendTask, "HMI", 100, (void *)&(data[1]), 2, NULL);
xTaskCreate(vReceiveTask, "receive", 100, NULL, 1, NULL);
vTaskStartScheduler();
}
printf("Hello, world!\r\n");
/* Will only get here if there was not enough heap space to create the
idle task. */
return 0;
}
- t1:HMI是最后创建的最高优先级任务,它先执行,一下子向队列写入5个数据,把队列都写满了
- t2:队列已经满了,HMI任务再发起第6次写操作时,进入阻塞状态。这时CAN任务是最高优先级
的就绪态任务,它开始执行 - t3:CAN任务发现队列已经满了,进入阻塞状态;接收任务变为最高优先级的就绪态任务,它开始
运行 - t4:现在,HMI任务、CAN任务的优先级都比接收任务高,它们都在等待队列有空闲的空间;一旦
接收任务读出1个数据,会马上被抢占。被谁抢占?谁等待最久?HMI任务!所以在t4时刻,切换
到HMI任务。 - t5:HMI任务向队列写入第6个数据,然后再次阻塞,这是CAN任务已经阻塞很久了。接收任务变
为最高优先级的就绪态任务,开始执行。 - t6:现在,HMI任务、CAN任务的优先级都比接收任务高,它们都在等待队列有空闲的空间;一旦
接收任务读出1个数据,会马上被抢占。被谁抢占?谁等待最久?CAN任务!所以在t6时刻,切换
到CAN任务。 - t7:CAN任务向队列写入数据,因为仅仅有一个空间供写入,所以它马上再次进入阻塞状态。这时
HMI任务、CAN任务都在等待空闲空间,只有接收任务可以继续执行。
传输大数据
使用地址来间接传输数据时,这些数据放在RAM里
RAM的所有者、操作者,必须清晰明了
这块内存,就被称为"共享内存"。要确保不能同时修改RAM。比如,在写队列之前只有由发送者修改这块RAM,在读队列之后只能由接收者访问这块RAM。
RAM要保持可用
这块RAM应该是全局变量,或者是动态分配的内存。对于动然分配的内存,要确保它不能提前释放:要等到接收者用完后再释放。另外,不能是局部变量。
QueueHandle_t xQueue;
static char pcBuffer[50];
static void vSendTask(void * param)
{
char *buffer;
BaseType_t xStatus;
static int cnt = 0;
while (1)
{
/* 写队列
* xQueue: 写哪个队列
* &buffer: 传输地址
* 0: 不阻塞,直接返回
*/
sprintf(pcBuffer, "This num is %d", cnt++);
buffer = pcBuffer;
xStatus = xQueueSend(xQueue, &buffer, 0);
printf("%s\n", pcBuffer);
if(xStatus != pdPASS){
printf("%s\n", "could not send");
}
}
}
static void vReceiveTask(void * param)
{
BaseType_t xStatus;
char *buffer;
const TickType_t wait = pdMS_TO_TICKS(100);
while (1)
{
/* 读队列
* xQueue: 读哪个队列
* &buffer: 读到的数据复制到这个地址
* wait: 如果队列为空, 阻塞一会
*/
xStatus = xQueueReceive(xQueue, &buffer, wait);
if(xStatus == pdPASS){
printf("Get: %s\n", buffer);
}
else
printf("%s\n", "could not receive");
}
}
int main( void )
{
#ifdef DEBUG
debug();
#endif
prvSetupHardware();
//创建队列,长度为1,
xQueue = xQueueCreate(1, sizeof(char *));
if(xQueue != NULL){
xTaskCreate(vSendTask, "send", 1000, NULL, 1, NULL);
xTaskCreate(vReceiveTask, "receive", 1000, NULL, 2, NULL);
vTaskStartScheduler();
}
printf("Hello, world!\r\n");
/* Will only get here if there was not enough heap space to create the
idle task. */
return 0;
}
信号量
信号:起通知作用
量:还可以用来表示资源的数量
当"量"没有限制时,它就是"计数型信号量"(Counting Semaphores)
当"量"只有0、1两个取值时,它就是"二进制信号量"(Binary Semaphores)
支持的动作:"give"给出资源,计数值加1;"take"获得资源,计数值减1
创建信号量
二进制信号量 计数型信号量
动态创建 xSemaphoreCreateBinary
计数值初始值为0 xSemaphoreCreateCounting
vSemaphoreCreateBinary(过时了)
计数值初始值为1
静态创建 xSemaphoreCreateBinaryStatic xSemaphoreCreateCountingStatic
创建二进制信号量的函数原型如下:
/* 创建一个二进制信号量,返回它的句柄。
* 此函数内部会分配信号量结构体
* 返回值: 返回句柄,非NULL表示成功
*/
SemaphoreHandle_t xSemaphoreCreateBinary( void );
/* 创建一个二进制信号量,返回它的句柄。
* 此函数无需动态分配内存,所以需要先有一个StaticSemaphore_t结构体,并传入它的指针
* 返回值: 返回句柄,非NULL表示成功
*/
SemaphoreHandle_t xSemaphoreCreateBinaryStatic( StaticSemaphore_t *pxSemaphoreBuffer );
/* 创建一个计数型信号量,返回它的句柄。
* 此函数内部会分配信号量结构体
* 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 );
删除
对于动态创建的信号量,不再需要它们时,可以删除它们以回收内存。
vSemaphoreDelete可以用来删除二进制信号量、计数型信号量,函数原型如下:
/*
* xSemaphore: 信号量句柄,你要删除哪个信号量
*/
void vSemaphoreDelete( SemaphoreHandle_t xSemaphore );
give与take
在任务中使用
give: xSemaphoreGive
take: xSemaphoreTake
在ISR中使用
give:xSemaphoreGiveFromISR
take:xSemaphoreTakeFromISR
BaseType_t xSemaphoreGive( SemaphoreHandle_t xSemaphore );
/*xSemaphore 信号量句柄,释放哪个信号量
返回值 pdTRUE表示成功,
如果二进制信号量的计数值已经是1,再次调用此函数则返回失败;
如果计数型信号量的计数值已经是最大值,再次调用此函数则返回失败
*/
BaseType_t xSemaphoreGiveFromISR(
SemaphoreHandle_t xSemaphore,
BaseType_t *pxHigherPriorityTaskWoken
);
/*xSemaphore 信号量句柄,释放哪个信号量
pxHigherPriorityTaskWoken 如果释放信号量导致更高优先级的任务变为了就绪态,
则*pxHigherPriorityTaskWoken = pdTRUE
返回值 pdTRUE表示成功,
如果二进制信号量的计数值已经是1,再次调用此函数则返回失败;
如果计数型信号量的计数值已经是最大值,再次调用此函数则返回失败
*/
BaseType_t xSemaphoreTake(
SemaphoreHandle_t xSemaphore,
TickType_t xTicksToWait
);
/*xSemaphore 信号量句柄,获取哪个信号量
xTicksToWait 如果无法马上获得信号量,阻塞一会:
0:不阻塞,马上返回
portMAX_DELAY: 一直阻塞直到成功
其他值: 阻塞的Tick个数,可以使用pdMS_TO_TICKS()来指定阻塞时间为若干ms
返回值 pdTRUE表示成功
*/
BaseType_t xSemaphoreTakeFromISR(
SemaphoreHandle_t xSemaphore,
BaseType_t *pxHigherPriorityTaskWoken
);
/*xSemaphore 信号量句柄,获取哪个信号量
pxHigherPriorityTaskWoken 如果获取信号量导致更高优先级的任务变为了就绪态,
则*pxHigherPriorityTaskWoken = pdTRUE
返回值 pdTRUE表示成功
*/
A:发送任务优先级高,先执行。连续3次释放二进制信号量,只有第1次成功
B:发送任务进入阻塞态
C:接收任务得以执行,得到信号量,打印OK;再次去获得信号量时,进入阻塞状态
在发送任务的vTaskDelay退出之前,运行的是空闲任务:现在发送任务、接收任务都阻塞了
D:发送任务再次运行,连续3次释放二进制信号量,只有第1次成功
E:发送任务进入阻塞态
F:接收任务被唤醒,得到信号量,打印OK;再次去获得信号量时,进入阻塞状态
计数型信号量
使用计数型信号量时,可以多次释放信号量;当信号量的技术值达到最大时,再次释放信号量就会出错。
如果信号量计数值为n,就可以连续n次获取信号量,第(n+1)次获取信号量就会阻塞或失败。
main函数中创建了一个计数型信号量,最大计数值为3,初始值计数值为0;然后创建2个任务:一个用于释放信号量,另一个用于获取信号量
出现问题,在freeRTOSconfig.h文件中定义该宏
#define configUSE_COUNTING_SEMAPHORES 1
示例
/* 二进制信号量句柄 */
SemaphoreHandle_t xCountingSemaphore;
static void vSendTask(void * param)
{
int i;
while (1)
{
/* 多次释放信号量 */
for(i = 0; i < 4; i++){
if(xSemaphoreGive(xCountingSemaphore) == pdTRUE){
printf("%s\n", "Give xCountingSemaphore OK");
}
else{
printf("%s\n", "Give xCountingSemaphore ERR");
}
}
vTaskDelay(pdMS_TO_TICKS(10));
}
}
static void vReceiveTask(void * param)
{
while (1)
{
/* 获取信号量,没有则一直阻塞 */
if(xSemaphoreTake(xCountingSemaphore, portMAX_DELAY) == pdTRUE){
printf("%s\n", "Take xCountingSemaphore OK");
}
else{
printf("%s\n", "Take xCountingSemaphore Delay");
}
}
}
int main( void )
{
#ifdef DEBUG
debug();
#endif
prvSetupHardware();
/* 创建记数型信号量 */
xCountingSemaphore = xSemaphoreCreateCounting(3, 0);
if(xCountingSemaphore != NULL){
xTaskCreate(vSendTask, "send", 1000, NULL, 2, NULL);
xTaskCreate(vReceiveTask, "receive", 1000, NULL, 1, NULL);
vTaskStartScheduler();
}
/* Will only get here if there was not enough heap space to create the
idle task. */
return 0;
}
互斥量
互斥量又称互斥信号量(本质是信号量),是一种特殊的二值信号量,它和信号量不同的是,它支持互斥量所有权、递归访问以及防止优先级翻转的特性,用于实现对临界资源的独占式处理。 任意时刻互斥量的状态只有两种,开锁或闭锁。当互斥量被任务持有时,该互斥量处于闭锁状态,这个任务获得互斥量的所有权。当该任务释放这个互斥量时,该互斥量处于开锁状态,任务失去该互斥量的所有权。当一个任务持有互斥量时,其他任务将不能再对该互斥量进行开锁或持有。持有该互斥量的任务也能够再次获得这个锁而不被挂起,这就是递归访问,也就是递归互斥量的特性,这个特性与一般的信号量有很大的不同,在信号量中,由于已经不存在可用的信号量,任务递归获取信号量时会发生主动挂起任务最终形成死锁
量:值为0、1
互斥:用来实现互斥访问
它的核心在于:谁上锁,就只能由谁开锁。
很奇怪的是,FreeRTOS的互斥锁,并没有在代码上实现这点:
即使任务A获得了互斥锁,任务B竟然也可以释放互斥锁。
谁上锁、谁释放:只是约定。
函数
要使用互斥量需要在FreeRTOSConfig.h文件中修改宏定义
#define configUSE_MUTEXES 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);
示例
/* 互斥量句柄 */
SemaphoreHandle_t xMutex;
static void vSendTask(void * param)
{
TickType_t wait = pdMS_TO_TICKS(10);
int num;
num = (int)param;
while (1)
{
/* 获取互斥量 加锁*/
xSemaphoreTake(xMutex, wait);
/* 输出语句长一点效果明显 */
printf("task%d xxxxxx ", num);
/* 释放互斥量 解锁 */
xSemaphoreGive(xMutex);
vTaskDelay(wait);
}
}
int main( void )
{
#ifdef DEBUG
debug();
#endif
prvSetupHardware();
/* 创建互斥量 */
xMutex = xSemaphoreCreateMutex();
if(xMutex != NULL){
xTaskCreate(vSendTask, "send1", 1000, (void*)1, 1, NULL);
xTaskCreate(vSendTask, "sand2", 1000, (void*)2, 1, NULL);
vTaskStartScheduler();
}
/* Will only get here if there was not enough heap space to create the
idle task. */
return 0;
}
优先级反转
示例
/* 使用二进制信号量演示 */
SemaphoreHandle_t xLock;
int flag1,flag2,flag3;
static void vTask1(void * param)
{
int i;
while (1)
{
flag1 = 1;
flag2 = 0;
flag3 = 0;
/* 获得锁 */
xSemaphoreTake(xLock, pdMS_TO_TICKS(10));
printf("%s\n", "vTask1 take long time");
for(i = 0; i < 500 ; i++){
flag1 = 1;
flag2 = 0;
flag3 = 0;
printf("a");
}
/* 释放锁 */
xSemaphoreGive(xLock);
vTaskDelay(pdMS_TO_TICKS(10));
}
}
static void vTask2(void * param)
{
flag1 = 0;
flag2 = 1;
flag3 = 0;
printf("%s\n", "vTask2 start");
/* 让task1运行 */
vTaskDelay(pdMS_TO_TICKS(20));
while (1)
{
flag1 = 0;
flag2 = 1;
flag3 = 0;
}
}
static void vTask3(void * param)
{
flag1 = 0;
flag2 = 0;
flag3 = 1;
printf("%s\n", "vTask3 start");
/* 让task2运行 */
vTaskDelay(pdMS_TO_TICKS(10));
while (1)
{
flag1 = 0;
flag2 = 0;
flag3 = 1;
printf("%s\n", "vTask3 run ");
/* 获得锁 */
xSemaphoreTake(xLock, portMAX_DELAY);
flag1 = 0;
flag2 = 0;
flag3 = 1;
/* 释放锁 */
xSemaphoreGive(xLock);
}
}
int main( void )
{
#ifdef DEBUG
debug();
#endif
prvSetupHardware();
/* 创建信号量 */
xLock = xSemaphoreCreateBinary();
if(xLock != NULL){
xTaskCreate(vTask1, "task1", 1000, (void*)1, 1, NULL);
xTaskCreate(vTask2, "task2", 1000, (void*)2, 2, NULL);
xTaskCreate(vTask3, "task3", 1000, (void*)3, 3, NULL);
vTaskStartScheduler();
}
/* Will only get here if there was not enough heap space to create the
idle task. */
return 0;
}
Task3优先级更高,阻塞结束需要执行,但是无法拿到信号量,无法运行。信号量被Task1拿到,Task2优先级高于Task1切一直运行,Task1得不到调度,一直阻塞。
为解决优先级反转问题,使用优先级继承解决该问题
等任务A释放互斥锁时,它就恢复为原来的优先级
互斥锁内部就实现了优先级的提升、恢复
将二进制信号量修改为互斥量可以减少影响
当task1运行时间较长
task3阻塞时间到,task3运行,因得不到互斥量进入阻塞,同优先级task1运行,直到释放互斥量,task3被唤醒,task3运行,task1优先级变回原来,task3一直加锁与解锁。
死锁
问题:任务A获得了互斥锁M,它调用一个库函数,库函数要去获取同一个互斥锁M,于是它阻塞:任务A休眠,等待任务A来释放互斥锁!死锁发生!
解决:使用递归锁
任务A获得递归锁M后,它还可以多次去获得这个锁
"take"了N次,要"give"N次,这个锁才会被释放
递归锁
/* 创建一个递归锁,返回它的句柄。
* 此函数内部会分配互斥量结构体
* 返回值: 返回句柄,非NULL表示成功
*/
SemaphoreHandle_t xSemaphoreCreateRecursiveMutex( void );
/* 释放 */
BaseType_t xSemaphoreGiveRecursive( SemaphoreHandle_t xSemaphore );
/* 获得 */
BaseType_t xSemaphoreTakeRecursive(
SemaphoreHandle_t xSemaphore,
TickType_t xTicksToWait
);
使用前需要使能configUSE_RECURSIVE_MUTEXES
#define configUSE_RECURSIVE_MUTEXES 1
示例
/* 递归锁句柄 */
SemaphoreHandle_t xRecursiveMutex;
void Task1(void *pvParameters) {
BaseType_t xStatus;
int i;
while (1) {
/* 获取递归锁 */
xStatus = xSemaphoreTakeRecursive(xRecursiveMutex, portMAX_DELAY);
printf("Task1 take xRecursiveMutex %s\n", (xStatus ? "sucess" : "failed"));
vTaskDelay(pdMS_TO_TICKS(100));
/* 再次获得递归锁并释放 */
for(i = 0; i < 5; i++){
xSemaphoreTakeRecursive(xRecursiveMutex, portMAX_DELAY);
printf("%s\n", "task1 take");
/* 释放递归锁 */
xSemaphoreGiveRecursive(xRecursiveMutex);
}
xSemaphoreGiveRecursive(xRecursiveMutex);
}
}
void Task2(void *pvParameters) {
BaseType_t xStatus;
while (1) {
/* 获取递归锁 */
xStatus = xSemaphoreTakeRecursive(xRecursiveMutex, 0);
printf("Task2 take xRecursiveMutex %s\n", (xStatus ? "sucess" : "failed"));
if(xStatus != pdTRUE){
xStatus = xSemaphoreGiveRecursive(xRecursiveMutex);
printf("Task2 give xRecursiveMutex %s\n", xStatus ? "sucess" : "failed");
}
xSemaphoreTakeRecursive(xRecursiveMutex, portMAX_DELAY);
}
}
int main( void )
{
#ifdef DEBUG
debug();
#endif
prvSetupHardware();
/* 创建递归锁 */
xRecursiveMutex = xSemaphoreCreateRecursiveMutex();
if (xRecursiveMutex != NULL) {
// 创建任务
xTaskCreate(Task1, "Task1", configMINIMAL_STACK_SIZE, NULL, 2, NULL);
xTaskCreate(Task2, "Task2", configMINIMAL_STACK_SIZE, NULL, 1, NULL);
// 启动FreeRTOS调度器
vTaskStartScheduler();
}
/* Will only get here if there was not enough heap space to create the
idle task. */
return 0;
}
A:任务1优先级最高,先运行,获得递归锁
B:任务1阻塞,让任务2得以运行
C:任务2运行,看看能否获得别人持有的递归锁:不能
D:任务2故意执行"give"操作,看看能否释放别人持有的递归锁:不能
E:任务2等待递归锁
F:任务1阻塞时间到后继续运行,使用循环多次获得、释放递归锁
递归锁在代码上实现了:谁持有递归锁,必须由谁释放## 功能快捷键
事件组
函数
创建
/* 创建一个事件组,返回它的句柄。
* 此函数内部会分配事件组结构体
* 返回值: 返回句柄,非NULL表示成功
*/
EventGroupHandle_t xEventGroupCreate( void );
/* 创建一个事件组,返回它的句柄。
* 此函数无需动态分配内存,所以需要先有一个StaticEventGroup_t结构体,并传入它的指针
* 返回值: 返回句柄,非NULL表示成功
*/
EventGroupHandle_t xEventGroupCreateStatic( StaticEventGroup_t *
pxEventGroupBuffer );
删除
/*
* xEventGroup: 事件组句柄,你要删除哪个事件组
*/
void vEventGroupDelete( EventGroupHandle_t xEventGroup )
设置事件
/* 设置事件组中的位
* 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 );
值得注意的是,ISR中的函数,比如队列函数 xQueueSendToBackFromISR 、信号量函数
xSemaphoreGiveFromISR ,它们会唤醒某个任务,最多只会唤醒1个任务。
但是设置事件组时,有可能导致多个任务被唤醒,这会带来很大的不确定性。所以
xEventGroupSetBitsFromISR 函数不是直接去设置事件组,而是给一个FreeRTOS后台任务(daemon
task)发送队列数据,由这个任务来设置事件组。
如果后台任务的优先级比当前被中断的任务优先级高, xEventGroupSetBitsFromISR 会设置
*pxHigherPriorityTaskWoken 为pdTRUE。
如果daemon task成功地把队列数据发送给了后台任务,那么 xEventGroupSetBitsFromISR 的返回值
就是pdPASS
等待时间
EventBits_t xEventGroupWaitBits( EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToWaitFor,
const BaseType_t xClearOnExit,
const BaseType_t xWaitForAllBits,
TickType_t xTicksToWait );
参数
eg:
你可以使用 xEventGroupWaitBits() 等待期望的事件,它发生之后再使用 xEventGroupClearBits()
来清除。但是这两个函数之间,有可能被其他任务或中断抢占,它们可能会修改事件组。
可以使用设置 xClearOnExit 为pdTRUE,使得对事件组的测试、清零都在 xEventGroupWaitBits()
函数内部完成,这是一个原子操作
示例
#include "event_groups.h"
#define WASH (1 << 0)
#define FIRE (1 << 1)
#define EAT (1 << 2)
int x1, x2, x3;
/* 事件组句柄 */
EventGroupHandle_t xEventGroup;
void Task1(void *pvParameters) {
while (1) {
/* 洗 */
printf("%s\n", "WASH is OK");
xEventGroupSetBits(xEventGroup, WASH);
/* 等待吃完再洗 */
xEventGroupWaitBits(xEventGroup, EAT, pdTRUE, pdTRUE, portMAX_DELAY);
}
}
void Task2(void *pvParameters) {
while (1) {
/* 等待一个事件,洗 */
xEventGroupWaitBits(xEventGroup, WASH, pdFALSE, pdTRUE, portMAX_DELAY);
printf("%s\n", "FIRE is OK");
/* 烧好 */
xEventGroupSetBits(xEventGroup, FIRE);
}
}
void Task3(void *pvParameters) {
while (1) {
/* 等待两件事,洗+烧 */
xEventGroupWaitBits(xEventGroup, WASH|FIRE, pdTRUE, pdTRUE, portMAX_DELAY);
printf("%s\n", "EAT is OK");
/* 吃好 */
xEventGroupSetBits(xEventGroup, EAT);
}
}
/*-----------------------------------------------------------*/
int main( void )
{
#ifdef DEBUG
debug();
#endif
prvSetupHardware();
/* 创建事件组 */
xEventGroup = xEventGroupCreate();
if (xEventGroup != NULL) {
// 创建任务
xTaskCreate(Task1, "wash", configMINIMAL_STACK_SIZE, NULL, 2, NULL);
xTaskCreate(Task2, "fire", configMINIMAL_STACK_SIZE, NULL, 2, NULL);
xTaskCreate(Task3, "eat", configMINIMAL_STACK_SIZE, NULL, 3, NULL);
// 启动FreeRTOS调度器
vTaskStartScheduler();
}
/* Will only get here if there was not enough heap space to create the
idle task. */
return 0;
}
- 任务3执行,不满足条件,阻塞
- 任务2执行,不满足条件,阻塞
- 任务1执行,设置事件洗0x01,并等待吃结束,阻塞
- 任务2满足,执行,设置事件烧0x10
- 任务3满足,执行,设置事件吃0x100
- 任务1满足,执行。循环往复
注:三个任务优先级不同,当事件设置,满足条件的任务会立刻执行。
任务通知
任务通知的优势:
- 效率更高:使用任务通知来发送事件、数据给某个任务时,效率更高。比队列、信号量、事件组都
有大的优势。 - 更节省内存:使用其他方法时都要先创建对应的结构体,使用任务通知时无需额外创建结构体。
任务通知的限制:
- 不能发送数据给ISR:
ISR并没有任务结构体,所以无法使用任务通知的功能给ISR发送数据。但是ISR可以使用任务通知
的功能,发数据给任务。 - 数据只能给该任务独享
使用队列、信号量、事件组时,数据保存在这些结构体中,其他任务、ISR都可以访问这些数据。
使用任务通知时,数据存放入目标任务中,只有它可以访问这些数据。
在日常工作中,这个限制影响不大。因为很多场合是从多个数据源把数据发给某个任务,而不是把
一个数据源的数据发给多个任务。 - 无法缓冲数据
使用队列时,假设队列深度为N,那么它可以保持N个数据。
使用任务通知时,任务结构体中只有一个任务通知值,只能保持一个数据。 - 无法广播给多个任务
使用事件组可以同时给多个任务发送事件。
使用任务通知,只能发个一个任务。 - 如果发送受阻,发送方无法进入阻塞状态等待
假设队列已经满了,使用 xQueueSendToBack() 给队列发送数据时,任务可以进入阻塞状态等待
发送完成。
使用任务通知时,即使对方无法接收数据,发送方也无法阻塞等待,只能即刻返回错误。
通知状态与通知值
一个是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;
taskNOT_WAITING_NOTIFICATION:任务没有在等待通知
taskWAITING_NOTIFICATION:任务在等待通知
taskNOTIFICATION_RECEIVED:任务接收到了通知,也被称为pending(有数据了,待处理)
通知值可以有很多种类型:
计数值
位(类似事件组)
任意数值
#define taskNOT_WAITING_NOTIFICATION ( ( uint8_t ) 0 ) /* 也是初始状态 */
#define taskWAITING_NOTIFICATION ( ( uint8_t ) 1 )
#define taskNOTIFICATION_RECEIVED ( ( uint8_t ) 2 )
函数
发通知与取通知
软件定时器
- 指定时间:启动定时器和运行回调函数,两者的间隔被称为定时器的周期(period)。
- 指定类型,定时器有两种类型:
一次性(One-shot timers):这类定时器启动后,它的回调函数只会被调用一次;可以手工再次启动它,但是不会自动启动它。
自动加载定时器(Auto-reload timers ):这类定时器启动后,时间到之后它会自动启动它;这使得回调函数被周期性地调用。 - 指定要做什么事,就是指定回调函数
定时器执行流程
configTIMER_TASK_PRIORITY,为了更好响应,该优先级应设置为所有任务中最高的优先级
守护任务
守护任务的调度,跟普通的任务并无差别。当守护任务是当前优先级最高的就绪态任务时,它就可以运
行。它的工作有两类:
处理命令:从命令队列里取出命令、处理
执行定时器的回调函数
回调函数
void ATimerCallback( TimerHandle_t xTimer );
定时器的回调函数是在守护任务中被调用的,守护任务不是专为某个定时器服务的,它还要处理其他定
时器。
所以,定时器的回调函数不要影响其他人:
回调函数要尽快实行,不能进入阻塞状态
不要调用会导致阻塞的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 );
定时器的很多API函数,都是通过发送"命令"到命令队列,由守护任务来实现。
如果队列满了,"命令"就无法即刻写入队列。我们可以指定一个超时时间 xTicksToWait ,等待一会。
启动/停止
启动定时器就是设置它的状态为运行态(Running、Active)。
停止定时器就是设置它的状态为冬眠(Dormant),让它不能运行。
/* 启动定时器
* 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 );
注意,这些函数的 xTicksToWait 表示的是,把命令写入命令队列的超时时间。命令队列可能已经满
了,无法马上把命令写入队列里,可以等待一会。
xTicksToWait 不是定时器本身的超时时间,不是定时器本身的"周期"。
创建定时器时,设置了它的周期(period)。 xTimerStart() 函数是用来启动定时器。假设调用
xTimerStart() 的时刻是tX,定时器的周期是n,那么在 tX+n 时刻定时器的回调函数被调用。
如果定时器已经被启动,但是它的函数尚未被执行,再次执行 xTimerStart() 函数相当于执行
xTimerReset() ,重新设定它的启动时间。
复位
从定时器的状态转换图可以知道,使用 xTimerReset() 函数可以让定时器的状态从冬眠态转换为运行
态,相当于使用 xTimerStart() 函数。
如果定时器已经处于运行态,使用 xTimerReset() 函数就相当于重新确定超时时间。假设调用
xTimerReset() 的时刻是tX,定时器的周期是n,那么 tX+n 就是重新确定的超时时间。
/* 复位定时器
* 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 );
示例
使用需要提前设置宏
/* 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"
创建两个定时器,第一个定时器只执行一次,第二个定时器定时反复执行
```c
uint8_t flagONEShotTimerRun = 0;
uint8_t flagAutoLoadTimerRun = 0;
#define mainONE_SHOT_TIMER_PERIOD pdMS_TO_TICKS( 10 )
#define mainAUTO_RELOAD_TIMER_PERIOD pdMS_TO_TICKS( 20 )
static void vONEShotTimerFunc( TimerHandle_t xTimer ){
static int cnt = 0;
flagONEShotTimerRun = !flagONEShotTimerRun;
printf("run vONEShotTimerFunc %d\r\n", cnt++);
}
static void vAutoLoadTimerFunc( TimerHandle_t xTimer ){
static int cnt = 0;
flagAutoLoadTimerRun = !flagAutoLoadTimerRun;
printf("run vAutoLoadTimerFunc %d\r\n", cnt++);
}
int main( void ){
#ifdef DEBUG
debug();
#endif
/* 创建两个定时器,一次性与周期 */
TimerHandle_t xOneShotTimer;
TimerHandle_t xAutoReloadTimer;
prvSetupHardware();
xOneShotTimer = xTimerCreate(
"OneShot", /* 名字, 不重要 */
mainONE_SHOT_TIMER_PERIOD, /* 周期 */
pdFALSE, /* 一次性 */
0, /* ID */
vONEShotTimerFunc /* 回调函数 */
);
xAutoReloadTimer = xTimerCreate(
"AutoReload", /* 名字, 不重要 */
mainAUTO_RELOAD_TIMER_PERIOD, /* 周期 */
pdTRUE, /* 自动加载 */
0, /* ID */
vAutoLoadTimerFunc /* 回调函数 */
);
if (xOneShotTimer && xAutoReloadTimer){
/* 启动定时器 */
xTimerStart(xOneShotTimer, 0);
xTimerStart(xAutoReloadTimer, 0);
/* 启动调度器 */
vTaskStartScheduler();
}
/* Will only get here if there was not enough heap space to create the
idle task. */
return 0;
}
消除抖动
使用机械开关时经常碰到抖动问题:引脚电平在短时间内反复变化。怎么读到确定的按键状态
创建了一个一次性的定时器,从来处理抖动;创建了一个任务,用来模拟产生抖动
static TimerHandle_t xKeyFilteringTimer;
void vEmulateKeyTask( void *pvParameters )
{
int cnt = 0;
const TickType_t xDelayTicks = pdMS_TO_TICKS( 200UL );
for( ;; )
{
xTimerReset(xKeyFilteringTimer, 0); cnt++;
xTimerReset(xKeyFilteringTimer, 0); cnt++;
xTimerReset(xKeyFilteringTimer, 0); cnt++;
printf("Key jitters %d\r\n", cnt);
vTaskDelay(xDelayTicks);
}
}
static void vKeyFilteringTimerFunc( TimerHandle_t xTimer )
{
static int cnt = 0;
printf("vKeyFilteringTimerFunc %d\r\n", cnt++);
}
/*-----------------------------------------------------------*/
int main( void )
{
TaskHandle_t xHandleTask1;
#ifdef DEBUG
debug();
#endif
prvSetupHardware();
xKeyFilteringTimer = xTimerCreate(
"KeyFiltering",
pdMS_TO_TICKS(20),
pdFALSE,
0,
vKeyFilteringTimerFunc
);
xTaskCreate( vEmulateKeyTask, "EmulateKey", 1000, NULL, 1, NULL );
/* Start the scheduler. */
vTaskStartScheduler();
/* Will only get here if there was not enough heap space to create the
idle task. */
return 0;
}
中断管理
补充
uxTaskGetStackHighWaterMark()
HighWaterMark译为高水位,返回值是任务创建至今任务栈剩余量的最小值,这个值越接近0,任务越有栈溢出的风险,一般要留有一定的余量
使用:
在FreeRTOS.很贱中使能 INCLUDE_uxTaskGetStackHighWaterMark或者在FreeRTOSConfig.h中使能
使用
int flag;
void Task1Function(void * param)
{
/*宏pdMS_TO_TICKS()可用于将以毫秒为单位指定的时间转换为以ticks为单位指定的时间*/
const TickType_t xDelay50ms = pdMS_TO_TICKS(50UL);
TickType_t xLastWakeTime;
int i;
while (1)
{
flag = 1;
for(i = 0; i < 4; i++){
printf("%s\n", "task1 run");
/*打印剩余的任务栈*/
printf("tsak1 size %d\n", uxTaskGetStackHighWaterMark(NULL));
#if 0
vTaskDelay(xDelay50ms);
#else
vTaskDelayUntil(&xLastWakeTime, xDelay50ms);
#endif
}
}
}