新版本高效跳闪-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 占用、实时性、功耗、代码复杂度等方面差异很大。下面按典型维度对照:
  1. CPU 占用
    • ulTaskNotifyTake:任务 99 % 的时间处于 Blocked 状态,不占 CPU;只有中断来时才被唤醒。
    • 轮询:任务必须不停地 digitalRead(),即使按钮没动作也 100 % 占满分配给它的 CPU 时间片。
  2. 实时性(延迟)
    • ulTaskNotifyTake:中断里调用 vTaskNotifyGiveFromISRportYIELD_FROM_ISR,按键事件可立即抢占当前任务,延迟≈几 µs 级。
    • 轮询:受限于轮询周期,最坏情况下要等一个周期才能发现跳变,延迟不可控。
  3. 功耗(对电池设备重要)
    • ulTaskNotifyTake:空闲时 CPU 可以进入 light-sleep / deep-sleep,电流降到 mA 甚至 µA 级。
    • 轮询:CPU 永远跑在最高频率,功耗大。
  4. 抖动/抗干扰
    • ulTaskNotifyTake:唤醒后仍可在任务里做软件消抖,逻辑集中,易维护。
    • 轮询:为了消抖通常也得在轮询里延时,CPU 被白白占着。
  5. 代码复杂度与可维护性
    • ulTaskNotifyTake:需要中断、任务、临界区、FreeRTOS API,初学者略高;但结构清晰,职责分明。
    • 轮询:代码量小,但 loop() 里一堆 delay() 会阻塞其它逻辑,后期功能一多就难维护。
  6. 多任务/实时系统相容性
    • ulTaskNotifyTake:与 FreeRTOS 调度器完全合拍,不会饿死其它任务。
    • 轮询:如果优先级设置不当,会把同优先级或更低优先级的任务饿死;需要手动 yield()/delay() 缓解。
  7. 中断丢失风险
    • ulTaskNotifyTake:如果中断频率远高于任务处理能力,可能丢通知(因为通知值只有 1 bit)。这种情况可以用队列/计数信号量改进。
    • 轮询:不存在“丢中断”,但高频率事件可能被漏采样,取决于轮询周期。
一句话总结
在带操作系统的场景下,“中断 + 任务通知”几乎在所有指标上都优于纯轮询;轮询只在极简裸机程序(无 RTOS、事件极少、功耗不敏感)时才显得简单够用。

全部评论

·