二进制信号量的基本用法
信号量分为:二进制信号量、计数信号量、互斥锁、递归互斥锁
二进制信号量:用于任务与任务,或者任务与中断之间同步
同步就是通过发信号,让本来不相关的两个进程联动起来。
二进制信号量的四种基本操作:
| 创建二进制信号量 | 删除信号量 | 给信号 | 取信号 |
|---|---|---|---|
| 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 | /* 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 | 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 *//* 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 | 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
还要去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 | /* 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优先级只能配置为5之后,因为在FreeRTOS的配置文件中:#define configMAX_SYSCALL_INTERRUPT_PRIORITY (5 << 4)
这是能调用FreeRTOS的API的最高优先级,配置成了(5 << 4),因此小于5的优先级,无法调用FreeRTOS的API。