FreeRTOS信号量
信号量是操作系统总重要的一部分,信号量一般用来进行资源管理和任务同步,FreeRTOS中信号量又分为二值信号量、计数型信号量、互斥信号量和递归互斥信号量。不同的信号量其应用场景不同,但是有些场景是可以互换着使用的。
信号量简介
信号量常常用于控制对共享资源的访问和任务同步。举一个很常见的例子,某个停车场有100个停车位,这100个停车位大家都可以使用,对于大家说这100个停车位就是共享资源。假设现在这个停车场正常运行,你要把车停到这个停车场肯定要先看一下现在停了多少车了?还有没有停车位?当前停车数量就是一个信号量,具体的停车数量就是这个信号量值。当这个值到100的时候,说明停车场满了。停车场满的时候你可以等一会儿看看有没有其他的车开出停车场,当有车看出停车场的时候,停车数量就会减一,也就是说信号量减一,此时你就可以把车停进去了,你把车停进去以后,停车数量就会加一,也就是信号量加一。这就是一个典型的使用信号量进行共享资源管理的案例。在这个案例中,使用的就是计数型信号量。在看另一个案例:使用公共电话。我们知道一次只能一个人使用电话,这个时候,公共电话只可能有两个状态:使用或未使用,如果用电话的这两个状态作为信号量的话,那么这个就是二值信号量。
信号量用于控制共享资源访问的场景相当于一个上锁机制,代码只有获得了这个锁的钥匙才能够执行。
上面我们讲了信号量在共享资源访问中的使用,信号量的另一个重要的应用场合就是任务同步,用于任务与任务或中断与任务之间的同步。在执行中断服务函数的时候就可以通过向任务发送信号量来通知任务它所期待的事件发生了,当退出中断服务函数以后,在任务调度器的调度下同步的任务就会执行。在编写中断服务函数的时候,我们都知道一定要快进快出,中断服务函数里面不能有太多的代码,否则的话会影响中断的实时性。裸机编写中断服务函数的时候一般都只是在中断服务函数中打个标记,然后在其他的地方根据标记来做具体的处理过程。在使用RTOS系统的时候,我们就可以借助信号量完成此功能,当中断发生的时候就释放信号量,如果获取到信号量就说明中断发生了,那么开始完成相应的处理,这样做的好处就是中断执行时间非常短。这个例子就是中断与任务之间使用信号量来完成同步,当然,任务与任务之间也可以使用信号量来完成同步。
FreeRTOS中还有一些其他特殊类型的信号量,比如互斥信号量和递归互斥信号量。
二值信号量
二值信号量简介
二值信号量通常用于互斥访问或同步,二值信号量与互斥信号量非常类似,但是还是有一些细微的差别,互斥信号量拥有优先级继承机制,二值信号量没有优先级继承。因此二值信号量更适合用于同步(任务与任务或任务与中断的同步),而互斥信号量适合用于简单的互斥访问。
和队列一样,信号量API函数允许设置一个阻塞时间,阻塞时间是当任务获取信号量的时候,由于信号量无效而导致任务进入阻塞态的最大时钟节拍数。如果多个任务同时阻塞在同一个信号量上的话,那么优先级最高的那个任务优先获得信号量,这样当当信号量有效的时候,高优先级的任务就会解除阻塞状态。
二值信号量其实就是一个只有一个队列项的队列,这个特殊的队列要么是满的,要么是空的,这正要就是二值。任务和中断使用这个特殊队列不同在乎队列中存的是什么消息,只需要知道这个队列是满的还是空的。可以利用这个机制来完成任务与中断之间的同步。
在使用应用中通常会使用一个任务来处理MCU的某个外设,比如网络应用中,一般最简单的方法就是使用一个任务去轮询地查询MCU的ETH(网络相关外设,如STM32的以太网MAC)外设是否有数据,当有数据的时候就处理这个网络数据。这样使用轮询的方式是很浪费CPU资源的,而且也阻止了其他任务的运行。最理想的方法就是当没有网络数据的时候,网络任务就进入阻塞态,把CPU让给其他的任务,当有数据的时候网络任务才去执行。现在使用二值信号量就可以实现这样的功能,任务通过获取信号量来判断是否有网络数据,没有的话就进入阻塞态,而网络中断服务函数(大多数的网络外设都与中断功能,比如STM32的MAC专用DMA中断,通过中断可以判断是否接收到数据)通过释放信号量来通知任务以太网外设接收到了网络数据,网络任务可以去提取处理了。网络任务只是在一直获取二值信号量,它不会释放信号量,而中断服务函数是一直在释放信号量,它不会获取信号量。在中断服务函数中发送信号量可以使用函数 xSemaphoreGiveFromISR() ,也可以使用任务通知功能来替他二值信号量,而且使用任务通知的话,速度更块,代码量更少。
使用二值信号量来完成中断与任务同步的这个机制中,任务优先级确保了外设能够得到及时的处理,这样做相当于推迟了中断处理过程。也可以使用队列替代二值信号量,在外设事件的中断服务函数中获取相关数据,并将相关数据通过队列发送给任务。如果队列无效的话,任务就进入阻塞态,直到队列中有数据,任务接收到数据以后就开始相关的处理过程。
下面几个步骤演示了二值信号量的工作过程。
1. 二值信号量无效
在上图中任务Task通过函数 xSemaphoreTake() 获取信号量,但是此时二值信号量无效,所以任务Task进入阻塞态。
2. 中断释放信号量
此时中断发生了,在中断服务函数中通过函数 xSemaphoreGiveFromISR() 释放信号量,因此信号量变为有效。
3. 任务获取信号量成功
由于信号量已经有效了,所以任务Task获取信号量成功,任务从阻塞态解除,开始执行相关处理过程。
4. 任务再次进入阻塞态
由于任务函数一般都是一个大循环,所以在任务做完相关的处理以后就会再次调用 xSemaphoreTask() 获取信号量。在执行完第三步以后,二值信号量就已经变为无效了,所以任务将再次进入阻塞态,和第一步一样,直到中断再次发生并且调用函数 xSemaphoreGiveFromISR() 释放信号量。
创建二值信号量
和队列一样,想要使用二值信号量就必须先创建二值信号量,二值信号量创建函数如下表:
函数 | 描述 |
vSemaphoreCreateBinary() | 动态创建二值信号量,这个是老版本FreeRTOS中使用的创建二值信号量的API函数。 |
xSemaphoreCreateBinary() | 动态创建二值信号量,新版FreeRTOS使用此函数创建二值信号量。 |
xSemaphoreCreateBinaryStatic() | 静态创建二值信号量。 |
1. 函数 vSemaphoreCreateBinary()
此函数是老版本FreeRTOS中创建二值信号量函数,新版本已经不再使用了,新版本的FreeRTOS使用 xSemaphoreCreateBinary() 来替代此函数,这里还保留这个函数是为了兼容哪些基于老版本FreeRTOS而做的应用层代码。此函数是个宏,具体创建过程是由函数 xQueueGenericCreate()
来完成的,在文件 semphr.h 中有如下定义:
void vSemaphoreCreateBinary( SemaphoreHandle_t xSemaphore )
参数:
xSemaphrore:保存创建成功的二值信号量句柄。
返回值:
NULL:二值信号量创建失败。
其他值:二值信号量创建成功。
2. 函数xSemaphoreCreateBinary()
此函数是vSemaphoreCreateBinary()的新版本,新版本中FreeRTOS中统一用此函数来创建二值信号量。使用此函数创建二值信号量的话,信号量所需RAM是由FreeRTOS的内存管理部分来动态分配的。次函数创建好的二值信号量默认是空的,也就是说刚刚创建好的二值信号量量使用函数xSemephoreTask()是获取不到的。vSemaphoreCreateBinary()也是个宏,具体创建过程是由函数xQueueGenericCreate()来完成的,其函数原型如下:
SemaphoreHandle_t xSemaphoreCreateBinary( void )
参数:
无。
返回值:
NULL:二值信号量创建失败。
其他值:创建成功的二值信号量的句柄。
3. 函数 xSemephroeCreateBinaryStatic()
此函数也是创建二值信号量的,只不过使用次函数创建二值信号量的话信号量所需要的RAM需要由用户来分配,此函数是个宏,具体创建过程是通过函数xQueueGenericCreateStatic()来完成的,函数原型如下:
SemaphoreHandle_t xSemaphoreCreateBinaryStatic( StaticSemaphore_t *pxSemaphoreBuffer )
参数:
pxSemaphoreBuffer:此参数指向一个StaticSemaphore_t类型的变量,用来保存信号量结构体。
返回值:
NULL:二值信号量创建失败。
其他值:创建成功的二值信号量句柄。
二值信号量创建过程分析
上一小节讲了三个用于二值信号量创建的函数,两个动态的创建函数和一个静态的创建函数。本节就来分析一下这两个动态的创建函数,静态创建函数和动态类似,就不做分析了。首先看一下老版本的二值信号量动态创建函数 vSemaphoreCreateBinary(),函数代码如下:
1 #if( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) 2 #define vSemaphoreCreateBinary( xSemaphore ) 3 { 4 ( xSemaphore ) = xQueueGenericCreate( ( UBaseType_t ) 1, 5 semSEMAPHORE_QUEUE_ITEM_LENGTH, 6 queueQUEUE_TYPE_BINARY_SEMAPHORE ); 7 if( ( xSemaphore ) != NULL ) 8 { 9 ( void ) xSemaphoreGive( ( xSemaphore ) ); 10 } 11 } 12 #endif
第4~6行:上面说了二值信号量是在队列的基础上实现的,所以创建二值信号量就是创建队列的过程。这里使用函数xQueueGenericCreate()创建了一个队列,队列长度为1,队列项长度为0,队列类型为queueQUEUE_TYPE_BINARY_SEMAPHORE,也就是二值信号量。
第9行:当二值信号量创建成功以后立即调用函数 xSemaphoreGive()释放二值信号量,此时新创建的二值信号量有效。
再来看一下新版本的二值信号量创建函数 xSemaphoreCreateBianry(),函数代码如下:
1 #if( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) 2 #define xSemaphoreCreateBinary() 3 xQueueGenericCreate( ( UBaseType_t ) 1, 4 semSEMAPHORE_QUEUE_ITEM_LENGTH, 5 queueQUEUE_TYPE_BINARY_SEMAPHORE ) 6 #endif
可以看出新版本的二值信号量创建函数也是使用函数 xQueueGenericCreate()来创建一个类型为queueQUEUE_TYPE_BINARY_SEMAPHORE、长度为1、队列项长度为0的队列。这一步和老版本的二值信号量创建函数一样,唯一不同的就是新版本的函数在成功创建二值信号量以后不会立即调用函数xSemephoreGive()释放二值信号量。也就是说新版函数创建的二值信号量默认是无效的,而老版本是有效的。
大家注意看,创建的队列是个没有存储区的队列,前面说了使用队列是否为空来表示二值信号量,而队列是否为空可以通过队列结构体的成员变量uxMessageWaiting来判断。
释放信号量
释放信号量的函数有两个:
函数 | 描述 |
xSemaphoreGive() | 任务级信号量释放函数 |
xSemaphoreGiveFromISR() | 中断级信号量释放函数 |
同队列一样,释放信号量也分为任务级和中断级。还有,不管是二值信号量、计数型信号量还是互斥信号量,它们都使用上表中的函数释放信号量,递归互斥信号量有专用的释放函数。
1. 函数 xSemaphoreGive ()
此函数用于释放二值信号量、计数型信号量或互斥信号量,此函数是一个宏,真正释放信号量的过程由函数 xQueueGenericSend()来完成。函数原型如下:
BaseType_t xSemaphoreGive( SemaphoreHandle_t xSemaphore )
参数:
xSemaphore:要释放的信号量句柄。
返回值:
pdPASS:释放信号量成功。
errQUEUE_FULL:释放信号量失败。
我们在来看一下函数 xSemaphoreGive()的具体内容,此函数在文件semphr.h中有如下定义:
1 #define xSemaphoreGive( xSemaphore ) 2 xQueueGenericSend( ( QueueHandle_t ) ( xSemaphore ), 3 NULL, 4 semGIVE_BLOCK_TIME, 5 queueSEND_TO_BACK )
可以看出任务级释放信号量就是想队列发送消息的过程,只是这里并没有发送具体的消息,阻塞时间为0(宏 semGIVE_BLOCK_TIME为0),入队方式采用后向入队。入队的时候队列结构体成员变量uxMessageWaiting会加一,对于二值信号量通过判断uxMessageWaiting就可以知道信号量是否有效了。uxMessageWaiting为1的话,说明二值信号量有效,为0就无效。如果队列满的话就返回错误值errQUEUE_FULL,提示队列满,入队失败。
2. 函数 xSemaphoreGiveFromISR()
此函数用于在中断中释放信号量,此函数智能用来释放二值信号量和计数型信号量,绝对不能用来在中断服务函数中释放互斥信号量!此函数是一个宏,真正执行的是函数 xQueueGiveGromISR(),此函数原型如下:
BaseType_t xSemaphoreGiveFromISR( SemaphoreHandle_t xSemaphore, BaseType_t *pxHigherPriorityTaskWoken )
参数:
xSemaphore:要释放的信号量句柄。
pxHigherPriorityTaskWoken:标记退出次函数以后是否进行任务切换,这个变量的值是由这个函数来设置的,用户不能进行设置,用于只需要提供一个变量来保存这个值就行了。当此值为pdTRUE的时候,在退出中断服务函数之前一定要进行一次任务切换。
返回值:
pdPASS:释放信号量成功。
errQUEUE_FULL:释放信号量失败。
在中断中释放信号量真正使用的事函数xQueueGiveFromISR(),此函数和中断级通用入队函数xQueueGenericSendFromISR()及其类似!只是针对信号量做了微小的改动。函数xSemaphoreGiveFromISR()不能用于在中断中释放互斥信号量,以为互斥信号量涉及到优先级继承的问题,而中断不属于任务,没法处理中断优先级继承。
获取信号量
获取信号量也有两个函数:
函数 | 描述 |
xSemaphoreTake() | 任务级获取信号量函数 |
xSemaphoreTakeFromISR() | 中断级获取信号量函数 |
同释放信号量的API一样,不管是二值信号量、计数型信号量还是互斥信号量,它们都使用上表中的函数获取信号量。
1. 函数 xSemaphoreTake()
此函数用于获取二值信号量、计数型信号量或互斥信号量。此函数是一个宏,真正获取信号量的过程由函数xQueueGenericReceive()来完成。函数原型如下:
1 BaseType_t xSemaphoreTake( 2 SemaphoreHandle_t xSemaphore, 3 TickType_t xBlockTime 4 )
参数:
xSemaphore:要获取的信号量句柄。
xBlockTime:阻塞时间
返回值:
pdTRUE:获取信号量成功。
pdFALSE:超时,获取信号量失败。
在来看一下函数xSemaphoreTake()的具体内容,此函数在文件semphr.h中有如下定义:
#define xSemaphoreTake( xSemaphore, xBlockTime ) xQueueGenericReceive( ( QueueHandle_t ) ( xSemaphore ), NULL, ( xBlockTime ), pdFALSE )
获取信号量的过程其实就是读取队列的过程,只是这里并不是为了读取队列中的消息。xQueueGenericReceive(),如果队列为空并且阻塞时间为0的话就立即返回errQUEUE_EMPTY,表示队列空。如果队列为空并且阻塞时间不为0的话就将任务添加到延时列表中。如果队列不为空的话就从队列中读取数据(获取信号量不执行这一步),数据读取完成以后还需要将队列结构体变量uxMessageWaiting减一,然后解除某些因为入队而阻塞的任务,最后返回pdPASS表示出队成功。互斥信号量涉及到优先级继承,处理方式不同。
2. 函数 xSemaphoreTaskFromISR()
此函数用于在中断服务函数中获取信号量,此函数用于获取二值信号量和计数型信号量,绝对不能使用此函数来获取互斥信号量!此函数是一个宏,真正执行的是函数xQueueReceiveFromISR(),此函数原型如下:
BaseType_t xSemaphoreGiveFromISR( SemaphoreHandle_t xSemaphore, BaseType_t *pxHigherPriorityTaskWoken )
参数:
xSemaphore:要获取的信号量句柄。
pxHigherPriorityTaskWoken:标记退出此函数以后是否进行任务切换,这个变量的值由这个函数来设置,用户不进行设置,用户只需要提供一个变量来保存这个值就行了。当此值为判断TRUE的时候在退出中断服务函数之前一定要进行一次任务切换。
返回值:
pdPASS:获取信号量成功。
pdFALSE:获取信号量失败。
在中断中获取信号量真正使用的是函数xQueueReceiveFromISR(),这个函数就是中断级出队函数。当队列不为空的时候就拷贝队列中的数据(用于信号量的时候不需要这一步),然后将队列结构体中的成员变量uxMessageWaiting减一,如果有任务因为入队而阻塞的话就解除阻塞态,当解除阻塞的任务拥有更高优先级的话就将参数pxHigherPriorityTaskWoken设置为pdTRUE,最后返回pdPASS表示出队成功。如果队列为空的话就直接返回pdFAIL表示出队失败。
二值信号量操作实验
二值信号量的使用就是同步,完成任务与任务或中断与任务之间的同步。大多数情况下都是中断与任务之间的同步。本实验室学习使用二值信号量来完成中断与任务之间的同步。任务与任务之间的同步。
本实验设计三个任务:start_task、task1_task、DataProcess_task。
start_task:用来创建其他2个任务。
task1_task:获取按键键值,释放信号量。用于任务与任务之间同步
DataProcess_task:
实验中还创建了一个二值信号量BinarySemaphore,用于完成串口中断和任务DataProcess_task之间的同步。
相关代码:
#define START_TASK_PRIO 1 // 任务优先级 #define START_STK_SIZE 128 // 任务堆栈大小 TaskHandle_t StartTask_Handler; // 任务句柄 void start_task(void *pvParameters); // 任务函数 #define LED0_TASK_PRIO 2 // 任务优先级 #define LED0_STK_SIZE 50 // 任务堆栈大小 TaskHandle_t LED0Task_Handler; // 任务句柄 void led0_task(void *pvParameters); // 任务函数 #define DATAPROCESS_TASK_PRIO 3 // 任务优先级 #define DATAPROCESS_STK_SIZE 50 // 任务堆栈大小 TaskHandle_t DataProcessTask_Handler; // 任务句柄 void DataProcess_task(void *pvParameters); // 任务函数 SemaphoreHandle_t BinarySemaphore = NULL; // 二值信号量句柄
main函数
int main(void) { NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);//设置系统中断优先级分组4 delay_init(); //延时函数初始化 uart_init(115200); //初始化串口 LED_Init(); //初始化LED KEY_Init(); // 初始化按键 //创建开始任务 xTaskCreate((TaskFunction_t )start_task, //任务函数 (const char* )"start_task", //任务名称 (uint16_t )START_STK_SIZE, //任务堆栈大小 (void* )NULL, //传递给任务函数的参数 (UBaseType_t )START_TASK_PRIO, //任务优先级 (TaskHandle_t* )&StartTask_Handler); //任务句柄 vTaskStartScheduler(); //开启任务调度 }
任务函数
//开始任务任务函数 void start_task(void *pvParameters) { taskENTER_CRITICAL(); //进入临界区 // 创建二值信号量 BinarySemaphore = xSemaphoreCreateBinary(); if(BinarySemaphore == NULL) { printf("Binary Sem Create Failed! "); } //创建LED0任务 xTaskCreate((TaskFunction_t )led0_task, (const char* )"led0_task", (uint16_t )LED0_STK_SIZE, (void* )NULL, (UBaseType_t )LED0_TASK_PRIO, (TaskHandle_t* )&LED0Task_Handler); //创建DataProcess任务 xTaskCreate((TaskFunction_t )DataProcess_task, (const char* )"DataProcess_task", (uint16_t )DATAPROCESS_STK_SIZE, (void* )NULL, (UBaseType_t )DATAPROCESS_TASK_PRIO, (TaskHandle_t* )&DataProcessTask_Handler); vTaskDelete(StartTask_Handler); //删除开始任务 taskEXIT_CRITICAL(); //退出临界区 } //LED0任务函数 void led0_task(void *pvParameters) { u8 key=0; while(1) { key = KEY_Scan(0); if((BinarySemaphore!=NULL) && (key==KEY1_PRES)) { xSemaphoreGive( BinarySemaphore ); // 发送信号量 任务与任务同步 } vTaskDelay(10); } } //DataProcess任务函数 void DataProcess_task(void *pvParameters) { u8 count = 0; BaseType_t xHigherPriorityTaskWoken; BaseType_t err; while(1) { count ++; if(BinarySemaphore!=NULL) { // err = xSemaphoreTake( BinarySemaphore, 1000 ); // 一直等待 任务与任务同步 // if(err == pdTRUE) // { // printf("KEY1_PRESS! count:%d ",count); // } err = xSemaphoreTakeFromISR( BinarySemaphore, &xHigherPriorityTaskWoken ); // 中断与任务同步 if(err == pdTRUE) { printf("recv: %s ",USART_RX_BUF); memset(USART_RX_BUF,0,USART_REC_LEN); USART_RX_STA = 0; }else { vTaskDelay(10); } } } }
中断服务函数:
extern SemaphoreHandle_t BinarySemaphore ; // 二值信号量句柄 void USART1_IRQHandler(void) //串口1中断服务程序 { u8 Res; BaseType_t xHigherPriorityTaskWoken = pdFALSE; if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) //接收中断(接收到的数据必须是0x0d 0x0a结尾) { Res =USART_ReceiveData(USART1); //读取接收到的数据 if((USART_RX_STA&0x8000)==0)//接收未完成 { if(USART_RX_STA&0x4000)//接收到了0x0d { if(Res!=0x0a)USART_RX_STA=0;//接收错误,重新开始 else USART_RX_STA|=0x8000; //接收完成了 } else //还没收到0X0D { if(Res==0x0d)USART_RX_STA|=0x4000; else { USART_RX_BUF[USART_RX_STA&0X3FFF]=Res ; USART_RX_STA++; if(USART_RX_STA>(USART_REC_LEN-1))USART_RX_STA=0;//接收数据错误,重新开始接收 } } } } if((BinarySemaphore!=NULL) && (USART_RX_STA&0x8000)) { xSemaphoreGiveFromISR( BinarySemaphore, &xHigherPriorityTaskWoken ); // 发送中断二值信号量 中断与任务同步 portYIELD_FROM_ISR(xHigherPriorityTaskWoken); } }
注意:串口中断初始化优先级设置,必须在FreeRTOS能管理的优先级范围内5~15。
计数型信号量
计数型信号量简介
有些资料中也将计数型信号量叫做数值信号量,二值信号量相当于长度为1的队列,那么计数型信号量就是长度大于1的队列。同二值信号量一样,用户不需要关系队列中存储了什么数据,只需要关系队列是否为空即可。计数型信号量通常用于如下两个场合:
1. 事件计数
在这个场合中,每次事件发生的时候就在事件处理函数中释放信号量(增加信号量计数值),其他任务会获取信号量(信号量计数值减一,信号量值就是队列结构体成员变量uxMessageWaiting)来处理事件。在这种场合中创建的计数型信号量的初始值为0。
2. 资源管理
在这个场合中,信号量的值代表当前资源的可用数量,比如停车场当前剩余的停车位数量。一个任务要想获取资源的使用权,首先必须获取信号量,信号量获取成功以后信号量值就会减一。当信号量值为0的时候说明没有资源了。当一个任务使用完资源以后一定要释放信号量,释放信号量以后信号量就会加一。在这个场合中创建的计数型信号量初始值应该是资源的数量,比如停车场移动有100个停车位,那么创建信号量的时候信号量值就应该是资源的数量。比如停车场一共有100个停车位,那么创建信号量的时候信号量值就应该初始化为100。
创建计数型信号量
FreeRTOS提供了两个计数型信号量创建函数:
函数 | 描述 |
xSemaphoreCreateCounting() | 使用动态方法创建计数型信号量 |
xSemaphoreCreateCountingStatic() | 使用静态方法创建计数型信号量 |
1. 函数 xSemaphoreCreateCounting()
此函数用于创建一个计数型信号量,所需要的内存通过动态内存管理方法分配。此函数本质是一个宏,
真正完成信号量创建的函数是 xQueueCreateCountingSemaphore(),此函数原型如下:
SemaphoreHandle_t xSemaphoreCreateCounting( UBaseType_t uxMaxCount,
UBaseType_t uxInitialCount )
参数:
uxMaxCount:计数信号量最大计数值,当信号量值等于此值得时候释放信号量就会失败。
uxInitialCount:计数信号量初始值。
返回值:
NULL:计数型信号量创建失败。
其他值:计数型信号量创建成功,返回计数型信号量句柄。
2. 函数 xSemaphoreCreateCountingStatic()
此函数也是用来创建计数型信号量的,使用此函数创建计数型信号量的时候所需要的内存由用户分配。此函数也是一个宏,真正执行的是函数是 xQueueCreateCountingSemaphoreStatic(),函数原型如下:
SemaphoreHandle_t xSemaphoreCreateCountingStatic( UBaseType_t uxMaxCount,
UBaseType_t uxInitialCount,
StaticSemaphore_t *pxSemaphoreBuffer )
参数:
uxMaxCount:计数型信号量最大计数值,当信号量值等于此值得时候释放信号量就会失败。
uxInitialCount:计数信号量初始值。
pxSemaphoreBuffer:指向一个StaticSemaphore_t类型的变量,用来保存信号量结构体。
返回值:
NULL:计数型信号量创建失败。
其他值:计数型信号量创建成功,返回计数型信号量句柄。
计数型信号量创建过程分析
这里只分析动态创建计数型信号量函数 xSemaphoreCreateCounting(),此函数是个宏,定义如下:
#if( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) #define xSemaphoreCreateCounting( uxMaxCount, uxInitialCount ) xQueueCreateCountingSemaphore( ( uxMaxCount ), ( uxInitialCount ) ) #endif
可以看出,真正干事的是函数 xQueueCreateCountingSemaphore(),此函数在文件queue.c中有如下定义:
1 QueueHandle_t xQueueCreateCountingSemaphore( const UBaseType_t uxMaxCount, const UBaseType_t uxInitialCount ) 2 { 3 QueueHandle_t xHandle; 4 5 configASSERT( uxMaxCount != 0 ); 6 configASSERT( uxInitialCount <= uxMaxCount ); 7 8 xHandle = xQueueGenericCreate( uxMaxCount, queueSEMAPHORE_QUEUE_ITEM_LENGTH, queueQUEUE_TYPE_COUNTING_SEMAPHORE ); 9 10 if( xHandle != NULL ) 11 { 12 ( ( Queue_t * ) xHandle )->uxMessagesWaiting = uxInitialCount; 13 14 traceCREATE_COUNTING_SEMAPHORE(); 15 } 16 else 17 { 18 traceCREATE_COUNTING_SEMAPHORE_FAILED(); 19 } 20 21 return xHandle; 22 }
第8行:计数型信号量也是在队列的基础上实现的,所以需要调用函数 xQueueGenericCreate()创建一个队列,队列的长度为uxMaxCount,队列项长度为queueSEMAPHORE_QUEUE_ITEM_LENGTH(此宏为0),队列的类型为 queueQUEUE_TYPE_COUNTING_SEMAPHORE,表示是个计数型信号量。
第12行:队列结构体成员变量uxMessageWaiting用于计数型信号量的计数,根据计数型信号量的初始值来设置uxMessageWaiting。
计数型信号量的释放和获取与二值信号量相同。
实验
计数型信号量一般用于事件计数和资源管理,计数型信号量在这个场景中的使用方法基本一样。这是使用计数型信号量在事件计数中的使用。
本实验设计三个任务:start_task、task1_task、DataProcess_task。
start_task:用来创建其他2个任务
task1_task:获取按键KEY1后就释放信号量
DataProcess_task:获取信号量,并打印信号量的值。
任务设置
//任务优先级 #define START_TASK_PRIO 1 //任务堆栈大小 #define START_STK_SIZE 128 //任务句柄 TaskHandle_t StartTask_Handler; //任务函数 void start_task(void *pvParameters); //任务优先级 #define TASK1_TASK_PRIO 2 //任务堆栈大小 #define TASK1_STK_SIZE 50 //任务句柄 TaskHandle_t Task1Task_Handler; //任务函数 void task1_task(void *pvParameters); //任务优先级 #define DATAPROCESS_TASK_PRIO 3 //任务堆栈大小 #define DATAPROCESS_STK_SIZE 50 //任务句柄 TaskHandle_t DataProcessTask_Handler; //任务函数 void DataProcess_task(void *pvParameters);
int main(void) { NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);//设置系统中断优先级分组4 delay_init(); //延时函数初始化 uart_init(115200); //初始化串口 LED_Init(); //初始化LED KEY_Init(); // 初始化按键 SemaphoreCount = xSemaphoreCreateCounting( 255, 0 ); // 创建计数型信号量 if(SemaphoreCount == NULL) { printf(" SemaphoreCount Created Failed! "); } //创建开始任务 xTaskCreate((TaskFunction_t )start_task, //任务函数 (const char* )"start_task", //任务名称 (uint16_t )START_STK_SIZE, //任务堆栈大小 (void* )NULL, //传递给任务函数的参数 (UBaseType_t )START_TASK_PRIO, //任务优先级 (TaskHandle_t* )&StartTask_Handler); //任务句柄 vTaskStartScheduler(); //开启任务调度 }
任务函数
//开始任务任务函数 void start_task(void *pvParameters) { taskENTER_CRITICAL(); //进入临界区 //创建LED0任务 xTaskCreate((TaskFunction_t )task1_task, (const char* )"task1_task", (uint16_t )TASK1_STK_SIZE, (void* )NULL, (UBaseType_t )TASK1_TASK_PRIO, (TaskHandle_t* )&Task1Task_Handler); //创建LED1任务 xTaskCreate((TaskFunction_t )DataProcess_task, (const char* )"DataProcess_task", (uint16_t )DATAPROCESS_STK_SIZE, (void* )NULL, (UBaseType_t )DATAPROCESS_TASK_PRIO, (TaskHandle_t* )&DataProcessTask_Handler); vTaskDelete(StartTask_Handler); //删除开始任务 taskEXIT_CRITICAL(); //退出临界区 } // TASK1任务函数 void task1_task(void *pvParameters) { u8 key = 0; while(1) { key = KEY_Scan(0); if((SemaphoreCount != NULL) && (key)) { if(key == KEY1_PRES) { xSemaphoreGive(SemaphoreCount); // 释放信号量 LED0=~LED0; } }else{ vTaskDelay(10); } } } //DataProcess任务函数 void DataProcess_task(void *pvParameters) { UBaseType_t countVal = 0; while(1) { if(SemaphoreCount != NULL) { xSemaphoreTake(SemaphoreCount,portMAX_DELAY); // 死等 countVal = uxSemaphoreGetCount(SemaphoreCount); printf("countVal = %d ",(uint8_t)countVal); }else{ vTaskDelay(10); } LED1=~LED1; vTaskDelay(1000); } }
注意:但第一次按键时,输出的countVal=0。因为在xSemaphoreTask()中有uxMessageWaiting-1的操作。
使用计数型信号量,首先创建。调用函数xSemaphoreCreateCounting()创建一个计数型信号量CountSemaphore。计数型信号量最大值设置为255,由于本实验中计数型信号量用于事件计数,所以信号量的初始值设置为255,如果计数型信号量用于资源管理的话,那么事件计数型信号量的初始值就应该根据资源的实际数量来设置。
如果按键KEY按下,表示事件发生了,就调用函数xSemaphoreGive()释放信号量SemaphoreCount.
调用函数uxSemaphoreGetCount()获取信号量SemaphoreCount的信号量值。释放信号量的话信号量值就会加一。函数uxSemaphoreGetCount()是来获取信号量值得,这个函数是个宏,是对函数uxQueueMessageWaiting()的一个简单封装,其实就是返回队列结构体成员变量uxMessageWaiting的值。
优先级翻转
在使用二值信号量的时候会遇到很常见的一个问题:优先级翻转。优先级翻转在可剥夺内核中是非常常见的,在实时系统中不允许出现这种现象,这样会破坏任务的预期顺序,可能会导致严重的后果。下图是一个优先级翻转的例子:
(1)任务H和任务M处于挂起状态,等待某一事件的发生,任务L正在运行。
(2)某一时刻任务L想要访问共享资源,在此之前它必须先获得对应资源的信号量。
(3)任务L获得信号量并开始使用该共享资源。
(4)由于任务H优先级高,它等待的时间发生后便剥夺了任务L的CPU使用权。
(5)任务H开始运行。
(6)任务H运行过程中也要使用任务L正在使用着的资源,由于该资源的信号量还被L占用着,任务H只能进入挂起状态,等待任务L释放该信号量。
(7)任务L继续运行。
(8)由于任务M优先级高于任务L,当任务M等待的事件发生后,任务M剥夺了任务L的CPU使用权。
(9)任务M处理该处理的事。
(10)任务M执行完毕后,将CPU使用权归还给任务L。
(11)任务L继续执行。
(12)最终任务L完成所有的工作并释放了信号量,到此为止,由于实时内核知道有个高优先级的任务正在等待这个信号量,故内核做任务切换。
(13)任务H得到该信号量并接着运行。
在这种情况下,任务H的优先级实际上降到了任务L的优先级水平。因为任务H要一直等待任务L释放其占用的那个共享资源。由于任务M剥夺了任务L的CPU使用权,使得任务H的情况更加恶化,这样就相当于任务M的优先级高于任务H,导致优先级翻转。
优先级翻转实验
在使用二值信号量的时候会存在优先级翻转的问题,本实验模拟实现优先级翻转,观察优先级翻转对抢占式内核的影响。
设计4个任务:start_task、high_task、middle_task、low_task,这四个任务的功能如下:
start_task:用来创建其他3个任务。
hight_task:高优先级任务,会获取二值信号量,获取成功以后会进行相应的处理,处理完成以后会释放二值信号量。
middle_task:中等优先级任务,一个简单的应用任务。
low_task:低优先级任务,和高优先级任务一样,会获取二值信号量,获取成功以后会进行相应的处理,不过不同之处在于低优先级任务占用二值信号量的时间要久一点(软件模拟占用)。
创建一个二值信号量BinarySemaphore,高优先级和低优先级这两个任务会使用这个二值信号量。
任务设置:
//任务优先级 #define START_TASK_PRIO 1 //任务堆栈大小 #define START_STK_SIZE 128 //任务句柄 TaskHandle_t StartTask_Handler; //任务函数 void start_task(void *pvParameters); //任务优先级 #define HIGH_TASK_PRIO 4 //任务堆栈大小 #define HIGH_STK_SIZE 50 //任务句柄 TaskHandle_t HIGHTask_Handler; //任务函数 void high_task(void *pvParameters); //任务优先级 #define MIDDLE_TASK_PRIO 3 //任务堆栈大小 #define MIDDLE_STK_SIZE 50 //任务句柄 TaskHandle_t MIDDLETask_Handler; //任务函数 void middle_task(void *pvParameters); //任务优先级 #define LOW_TASK_PRIO 2 //任务堆栈大小 #define LOW_STK_SIZE 50 //任务句柄 TaskHandle_t LOWTask_Handler; //任务函数 void low_task(void *pvParameters); SemaphoreHandle_t BinarySemaphore = NULL; // 二值信号量
main() 函数
int main(void) { NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);//设置系统中断优先级分组4 delay_init(); //延时函数初始化 uart_init(115200); //初始化串口 LED_Init(); //初始化LED //创建开始任务 xTaskCreate((TaskFunction_t )start_task, //任务函数 (const char* )"start_task", //任务名称 (uint16_t )START_STK_SIZE, //任务堆栈大小 (void* )NULL, //传递给任务函数的参数 (UBaseType_t )START_TASK_PRIO, //任务优先级 (TaskHandle_t* )&StartTask_Handler); //任务句柄 vTaskStartScheduler(); //开启任务调度 }
任务函数:
//开始任务任务函数 void start_task(void *pvParameters) { taskENTER_CRITICAL(); //进入临界区 BinarySemaphore = xSemaphoreCreateBinary(); // 创建二值信号量 if(BinarySemaphore == NULL) { printf("BinarySemaphore Created Failed! "); }else { xSemaphoreGive(BinarySemaphore); // 释放二值信号量 } //创建HIGH任务 xTaskCreate((TaskFunction_t )high_task, (const char* )"high_task", (uint16_t )HIGH_STK_SIZE, (void* )NULL, (UBaseType_t )HIGH_TASK_PRIO, (TaskHandle_t* )&HIGHTask_Handler); //创建MIDDLE任务 xTaskCreate((TaskFunction_t )middle_task, (const char* )"middle_task", (uint16_t )MIDDLE_STK_SIZE, (void* )NULL, (UBaseType_t )MIDDLE_TASK_PRIO, (TaskHandle_t* )&MIDDLETask_Handler); //创建LOW任务 xTaskCreate((TaskFunction_t )low_task, (const char* )"low_task", (uint16_t )LOW_STK_SIZE, (void* )NULL, (UBaseType_t )LOW_TASK_PRIO, (TaskHandle_t* )&LOWTask_Handler); vTaskDelete(StartTask_Handler); //删除开始任务 taskEXIT_CRITICAL(); //退出临界区 } //HIGH任务函数 void high_task(void *pvParameters) { while(1) { printf("high_task pending! "); // 任务挂起 xSemaphoreTake(BinarySemaphore,portMAX_DELAY); // 获取二值信号量 printf("high_task running! "); // 任务运行 xSemaphoreGive(BinarySemaphore); // 释放二值信号量 vTaskDelay(500); } } //MIDDLE任务函数 void middle_task(void *pvParameters) { while(1) { printf("middle_task running! "); vTaskDelay(1000); } } //LOW任务函数 void low_task(void *pvParameters) { u32 i = 0; while(1) { xSemaphoreTake(BinarySemaphore,portMAX_DELAY); // 获取二值信号量 printf("low_task running! "); // 任务运行 for(i=0;i<2500000;i++) { taskYIELD(); // 发起任务调度 } xSemaphoreGive(BinarySemaphore); // 释放二值信号量 vTaskDelay(1000); } }
打印输出结果:
high_task pending! 和 high_task running!之间出现 middle_task running!
高优先级任务执行过程中,中等优先级多次运行,发生优先级翻转。
当一个地优先级任务和一个高优先级任务同时使用同一个信号量,而系统中还有其他中等优先级任务时。如果低优先级获得了信号量,那么高优先级任务就会处于等待状态,但是,中等优先级任务可以打断低优先级任务而先于高优先级任务运行(此时高优先级的任务在等待信号量,所以不能运行),这就出现了优先级翻转现象。
优先级翻转问题很严重,可以使用互斥信号量。
互斥信号量
互斥信号量简介
互斥信号量其实就是一个拥有优先级继承的二值信号量,在同步的应用中(任务与任务或中断与任务之间的同步)二值信号量最合适。互斥信号量适合用于那些需要互斥访问的应用中。在互斥访问中互斥信号量相当于一个钥匙,当任务想要使用资源的时候就必须先获得这个钥匙,但使用完资源以后就必须归还这个钥匙,这样其他的任务就可以拿着这个钥匙去使用资源。
互斥信号量使用和二值信号量具有相同的API操作函数,所以互斥信号量也可以设置阻塞时间,不同于二值信号量的事互斥信号量具有优先级继承的特性。当一个互斥信号量正在别一个低优先级的任务使用,而此时有个高优先级的任务也尝试获取这个互斥信号量的话就会被阻塞。不过这个高优先级任务会将低优先级的任务的优先级提升到与自己相同的优先级,这个过程就是优先级继承。优先级继承尽可能地降低了高优先级任务处于阻塞态的时间,并且将已出现的“优先级翻转”的影响降到最低。
优先级继承并不能完全消除优先级翻转,它只是尽可能地降低优先级翻转带来的影响。硬实时应用应该在设计之初就要避免优先级翻转发生。互斥信号量不能用于中断服务函数中,原因如下:
1. 互斥信号量有优先级继承的机制,所以只能用在任务中,不能用于中断服务函数。
2. 中断服务函数中不能因为要等待互斥信号量而设置阻塞时间而进入阻塞态
创建互斥信号量
FreeRTOS提供了两个互斥信号量创建函数:
函数 | 描述 |
xSemaphoreCreateMutex() | 使用动态方法创建互斥信号量 |
xSemaphoreCreateMutexStatic() | 使用静态方法创建互斥信号量 |
1. 函数 xSemaphoreCreateMutex()
此函数用于创建一个互斥信号量,所需要的内存通过动态内存管理方法分配。此函数本质是一个宏,真正完成信号量创建的是函数xQueueCreateMutex(),此函数原型如下:
SemaphoreHandle_t xSemaphoreCreateMutex( void )
参数:
无。
返回值:
NULL:互斥信号量创建失败。
其他值:创建成功的互斥信号量的句柄。
2. 函数 xSemaphoreCreateMutexStatic()
此函数也是创建互斥信号量的,只不过使用次函数创建互斥信号量的话,信号量所需要的RAM需要由用户来分配,此函数是个宏,具体创建过程是通过函数 xQueueCreateMutexStatic() 来完成的,函数原型如下:
SemaphoreHandle_t xSemaphoreCreateMutexStatic( StaticSemaphore_t *pxMutexBuffer )
参数:
pxMutexBuffer:此参数指向一个StaticSemaphore_t类型的变量,用来保存信号量结构体。
返回值:
NULL:互斥信号量创建失败。
其他值:创建成功的互斥信号量的句柄。
互斥信号量创建过程分析
这里只分析动态创建互斥信号量函数 xSemaphoreCreateMutex(),此函数是个宏,定义如下:
#define xSemaphoreCreateMutex() xQueueCreateMutex( queueQUEUE_TYPE_MUTEX )
可以看出,真正执行的是函数xQueueCreateMutex(),此函数在queue.c中有如下定义:
1 QueueHandle_t xQueueCreateMutex( const uint8_t ucQueueType ) 2 { 3 Queue_t *pxNewQueue; 4 const UBaseType_t uxMutexLength = ( UBaseType_t ) 1, uxMutexSize = ( UBaseType_t ) 0; 5 6 pxNewQueue = ( Queue_t * ) xQueueGenericCreate( uxMutexLength, uxMutexSize, ucQueueType ); 7 prvInitialiseMutex( pxNewQueue ); 8 9 return pxNewQueue; 10 }
第6行:调用函数xQueueGenericCreate()创建一个队列,队列长度为1,队列项长度为0,队列类型为参数ucQueueType。由于本函数是创建互斥信号量,所以参数ucQueueType为queueQUEUE_TYPE_MUTEX。
第7行:调用函数prvInitialiseMutex()初始化互斥信号量。
函数prvInitialiseMutex()代码如下:
static void prvInitialiseMutex( Queue_t *pxNewQueue ) { if( pxNewQueue != NULL ) { /* The queue create function will set all the queue structure members correctly for a generic queue, but this function is creating a mutex. Overwrite those members that need to be set differently - in particular the information required for priority inheritance. */ pxNewQueue->pxMutexHolder = NULL; pxNewQueue->uxQueueType = queueQUEUE_IS_MUTEX; /* In case this is a recursive mutex. */ pxNewQueue->u.uxRecursiveCallCount = 0; traceCREATE_MUTEX( pxNewQueue ); /* Start with the semaphore in the expected state. */ ( void ) xQueueGenericSend( pxNewQueue, NULL, ( TickType_t ) 0U, queueSEND_TO_BACK ); } else { traceCREATE_MUTEX_FAILED(); } }
实验
将上面优先级翻转的程序中的二值信号量改为互斥信号量
任务设置:
//任务优先级 #define START_TASK_PRIO 1 //任务堆栈大小 #define START_STK_SIZE 128 //任务句柄 TaskHandle_t StartTask_Handler; //任务函数 void start_task(void *pvParameters); //任务优先级 #define HIGH_TASK_PRIO 4 //任务堆栈大小 #define HIGH_STK_SIZE 50 //任务句柄 TaskHandle_t HIGHTask_Handler; //任务函数 void high_task(void *pvParameters); //任务优先级 #define MIDDLE_TASK_PRIO 3 //任务堆栈大小 #define MIDDLE_STK_SIZE 50 //任务句柄 TaskHandle_t MIDDLETask_Handler; //任务函数 void middle_task(void *pvParameters); //任务优先级 #define LOW_TASK_PRIO 2 //任务堆栈大小 #define LOW_STK_SIZE 50 //任务句柄 TaskHandle_t LOWTask_Handler; //任务函数 void low_task(void *pvParameters); SemaphoreHandle_t MutexSemaphore = NULL; // 互斥信号量句柄
main() 函数:
int main(void) { NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);//设置系统中断优先级分组4 delay_init(); //延时函数初始化 uart_init(115200); //初始化串口 LED_Init(); //初始化LED //创建开始任务 xTaskCreate((TaskFunction_t )start_task, //任务函数 (const char* )"start_task", //任务名称 (uint16_t )START_STK_SIZE, //任务堆栈大小 (void* )NULL, //传递给任务函数的参数 (UBaseType_t )START_TASK_PRIO, //任务优先级 (TaskHandle_t* )&StartTask_Handler); //任务句柄 vTaskStartScheduler(); //开启任务调度 }
任务函数:
//开始任务任务函数 void start_task(void *pvParameters) { taskENTER_CRITICAL(); //进入临界区 MutexSemaphore = xSemaphoreCreateMutex(); // 创建互斥信号量 if(MutexSemaphore == NULL) { printf("BinarySemaphore Created Failed! "); }else { xSemaphoreGive(MutexSemaphore); // 释放二值信号量 } //创建HIGH任务 xTaskCreate((TaskFunction_t )high_task, (const char* )"high_task", (uint16_t )HIGH_STK_SIZE, (void* )NULL, (UBaseType_t )HIGH_TASK_PRIO, (TaskHandle_t* )&HIGHTask_Handler); //创建MIDDLE任务 xTaskCreate((TaskFunction_t )middle_task, (const char* )"middle_task", (uint16_t )MIDDLE_STK_SIZE, (void* )NULL, (UBaseType_t )MIDDLE_TASK_PRIO, (TaskHandle_t* )&MIDDLETask_Handler); //创建LOW任务 xTaskCreate((TaskFunction_t )low_task, (const char* )"low_task", (uint16_t )LOW_STK_SIZE, (void* )NULL, (UBaseType_t )LOW_TASK_PRIO, (TaskHandle_t* )&LOWTask_Handler); vTaskDelete(StartTask_Handler); //删除开始任务 taskEXIT_CRITICAL(); //退出临界区 } //HIGH任务函数 void high_task(void *pvParameters) { while(1) { vTaskDelay(500); printf("high_task pending! "); // 任务挂起 xSemaphoreTake(MutexSemaphore,portMAX_DELAY); // 获取二值信号量 printf("high_task running! "); // 任务运行 xSemaphoreGive(MutexSemaphore); // 释放二值信号量 vTaskDelay(500); } } //MIDDLE任务函数 void middle_task(void *pvParameters) { while(1) { printf("middle_task running! "); vTaskDelay(1000); } } //LOW任务函数 void low_task(void *pvParameters) { u32 i = 0; while(1) { xSemaphoreTake(MutexSemaphore,portMAX_DELAY); // 获取二值信号量 printf("low_task running! "); // 任务运行 for(i=0;i<2500000;i++) { taskYIELD(); // 发起任务调度 } xSemaphoreGive(MutexSemaphore); // 释放二值信号量 vTaskDelay(1000); } }
实验现象:
在high_task pending!和 high_task_running!之间没有其他低优先级的任务运行。