RTOS
RTOS属于操作系统(OS)
软件和硬件之间的桥梁,
本质
专用设备:C51单片机、STM32、嵌入式
OS:硬件驱动(内存管理、GPIO、Timer)
应用:直接调用驱动,开发应用逻辑
OS的使用场景
外设资源多、有并行的需求(多任务)、实时性
所以OS的本质就是一种分工的思想
逻辑系统:主程序在while(1)循环中,有中断机制
main( ){
//硬件初始化while(1){//应用}
}
xxx handler(){
//处理中断
}
多任务系统:任务去处理程序、中断
main( ){
//硬件初始化//RTOS初始化//多种任务初始化//开启任务:while(1)//这是每个任务中的whilewhile(1)//程序不能执行到这里
}
逻辑系统和多任务系统的区别:
逻辑系统主程序在while循环里面,实时性是中断保证的;
多任务是用任务去处理事件,每个事件对于每个任务而言都是主逻辑,实时性也是靠中断保证的;
程序结构 事件处理 紧急处理 特点:
裸机 主程序中轮询 中断 轮询+中断
多任务 任务为主 中断 任务+中断
正常的操作系统,其实也是有实时性的,但是性能稍微差点,主要去管理庞大的底层资源,内核特别大
RTOS对实时性要求非常高,对于管理底层资源可是可以做到了,因为单片机底层资源很少;
RTOS特点:
稳定性;开源协议;开发成本;外部扩展资源:有丰富的第三方资源库
(LOT:物联网) 实时性;市场占有率(bug少,学习资源多,产品推广方便)
以前的RTOS,追求稳定和实时,实现多任务和中断,设备的每个地址是实地址绑定的;
现在的RTOS加了第三方物联网组件(因为万物互联的趋势LOT)
RTOS学什么
应用开发、内核开发(kernel):kernel,port是具体的板卡资源,需要单独移植(软件控制硬件)、LOT开发:第三方资源
RTOS怎么学
RTOS的概念:任务、空闲、抢占、消息等等、RTOS的设计思路:就绪队列中如何去挑出最适合运行的任务(任务的优先级)、RTOS的接口使用:常见的API用熟
最终目标:会看(看懂源码)、会用(会用API)、会改(遇到的bug)
CMSIS标准
CMSIS: Cortex-M(微处理器)定义的一套标准
通用的数据类型;通用API:RTOS、Driver
(GPIO、Timer、ADC、DMA)
用这套标准打造一个生态
FreeRTOS
起源:一个英国公司发起的,被亚马逊看中,被收购,(电商,云计算:部署一些LOT设备,用rtos能够扩大团队、优化程序,开源用来推广产品)
特性:开源、商用免费、市场占有率高
组成:kernel、port、第三方库
任务;IPC;内存
内存管理:
五个策略:正常使用第四个就足够了;
1、可申请,不释放;
2、可申请,可释放,不整理;
3、2+线程安全优化
4、可申请,可释放,可整理
5、4+识别外扩的内存芯片
编程规范:
变量名:数据类型+单词(见名知意)
函数名:返回值类型+文件名+单词
宏名:文件名+单词
数据类型:标准数据类型和非标准数据类型
例:定义一个char类型的无符号计数值
uint8-t uccount;
定义一个赋值函数:
void vtimGetCNT( ){ }
接口API:
FreeRTOS的原生API
符合CMSIS标准的API
符合HAL库的API
配置:
FreeRTOS的Config.h(程序员根据应用去做配置,自定义Kernel)
FreeRTOS.h:默认文件 ifdef define
任务管理:
什么是任务:
竞争系统资源的最小单位;
每个任务都有属于自己的私有栈;
每个任务都在各自的while(1)中去死循环;
时间点上是串行;时间段上是并行;
任务的状态切换:
创建、删除、挂起、恢复、阻塞
Task1:每隔1s 打印一次 “task1删除”
Task2:每隔1s 打印一次 “task2创建”
Task3:{
while(1){if("1"){删除}if("2"){创建}if("3"){挂起}if("4"){恢复}}
}
1任务管理
概念、
状态切换流程
相关函数
系统启动
空闲任务
任务切换
2状态切换:创建、就绪、运行、阻塞、挂起
3相关函数:
1、创建、
2、删除、
3、挂起、
4、恢复挂起
1、可在任务中恢复
函数:vTaskResume( )
可以有多次挂起,但只需要在最后恢复一次即可;
不可以在中断中使用;
2、可在中断中恢复
vTaskResumeFromISR()
(fromISR:在中断中使用的API)
xxx–callback ( ){
BaseType-t temp;(判断是否挂起成功)
temp=vTaskResumeFromISR(句柄)
if(temp==pdtrue){
portYIEID–from–ISP( ) //在中断中主动进行任务切换
}
}
4系统启动:
osKernelStart() -> vTaskStartScheldmr( )
1、启动RTOS多任务
2、创建空闲任务和可选的软件定时器;
3、正常状态下无返回值,如果出现返回值,要么是空闲任务的创建异常,要么是软件定时器的创建异常;
5空闲任务:
1、主要是用来回收要被删除的任务的资源;
2、实现低功耗;
6任务切换
1阻塞:延时阻塞/条件阻塞;
2时间片时间到了,让出CPU(本质上是触发中断:pendSV(优先级是15,这个中断优先级数字越大,优先级越低,所以这个触发中断的优先级是最低的)->触发中断)
任务调度:
三种方式:
合作式调度:上一个结束,下一个才开始
时间片调度:同等任务优先级的时候,使用时间片来进行任务切换
抢占式调度:高优先级任务-> 低优先级任务;当前运行的任务是低优先级,如果来了一个高优先级任务,CPU会立刻将低的停下,让高任务去运行;
任务栈:
常用的栈:满减栈,满增栈;空减栈;空增栈;
sp指向的位置有没有数据,有的话,再看往大的地址加入数据还是往小的数据加入数据
每个任务都会有一个私有栈:
如何确定栈的大小:
首先,是估计可能的栈的大小:
函数:局部变量,形参,返回地址,状态;
中断;
任务切换
以上是可以评估的;
不可评估的:
printf( )
函数指针 ->接口
递归代码
先给一个大的栈空间
通过调试函数,得到剩余栈空间(真是利用的栈的大小)
栈的大小乘1.5或者 乘2
栈溢出检测:
有一个配置, xxx-stack-overflow,它的值分为1和2,代表两个不同的检测标准;1:看sp的地址是否超出;2:将整个栈都赋值为一个特殊的值:0xa5,每次都去查看最后16位是否为0xa5;如果是,那就说明还有一些值没有用;
配置:vApplicationStartOverFlowHook( ){ }
硬件错误:中断:HandFault-handler(void){}
中断优先级
任务优先级:
配置:MAX-priority
值越大,等级越高
不同优先级的任务:抢占式调度
同一优先级的任务:时间片调度
明确抢占时的场景
2个API:配置
设置优先级/获取优先级
uxRTaskpritorityGet( )
vTaskpritoritySet( )
6、临界区
当前运行的代码不可被高优先级的中断打断;
何时用临界区:全局可写变量和全局函数
实现:
7、调度锁、中断锁、任务锁
调度锁:防止当前任务被其他更高优先级的任务切换,将其他人都挂起
阻塞延时:相对延时/绝对延时
IPC:
中断和任务之间的通讯;
重点在于API函数的调用;
首先是消息队列(先进先出,用来存放消息)
在任务与任务或者中断与任务之间传递不固定长度的信息;
全局数组 VS 消息队列 使用消息队列的好处:
消息队列可以防止多任务同时操作;
还有超时的机制,可以帮助RTOS更好地管理任务;
先进先出,后进后出-----可以更好地管理数据;
内部的数据都是直接复制的,并非引用的;如果给消息队列传数据进去,并不是传的引用,虽然传地址省空间,但是消息队列本身为了安全性,所以直接传消息数据本身,并非传引用;
执行流程:
消息队列的TCB(控制块(也就是所谓的句柄)消息队列也是有句柄的)
有一个头指针,一个尾指针;要将数据不断插入;插入的数据,就在消息空间;发送方就是任务,中断,任务1,任务2,接收中断(Rx中断–>中断函数)也可以发送数据;
任务和中断都可以接收数据(可以是字符串,数组,结构体等等,都能发送)
应用场景:
双方发送一个不定长的消息
消息队列的API :
1、创建消息队列
□ xQueueCreate(消息队列的大小,每个消息的内存大小)
返回值就是消息队列的句柄
基本使用:
首先就是要创建一个消息队列
Queuehandle-t temp;
temp;=xQueueCreate(2,10);
if(temp==pdtrue){
成功;
}else{
失败;
}
2、删除消息队列
xQueueDelete(句柄)
删除消息队列
删除队列中的数据
3、发送消息队列(任务中发送/中断中发送)
xQueueSend(句柄,数据地址,)
数据是复制而不是引用进去;不能中断使用;
若第三个参数=0,返回
if(xQueueSend…==pdturn){
消息队列写入成功;
}else{
消息队列写入失败;
}
中断里:
xxx-Callback( ){
BaseType-t temp;xQueueSendFromISR( ,,&temp){portYIEID-From-ISR(temp);//temp的值就决定要不要把值传入进去
}
}
4、获取消息队列
2、信号量
消息队列 ->数组
信号量 ->标志位
三大用途:
同步应用;二值信号量
资源管理;计数信号量
临界资源的互斥访问;互斥信号量
二值信号量取值:0/1
计数信号量取值:0到N
互斥信号量数值就是真或假
0代表信号量中没有数据,想要获取数据就要一直等待,这就是一种阻塞;如果是1或者N,那就是有数据了,接触阻塞
专业术语:
0代表信号量为空,任务要获取必须先阻塞延时;有两种退出的情况:一是超时;二是其他任务主动释放资源
二值信号量本质是一个长度为1,消息大小为0的消息队列;
整个状态就2种:空/满
0:当前二值信号量未被获取,此时再次获取信号量的任务,就必须阻塞;
1:可以正常获取
应用:
有数据变化 LCD刷新
无数据变化 LCD不刷新
运行流程:
二值信号量的API:
创建二值信号量:
□ xSemaphoreCreate( ){
osSemaphoreID temp;temp=xSemaphoreCreate( );
if(temp==pdture){
成功
}else失败
}
删除二值信号量:
xSemaphoreDelete( 句柄){
}
释放二值信号量
xSemaphoreGive( 句柄){
//调用此函数时,句柄要存在//不可用于中断
}
中断:
xSemaphoreGiveFromISR( ){
BaseType-t temp;xSemaphoreGiveFromISR(句柄,&temp);portYIEID-From-ISR(temp);
}
获取二值信号量:
xSemaphoreTake(句柄,等待时间){
//等待时间为0:
}
计数信号量:
取值>1的特殊的二值信号量(长度为1,消息大小为0的特殊的消息队列)
应用场景:允许多任务访问同一资源,但是又限制了任务的个数
运行流程:
释放资源后,task1可以继续运行,资源不够的时候,task1是阻塞的,task1占用资源之后也是要释放资源的;task2也需要调用资源,如果无法调用到资源,那么task2就是原地打转;
计数信号量的API:
创建:
xSemaphoreCreateCounting(支持的最大数,初始值 )
创建句柄;
句柄函数;
if(句柄,成功)else
删除:
xSemaphoreDelete(句柄)
释放:
xSemaphoreGive(句柄)
还有中断的函数
互斥信号量本质上也是一个特殊的二值信号量;
区别在于:二值信号量
task1和task2都要打印数据,共用串口资源(只有一个串口)
二值信号量在做同步 出现一个优先级问题,违反了抢占式调度的原则
原因:低优先级抢占资源,不放手,高优先级任务去获取时被阻塞,此时若有次高优先级任务加入,则当高优先级条件无法满足,出现次高先行的情况->抢占原则
解决:将正在使用的资源的低优先级任务,临时将其优先级提升至和阻塞的任务平级,当资源释放后,恢复
互斥信号量的API:
创建互斥信号量
删除互斥信号量
获取互斥信号量
释放互斥信号量
创建: □(互斥句柄) xSemaphoreMutux( )
句柄变量,赋值,判断是否成功
删除:vSemaphoreDelete(句柄)
释放:vSemaphoreGive(句柄)
vSemaphoreGiveFromISR(句柄,高优先级的状态唤醒)
portYIEID-From-ISR
获取:xSemaphoreTask( )
事件:
任务与任务之间,任务与中断中的一种同步机制;
应用:一些危险的(机器)启动,要经过很多的检测(校验),每个或者部分条件满足才可启动;
运行流程:
Task1 Task2 Task3等等
事件组
中断1 中断2
task1的启动,几个条件都要满足,如果有一个条件不满足,那么task1只能打转;
事件可以做到一对多,多对多;
而且没有一个具体的数据的传输,只有一个标志位;
32位的,只用到了低的24位,整个事件组用到了0到23位;
事件发生了,就置为1,如果四个都置为1,就说明了条件都满足了,就可启动。
事件的API:
创建事件组:
□(事件句柄) xEventGroupCreate( )
创建句柄;句柄赋值;判断成功/失败
删除事件组:
xEventGroupDelete(句柄)
置位事件组:
xEventGroupSitBits(句柄,事件标志位)
事件标志位:
在中断里的置位:
xEventGroupSitBitsFromISR(句柄,事件标志位,唤醒状态位)//用守护任务,去负责给事件组置位;因为freeRTOS有个原则:在中断里不能去处理不确定的任务(不知道事件组给谁用,给哪些任务或者中断去用)守护任务其实就是软件定时器
等待事件组:
xEventGroupWaitBits(句柄,等待被设置的标志位,选择清零,选择是否满足所有事件,超时等待)
//3:如果不清除,表示以后会反复被触发(和中断一样,如果中断标志位不清除的话,就会一直被触发)
//4:事件组中有几个标志位已经被设置为1了,选择满足几个条件事件才触发,其实就是一个满足规则
freeRTOS的软件定时器
介绍:
基于SYSTick实现的
与硬件无关
可以创建N个 -->软件定时器组
应用:当有多个定时需求而硬件定时器资源不多时,就要用软件定时器在一些对精准要求不高的定时场景下(SYSTick负责计时,但是中断优先级不高)
运行流程:RTOS启动------守护任务(prvTimertask/Daemon),在这个守护进程中,主要做的事:获取到定时器相关的回调函数;将所有的定时器指令放到定时器命名队列(定时器指令:开启、停止、恢复、删除等等,这些指令会被放到特殊的队列里,Time Commod queue定时器命名队列)(定时器指令下发时机在守护进程创建之后)
定时器的API:
创建定时器:
□定时器的句柄 xTimerCreate (定时器名字,定时时间,模式,ID,回调函数)
//模式分为:单次模式和周期模式
//回调函数:当前定时器要执行的业务(可能是lcd屏幕的刷新,也可能是DHT11的数据读取)
开启定时器:
停止定时器:
删除定时器:
这三个是一组函数:
pdfault/pdpass(两种返回值,前者是指令发送失败,后者是指令通过) xTimer Delete/Start/Stop(句柄,超时)
//超时:有两层含义:
1、具体的任务执行由守护进程去做;
2、当定时器消息队列已满,当前指令要等待队列有空位才能加入
获取定时器的ID:
ID prTimerGetTimerIDC(句柄)
//返回的是ID
独立看门狗:
这里的看门狗与STM32中的是同一个,但是用法不同;
在STM32中的应用:喂狗,改变OVT
在freeRTOS中的应用:
监控任务
监控任务由独立看门狗去监控自身;
只有规定的任务事件发生了,才回去喂狗
注意:监控任务的优先级必须最高—发条件满足,立即喂狗;
当前项目中的所有任务,不可阻塞/挂起/删除
(所有核心任务)
喂狗给的时间要足够长—被监控任务重最长发送事件标志位的时间
被监控的任务最多有24个(事件组只有24个标志位可用)
动态内存管理:
任务、消息队列、信号量、定时器都需要RAM(内存空间),所以就需要动态内存管理
主要管理:
空间申请,释放,合并
5种管理策略:
1、只申请,不释放
2、申请然后释放,但是不合并
3、2+线程安全(malloc/free)
4、申请,释放,合并
5、4+支持将动态内存设置在不连续的区域上
Tickless低功耗模式:
电池类电子产品–>低功耗
低功耗的核心思想:如果不工作,就停止运行;就不去耗电;
睡眠模式:CPU不工作,其他外设工作
停止:时钟总线不工作,杀死了所有外设
待机模式:切断CPU的供电(1.8V)
freeRTOS中的低功耗
Tickless模式:
工作原理:
减少SYSTick的节拍运行
Tickless的低功耗模式使我们现在所有小型的RTOS中通用的;
触发时机:当所有任务都被阻塞/挂起时,由空闲任务去执行低功耗
低功耗主要针对的对象是SYSTick,让其在无任务时尽量不工作;
运行流程:
1、在空闲任务中关闭SYSTick中断(坏处:只让任务一阻塞一秒钟–osDelay(1000),然而systick被关闭,这个任务就无法被唤醒了)–因此这个方案不可行;
2、当进入空闲任务后,计算出下一个执行的高优先级任务还剩多少阻塞时间,以此去改变SYSTick的重装载值,作为这段时间的唤醒时间;
Tickless-IDIE设置为1,自动进入低功耗