FreeRTOS
一、简介 、特点
FreeRTOS (Free 免费的 Real Time Operate System 实时操作系统)。文件数量比UCOS少(4-9K字节)。特点:可裁剪(通过配置文件里的宏定义),任务数量、优先级不限,支持低功耗的Tickless模式,堆栈溢出检测。
二、源码获取 (官网: www.freertos.org)
1、提取文件
2、实现串口收发(printf重定向,调试)和系统嘀嗒定时器(涉及RTOS的系统时钟)如果实现了timer2定时器先注释,笔者发现两个都初始化后任务创建不成功(timer2定时器中断优先级比较高的原因,调低就可以了)。
systic.c文件里初始化完后要实现中断服务函数,实现delay_us(u32 us) 和 delay_xms(u32 ms) 延时函数,初始化和中断这里跟我们平时写的有差异,只因他是专门服务于FreeRTOS,文件放到博客园下了。
FreeRTOS 心跳由滴答定时器产生,根据 FreeRTOS 的系统时钟节拍设置好滴答定时器的周期,这样就会周期触发滴答定时器中断了。在滴答定时器中断服务函数中调用FreeRTOS 的 API 函数 xPortSysTickHandler()。
如上1、2做完,工程添加完毕后编译:除图二错误可能还有中断函数重复定义等,因为RTOS里又定义了一次,把stm32f10x_it.c里的注释掉。
三、配置FreeRTOSConfig.h
实际使用中通过该文件完成裁剪和配置:INCLUDE 开头的宏表示使能或不使用对应的API函数。如图所示(部分):具体文件已保存到博客园文件下了,F1和F4均通用。
四、任务基础 (创建、删除、挂起、恢复)
1、简介:任何一个时间点只能有一个任务运行,由调度器决定,其职责是保证任务执行时的上下文环境 (寄存器值、堆栈内容等)和任务上一次退出时相同。为此,任务必须有其堆栈(保存上下文环境)。
2、特性:无限制、支持抢占、优先级,任务都有堆栈导致RAM增大,使用抢占须考虑重入问题。
3、状态:运行态、就绪态、阻塞态、挂起态。(由图可知,挂起态任务恢复后不是直接进到运行态,而是进到就绪态,阻塞态同理)
4、优先级:每个任务都能分配0~(configMAX_PRIORITIES-1)的优先级,共32级,0-31,数值越低优先级越低(与单片机中断优先级和UCOS相反),空闲任务优先级最低,为0。同一个优先级时通过时间片轮转调度器获取运行时间。
5、任务创建: xTaskCreate() 动态创建:任务所需RAM从RTOS堆中分配,需提供heap_4.c内存管理文件,宏 configSUPPORT_DYNAMIC_ALLOCATION 须为 1; xTaskCreateStatic() ;静态创建:RAM需用户提供。
创建的任务是就绪态,若没有比它高优先级的任务运行则立即进入运行态。不管调度器是否启用。
int main(void)
TaskHandle_t StartTask_Handler; //任务句柄 ----堆栈大小,优先级等也可以使用宏定义 ,如果没有其它API使用任务句柄也可以为NULL。任务创建定义在主函数最后面,则main.c不需要while(1)。
BaseType_t xTaskCreate( (TaskFunction_t )pxTaskCode, //任务函数 (动态创建) (实际参数如:stat_task)
(const char * )const pcName, //任务名字,一般用于追踪和调试,长度不超过configMAX_TASK_NAME_LEN (如:"stat_task")
(const uint16_t) usStackDepth, //堆栈大小,申请的实际字节为传入数据的4倍。空闲任务堆栈大小为:configMINIMAL_STACK_SIZE (如:128)
(void *) const pvParameters, //传递给任务函数的参数 (如:NULL)
(UBaseType_t) uxPriority, //任务优先级, 0~ configMAX_PRIORITIES-1 值越大优先级越高 (如:3)
(TaskHandle_t *) const pxCreatedTask ) // 成功返回句柄, 其实就是任务堆栈,堆栈就是保存任务句柄,其它API操作这个任务就是使用任务句柄 (如:&StartTask_Handler)
pdPASS :创建成功返回 errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY: 创建失败,堆内存不足(太大空间不够,太小任务创建失败)
如下,图一先创建开始任务,再用开始任务去创建任务函数,图二直接创建任务函数,不要开始任务,实现效果一致,但更简洁:
很多实际运用中就像图二这种创建几个任务函数,实现多任务就够了,什么队列信号量都不用,自写一个 send data() 函数集中处理数据
TaskHandle_t xTaskCreateStatic( TaskFunction_t pxTaskCode, //任务函数 (静态创建)
const char * const pcName, //任务名字
const uint32_t ulStackDepth, //任务堆栈大小,静态创建由用户给,一般是个数组,传入的参数就是数组大小
void * const pvParameters, //传递给任务函数的参数
UBaseType_t uxPriority, //任务优先级, 0~ configMAX_PRIORITIES-1
StackType_t * const puxStackBuffer, //任务堆栈,一般为数组,数组类型: StackType_t
StaticTask_t * const pxTaskBuffer ) //任务控制块
NULL:创建失败,puxStackBuffer 或 pxTaskBuffer 为 NULL时 其它值:任务创建成功,返回任务句柄
xTaskCreateRestricted() :此函数创建的任务会受到MPU的保护,要求MCU有MPU(内存保护单元),其它功能和 xTaskCreate() 一致。
6、任务删除:删除后不会进入运行态,也不能再使用任务句柄,动态创建的任务删除后空间被释放,但如果任务中调用 pvPortMalloc()分配的则删除任务后调用vPortFree()释放,否则内存泄漏。
vTaskDelete( TaskHandle_t xTaskToDelete ) 参数:要删除的任务句柄
7、任务挂起和恢复
(1)、挂起:void vTaskSuspend( TaskHandle_t xTaskToSuspend) 参数:要挂起的任务句柄 参数为NULL表示挂起任务自己。特性:恢复后任务中变量保存的值不变。
(2)、恢复:void vTaskResume( TaskHandle_t xTaskToResume) 参数:要恢复的任务句柄
(3)、恢复(中断版本):BaseType_t xTaskResumeFromISR( TaskHandle_t xTaskToResume) 用于在中断服务函数中恢复一个任务。
返回值pdTRUE:优先级 >= 正在运行的任务,退出中断函数后必须进行一次上下文切换。pdFALSE:优先级 < 正在运行的任务,退出中断函数后不用进行上下文切换。
五、任务查询、统计相关API(调试用)
1、void vTaskList( char * pcWriteBuffer) 创建一个表格列出所有任务信息(任务名称,优先级,历史最低堆栈大小),参数:pcWriteBuffer :保存任务状态信息表的存储区,要大,可动态申请。
IDLE:空闲任务 Tmr:定时器任务
六、内核控制函数(这些函数本质上是一个宏)
七、临界段代码保护
1、简介:指必须完整运行,不能被打断的代码段,如有的外设初始化需要严格的时序,如模拟IIC等,进入临界段代码时需要关中断,当处理完临界段代码后再开中断。FreeRTOS 系统本身就有很多的临界段代码, 这些代码都加了临界段代码保护。
2、任务级临界段使用方法:
void taskcritical_test(void){ //任务级临界段代码保护使用方法
while(1) {
taskENTER_CRITICAL(); (1) //进入临界区
total_num+=0.01f; //中间的是临界区代码,一定要精简,因为进入临界区会关闭中断,这样会导致优先级低于configMAX_SYSCALL_INTERRUPT_PRIORITY的中断
printf("total_num 的值为: %.4f\r\n",total_num); //得不到及时响应(如:两个定时器中断,一个定时器优先级高于宏,高于宏的可以正常运行,低于宏的不能运行,退出时才可以运行)。
taskEXIT_CRITICAL(); (2) //退出临界区
vTaskDelay(1000);
}
}
3、中断级临界段使用方法:
void TIM3_IRQHandler(void) {
if(TIM_GetITStatus(TIM3,TIM_IT_Update)==SET) { //溢出中断
status_value=taskENTER_CRITICAL_FROM_ISR(); (1) //进入临界区
total_num+=1;
printf("float_num 的值为: %d\r\n",total_num);
taskEXIT_CRITICAL_FROM_ISR(status_value); (2) //退出临界区
}
TIM_ClearITPendingBit(TIM3,TIM_IT_Update); //清除中断标志位
}
(1)、设置定时器 3 的抢占优先级为 4,高于 configMAX_SYSCALL_INTERRUPT_PRIORITY, 因此在调用函数 portDISABLE_INTERRUPTS()关闭中断的时候定时器 3 是不会受影响的。
(2)、设置定时器 5 的抢占优先级为 5,等于 configMAX_SYSCALL_INTERRUPT_PRIORITY, 因此在调用函数 portDISABLE_INTERRUPTS()关闭中断的时候定时器 5 中断肯定会被关闭的。
八、消息队列
1、简介:队列可在任务与任务、任务与中断间传递消息,队列中可以存储有限的、大小固定的数据项目。队列能保存的最大数据项目数量叫队列长度(如char类型的数据项大小为1),创建队列时会指定数据项大小和队列长度。
FreeRTOS 中信号量也是依据队列实现的。
2、特点:
(1)、数据存储:通常采用先进先出(FIFO)的缓冲机制,也可以使用 LIFO 的存储缓冲,即后进先出。数据发送到队列中会导致数据拷贝,即值传递(虽浪费一点时间,但消息发送到队列后原始的数据缓冲区就可以删除或覆写)。如果数据量大也可以向队列中发送指向这个消息的地址(址传递:址传递的话消息内容必须一直保持可见性)。
(2)、多任务访问:队列不属于某个指定的任务。每个任务都可以向队列发送和读取消息。
(3)、出队阻塞:任务从一个队列中读取消息时可指定阻塞时间(即等待消息),时间单位为时钟节拍数。为0则不阻塞。
(4)、入队阻塞:队列满的情况下发送肯定失败。
如图,此时队列剩余长度就是三了,发送完成x变量可以再次使用
任务 B 从队列中读取完成后可清除消息或不清除。如过清除则其他任务或中断就不能获取这个消息,而队列剩余大小就会加一。
3、创建队列(下列函数中相同的参数和返回值就没有重复说明)
(1)、QueueHandle_t xQueueCreate (UBaseType_t uxQueueLength, UBaseType_t uxItemSize) //返回值:成功:返回队列句柄,失败:返回NULL。
参数 :uxQueueLength :队列长度,即队列的项目数
uxItemSize:队列中每个项目(消息)的长度,单位:字节
(2)、静态创建:QueueHandle_t xQueueCreateStatic(UBaseType_t uxQueueLength,UBaseType_t uxItemSize, uint8_t * pucQueueStorageBuffer, StaticQueue_t * pxQueueBuffer) 参数:前两个与上面函数一致 ,返回同上。
参数 :pucQueueStorageBuffer:指向自行分配的uint8_t 类型消息存储数组,内存大于等于(uxQueueLength * uxItemsSize)字节。
pxQueueBuffer: 此参数指向一个 StaticQueue_t 类型的变量,用来保存队列结构体。
(3)、动态创建:QueueHandle_t xQueueGenericCreate( const UBaseType_t uxQueueLength, const UBaseType_t uxItemSize, const uint8_t ucQueueType )
参数 : ucQueueType :队列类型,由于信号量也是通过队列实现,也是通过此函数创建,因此创建时要指定用途,即类型:六种
(1)、BaseType_t xQueueSend( QueueHandle_t xQueue, const void * pvItemToQueue, TickType_t xTicksToWait) //返回值:pdPASS: 成功 errQUEUE_FULL: 失败,队列满。
参数:xQueue: 队列句柄,指明向哪个队列的句柄发送数据。
pvItemToQueue:指向要发送的消息,发送时候会将这个消息拷贝到队列中。
xTicksToWait: 阻塞时间,当队列满时任务进入阻塞态等待,0:队列满立即返回。portMAX_DELAY:死等,但宏INCLUDE_vTaskSuspend必须为1。
(2)、此函数才是真正干活的,其它所有的任务级入队函数最终都是调用此函数:
BaseType_t xQueueGenericSend( QueueHandle_t xQueue, const void * const pvItemToQueue, TickType_t xTicksToWait, const BaseType_t xCopyPosition )
参数 :xCopyPosition: 入队方式,有三种入队方式:queueSEND_TO_BACK: 后向入队
参数 :pxHigherPriorityTaskWoken: 标记退出此函数后是否进行任务切换,此变量值自动设置,用户只需提供一 个变量来保存。当此值为 pdTRUE 时:退出中断服务函数前必须进行一次任务切换。
这些函数都是在中断中,不在任务中,所以没有阻塞
(4)、此函数才是真正干活的,其它所有的中断级入队函数最终都是调用此函数:
BaseType_t xQueueGenericSendFromISR(QueueHandle_t xQueue, const void* pvItemToQueue, BaseType_t* pxHigherPriorityTaskWoken, BaseType_t xCopyPosition)
(1)、BaseType_t xQueueReceive(QueueHandle_t xQueue, void * pvBuffer, TickType_t xTicksToWait) 返回: 读取成功:pdTRUE 读取失败:pdFALSE
详解:读取成功会将队列中的这条数据删除,本质是个宏,真正执行的函数是 xQueueGenericReceive(),读取消息时采用拷贝方式,需要提供一个数组或缓冲区来保存,读取长度为创建队列时每个队列项目的长度。
参数:xQueue: 队列句柄,读取哪个队列数据(2)、BaseType_t xQueuePeek(QueueHandle_t xQueue, void * pvBuffer, TickType_t xTicksToWait) 与a函数区别为:读取成功不会将队列中的这条数据删除。
(3)、 BaseType_t xQueueGenericReceive(QueueHandle_t xQueue, void* pvBuffer, TickType_t xTicksToWait ,BaseType_t xJustPeek) //此函数才是真正干活的,如上a、b函数最终都是调用它:
参数:标记读取成功后是否删除队列项,为pdTRUE时不删除,a函数参数,pdFALSE删除,b函数参数。
(4)、此函数是a函数的中断版本:BaseType_t xQueueReceiveFromISR(QueueHandle_t xQueue, void* pvBuffer, BaseType_t * pxTaskWoken)
参数:中断没有阻塞概念,pxTaskWoken:标记此函数退出后是否进行任务切换,此变量自动设置,提供一个变量保存这个值即可,值为pdTRUE 时需要切换
(5)、此函数是b函数的中断版本:BaseType_t xQueuePeekFromISR(QueueHandle_t xQueue, void * pvBuffer)
7、获取队列消息数量和剩余大小:uxQueueSpacesAvailable()获取队列剩余大小;uxQueueMessagesWaiting()获取队列当前消息数量;
九、信号量
1、简介:信号量一般用来进行资源管理和任务同步,信号量分为二值信号量、计数型信号量、互斥信号量和递归互斥信号量。
二值信号量
2、二值信号量:(1)、二值信号量其实就是一个只有一个队列项的队列,此队列要么是满的,要么是空 的,这不正好就是二值吗?
通常用于互斥访问或同步,与互斥信号量类似,区别在于互斥信号量拥有优先级继承机制,二值信号量没有。因此更适合用于任务与任务或任务与中断间的同步。而互斥信号量适用于简单的互斥访问。
(2)、应用场景:网络中,一般用一个任务去轮询查询 MCU 的 ETH(如 STM32 的以太网MAC)外设是否有数据,轮询即浪费CPU资源,又阻止了其他任务运行。理想是无数据时阻塞把CPU让出。如:使用二值信号量,任务通过获取信号量判断是否有数据,没有则阻塞,(一般通过中断判断是否有数据,如STM32的MAC的DMA中断)中断服务函数释放信号量,任务接收信号量。也可使用队列替代,在中断中发送数据到队列,队列无效任务阻塞,直至有数据。
3、创建二值信号量:和队列一样,要想使用就要先创建二值信号量:
(1)、SemaphoreHandle_t xSemaphoreCreateBinary( void ) 此函数创建时 RAM 由 FreeRTOS 内存管理部分来动态分配。创建好的二值信号量默认为空,即使用函数 xSemaphoreTake()获取不到,相当于宏。
(2)、如下函数创建信号量所需要的RAM由用户分配,相当于宏,具体是调用 xQueueGenericCreateStatic() 函数实现。
SemaphoreHandle_t xSemaphoreCreateBinaryStatic( StaticSemaphore_t *pxSemaphoreBuffer ) //返回:创建失败: NULL, 成功:二值信号量句柄
参数:pxSemaphoreBuffer :此参数指向一个 StaticSemaphore_t 类型的变量,用来保存信号量结构体
4、优先级翻转
使用二值信号量时常见的一个问题——优先级翻转,在可剥夺内核中比较常见,但在实时系统中不允许出现这种现象,这样会破坏任务的预期顺序,可能会导致严重的后果。如下示意图:
如上, H 优先级实际上降到了 L 的水平。因为 H 要一直等待直到 L 释放其占用的共享资源。由于 M 剥夺了 L 的 CPU 使用权,使得 H 情况更糟,就相当于 M 的优先级高于 H,导致优先级翻转。
注意:优先级翻转只会在使用共享资源时发生,因此在任务设计中应该尽可能避免共享资源的竞争。合理使用优先级继承和优先级屏蔽也能避免此问题,如互斥信号量。
递归互斥信号量
1、简介:特殊互斥信号量,已经获取了互斥信号量的任务就不能再次获取这个互斥信号量,但递归互斥信号量中可以再次获取,且次数不限。
2、创建:(这里不详解了)
计数型信号量
1、简介:二值信号量相当于长度为 1 的队列,那么 计数型信号量就是长度大于 1 的队列。同二值信号量一样,用户不需要关心队列中存储了什么 数据,只需关心队列是否为空即可。
2、如何使用:事件发生时在处理函数中释放信号量(增加信号量的计数值),其他任务获取信号量(信号量计数值减一,值为队列结构体变量uxMessagesWaiting)处理事件。值为 0 说明没有资源了 ,任务使用完资源后要释放信号量,创建的信号量初值为 0。
3、创建计数型信号量:
(1)、SemaphoreHandle_t xSemaphoreCreateCounting(UBaseType_t uxMaxCount, UBaseType_t uxInitialCount ) //所需内存动态分配,本质为宏,真正完成创建的是 xQueueCreateCountingSemaphore() 。
(2)、SemaphoreHandle_t xSemaphoreCreateCountingStatic( UBaseType_t uxMaxCount, UBaseType_t uxInitialCount, StaticSemaphore_t * pxSemaphoreBuffer ) //返回:失败:NULL 成功:句柄
内存用户分配,相当于宏,执行函数:xQueueCreateCountingSemaphoreStatic()
释放信号量(同队列一样,不管什么类型信号量,释放时也分为任务级和中断级)
1、BaseType_t xSemaphoreGive( xSemaphore ) //参数:信号量句柄 返回:成功:pdPASS 失败:errQUEUE_FULL
2、如下函数用于中断中释放信号量,只能释放二值信号量和计数型信号量,不能用来在中断中释放互斥信号量。
BaseType_t xSemaphoreGiveFromISR( SemaphoreHandle_t xSemaphore, BaseType_t * pxHigherPriorityTaskWoken) //返回:成功:pdPASS 失败:errQUEUE_FULL
参数: xSemaphore : 信号量句柄
pxHigherPriorityTaskWoken:退出后是否任务切换,此变量自动设置,提供一个变量保存这个值即可,为 pdTRUE 时需要切换。
获取信号量(不管什么类型信号量,都使用如下函数获取)
1、BaseType_t xSemaphoreTake(SemaphoreHandle_t xSemaphore, TickType_t xBlockTime) //返回:pdTRUE:获取信号量成功 pdFALSE:超时,获取失败
参数:xSemaphore:要获取的信号量句柄
xBlockTime: 阻塞时间
2、BaseType_t xSemaphoreTakeFromISR(SemaphoreHandle_t xSemaphore, BaseType_t * pxHigherPriorityTaskWoken) //返回:pdPASS: 获取信号量成功 pdFALSE: 获取失败
参数: pxHigherPriorityTaskWoken: 标记退出函数后是否任务切换,此变量自动设置,提供一个变量保存这个值即可,为 pdTRUE 时需要切换。
十、事件标志组(不详解)
1、简介:使用信号量只能与单个的事件或任务进行同步。有时某个任务可能需要与多个事件或任务进行同步,为此FreeRTOS 提供了事件标志组。
2、创建事件标志组:
3、设置事件位:
4、 获取事件标志组值:
十一、任务通知
从 v8.2.0 版本开始,FreeRTOS 新增了任务通知(Task Notifictions)功能,可以用来代替信号量、消息队列、事件标志组等这些东西。且效率更高。可选功能,使用时将宏 configUSE_TASK_NOTIFICATIONS 定义为 1。
1、简介:任务通知是一个事件,假如某个任务通知的接收任务因为等待任务通知而阻塞的话,向这个接收任务发送任务通知后就会解除这个任务的阻塞状态,也可以更新接收任务的任务通知值:
● 不覆盖接收任务的通知值(如果上次发送给接收任务的通知还没被处理)。
● 覆盖接收任务的通知值。
● 更新接收任务通知值的一个或多个 bit。
● 增加接收任务的通知值。
2、特点:● 只能有一个接收任务,其实大多数的应用都是这种情况。
● 接收任务可以因为接收任务通知失败而进入阻塞态,但是发送任务不会因为任务通知发送失败而阻塞。
● 只能发送32位的数据值(可以模拟一个轻量级消息邮箱而不是轻量级消息队列,任务通知值就是消息邮箱的值)。
3、发送任务通知:
(1)、BaseType_t xTaskNotify( TaskHandle_t xTaskToNotify, uint32_t ulValue, eNotifyAction eAction ) //此函数是个宏,真正执行的函数 xTaskGenericNotify()
参数:xTaskToNotify: 任务句柄,指定任务通知是发送给哪个任务的。
4、获取任务通知:
(1)、uint32_t ulTaskNotifyTake( BaseType_t xClearCountOnExit, TickType_t xTicksToWait )
参数:xClearCountOnExit: 参数为 pdFALSE:在退出函数时任务通知值减一,类似计数型信号量。参数为 pdTRUE:在退出函数时任务任务通知值清零,类似二值信号量。
十、软件定时器
FreeRTOS 提供了软件定时器功能,不过精度没有MCU 自带硬件定时器高,但 对于精度要求不高的周期性处理任务来说够了。(不详解)