第23章
FreeRTOS互斥信号量
本章节讲解FreeRTOS重要的资源共享机制---互斥信号量(Mutex,即Mutual
Exclusion的缩写)。注意,建议初学者学习完前两个章节的信号量后再学习本章节的互斥信号量。
FreeRTOS中互斥信号量的源码实现是基于消息队列实现的。
本章教程配套的例子含Cortex-M3内核的STM32F103和Cortex-M4内核的STM32F407以及F429。
23.1
互斥信号量
23.2
互斥信号量API函数
23.3
实验例程说明
23.4
总结
23.1 互斥信号量
23.1.1 互斥信号量的概念及其作用
互斥信号量的主要作用是对资源实现互斥访问,使用二值信号量也可以实现互斥访问的功能,不过互斥信号量与二值信号量有区别。下面我们先举一个通过二值信号量实现资源独享,即互斥访问的例子,让大家有一个形象的认识,进而引出要讲解的互斥信号量。
运行条件:
u
让两个任务Task1和Task2都运行串口打印函数printf,这里我们就通过二值信号量实现对函数printf的互斥访问。如果不对函数printf进行互斥访问,串口打印容易出现乱码。
u
用计数信号量实现二值信号量只需将计数信号量的初始值设置为1即可。
代码实现:
u
创建二值信号量
static
SemaphoreHandle_t xSemaphore = NULL;
static
void AppObjCreate (void)
{
xSemaphore = xSemaphoreCreateBinary();
if(xSemaphore == NULL)
{
}
xSemaphoreGive(xSemaphore);
}
u
通过二值信号量实现对printf函数互斥访问的两个任务
static
void vTaskLED(void *pvParameters)
{
TickType_t xLastWakeTime;
const TickType_t xFrequency = 300;
xLastWakeTime = xTaskGetTickCount();
while(1)
{
xSemaphoreTake(xSemaphore,
portMAX_DELAY);
printf("任务vTaskLED在运行\r\n");
bsp_LedToggle(1);
bsp_LedToggle(4);
xSemaphoreGive(xSemaphore);
vTaskDelayUntil(&xLastWakeTime, xFrequency);
}
}
static
void vTaskMsgPro(void *pvParameters)
{
TickType_t xLastWakeTime;
const TickType_t xFrequency = 300;
xLastWakeTime = xTaskGetTickCount();
while(1)
{
xSemaphoreTake(xSemaphore, portMAX_DELAY);
printf("任务vTaskMsgPro在运行\r\n");
bsp_LedToggle(2);
bsp_LedToggle(3);
xSemaphoreGive(xSemaphore);
vTaskDelayUntil(&xLastWakeTime, xFrequency);
}
}
有了上面二值信号量的认识之后,互斥信号量与二值信号量又有什么区别呢?互斥信号量可以防止优先级翻转,而二值信号量不支持,下面我们就讲解一下优先级翻转问题。
23.1.2 优先级翻转问题
下面我们通过如下的框图来说明一下优先级翻转的问题,让大家有一个形象的认识。
运行条件:
u
创建3个任务Task1,Task2和Task3,优先级分别为3,2,1。也就是Task1的优先级最高。
u
任务Task1和Task3互斥访问串口打印printf,采用二值信号实现互斥访问。
u
起初Task3通过二值信号量正在调用printf,被任务Task1抢占,开始执行任务Task1,也就是上图的起始位置。
运行过程描述如下:
u
任务Task1运行的过程需要调用函数printf,发现任务Task3正在调用,任务Task1会被挂起,等待Task3释放函数printf。
u
在调度器的作用下,任务Task3得到运行,Task3运行的过程中,由于任务Task2就绪,抢占了Task3的运行。优先级翻转问题就出在这里了,从任务执行的现象上看,任务Task1需要等待Task2执行完毕才有机会得到执行,这个与抢占式调度正好反了,正常情况下应该是高优先级任务抢占低优先级任务的执行,这里成了高优先级任务Task1等待低优先级任务Task2完成。所以这种情况被称之为优先级翻转问题。
u
任务Task2执行完毕后,任务Task3恢复执行,Task3释放互斥资源后,任务Task1得到互斥资源,从而可以继续执行。
上面就是一个产生优先级翻转问题的现象。
23.1.3 FreeRTOS互斥信号量的实现
FreeRTOS互斥信号量是怎么实现的呢?其实相对于二值信号量,互斥信号量就是解决了一下优先级翻转的问题。下面我们通过如下的框图来说明一下FreeRTOS互斥信号量的实现,让大家有一个形象的认识。
运行条件:
u
创建2个任务Task1和Task2,优先级分别为1和3,也就是任务Task2的优先级最高。
u
任务Task1和Task2互斥访问串口打印printf。
u
使用FreeRTOS的互斥信号量实现串口打印printf的互斥访问。
运行过程描述如下:
u
低优先级任务Task1执行过程中先获得互斥资源printf的执行。此时任务Task2抢占了任务Task1的执行,任务Task1被挂起。任务Task2得到执行。
u
任务Task2执行过程中也需要调用互斥资源,但是发现任务Task1正在访问,此时任务Task1的优先级会被提升到与Task2同一个优先级,也就是优先级3,这个就是所谓的优先级继承(Priority
inheritance),这样就有效地防止了优先级翻转问题。任务Task2被挂起,任务Task1有新的优先级继续执行。
u
任务Task1执行完毕并释放互斥资源后,优先级恢复到原来的水平。由于互斥资源可以使用,任务Task2获得互斥资源后开始执行。
上面就是一个简单的FreeRTOS互斥信号量的实现过程。
23.1.4FreeRTOS中断方式互斥信号量的实现
互斥信号量仅支持用在FreeRTOS的任务中,中断函数中不可使用。
23.2互斥信号量API函数
使用如下18个函数可以实现FreeRTOS的信号量(含计数信号量,二值信号量和互斥信号):
(1) xSemaphoreCreateBinary()
(2) xSemaphoreCreateBinaryStatic()
(3) vSemaphoreCreateBinary()
(4) xSemaphoreCreateCounting()
(5) xSemaphoreCreateCountingStatic()
(6) xSemaphoreCreateMutex()
(7) xSemaphoreCreateMutexStatic()
(8) xSem'CreateRecursiveMutex()
(9) xSem'CreateRecursiveMutexStatic()
(10) vSemaphoreDelete()
(11) xSemaphoreGetMutexHolder()
(12) uxSemaphoreGetCount()
(13) xSemaphoreTake()
(14) xSemaphoreTakeFromISR()
(15) xSemaphoreTakeRecursive()
(16) xSemaphoreGive()
(17) xSemaphoreGiveRecursive()
(18) xSemaphoreGiveFromISR()
关于这18个函数的讲解及其使用方法可以看FreeRTOS在线版手册:

