文章依据韦东山老师的FreeRTOS教学FreeRTOS入门与工程实训文档

1 堆和栈

1.1 堆

堆是程序员可以手动控制的一块内存空间

程序员可以通过pvPortMalloc 、vPortFree(即C语言中的malloc、free)等函数来管理栈

在FreeRTOS中,有五种不同的堆管理方法,分别为Heap_1-Heap_5

FreeRTOS五种内存分配方案比较

C文件 优点 缺点
Heap_1.c 分配简单、时间确定 只分配、不回收
Heap_2.c 动态分配、最佳匹配 会有碎片内存、时间不定
Heap_3.c 调用标准库函数 速度慢、时间不定
Heap_4.c 相邻空闲内存可合并 可解决碎片问题、时间不定
Heap_5.c 在heap_4基础上可管理多块、分隔开的内存 可解决碎片问题

1.2 栈

栈是有编译器来自动控制的一块内存空间,在FreeRTOS中,每一个任务都有一个单独的栈

栈一般用来存放任务中的局部变量,以及任务切换时保护现场

2 任务

2.1 基本概念

对于整个单片机程序,我们称之为application,应用程序。

使用FreeRTOS时,我们可以在application中创建多个任务(task),有些文档把任务也称为线程(thread)。

FreeRTOS的任务有三要素:要做什么、栈和优先级

在FreeRTOS中,任务就是一个函数,原型如下:

void ATaskFunction( void *pvParameters );

要注意的是:

  • 这个函数不能返回
  • 同一个函数,可以用来创建多个任务;换句话说,多个任务可以运行同一个函数
  • 函数内部,尽量使用局部变量:
    • 每个任务都有自己的栈
    • 每个任务运行这个函数时
      • 任务A的局部变量放在任务A的栈里、任务B的局部变量放在任务B的栈里
      • 不同任务的局部变量,有自己的副本
    • 函数使用全局变量、静态变量的话
      • 只有一个副本:多个任务使用的是同一个副本
      • 要防止冲突(后续会讲) 下面是一个示例:
void ATaskFunction( void *pvParameters )
{
    /* 对于不同的任务,局部变量放在任务的栈里,有各自的副本 */
    int32_t lVariableExample = 0;

    /* 任务函数通常实现为一个无限循环 */
    for( ;; )
    {
        /* 任务的代码 */
    }

    /* 如果程序从循环中退出,一定要使用vTaskDelete删除自己
     * NULL表示删除的是自己
     */
    vTaskDelete( NULL );

    /* 程序不会执行到这里, 如果执行到这里就出错了 */
}

2.2创建任务

创建任务时使用的函数如下:

BaseType_t xTaskCreate( TaskFunction_t pxTaskCode, // 函数指针, 任务函数
                        const char * const pcName, // 任务的名字
                        const configSTACK_DEPTH_TYPE usStackDepth, // 栈大小,单位为word,10表示40字节
                        void * const pvParameters, // 调用任务函数时传入的参数
                        UBaseType_t uxPriority,    // 优先级
                        TaskHandle_t * const pxCreatedTask ); // 任务句柄, 以后使用它来操作这个任务
参数 描述
pvTaskCode 函数指针,任务对应的 C 函数。任务应该永远不退出,或者在退出时调用 "vTaskDelete(NULL)"。(NULL)表示要删除的任务是自己
pcName 任务的名称,仅用于调试目的,FreeRTOS 内部不使用。pcName 的长度为 configMAX_TASK_NAME_LEN。
usStackDepth 每个任务都有自己的栈,usStackDepth 指定了栈的大小,单位为 word。例如,如果传入 100,表示栈的大小为 100 word,即 400 字节。最大值为 uint16_t 的最大值。确定栈的大小并不容易,通常是根据估计来设定。精确的办法是查看反汇编代码。
pvParameters 调用 pvTaskCode 函数指针时使用的参数:pvTaskCode(pvParameters)。
uxPriority 任务的优先级范围为 0~(configMAX_PRIORITIES – 1)。数值越小,优先级越低。如果传入的值过大,xTaskCreate 会将其调整为 (configMAX_PRIORITIES – 1)。
pxCreatedTask 用于保存 xTaskCreate 的输出结果,即任务的句柄(task handle)。如果以后需要对该任务进行操作,如修改优先级,则需要使用此句柄。如果不需要使用该句柄,可以传入 NULL。
返回值 成功时返回 pdPASS,失败时返回 errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY(失败原因是内存不足)。请注意,文档中提到的失败返回值是 pdFAIL 是不正确的。pdFAIL 的值为 0,而 errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY 的值为 -1。

