信号量是操作系统用来实现资源管理和任务同步的消息机制。FreeRTOS信号量分为二值信号量、计数信号量、互斥信号量和递归互斥信号量。可以将互斥信号量看成一种特殊的二值信号量,但互斥信号量和二值信号量之间还是有一些区别的。
- 使用目的不同:二值信号量用于同步,可实现任务和任务之间及任务和中断之间的同步。互斥信号量用于互锁,保证在同一时间只有一个任务访问某个资源。
- 操作方法不同:二值信号量在用于同步时,一般是一个任务(或中断)给出信号,另一个任务获取信号。互斥信号量必须在同一个任务中获取并在同一个任务中给出信号。
- 使用场合不同:互斥信号量具有优先级继承机制,而二值信号量没有。互斥信号量不能用在中断服务函数中,而二值信号量可以。
- 创建方法不同:用于创建互斥信号量和用于创建二值信号量的API函数不同,但是获取和给出信号的API函数相同。
1. 二值信号量
二值信号量相当于只有一个队列项的队列,创建二值信号量与创建队列使用的是同一个函数。二值信号量只关心这个特殊的队列状态,要不为空,要不为满,并不关心队列中存放的是什么消息。
二值信号量主要用于同步,可实现任务和任务之间及任务和中断之间的同步。二值信号量用于实现任务和中断之间同步的工作过程如下。
1.1 任务因请求信号量而阻塞
任务通过xSemaphoreTake()函数试图获取信号量,但此时二值信号量无效,任务进入
阻塞态。
1.2 中断服务函数释放信号量
在任务阻塞过程中,有中断发生,在该中断的中断服务函数中用xSemaphoreGiveFromISR()函数释放了信号量,二值信号量变为有效的。
1.3 任务成功获取任务量
任务获取二值信号量成功,任务解除阻塞,开始执行任务处理程序
1.4 任务因请求信号量再次进入阻塞态
通常任务函数都是一个大循环,在任务处理完相关事务后,又会再次调用xSemaphoreTake()函数试图再次获取信号量,但此时二值信号量已无效,任务再次进入阻塞态。
2. 创建二值信号量
FreeRTOS 创建二值信号量可使用两个宏:vSemaphoreCreateBinary()和xSemaphoreCreateBinary()。这两个宏最终被队列创建函数xQueueGenericCreate()替换,使用这两个宏创建二值信号量,需要将宏configSUPPORT_DYNAMIC_ALLOCATION设置为1。
2.1 vSemaphoreCreateBinary()
vSemaphoreCreateBinaryO是旧版本的FreeRTOS 使用的二值信号量创建宏,在
semphr.h文件中定义。
#define vSemaphoreCreateBinary( xSemaphore ) \
{ \
( xSemaphore ) = xQueueGenericCreate( ( UBaseType_t ) 1, semSEMAPHORE_QUEUE_ITEM_LENGTH, queueQUEUE_TYPE_BINARY_SEMAPHORE ); \
if( ( xSemaphore ) != NULL ) \
{ \
( void ) xSemaphoreGive( ( xSemaphore ) ); \
} \
}
该宏有一个参数xSemaphore,用来保存创建成功的二值信号量的句柄。实际用于创建二值信号量的是队列创建函数xQueueGenericCreate(),创建成功后会返回这个二值信号量的句柄且二值信号量默认有效,创建失败则返回NULL。
2.2 xSemaphoreCreateBinary()
xSemaphoreCrcateBinary()是新版本的FreeRTOS 使用的二值信号量创建宏,同样是通过调用队列创建函数xQucueGenericCrcate()来创建二值信号量的,只不过创建成功后并没有释放此二值信号量。
#define xSemaphoreCreateBinary()
xQueueGenericCreate( ( UBaseType_t ) 1,
semSEMAPHORE_QUEUE_ITEM_LENGTH, queueQUEUE_TYPE_BINARY_SEMAPHORE );
xQueueGenericCreate()函数如果成功创建了二值信号量,则返回这个二值信号量的句
柄,创建失败则返回NULL。
3. 释放二值信号量
3.1 xSemaphoreGive()
xSemaphoreGive()是一个宏,用于释放二值信号量,最终实现功能的是队列发送函数
xQueueGenericSend()。
#define xSemaphoreGive( xSemaphore )
xQueueGenericSend( ( QueueHandle_t ) ( xSemaphore ),
NULL,
semGIVE_BLOCK_TIME, queueSEND_TO_BACK )
该宏有一个参数xSemaphore,指明要释放的信号量句柄。该宏不仅可用于释放二值信号量,还可以释放计数信号量和互斥信号量。释放成功则返回pdTREU,释放失败则返回pdFALSE。
3.2 xSemaphoreGiveFromlSR()
xSemaphoreGiveFromISR()是释放信号量的宏的中断版本,在中断服务函数中使用,同样也是一个宏。该宏可用于释放二值信号量和计数信号量,但不能用于释放互斥信号量最终实现功能的是xQueueGiveFromISR()函数。
#define xSemaphoreGiveFromISR( xSemaphore, pxHigherPriorityTaskWoken )
xQueueGiveFromISR( ( QueueHandle_t ) ( xSemaphore ),
( pxHigherPriorityTaskWoken ) )
参数说明如下。
xSemaphore: 信号量句柄。
pxHigherPriorityTaskWoken : 指向一个用于保存调用函数后是否进行任务切换的变量,若执行函数后值为pdTRUE,则要在退出中断服务函数后执行一次任务切换。
返回值:pdTRUE,成功释放信号量;errQUEUE_FULL,在指定阻塞时间内释放信号量失败。
4. 获取信号量
4.1 xSemaphoreTake()
xSemaphoreTake()是一个宏,用于获取二值信号量。该宏还可用于获取计数信号量和互斥信号量。该宏的定义如下。
#define xSemaphoreTake( xSemaphore, xBlockTime )
xQueueSemaphoreTake( ( xSemaphore ),
( xBlockTime ) )
参数说明如下。
xSemaphore: 信号量句柄。
xBlockTime : 任务阻塞超时时间,当信号量无效时,若该值为0则函数立即返回,若该值为portMAXDELAY 则任务将无限期阻塞,但要将宏INCLUDE_vTaskSuspend设置为1。
其他值为任务阻塞的系统时钟节拍。
返回值:pdTRUE,成功获取信号量;pdFALSE,在指定阻塞时间内获取信号量失败。
4.2 xSemaphoreTakeFromISR()
xSemaphoreTakeFromISR()是获取信号量的宏的中断版本,在中断服务函数中使用,同样也是一个宏。该宏可以用来获取二值信号量和计数信号量,但不能用于获取互斥信号量。最终实现功能的是xQueueReceiveFromISR()函数。
#define xSemaphoreTakeFromISR( xSemaphore, pxHigherPriorityTaskWoken )
xQueueReceiveFromISR( ( QueueHandle_t ) ( xSemaphore ),
NULL,
( pxHigherPriorityTaskWoken ) )
参数说明如下。
xSemaphore: 信号量句柄。
pxHigherPriorityTaskWoken : 指向一个用于保存调用函数后是否进行任务切换的变量,若执行函数后值为pdTRUE,则要在退出中断服务函数后执行一次任务切换。
返回值:pdTRUE,成功获取信号量;pdFALSE,在指定阻塞时间内获取信号量失败。
5. 用二值信号量实现任务同步
本示例通过对二值信号量的操作,实现任务与任务之间及任务与中断之间的同步。本
示例通过appStartTaskO函数创建4个FreeRTOS任务。
- 任务1的任务函数为Led0Task(),其功能是使LEDO闪烁,优先级为3,通过二值信号量实现任务与任务之间的同步,输出信息到串口。
- 任务2的任务函数为Led1Task(),其功能是使LED1闪烁,优先级为3,通过二值信号量实现任务与中断之间的同步,输出信息到串口。
- 任务3是串口守护任务,任务函数为printTask(),优先级为3,其功能是将通过队列传送过来的字符信息在串口上输出,任何时候只有该守护任务能访问串口。
- 任务4是按键扫描任务,任务函数为keyTask(),优先级为4,其功能是按键扫描,并根据返回的键值执行释放二值信号量、启动定时器等操作。本示例中使用到3个按键,Key1按键用于释放任务与任务之间同步的二值信号量,Key2用于启动TIM2,利用TIM2的更新中断释放任务与中断之间同步的二值信号量,Key3用于停止TIM2。
5.1 配置TIM2
使用定时器TIM2,利用它的更新中断在中断服务函数中释放二值信号量,实现任务
与中断之间的同步。在库函数环境配置TIM2,使能TIM2全局中断,设置中断抢占优先级为6(系统配置FreeRTOS 能管理的最高中断优先级为5,TIM2的中断抢占优先不能高于5),溢出时间为10ms。
#include "stm32f10x.h" // Device header
#include "Timer.h"
#include "appTask.h"
#include "Serial.h"
extern volatile uint8_t uIRQCounter; //统计中断次数
extern SemaphoreHandle_t binIRQSemaphore; //任务与中断之间同步二值信号量句柄
BaseType_t xHighPriorityWaskWoken = pdFALSE ; //退出中断后是否进行任务切换
void Timer_Init()
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);
TIM_InternalClockConfig(TIM2);//开启内部时钟
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision=TIM_CKD_DIV1;
TIM_TimeBaseInitStructure.TIM_CounterMode=TIM_CounterMode_Up;
TIM_TimeBaseInitStructure.TIM_Period=100 -1 ;//ARR
TIM_TimeBaseInitStructure.TIM_Prescaler=7200 -1 ;//PSC
//定时1秒 也是1HZ 72M/PSC/ARR =1HZ
TIM_TimeBaseInitStructure.TIM_RepetitionCounter=0;
TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitStructure);
TIM_ITConfig(TIM2,TIM_IT_Update,ENABLE);//中断使能 更新中断
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);//清除更新中断请求位
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel=TIM2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=6;
NVIC_InitStructure.NVIC_IRQChannelSubPriority=1;
NVIC_Init(&NVIC_InitStructure);
}
void TIM2_IRQHandler(void)
{
if(TIM_GetFlagStatus(TIM2,TIM_FLAG_Update)==SET)
{
uIRQCounter++; //中断次数
xSemaphoreGiveFromISR(binIRQSemaphore,&xHighPriorityWaskWoken);
portYIELD_FROM_ISR(xHighPriorityWaskWoken);
TIM_ClearFlag(TIM2,TIM_FLAG_Update);
}
}
5.2 任务函数
static char pcToPrint[80]; //待打印内容缓冲区
xQueueHandle xQueuePrint; //消息队列句柄
volatile uint8_t uIRQCounter; //用于统计中断次数
SemaphoreHandle_t binKeySemaphore; //任务与任务之间同步二值信号量句柄
SemaphoreHandle_t binIRQSemaphore; //任务与中断之间同步二值信号量句柄
/**********************************************************************
函 数 名:Led0Task
功 能:使LED0闪烁,通过二值信号量实现任务与任务之间的同步,输出信息到串口
形 参:pvParameters 是在创建该任务时传递的参数
返 回 值:无
优 先 级: 3
**********************************************************************/
static void Led0Task(void *pvParameters)
{
while(1)
{
GPIO_WriteBit(GPIOA,GPIO_Pin_4,(BitAction)(1 - GPIO_ReadOutputDataBit(GPIOA,GPIO_Pin_4))); //LED0闪烁
vTaskDelay(pdMS_TO_TICKS(500));//延时500ms
if(xSemaphoreTake(binKeySemaphore,10) == pdTRUE)
{
//生成待打印出信息
sprintf(pcToPrint,"按键任务通过二值信号量同步任务 1\r\n\r\n");
//打印信息,所有发送串口的信息不能直接输出,通过队列发送给串口守护任务
xQueueSendToBack(xQueuePrint,pcToPrint,0);
}
}
}
/**********************************************************************
函 数 名:Led1Task
功 能:使LED1闪烁,通过二值信号量实现任务与中断之间的同步,输出信息到串口
形 参:pvParameters 是在创建该任务时传递的参数
返 回 值:无
优 先 级: 3
**********************************************************************/
static void Led1Task(void *pvParameters)
{
while(1)
{
GPIO_WriteBit(GPIOA,GPIO_Pin_5,(BitAction)(1 - GPIO_ReadOutputDataBit(GPIOA,GPIO_Pin_5))); //LED1闪烁
vTaskDelay(pdMS_TO_TICKS(500));//延时500ms
//若获取二值信号量成功,则执行一些操作,本示例发送相应信息到串口
if(xSemaphoreTake(binIRQSemaphore,10) == pdTRUE)
{
//生成待打印出信息
sprintf(pcToPrint,"Tim2中断同步任务2,中断 %3d \r\n\r\n",uIRQCounter);
//打印信息,所有发送串口的信息不能直接输出,通过队列发送给串口守护任务
xQueueSendToBack(xQueuePrint,pcToPrint,0);
}
}
}
/**********************************************************************
函 数 名:printTask
功能说明:串口守护任务使用了一个FreeRTOS队列来对串口实现串行化访问,该守护任务是唯一能够直接访问串口的任务。
串口守护任务大部分时间都在阻塞态等待队列中有消息到来,当一个消息到达时,
串口守护任务仅简单地将接收到的消息发送到串口上,然后又返回阻塞态,继续等待下一条消息的到来。
形 参:pvParameters 是在创建该任务时传递的参数
返 回 值:无
优 先 级: 3
**********************************************************************/
void printTask(void *pvParameters)
{
char pcTowrite[80]; //缓存从队列接受到的数据
while(1)
{
/*当队列为空,即没有字符需要输出时间,阻塞超时时间为portMAX_DELAY,任务将进入无期限等待
状态,可以不检测队列读取函数的返回值*/
xQueueReceive(xQueuePrint,pcTowrite,portMAX_DELAY);
printf("%s",pcTowrite);
}
}
/**********************************************************************
函 数 名:keyTask
功能说明:按键扫描任务,根据键值执行相应的操作
形 参:pvParameters 是在创建该任务时传递的参数
返 回 值:无
优 先 级: 4
**********************************************************************/
static void keyTask(void *pvParameters)
{
uint8_t keyValue = 0; //存储键值
while(1)
{
keyValue =Key_getNum();
if(keyValue == 1)
{
sprintf(pcToPrint,"Key1按键按下,发送同步信号量 .........\r\n\r\n");
xQueueSendToBack(xQueuePrint,pcToPrint,0);
//发送二值信号量给任务1进行同步
xSemaphoreGive(binKeySemaphore);
}else if(keyValue == 2)
{
//启动TIM2定时器
TIM_Cmd(TIM2,ENABLE);
sprintf(pcToPrint,"Key2按键按下,启动TIM2更新中断 .........\r\n\r\n");
xQueueSendToBack(xQueuePrint,pcToPrint,0);
//在TIM2中断中,通过发送二值信号量给任务2进行同步
}else if(keyValue == 3)
{
//关闭TIM2定时器
TIM_Cmd(TIM2,DISABLE);
xSemaphoreTake(binIRQSemaphore,10); //在使用计数信号量时注释掉
sprintf(pcToPrint,"Key3按键按下,关闭TIM2 .........\r\n\r\n");
xQueueSendToBack(xQueuePrint,pcToPrint,0);
}
vTaskDelay(pdMS_TO_TICKS(100));
}
}
5.3 信号量及任务创建
static TaskHandle_t Led0TaskHandle = NULL;//任务LED0任务句柄
static TaskHandle_t Led1TaskHandle = NULL;//任务LED1任务句柄
static TaskHandle_t printTaskHandle = NULL;//任务printTask任务句柄
static TaskHandle_t keyTaskHandle = NULL;//按键扫描任务句柄
/**********************************************************************
函 数 名:appStartTask
功能说明:任务开始函数,用于创建其他函数并且开启调度器
形 参:pvParameters 是在创建该任务时传递的参数
返 回 值:无
**********************************************************************/
void appStartTask(void)
{
/*创建一个长度为2,队列项大小足够容纳待输出字符的队列*/
xQueuePrint = xQueueCreate(2,sizeof(pcToPrint));
/*创造2个二值信号量,一个实现任务与任务之间的同步,一个用于实现任务与中断之间的同步*/
binIRQSemaphore = xSemaphoreCreateBinary();
binKeySemaphore = xSemaphoreCreateBinary();
if(xQueuePrint && binKeySemaphore && binIRQSemaphore)//如果队列信号量创建成功
{
taskENTER_CRITICAL(); /*进入临界段,关中断*/
xTaskCreate(Led0Task,"Led0Task",128,NULL,3,&Led0TaskHandle);
xTaskCreate(Led1Task,"Led1Task",128,NULL,3,&Led1TaskHandle);
xTaskCreate(printTask,"printTask",128,NULL,3,&printTaskHandle);
xTaskCreate(keyTask,"keyTask",128,NULL,4,&keyTaskHandle);
taskEXIT_CRITICAL(); /*退出临界段,关中断*/
vTaskStartScheduler();/*开启调度器*/
}
}
5.4 下载测试
编译无误下载后,可以看到LED2个灯在闪烁,按Key1,串口打印输出任务同步信息;按Key2按键,启动Tim2,Tim2的更新中断发送二值信号量同步任务2(间隔100ms),串口打印任务与中断的同步信息;按下Key3按键关闭Tim2,停止任务与中断的同步。