这里我们重点的说以下3个函数:
(1) xSemaphoreCreateMutex
()
(2) xSemaphoreGive
()
(3) xSemaphoreTake
()
因为本章节配套的例子使用的是这3个函数。
23.2.1 函数xSemaphoreCreateMutex
函数原型:
SemaphoreHandle_t
xSemaphoreCreateMutex( void )
函数描述:
函数xSemaphoreCreateMutex用于创建互斥信号量。
u
返回值,如果创建成功会返回互斥信号量的句柄,如果由于FreeRTOSConfig.h文件中heap大小不足,无法为此互斥信号量提供所需的空间会返回NULL。
使用这个函数要注意以下问题:
1.
此函数是基于函数xQueueCreateMutex实现的:
#define
xSemaphoreCreateMutex() xQueueCreateMutex( queueQUEUE_TYPE_MUTEX
)
函数xQueueCreateMutex的实现是基于消息队列函数xQueueGenericCreate实现的。
2.
使用此函数要在FreeRTOSConfig.h文件中使能宏定义:
#define
configUSE_MUTEXES 1
使用举例:
static
SemaphoreHandle_t xMutex = NULL;
static
void AppObjCreate (void)
{
xMutex =
xSemaphoreCreateMutex();
if(xSemaphore == NULL)
{
}
}
23.2.2函数xSemaphoreGive
函数原型:
xSemaphoreGive(
SemaphoreHandle_t xSemaphore );
函数描述:
函数xSemaphoreGive用于在任务代码中释放信号量。
u
第1个参数是信号量句柄。
u
返回值,如果信号量释放成功返回pdTRUE,否则返回pdFALSE,因为信号量的实现是基于消息队列,返回失败的主要原因是消息队列已经满了。
使用这个函数要注意以下问题:
1.
此函数是基于消息队列函数xQueueGenericSend实现的:
#define
xSemaphoreGive( xSemaphore
)
\
xQueueGenericSend( (
QueueHandle_t ) ( xSemaphore ), NULL, semGIVE_BLOCK_TIME,
\
queueSEND_TO_BACK
)
2.
此函数是用于任务代码中调用的,故不可以在中断服务程序中调用此函数,中断服务程序中使用的是xSemaphoreGiveFromISR。
3.
使用此函数前,一定要保证用函数xSemaphoreCreateBinary(),
xSemaphoreCreateMutex() 或者
xSemaphoreCreateCounting()创建了信号量。
4.
此函数不支持使用xSemaphoreCreateRecursiveMutex()创建的信号量。
使用举例:
static
SemaphoreHandle_t xMutex = NULL;
static
void vTaskLED(void *pvParameters)
{
TickType_t xLastWakeTime;
const TickType_t xFrequency = 200;
xLastWakeTime = xTaskGetTickCount();
while(1)
{
xSemaphoreTake(xMutex, portMAX_DELAY);
printf("任务vTaskLED在运行\r\n");
bsp_LedToggle(2);
bsp_LedToggle(3);
xSemaphoreGive(xMutex);
vTaskDelayUntil(&xLastWakeTime, xFrequency);
}
}
23.2.3函数xSemaphoreTake
函数原型:
xSemaphoreTake(
SemaphoreHandle_t xSemaphore,
TickType_t xTicksToWait
);
函数描述:
函数xSemaphoreTake用于在任务代码中获取信号量。
u
第1个参数是信号量句柄。
u
第2个参数是没有信号量可用时,等待信号量可用的最大等待时间,单位系统时钟节拍。
u
返回值,如果创建成功会获取信号量返回pdTRUE,否则返回pdFALSE。
使用这个函数要注意以下问题:
1.
此函数是用于任务代码中调用的,故不可以在中断服务程序中调用此函数,中断服务程序使用的是xSemaphoreTakeFromISR。
2.
如果消息队列为空且第2个参数为0,那么此函数会立即返回。
3.
如果用户将FreeRTOSConfig.h文件中的宏定义INCLUDE_vTaskSuspend配置为1且第2个参数配置为portMAX_DELAY,那么此函数会永久等待直到信号量可用。
使用举例:
static
SemaphoreHandle_t xMutex = NULL;
static
void vTaskLED(void *pvParameters)
{
TickType_t xLastWakeTime;
const TickType_t xFrequency = 200;
xLastWakeTime = xTaskGetTickCount();
while(1)
{
xSemaphoreTake(xMutex, portMAX_DELAY);
printf("任务vTaskLED在运行\r\n");
bsp_LedToggle(2);
bsp_LedToggle(3);
xSemaphoreGive(xMutex);
vTaskDelayUntil(&xLastWakeTime, xFrequency);
}
}
23.3实验例程说明
23.3.1STM32F103开发板实验
配套例子:
V4-323_FreeRTOS实验_互斥信号量
实验目的:
1.
学习FreeRTOS的互斥信号量。
实验内容:
1.
K1按键按下,串口打印任务执行情况(波特率115200,数据位8,奇偶校验位无,停止位1)。
2.
K2键按下,直接发送同步信号给任务vTaskMsgPro。
3.
各个任务实现的功能如下:
vTaskUserIF任务 :按键消息处理。
vTaskLED任务
:实现串口的互斥访问,防止多个任务同时访问造成串口打印乱码。
vTaskMsgPro任务:实现串口的互斥访问,防止多个任务同时访问造成串口打印乱码。
vTaskStart任务
:启动任务,也是最高优先级任务,这里实现按键扫描。
FreeRTOS的配置:
FreeRTOSConfig.h文件中的配置如下:
#if
defined(__ICCARM__) || defined(__CC_ARM) ||
defined(__GNUC__)
#include
extern
volatile uint32_t ulHighFrequencyTimerTicks;
#endif
#define
configUSE_PREEMPTION
1
#define
configUSE_IDLE_HOOK
0
#define
configUSE_TICK_HOOK
0
#define
configCPU_CLOCK_HZ
( ( unsigned long ) 72000000
)
#define
configTICK_RATE_HZ
( ( TickType_t ) 1000 )
#define
configMAX_PRIORITIES
( 5 )
#define
configMINIMAL_STACK_SIZE
( ( unsigned short ) 128
)
#define
configTOTAL_HEAP_SIZE
( ( size_t ) ( 17 * 1024 ) )
#define
configMAX_TASK_NAME_LEN
( 16 )
#define
configUSE_TRACE_FACILITY
1
#define
configUSE_16_BIT_TICKS
0
#define
configIDLE_SHOULD_YIELD
1
#define
configUSE_MUTEXES
1
#define
configGENERATE_RUN_TIME_STATS
1
#define
configUSE_STATS_FORMATTING_FUNCTIONS
1
#define
portCONFIGURE_TIMER_FOR_RUN_TIME_STATS()
(ulHighFrequencyTimerTicks = 0ul)
#define
portGET_RUN_TIME_COUNTER_VALUE()
ulHighFrequencyTimerTicks
//#define
portALT_GET_RUN_TIME_COUNTER_VALUE
1
#define
configUSE_CO_ROUTINES
0
#define
configMAX_CO_ROUTINE_PRIORITIES ( 2 )
#define
INCLUDE_vTaskPrioritySet
1
#define
INCLUDE_uxTaskPriorityGet
1
#define
INCLUDE_vTaskDelete
1
#define
INCLUDE_vTaskCleanUpResources
0
#define
INCLUDE_vTaskSuspend
1
#define
INCLUDE_vTaskDelayUntil
1
#define
INCLUDE_vTaskDelay
1
#ifdef
__NVIC_PRIO_BITS
#define
configPRIO_BITS
__NVIC_PRIO_BITS
#else
#define
configPRIO_BITS
4
#endif
#define
configLIBRARY_LOWEST_INTERRUPT_PRIORITY
0x0f
#define
configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY
0x01
几个重要选项说明:
u
#define
configUSE_PREEMPTION
1
使能抢占式调度器
u
#define
configCPU_CLOCK_HZ
( ( unsigned long ) 72000000
)
系统主频72MHz。
u
#define
configTICK_RATE_HZ
( ( TickType_t ) 1000 )
系统时钟节拍1KHz,即1ms。
u
#define
configMAX_PRIORITIES
( 5 )
定义可供用户使用的最大优先级数,如果这个定义的是5,那么用户可以使用的优先级号是0,1,2,3,4,不包含5,对于这一点,初学者要特别的注意。
u
#define
configTOTAL_HEAP_SIZE
( ( size_t ) ( 17 * 1024 ) )
定义堆大小,FreeRTOS内核,用户动态内存申请,任务栈等都需要用这个空间。
u
#define
configUSE_MUTEXES
1
使能互斥信号量。
u
configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY
0x01
定义受FreeRTOS管理的最高优先级中断。简单的说就是允许用户在这个中断服务程序里面调用FreeRTOS的API的最高优先级。为了进一步说明这个宏定义的的作用,解释如下:
l
使用CM内核的MCU,官方强烈建议将NVIC的优先级分组配置为全抢占式优先级,全部配置为抢占式优先级的好处就是方便管理。
l
对于STM32来说,设置NVIC的优先级分组为4时,NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4)就是全部配置为抢占式优先级。又因为STM32的优先级设置仅使用CM内核8bit中的高4bit,即只能区分2^4
=
16种优先级。因此当优先级分组设置为4的时候可供用户选择抢占式优先级为0到15,共16个优先级,配置为0表示最高优先级,配置为15表示最低优先级,不存在子优先级。
l
这里配置configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY为0x01表示用户可以在抢占式优先级为1到15的中断里面调用FreeRTOS的API函数,抢占式优先级为0的中断里面是不允许调用的。
更多关于这个参数说明请参看第12章。
FreeRTOS任务调试信息(按K1按键,串口打印):