2.3 任务管理与调度

※任务的结束不能return,只能够删除

FreeRTOS的调度原则:

1.相同优先级的任务轮流运行

2.高优先级的任务

结论:

1.高优先级的任务未执行完,低优先级的任务无法执行

2.一旦高优先级任务就绪,会马上执行

3.高优先级的任务有多个,它们轮流运行


FreeRTOS源码中会有一个ReadyLists数组存放56个链表,代表相对于优先级为N的处于Ready/Running的任务

新建任务的时候会在同优先级pxCurrentTCB指向的任务后面添加任务,并将pxCurrentTCB指针指向新创建的任务

使用vTastDelay函数等触发等待态的任务会从ReadyLists数组转移到DelayedTaskList中,记录等待时长,并马上发出一次调度信号

使用vTaskSuspend函数后会将任务转入xSuspendedTaskList中,直到使用xTaskResume函数后转入ReadLists数组


FreeRTOS调度逻辑:

​ 1.使用内部Tick每1ms中断一次,并计数调度次数(cnt++)

​ 2.判断DelayedTaskLIst中的任务是否可恢复,并启动一次调度

​ 3.调度:遍历ReadyLists数组中的链表,找到第一个非空的List,并运行pxCurrentTCB指针指向的任务(当前指向的任务)

2.4 任务的状态

file
FreeRTOS中主要分为四种状态

1.Ready(就绪态):FreeRTOS中任务呗创建一定处于就绪态,就绪态的任务一旦有条件资源就会立刻转换为运行态并运行

2.Running(运行态):运行态是表示任务正在运行,正在运行的任务会被更高优先级的任务抢占(如果设置了可抢占),并转入就绪态,等待更高级优先级的任务释放资源

3.Blocked(等待事件态):等待时间态只有在运行态

4.Suspended(暂停态):暂停态只会在自身调用或其他任务调用vTastSuspend函数暂停某个任务

2.5 FreeRTOS的Delay

void vTaskDelay( const TickType_t xTicksToDelay ); /* xTicksTpDelay: 等待多少给Tick , 跟HAL_Delay一样*/
BaseType_t xTaskDelayUntil( TickType_t * const pxPreviousWakeTime, const TickType_t xTimeIncrement );
函数说明 周期性的阻塞任务
参数1 pxPreiousWakeTime:上一次记录的使用,需使用xTaskGetTickCount函数获取
参数2 周期阻塞任务的时长
注意事项 函数阻塞任务的时长是不固定的,但如果在函数开始前获取了时间,那函数就会被周期性的阻塞。
不需要在while循环里获取时间,while循环开始前获取完第一次的开始事件后函数在后续会自动更新上一次记录的时间

2.6 空闲任务

FreeRTOS的调度器启动之后就会自动创建一个空闲任务,这样可以确保至少有一个任务可以运行,但是这个空闲任务使用最低优先级,如果应用中有其他高优先级的任务处于就绪态的话这个空闲任务就不会跟高优先级的任务抢占CPU资源。

空闲任务还有另外一个重要的职责,如果某个任务要调用函数vTaskDelete()删除自身,那么这个人物的任务控制块TCB和任务堆栈段这些由FreeRTOS系统自动分配的内存就需要在空闲任务中释放,如果删除的是别的任务那边相应的内存就会被直接释放掉,不需要再空闲任务中释放。因此,一定要给空闲任务执行的机会(既其他任务在不需要的是后都挂起)

3 同步互斥与通信

3.1 同步与互斥的概念

一句话理解同步与互斥:我等你用完厕所,我再用厕所。

什么叫同步?就是:哎哎哎,我正在用厕所,你等会。 什么叫互斥?就是:哎哎哎,我正在用厕所,你不能进来。

