1. RTOS简介
1.1 基本概念
- 多任务
- 处理能被区分优先次序的进程线
- 一个中断水平的充分数量
实时操作系统(RTOS)是指当外界事件或数据产生时,能够接受并足够快的速度予以处理,其处理的结果又能在规定的时间之内来控制生产过程或对处理系统做出快速响应,调度一切可利用的资源完成实时任务,并控制所有实时任务协调一致运行的操作系统
提供及时响应和高可靠性是其主要特点
实时操作系统是保证在一定时间限制内完成特定功能的操作系统。实时操作系统有硬实时和软实时之分,硬实时要求在规定的时间内必须完成操作,这是在操作系统设计时保证的;软实时则只要按照任务的优先级,尽可能快地完成操作即可。我们通常使用的操作系统在经过一定改变之后就可以变成实时操作系统
1.2 基本名词
- 代码临界段:指处理时不可分割的代码。一旦这部分代码开始执行则不允许中断打入
- 资源:任何为任务所占用的实体
- 共享资源:可以被一个以上任务使用的资源
- 任务:也称作一个线程,是一个简单的程序。每个任务被赋予一定的优先级,有它自己的一套CPU寄存器和自己的栈空间。典型地,每个任务都是一个无限的循环,每个任务都处在以下五个状态:休眠态、就绪态、运行态、挂起态、被中断态
- 任务切换:将正在运行任务的当前状态(CPU寄存器中的全部内容)保存在任务自己的栈区,然后把下一个将要运行的任务的当前状态从该任务的栈中重新装入CPU的寄存器,并开始下一个任务的运行
- 内核:负责管理各个任务,为每个任务分配CPU时间,并负责任务之间的通讯。分为不可剥夺型内核
- 调度:内核的主要职责之一,决定轮到哪个任务运行。一般基于优先级调度法
1.3 FreeRTOS
FreeRTOS是一个迷你的实时操作系统内核。作为一个轻量级的操作系统,功能包括:任务管理、时间管理、信号量、消息队列、内存管理、记录功能、软件定时器、协程等,可基本满足较小系统的需要
由于RTOS需占用一定的系统资源(尤其是RAM资源),只有μC/OS-II、embOS、salvo、FreeRTOS等少数实时操作系统实时操作系统能在小RAM单片机上运行。相对μC/OS-II、embOS等商业操作系统,FreeRTOS操作系统是完全免费的操作系统,具有源码公开、可移植、可裁减、调度策略灵活的特点,可以方便地移植到各种单片机上运行
FreeRTOS知识框架:
2. 任务
2.1 基本属性
2.1.1 优先级
-
每个任务分配一个从0~configMAX_PRIORITIES-1的优先级,优先级的数字越低表示任务的优先级越低 -
高优先级抢占低优先级:
-
时间片轮转:当宏configUSE_TIME_SLICING定义为1的时候,多个任务可以共用一个优先级,数量不限。此时处于就绪态的优先级相同的任务就会使用时间片轮转调度器获取运行时间
2.1.2 任务控制块/任务堆栈
任务控制块
- FreeRTOS的每个任务都有一些属性需要存储,FreeRTOS把这些属性集合到一起用一个结构体来表示,这个结构体叫做任务控制块
- 属性:任务名字、优先级、任务堆栈大小、任务句柄等
任务堆栈
FreeRTOS之所以能正确的恢复一个任务的运行就是因为有任务堆栈在保驾护航,任务调度器在进行任务切换的时候会将当前任务现场(CPUI寄存器值等)保存在此任务堆栈中,等到此任务下次运行的时候就会先用堆栈中保存的值来恢复现场,恢复现场以后任务就会接着从上次中断的地方开始运行
2.2 状态
- 运行态
- 就绪态
- 阻塞态
- 挂起态
2.3 操作
创建——删除
挂起——恢复
3. 机制简介
3.1 队列
- 所有的通信和同步机制都是基于队列实现的
- 队列不但可以传递数组,也可以传递结构体
- 队列是为了任务与任务、任务与中断之间的通信而准备的,可以在任务与任务、任务与中断之间传递消息,队列中可以存储有限的、大小固定的数据项目
- 队列所能保存的最大数据项目数量叫做队列的长度,创建队列的时候会指定数据项目的大小和队列的长度
- 队列不是属于某个特定任务的,任何任务都可以向队列中发送消息,或者从队列中提取消息
3.2 信号量
信号量是深度为1的队列
3.3 任务通知
任务通知来替代信号量、消息队列、事件标志组等这些东西。使用任务通知的话效率会更高
3.4 低功耗模式
3.4.1 STM32支持的低功耗模式
- 睡眠模式:CM3内核停止,外设仍然运行
- 停止模式:所有时钟都停止
- 待机模式:1.8V内核电源关闭
3.4.2 空闲任务的钩子函数实现的低功耗
-
通过空闲任务钩子函数(或称回调,hook,or call-back),可以直接在空闲任务中添加应用程序相关的功能。空闲任务钩子函数会被空闲任务没循环一次就调用一次 -
通常空闲任务钩子函数被用于:
- 执行低优先级,后台或需要不停处理的功能代码
- 测试处系统处理裕量(空闲任务只会在所有其他任务都不运行时才有机会执行,所以测量出空闲任务占用的处理时间就可以清楚的知道系统有多少富裕的处理时间)
- 将处理器配置到低功耗模式——提供一种自动省电方法,使得在没有任何应用功能需要处理的时候,系统自动进入省电模式
-
FreeRTOS是通过在处理器处理空闲任务的时候将处理器设置为低功耗模式来降低能耗。一般会在空闲任务的钩子函数中执行低功耗相关处理,比如设置处理器进入低功耗模式(见3.4.1)、关闭其他外设时钟、降低系统主频等等
3.4.3 Tickless模式
FreeRTOS系统提供的低功耗模式,当处理器进入空闲任务周期以后就关闭系统节拍中断(滴答定时器中断),只有当其他中断发生或者其他任务需要处理的时侯处理器才会从低功耗模式中唤醒
3.5 列表和列表项
- 列表是FreeRTOS中的一个数据结构,概念上和链表有点类似,列表被用来跟踪FreeRTOS中的任务
- 列表项对应C语言中的结点
- 作用:用于追踪FreeRTOS的任务,内核层面
3.6 内存管理
内存管理是一个系统基本组成部分,FreeRTOS中大量使用到了内存管理,比如创建任务、信号量、队列等会自动从堆中申请内存。用户应用层代码也可以用FreeRTOS提供的内存管理函数来申请和释放内存
4. 任务相关API函数
4.1 任务创建和删除
4.1.1 创建:xTaxkCreate()
功能
- 任务需要RAM来保存与任务有关的状态信息(任务控制块),任务也需要一定的RAM来作为任务堆栈,该函数会自动从FreeRTOS的堆中为任务分配所需的RAM
- 新创建的任务默认就是就绪态的,如果当前没有比它更高优先级的任务运行,那么此任务就会立即进入运行态开始运行
- 不管在任务调度器启动前还是启动后,都可以创建任务
注意条件
- 需要提供内存管理文件(默认使用heap_4.c)
- 宏configSUPPORT_DYNAMIC_ALLOCATION 必须为1
函数原型
BaseType_t xTaskCreate( TaskFunction_t pxTaskCode,
const char * const pcName,
const uint16_t usStackDepth,
void * const pvParameters,
UBaseType_t uxPriority,
TaskHandle_t * const pxCreatedTask )
参数
- pxTaskCode:任务函数
- pcName:任务名字,一般用于追踪和调试,任务名字长度不能超过 configMAX_TASK_NAME_LEN
- usStackDepth:任务堆栈大小,注意实际申请到的堆栈是usStackDepth的四倍。其中空闲任务的任务堆栈大小为configMINIMAL_STACK_SIZE
- pvParameters:传递给任务函数的参数
- uxPriotiry:任务优先级,范围0~configMAX_PRIORITIES-1
- pxCreatedTask:任务句柄,任务创建成功以后会返回此任务的任务句柄,这个句柄其实就是任务的任务堆栈。此参数就用来保存这个任务句柄。其他API函数可能会使用到这个句柄
返回值
- pdPASS:任务创建成功
- errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY:任务创建失败,因为堆内存不足
4.1.2 删除:vTaskDelete()
功能
- 删除一个用函数 xTaskCreate()或者 xTaskCreateStatic()创建的任务,被删除了的任务不再存 在,也就是说再也不会进入运行态。任务被删除以后就不能再使用此任务的句柄
注意条件
- 如果此任务是使用动态方法创建的,也就是使用函数 xTaskCreate()创建的,那么在此任务被删除以后此任 务之前申请的堆栈和控制块内存会在空闲任务中被释放掉,因此当调用函数 vTaskDelete()删除任务以后必须给空闲任务一定的运行时间
- 只有那些由内核分配给任务的内存才会在任务被删除以后自动的释放掉,用户分配给任务 的内存需要用户自行释放掉,比如某个任务中用户调用函数 pvPortMalloc()分配了 500 字节的内 存,那么在此任务被删除以后用户也必须调用函数 vPortFree()将这 500 字节的内存释放掉,否 则会导致内存泄露
函数原型
vTaskDelete( TaskHandle_t xTaskToDelete )
参数
xTaskToDelete:要删除的任务的任务句柄
4.1.3 任务创建和删除实验
(1) 用宏来表示任务的优先级、堆栈大小(便于调试修改),并定义任务句柄和声明任务函数
#define START_TASK_PRIO 1
#define START_STK_SIZE 128
TaskHandle_t StartTask_Handler;
void start_task(void *pvParameters);
(2) 通常会定义一个开始任务任务函数,用来创建其他的任务
void start_task(void *pvParameters)
{
taskENTER_CRITICAL();
xTaskCreate((TaskFunction_t )led0_task,
(const char* )"led0_task",
(uint16_t )LED0_STK_SIZE,
(void* )NULL,
(UBaseType_t )LED0_TASK_PRIO,
(TaskHandle_t* )&LED0Task_Handler);
xTaskCreate((TaskFunction_t )led1_task,
(const char* )"led1_task",
(uint16_t )LED1_STK_SIZE,
(void* )NULL,
(UBaseType_t )LED1_TASK_PRIO,
(TaskHandle_t* )&LED1Task_Handler);
vTaskDelete(StartTask_Handler);
taskEXIT_CRITICAL();
}
(3) main()函数,设置系统中断优先级分组,初始化外设,创建开始任务,开启FreeRTOS的任务调度器
int main(void)
{
HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4);
HAL_Init();
Stm32_Clock_Init(RCC_PLL_MUL9);
delay_init(72);
uart_init(115200);
LED_Init();
KEY_Init();
usmart_dev.init(84);
xTaskCreate((TaskFunction_t )start_task,
(const char* )"start_task",
(uint16_t )START_STK_SIZE,
(void* )NULL,
(UBaseType_t )START_TASK_PRIO,
(TaskHandle_t* )&StartTask_Handler);
vTaskStartScheduler();
}
4.2 任务的挂起和恢复
任务挂起、恢复和任务删除、重建的区别:保存任务运行的数据不会丢失
4.2.1 挂起:vTaskSuspend()
功能
- 用于将某个任务设置为挂起态,进入挂起态的任务永远都不会进入运行态
- 退出挂起态的唯一方法是调用任务恢复函数vTaskResume()或xTaskResumeFromISR()
函数原型
void vTaskSuspend( TaskHandle_t xTaskToSuspend)
参数
xTaskToSuspend:要挂起的任务的句柄,创建任务的时候会为每个任务分配一个任务句柄。若使用函数xTaskCreate()创建任务的话,那么函数的参数pxCreatedTask就是此任务的任务句柄
注:如果参数为NULL的话表示挂起任务自己
4.2.2 恢复:vTaskResume()
功能
将一个任务从挂起态恢复到就绪态
注意条件
只有通过函数vTaskSuspend()设置为挂起态的任务才可以使用vTaskRexume()恢复
函数原型
void vTaskResume( TaskHandle_t xTaskToResume)
参数
xTaskToResume:要恢复的任务的任务句柄
4.2.3 恢复:xTaskResumeFromISR()
功能
此函数是vTaskResume()的中断版本,用于在中断服务函数中恢复一个任务
函数原型
BaseType_t xTaskResumeFromISR( TaskHandle_t xTaskToResume)
参数
xTaskToResume:要恢复的任务的任务句柄
返回值
- pdTRUE:恢复运行的任务的任务优先级等于或者高于正在运行的任务(被中断打断的任务),这意味着在退出中断服务函数以后必须进行一次上下文切换
- pdFALSE:恢复运行的任务的任务优先级低于当前正在运行的任务(被中断打断的任务),这意味着在退出中断服务函数的以后不需要进行上下文切换
5. 中断配置和临界段
5.1 Cortex-M中断
- 中断由硬件产生,Cortex-M内核的MCU提供了一个用于中断管理的嵌套向量中断控制器(NVIC)
- STM32有5个优先级分组,而移植FreeRTOS的时候我们配置的是组4,能够提供0~15共16个优先级,且全为抢占优先级
5.2 临界段代码
5.2.1 简介
- 临界段代码也叫做临界区,是指那些必须完整运行,不能被打断的代码段
- FreeRTOS在进入临界段代码的时候需要关闭中断,当处理完临界段代码以后再打开中断
5.2.2 任务级
taskENTER_CRITICAL()和 taskEXIT_CRITICAL()是任务级的临界代码保护,一个是进入临 界段,一个是退出临界段,这两个函数是成对使用的
使用方法:
void taskcritical_test(void)
{
while(1)
{
taskENTER_CRITICAL();
total_num+=0.01f;
printf("total_num 的值为: %.4f\r\n",total_num);
taskEXIT_CRITICAL();
vTaskDelay(1000);
}
}
注意:临界区代码一定要精简!因为进入临界区会关闭中断,这样会导致优先级低于configMAX_SYSCALL_INTERRUPT_PRIORITY的中断得不到及时的响应
5.2.3 中断级
函数taskENTER_CRITICAL_FROM_ISR()和taskEXIT_CRITICAL_FROM_ISR()中断级别临界段代码保护,是用在中断服务程序中的,而且这个中断的优先级一定要低于configMAX_SYSCALL_INTERRUPT_PRIORITY
使用方法:
void TIM3_IRQHandler(void)
{
if(TIM_GetITStatus(TIM3,TIM_IT_Update)==SET)
{
status_value=taskENTER_CRITICAL_FROM_ISR();
total_num+=1;
printf("float_num 的值为: %d\r\n",total_num);
taskEXIT_CRITICAL_FROM_ISR(status_value);
}
TIM_ClearITPendingBit(TIM3,TIM_IT_Update);
}
6. 队列相关API函数
6.1 队列结构体
typedef struct QueueDefinition
{
int8_t *pcHead;
int8_t *pcTail;
int8_t *pcWriteTo;
union
{
int8_t *pcReadFrom;
UBaseType_t uxRecursiveCallCount;
} u;
List_t xTasksWaitingToSend;
List_t xTasksWaitingToReceive;
volatile UBaseType_t uxMessagesWaiting;
UBaseType_t uxLength;
UBaseType_t uxItemSize;
volatile int8_t cRxLock;
volatile int8_t cTxLock;
#if((configSUPPORT_STATIC_ALLOCATION == 1)&&\(configSUPPORT_DYNAMIC_ALLOCATION == 1))
uint8_t ucStaticallyAllocated;
#endif
#if ( configUSE_QUEUE_SETS == 1 )
struct QueueDefinition *pxQueueSetContainer;
#endif
#if ( configUSE_TRACE_FACILITY == 1 )
UBaseType_t uxQueueNumber;
uint8_t ucQueueType;
#endif
}xQUEUE;
typedef xQUEUE Queue_t;
6.2 队列的创建
6.2.1 动态创建:xQueueCreate()
功能
动态创建队列
函数原型
QueueHandle_t xQueueCreate(UBaseType_t uxQueueLength,UBaseType_t uxItemSize)
参数
- uxQueueLength:要创建的队列的队列长度,这里是队列的项目数
- uxItemSize:队列中每个项目(消息)的长度,单位为字节
返回值
- 其他值:队列创建成功以后返回的队列句柄
- NULL:队列创建失败
6.3 队列消息的发送
6.3.1 入队:xQueueSend()
功能
- 用于向队列中发送消息,是后向入队,即将新的消息插入到队列后面
- 本质上是一个宏,调用的是xQueueGenericSend()
函数原型
BaseType_t xQueueSend( QueueHandle_t xQueue,
const void * pvItemToQueue,
TickType_t xTicksToWait);
参数
- xQueue:任务句柄,指明要向哪个队列发送数据,在创建队列成功的时候会返回此队列的队列句柄
- pvItemToQueue:指向要发送的消息,发送时候会将这个消息拷贝到队列中
- xTicksToWait:阻塞时间,此参数指示当队列满的时候任务进入阻塞态等待队列空闲的最大时间。如果为0的话当队列满的时候就立即返回;当为portMAX_DELAY的话就会一直等待,直到队列有空闲的队列项,也就是死等,但是宏INCLUDE_vTaskSuspend必须为1
返回值
- pdPASS:向队列发送消息成功
- errQUEUE_FULL:队列已经满了,消息发送失败
6.4 队列消息的读取
6.4.1 出队:xQueueReceive
功能
- 用于在任务中从队列中读取一条(请求)消息,读取成功以后就会将队列中的这条数据删除
- 本质上是一个宏,调用的是xQueueGenericReceive()
注意条件
此函数在读取消息的时候采用的是拷贝的方式,所以用户需要提供一个数组或缓冲区来保存读取到的数据,所读取的数据长度是创建队列的时候所设定的每个队列项目的长度
函数原型
BaseType_t xQueueReceive(QueueHandle_t xQueue,
void * pvBuffer,
TickType_t xTicksToWait);
参数
-
xQueue:队列句柄,指明要读取哪个队列的数据,创建队列成功的时候会返回此队列的队列句柄 -
pvBuffer:保存数据的缓冲区,读取队列的过程中会将读取到的数据拷贝到这个缓冲区中 -
xTicksToWait:阻塞时间,此参数指示当队列空的时候任务进入阻塞态等待队列有数据的最大时间。如果为0的话当队列空的时候就立即返回;当为portMAX_DELAY的话就会一直等待,直到队列有数据,也就是死等,当时宏INCLUDE_vTaskSuspend必须为1
返回值
- pdTRUE:从队列中读取数据成功
- pdFALSE:从队列中读取数据失败
6.5 队列操作实验
|