文档
amazon官方文档, 中文好评!https://docs.amazonaws.cn/freertos/index.html
官网: https://freertos.org/
官方文档,英文的
AWS上的文档,也是英文的
FreeRTOS与RT-Thread对比: 后者自带很多第三方协议, 如i2c/mqtt等等, 而FreeRTOS似乎要自己找自己组装进去.
根据其他乱七八糟文档的整理
根据深入浅出,FreeRTOS新手+入门学习笔记整理.
-
任务:
xTaskCreate
建立多个任务, 然后使用vTaskStartScheduler
调度任务函数. 任务函数执行完成任务后应该进入等待.
-
队列:
xQueueCreate
建立队列, xQueueReceive
接收队列.
根据FreeRTOS操作系统最全面使用指南 整理
似乎就主要3个功能: 任务调度/FIFO队列通讯/内存管理.
时间管理主要就是一个延迟函数vTaskDelay
下面看官网教程https://www.freertos.org/FreeRTOS-quick-start-guide.html的Developer Docs
部分. 描述Kernel部分.
编码标准
FreeRTOS用的是MISRA标准.
前缀
变量类型 |
前缀 |
uint32_t |
ul |
uint16_t |
us |
uint8_t |
uc |
有符号非标准int(如Type_t/TickType_t/size_t) |
x |
无符号非标准int(如UbaseType_t) |
ux |
enum类型 |
e |
* 指针 |
p |
函数类型 |
前缀 |
文件内函数(类似private) |
prv |
void空返回函数 |
v |
其他返回值函数 |
参照变量前缀 |
宏也有前缀, 应含有或部分含有所在的文件名(小写).其他部分为大写, 如: configUSE_PREEMPTION
, 习惯了全大写的看起来怪怪的
移植层定义的适配类型
- TickType_t:如果configUSE_16_BIT_TICKS为非零(条件为真),TickType_t定义为无符号16位类型。如果configUSE_16_BIT_TICKS为零(条件为假),TickType_t定义为无符号32位类型。注:32位架构的微处理器应设置configUSE_16_BIT_TICKS为零。
- BaseType_t:定义为微处理器架构效率最高的数据类型。比如,在32位架构处理器上,BaseType_t应该定义为32位类型。在16位架构处理器上,BaseType_t应该定义为16位类型。如果BaseType_t定义为char,对于函数返回值一定要确保使用的是signed char,否则可能造成负数错误。
- UbaseType_t:这是一个无符号BaseType_t类型
其他风格
- 坚决不用
//
注释, 哈哈. 一行80列以内.
- 4格缩进
#include
的顺序是标准库/自己的库/硬件特定的库
#include
后面依次是#define
, 文件内函数声明(都加了static
), 文件内全局变量声明, 函数定义
- 所有函数定义都有
/*-------*/
分行
任务task
FreeRTOS要做的跟Windows一样, 一个任务是一个窗口, 有自己的上下文和环境, 看上去就像独立运行的一样. 多个任务看起来就像是同时在运行, 实际上由任务调度去决定任务的切换, 每一时间只有一个任务占领CPU(当然Windows也是一样的)
任务状态
占领CPU的时候就是 running态, 暂时调度给其他任务就是ready态.
任务自己进入等待而把CPU主动让出来交Blocked态, 如调用了vBlockDelay()
或等待其他事件. 等到了就恢复ready,或者实在等不到(超时timeout了)也恢复ready
啥也不干的任务可以调用vTaskSuspend()
进入Suspended态, 跟Blocked区别就是不会超时. 只有调用xTaskResume()
才会回到ready态.
任务优先级
数字高的先运行. 0是最低数字. 当前运行的就是最高优先级的一个或多个ready任务. 多个任务同时的话就用时间片切换.
优先级原则
- Fixed priority固定优先级. 调度器不会更改优先级
- Preemptive抢占式. 最高优先级永远先运行. 如果一个任务优先级被中断由低调到最高, 它立刻把抢到CPU, 无论其他任务运行到哪儿了, 即便是一个时间片没有执行完.
- Round-robin同优先级循环切换
- Time sliced时间分片.一个tick中断一片时间.
要小心饿死低优先级任务
高优先级永远最先运行, 如果占着CPU不放, 低优先级永远不会运行. 所以要高风亮节, 等事的时候就把CPU让出来, 进入Blocked或者Suspended, 事儿来了再处理.
优先级其他
抢占式和时间分片这两个特性可以在FreeRTOSConfig.h
中关闭.
另外支持大小核(AMP)和同等多核(SMP)等CPU
任务实现
void vATaskFunction( void *pvParameters )
{
for( ;; )
{
-- Task application code here. --
}
/* Tasks must not attempt to return from their implementing
function or otherwise exit. In newer FreeRTOS port
attempting to do so will result in an configASSERT() being
called if it is defined. If it is necessary for a task to
exit then have the task call vTaskDelete( NULL ) to ensure
its exit is clean.
翻译: 任务别想着return, 新的port里这样做会导致configASSERT()
调用报错. 如果非得退出, 调用vTaskDelete( NULL ) */
vTaskDelete( NULL );
}
函数形态咯. for(;;)
的意思是任务一直执行, 直到vTaskDelete
, 相当于windows的×
. 任务里面是坚决不能有return
的, 否则就会报错.
一直执行的任务里面, 自己决定是不是要Blocked或者Suspended把CPU让出来. 所以这个for(;;)在裸机系统里面看起来像是个死锁, 实际上却是让任务保持运行的方式, 毕竟它不能return
任务函数的类型是TaskFunction_t
void vATaskFunction( void *pvParameters )
{
for( ;; )
{
/* Psudeo code showing a task waiting for an event
with a block time. If the event occurs, process it.
If the timeout expires before the event occurs, then
the system may be in an error state, so handle the
error. Here the pseudo code "WaitForEvent()" could
replaced with xQueueReceive(), ulTaskNotifyTake(),
xEventGroupWaitBits(), or any of the other FreeRTOS
communication and synchronisation primitives.
翻译: 下面伪码展示了任务等待事件的方式. 事件发生了就处理事件,
超时了就处理报错. 伪码WaitForEvent可以换成 xQueueReceive(),
ulTaskNotifyTake(), xEventGroupWaitBits()或其他通讯同步机制 */
if( WaitForEvent( EventObject, TimeOut ) == pdPASS )
{
-- Handle event here. --
}
else
{
-- Clear errors, or take actions here. --
}
}
/* As per the first code listing above. */
vTaskDelete( NULL );
}
任务使用 xTaskCreate()
xTaskCreateStatic()
创建, 使用vTaskDelete()
销毁.
Co-routines协程
超小RAM的MCU可以用协程, 32位的CPU一般就用不到了. 这说的是Arduino的那个8位机吗? 这儿也不介绍了, 一般这种情况估计都用裸机了.
任务/中断间通讯机制
Queues队列
- 队列是任务间以及中断和任务间的主要通讯方式. 采用的是先进先出FIFO, 跟排队一样, 先到先得.
- 发到队列的数据是复制的, 这和其他消息机制()类似, 也就是发出去以后, 不管对方收了没有, 自己这一份数据都可以改了, 类似于函数调用的实参传递.
- 中断使用队列和任务使用队列的API不一样, 方便识别.
- 读取空队列的任务将进入Blocking态, 直到收到消息. 发送消息给满队列的任务将进入Blocking态, 直到队列有空.
- 中断不允许调用以
fromISR
结尾的API函数. ISR意思是Interrupt Service Routine中断服务事务,就是中断处理函数.
Binary Semaphores二进制信号量
先说啥是信号量. 简单地说就是解决资源占用的方式, 跟十字路口的信号灯一样. 拿到绿灯的占用路口走, 给另一个方向的车辆红灯, 另一个方向的就等着(Blocking).有限的资源多个任务都想用, 就用信号量来解决. 也有点像停车场剩余车位数(计数信号量). 没车位的都在外面等着(Blocking).
这儿和这儿有中文讲解.
互斥和同步. 互斥就是一个球只有一个人拍, "排斥"了其他人拍球, 想玩的排队. 同步就是想做才得先小工洗菜,再大厨炒菜,再跑堂端菜, 都是菜, 但就是得按顺序处理, 没到操作步骤的也排着队等.这儿看到同步也是一种互斥, 除了等资源, 还得步骤到了.
我在处理腾讯连连的MQTT时可能就是一种同步机制, 需要按顺序处理ready->GOT IP->时间->MQTT.只不过我是用enum的不同状态表示的.
二进制信号量就是占用时加锁, 释放时解锁, 表明这两种状态的.
Task Notifications任务通知
有时候可以替代二进制信号量, 并且更轻量一些.
- 二进制信号量同时用于同步和互斥机制.
- 二进制信号量和mutexes互斥量很相似, 其差别只在于互斥有优先级继承, 而二进制信号量没有. 因此互斥量更适合互斥场景, 而二进制信号量更适合任务间(包括中断)同步场景.
- 二进制信号量相当于只有1位的队列Queue.
- 中断处理后才能执行的任务可以等待这个信号量, 而中断给出这个信号量,函数是
xSemaphoreGiveFromISR()
- 任务的优先级可以保证获取信号量以后的处理顺序(同步机制?).
Counting Semaphores计数信号量
同样 Task Notifications任务通知
有时候做轻量级替代.
我在MQTT处理对pub消息的回应时用了类似的计数机制. 为防止在连续发出的第二条pub指令之后, 实际执行pub之前, 收到了上一条pub指令的success消息, 将pub指令取消导致第二条pub没有执行(同时还要兼顾失败重发机制), 我在pub发出指令后增加计数/在success时减少计数, 只有计数为0时才完全停止pub.
- 相当于长度大于1的队列
- 可用于事件的计数. 计数值就是未处理事件的值
- 可用于资源管理. 跟停车场一个道理, 计数值表示空余车位的数量. 车走了加1车进来减1, 为0就等着.
Mutexes互斥量
用于对有限资源的访问.
互斥量借用完要自己还回去(只有1个且不会被消耗), 而二进制信号量用完不用还(持续被生产出来并且被消耗掉).
低优先级的任务持有互斥量时, 高优先级的再去获取, 会把低优先级的任务的优先级暂时提高到高优先级.
适合任务间确认谁访问资源, 不适合中断使用. 因为中断是不可能去等待任务给他返还可用性的.
Recursive Mutexes递归互斥量
可以被一个任务连续借用多次, 但也要被还回去这么多次, 其他任务才能用.
Task Notifications任务通知
算是新特性, 从v8.2.0开始引入, 在v10.4.0中增加了单任务多通知功能
- 每个任务有1个或1组(v10.4以后)通知. 任务A可以发直达信息(direct, 一个32位的值)到任务B的通知里, 发到以后任务B的通知会变成Pending.
- 任务B可以等待通知的Pending时Blocking.
- 任务A发了direct后还能更新direct的值. 可以不管任务B读了没有, 也可以等待任务B读完再更新, 可以改1位或多位, 也可以递增
- 任务B调用
xTaskNotifyWait()/xTaskNotifyWaitIndexed()
读了值以后, 通知状态就会变成Not Pending.
- 任务B也可以直接调用
xTaskNotifyStateClear()/xTaskNotifyStateClearIndexed()
清除通知的Pending态
- 即便有v10.4以后有一组通知, 任务B同时也只能等一个通知的Pending, 而不能同时等多个通知的Pending
- 通知比之前那些方式要快45%, 并且使用RAM更少, 只是使用的时候有些限制: 1. 只有1个接受者; 2. 发送者一次发不完的时候不能block;
- 任务A使用
xTaskNotifyIndexed()
xTaskNotifyGiveIndexed()
发通知. 通知进入Pending态, 直到任务B调用 xTaskNotifyWaitIndexed()
ulTaskNotifyTakeIndexed()
. 函数名里面的Indexed
可以去掉, 就对应的是Index==0
的通知(感觉是为了保持版本的兼容性)
- 通知可部分替代二进制信号量/计数信号量/事件组/邮箱机制. 官方文档对此有专门章节做示例
Stream & Message Buffers流和消息缓冲区
10.0.0以后才有的特性
TBD. 这么新的特性估计暂时用不到, 晚些在研究.
Software Timers软时钟
- 是可选功能, 需要自行开启
- 分一次性时钟和自重启时钟
- 在时钟未溢出前还可以Reset时钟以重新计时
Event Bits (or flags) and Event Groups 事件位(标志)和事件组
- 事件位指示事件是否发生, 一组事件位就是事件组
- 可以在事件位变成1以前等待
静态和动态内存分配
- 很多前面说的对象都可以动态或者静态分配内存. 动态分配就是在运行时再分配, 静态分配就是在link的时候就分配好,还能指定分配的地址.
heap堆管理
最新版本有5种堆管理方式.
这部分感觉暂时也用不上, 看原文吧.
栈溢出检查
新建项目
直到vTaskStartScheduler()
调用后, 程序才表现得像个RTOS的样子.
调试
百度搜索了资料, 似乎只能通过打印信息调试, 搜索"FreeRTOS 断点调试"找不到结果.
Bing搜索"FreeRTOS breakpoint debug"还是能找到几条使用Keil设置断点调试的方式, 主要是说要在task里面设置断点, 并且不能用步入模式调试.
我设想一种调试方式, 即只针对task进行调试, 把task当做一个系统. 把FreeRTOS的API函数和硬件函数全部Stub来调试逻辑. 剩下的硬件逻辑和OS相关逻辑通过真机printf调试. 当然单元测试应该是可以用的.