FreeRTOS 二进制信号量

二进制信号量的基本用法

信号量分为:二进制信号量、计数信号量、互斥锁、递归互斥锁

二进制信号量:用于任务与任务,或者任务与中断之间同步
同步就是通过发信号,让本来不相关的两个进程联动起来。

二进制信号量的四种基本操作:

创建二进制信号量删除信号量给信号取信号
xSemaphoreCreateBinary()vSemaphoreDelete()xSemaphoreGive()xSemaphoreTake()

创建二进制信号量

123
//作用:创建二进制信号量//返回:新创建的二进制信号量的句柄,创建失败时返回NULLSemaphoreHandle_t xSemaphoreCreateBinary(void);

删除信号量

123
//作用:删除信号量,比如二进制信号量、计数信号量、互斥锁、递归互斥锁//参数:xSemaphore - 要被删除的信号量的句柄void vSemaphoreDelete(SemaphoreHandle_t xSemaphore);

给信号

1234
//作用:给信号。适用于所有的信号量类型//参数:xSemaphore - 信号量的句柄//返回:pdPASS - 成功,pdFAIL - 失败BaseType_t xSemaphoreGive(SemaphoreHandle_t xSemaphore);

取信号

12345
//作用:取信号。适用于所有信号量。//参数:xSemaphore - 信号量的句柄//参数:xTicksToWait - 最长等待时间,portMAX_DELAY - 表示无限等待//返回:pdPASS - 成功取到信号,pdFAIL - 因为超时失败BaseType_t xSemaphoreTake(SemaphoreHandle_t xSemaphore,TickType_t xTicksToWait);

使用二进制信号量检测按钮是否被按下

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647
#include "semphr.h" /* USER CODE BEGIN PV */SemaphoreHandle_t xSendSemphr;//声明一个全局信号量/* USER CODE END PV */ /* Private user code ---------------------------------------------------------*//* USER CODE BEGIN 0 */void print(const char * str){	vTaskSuspendAll();//暂停调度器,避免切换到其他任务导致交叉	HAL_UART_Transmit(&huart1, (const uint8_t *)str, strlen(str), HAL_MAX_DELAY);	xTaskResumeAll();//恢复调度器} void vButtonTask(void *pvParameters){	GPIO_PinState previous = GPIO_PIN_SET;	GPIO_PinState current = GPIO_PIN_SET;//类型是GPIO_PinState,先都赋值成GPIO_PIN_SET	for(;;)	{		current = HAL_GPIO_ReadPin(KEY1_GPIO_Port, KEY1_Pin);//读KEY1引脚的值		if(previous == GPIO_PIN_RESET && current == GPIO_PIN_SET)//按钮被点击,即上一次为0,这一次为1		{			//检测到按钮点击的动作,给信号			xSemaphoreGive(xSendSemphr);		}		previous = current;//把本次的状态给到上一次,用于下次判断使用。		vTaskDelay(pdMS_TO_TICKS(10));//延迟10ms	}} void vSendTask(void *pvParameters){	print("等待按钮按下\n");	xSemaphoreTake(xSendSemphr,portMAX_DELAY);//取信号,如果按钮一直没有按下,任务阻塞在这里	print("按钮被按下\n");//按钮被按下,发送消息	vTaskDelete(NULL);//删除任务自身}/* USER CODE END 0 */ /* USER CODE BEGIN 2 */xSendSemphr = xSemaphoreCreateBinary();//创建信号量xTaskCreate( vButtonTask, "Button", 128, NULL, 2 , NULL );xTaskCreate( vSendTask, "Send", 128, NULL, 1 , NULL );vTaskStartScheduler();//启用调度器/* USER CODE END 2 */

实验现象:
串口发送现象

如果取信号时,只希望等待有限的时间,而不是无限等待,比如等待5s:

123456789101112131415
void vSendTask(void *pvParameters){	BaseType_t xReturn;//用来接收返回值	print("等待按钮按下\n");	xReturn = xSemaphoreTake(xSendSemphr,pdMS_TO_TICKS(5000));//取信号	if(xReturn == pdPASS)	{		print("按钮被按下\n");//按钮被按下,发送消息	}	else	{		print("按钮超时\n");	}	vTaskDelete(NULL);//删除任务自身}

实验现象:
按钮超时串口现象

二进制信号量的用法:按钮音效

1234567891011121314151617
void vButtonTask(void *pvParameters){	GPIO_PinState previous = GPIO_PIN_SET;	GPIO_PinState current = GPIO_PIN_SET;//类型是GPIO_PinState,先都赋值成GPIO_PIN_SET	for(;;)	{		current = HAL_GPIO_ReadPin(KEY1_GPIO_Port, KEY1_Pin);//读KEY1引脚的值		if(previous == GPIO_PIN_RESET && current == GPIO_PIN_SET)//按钮被点击,即上一次为0,这一次为1		{			HAL_GPIO_WritePin(BUZZER_GPIO_Port, BUZZER_Pin, GPIO_PIN_SET);//高电压,蜂鸣器鸣叫			vTaskDelay(pdMS_TO_TICKS(100));//延迟100ms			HAL_GPIO_WritePin(BUZZER_GPIO_Port, BUZZER_Pin, GPIO_PIN_RESET);//低电压,蜂鸣器不鸣叫		}		previous = current;//把本次的状态给到上一次,用于下次判断使用。		vTaskDelay(pdMS_TO_TICKS(10));//延迟10ms	}}

上方的代码无法正常运行,因为在蜂鸣器延迟的100ms过程中,按钮程序进入阻塞,按钮完全失效。
参考时序图:
上方代码的时序图

因此蜂鸣器鸣叫的代码和按钮代码,应该要拆开来。

在User -> Task目录下,新建Buzzer_Task.c和Buzzer_Task.h文件
Buzzer_Task.c

1234567891011121314151617181920212223242526
#include "FreeRTOS.h"#include "semphr.h"//二进制信号量使用#include "main.h" static SemaphoreHandle_t xBuzzerSemphr; void Buzzer_Init(){	xBuzzerSemphr = xSemaphoreCreateBinary();//创建一个二进制信号量	HAL_GPIO_WritePin(BUZZER_GPIO_Port, BUZZER_Pin, GPIO_PIN_SET);}void vBuzzerTask(void *pvParameters){	for(;;)	{		xSemaphoreTake(xBuzzerSemphr,portMAX_DELAY);//取信号		HAL_GPIO_WritePin(BUZZER_GPIO_Port, BUZZER_Pin, GPIO_PIN_RESET);		vTaskDelay(pdMS_TO_TICKS(100));		HAL_GPIO_WritePin(BUZZER_GPIO_Port, BUZZER_Pin, GPIO_PIN_SET);	} }void Buzzer_Beep(){	xSemaphoreGive(xBuzzerSemphr);//给信号}

main.c

1234567891011121314
/* Private includes ----------------------------------------------------------*//* USER CODE BEGIN Includes */#include "FreeRTOS.h"#include "task.h"#include "Key_Task.h"#include "Buzzer_Task.h"/* USER CODE END Includes */ /* USER CODE BEGIN 2 */Buzzer_Init();xTaskCreate( KeyTask, "Key", 128, NULL, 3, NULL );xTaskCreate( vBuzzerTask, "BUZZER", 32, NULL, 1, NULL );vTaskStartScheduler();//启用调度器/* USER CODE END 2 */

key1.c

12345678910111213141516171819202122
#include "key.h"#include <string.h>#include "Buzzer_Task.h"static KeyHandle_TypeDef hkey1;static void KEY1_ClickedCallback(void);void Key1_Init(void){hkey1.GPIO_Port = KEY1_GPIO_Port;hkey1.GPIO_Pin = KEY1_Pin;hkey1.ClickedCallback = KEY1_ClickedCallback;Key_Init(&hkey1);} void Key1_Scan(void){Key_Scan(&hkey1);} static void KEY1_ClickedCallback(void){Buzzer_Beep();//调用蜂鸣器响函数}

其他按键处理方法类似。

中断中使用二进制信号量

例:完成下面两个要求:
1、控制LED闪灯,200ms闪烁一次
2、使用中断方式编写KEY的程序,按下KEY蜂鸣器鸣叫五次。

首先需要确认,按钮接在PE4上,PE4对应的是EXTI4
GPIO配置
还要去NVIC打开这个中断。
NVIC打开中断

1234567891011121314151617181920212223242526272829
void vLEDTask(void *pvParameters){	for(;;)	{		HAL_GPIO_WritePin(LED1_GPIO_Port, LED1_Pin, GPIO_PIN_SET);		vTaskDelay(pdMS_TO_TICKS(100));		HAL_GPIO_WritePin(LED1_GPIO_Port, LED1_Pin, GPIO_PIN_RESET);		vTaskDelay(pdMS_TO_TICKS(100));	}} //中断回调函数,HAL库里EXTI的中断服务程序(ISR)void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin){	static uint32_t debounceTime = 0;	uint32_t now = HAL_GetTick();	if((GPIO_Pin == GPIO_PIN_4) && (now - debounceTime > 10))	{		for(int i = 0; i < 5; i++)//蜂鸣器鸣叫五次		{			//延迟一段时间,ISR里不能使用vTaskDelay或HAL_Delay			HAL_GPIO_WritePin(BUZZER_GPIO_Port, BUZZER_Pin, GPIO_PIN_RESET);			for(int j = 0; j < 300000; j++);			HAL_GPIO_WritePin(BUZZER_GPIO_Port, BUZZER_Pin, GPIO_PIN_SET);			for(int j = 0; j < 300000; j++);		}	}	debounceTime = now;}

使用上面的程序,在按下按钮蜂鸣器响时,停止闪灯了,这是因为闪灯程序被中断程序打断了。
因此在编写单片机程序时,应该保证中断程序足够短,因为如果中断程序执行时间太长,其他程序没有执行机会。
因此,上方的程序,应该使用二进制信号量,把蜂鸣器鸣叫的程序,拆分给另一个非中断程序去做。
时序图

此外,想要在中断中调用FreeRTOS的API,必须使用带FromISR后缀版本的。该版本是FreeRTOS专门为中断程序提供的版本。
比如我们之前使用的:xSemaphoreGive(xBuzzerSemphr);就不适合在中断中使用,会报错:

12345
/* This is not the interrupt safe version of the enter critical function so* assert() if it is being called from an interrupt context. Only API* functions that end in "FromISR" can be used in an interrupt. Only assert if* the critical nesting count is 1 to protect against recursive calls if the* assert function also uses a critical section. */
12345
//简介:xSemaphoreGive的FromISR版本,可以在中断上下文被调用//参数:xSemaphore - 要操作的信号量的句柄//参数:*pxHigherPriorityTaskWoken - 调用此API是否使得更高优先级的任务苏醒,如果有更高优先级任务苏醒,该参数会在API内部被设置为pdTRUE,否则不变//返回:pdPASS - 给信号成功;pdFAIL - 给信号失败BaseType_t xSemaphoreGiveFromISR( SemaphoreHandle_t xSemaphore, BaseType_t *pxHigherPriorityTaskWoken );

时序图

FromISR版本的API一般用法参考:

123456789101112
void ISR1(void){	BaseType_t xHigherPriorityTaskWoken = pdFALSE;//设置pdFALSE,表示最初没有高优先级任务苏醒	...	xxxxFromISR(..., &xHigherPriorityTaskWoken);//可以执行多个FreeRTOS的API	...	xxxxFromISR(..., &xHigherPriorityTaskWoken);//通过xHigherPriorityTaskWoken标记是否有高优先级任务苏醒,只标记不切换。	...	xxxxFromISR(..., &xHigherPriorityTaskWoken);	...	portYIELD_FROM_ISR(xHigherPriorityTaskWoken);//在ISR末尾,执行切换}

流程

因此,程序应该写为:

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657
#include "semphr.h" /* USER CODE BEGIN PV */SemaphoreHandle_t xBuzzerSemphr;//声明一个全局信号量/* USER CODE END PV */ /* USER CODE BEGIN 0 */void vLEDTask(void *pvParameters){	for(;;)	{		HAL_GPIO_WritePin(LED1_GPIO_Port, LED1_Pin, GPIO_PIN_SET);		vTaskDelay(pdMS_TO_TICKS(100));		HAL_GPIO_WritePin(LED1_GPIO_Port, LED1_Pin, GPIO_PIN_RESET);		vTaskDelay(pdMS_TO_TICKS(100));	}} //中断回调函数,HAL库里EXTI的中断服务程序(ISR)void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin){	static uint32_t debounceTime = 0; 	BaseType_t xHigherPriorityTaskWoken = pdFALSE;//设置pdFALSE,表示最初没有高优先级任务苏醒	uint32_t now = HAL_GetTick();	if((GPIO_Pin == GPIO_PIN_4) && (now - debounceTime > 10))	{		//xSemaphoreGive(xBuzzerSemphr);		xSemaphoreGiveFromISR(xBuzzerSemphr, &xHigherPriorityTaskWoken);	}	debounceTime = now;	portYIELD_FROM_ISR(xHigherPriorityTaskWoken);//在ISR末尾,执行切换} //蜂鸣器任务void vBuzzerTask(void *pvParameters){	for(;;)	{		xSemaphoreTake(xBuzzerSemphr, portMAX_DELAY);		for(int i = 0; i < 5; i++)		{			HAL_GPIO_WritePin(BUZZER_GPIO_Port, BUZZER_Pin, GPIO_PIN_RESET);			vTaskDelay(pdMS_TO_TICKS(150));			HAL_GPIO_WritePin(BUZZER_GPIO_Port, BUZZER_Pin, GPIO_PIN_SET);			vTaskDelay(pdMS_TO_TICKS(150));		}	}}/* USER CODE END 0 */ /* USER CODE BEGIN 2 */xBuzzerSemphr = xSemaphoreCreateBinary();//创建信号量xTaskCreate( vLEDTask, "LED", 32, NULL, 1 , NULL );xTaskCreate( vBuzzerTask, "Buzzer", 32, NULL, 1 , NULL );vTaskStartScheduler();//启用调度器/* USER CODE END 2 */

此外,还要配置EXTI的优先级,应该配置为5之后。
EXTI优先级设置
EXTI优先级只能配置为5之后,因为在FreeRTOS的配置文件中:
#define configMAX_SYSCALL_INTERRUPT_PRIORITY (5 << 4)
这是能调用FreeRTOS的API的最高优先级,配置成了(5 << 4),因此小于5的优先级,无法调用FreeRTOS的API。

🔍 ×