同步与互斥经常放在一起讲,是因为它们之的关系很大,“互斥”操作可以使用“同步”来实现。我“等”你用完厕所,我再用厕所。这不就是用“同步”来实现“互斥”吗?

再举一个例子。在团队活动里,同事A先写完报表,经理B才能拿去向领导汇报。经理B必须等同事A完成报表,AB之间有依赖,B必须放慢脚步,被称为同步。在团队活动中,同事A已经使用会议室了,经理B也想使用,即使经理B是领导,他也得等着,这就叫互斥。经理B跟同事A说:你用完会议室就提醒我。这就是使用"同步"来实现"互斥"。

有时候看代码更容易理解,伪代码如下:

 void  抢厕所(void)
 {
   if (有人在用) 我眯一会;
   用厕所;
   喂,醒醒,有人要用厕所吗;
 }

假设有A、B两人早起抢厕所,A先行一步占用了;B慢了一步,于是就眯一会;当A用完后叫醒B,B也就愉快地上厕所了。

在这个过程中,A、B是互斥地访问“厕所”,“厕所”被称之为临界资源。我们使用了“休眠-唤醒”的同步机制实现了“临界资源”的“互斥访问”。

同一时间只能有一个人使用的资源,被称为临界资源。比如任务A、B都要使用串口来打印,串口就是临界资源。如果A、B同时使用串口,那么打印出来的信息就是A、B混杂,无法分辨。所以使用串口时,应该是这样:A用完,B再用;B用完,A再用。

3.2 数据传输的方法

多种方法比较:

数据个数 互斥措施 阻塞-唤醒 使用场景
全局变量 1 一读一写
环形缓冲区 多个 一读一写
队列 多个 多读多写

4 队列

4.1 队列的本质

队列中,数据的读写本质就是环形缓冲区,在这个基础上增加了互斥措施、阻塞-唤醒机制。

如果这个队列不传输数据,只调整“数据个数”,它就是信号量(semaphore)。

如果信号量中,限定“数据个数“最大值为1, 它就是互斥量(mutex)

4.2 队列的常规操作

  • 队列可以包含若干个数据:队列中有若干项,这被称为"长度"(length)
  • 每个数据大小固定
  • 创建队列时就要指定长度、数据大小
  • 数据的操作采用先进先出的方法(FIFO,First In First Out):写数据时放到尾部,读数据时从头部读
  • 也可以强制写队列头部:覆盖头部数据

4.3 传输数据的两种方式:

使用队列传输数据时有两种方法:

  • 拷贝:把数据、把变量的值复制进队列里
  • 引用:把数据、把变量的地址复制进队列里

FreeRTOS使用拷贝值的方法,这更简单:

  • 局部变量的值可以发送到队列中,后续即使函数退出、局部变量被回收,也不会影响队列中的数据
  • 无需分配buffer来保存数据,队列中有buffer
  • 局部变量可以马上再次使用
  • 发送任务、接收任务解耦:接收任务不需要知道这数据是谁的、也不需要发送任务来释放数据
  • 如果数据实在太大,你还是可以使用队列传输它的地址
  • 队列的空间有FreeRTOS内核分配,无需任务操心
  • 对于有内存保护功能的系统,如果队列使用引用方法,也就是使用地址,必须确保双方任务对这个地址都有访问权限。使用拷贝方法时,则无此限制:内核有足够的权限,把数据复制进队列、再把数据复制出队列。

4.4队列函数

使用队列的流程:创建队列、写队列、读队列、删除队列。

4.4.1 创建队列

队列的创建有两种方法:动态分配内存、静态分配内存,

  • 动态分配内存:xQueueCreate,队列的内存在函数内部动态分配
QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength, UBaseType_t uxItemSize );
参数 说明
uxQueueLength 队列长度,最多能存放多少个数据(item)
uxItemSize 每个数据(item)的大小:以字节为单位
返回值 非0:成功,返回句柄,以后使用句柄来操作队列 NULL:失败,因为内存不足
  • 静态分配内存:xQueueCreateStatic,队列的内存要事先分配好
QueueHandle_t xQueueCreateStatic(
                    UBaseType_t uxQueueLength,
                    UBaseType_t uxItemSize,
                    uint8_t *pucQueueStorageBuffer,
                    StaticQueue_t *pxQueueBuffer
                 );