上面截图中打印出来的任务状态字母B,
R, D, S对应如下含义:
#define
tskBLOCKED_CHAR
( 'B' ) 任务阻塞
#define
tskREADY_CHAR
( 'R'
) 任务就绪
#define
tskDELETED_CHAR
( 'D' ) 任务删除
#define
tskSUSPENDED_CHAR ( 'S'
) 任务挂起
程序设计:
u
任务栈大小分配:
vTaskUserIF任务
:2048字节
vTaskLED任务
:2048字节
vTaskMsgPro任务 :2048字节
vTaskStart任务
:2048字节
任务栈空间是在任务创建的时候从FreeRTOSConfig.h文件中定义的heap空间中申请的
#define
configTOTAL_HEAP_SIZE
( ( size_t ) ( 17 * 1024 ) )
u
系统栈大小分配:

u
FreeROTS初始化:
int
main(void)
{
__set_PRIMASK(1);
bsp_Init();
vSetupSysInfoTest();
AppTaskCreate();
AppObjCreate();
vTaskStartScheduler();
while(1);
}
u
硬件外设初始化
硬件外设的初始化是在bsp.c文件实现:
void
bsp_Init(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);
bsp_InitUart();
bsp_InitLed();
bsp_InitKey();
}
u
FreeRTOS任务创建:
static
void AppTaskCreate (void)
{
xTaskCreate(
vTaskTaskUserIF,
"vTaskUserIF",
512,
NULL,
1,
&xHandleTaskUserIF ); /*
任务句柄 */
xTaskCreate(
vTaskLED,
"vTaskLED",
512,
NULL,
2,
&xHandleTaskLED );
xTaskCreate(
vTaskMsgPro,
"vTaskMsgPro",
512,
NULL,
3,
&xHandleTaskMsgPro ); /*
任务句柄 */
xTaskCreate(
vTaskStart,
"vTaskStart",
512,
NULL,
4,
&xHandleTaskStart );
}
u
FreeRTOS互斥信号量创建:
static
void AppObjCreate (void)
{
xMutex = xSemaphoreCreateMutex();
if(xMutex == NULL)
{
}
}
u
四个FreeRTOS任务的实现:
static
void vTaskTaskUserIF(void *pvParameters)
{
uint8_t ucKeyCode;
uint8_t pcWriteBuffer[500];
while(1)
{
ucKeyCode = bsp_GetKey();
if (ucKeyCode != KEY_NONE)
{
switch (ucKeyCode)
{
case KEY_DOWN_K1:
xSemaphoreTake(xMutex,
portMAX_DELAY);
printf("=================================================\r\n");
printf("任务名
任务状态 优先级 剩余栈
任务序号\r\n");
vTaskList((char *)&pcWriteBuffer);
printf("%s\r\n", pcWriteBuffer);
printf("\r\n任务名
运行计数
使用率\r\n");
vTaskGetRunTimeStats((char *)&pcWriteBuffer);
printf("%s\r\n", pcWriteBuffer);
xSemaphoreGive(xMutex);
break;
default:
break;
}
}
vTaskDelay(20);
}
}
static
void vTaskLED(void *pvParameters)
{
TickType_t xLastWakeTime;
const TickType_t xFrequency = 200;
xLastWakeTime = xTaskGetTickCount();
while(1)
{
xSemaphoreTake(xMutex, portMAX_DELAY);
printf("任务vTaskLED在运行\r\n");
bsp_LedToggle(2);
bsp_LedToggle(3);
xSemaphoreGive(xMutex);
vTaskDelayUntil(&xLastWakeTime, xFrequency);
}
}
static
void vTaskMsgPro(void *pvParameters)
{
TickType_t xLastWakeTime;
const TickType_t xFrequency = 300;
xLastWakeTime = xTaskGetTickCount();
while(1)
{
xSemaphoreTake(xMutex, portMAX_DELAY);
printf("任务vTaskMsgPro在运行\r\n");
bsp_LedToggle(1);
bsp_LedToggle(4);
xSemaphoreGive(xMutex);
vTaskDelayUntil(&xLastWakeTime, xFrequency);
}
}
static
void vTaskStart(void *pvParameters)
{
while(1)
{
bsp_KeyScan();
vTaskDelay(10);
}
}