巧用 FreeRTOS 任务通知作“邮箱”:NeoPixel 灯环控制实战

巧用 FreeRTOS 任务通知作"邮箱":NeoPixel 灯环控制实战

本文通过一个生动的 ESP32 示例,演示如何将 FreeRTOS 的任务通知(Direct Task Notification)当作轻量级"邮箱" ,实现 4 个 uint8_t 数据的打包、传递与拆解,从而灵活控制 NeoPixel 灯环的颜色与流动速度。


一、代码与逐行注释

cpp 复制代码
/*
   程序:  Direct Task Notification 邮箱功能
   要点:
      如此复杂的电灯程序,只是大家演示一下
      巧用任务的通知Value当作邮箱功能
      
      如何将4个uint8_t的数据打包成uint32_t,
      然后通过任务通知传递过去,
      并且在目的地进行拆解的方法。

      写完整个程序,感觉和走私,偷渡一样
      
   公众号:孤独的二进制
*/

// 引入 Adafruit NeoPixel 库,用于驱动 WS2812 之类的可编程 LED
#include <Adafruit_NeoPixel.h>

// 定义 NeoPixel 数据引脚为 GPIO 33
#define NEOPIN 33
// 定义灯环上 LED 的数量为 16 颗
#define NUMPIXELS 16

// 定义红色电位器输入引脚为 GPIO 34
#define RPIN 34
// 定义绿色电位器输入引脚为 GPIO 35
#define GPIN 35
// 定义蓝色电位器输入引脚为 GPIO 32
#define BPIN 32
// 定义速度电位器输入引脚为 GPIO 36
#define SPIN 36 // speed

// 定义一个任务句柄,用于后续向 NeoPixel 控制任务发送通知
static TaskHandle_t xTaskNeoRing = NULL;

/**
 * @brief 配置任务:读取电位器,打包数据并发送通知
 * @param pvParam 任务创建时传入的参数(此处未使用)
 */
void configTask(void *pvParam) {
  // 配置红色通道引脚为输入模式
  pinMode(RPIN, INPUT);
  // 配置绿色通道引脚为输入模式
  pinMode(GPIN, INPUT);
  // 配置蓝色通道引脚为输入模式
  pinMode(BPIN, INPUT);
  // 配置速度调节引脚为输入模式
  pinMode(SPIN, INPUT);
  
  // 定义变量存储红色分量值 (0-255)
  uint8_t r_value = 0;
  // 定义变量存储绿色分量值 (0-255)
  uint8_t g_value = 0;
  // 定义变量存储蓝色分量值 (0-255)
  uint8_t b_value = 0;
  // 定义变量存储速度值 (默认100)
  uint8_t s_value = 100;

  // 任务主循环
  while (1) {
    // 读取红色电位器 ADC 值 (0-4095) 并映射为 PWM 亮度 (0-255)
    r_value = map(analogRead(RPIN), 0, 4095, 0, 255);
    // 读取绿色电位器 ADC 值并映射为 PWM 亮度
    g_value = map(analogRead(GPIN), 0, 4095, 0, 255);
    // 读取蓝色电位器 ADC 值并映射为 PWM 亮度
    b_value = map(analogRead(BPIN), 0, 4095, 0, 255);
    // 读取速度电位器 ADC 值并映射为延迟系数 (10-200)
    s_value = map(analogRead(SPIN), 0, 4095, 10, 200);

    // 【核心】数据打包:将 4 个 uint8_t 拼接成 1 个 uint32_t
    // 格式:[速度(8bit)][红(8bit)][绿(8bit)][蓝(8bit)]
    uint32_t rgb = s_value << 24 | r_value << 16 | g_value << 8 | b_value << 0;

    // 【核心】发送通知:将打包好的 rgb 作为通知值发送给 NeoPixel 任务
    // eSetValueWithOverwrite 表示覆盖上一次的值,确保总是最新数据
    xTaskNotify(xTaskNeoRing, rgb, eSetValueWithOverwrite);

    // 延时 100ms,避免过于频繁地读取和发送
    vTaskDelay(100);
  }
}

/**
 * @brief NeoPixel 控制任务:接收通知,拆解数据,驱动灯环
 * @param pvParam 任务创建时传入的参数(此处未使用)
 */
