FreeRTOS
一、任务
FreeRTOS操作系统支持多任务并发执行,可以看成每个任务可以写一个‘main’函数,在死循环里执行。
1.任务创建与删除
创建
(1)任务可以在CubeMx中创建,设置任务名称、优先级、堆栈大小。
(2)在程序中:
MX_FREERTOS_Init();
调用 osThreadCreate来创建一个任务,函数返回一个任务句柄。
defaultTask:任务名称
StartDefaultTask:任务的启动函数,逻辑代码在这里面写,。
osPriorityBelowNormal:任务优先级
osThreadId defaultTaskHandle;
osThreadDef(defaultTask, StartDefaultTask, osPriorityBelowNormal, 0, 128);
defaultTaskHandle = osThreadCreate(osThread(defaultTask), NULL);
删除
vTaskDelete(LEDTaskHandle);
2.任务调度
(1)osKernelStart():开启任务调度,之后程序就交给操作系统了,总是在中断和任务中来回切换。
(2)在每个任务的启动函数中编写逻辑代码,注意每个启动函数中都要有osDelay,因为多任务的实现机制就是在就绪态中取优先级最高的执行,当每个任务处于osDelay的时候进入阻塞,使其他低优先级的任务得以调度。不可使用HAL_Delay,这个不行。osDelay可以让任务定时进入阻塞态。
void StartDefaultTask(void const * argument)
{
for(;;)
{
printf("test 1 running\r\n");
osDelay(1000);
}
}
3.任务挂起与解挂
任务挂起后就不会处于就绪态和运行态了,相当于暂停该任务,该任务的所有信息都还保存着。
(1)任务挂起:
vTaskSuspend(LEDTaskHandle);
osThreadSuspendAll();//全部挂起
(2)任务解挂:
vTaskResume(LEDTaskHandle);
vTaskResumeFromISR();//中断中使用
osThreadResumeAll();//全部继续
说明:
单个任务不管挂起几次,解挂一次就可以了;传递NULL挂起自身。
全部挂起几次就需要解挂几次,本质是挂起任务调度器,导致不能切换上下文,中间会有一个变量记录被挂起了几次,但是中断还能执行,不过如果是进入需要进行上下文切换的中断也会被挂起,要是全部挂起了我应该在哪里全部解挂,是中断中吗?
ISR是用于中断中的,具体为什么中断中要用不一样的,我还没有考究。
4.代码测试
uint8_t task2 = 0;
void StartLEDTask(void const * argument)
{
for(;;)
{
printf("test 2 running\r\n");
task2++;
osDelay(1000);
}
}
void StartHC_SR04Task(void const * argument)
{
for(;;)
{
if(task2>5)
{
task2=0;
vTaskSuspend(LEDTaskHandle);
printf("任务2已挂起\r\n");
osDelay(2200);
vTaskResume(LEDTaskHandle);
printf("任务2继续\r\n");
osDelay(2200);
vTaskDelete(LEDTaskHandle);
printf("任务2已删除\r\n");
osDelay(2200);
osThreadDef(LEDTask, StartLEDTask, osPriorityIdle, 0, 128);
LEDTaskHandle = osThreadCreate(osThread(LEDTask), NULL);
printf("任务2已创建\r\n");
}
osDelay(1);
}
}
二、消息队列
消息队列是一个队列,它不属于任何一个任务,支持各任务间的异步通信。比如说定义了一个消息队列,那么每个任务都可以向它里面发数据,也可以从里面读数据,一般常用于多个任务发,一个任务收。
1.FreeRTOS消息队列特点
(1)支持先入先出和后入先出两种模式,不过我们一般用先入先出。
(2)读写队列均支持超时机制。如果读时队列为空或者写时队列为满,可以设置超时时间,在时间之内任务阻塞等待,超时后进入就绪并且可能有一个标志或者出发一个错误提醒。
(3)入队和出队是拷贝赋值的方式,不是引用。如果数据过大,可以选择传递指针,不过应注意发送方和接收方对这段地址的读写访问冲突,即保证收发只有一方在操作。
(4)注意在中断中调用的函数要用其专门的ISR函数(下面就不说ISR了,自己可以对应这搜一下),原因是大概是因为中断嵌套是中断优先级导致出错,挺复杂的,这个有说一些,但是我没太看明白https://www.cnblogs.com/w-smile/p/11333950.html。
2.消息队列使用
(1)创建消息队列
QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength, UBaseType_t uxItemSize );
uxQueueLength:队列长度
uxItemSize:队列中每个元素的大小
返回一个消息队列句柄
技巧:一般使用时传递指针,可以节省空间,即uxItemSize=sizeof(uint32_t)。
(2)发送消息
BaseType_t xQueueSend(QueueHandle_t xQueue, const void * pvItemToQueue, TickType_t xTicksToWait);
xQueue:消息队列句柄
pvItemToQueue:发送数据地址
xTicksToWait:等待时间
返回状态信息
其他常用函数
xQueueSendToFront():一般用于紧急消息,需要立即处理.
xQueueOverwrite():可在队列满时,自动覆盖最旧的消息。
(3)接受消息
BaseType_t xQueueReceive(QueueHandle_t xQueue, void *pvBuffer, TickType_t xTicksToWait);
(对应于发送)
BaseType_t xQueuePeek(QueueHandle_t xQueue, void * pvBuffer, TickType_t xTicksToWait);//用于从队列中接收一个消息,但读取之后消息还保留在队列中
3.应用示例
简单拷贝赋值消息队列的创建与收发:
#include "queue.h"
#include "usart.h"
QueueHandle_t xQueue; //消息队列
void StartDefaultTask(void const * argument)
{
xQueue = xQueueCreate(10, sizeof(uint32_t));//这个函数需要放到一开始可以执行的程序地方,我这里放到默认的任务起始执行,并设置默认任务的优先级高于其他任务,保证这句话先执行。 也可以放到其他合适的地方,后来发现放到MX_FREERTOS_Init()函数的结尾比较合适
for(;;)
{
printf("task one running\r\n");
osDelay(1000);
}
}
//该任务发送消息到队列
void StartLEDTask(void const * argument)
{
uint32_t ulValueToSend = 0;
for(;;)
{
xQueueSend(xQueue, &ulValueToSend, 1000); //向队列里发送数据
ulValueToSend++;
osDelay(1000);
}
}
//接受消息队列
void StartHC_SR04Task(void const * argument)
{
uint32_t ulReceivedValue;
for(;;)
{
if (xQueueReceive(xQueue, &ulReceivedValue, portMAX_DELAY) == pdTRUE)
printf("Q:%d\r\n", ulReceivedValue);
osDelay(100);
}
}
一般的消息队列元素可以封装到结构体里,用一个变量标记发送方。https://blog.csdn.net/weixin_44333597/article/details/107725480有讲一点。
三、信号量
信号量是消息队列的一种应用,内部都是调用的消息队列的函数,可以用于进程同步、对临界资源互斥访问等功能。
信号量是一个队列,这个队列的长度根据类型设置有所不同,队列中的元素大小都是NULL。也就是说,入队的元素都是NULL,那么其实就是不会将在队列中存东西,但是队列句柄中有一个变量uxMessagesWaiting 表示队列中有几个元素,它的值是会在每次入队的时候+1,出队的时候-1。这样就可以通uxMessagesWaiting 与uxLength队列长度进行比较就可以知道是否可以出队入队。
其实就和队列一样,在队空和堆满的时候任务会进入阻塞状态,有一个等待时长,通过返回值判断是否成功。就是因为有这个阻塞在,所以可以达到进程同步或者共享资源访问控制的目的。
参考:http://t.csdn.cn/tiJG4(这个针对源码分析)
http://t.csdn.cn/Y3u5X(这个适合学习使用)
1.创建信号量
(1)二值信号量
#define semSEMAPHORE_QUEUE_ITEM_LENGTH ( ( uint8_t ) 0U )
#define xSemaphoreCreateBinary() xQueueGenericCreate( ( UBaseType_t ) 1, semSEMAPHORE_QUEUE_ITEM_LENGTH, queueQUEUE_TYPE_BINARY_SEMAPHORE )
可以看到,创建二值信号量就是创建了一个长度为1,元素大小为0的消息队列,传入标记是一个二值信号量,在队列操作中会根据类型来自己处理。
创建好的二值信号量默认是空的,也就是说使用xSemaphoreTake()是获取不到的。
二值信号量的uxMessagesWaiting值只能在0或1之间转变。适用于任务同步。
xSemaphoreCreateBinary()
返回:信号量句柄,失败返回NULL。
示例:
SemaphoreHandle_t xSemaphore = NULL;
xSemaphore = xSemaphoreCreateBinary();
if (NULL == xSemaphore)
{
printf("xSemaphoreCreateBinary error!\r\n");
}
(2)互斥信号量
互斥信号量的队列设置和二值信号量是类似的,只不过因为二值信号量在应用时可能会发生优先级翻转的情况,实时性有所损失,互斥信号量在其后执行了prvInitialiseMutex()进行初始化。互斥信号量具有优先级继承机制,在发生获取信号量的时候,会将发送信号量的任务的优先级设置为和自己一样,直到任务发送信号量之后才会将优先级恢复,避免了优先级翻转。不过最好不要用互斥信号量的ISR。适用于互斥访问。
#define xSemaphoreCreateMutex() xQueueCreateMutex( queueQUEUE_TYPE_MUTEX )
xQueueCreateMutex()
返回值:
NULL :创建失败
其他值:创建成功返回的信号量句柄
示例:
SemaphoreHandle_t xSemaphore;
xSemaphore = xSemaphoreCreateMutex();
if (NULL != xSemaphore) //成功
(3)计数信号量
计数信号量创建的消息队列的长度可以不为1,则计数可以在0到这个数之间。
#define xSemaphoreCreateCounting( uxMaxCount, uxInitialCount ) xQueueCreateCountingSemaphore( ( uxMaxCount ), ( uxInitialCount ) )
xSemaphoreCreateCounting()
参数:
uxMaxCount:计数信号量最大计数值,当信号量值等于此值的时候释放信号量就会失败。
uxInitialCount :计数信号量初始值
返回值:
NULL:创建失败
其他值,创建成功,返回信号量句柄。
示例:
SemaphoreHandle_t xSemaphore;
xSemaphore = xSemaphoreCreateCounting( 10, 0 );
2.发送信号量
#define xSemaphoreGive( xSemaphore ) xQueueGenericSend( ( QueueHandle_t ) ( xSemaphore ), NULL, semGIVE_BLOCK_TIME, queueSEND_TO_BACK )
xSemaphoreGive( xSemaphore )
参数:
xSemaphore :要释放的信号量句柄
返回值:
pdPASS:释放成功
errQUEUE_FULL:释放失败。
示例:
if( xSemaphoreGive( xSemaphore ) != pdTRUE )
{
printf("xSemaphoreGive error!\r\n");
}
3.接收信号量
#define xSemaphoreTake( xSemaphore, xBlockTime ) xQueueSemaphoreTake( ( xSemaphore ), ( xBlockTime ) )
xSemaphoreTake( xSemaphore, xBlockTime )
参数:
xSemaphore:要获取的信号量句柄
xBlockTime :阻塞时间
返回值:
pdTRUE:成功
pdFALSE:失败
示例:
if( xSemaphoreTake( xSemaphore, ( TickType_t ) 10 ) == pdTRUE )
{
//成功
}
4.应用示例
二值信号量优先级翻转测试:创建一个二值信号量,初始时give。设置任务在执行前必须要take到才可以执行,执行完后give释放,这样的话就符合实际中的进程同步,例如高优先级的任务会打断低优先级的任务,如果想让低优先级的任务执行不被打断,那么就用二值信号量,当低优先级任务执行时锁住,让高优先级任务阻塞。任务1与任务3进行信号量访问,任务1可以设置任务时间较长一点,用一个for循环占用时间,以此来测试。任务2正常运行。
SemaphoreHandle_t Binary_Handler = NULL; //二值信号量句柄
//这一段放到初始化MX_FREERTOS_Init()里
{
Binary_Handler = xSemaphoreCreateBinary(); //创建二值信号量if (Binary_Handler == NULL)
{
printf("Semaphore Binary Creat Failed!!!\r\n");
}
xSemaphoreGive(Binary_Handler); //先给一个
}
void StartDefaultTask(void const * argument)
{
long i;
for(;;)
{
if (xSemaphoreTake(Binary_Handler,portMAX_DELAY) == pdTRUE)
{
printf("low task running\r\n");
for (i=0; i<22222222; i++); //让任务1执行时间长
printf("low task give\r\n"); //执行完毕释放
xSemaphoreGive(Binary_Handler);
}
else
printf("low task xSemaphoreTake error!\r\n");
osDelay(1000);
}
}
void StartLEDTask(void const * argument)
{
for(;;)
{
printf("middle task running\r\n");
osDelay(1000);
}
}
void StartHC_SR04Task(void const * argument)
{
for(;;)
{
printf("high task ask the semaphore\r\n");
if (xSemaphoreTake(Binary_Handler,portMAX_DELAY) == pdTRUE)
{
printf("high task running\r\n");
printf("high task give\r\n");
xSemaphoreGive(Binary_Handler);
}
else
printf("high task xSemaphoreTake error!\r\n");
osDelay(1000);
}
}
现象说明:
①高优先级先执行,执行完后osDelay(1000)进入阻塞;
②中优先级执行,阻塞;
③低优先级执行,一直在for,时间较长,还未结束;
④高优先级阻塞完毕,进入就绪,抢占了低优先级使用权,但是由于低优先级还没有执行完毕give,所以高优先级在take处阻塞;
⑤低优先级继续执行for;
⑥中优先级阻塞结束,进入就绪,抢占低优先级执行,执行完后阻塞;
…(低优先级与中优先级交替进行,低优先级在中优先级阻塞的时候继续执行)
⑦低优先级执行结束,give,此时高优先级立马抢占执行。
发现中间高优先级因为take不到被阻塞了,导致优先级更低的中优先级先于它执行,出现优先级翻转现象。