目录
3.1.1 创建二值信号量 xSemaphoreCreateBinary()
3.1.2 创建计数信号量 xSemaphoreCreateCounting()
3.1.3 信号量删除函数 vSemaphoreDelete()
3.2.2 中断释放函数 xSemaphoreGiveFromISR()
3.3.2 中断获取函数 xSemaphoreTakeFromISR()
1. 信号量简介
信号量(Semaphore)是一种实现任务间通信的机制,可以实现任务之间同步或临界资源的互斥访问,常用于协助一组相互竞争的任务来访问临界资源。在多任务系统中,各任务之间需要同步或互斥实现临界资源的保护,信号量功能可以为用户提供这方面的支持。
1.1 同步和互斥
1.1.1 同步
定义:同步是指在执行任务时,通过某种机制来协调不同任务的行为,确保它们以正确的顺序和条件进行。简单来说,就是使得一个任务的执行进度能够与另一个任务的状态保持一致。
例子:通俗点解释,如果你去包子店想买包子,但店里暂时没有包子,你就需要等待店主做包子。这种等待就是一种同步行为,确保你在包子准备好之前不能买到包子。
又例如,你需要进行传感器数据采集,一个采集数据的传感器任务,一个处理数据的任务,你想要处理数据,则需要等待传感器先去采用数据,这种等待的行为就称为同步行为。
1.1.2 互斥
定义:互斥是指确保在任何时刻只有一个任务能够访问共享资源。它防止了多个任务同时访问共享资源,从而避免数据冲突或资源争用的问题。
例子:厕所(临界资源或者说是共享资源),人上厕所(执行的任务)
为了保证资源的合理使用,互斥机制确保在任何时刻只有一个人可以使用厕所。当一个人正在使用厕所时,其他人必须等待。
1.1.3 总结
同步是一种更为复杂的互斥,而互斥是一种特殊的同步。
例子:还是以上厕所为例。
厕所(临界资源或者说是共享资源),甲、乙、丙、丁四个人上厕所(执行的任务)
同步:相当于我给你规定了上厕所的顺序,甲、乙、丙、丁(任务)排队上厕所(共享资源),是有顺序的,例如甲执行完才是乙,乙执行完才是丙,丙执行完才是丁,你需要按照我给你排的顺序上厕所。
互斥:也是甲、乙、丙、丁(任务)排队上厕所(共享资源),不过并没有顺序,互斥就保证你们四个人就只能一个人到厕所里。但是你们谁先上我不管,我就保证你们在同一个时间内,只有一个人在厕所里(使用共享资源)。
1.2 分类
1.2.1 二值信号量
定义:二值信号量是一种特殊的信号量,其内部状态只有两种:被释放(释放状态)或被占用(占用状态)。二值信号量既可以用于临界资源访问,也可用于同步功能。
特点:只有两种状态(0 或 1),类似于一个长度为1的队列。
用途:同步,可以用来实现任务间的事件通知。
互斥,用于保护临界区,但不支持优先级继承机制。
示例:一个任务用二值信号量通知另一个任务某个事件的发生。
xSemaphoreGive(xBinarySemaphore); // 释放信号量
xSemaphoreTake(xBinarySemaphore, portMAX_DELAY); // 等待信号量
1.2.2 计数信号量
定义:计数信号量是一种信号量,其内部状态可以是任意非负整数。 它可以被认为长度大于1的队列,信号量使用者依然不必关心存储在队列中的消息,只需要关心队列是否有消息即可。
特点:可以有多个状态值,表示可用的资源数量。
用途:资源计数,用于表示可用资源的数量。例如,有限数量的资源池。
任务同步,用于控制任务之间的协调。
示例:一个任务使用计数信号量来限制对共享资源的访问,例如限制最大并发线程数。
xSemaphoreGive(xCountingSemaphore); // 增加信号量计数
xSemaphoreTake(xCountingSemaphore, portMAX_DELAY); // 减少信号量计数
1.2.3 互斥信号量
定义:互斥信号量其实是特殊的二值信号量,由于其特有的优先级继承机制从而使他更适用于简单互锁,也就是保护临界资源。
特点:当一个低优先级的任务持有互斥信号量时,如果一个高优先级的任务请求该信号量,低优先级任务的优先级会临时提升,以避免优先级反转问题。
用途:专门用于保护临界区和同步任务。
示例:使用互斥信号量来保护一个共享资源,确保在任何时刻只有一个任务可以访问该资源。
xSemaphoreTake(xMutex, portMAX_DELAY); // 获取互斥信号量
// 访问共享资源
xSemaphoreGive(xMutex); // 释放互斥信号量
1.2.4 递归信号量
定义:递归信号量允许同一任务多次获取相同的信号量,而不需要释放每次获取时都释放。递归信号量的使用场景通常是需要递归调用的任务或函数。
特点:递归获取,同一任务可以多次获取递归信号量,每次获取时信号量计数会增加。
递归释放,需要和获取次数相同次数的释放才能真正释放信号量,使得其他任务可以获取它。
用途:适用于递归函数或需要在同一任务中多次获取信号量的场景。
示例:一个任务递归调用函数时,使用递归信号量保护临界区。
xSemaphoreTakeRecursive(xRecursiveSemaphore, portMAX_DELAY); // 递归获取信号量
// 递归函数调用
xSemaphoreGiveRecursive(xRecursiveSemaphore); // 递归释放信号量
2. 信号量控制块
/*
* 定义调度器使用的队列。
* 项目通过复制而非引用排队。请参见以下链接了解详细原因:http://www.freertos.org/Embedded-RTOS-Queues.html
*/
typedef struct QueueDefinition
{
int8_t *pcHead; /*< 指向队列存储区域的开始位置。 */
int8_t *pcTail; /*< 指向队列存储区域末尾的字节。一次分配比实际需要的多一个字节,以作为标记。 */
int8_t *pcWriteTo; /*< 指向存储区域中下一个可写的位置。 */
union /* 使用联合体是为了确保两个互斥的结构成员不会同时出现(避免浪费内存)。 */
{
int8_t *pcReadFrom; /*< 当结构体被用作队列时,指向最后一个读取项目的位置。 */
UBaseType_t uxRecursiveCallCount; /*< 当结构体用作互斥量时,维护递归获取互斥量的次数。 */
} u;
List_t xTasksWaitingToSend; /*< 阻塞等待向此队列发送项目的任务列表,按优先级排序。 */
List_t xTasksWaitingToReceive; /*< 阻塞等待从此队列接收项目的任务列表,按优先级排序。 */
volatile UBaseType_t uxMessagesWaiting; /*< 当前队列中项目的数量。 */
UBaseType_t uxLength; /*< 队列的长度,定义为队列可以容纳的项目数,而不是字节数。 */
UBaseType_t uxItemSize; /*< 队列将持有的每个项目的大小。 */
volatile int8_t cRxLock; /*< 存储在队列锁定时从队列中接收的项目数量(从队列中移除)。队列未锁定时设置为 queueUNLOCKED。 */
volatile int8_t cTxLock; /*< 存储在队列锁定时传输到队列的项目数量(添加到队列中)。队列未锁定时设置为 queueUNLOCKED。 */
#if( ( configSUPPORT_STATIC_ALLOCATION == 1 ) && ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) )
uint8_t ucStaticallyAllocated; /*< 如果队列的内存是静态分配的,设置为 pdTRUE,以确保不尝试释放内存。 */
#endif
#if ( configUSE_QUEUE_SETS == 1 )
struct QueueDefinition *pxQueueSetContainer; /*< 如果启用了队列集功能,指向包含此队列的队列集。 */
#endif
#if ( configUSE_TRACE_FACILITY == 1 )
UBaseType_t uxQueueNumber; /*< 队列编号,用于跟踪功能。 */
uint8_t ucQueueType; /*< 队列类型,用于跟踪功能。 */
#endif
} xQUEUE;
/* 旧的 xQUEUE 名称在上面维护,然后重新定义为新的 Queue_t 名称,以便支持旧的内核调试工具。 */
typedef xQUEUE Queue_t;
3. 常用信号量API函数
3.1 创建信号量函数
3.1.1 创建二值信号量 xSemaphoreCreateBinary()
#define xSemaphoreCreateBinary()
xQueueGenericCreate( ( UBaseType_t ) 1,
semSEMAPHORE_QUEUE_ITEM_LENGTH,
queueQUEUE_TYPE_BINARY_SEMAPHORE )
( UBaseType_t ) 1:这是传递给 xQueueGenericCreate 函数的第一个参数,表示队列(或信号量)可以容纳的最大项数。在创建二进制信号量时,这里设置为 1,因为二进制信号量只需要一个项目来表示“有信号”或“无信号”。
semSEMAPHORE_QUEUE_ITEM_LENGTH:这是第二个参数,指定队列项的长度,这里设置为 0。对于二进制信号量,这个值通常是sizeof(xSemaphoreHandle)或其他相应长度。它定义了队列中每个项的大小。在实际实现中,这个值通常是一个宏,确保与信号量的数据结构对齐。但是在二值信号量,我们不关注它的消息内容是什么,只关心它的里面有没有消息,因此这个值设为0。
queueQUEUE_TYPE_BINARY_SEMAPHORE:这是第三个参数,指定队列的类型。在这里,queueQUEUE_TYPE_BINARY_SEMAPHORE表示这是一个二进制信号量。这个宏在 FreeRTOS 的内部实现中定义了队列的具体类型,帮助系统正确管理不同类型的队列和信号量。
简单来说,就是创建一个没有消息存储空间的队列。
3.1.2 创建计数信号量 xSemaphoreCreateCounting()
函数原型 | SemaphoreHandle_t xSemaphoreCreateCounting( UBaseType_t uxMaxCount, UBaseType_tuxInitialCount); | |
功能 | 创建一个计数信号量。 | |
参数 | uxMaxCount | 计数信号量的最大值,当达到这个值的时候,信号量不能再被释放。 |
uxInitialCount | 创建计数信号量的初始值。 | |
返回值 | 如果创建成功则返回一个计数信号量句柄,用于访问创建的计数信号量。如果创建不成功则返回 NULL。 |
3.1.3 信号量删除函数 vSemaphoreDelete()
函数原型 | void vSemaphoreDelete( SemaphoreHandle_t xSemaphore ); | |
功能 | 删除一个信号量 | |
参数 | xSemaphore | 信号量句柄 |
返回值 | 无 |
3.2 信号量释放函数
3.2.1 普通释放函数 xSemaphoreGive()
#define xSemaphoreGive( xSemaphore )
xQQueueGenericSend( ( QueueHandle_t ) ( xSemaphore ),
NULL,
semGIVE_BLOCK_TIME,
queueSEND_TO_BACK)
3.2.2 中断释放函数 xSemaphoreGiveFromISR()
#define xSemaphoreGiveFromISR( xSemaphore, pxHigherPriorityTaskWoken )
xQueueGiveFromISR((QueueHandle_t)( xSemaphore ),
(pxHigherPriorityTaskWoken ))
3.3 信号量获取函数
3.3.1 普通获取函数 xSemaphoreTake()
函数原型 | #define xSemaphoreTake( xSemaphore, xBlockTime) xQueueGenericReceive( (QueueHandle_t )(xSemaphore ), NULL, ( xBlockTime ), pdFALSE) | |
功能 | 获取一个信号量,可以是二值信号量、计数信号量、互斥量。 | |
参数 | xSemaphore | 信号量句柄。 |
xBlockTime | 等待信号量可用的最大超时时间,单位为tick(即系统节拍周期)如果宏INCLUDE vTaskSuspend定义为1且形参xTicksToWait设置为portMAX_DELAY,则任务将一直阻塞在该信号量上(即没有超时时间) | |
返回值 | 获取成功则返回pTRUE,在指定的超时时间中没有获取成功则返回 errQUEUE_EMPTY。 |
3.3.2 中断获取函数 xSemaphoreTakeFromISR()
函数原型 | xSemaphoreTakeFromISR(SemahoreHale_t xmar, signed BaseType_t pxHigherPriorityTaskWoken) | |
功能 | 在中断中获一个信号量(其实很少在中断中获取信号量)。可以是二值信号量、计数信号量。 | |
参数 | xSemaphore | 信号量句柄。 |
pxHigherPriority TaskWoken | 一个或者多个任务有可能阻塞在同一个信号量上,调用函数SemaphoreTakeFromISRO会唤醒阻塞在该信号量上优先级最高的信号量入队任务,如果被唤醒的任务的优先级大于或者等于被中断的任务的优先级,那么形参pxHigherPriorityTaskWoken就会被设置为pdTRUE.然后在中断退出前执行一次上下文切换,中断退出后则直接返回刚刚被唤醒的高优先级的任务。从FreeRTOS V7.3.0版本开始, pxHigherPriorityTaskWoken是一个可选的参数,可以设置为 NULL。 | |
返回值 | 获取成功则返回pdTRUE,没有获取成功则返回errQUEUE_EMPTY,没有获取成功是因为信号量不可用。 |
4. 代码编写
4.1 二值信号量
首先,找到一个工程,这里我们以动态任务为例:
4.1.1 应用任务创建
任务1函数:
实现LED灯的闪烁:
//任务1函数
void led1_task(void *pvParameters)
{
while(1)
{
LED1=0;
vTaskDelay(200);
LED1=1;
vTaskDelay(800);
}
}
接收任务函数:
用于接收消息变量,当接受到发送任务函数发送的BinarySem_Handle,串口打印BinarySem_Handle二值信号量获取成功!,并实现led2的翻转:
//接收任务函数
void receive_task(void *pvParameters)
{
BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
while(1)
{
//获取二值信号量 xSemaphore,没获取到则一直等待
xReturn = xSemaphoreTake(BinarySem_Handle,/* 二值信号量句柄 */
portMAX_DELAY); /* 等待时间 */
if(pdTRUE == xReturn)
printf("BinarySem_Handle二值信号量获取成功!\n\n");
LED2=!LED2;
}
}
这里的任务不需要再调用vTaskDelay();进行延时阻塞让出CPU的资源,因为在:
//获取二值信号量 xSemaphore,没获取到则一直等待
xReturn = xSemaphoreTake(BinarySem_Handle,/* 二值信号量句柄 */
portMAX_DELAY); /* 等待时间 */
portMAX_DELAY表示无限等待,直到有消息可接收,已经进行了阻塞。
发送任务函数:
用于发送消息,当按键PB1按下给出二值信号量BinarySem_Handle,当按键PB11按下给出二值信号量BinarySem_Handle:
//发送任务函数
void send_task(void *pvParameters)
{
BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
uint8_t KeyNum;
while(1)
{
KeyNum=Key_GetNum();
if(KeyNum==1)
{
xReturn = xSemaphoreGive( BinarySem_Handle );//给出二值信号量
if( xReturn == pdTRUE )
printf("BinarySem_Handle二值信号量释放成功,按键1!\r\n");
else
printf("BinarySem_Handle二值信号量释放失败,按键1!\r\n");
}
else if(KeyNum==2)
{
xReturn = xSemaphoreGive( BinarySem_Handle );//给出二值信号量
if( xReturn == pdTRUE )
printf("BinarySem_Handle二值信号量释放成功,按键2!\r\n");
else
printf("BinarySem_Handle二值信号量释放失败,按键2!\r\n");
}
vTaskDelay(20);
}
}
相关宏定义:
//任务优先级
#define LED1_TASK_PRIO 2
//任务堆栈大小
#define LED1_STK_SIZE 50
//任务句柄
TaskHandle_t LED1Task_Handler;
//任务函数
void led1_task(void *pvParameters);
//任务优先级
#define RECEIVE_TASK_PRIO 3
//任务堆栈大小
#define RECEIVE_STK_SIZE 50
//任务句柄
TaskHandle_t ReceiveTask_Handler;
//任务函数
void receive_task(void *pvParameters);
//任务优先级
#define SEND_TASK_PRIO 4
//任务堆栈大小
#define SEND_STK_SIZE 50
//任务句柄
TaskHandle_t SendTask_Handler;
//任务函数
void send_task(void *pvParameters);
4.1.2 开始任务的创建
之前讲过,开始任务的作用就是将,应用任务的三个任务完整的建立起来,通过调用临界区的API,规避中断带来的风险,创建完后删除自身,只进行应用任务的调度:
//开始任务任务函数
void start_task(void *pvParameters)
{
taskENTER_CRITICAL(); //进入临界区
/* 创建 BinarySem */
BinarySem_Handle = xSemaphoreCreateBinary();
//创建LED1任务
xTaskCreate((TaskFunction_t )led1_task,
(const char* )"led1_task",
(uint16_t )LED1_STK_SIZE,
(void* )NULL,
(UBaseType_t )LED1_TASK_PRIO,
(TaskHandle_t* )&LED1Task_Handler);
//创建接收任务
xTaskCreate((TaskFunction_t )receive_task,
(const char* )"receive_task",
(uint16_t )RECEIVE_STK_SIZE,
(void* )NULL,
(UBaseType_t )RECEIVE_TASK_PRIO,
(TaskHandle_t* )&ReceiveTask_Handler);
//创建发送任务
xTaskCreate((TaskFunction_t )send_task,
(const char* )"send_task",
(uint16_t )SEND_STK_SIZE,
(void* )NULL,
(UBaseType_t )SEND_TASK_PRIO,
(TaskHandle_t* )&SendTask_Handler);
vTaskDelete(StartTask_Handler); //删除开始任务
taskEXIT_CRITICAL(); //退出临界区
}
消息队列的创建以及开始任务相关宏定义:
//任务优先级
#define START_TASK_PRIO 1
//任务堆栈大小
#define START_STK_SIZE 128
//任务句柄
TaskHandle_t StartTask_Handler;
//任务函数
void start_task(void *pvParameters);
SemaphoreHandle_t BinarySem_Handle =NULL;
4.1.3 主函数
创建开始任务开始调用:
int main()
{
SysTick_Init(72);
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);//设置系统中断优先级分组4
LED_Init();
KEY_Init();
USART1_Init(115200);
printf("FreeRTOS二值信号量实验\r\n");
printf("按下KEY_UP或者KEY1进行任务与任务间的同步\r\n");
printf("Receive任务接收到消息在串口回显\r\n");
//创建开始任务
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(); //开启任务调度
}
4.1.4 完整代码
4.2 计数信号量
4.2.1 应用任务创建
任务1函数:
实现LED灯的闪烁:
//任务1函数
void led1_task(void *pvParameters)
{
while(1)
{
LED1=0;
vTaskDelay(200);
LED1=1;
vTaskDelay(800);
}
}
接收任务函数:
用于接收消息变量,当按键1按下,释放一个空间(停车位):
//接收任务函数
void receive_task(void *pvParameters)
{
BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS,值为1 */
uint8_t KeyNum;
while(1)
{
KeyNum=Key_GetNum();
if(KeyNum==1)
{
xReturn = xSemaphoreTake( CountSem_Handle,0 );//获取计数信号量
if( xReturn == pdTRUE )
printf( "KEY1被按下,释放1个停车位。\r\n" );
else
printf( "KEY1被按下,但已无车位可以释放!\r\n" );
}
vTaskDelay(20);
}
}
发送任务函数:
用于发送消息,当按键2按下,申请占用一个空间(停车位):
//发送任务函数
void send_task(void *pvParameters)
{
BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
uint8_t KeyNum;
while(1)
{
KeyNum=Key_GetNum();
if(KeyNum==2)
{
xReturn = xSemaphoreGive( CountSem_Handle );//给出计数信号量
if( xReturn == pdTRUE )
printf("KEY_UP被按下,成功申请到停车位。\r\n");
else
printf("KEY_UP被按下,不好意思,现在停车场已满!\r\n");
}
vTaskDelay(20);
}
}
相关宏定义:
//任务优先级
#define LED1_TASK_PRIO 2
//任务堆栈大小
#define LED1_STK_SIZE 50
//任务句柄
TaskHandle_t LED1Task_Handler;
//任务函数
void led1_task(void *pvParameters);
//任务优先级
#define RECEIVE_TASK_PRIO 3
//任务堆栈大小
#define RECEIVE_STK_SIZE 50
//任务句柄
TaskHandle_t ReceiveTask_Handler;
//任务函数
void receive_task(void *pvParameters);
//任务优先级
#define SEND_TASK_PRIO 4
//任务堆栈大小
#define SEND_STK_SIZE 50
//任务句柄
TaskHandle_t SendTask_Handler;
//任务函数
void send_task(void *pvParameters);
4.1.2 开始任务的创建
之前讲过,开始任务的作用就是将,应用任务的三个任务完整的建立起来,通过调用临界区的API,规避中断带来的风险,创建完后删除自身,只进行应用任务的调度:
//开始任务任务函数
void start_task(void *pvParameters)
{
taskENTER_CRITICAL(); //进入临界区
/* 创建 CountSem */
CountSem_Handle = xSemaphoreCreateCounting(5,5);
//创建LED1任务
xTaskCreate((TaskFunction_t )led1_task,
(const char* )"led1_task",
(uint16_t )LED1_STK_SIZE,
(void* )NULL,
(UBaseType_t )LED1_TASK_PRIO,
(TaskHandle_t* )&LED1Task_Handler);
//创建接收任务
xTaskCreate((TaskFunction_t )receive_task,
(const char* )"receive_task",
(uint16_t )RECEIVE_STK_SIZE,
(void* )NULL,
(UBaseType_t )RECEIVE_TASK_PRIO,
(TaskHandle_t* )&ReceiveTask_Handler);
//创建发送任务
xTaskCreate((TaskFunction_t )send_task,
(const char* )"send_task",
(uint16_t )SEND_STK_SIZE,
(void* )NULL,
(UBaseType_t )SEND_TASK_PRIO,
(TaskHandle_t* )&SendTask_Handler);
vTaskDelete(StartTask_Handler); //删除开始任务
taskEXIT_CRITICAL(); //退出临界区
}
消息队列的创建以及开始任务相关宏定义:
//任务优先级
#define START_TASK_PRIO 1
//任务堆栈大小
#define START_STK_SIZE 128
//任务句柄
TaskHandle_t StartTask_Handler;
//任务函数
void start_task(void *pvParameters);
SemaphoreHandle_t CountSem_Handle =NULL;
4.1.3 主函数
创建开始任务开始调用:
int main()
{
SysTick_Init(72);
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);//设置系统中断优先级分组4
LED_Init();
KEY_Init();
USART1_Init(115200);
printf("FreeRTOS计数信号量实验\r\n");
printf("车位默认值为5个,按下KEY_UP申请车位,按下KEY1释放车位\r\n");
//创建开始任务
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(); //开启任务调度
}
4.1.4 完整代码
标签:STM32F103,task,函数,FreeRTOS,队列,void,信号量,任务 From: https://blog.csdn.net/MANONGDKY/article/details/141340291