1、数据类型
每个移植版本都有的官方的portmacro.h文件里定义了两种数据类型。
TickType_t:
freertos配置了一个周期性时钟中断,Tick interrupt,每发生一次Tick count就增加一次,这个Tick count类ickType_t;
BaseType_t:
架构中最高效的数据类型,根据架构类型,是uint32_t或uint16_t;
2、空闲任务和钩子函数
空闲任务优先级为0,执行清理工作,
比如有任务“自杀时”,他就负责释放这个任务的内存。 如果是其他任务调用vtaskdelete删除,就由调用的函数释放内存
空闲任务里还设置了钩子函数,可以让空闲任务在执行时,执行这个钩子函数,达到自己想要的效果。但是钩子函数不能阻塞或暂停,否则空闲任务就无法被唤醒。
3、调度策略
是否允许抢占
若允许抢占,是否允许时间片轮转
允许抢占和时间片轮转,空闲任务是否让步
4、队列的使用
1:特点
先进先出 ,队列本质是环形缓冲区和两个链表
2:相关内部结构
队列有很多指针,头指针指向队列头部,read指针指向上次读取到的位置,write指针指向上次写的位置;
读队列时,readfrom指针会先++(就指向了当前位置),然后再读。写队列时,先写,写完之后把指针指向下一个位置。
当队列写满时,不会覆盖当前元素(但是队列长度为1时,可以使用xQueueOverwrite() 或 xQueueOverwriteFromISR()来“覆盖”,所以不会被阻塞。同理,也可“偷看”)
3:创建队列
xQueueHandle1 =xQueueCreate(2,sizeof(int));//创建队列,参数为元素个数,和元素的size
BaseType_t xQueueReset( QueueHandle_t pxQueue);//队列恢复为初始状态
int i;
xQueueSend(xQueueHandle1,&i,portMAX_DELAY);//发送数据,第三个参数为等待时间,portMAX_DELAY表示一直阻塞
xQueueSet1=xQueueCreateSet(2);//队列集,参数为队列的总长度
xQueueAddToSet(xQueueHandle1,xQueueSet1);//添加队列进队列集
xQueueSetMemberHandle hadle;
hadle=xQueueSelectFromSet(xQueueSet1,portMAX_DELAY);//当队列集中队列发送数据时,会被唤醒,并返回队列的hadle
int re;
xQueueReceive(hadle,&re,0);//接收数据
队列集。........
4.5、信号量
底层基于队列实现
1:既然有队列,那为什么还要用信号量
队列可以传输数据,所以涉及拷贝复制操作;
队列传输数据需要消耗内存,涉及了传输数据和读取数据操作;对于数据很大的时候,它的拷贝复制操作都很耗时。
2:基础操作
SemaphoreHandle_t semHadle;
int sum=0;
void Task4(void *parme)
{
int i;
while(1)
{
for(i=0;i<100000;i++)
{sum++;}
xSemaphoreGive(semHadle);//信号量资源+1
taskYIELD();//主动退出一次调度
}
}
void Task5(void *parme)
{
while(1)
{
xSemaphoreTake(semHadle, portMAX_DELAY);//取出信号量,信号量资源-1
printf("%d\r\n",sum);
}
}
void main()
{
//semHadle=SemaphoreCreateBinary(10,0);//技术型信号量,10代表最大资源计数,0代表初始资源数,返回值为一个hadle,不为null时创建成功
semHadle=xSemaphoreCreateBinary();//二进制信号量
if(semHadle==NULL)
{
printf("create semaphore fail!\r\n");
}
xTaskCreate(Task4,"Task4",100,NULL,1,NULL);//创建任务
xTaskCreate(Task5,"Task5",100,NULL,1,NULL);
//idletask3=xTaskCreateStatic(vTask3,"Task3",100,NULL,1,xTask3Stack,&xTask3TCB);
vTaskStartScheduler();
}
5、互斥量
1:与二元信号量的对比
可以解决优先级反转的问题:比如有三个优先级A<B<C的任务,A拿到锁之后,到C执行,因为C拿不到锁,就会阻塞,然后就到B执行了,B优先级低反而先执行,这个现象就叫优先级反转。而互斥量可以把保证,C拿不到锁,他就会把自己的优先级继承给A,A就会比B先执行,这个就叫优先级继承。
可以递归继承锁:比如这个程序拿到锁之后,这个程序内部的代码还要获取这个锁,这时候就会阻塞。递归继承锁就可以让它再次获取锁而不阻塞。但是要保证释放锁,因为lock和unlock是成对出现的。这个使用递归锁实现
二元信号量初始值为0,互斥量为1;其他的give、take操作都一样
2:基本使用
SemaphoreHandle_t semHadle;
int sum=0;
void Task4(void *parme)
{
int i;
while(1)
{
xSemaphoreTake(semHadle, portMAX_DELAY);
for(i=0;i<100000;i++)
{
sum++;
}
xSemaphoreGive(semHadle);
taskYIELD();
}
}
void Task5(void *parme)
{
while(1)
{
xSemaphoreTake(semHadle, portMAX_DELAY);//获取
printf("%d\r\n",sum);
xSemaphoreGive(semHadle);//释放
taskYIELD();
}
}
void main()
{
semHadle=xSemaphoreCreateMutex();//互斥量
if(semHadle==NULL)
{
printf("create semaphore fail!\r\n");
}
xTaskCreate(Task4,"Task3",100,NULL,1,NULL);
xTaskCreate(Task5,"Task3",100,NULL,1,NULL);
vTaskStartScheduler();
}
3、互斥量缺陷
并不是谁上锁,谁就解锁,其他任务也可以解锁,这时候程序就会出现bug;而递归锁是谁上锁,谁解锁,可以用递归锁来实现这种效果。
SemaphoreHandle_t semHadle;
int sum=0;
void Task4(void *parme)
{
int i;
while(1)
{
xSemaphoreTakeRecursive(semHadle, portMAX_DELAY);
for(i=0;i<100000;i++)
{sum++;}
xSemaphoreGiveRecursive(semHadle);
taskYIELD();
}
}
void Task5(void *parme)
{
//int i=0;
while(1)
{
xSemaphoreTakeRecursive(semHadle, portMAX_DELAY);
printf("%d\r\n",sum);
xSemaphoreGiveRecursive(semHadle);
taskYIELD();
}
}
void main()
{
semHadle=xSemaphoreCreateRecursiveMutex();//互斥量
if(semHadle==NULL)
{
printf("create semaphore fail!\r\n");
}
xTaskCreate(Task4,"Task3",100,NULL,1,NULL);
xTaskCreate(Task5,"Task3",100,NULL,1,NULL);
vTaskStartScheduler();
}
6、事件组
1、概念
事件组等待时,要么等待事件组中的某个事件,要么等待所有事件。
事件组用一个整数来表示,其中的高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位)
2、基础使用(等待事件和同步点)
SemaphoreHandle_t semHadle;
EventGroupHandle_t eventHadle;//事件组handle
int sum=0;
void Task4(void *parme)
{
int i;
while(1)
{
xSemaphoreTakeRecursive(semHadle, portMAX_DELAY);
for(i=0;i<100000;i++)
{sum++;}
xSemaphoreGiveRecursive(semHadle);
xEventGroupSetBits(eventHadle, 0); //设置事件已完成
taskYIELD();
}
}
void Task5(void *parme)
{
while(1)
{
xSemaphoreTakeRecursive(semHadle, portMAX_DELAY);
printf("%d\r\n",sum);
xSemaphoreGiveRecursive(semHadle);
xEventGroupSetBits(eventHadle, 1); //设置事件已完成
taskYIELD();
}
}
void Task6(void *parme)
{
while(1)
{
xEventGroupWaitBits(eventHadle,(1|0),pdTRUE,pdTRUE,portMAX_DELAY);//等待事件完成
printf("task 4 and task5 end toghter\r\n");
}
}
void main()
{
semHadle=xSemaphoreCreateRecursiveMutex();//互斥量
eventHadle = xEventGroupCreate();//事件组
if(semHadle==NULL)
{
printf("create semaphore fail!\r\n");
}
xTaskCreate(Task4,"Task3",100,NULL,1,NULL);
xTaskCreate(Task5,"Task3",100,NULL,1,NULL);
xTaskCreate(Task6,"Task6",100,NULL,1,NULL);
vTaskStartScheduler();
}
3、相关函数
xEventGroupSetBits(eventHadle, 0); //设置某一个事件已完成
xEventGroupWaitBits(eventHadle,(1|0),pdTRUE,pdTRUE,portMAX_DELAY);//等待事件完成
/*前一个pdTRUE的位置表示事件返回时是否把所有事件置零,后一个表示是否等待所有事件,还是其中一个事件。*/
同步点:可以同步多个任务
xEventGroupSync(EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToSet, const EventBits_t uxBitsToWaitFor,portMAX_DELAY)
/*
const EventBits_t uxBitsToSet 设置完成的事件是哪一位
const EventBits_t uxBitsToWaitFor 等待哪些位
portMAX_DELAY 等待事件。这里这个表示不成功就阻塞
*/
7、任务通知
1:特点
多对一的关系,通知的任务不会阻塞(要么成功,要么失败,但是都不等待),被通知的任务没数据可以等待,有数据就返回
传递数据,发送事件时更快
更节省内存,因为不需要创建额外的结构体。
不能在中断中使用,因为中断不属于任务,内部没有TCB结构体
接收方只有一个任务
不能缓冲多个数据,只能保存一个,因为TCB结构体里只有一个value值和statu状态。
2:相关函数
3:实现轻量级信号量
xTaskHandle taskHandle1;
xTaskHandle taskHandle2;
void Task1(void *parme)
{
int i;
int count=0;
while(1)
{
for(i=0;i<10;i++)
{
xTaskNotifyGive(taskHandle2);//向哪个任务发送通知,让其val++
}
printf("task1=%d\r\n",count++);
vTaskDelay(1);
taskYIELD();//主动退出调度
}
}
void Task2(void *parme)
{
int i=0;
int val;
while(1)
{
i++;
//接收通知,参数一表示函数退出时是否将自身value清零;参数二表示未收到通知,等待多久
//pdFALSE:如果通知值大于0,则把通知值减1
val= ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
printf("val= %d,i=%d\r\n",val,i);
vTaskDelay(1);
}
}
void main()
{
xTaskCreate(Task1,"Task1",100,NULL,1,&taskHandle1);
xTaskCreate(Task2,"Task2",100,NULL,1,&taskHandle2);
vTaskStartScheduler();
}
4:实现轻量级队列
xTaskHandle taskHandle1;
xTaskHandle taskHandle2;
void Task1(void *parme)
{
int i;
while(1)
{
for(i=0;i<10;i++)
{
xTaskNotify(taskHandle2,i, eSetValueWithOverwrite);//如果有数据未读,不覆盖
}
taskYIELD();
//vTaskDelete(NULL);
}
}
void Task2(void *parme)
{
int i=0;
int val;
while(1)
{
i++;
xTaskNotifyWait(0, 0,&val, portMAX_DELAY);//接收
printf("val= %d,i=%d\r\n",val,i);
}
}
int main( void )
{
prvSetupHardware();//硬件设置
xTaskCreate(Task1,"Task1",100,NULL,1,&taskHandle1);
xTaskCreate(Task2,"Task2",100,NULL,1,&taskHandle2);
vTaskStartScheduler();
return 0;
}
5:实现轻量级事件组
注意,和事件组是有区别的:事件组可以选择等待某一个事件或者所有事件,而任务通知只要有bit位被设置,都会被唤醒
xTaskHandle taskHandle1;
xTaskHandle taskHandle2;
xTaskHandle taskHandle3;
void Task1(void *parme)
{
int i;
while(1)
{
vTaskDelay(20);
xTaskNotify(taskHandle3,(1<<0), eSetBits);//设置bit位
//taskYIELD();
vTaskDelete(NULL);
}
}
void Task2(void *parme)
{
while(1)
{
vTaskDelay(30);
xTaskNotify(taskHandle3,(1<<1), eSetBits);//设置bit位
//taskYIELD();
vTaskDelete(NULL);
}
}
void Task3(void *parme)
{
int bit;
while(1)
{
xTaskNotifyWait(0, 0,&bit, portMAX_DELAY);//接收
if((bit&0x03)==0x03)//如果只设置了一次
{
printf("get toghter bit=%d\r\n",bit);
}
else
{
printf("get only one bit=%d\r\n",bit);
}
}
}
void main()
{
xTaskCreate(Task1,"Task1",100,NULL,1,&taskHandle1);
xTaskCreate(Task2,"Task2",100,NULL,1,&taskHandle2);
xTaskCreate(Task3,"Task3",100,NULL,1,&taskHandle3);
vTaskStartScheduler();
}
8、定时器
1:概念
每隔一定的tick时间就触发。分为周期性定时器和一次性定时器,两者都只有两种状态,冬眠态或者running态。当timercreate时,进入dormant态,当到达指定tick,或有任务要修改定时器相关设置,会进入running态。(注:周期性定时器,进入running后,一直保持此状态,直到调用xTimerstop()才变为dormant)。
如果使用,需要配置等待时间、队列长度和守护任务的栈大小等参数
要实现回调函数,会创建一个守护任务来执行定时器触发时的任务。
其结构体中队列的作用是:启动或修改定时器时,会把命令放入定时器队列中,由守护任务读取这个队列,然后执行相关操作,因为队列可能满,所以会有等待时间。
2:基础使用
xTaskHandle taskHandle1;
xTimerHandle xMyTimerHadle;//定时器的hadle
static int cut;
//定时器执行的回调函数
void vMyTimerCallbackFunction( TimerHandle_t xTimer )
{
printf("callback function running....%d times\r\n",cut++);
}
void Task1(void *parme)
{
xTimerStart(xMyTimerHadle, portMAX_DELAY);//启动定时器
while(1)
{
printf("task1 running...\r\n");
}
}
void main(void)
{
prvSetupHardware();//硬件设置
xMyTimerHadle=xTimerCreate("myTimer",100,pdFALSE,NULL,vMyTimerCallbackFunction);//创建定时器
xTaskCreate(Task1,"Task1",100,NULL,1,&taskHandle1);
vTaskStartScheduler();
}
9、中断
1:概念
RTOS里,需应对各种事件,比如用户按下按键,这些事件大多通过硬件中断产生。
中断处理流程:CPU跳到固定地址执行代码,这个固定地址叫中断向量,以下是执行代码做的事:
1、保护现场,比如任务1被打断,保护它的运行环境,比如寄存器的值。
2、调用中断处理函数(ISR)
3、恢复运行任务1,或优先级更高的任务
2:注意点
RTOS中,任务优先级与硬件无关,由程序员决定,而中断看作硬件特性的一部分,它的执行由硬件决定
ISR在内核空间调用,调用时用户任务无法执行,因为即使优先级最低的ISR都比任务的优先级高
中断函数不能被阻塞,它的执行要尽量快,如果非常耗时的话,可以把它放到任务中去执行,所以ISR涉及程序间的通信.
3:函数区别
中断处理不能用普通的函数,需要用后缀带FromISR的版本。
他们功能差不多,为什么要用呢?有什么好处呢?他们有什么区别吗?
因为ISR不能阻塞,所以他们参数不一样。如果在一般函数,需判断此程序是是否在进行中断处理,就需要在程序中添加大量分支,这会造成大量的代码冗余,有些架构很难分清任务中还是ISR中,就需要添加更多的代码。
其次,移植FreeRTOS时,还需要提供监测上下文的函数,比如is_in_isr()。
也因此,任务中或ISR中都可以用FromISR函数,而反过来不行。毕竟任务也可以不等待阻塞。
10、资源管理
我们在使用临界资源的时候,要让程序互斥执行,就需要进行资源管理,有两种情况
1、两个任务要用到临界资源,任务在运行时,停止任务调度
2、任务和中断用到临界资源,任务在运行时,屏蔽中断(中断不会被任务打断,所以不需要屏蔽中断)
一般架构中,会有个4位寄存器0~255,保存中断优先级,最低位的中断不可被屏蔽;一般191~255(根据硬件情况会有不同)的中断会用到sysctl,我们在屏蔽中断的时候,就可以按照这一类来屏蔽,就避免了需要屏蔽很多个中断的情况。
在任务中,是屏蔽后重新使能中断,而中断中是恢复中断,为什么是恢复呢?因为屏蔽中断之前可能有两种情况,中断是禁止的或者开启的,调用后恢复之前的状态。
11、总结
1、任务创建时,先创建个TCB结构体,然后把提供的函数和栈大小传给它。