学习有感
- 对于搬运大量数据,利用指针效率高于赋值,即直接访问数据量
- 对于FreeRTOS可以解决在裸机开发过程中的不同任务的执行频率和延时函数的CPU的死等的问题,提高了CPU对计算资源的利用率。
- 对于内存小的单片机,FreeRTOS占用资源过多,不利于使用,对于任务过于简单,由于切换任务占比执行任务太大,不利于使用,对于每个任务执行时间较长,使用RTOS的时间片机制能够增强每个任务的实时性。
1.创建函数和删除
- 动态创建为FreeRTOS分配的堆栈(方便),而静态创建为人为分配空间。动态应用多
- 任务中必须有while(1)否则只会执行一次
- 任务中的延时要用 vTaskDelay(500); 延时期间执行其它任务
-
任务中的延时使用的是软件延时,即还是让CPU等待来达到延时的效果。使用RTOS的优势就是能充分发挥CPU的性能,永远不让它闲着。任务如果需要延时,也就不能再让CPU空等来实现延时的效果。RTOS中的延时叫作阻塞延时,即任务需要延时时,会放弃CPU的使用权,CPU可以去做其他的事情,当任务延时时间到,重新获取CPU使用权,任务继续运行,这样就充分地利用了CPU的资源,而不是空等
当任务需要延时而进入阻塞状态时,那CPU在做什么?如果没有其他任务可以运行,RTOS都会为CPU创建一个空闲任务,这个时候CPU就运行空闲任务。在FreeRTOS中,空闲任务是系统在启动调度器时创建的优先级最低的任务,空闲任务主体主要是做一些系统内存的清理工作。
-
- 任务创建后马上就执行,一个MCU能够支持多少任务,取决于RAM空间的大小。
- FreeRTOS支持抢占式和时间片两种方式同时运行。
- 进入临界区,不使用任务调度,退出临界区,开始任务调度
- 关于堆栈大小可以使用这个函数:
xGetFreeStackSpace 任务栈历史剩余最小值
动态创建任务
#define Led2_PRIO 1 /* 任务优先级 */
#define Led2_STACK_SIZE 128 /* 任务堆栈大小*/
TaskHandle_t Led2_task_handler; /* 任务句柄 */
void Led2Task(void *argument); /* 任务函数声明 */
xTaskCreate((TaskFunction_t ) Led2Task, /* 任务函数 */
(char * ) "Led2", /* 任务名字 */
(configSTACK_DEPTH_TYPE ) Led2_STACK_SIZE, /* 堆栈大小 */
(void * ) NULL, /* 入口参数 */
(UBaseType_t ) Led2_PRIO, /* 任务优先级 */
(TaskHandle_t * ) &Led2_task_handler ); /* 任务句柄 */
任务函数
void Led2Task(void *argument)
{
/* USER CODE BEGIN LedTask */
/* Infinite loop */
for(;;)
{
LED2_ON;
vTaskDelay(500);
LED2_OFF;
vTaskDelay(500);
}
/* USER CODE END LedTask */
}
创建和删除任务
vTaskDelete(task1_handler); //填写任务句柄
// 删除任务
if(task1_handler!= NULL)
{
printf("删除start_task任务\r\n");
vTaskDelete(task1_handler); //填写任务句柄
task1_handler = ZERO;
}
//创建任务
if(Led2_task_handler == NULL)
{
xTaskCreate(Led2Task,"Led2",Led2_STACK_SIZE,
ZERO,Led2_PRIO,&Led2_task_handler );
}
vTaskDelete(null); Delete your own tasks
保护任务:
// 保护任务
taskENTER_CRITICAL(); /* 进入临界区 */
/* 被保护的代码 */
taskEXIT_CRITICAL(); /* 退出临界区 */
静态创建:
静态创建时,任务句柄作为返回值。
#define Led3_PRIO osPriorityLow /* 任务优先级 */
#define Led3_STACK_SIZE 128 /* 任务堆栈大小*/
TaskHandle_t Led3_task_handler; /* 任务句柄 */
StackType_t Led3_task_stack[Led3_STACK_SIZE]; /* 任务堆栈*/
StaticTask_t Led3_task_tcb; /* 任务控制块*/
void Led3Task(void *argument); /* 任务函数声明 */
Led3_task_handler = xTaskCreateStatic( (TaskFunction_t ) Led3Task, /* 任务函数 */
(char * ) "Led3", /* 任务名字 */
(uint32_t ) Led3_STACK_SIZE, /* 堆栈大小 */
(void * ) NULL, /* 入口参数 */
(UBaseType_t ) Led3_PRIO, /* 任务优先级* /
(StackType_t * ) Led3_task_stack, /* 任务堆栈 */
(StaticTask_t * ) &Led3_task_tcb ); /* 任务名字 */
2.任务挂起和恢复
- 任务优先级越大,优先程度越高,中断优先级数值越小,优先级越高。中断中优先级恢复任务中 , 中断优先级在5—15级
vTaskSuspend(TaskHandle_t xTaskToSuspend) //任务挂起 ——任务句柄
vTaskResume(TaskHandle_t xTaskToResume) //任务恢复 ——任务句柄
xTaskResumeFromISR(TaskHandle_t xTaskToResume) //中断中任务恢复 ——任务句柄
挂起任务与恢复任务:
vTaskSuspend(LedHandle);
vTaskResume(LedHandle);
3.中断:
STM32 总共有16个优先级(0—15)
- 中断优先级在5—15级的中断中才能调用FreeRTOS中的函数,小于5的中断无法使用。且要使用尾部为FromISR的函数。
- 对 5—15 等级中断的管理
- vTaskDelay(500); 会打开中断
portENABLE_INTERRUPTS(); //开启中断
portDISABLE_INTERRUPTS(); //关闭中断
5.临界段代码保护:
同一时间只能有一个人使用的资源,被称为临界资源。比如任务A、B都要使用串口来打印,串口就是临界资源。如果A、B同时使用串口,那么打印出来的信息就是A、B混杂,无法分辨。所以使用串口时,应该是这样:A用完,B再用;B用完,A再用。
什么是临界段:
临界段,用一句话概括就是一段在执行时不能被中断的代码段。在FreeRTOS中,临界段最常出现的地方就是对全局变量的操作。全局变量就像是一个靶子,谁都可以对其开枪,但是有一人开枪,其他人就不能开枪,否则就不知道是谁命中了靶子。
那么什么情况下临界段会被打断?一个是系统调度,还有一个就是外部中断。在FreeRTOS中,系统调度最终也是产生PendSV中断,在PendSV Handler中实现任务的切换,所以还是可以归结为中断。既然这样,FreeRTOS对临界段的保护最终还是回到对中断的开和关的控制。
作用: 防止中断和任务调度的打断。
应用方面:
- 通信时候,IIC SPI
- 系统自身需求
- 用户需求
特点:
- 成对使用
- 主要通过关闭和开启中断实现,和开启和关闭任务调度器
- 被保护的代码一定要短
- 关闭中断,即所有中断都不能执行;而关闭任务调度器,可以任务无法抢占,而可以中断可以打断。
//开启和关闭任务调度器 #include "FreeRTOS.h"
vTaskSuspendAll() ;
{
… … /* 内容 */
}
xTaskResumeAll() ;
// 代码中使用,关闭所有中断
taskENTER_CRITICAL() ;
{
… … /* 临界区 */
}
taskEXIT_CRITICAL() ;
// 中断中使用,关闭所有中断
uint32_t save_status;
save_status = taskENTER_CRITICAL_FROM_ISR();
{
… … /* 临界区 */
}
taskEXIT_CRITICAL_FROM_ISR(save_status );
6.列表和列表项
- 列表和C语言中的链表类似
- 列表项为列表的子集
- FreeRTOS内部是一个双向环形列表
- 链表可以随意删除和加入成员,而且成员间地址并不是连续的,成员数字可以随意更改。
- 每个列表项就是一个任务
- 任务列表分为:阻塞列表,就绪列表,运行列表
6.任务状态查询
1.获取与设置任务优先级:
参数: 任务句柄,获取自己填写NULL
UBaseType_t priority_num = 0;
vTaskPrioritySet( task2_handler,4 ); //设置任务的优先级
priority_num = uxTaskPriorityGet( NULL ); //获取任务的优先级
printf("task2任务优先级为%ld\r\n",priority_num);
2.获取任务数量:
参数:无
UBaseType_t task_num = 0;
task_num = uxTaskGetNumberOfTasks();
printf("任务数量:%ld\r\n",task_num);
3.由任务名获取任务句柄:
参数:无
TaskHandle_t task_handle = 0;
task_handle = xTaskGetHandle( "task1" );
printf("任务句柄:%#x\r\n",(int)task_handle);
4.查询历史剩余最小堆栈
如果剩余很小,则表明要调大堆栈。剩余字节=剩余数*4 ,
与创建的时候填写的是同一个单位,即将初级填写的堆栈大小减去剩余的大小,再稍微加点就是比较合适的堆栈。
UBaseType_t task_stack_min = 0; /* 局部 */
printf("Task:\t Residual\r\n");
vTaskDelay(500);
task_stack_min = uxTaskGetStackHighWaterMark( LvglHandle );
printf("Lvgl:\t%ld\r\n",task_stack_min);//
vTaskDelay(500);
task_stack_min = uxTaskGetStackHighWaterMark( LedHandle );
printf("Led:\t%ld\r\n",task_stack_min);
vTaskDelay(500);
task_stack_min = uxTaskGetStackHighWaterMark( Led2_task_handler );
printf("Led2:\t%ld\r\n",task_stack_min);
printf("\r\n");
5.统计任务时间占比:
1、将宏 configGENERATE_RUN_TIME_STATS 置1
2、将宏 configUSE_STATS_FORMATTING_FUNCTIONS 置1
当将此宏 configGENERATE_RUN_TIME_STAT 置1之后,还需要实现2个宏定义:
portCONFIGURE_TIMER_FOR_RUNTIME_STATE() :用于初始化用于配置任务运行时间统计的时基定时器;
注意:这个时基定时器的计时精度需高于系统时钟节拍精度的10至100倍!
3、portGET_RUN_TIME_COUNTER_VALUE():用于获取该功能时基硬件定时器计数的计数值 。
7.延时函数
相对延时:vTaskDelay()
从延时开始计数,到了计时时间,返回执行。
绝对延时:xTaskDelayUntil()
适用于需要按照一定频率运行的任务,把任务和延时组成一个周期,按照一定的频率执行。
任务以周期性固定执行。
8.进程间通信
基本概念
1. 消息队列:
1. 队列是任务到任务、任务到中断、中断到任务数据交流的一种机制(消息传递)
2. 全局变量在多任务操作时容易数据受损。由于队列读写队列是会保护
3. 先进先出(FIFO),任务先得到的是最先进入消息队列的消息,
4. 用于任务间通信的数据结构,在创建队列时,就要指定队列长度以及队列项目的大小!
5 .队列传递实际值,不好的是耗时,而ucos,是传递值的地址,值就会不安全。当传递较大的数据时FreeRTOS也可以传递指针。
6. 当队列已满,可以设置阻塞时间,种类有:未读到直接返回 向下执行,等待特定时间,时间到就向下执行 ,直到等到消息才往下执行,否则一直是阻塞状态。
用法:
1.创建队列
xQueueCreate() | 动态方式创建队列 |
xQueueCreateStatic() | 静态方式创建队列 |
动态创建:
参数:队列长度,队列数据类型长度
返回: 队列句柄 ( 用于读取和写入)
创建数据类型
#define queueQUEUE_TYPE_BASE ( ( uint8_t ) 0U ) /* 队列 */
#define queueQUEUE_TYPE_SET ( ( uint8_t ) 0U ) /* 队列集 */
#define queueQUEUE_TYPE_MUTEX ( ( uint8_t ) 1U ) /* 互斥信号量 */
#define queueQUEUE_TYPE_COUNTING_SEMAPHORE ( ( uint8_t ) 2U ) /* 计数型信号量 */
#define queueQUEUE_TYPE_BINARY_SEMAPHORE ( ( uint8_t ) 3U ) /* 二值信号量 */
#define queueQUEUE_TYPE_RECURSIVE_MUTEX ( ( uint8_t ) 4U ) /* 递归互斥信号量 */
写入位置:
覆写方式写入队列,只有在队列的队列长度为 1 时,才能够使用
#define queueSEND_TO_BACK ( ( BaseType_t ) 0 ) /* 写入队列尾部 */
#define queueSEND_TO_FRONT ( ( BaseType_t ) 1 ) /* 写入队列头部 */
#define queueOVERWRITE ( ( BaseType_t ) 2 ) /* 覆写队列*/
示例创建:
先添加头文件
#include "queue.h"
创建:
QueueHandle_t key_queue; /* 数据句柄 放在全局 */
/* 队列的创建 返回句柄 */
key_queue = xQueueCreate( 2, sizeof(uint8_t) ); /* 几个这样的数据,每个数据的大小 */
if(key_queue != NULL)
{
printf("key_queue队列创建成功!!\r\n");
}else printf("key_queue队列创建失败!!\r\n");
2.写入队列
函数 | 描述 |
xQueueSend() | 往队列的尾部写入消息 |
xQueueSendToBack() | 同 xQueueSend() |
xQueueSendToFront() | 往队列的头部写入消息 |
xQueueOverwrite() | 覆写队列消息(只用于队列长度为 1 的情况) |
xQueueSendFromISR() | 在中断中往队列的尾部写入消息 |
xQueueSendToBackFromISR() | 同 xQueueSendFromISR() |
xQueueSendToFrontFromISR() | 在中断中往队列的头部写入消息 |
xQueueOverwriteFromISR() | 在中断中覆写队列消息(只用于队列长度为 1 的情况) |
写入:
uint8_t key = 0; //返回值 局部 类型要和写入队列一致
BaseType_t err = 0; //返回值 局部
// key =传入的值;
// 句柄 传入的值 阻塞方式(死等)
err = xQueueSend( key_queue, &key, portMAX_DELAY );
if(err != pdTRUE)
{
printf("队列写入失败\r\n");
}
printf("队列写入成功\r\n");
3.读取队列
函数 | 描述 |
xQueueReceive() | 从队列头部读取消息,并删除消息 |
xQueuePeek() | 从队列头部读取消息 |
xQueueReceiveFromISR() | 在中断中从队列头部读取消息,并删除消息 |
xQueuePeekFromISR() | 在中断中从队列头部读取消息 |
uint8_t key = 0;
BaseType_t err = 0;
// 读取队列 句柄 取值变量 阻塞方式(死等)
err = xQueueReceive( key_queue,&key,portMAX_DELAY);
if(err != pdTRUE)
{
printf("key_queue队列读取失败\r\n");
}else
{
printf("key_queue读取队列成功,数据:%d\r\n",key);
}
2. 事件组:
3. 信号量:
通知任务完成的信号,解决同步问题的机制,对共享资源的有序访问。
最大值为1,即只有0和1两种,就是二值信号量。(单个资源,例如串口)
最大值不为1,他就是计数型信号量(多个资源,例如多个车位的使用)。
二值信号量:
互斥访问和任务同步,但更适合与同步,用于对一个外设资源的获取和释放。
互斥信号量:
用于互斥访问资源