参数 说明
uxQueueLength 队列长度,最多能存放多少个数据(item)
uxItemSize 每个数据(item)的大小:以字节为单位
pucQueueStorageBuffer 如果uxItemSize非0,pucQueueStorageBuffer必须指向一个uint8_t数组, 此数组大小至少为"uxQueueLength * uxItemSize"
pxQueueBuffer 必须执行一个StaticQueue_t结构体,用来保存队列的数据结构
返回值 非0:成功,返回句柄,以后使用句柄来操作队列 NULL:失败,因为pxQueueBuffer为NULL

4.4.2 写队列

可以把数据写到队列头部,也可以写到尾部,这些函数有两个版本:在任务中使用、在ISR中使用。函数原型如下:

/* 等同于xQueueSendToBack
 * 往队列尾部写入数据,如果没有空间,阻塞时间为xTicksToWait
 */
BaseType_t xQueueSend(QueueHandle_t xQueue,const void *pvItemToQueue, ickType_t xTicksToWait );
/* 
 * 往队列尾部写入数据,如果没有空间,阻塞时间为xTicksToWait
 */
BaseType_t xQueueSendToBack(  QueueHandle_t xQueue, const void *pvItemToQueue, TickType_t xTicksToWait );
/* 
 * 往队列尾部写入数据,此函数可以在中断函数中使用,不可阻塞
 */
BaseType_t xQueueSendToBackFromISR(QueueHandle_t xQueue,const void *pvItemToQueue,BaseType_t *pxHigherPriorityTaskWoken);
/* 
 * 往队列头部写入数据,如果没有空间,阻塞时间为xTicksToWait
 */
BaseType_t xQueueSendToFront( QueueHandle_t xQueue, const void *pvItemToQueue, TickType_t xTicksToWait );
/* 
 * 往队列头部写入数据,此函数可以在中断函数中使用,不可阻塞
 */
BaseType_t xQueueSendToFrontFromISR( QueueHandle_t xQueue, const void *pvItemToQueue, BaseType_t *pxHigherPriorityTaskWoken );

这些函数用到的参数是类似的,统一说明如下:

参数 说明
xQueue 队列句柄,要写哪个队列
pvItemToQueue 数据指针,这个数据的值会被复制进队列, 复制多大的数据?在创建队列时已经指定了数据大小
xTicksToWait 如果队列满则无法写入新数据,可以让任务进入阻塞状态, xTicksToWait表示阻塞的最大时间(Tick Count)。 如果被设为0,无法写入数据时函数会立刻返回; 如果被设为portMAX_DELAY,则会一直阻塞直到有空间可写
返回值 pdPASS:数据成功写入了队列 errQUEUE_FULL:写入失败,因为队列满了。

4.4.3 读队列

使用 xQueueReceive() 函数读队列,读到一个数据后,队列中该数据会被移除。这个函数有两个版本:在任务中使用、在ISR中使用。函数原型如下:

BaseType_t xQueueReceive( QueueHandle_t xQueue, void * const pvBuffer, TickType_t xTicksToWait );

BaseType_t xQueueReceiveFromISR( QueueHandle_t    xQueue, void *pvBuffer, BaseType_t *pxTaskWoken );

参数说明如下:

参数 说明
xQueue 队列句柄,要读哪个队列
pvBuffer bufer指针,队列的数据会被复制到这个buffer 复制多大的数据?在创建队列时已经指定了数据大小
xTicksToWait 果队列空则无法读出数据,可以让任务进入阻塞状态, xTicksToWait表示阻塞的最大时间(Tick Count)。 如果被设为0,无法读出数据时函数会立刻返回; 如果被设为portMAX_DELAY,则会一直阻塞直到有数据可写
返回值 pdPASS:从队列读出数据入 errQUEUE_EMPTY:读取失败,因为队列空了。

4.5 队列集

※STM32CubeMX生成的代码在FreeRTOS的头文件宏定义中

define configUSE_QUEUE_SETS 0使用队列集的宏定义默认为0

可以在FreeRTOSConfig.h配置头文件中添加#define configUSE_QUEUE_SETS 1

