实时操作系统(RTOS)是指当外界事件或数据产生时,能够接受并足够快的速度予以处理,其处理的结果又能在规定的时间之内来控制生产过程或对处理系统做出快速响应,调度一切可利用的资源完成实时任务,并控制所有实时任务协调一致运行的操作系统
提供及时响应和高可靠性是其主要特点
实时操作系统是保证在一定时间限制内完成特定功能的操作系统。实时操作系统有硬实时和软实时之分,硬实时要求在规定的时间内必须完成操作,这是在操作系统设计时保证的;软实时则只要按照任务的优先级,尽可能快地完成操作即可。我们通常使用的操作系统在经过一定改变之后就可以变成实时操作系统
FreeRTOS是一个迷你的实时操作系统内核。作为一个轻量级的操作系统,功能包括:任务管理、时间管理、信号量、消息队列、内存管理、记录功能、软件定时器、协程等,可基本满足较小系统的需要
由于RTOS需占用一定的系统资源(尤其是RAM资源),只有μC/OS-II、embOS、salvo、FreeRTOS等少数实时操作系统实时操作系统能在小RAM单片机上运行。相对μC/OS-II、embOS等商业操作系统,FreeRTOS操作系统是完全免费的操作系统,具有源码公开、可移植、可裁减、调度策略灵活的特点,可以方便地移植到各种单片机上运行
FreeRTOS知识框架:
每个任务分配一个从0~configMAX_PRIORITIES-1的优先级,优先级的数字越低表示任务的优先级越低
高优先级抢占低优先级:
当一个任务A正在运行,另一个任务B(优先级高于A)阻塞时间到或者时间触发处于就绪态,那么B会从A那抢占处理器,B开始运行,A停止运行
FreeRTOS调度器确保处于就绪态或运行态的高优先级的任务获取处理器的使用权,换句话说就是处于就绪态的最高优先级的任务才会运行
时间片轮转:当宏configUSE_TIME_SLICING定义为1的时候,多个任务可以共用一个优先级,数量不限。此时处于就绪态的优先级相同的任务就会使用时间片轮转调度器获取运行时间
FreeRTOS之所以能正确的恢复一个任务的运行就是因为有任务堆栈在保驾护航,任务调度器在进行任务切换的时候会将当前任务现场(CPUI寄存器值等)保存在此任务堆栈中,等到此任务下次运行的时候就会先用堆栈中保存的值来恢复现场,恢复现场以后任务就会接着从上次中断的地方开始运行
创建——删除
挂起——恢复
信号量是深度为1的队列
任务通知来替代信号量、消息队列、事件标志组等这些东西。使用任务通知的话效率会更高
通过空闲任务钩子函数(或称回调,hook,or call-back),可以直接在空闲任务中添加应用程序相关的功能。空闲任务钩子函数会被空闲任务没循环一次就调用一次
通常空闲任务钩子函数被用于:
FreeRTOS是通过在处理器处理空闲任务的时候将处理器设置为低功耗模式来降低能耗。一般会在空闲任务的钩子函数中执行低功耗相关处理,比如设置处理器进入低功耗模式(见3.4.1)、关闭其他外设时钟、降低系统主频等等
FreeRTOS系统提供的低功耗模式,当处理器进入空闲任务周期以后就关闭系统节拍中断(滴答定时器中断),只有当其他中断发生或者其他任务需要处理的时侯处理器才会从低功耗模式中唤醒
内存管理是一个系统基本组成部分,FreeRTOS中大量使用到了内存管理,比如创建任务、信号量、队列等会自动从堆中申请内存。用户应用层代码也可以用FreeRTOS提供的内存管理函数来申请和释放内存
BaseType_t xTaskCreate( TaskFunction_t pxTaskCode,
const char * const pcName,
const uint16_t usStackDepth,
void * const pvParameters,
UBaseType_t uxPriority,
TaskHandle_t * const pxCreatedTask )
vTaskDelete( TaskHandle_t xTaskToDelete )
xTaskToDelete:要删除的任务的任务句柄
(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(); //进入临界区
//创建LED0任务
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);
//创建LED1任务
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); //设置系统中断优先级分组4
//移植FreeRTOS的默认配置,可提供16个抢占优先级
HAL_Init(); //初始化HAL库
Stm32_Clock_Init(RCC_PLL_MUL9); //设置时钟,72M
delay_init(72); //初始化延时函数
uart_init(115200); //初始化串口
LED_Init(); //初始化LED
KEY_Init(); //初始化按键
usmart_dev.init(84); //初始化USMART
//创建开始任务
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(); //开启任务调度
}
任务挂起、恢复和任务删除、重建的区别:保存任务运行的数据不会丢失
void vTaskSuspend( TaskHandle_t xTaskToSuspend)
xTaskToSuspend:要挂起的任务的句柄,创建任务的时候会为每个任务分配一个任务句柄。若使用函数xTaskCreate()创建任务的话,那么函数的参数pxCreatedTask就是此任务的任务句柄
注:如果参数为NULL的话表示挂起任务自己
将一个任务从挂起态恢复到就绪态
只有通过函数vTaskSuspend()设置为挂起态的任务才可以使用vTaskRexume()恢复
void vTaskResume( TaskHandle_t xTaskToResume)
xTaskToResume:要恢复的任务的任务句柄
此函数是vTaskResume()的中断版本,用于在中断服务函数中恢复一个任务
BaseType_t xTaskResumeFromISR( TaskHandle_t xTaskToResume)
xTaskToResume:要恢复的任务的任务句柄
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的中断得不到及时的响应
函数taskENTER_CRITICAL_FROM_ISR()和taskEXIT_CRITICAL_FROM_ISR()中断级别临界段代码保护,是用在中断服务程序中的,而且这个中断的优先级一定要低于configMAX_SYSCALL_INTERRUPT_PRIORITY
使用方法:
//定时器 3 中断服务函数
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); //清除中断标志位
}
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; //当队列上锁以后用来统计从队列中接收到的队列项数量,也就是出队的队列项数量 //当队列没有上锁的话此字段为 queueUNLOCKED
volatile int8_t cTxLock; //当队列上锁以后用来统计发送到队列中的队列项数量,也就是入队的队列项数量
//当队列没有上锁的话此字段为 queueUNLOCKED
#if((configSUPPORT_STATIC_ALLOCATION == 1)&&\(configSUPPORT_DYNAMIC_ALLOCATION == 1))
uint8_t ucStaticallyAllocated; //如果使用静态存储的话此字段设置为 pdTURE。
#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;
动态创建队列
QueueHandle_t xQueueCreate(UBaseType_t uxQueueLength,UBaseType_t uxItemSize)
BaseType_t xQueueSend( QueueHandle_t xQueue,
const void * pvItemToQueue,
TickType_t xTicksToWait);
此函数在读取消息的时候采用的是拷贝的方式,所以用户需要提供一个数组或缓冲区来保存读取到的数据,所读取的数据长度是创建队列的时候所设定的每个队列项目的长度
BaseType_t xQueueReceive(QueueHandle_t xQueue,
void * pvBuffer,
TickType_t xTicksToWait);
xQueue:队列句柄,指明要读取哪个队列的数据,创建队列成功的时候会返回此队列的队列句柄
pvBuffer:保存数据的缓冲区,读取队列的过程中会将读取到的数据拷贝到这个缓冲区中
xTicksToWait:阻塞时间,此参数指示当队列空的时候任务进入阻塞态等待队列有数据的最大时间。如果为0的话当队列空的时候就立即返回;当为portMAX_DELAY的话就会一直等待,直到队列有数据,也就是死等,当时宏INCLUDE_vTaskSuspend必须为1