巧用 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_tValue 字段来"走私"数据。
2. 数据的"打包"与"拆解"
这是本例最核心的技巧。我们有 4 个 uint8_t (R, G, B, S),但任务通知一次只能传一个 uint32_t。
- 发送端(打包) :利用位运算 (
<<左移 和|按位或)将 4 个字节拼成 1 个 32位整数。 - 接收端(拆解) :利用位掩码 (
&)和右移 (>>)还原出每一个字节。
3. 程序流程
configTask:每隔 100ms 读取 4 个电位器(模拟量输入),将其映射为 0-255 的值,打包成一个uint32_t,通过xTaskNotify发送出去。neoRing:在循环中不断调用xTaskNotifyWait获取最新数据,拆解出 RGB 颜色和速度,让灯珠一颗一颗亮起来,形成流动效果。
三、总结
这个例子虽然是"用大炮打蚊子"般复杂的点灯程序,但非常直观地展示了:
- 位操作的艺术:如何在嵌入式中紧凑地传递数据。
- FreeRTOS 任务通知的妙用:不仅能当信号量,还能当一个简单的"邮箱"或"消息队列"。
如果你手头有 ESP32 和 NeoPixel 灯环,不妨烧录进去,拧拧电位器,感受一下这种"走私数据"的乐趣!