队列集的本质也是队列,但其中存放的是“队列句柄”。

当驱动程序往队列A中写数据的时候,会自动判断当前队列是否属于某一个队列集,如果有则将队列A的句柄写入队列集中

上层任务的数据处理流程

​ 1.阻塞读队列集

​ 2.读出队列集中的队列句柄

​ 3.读队列句柄中的数据

4.5.1 创建队列集

QueueSetHandle_t xQueueCreateSet( const UBaseType_t uxEventQueueLength )
参数 说明
uxQueueLength 队列集长度,最多能存放多少个数据(队列句柄)
返回值 非0:成功,返回句柄,以后使用句柄来操作队列
NULL:失败,因为内存不足

4.5.2 把队列加入队列集

BaseType_t xQueueAddToSet( QueueSetMemberHandle_t xQueueOrSemaphore,

                QueueSetHandle_t xQueueSet );
参数 说明
xQueueOrSemaphore 队列句柄,这个队列要加入队列集
xQueueSet 队列集句柄
返回值 pdTRUE:成功pdFALSE:失败

4.5.3 读取队列集

QueueSetMemberHandle_t xQueueSelectFromSet( QueueSetHandle_t xQueueSet,

                        TickType_t const xTicksToWait );
参数 说明
xQueueSet 队列集句柄
xTicksToWait 如果队列集空则无法读出数据,可以让任务进入阻塞状态,xTicksToWait表示阻塞的最大时间(Tick Count)。如果被设为0,无法读出数据时函数会立刻返回;如果被设为portMAX_DELAY,则会一直阻塞直到有数据可写
返回值 NULL:失败,队列句柄:成功

5 信号量

5.1 信号量的本质

  • 信号:起通知作用
  • 量:还可以用来表示资源的数量
    • 当"量"没有限制时,它就是"计数型信号量"(Counting Semaphores)
    • 当"量"只有0、1两个取值时,它就是"二进制信号量"(Binary Semaphores)
  • 支持的动作:"give"给出资源,计数值加1;"take"获得资源,计数值减1

计数型信号量的典型场景是:

  • 计数:事件产生时"give"信号量,让计数值加1;处理事件时要先"take"信号量,就是获得信号量,让计数值减1。
  • 资源管理:要想访问资源需要先"take"信号量,让计数值减1;用完资源后"give"信号量,让计数值加1。 信号量的"give"、"take"双方并不需要相同,可以用于生产者-消费者场合:
  • 生产者为任务A、B,消费者为任务C、D
  • 一开始信号量的计数值为0,如果任务C、D想获得信号量,会有两种结果:
    • 阻塞:买不到东西咱就等等吧,可以定个闹钟(超时时间)
    • 即刻返回失败:不等
  • 任务A、B可以生产资源,就是让信号量的计数值增加1,并且把等待这个资源的顾客唤醒
  • 唤醒谁?谁优先级高就唤醒谁,如果大家优先级一样就唤醒等待时间最长的人

二进制信号量跟计数型的唯一差别,就是计数值的最大值被限定为1。

file

5.2 信号量跟队列的对比

差异列表如下:

队列 信号量
可以容纳多个数据, 创建队列时有2部分内存: 队列结构体、存储数据的空间 只有计数值,无法容纳其他数据。 创建信号量时,只需要分配信号量结构体
生产者:没有空间存入数据时可以阻塞 生产者:用于不阻塞,计数值已经达到最大时返回失败
消费者:没有数据时可以阻塞 消费者:没有资源时可以阻塞

5.3 两种信号量的对比

信号量的计数值都有限制:限定了最大值。如果最大值被限定为1,那么它就是二进制信号量;如果最大值不是1,它就是计数型信号量。

差别列表如下:

二进制信号量 技术型信号量
被创建时初始值为0 被创建时初始值可以设定
其他操作是一样的 其他操作是一样的

5.4 信号量函数

5.4.1 创建信号量

使用信号量之前,要先创建,得到一个句柄;使用信号量时,要使用句柄来表明使用哪个信号量。 对于二进制信号量、计数型信号量,它们的创建函数不一样:

