笔者在学习完常用的STM32内容后,听朋友推荐FreeRTOS具有很大操作空间,于是决定开始这一部分的探索。在学习完普中的FreeRTOS视频后,笔者总结了一些自己的看法。
1. 什么是RTOS
实时操作系统(Real Time Operating System,简称RTOS)是指当外界事件或数据产生时,能够接受并以足够快的速度予以处理,其处理的结果又能在规定的时间之内来控制生产过程或对处理系统做出快速响应,并控制所有实时任务协调一致运行的操作系统。提供及时响应和高可靠性是其主要特点。这里要注意,RTOS 不是指某一个确定的系统,而是指一类系统。比如 UCOS,FreeRTOS,RTX,RT-Thread 等这些都是 RTOS 类操作系统。本次所学的FreeROTS 就是一个免费的 RTOS 类系统,他十分小巧可以在资源有限的MCU中运行当然它也可以被扩展到其他的控制器中。
一般来说,我们需要的是能够在别人移植好的系统之上,利用RTOS里的API说明,然后使得这些API可以实现自己的功能即可。当然,如果有更高要求的读者也可以更加深入的了解其中的一款。
2. Free RTOS
- 内核配合支持抢占式、合作式和时间片调度;
- 提供了一个用于低功耗的Tickless模式;
- 系统的组件在创建时可以选择动态或静态RAM,如:任务、消息队列、信号量、软件定时器等;
- 可移植性高,主要以C语言编写为主;
- 支持实时任务和协程;
- 系统简单小巧应用通常占用内核的4K到9K字节空间。
2.1 Free RTOS编程风格
数据类型
新定义的数据类型 | 实际的数据类型(C标准类型) | 其他 |
---|---|---|
portCHAR | char | |
portSHORT | short | 16位 |
portLONG | long | 32位 |
portTickType | unsigned short int | 用于定义系统时其计数器的值和阻塞时间的值,在头文件Free RTOSConfig.h头文件中的宏configUSE_16BIT_TICKS为1时则为16位 |
unsigned int | 用于定义系统时其计数器的值和阻塞时间的值 ,在头文件Free RTOSConfig.h头文件中的宏configUSE_16BIT_TICKS为0时则为32位 | |
portBASE_TYPE | long | 根据处理器的架构来决定多少位,如果是32/16/8bit的处理器则对应32/16/8bit的数据类型,一般用于定义函数的返回值或者布尔类型 |
变量名
例如char型变量的前缀是c,short型的变量前缀是s, long型变量的前缀是l, portBASE_TYPE类型的前缀是x,还有其他的数据类型例如数据结构、任务句柄、队列句柄等定义的变量名前缀也是x。如果是一个无符号型的前面还会有一个u的前缀,如果是一个指针型变量型则会有一个前缀p。
函数名
如果是一个私有的函数,则会加一个prv(private)的前缀。如果在函数前加上一个v的前缀,则表示该函数的返回值为void的类型;若函数前缀为x,则返回值为上文所提到的portBASE_TYPE类型。
宏
前缀 | 所在宏定义文件 |
---|---|
port | portable.h |
task | task.h |
pd | projdefsh |
config | FreeRTOSConfig.h |
err | projdefsh |
2.2 Free RTOS的配置文件
configASSERT(x)
断言,类似 C 标准库中的 assert()函数,调试代码的时候可以检查传入的参数是否合理,FreeRTOS 内核中的关键点都会调用 configASSERT(x),当 x 为 0 的时候说明有错误发生。
configUSE_PREEMPTION
使能抢占式调度器,为 1 时使用抢占式调度器,为 0 时使用协程。如果使用抢占式调度器的话内核会在每个时钟节拍中断中进行任务切换,在任务结束后CPU不会等待低优先级任务,直接由高优先级占取,而使用协程的话会在如下地方进行任务切换:
● 一个任务调用了函数 taskYIELD()。
● 一个任务调用了可以使任务进入阻塞态的 API 函数。
● 应用程序明确定义了在中断中执行上下文切换。
configUSE_TIME_SLICING
configCPU_CLOCK_HZ (SystemCoreClock)
配置CPU时钟频率。
configTICK_RATE_HZ (1000)
配置系统节拍中断的频率,即一秒钟中断的次数,每次中断RTOS都会进行任务调动。此频率也是滴答定时器的中断频率,需要使用此宏来配置滴答定时器的中断。
configMAX_PRIORITIES (32)
配置优先级数字越大优先级越高,数字越小优先级越低。
configMINIMAL_STACK_SIZE ((unsigned short)130)
设置空闲任务的最小任务堆栈大小,以字为单位,不是字节。比如在STM32 上设置为100的话,那么真正的堆栈大小就是 100*4=400 字节
configMAX_TASK_NAME_LEN
设置任务名最大长度。
configUSE_16_BIT_TICKS
设置系统节拍计数器变量数据类型,系统节拍计数器变量类型为 TickType_t,当该宏为1的时候 TickType_t 就是 16 位的,当 configUSE_16_BIT_TICKS为 0 的话 TickType_t 就是 32 位。
configIDLE_SHOULD_YIELD
默认为高电平,此时空闲任务放弃CPU的使用权给其他同优先级的任务。
configUSE_QUEUE_SETS
默认为低电平,启动队列。
configUSE_TASK_NOTIFICATIONS
默认为高电平,开启任务通知功能。
configUSE_MUTEXES
默认为高电平,使用互斥信号。
configUSE_COUNTING_SEMAPHORES
设置为 1 的时候启用计数型信号量,相关的 API 函数会被编译。
configQUEUE_REGISTRY_SIZE
设置可以注册的队列和信号量的最大数量,在使用内核调试器查看信号量和队列的时候需要设置此宏,而且要先将消息队列和信号量进行注册,只有注册了的队列和信号量才会再内核 调试器中看到,如果不使用内核调试器的话此宏设置为 0 即可。
configSUPPORT_DYNAMIC_ALLOCATION
默认为高电平,此时在创建 FreeRTOS 的内核对象的时候所需要的 RAM 就会从 FreeRTOS 的堆中 动态的获取内存,如果定义为 0 的话所需的 RAM 就需要用户自行提供,默认情况下宏configSUPPORT_DYNAMIC_ALLOCATION 为 1。
configTOTAL_HEAP_SIZE ((size_t)(20*1024))
设置系统所有堆大小,如果使用了动态内存管理的话,FreeRTOS 在创建任务、信号量、队列等的时候 就 会 使 用 heap_x.c(x 为 1~5) 中 的 内 存 申 请 函 数 来 申 请 内 存 。 这 些 内 存 就 是 从 堆 ucHeap[configTOTAL_HEAP_SIZE]中申请的,堆的大小由 configTOTAL_HEAP_SIZE 来定义。
configUSE_IDLE_HOOK
默认为低电平,使用空闲任务钩子函数,用户需要实现空闲任务钩子函数,函数的原型为:void vApplicationIdleHook( void )。这个函数在每个空闲任务周期都会被调用,对已经删除的任务空闲任务可以释放给他们堆栈内存,因此需要保证空闲任务可以被CPU所执行,不可调用的任务会引起空闲任务阻塞的API函数。
configUSE_TICK_HOOK
默认为低电平,也是一个函数void vApplicationTickHook( void )。时间片中断可以周期调用,但必须十分短小,不可使用大量的堆栈,并且不能以调用"FromISR"或者"FROM_ISR"结尾的API函数。
configGENERATE_RUN_TIME_STATS
默认为低电平,设置为 1 开启时间统计功能,相应的 API 函数会被编译,为 0 时关闭时间统计功能。如果宏 configGENERATE_RUN_TIME_STATS 为 1 的话还需要定义下表中的宏。
3.任务基础知识
我们以前在使用 51、STM32 单片机的时候一般都是在main 函数里面用 while(1)做一个大循环来完成所有的处理,即应用程序是一个无限的循环,循环中调用相应的函数完成所需的处理。有时候我们也需要中断中完成一些处理。相对于多任务系统而言,这个就是单任务系统,也称作前后台系统,中断服务函数作为前台程序,大循环while(1)作为后台程序。但是这种系统的实时性差并且对于CPU资源的利用率不高,这时候我们就引出了任务系统的概念了。多任务系统的核心便是“分而治之”的思想,将许多大的问题分散为多个很小的问题,逐步将小问题解决掉,因此可以把每一个小问题当做是一个小任务来处理。
3.1 任务状态
一般来说 FreeRTOS中的任务永远是处于运行态、就绪态、阻塞态和挂起态四种状态。其中阻塞态是指一个任务当前正在等待某个外部事件,比如说如果某个任务调用了函数 vTaskDelay()的话就会进入阻塞态,直到延时周期完成。任务在等待队列、信号量、事件组、通知或互斥信号量的时候也会进入阻塞态。任务进入阻塞态会有一个超时时间,当超过这个超时时间任务就会退出阻塞态,即使所等待的事件还没有来临!
像阻塞态一样,挂起态也是当任务进入挂起态以后也不能被调度器调用,但是进入挂起态的任务没有超时时间。任务进入和退出挂起态通过调用函数 vTaskSuspend()和 xTaskResume()。
3.2 任务的实现
- 任务函数本质也是函数,所以肯定有任务名什么的,不过这里我们要注意:任务函数的返回类型一定要为 void 类型,也就是无返回值,而且任务的参数也是 void 指针类型的!任务函数名可以根据实际情况定义。
- 任务的具体执行过程是一个大循环,for(; ; )就代表一个循环,作用和 while(1)一样,笔者习惯用 while(1)。
- 循环里面就是真正的任务代码了,此任务具体要干的活就在这里实现!
- FreeRTOS 的延时函数,此处不一定要用延时函数,其他只要能让 FreeRTOS 发生任务切换的 API 函数都可以,比如请求信号量、队列等,甚至直接调用任务调度器。只不过最常用的就是 FreeRTOS 的延时函数。
- 任务函数一般不允许跳出循环,如果一定要跳出循环的话在跳出循环以后一定要调用函数 vTaskDelete(NULL)删除此任务!