FreeRTOS笔记
2022-09-13 16:52:48 2 举报
AI智能生成
FreeRTOS是一个开源的实时操作系统内核,适用于微控制器和嵌入式系统。它提供了任务调度、同步与互斥、内存管理等功能,使得开发者能够轻松地构建复杂的实时应用程序。FreeRTOS具有高度可移植性,支持多种处理器架构,如ARM、MSP430等。此外,FreeRTOS还具有良好的文档和社区支持,便于学习和使用。总之,FreeRTOS是一个强大且易于使用的实时操作系统,适用于各种嵌入式应用场景。
作者其他创作
大纲/内容
可以容纳多个数据,创建队列时有2部分内存: 队列结构体、存储数据的空间
生产者:没有空间存入数据时可以阻塞
消费者:没有数据时可以阻塞
队列
只有计数值,无法容纳其他数据。创建信号量时,只需要分配信号量结构体
生产者:用于不阻塞,计数值已经达到最大时返回失败
消费者:没有资源时可以阻塞
信号量
信号量与队列的对比
一块空闲的内存,需要提供管理函数
malloc:从堆里划出一块空间给程序使用
free:用完后,再把它标记为\"空闲\"的,可以再次使用
函数
堆(heap)
函数调用时局部变量保存在栈中,当前程序的环境也是保存在栈中
可以从堆中分配一块空间用作栈
栈(stack)
堆和栈
内存管理
运行(running)
设置超时时间:在指定时间内阻塞,时间到了就进入就绪状态
时间相关的事件
事件通过:任务通知(task notification)、队列(queue)、事件组(event group)、信号量(semaphoe)、互斥量(mutex)等来获取事件
同步事件:事件由别的任务,或是中断程序产生
等待事件到来
阻塞状态(Blocked)
void vTaskSuspend( TaskHandle_t xTaskToSuspend );
参数xTaskToSuspend表示要暂停的任务,如果为NULL,表示暂停自己
暂停
其他任务调用:vTaskResume
中断程序调用:xTaskResumeFromISR
退出暂停
暂停状态(Suspended)
任务随时可以运行
就绪状态(Ready)
非运行(Not Running)
任务状态
至少等待指定个数的Tick Interrupt才能变为就绪状态
从进入delay开始到退出这段时间
vTaskDelay
等待到指定的绝对时刻,才能变为就绪态
可以使用xTaskDelayUntil来让任务周期性地运行
从任务开始运行到下一次任务运行之前的时间
PS:需要考虑到delay的时间要比任务运行的时间长
vTaskDelayUntil
Delay函数
释放被删除的任务的内存
就绪态与运行态
不能阻塞与暂停
只有两种状态
空闲任务的循环每执行一次,调用一次钩子函数
执行一些低优先级的、后台的、需要连续执行的函数
测量系统的空闲时间,可以算出处理器占用率
系统进入省电模式
可以做的事情
把这个宏定义为1:configUSE_IDLE_HOOK
实现 vApplicationIdleHook 函数
开启函数条件
空闲任务的钩子函数(Idle Task Hook Functions)
空闲任务(idle)
怎么确定哪个就绪态的任务可以切换为运行状态
configUSE_TIME_SLICING
configUSE_PREEMPTION
configUSE_TICKLESS_IDLE
用于关闭Tick中断来实现省电。
高级选项
配置文件FreeRTOSConfig.h的两个配置项
配置调度算法
高优先级的任务先运行、同优先级的就绪态任务如何被选中。
调度算法要确保同优先级的就绪态任务,能\"轮流\"运行。
并不保证任务的运行时间是公平分配的。
策略是\"轮转调度\"(Round Robin Scheduling)
行为体现
被称作\"可抢占调度\"(Pre-emptive),高优先级的就绪任务马上执行。
可以
被称作\"合作调度模式\"(Co-operative Scheduling),不能抢就只能协商。
当前任务执行时,更高优先级的任务就绪了也不能马上运行,只能等待当前任务主动让出CPU资源。
更高优先级的任务都不能抢占,其他同优先级的任务也只能等待。
不可以
1.可否抢占?高优先级的任务能否优先执行(配置项: configUSE_PREEMPTION)
被称为\"时间片轮转\"(Time Slicing),同优先级的任务轮流执行,你执行一个时间片、我再执行一个时间片
轮流执行
(without Time Slicing),当前任务会一直执行,直到主动放弃、或者被高优先级任务抢占
不轮流执行
2.可抢占的前提下,同优先级的任务是否轮流执行(配置项:configUSE_TIME_SLICING)
1).空闲任务优先级最低,每执行一次循环,就看看是否主动让位给用户任务
2).空闲任务跟用户任务一样,大家轮流执行,没有谁更特殊
3.在\"可抢占\"+\"时间片轮转\
可抢占+时间片轮转+空闲任务让步
常用配置
3个角度统一理解多种调度算法
调度算法
任务管理
多任务系统是一个团队,每一个任务就是团队里的一个人
A是厨师炒菜,B是服务员上菜,B必须等待A菜做好之后才能上菜,B依赖A
同步
B对A说用完灶台后提醒我,同步实现互斥
A厨师用灶台炒菜,B厨师长也想用,B只能等着
互斥
“同步”可以实现“互斥”
同一时间只能有一个人使用的资源,被称为临界资源
临界资源
概念
A获取资源,用完后A释放资源
A获取不到资源则阻塞,B释放资源并把A唤醒
A获取不到资源则阻塞,并定个闹钟;A要么超时返回,要么在这段时间内因为B释放资源而被唤醒。
具有类似的操作方法:获取/释放、阻塞/唤醒、超时
任务通知(task notification)、队列(queue)、事件组(event group)、信号量(semaphoe)、互斥量(mutex)
内核方法实现有
数据、状态都可以传输,使用任务通知时,必须指定接受者
N对1的关系:发送者无限制,接收者只能是这个任务
会被覆盖
发通知给谁?必须指定接收任务
只能由接收任务本身获取该通知
核心是任务的TCB里的数值
任务通知
数据:若干个数据谁都可以往队列里放数据,谁都可以从队列里读数据
用来传递数据,发送者、接收者无限制,一个数据只能唤醒一个接收者
数量:0~n谁都可以增加一个数量,谁都可消耗一个数量
用来维持资源的个数,生产者、消费者无限制,1个资源只能唤醒1个接收者
任务、ISR释放信号量时让计数值加1
任务、ISR获得信号量时,让计数值减
核心是\"计数值\"
一个事件用一bit表示,1表示事件发生了,0表示事件没发生
多个位:或、与谁都可以设置(生产)多个位,谁都可以等待某个位、若干个位
用来传递事件,可以是N个事件,发送者、接受者无限制,可以唤醒多个接收者:像广播
事件组
位:数值只有0或1我上锁:1变为0,只能由我开锁:0变为1
就像一个房间,谁使用谁上锁,也只能由他开锁
谁获得互斥量,就必须由谁释放同一个互斥量
互斥量
内核对象
同步互斥与通信
可以用于\"任务到任务\"、\"任务到中断\"、\"中断到任务\"直接传输信息。
队列中有若干项,称为\"长度\"(length)
每个数据大小固定,创建队列时就要指定长度、数据大小
数据的操作采用先进先出的方法(FIFO,First In First Out):写数据时放到尾部,读数据时从头部读
PS:读取队列中的头部数据后,该数据会被删除,头部指向下一个数据
PS:数据写到队列头部,不会覆盖原来头部的数据
基本知识点
局部变量的值可以发送到队列中,后续即使函数退出、局部变量被回收,也不会影响队列中的数据
无需分配buffer来保存数据,队列中有buffer
局部变量可以马上再次使用
发送任务、接收任务解耦:接收任务不需要知道这数据是谁的、也不需要发送任务来释放数据
如果数据实在太大,你还是可以使用队列传输它的地址
队列的空间有FreeRTOS内核分配,无需任务操心
拷贝:把数据、把变量的值复制进队列里
对于有内存保护功能的系统,如果队列使用引用方法,也就是使用地址,必须确保双方任务对这个地址都有访问权限。
引用:把数据、把变量的地址复制进队列里
传输数据的两种方法
只要知道队列的句柄,谁都可以读、写该队列。
可以设置定时:如果能读写了就马上进入就绪态,否则就阻塞直到超时。
如果读写不成功,则阻塞;可以指定超时时间。
当队列中有数据时,优先级最高的任务会进入就绪态
任务的优先级相同,那等待时间最久的任务会进入就绪态
读取队列的任务个数没有限制,当多个任务读取空队列时,这些任务都会进入阻塞状态:
任务读队列时,如果队列没有数据,则该任务可以进入阻塞状态:还可以指定阻塞的时间,队列有数据了,则该阻塞的任务会变为就绪态。如果一直都没有数据,则时间到之后它也会进入就绪态。
当队列中有空间时,优先级最高的任务会进入就绪态
写队列的任务个数没有限制,当多个任务写\"满队列\"时,这些任务都会进入阻塞状态:
任务要写队列时,如果队列满了,该任务也可以进入阻塞状态:可以指定阻塞的时间。队列有空间了,则该阻塞的任务会变为就绪态。如果一直都没有空间,则时间到之后它也会进入就绪态。
说明
队列的阻塞访问
特性
怎么创建、清除、删除队列
队列中消息如何保存
怎么向队列发送数据、怎么从队列读取数据、怎么覆盖队列的数据
在队列上阻塞是什么意思
怎么在多个队列上阻塞
读写队列时如何影响任务的优先级
主要问题
流程:创建队列、写队列、读队列、删除队列。
xQueueCreate,队列的内存在函数内部动态分配
uxQueueLength:队列长度,最多能存放多少个数据(item)
uxItemSize:每个数据(item)的大小:以字节为单位
返回值:非0:成功,返回句柄,以后使用句柄来操作队列NULL:失败,因为内存不足
动态分配内存
xQueueCreateStatic,队列的内存要事先分配好
pucQueueStorageBuffer:如果uxItemSize非0,pucQueueStorageBuffer必须指向一个uint8_t数组,此数组大小至少为\"uxQueueLength * uxItemSize\"
pxQueueBuffer:必须执行一个StaticQueue_t结构体,用来保存队列的数据结构
返回值:非0:成功,返回句柄,以后使用句柄来操作队列NULL:失败,因为pxQueueBuffer为NULL
静态分配内存
创建方法
队列刚被创建时,里面没有数据;使用过程中可以调用 xQueueReset() 把队列恢复为初始状态
pxQueue : 复位哪个队列(队列句柄)
返回值: pdPASS(必定成功)
BaseType_t xQueueReset( QueueHandle_t pxQueue);
复位
只能删除使用动态方法创建的队列,它会释放内存
void vQueueDelete( QueueHandle_t xQueue );
删除
可以把数据写到队列头部,也可以写到尾部
在任务中使用
在ISR中使用
写队列头部
写队列尾部
写队列
读到一个数据后,队列中该数据会被移除
xQueue:队列句柄,要读哪个队列
pvBuffer:bufer指针,队列的数据会被复制到这个buffer复制多大的数据?在创建队列时已经指定了数据大小
xTicksToWait:队列空则无法读出数据,可以让任务进入阻塞状态,xTicksToWait表示阻塞的最大时间(Tick Count)。 如果被设为0,无法读出数据时函数会立刻返回;如果被设为portMAX_DELAY,则会一直阻塞直到有数据可写
返回值:pdPASS:从队列读出数据errQUEUE_EMPTY:读取失败,因为队列为空
参数说明
两种方式
读队列
可以查询队列中有多少个数据、有多少空余空间
UBaseType_t uxQueueMessagesWaiting( const QueueHandle_t xQueue );
UBaseType_t uxQueueSpacesAvailable( const QueueHandle_t xQueue );
返回队列中可用数据的个数
查询
当队列长度为1时,可以使用 xQueueOverwrite() 或 xQueueOverwriteFromISR() 来覆盖数据。
当队列满时,函数会覆盖队列里的数据,可认为这些函数不会被阻塞。
PS:队列长度必须为1
xQueue: 写哪个队列
pvItemToQueue: 数据地址
覆盖
如果想让队列中的数据供多方读取,也就是说读取时不要移除数据,要保留数据
函数会从队列中复制出数据,但是不移除数据
如果队列中没有数据,那么\"偷看\"时会导致阻塞;一旦队列中有数据,以后每次\"偷看\"都会成功
xQueue: 偷看哪个队列
xTicksToWait: 没有数据的话阻塞一会
偷看
向队列写入一个结构体数据
typedef struct { ID_t eDataID; int32_t lDataValue;}Data_t;
分辨数据源
例如传输1000字节的结构体:把它的地址写入队列,对方从队列得到这个地址,使用地址去访问那1000字节的数据。
RAM的所有者、操作者,必须清晰明了
确保不能同时修改RAM,比如,在写队列之前只有由发送者修改这块RAM,在读队列之后只能由接收者访问这块RAM。
RAM应该是全局变量,或者是动态分配的内存。对于动然分配的内存,要确保它不能提前释放:要等到接收者用完后再释放。
使用地址来间接传输数据,数据放在RAM里,对于这块RAM:
队列的长度为1,只保存地址
大数据传输
队列长度只有1
因为是覆盖,那么无论邮箱中是否有数据,这些函数总能成功写入数据
写邮箱:新数据覆盖旧数据,在任务中使用 xQueueOverwrite() ,在中断中使用xQueueOverwriteFromISR() 。
第一次调用时会因为无数据而阻塞,一旦写入数据,以后读邮箱时总能成功。
读邮箱:读数据时,数据不会被移除;在任务中使用 xQueuePeek() ,在中断中使用xQueuePeekFromISR() 。
邮箱(Mailbox)
实操
队列(queue)
例:我的事做完了,通知你
例:厨师炒菜,炒好一个菜了,炒好两个菜了......
传递状态,并不需要传递具体的信息,更节省内存
怎么创建、删除信号量
怎么发送、获得信号量
什么是计数型信号量?什么是二进制信号量?
信号:起通知作用
当\"量\"没有限制时,它就是\"计数型信号量\"(Counting Semaphores)
当\"量\"只有0、1两个取值时,它就是\"二进制信号量\"(Binary Semaphores)
量:还可以用来表示资源的数量
支持的动作:\"give\"给出资源,计数值加1;\"take\"获得资源,计数值减1
计数:事件产生时\"give\"信号量,让计数值加1;处理事件时要先\"take\"信号量,就是获得信号量,让计数值减1。
资源管理:要想访问资源需要先\"take\"信号量,让计数值减1;用完资源后\"give\"信号量,让计数值加1
生产者为任务A、B,消费者为任务C、D
阻塞:没有值,设置闹钟(超时时间)
即刻返回失败:不等
一开始信号量的计数值为0,任务C、D想获得信号量,两种方式:
优先高优先级唤醒
优先级相同时,优先等待时间最长的
任务A、B可以生产资源,让信号量的计数值增加1,唤醒消费者,两种情况:
具体场景
计数型信号量的典型场景
信号量的计数值都有限制:限定了最大值。如果最大值被限定为1,那么它就是二进制信号量;如果最大值不是1,它就是计数型信号量。
被创建时初始值为0
二进制信号量
被创建时初始值可以设定
计数型信号量
两种类型对比
使用信号量时,先创建、然后去添加资源、获得资源。使用句柄来表示一个信号量
使用信号量时,要使用句柄来表明使用哪个信号量
计数值初始值为0
函数内部会分配信号量结构体
返回值: 返回句柄,非NULL表示成功
SemaphoreHandle_t xSemaphoreCreateBinary( void );
动态创建
此函数无需动态分配内存,所以需要先有一个StaticSemaphore_t结构体,并传入它的指针
SemaphoreHandle_t xSemaphoreCreateBinaryStatic( StaticSemaphore_t*pxSemaphoreBuffer );
静态创建
uxMaxCount: 最大计数值
uxInitialCount: 初始计数值
SemaphoreHandle_t font color=\"#2196f3\
创建
对于动态创建的信号量,不再需要它们时,可以删除它们以回收内存
xSemaphore: 信号量句柄
void vSemaphoreDelete( SemaphoreHandle_t xSemaphore );
xSemaphore:信号量句柄
如果二进制信号量的计数值已经是1,再次调用此函数则返回失败;
如果计数型信号量的计数值已经是最大值,再次调用此函数则返回失败
返回值:pdTRUE表示成功
BaseType_t xSemaphoreGive( SemaphoreHandle_t xSemaphore );
任务中使用
如果释放信号量导致更高优先级的任务变为了就绪态,则*pxHigherPriorityTaskWoken = pdTRUE
pxHigherPriorityTaskWoken:
BaseType_t font color=\"#2196f3\
ISR中使用
give
如果无法马上获得信号量,阻塞一会
0:不阻塞,马上返回
portMAX_DELAY: 一直阻塞直到成功
其他值: 阻塞的Tick个数,可以使用 pdMS_TO_TICKS() 来指定阻塞时间为若干ms
xTicksToWait:
如果获取信号量导致更高优先级的任务变为了就绪态,则*pxHigherPriorityTaskWoken = pdTRUE
take
give/take
信号量(semaphore)
为什么要实现互斥操作
怎么使用互斥量
互斥量导致的优先级反转、优先级继承
信号量初始值为1
厨师A要用灶台炒菜,\"take\"信号量成功,开火炒菜
厨师B也想炒菜,\"take\"信号量不成功,等待
厨师A用完灶台,\"give\"信号量;轮到任务B使用
PS:需要用户保证厨师B按规矩执行,不抢灶台使用,别的厨师不会\"give\"信号量
信号量例子
队列、信号量,都可以实现互斥访问
量:值为0、1
互斥:用来实现互斥访问
谁上锁,就只能由谁开锁
需用户自己保证,做好规定
定义
多任务系统中,任务A正在使用某个资源,使用过程中任务B也来使用的话,就可能导致问题
修改变量、设置结构体、在16位的机器上写32位的变量,这些操作都是非原子的。这些的操作过程都可能被打断,如果被打断的过程有其他任务来操作这些变量,就可能导致冲突
对变量的非原子化访问
多个任务同时调用它、任务和中断同时调用它,函数的运行也是安全的。可重入的函数也被称为\"线程安全\"(thread safe)
如果一个函数只使用局部变量,那么它就是线程安全
函数中一旦使用了全局变量、静态变量、其他外设,它就不是\"可重入的\",如果此函数正在被调用,就必须阻止其他任务、中断再次调用它。所以引入了互斥量来保证
可重入的函数
使用场景
互斥量初始值为1
任务A想访问临界资源,先获得并占有互斥量,然后开始访问
任务B也想访问临界资源,也要先获得互斥量:被任务A占有了,于是阻塞
任务A使用完毕,释放互斥量;任务B被唤醒、得到并占有互斥量,然后开始访问临界资源
任务B使用完毕,释放互斥量
底层原理
##define configUSE_MUTEXES 1
PS:想使用互斥量,需要在配置文件FreeRTOSConfig.h中定义
互斥量是一种特殊的二进制信号量
使用互斥量时,先创建、然后去获得、释放它。使用句柄来表示一个互斥量
此函数内部会分配互斥量结构体
SemaphoreHandle_t xSemaphoreCreateMutex( void );
SemaphoreHandle_t xSemaphoreCreateMutexStatic( StaticSemaphore_t *pxMutexBuffer);
释放互斥量
ISR中释放
获得互斥量
ISR中获得
其他函数
PS:互斥量不能在ISR中使用。
刚创建的互斥量可以被成功\"take
\"take\"互斥量成功的任务,被称为\"holder\",只能由它\"give\"互斥量;别的任务\"give\"不成功
在ISR中不能使用互斥量
特点
低优先级任务A获得了串口的互斥量
高优先级任务B也想使用串口,它将会阻塞、等待A释放互斥量
高优先级的任务,被低优先级的任务延迟,这被称为\"优先级反转\"(priority inversion)
场景
互斥量可以通过\"优先级继承\",可以很大程度解决\"优先级反转\"的问题,这也是FreeRTOS中互斥量和二级制信号量的差别
优先级反转
假设持有互斥锁的是任务A,如果更高优先级的任务B也尝试获得这个锁
任务B发现低优先级的A上锁后,就会把自己的高优先级状态给A
等任务A释放互斥锁时,它就恢复为原来的优先级
互斥锁内部就实现了优先级的提升、恢复
优先级继承
基本使用
任务A获得互斥量1,任务B获得互斥量2
任务A还要互斥量2才进行下一步,任务B也还要互斥量1才进行下一步,都阻塞等待互斥量解锁
死锁
任务A获得了互斥锁后,去调用另一个函数,这个函数也需要这个互斥锁
这个函数便阻塞等待任务A来释放互斥锁!
自我死锁
任务A获得递归锁M后,它还可以多次去获得这个锁
\"take\"了N次,要\"give\"N次,这个锁才会被释放
SemaphoreHandle_t xSemaphoreCreateRecursiveMutex( void );
BaseType_t xSemaphoreGiveRecursive( SemaphoreHandle_t xSemaphore );
释放
获得
递归锁
互斥量(mutex)
事件组的概念与操作函数
事件组的优缺点
怎么设置、等待、清除事件组中的位
使用事件组来同步多个任务
助手A备菜;助手B洗菜;助手C切菜
厨师看都准备好了便开始炒菜
厨师炒菜
四五家供应商可提供新鲜菜
厨师看哪家最快送到厨房便用哪家的炒菜
厨师买菜
使用说明
可简单认为是一个整数,它每一位表示一个事件
每一位事件的含义由用户决定,比如:Bit0表示用来串口是否就绪,Bit1表示数据是否接收完成
值为1表示事件已发生,值为0表示事件未发生
一个或多个任务、ISR都可以去写这些位;一个或多个任务、ISR都可以去读这些位
可以等待某一位、某些位中的任意一个,也可以等待多位
高8位留给内核使用,只能用其他的位来表示事件
子主题
事件组(event group)
FreeRTOS
收藏
0 条评论
回复 删除
下一页