二进制信号量 计数型信号量
动态创建 xSemaphoreCreateBinary 计数值初始值为0 xSemaphoreCreateCounting
静态创建 xSemaphoreCreateBinaryStatic xSemaphoreCreateCountingStatic

创建二进制信号量的函数原型如下:

/* 创建一个二进制信号量,返回它的句柄。
 * 此函数内部会分配信号量结构体 
 * 返回值: 返回句柄,非NULL表示成功
 */
SemaphoreHandle_t xSemaphoreCreateBinary( void );
/* 创建一个二进制信号量,返回它的句柄。
 * 此函数无需动态分配内存,所以需要先有一个StaticSemaphore_t结构体,并传入它的指针
 * 返回值: 返回句柄,非NULL表示成功
 */
SemaphoreHandle_t xSemaphoreCreateBinaryStatic( StaticSemaphore_t *pxSemaphoreBuffer );

创建计数型信号量的函数原型如下:

/* 创建一个计数型信号量,返回它的句柄。
 * 此函数内部会分配信号量结构体 
 * uxMaxCount: 最大计数值
 * uxInitialCount: 初始计数值
 * 返回值: 返回句柄,非NULL表示成功
 */
SemaphoreHandle_t xSemaphoreCreateCounting(UBaseType_t uxMaxCount, UBaseType_t uxInitialCount);

/* 创建一个计数型信号量,返回它的句柄。
 * 此函数无需动态分配内存,所以需要先有一个StaticSemaphore_t结构体,并传入它的指针
 * uxMaxCount: 最大计数值
 * uxInitialCount: 初始计数值
 * pxSemaphoreBuffer: StaticSemaphore_t结构体指针
 * 返回值: 返回句柄,非NULL表示成功
 */
SemaphoreHandle_t xSemaphoreCreateCountingStatic( UBaseType_t uxMaxCount,   UBaseType_t uxInitialCount,  StaticSemaphore_t *pxSemaphoreBuffer );

5.4.2 give/take

二进制信号量、计数型信号量的give、take操作函数是一样的。这些函数也分为2个版本:给任务使用,给ISR使用。列表如下:

在任务中使用 在ISR中使用
give xSemaphoreGive xSemaphoreGiveFromISR
take xSemaphoreTake xSemaphoreTakeFromISR

xSemaphoreGive的函数原型如下:

BaseType_t xSemaphoreGive( SemaphoreHandle_t xSemaphore );

xSemaphoreGive函数的参数与返回值列表如下:

参数 说明
xSemaphore 信号量句柄,释放哪个信号量
返回值 pdTRUE表示成功, 如果二进制信号量的计数值已经是1,再次调用此函数则返回失败; 如果计数型信号量的计数值已经是最大值,再次调用此函数则返回失败

pxHigherPriorityTaskWoken的函数原型如下:

BaseType_t xSemaphoreGiveFromISR(
                        SemaphoreHandle_t xSemaphore,
                        BaseType_t *pxHigherPriorityTaskWoken
                    );

xSemaphoreGiveFromISR函数的参数与返回值列表如下:

参数 说明
xSemaphore 信号量句柄,释放哪个信号量
pxHigherPriorityTaskWoken 如果释放信号量导致更高优先级的任务变为了就绪态, 则*pxHigherPriorityTaskWoken = pdTRUE
返回值 pdTRUE表示成功, 如果二进制信号量的计数值已经是1,再次调用此函数则返回失败; 如果计数型信号量的计数值已经是最大值,再次调用此函数则返回失败

xSemaphoreTake的函数原型如下:

BaseType_t xSemaphoreTake(
                   SemaphoreHandle_t xSemaphore,
                   TickType_t xTicksToWait
               );

xSemaphoreTake函数的参数与返回值列表如下:

参数 说明
xSemaphore 信号量句柄,获取哪个信号量
xTicksToWait 如果无法马上获得信号量,阻塞一会: 0:不阻塞,马上返回 portMAX_DELAY: 一直阻塞直到成功 其他值: 阻塞的Tick个数,可以使用pdMS_TO_TICKS()来指定阻塞时间为若干ms
返回值 pdTRUE表示成功

xSemaphoreTakeFromISR的函数原型如下:

BaseType_t xSemaphoreTakeFromISR(
                        SemaphoreHandle_t xSemaphore,
                        BaseType_t *pxHigherPriorityTaskWoken
                    );

xSemaphoreTakeFromISR函数的参数与返回值列表如下:

参数 说明
xSemaphore 信号量句柄,获取哪个信号量
pxHigherPriorityTaskWoken 如果获取信号量导致更高优先级的任务变为了就绪态, 则*pxHigherPriorityTaskWoken = pdTRUE
返回值 pdTRUE表示成功

6 互斥量

6.1 互斥量的使用场合

在多任务系统中,任务A正在使用某个资源,还没用完的情况下任务B也来使用的话,就可能导致问题。

比如对于串口,任务A正使用它来打印,在打印过程中任务B也来打印,客户看到的结果就是A、B的信息混杂在一起。

这种现象很常见:

  • 访问外设:刚举的串口例子
  • 读、修改、写操作导致的问题

对于同一个变量,比如int a,如果有两个任务同时写它就有可能导致问题。 对于变量的修改,C代码只有一条语句,比如:a=a+8;,它的内部实现分为3步:读出原值、修改、写入。

file

我们想让任务A、B都执行add_a函数,a的最终结果是1+8+8=17。

假设任务A运行完代码①,在执行代码②之前被任务B抢占了:现在任务A的R0等于1。

任务B执行完add_a函数,a等于9。

任务A继续运行,在代码②处R0仍然是被抢占前的数值1,执行完②③的代码,a等于9,这跟预期的17不符合。

  • 对变量的非原子化访问

修改变量、设置结构体、在16位的机器上写32位的变量,这些操作都是非原子的。也就是它们的操作过程都可能被打断,如果被打断的过程有其他任务来操作这些变量,就可能导致冲突。

  • 函数重入

"可重入的函数"是指:多个任务同时调用它、任务和中断同时调用它,函数的运行也是安全的。可重入的函数也被称为"线程安全"(thread safe)。

每个任务都维持自己的栈、自己的CPU寄存器,如果一个函数只使用局部变量,那么它就是线程安全的。

函数中一旦使用了全局变量、静态变量、其他外设,它就不是"可重入的",如果该函数正在被调用,就必须阻止其他任务、中断再次调用它。

上述问题的解决方法是:任务A访问这些全局变量、函数代码时,独占它,就是上个锁。这些全局变量、函数代码必须被独占地使用,它们被称为临界资源。

互斥量也被称为互斥锁,使用过程如下:

  • 互斥量初始值为1
  • 任务A想访问临界资源,先获得并占有互斥量,然后开始访问
  • 任务B也想访问临界资源,也要先获得互斥量:被别人占有了,于是阻塞
  • 任务A使用完毕,释放互斥量;任务B被唤醒、得到并占有互斥量,然后开始访问临界资源
  • 任务B使用完毕,释放互斥量

正常来说:在任务A占有互斥量的过程中,任务B、任务C等等,都无法释放互斥量。 但是FreeRTOS未实现这点:任务A占有互斥量的情况下,任务B也可释放互斥量。

6.2 互斥量函数

6.2.1 创建

互斥量是一种特殊的二进制信号量。

使用互斥量时,先创建、然后去获得、释放它。使用句柄来表示一个互斥量。

创建互斥量的函数有2种:动态分配内存,静态分配内存,函数原型如下:

/* 创建一个互斥量,返回它的句柄。
 * 此函数内部会分配互斥量结构体 
 * 返回值: 返回句柄,非NULL表示成功
 */
SemaphoreHandle_t xSemaphoreCreateMutex( void );

/* 创建一个互斥量,返回它的句柄。
 * 此函数无需动态分配内存,所以需要先有一个StaticSemaphore_t结构体,并传入它的指针
 * 返回值: 返回句柄,非NULL表示成功
 */
SemaphoreHandle_t xSemaphoreCreateMutexStatic( StaticSemaphore_t *pxMutexBuffer );

要想使用互斥量,需要在配置文件FreeRTOSConfig.h中定义:

#define configUSE_MUTEXES 1

6.2.2 其他函数

要注意的是,互斥量不能在ISR中使用。

各类操作函数,比如删除、give/take,跟一般是信号量是一样的。

By karlren

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注