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