计数信号量
1. 今天必须记住的 3 句话
- 计数信号量 = N 个二值信号量,计数值可以累加(0~N)
- 核心作用:事件计数(按键按了几次)、资源管理(有几个车位可用)
- 和二值信号量的最大区别:二值信号量连续 Give 多次,值还是 1,会丢事件;计数信号量连续 Give 多次,值会累加,不会丢事件!
2. 最直观的类比:停车场车位模型
把计数信号量想象成有 N 个车位的停车场:
表格
| 停车场概念 | 对应计数信号量 API | 含义 |
|---|---|---|
| 停车场总共有 10 个车位 | xSemaphoreCreateCounting(10, 0) | 最大计数值 10,初始值 0(初始时 0 个车位可用,或者反过来理解) |
| 一辆车开走,释放 1 个车位 | xSemaphoreGive() | 计数值 + 1(可用车位 + 1) |
| 一辆车开进来,占用 1 个车位 | xSemaphoreTake() | 计数值 - 1(可用车位 - 1) |
| 车位全满了,Take 会阻塞 | xSemaphoreTake(..., 阻塞时间) | 计数值为 0 时,Take 会等待,直到有车位释放 |
3. 今天的核心:2 个 API(比二值信号量多 1 个创建参数)
(1)创建计数信号量
SemaphoreHandle_t xSemaphoreCreateCounting( UBaseType_t uxMaxCount, // 【参数1】最大计数值(比如100,对应100个车位) UBaseType_t uxInitialCount // 【参数2】初始计数值(一般设为0) );- 返回值:成功返回句柄,失败返回
NULL(堆内存不足)。 - 对比二值信号量:二值信号量相当于
xSemaphoreCreateCounting(1, 0),最大计数值只能是 1。
(2)获取 / 释放信号量(和二值信号量完全一样)
- 任务中获取:
xSemaphoreTake(xSem, xBlockTime)(计数值 - 1) - 任务中释放:
xSemaphoreGive(xSem)(计数值 + 1) - 中断中释放:
xSemaphoreGiveFromISR(xSem, &xHigherPriorityTaskWoken)(计数值 + 1,必须配合portYIELD_FROM_ISR())
4. 今天的练习:按键计数(不会丢事件)
需求:
- 按键快速按下多次(比如 1 秒按 5 次)
- 中断里只发计数信号量(Give),计数值累加
- 任务里慢慢处理(Take),每次处理间隔 1 秒,不会丢任何一次按键事件
完整可复制代码
第一步:全局变量定义
/* USER CODE BEGIN PV */ SemaphoreHandle_t xKeyCountSem; // 按键计数用的计数信号量 SemaphoreHandle_t xUARTMutex; // 串口打印互斥锁(解决乱码) /* USER CODE END PV */第二步:任务函数(慢慢处理,不会丢事件)
/* USER CODE BEGIN 0 */ void Key_Count_Task(void *pvParameters) { uint32_t process_count = 0; printf("=== 按键任务启动 ===\r\n"); while(1) { // 调试打印:查看信号量累积情况 printf(">>> 当前信号量计数值:%d\r\n", (int)uxSemaphoreGetCount(xKeyCountSem)); // 等待按键触发的信号量 if(xSemaphoreTake(xKeyCountSem, portMAX_DELAY) == pdPASS) { // 2. 有效按键:计数+打印 process_count++; printf("任务处理第 %d 次按键事件\r\n", process_count); // 3. 消抖完成,解锁,响应下一次按键 key_lock = 0; // 4. 你的需求:打印后间隔1秒 vTaskDelay(3000); } } } /* USER CODE END 0 */第三步:main 函数里创建(顺序不能乱)
int main(void) { /* USER CODE BEGIN 1 */ /* USER CODE END 1 */ /* MCU Configuration--------------------------------------------------------*/ /* Reset of all peripherals, Initializes the Flash interface and the Systick. */ HAL_Init(); /* USER CODE BEGIN Init */ /* USER CODE END Init */ /* Configure the system clock */ SystemClock_Config(); /* USER CODE BEGIN SysInit */ /* USER CODE END SysInit */ /* Initialize all configured peripherals */ MX_GPIO_Init(); MX_USART1_UART_Init(); /* USER CODE BEGIN 2 */ xUARTMutex = xSemaphoreCreateMutex(); // 创建互斥锁 xKeyCountSem = xSemaphoreCreateCounting(20, 0); xDataQueue = xQueueCreate(5, sizeof(uint32_t)); if(xTaskCreate(Key_Count_Task, "Key_Count_Task", 128 , NULL, 2, NULL) == pdPASS) { HAL_UART_Transmit(&huart1, (uint8_t *)"--- KEY_Task 创建成功 ---\r\n", strlen("--- KEY_Task 创建成功 ---\r\n"), 100); } else HAL_UART_Transmit(&huart1, (uint8_t *)"--- KEY_Task 创建失败 ---\r\n", strlen("--- KEY_Task 创建失败 ---\r\n"), 100); if(xTaskCreate(Key_Scan_Task, "Key_Scan_Task", 128 , NULL, 3, NULL) == pdPASS) { HAL_UART_Transmit(&huart1, (uint8_t *)"--- Key_Scan_Task 创建成功 ---\r\n", strlen("--- Key_Scan_Task 创建成功 ---\r\n"), 100); } else HAL_UART_Transmit(&huart1, (uint8_t *)"--- Key_Scan_Task 创建失败 ---\r\n", strlen("--- Key_Scan_Task 创建失败 ---\r\n"), 100); if(xKeyCountSem == NULL) { HAL_UART_Transmit(&huart1, (uint8_t *)" 计数信号量创建失败\r\n", strlen(" 计数信号量创建失败\r\n"), 100); } else HAL_UART_Transmit(&huart1, (uint8_t *)" 计数信号量创建成功\r\n", strlen(" 计数信号量创建成功\r\n"), 100); /* USER CODE END 2 */ /* Call init function for freertos objects (in cmsis_os2.c) */ MX_FREERTOS_Init(); /* Start scheduler */ osKernelStart(); /* We should never get here as control is now taken by the scheduler */ /* Infinite loop */ /* USER CODE BEGIN WHILE */ while (1) { /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ } /* USER CODE END 3 */ }第四步:按键扫描函数
void Key_Scan_Task(void *pvParameters) { uint8_t key_last_state = 1; // 上一次状态:默认高电平(上拉) uint8_t key_current_state; TickType_t key_press_tick = 0; // 记录按键按下的起始时间 const TickType_t long_press_time = pdMS_TO_TICKS(2000); // 长按阈值:2秒 printf("=== 按键扫描任务启动(短按<2s生效) ===\r\n"); while(1) { // 1. 读取当前按键电平 key_current_state = HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0); // ============================================== // 【检测按下:下降沿】记录按下的时间点 // ============================================== if(key_last_state == 1 && key_current_state == 0) { key_press_tick = xTaskGetTickCount(); // 保存按下时刻 } // ============================================== // 【检测松手:上升沿】判断按下时长,短按才生效 // ============================================== if(key_last_state == 0 && key_current_state == 1) { vTaskDelay(20); // 20ms消抖 // 再次确认松手 if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == 1) { // 计算按下总时长 TickType_t press_time = xTaskGetTickCount() - key_press_tick; //核心判断:按下时间 < 2秒 → 触发;≥2秒 → 忽略 if(press_time < long_press_time) { xSemaphoreGive(xKeyCountSem); // 短按有效,释放信号量 } else { printf("长按超过2秒,忽略触发\r\n"); // 可选调试打印 } } } // 更新状态 key_last_state = key_current_state; // 扫描周期10ms vTaskDelay(10); } }5. 今天必须搞懂的核心:为什么计数信号量不会丢事件?
对比二值信号量和计数信号量
场景:1 秒内快速按下 5 次按键
表格
| 类型 | 连续 Give 5 次后的计数值 | 任务 Take 的结果 | 结论 |
|---|---|---|---|
| 二值信号量 | 只能是 1(因为最大计数值是 1,连续 Give 多次值不会累加) | 任务只能 Take 1 次,剩下 4 次事件直接丢失 | ❌ 会丢事件 |
| 计数信号量 | 变成 5(连续 Give 5 次,值累加 5 次) | 任务可以 Take 5 次,每次处理 1 次,不会丢任何事件 | ✅ 不会丢事件 |
6. 运行效果(你会看到)
哪怕你按得很快,任务处理得很慢,计数信号量会把所有按键事件都记下来,任务会一个一个慢慢处理,绝对不会丢!
7. 今日总结(背下来)
- 计数信号量 = N 个二值信号量,计数值可以累加(0~N)
- 核心作用:事件计数(按键按了几次)、资源管理(有几个车位可用)
- 和二值信号量的最大区别:二值信号量连续 Give 多次会丢事件,计数信号量不会丢
- 固定模板必须背:中断里用
xSemaphoreGiveFromISR+portYIELD_FROM_ISR