新版本高效跳闪-2之轮询与中断
74 2025-08-22 11:04
#include <Arduino.h>
#include <BleKeyboard.h>
#define PIN_BTN 4 // → GND
#define PIN_LED 16 // → LED
constexpr TickType_t REPEAT_RATE = pdMS_TO_TICKS(500); // 发送间隔
/*--------------------------------------------------*/
BleKeyboard bleKeyboard("21's Knob", "luowei", 88);
/*--------------------------------------------------*/
/* 中断服务程序 */
static TaskHandle_t hKeyTask = NULL;
static void IRAM_ATTR onBtnChange(void *)
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
if (hKeyTask != NULL)
vTaskNotifyGiveFromISR(hKeyTask, &xHigherPriorityTaskWoken); // 中断唤醒任务
portYIELD_FROM_ISR(xHigherPriorityTaskWoken); // 效果:把“按键事件”的响应延迟缩到最短,避免 ISR 结束后还要等系统时钟滴答才轮到 keyTask 运行。
}
/*--------------------------------------------------*/
/* 负责真正干活的任务,优先级高于 loop() */
void keyTask(void *)
{
for (;;)
{
/* 自己主动睡觉,等待中断唤醒 */
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
/* 抗尖刺:再读一次,确认仍是按下 */
if (digitalRead(PIN_BTN) == LOW)
{
/* —— 第一次按下 —— */
if (bleKeyboard.isConnected())
{
bleKeyboard.write(KEY_LEFT_CTRL);
vTaskDelay(pdMS_TO_TICKS(30));
bleKeyboard.print(" ");
}
digitalWrite(PIN_LED, HIGH);
vTaskDelay(REPEAT_RATE);
/* —— 连发循环 —— */
while (digitalRead(PIN_BTN) == LOW)
{
if (bleKeyboard.isConnected())
{
bleKeyboard.write(KEY_LEFT_CTRL);
vTaskDelay(pdMS_TO_TICKS(30));
bleKeyboard.print(" ");
}
vTaskDelay(REPEAT_RATE);
}
/* 松手后关灯 */
digitalWrite(PIN_LED, LOW);
}
}
}
/*--------------------------------------------------*/
void setup()
{
Serial.begin(115200);
pinMode(PIN_BTN, INPUT_PULLUP);
pinMode(PIN_LED, OUTPUT);
/* 1. 下落沿中断,唤醒 keyTask */
attachInterruptArg(PIN_BTN, onBtnChange, nullptr, FALLING);
/* 2. 创建高优先进程 */
xTaskCreatePinnedToCore(
keyTask,
"keyTask",
4096,
nullptr,
3, // 优先级高于 loop()
&hKeyTask,
1); // 跑在 Core 1,蓝牙跑在 Core 0,互不抢占
bleKeyboard.begin();
}
/*--------------------------------------------------*/
void loop()
{
/* 所有实时工作已放在 keyTask,loop() 空转即可 */
vTaskDelay(portMAX_DELAY);
}
ulTaskNotifyTake设计对比任务一直检测按钮高低电平有什么优缺点?
用
ulTaskNotifyTake
(“中断唤醒”)和让任务一直轮询(digitalRead
死循环检测)都能实现按钮响应,但它们在 CPU 占用、实时性、功耗、代码复杂度等方面差异很大。下面按典型维度对照:-
CPU 占用
• ulTaskNotifyTake:任务 99 % 的时间处于 Blocked 状态,不占 CPU;只有中断来时才被唤醒。
• 轮询:任务必须不停地digitalRead()
,即使按钮没动作也 100 % 占满分配给它的 CPU 时间片。 -
实时性(延迟)
• ulTaskNotifyTake:中断里调用vTaskNotifyGiveFromISR
并portYIELD_FROM_ISR
,按键事件可立即抢占当前任务,延迟≈几 µs 级。
• 轮询:受限于轮询周期,最坏情况下要等一个周期才能发现跳变,延迟不可控。 -
功耗(对电池设备重要)
• ulTaskNotifyTake:空闲时 CPU 可以进入 light-sleep / deep-sleep,电流降到 mA 甚至 µA 级。
• 轮询:CPU 永远跑在最高频率,功耗大。 -
抖动/抗干扰
• ulTaskNotifyTake:唤醒后仍可在任务里做软件消抖,逻辑集中,易维护。
• 轮询:为了消抖通常也得在轮询里延时,CPU 被白白占着。 -
代码复杂度与可维护性
• ulTaskNotifyTake:需要中断、任务、临界区、FreeRTOS API,初学者略高;但结构清晰,职责分明。
• 轮询:代码量小,但loop()
里一堆delay()
会阻塞其它逻辑,后期功能一多就难维护。 -
多任务/实时系统相容性
• ulTaskNotifyTake:与 FreeRTOS 调度器完全合拍,不会饿死其它任务。
• 轮询:如果优先级设置不当,会把同优先级或更低优先级的任务饿死;需要手动yield()
/delay()
缓解。 -
中断丢失风险
• ulTaskNotifyTake:如果中断频率远高于任务处理能力,可能丢通知(因为通知值只有 1 bit)。这种情况可以用队列/计数信号量改进。
• 轮询:不存在“丢中断”,但高频率事件可能被漏采样,取决于轮询周期。
一句话总结
在带操作系统的场景下,“中断 + 任务通知”几乎在所有指标上都优于纯轮询;轮询只在极简裸机程序(无 RTOS、事件极少、功耗不敏感)时才显得简单够用。
在带操作系统的场景下,“中断 + 任务通知”几乎在所有指标上都优于纯轮询;轮询只在极简裸机程序(无 RTOS、事件极少、功耗不敏感)时才显得简单够用。
全部评论