void neoRing(void *pvParam) {

  // 初始化 NeoPixel 对象:数量、引脚、灯珠类型 (GRB + 800KHz)
  Adafruit_NeoPixel pixels(NUMPIXELS, NEOPIN, NEO_GRB + NEO_KHZ800);
  // 初始化 NeoPixel
  pixels.begin();

  // 用于存储接收到的 32位 打包数据
  uint32_t srgb = 0;
  // 拆解后的红色分量
  uint8_t r = 0;
  // 拆解后的绿色分量
  uint8_t g = 0;
  // 拆解后的蓝色分量
  uint8_t b = 0;
  // 拆解后的速度分量
  uint8_t s = 100;

  // 任务主循环
  while (1) {

    // 清空所有灯珠的颜色缓存
    pixels.clear();
    // 依次点亮每一颗灯珠
    for (int i = 0; i < NUMPIXELS; i++) {

      // 【核心】等待通知:接收来自 configTask 的数据
      // 参数说明:入口时不清零掩码、出口时不清零掩码、存储接收值、超时为0(不阻塞,查看当前状态)
      xTaskNotifyWait(0x00, 0x00, &srgb, 0);
      
      // 【核心】数据拆解:从 32位 整数中还原出 4 个 8位 数据
      // 提取最高 8位 -> 速度
      s = (srgb & 0xff000000) >> 24;
      // 提取次高 8位 -> 红色
      r = (srgb & 0x00ff0000) >> 16;
      // 提取次低 8位 -> 绿色
      g = (srgb & 0x0000ff00) >> 8;
      // 提取最低 8位 -> 蓝色
      b = (srgb & 0x000000ff) >> 0;

      // 设置第 i 颗灯珠的颜色
      pixels.setPixelColor(i, pixels.Color(r, g, b));
      // 将颜色数据刷新到物理灯珠上显示
      pixels.show();
      // 根据速度值延时,控制灯环流动速度
      vTaskDelay(s * 5);
    }
  }
}

void setup() {
  // 初始化串口,波特率 115200(本例未实际使用串口)
  Serial.begin(115200);

  // 创建配置任务:任务函数、任务名、栈大小、参数、优先级、任务句柄(不需要)
  xTaskCreate(configTask, "Configuration", 1024 * 10, NULL, 1, NULL);
  // 创建 NeoPixel 控制任务:并保存其任务句柄到 xTaskNeoRing,以便发送通知
  xTaskCreate(neoRing, "Neo Ring", 1024 * 20, NULL, 1, &xTaskNeoRing);

  // setup() 本身也是一个任务,做完初始化后删除自己,节省资源
  vTaskDelete(NULL); //没我啥事了,自宫ba
}

// loop() 留空,因为所有逻辑都在 FreeRTOS 任务中完成,Arduino 主循环不再需要
void loop() {
}

二、程序要点总结

1. 为什么用任务通知当"邮箱"?

FreeRTOS 的任务通知(Direct Task Notification)是一种轻量级、高效率的通信机制。

  • 速度快:比队列、信号量开销更小。
  • 内存省:不需要创建额外的队列对象。
  • 够简单 :本例中我们直接利用通知的 uint32_t Value 字段来"走私"数据。

2. 数据的"打包"与"拆解"

这是本例最核心的技巧。我们有 4 个 uint8_t (R, G, B, S),但任务通知一次只能传一个 uint32_t

  • 发送端(打包) :利用位运算<< 左移 和 | 按位或)将 4 个字节拼成 1 个 32位整数。
  • 接收端(拆解) :利用位掩码&)和右移>>)还原出每一个字节。

3. 程序流程

  1. configTask :每隔 100ms 读取 4 个电位器(模拟量输入),将其映射为 0-255 的值,打包成一个 uint32_t,通过 xTaskNotify 发送出去。
  2. neoRing :在循环中不断调用 xTaskNotifyWait 获取最新数据,拆解出 RGB 颜色和速度,让灯珠一颗一颗亮起来,形成流动效果。

三、总结

这个例子虽然是"用大炮打蚊子"般复杂的点灯程序,但非常直观地展示了:

  1. 位操作的艺术:如何在嵌入式中紧凑地传递数据。
  2. FreeRTOS 任务通知的妙用:不仅能当信号量,还能当一个简单的"邮箱"或"消息队列"。

如果你手头有 ESP32 和 NeoPixel 灯环,不妨烧录进去,拧拧电位器,感受一下这种"走私数据"的乐趣!

相关推荐
老四啊laosi2 小时前
[双指针] 4. 力扣--盛最多水的容器
算法·leetcode·装水最多的容器
wanderist.2 小时前
高维矩阵的压维存储和高维差分
c++·算法·蓝桥杯
茶底世界之下2 小时前
Harbeth:高性能Metal图像处理库,让你的图片处理速度飞起来!
前端·github·swift
东离与糖宝2 小时前
Java 26 FFM API进阶:零JNI调用TensorRT/OpenVINO,AI端到端延迟砍半
java·人工智能
红云梦2 小时前
互联网三高-高性能之线程池与连接池调优
java·线程池·连接池·池化技术
wangfpp2 小时前
Pretext 如何颠覆前端文本布局
前端
瑶山2 小时前
SpringBoot + MongoDB 5分钟快速集成:从0到1实操指南
java·数据库·spring boot·后端·mongodb
迈巴赫车主2 小时前
蓝桥杯192.等差数列java
java·数据结构·算法·职场和发展·蓝桥杯
从文处安2 小时前
「前端何去何从」AI 把开发变快之后:Monorepo 与 Turborepo 如何接住被放大的工程复杂度
前端·人工智能