示例:RMT 驱动 WS2812 LED
本教程将介绍如何使用乐鑫 ESP-IDF 框架,通过 RMT 外设驱动 微雪 ESP32-S3-Zero 迷你开发板 板载 WS2812 可寻址 LED,实现灯珠点亮和颜色切换功能。
1. WS2812
1.1 WS2812 简介
WS2812 是一种集成了控制电路和发光电路的智能 RGB LED。它将一个 RGB LED 灯珠和一个控制芯片封装在一起,有时也被称为 "NeoPixel"。
特点:单数据线控制、可级联、内置 PWM 驱动、24 位色彩数据(8 位 R, 8 位 G, 8 位 B)。这意味着你可以用一根 GPIO 口控制多个 LED,并且独立设置每个灯珠的颜色和亮度。
1.2 工作原理
WS2812 的控制依赖于一种基于脉冲宽度的单线时序协议来传输数据。
-
数据传输:通过发送一连串高低电平脉冲来表示 "0" 码和 "1" 码。一个完整的周期大约为 1.25µs (TH+TL=1.25µs±600ns)。
参数 描述 时间 容差 T0H 0 码,高电平时间 0.4µs ±150ns T0L 0 码,低电平时间 0.85µs ±150ns T1H 1 码,高电平时间 0.8µs ±150ns T1L 1 码,低电平时间 0.45µs ±150ns RES 低电平时间 大于 50μs - "0" 码:一个较短的高电平(约 0.4µs)+ 一个较长的低电平(约 0.85µs)。
- "1" 码:一个较长的高电平(约 0.8µs)+ 一个较短的低电平(约 0.45µs)。
- RESET (RET) 码:一个持续较长时间(>50µs)的低电平。当数据线保持低电平超过 50µs 后,所有 LED 会锁定当前显示的颜色,并准备接收下一帧数据。
-
数据格式:
数据以 24 位序列发送,每种颜色占 8 位,高位在前,通常采用 GRB 序列,发送完成后需要至少 50µs 的低电平复位脉冲。
1.3 驱动方式
由于 WS2812 需要微秒级别甚至亚微秒级别的精确时序控制,软件 GPIO 无法保证可靠性(特别是运行 Wi-Fi、蓝牙或 FreeRTOS 时)。因此需要借助硬件外设生成精确时序信号,主要有 RMT 和 SPI 两种方案。
-
RMT (Remote Control Transceiver):
- 优点:资源占用经济,通常只消耗一个 RMT 通道。
- 缺点:内存占用会随 LED 数量增加而显著增长。在没有 DMA 辅助时会频繁进入中断,增加 CPU 负载。如果 RMT 中断被延迟(如与 Wi-Fi 中断冲突),数据传输可能被破坏。因此,如果驱动大量 LED,建议启用 DMA 功能。
-
SPI (Serial Peripheral Interface):
- 优点:通常性能更稳定,受中断影响较小,尤其是在驱动大量灯珠时。
- 缺点:资源占用不经济,会独占整个 SPI 总线。由于 WS2812 没有"片选"信号,该 SPI 总线上不能再挂载其他任何 SPI 设备。
2. RMT 外设
ESP 系列芯片的 红外遥控(RMT)外设 是一个灵活的信号发射与接收控制器。虽然最初设计用于红外遥控,但其“符号”机制允许开发者灵活生成任意脉冲序列,因此也常用于如 WS2812 灯带等对时序要求极高的应用。
-
RMT 符号
RMT 硬件定义了“RMT 符号”数据结构。每个符号包含两组“电平+持续时间”对,分别指定高/低电平及其持续的时钟周期数。这使得 RMT 能精确描述和输出复杂的脉冲时序。 -
协议编码
以 WS2812 协议为例,其“1”码(长高+短低)和“0”码(短高+长低)可分别编码为不同的 RMT 符号:- “1”码:
{ duration0: T1H, level0: 1, duration1: T1L, level1: 0 }
- “0”码:
{ duration0: T0H, level0: 1, duration1: T0L, level1: 0 }
这样,任意数据都能通过软件编码为 RMT 符号序列。
- “1”码:
-
数据转换与填充
发送数据时,RMT 编码器(Encoder)会将每一位数据转换为对应的 RMT 符号,并填充到 RMT 的内存缓冲区中。 -
硬件自动输出
启动 RMT 发送后,RMT 控制器会自动、精准地按照缓冲区中的符号序列,在指定 GPIO 引脚输出高低电平波形,实现对外设的时序控制,无需 CPU 参与,保证了时序的准确性。
3. led_strip 组件
为了简化开发,乐鑫官方提供了 espressif/led_strip 组件,这是专为驱动 WS2812 等可寻址 LED 设计的驱动库。该组件封装了底层 RMT 和 SPI 的复杂操作,开发者可通过简单易用的 API 控制 LED 灯带。
主要特性:
- 支持 RMT 和 SPI 作为后端驱动,适配多种硬件场景。
- 屏蔽硬件细节,开发者只需设置灯珠颜色,无需关心底层时序。
- 可通过 IDF 组件管理系统便捷集成到项目中。
如果你对底层实现感兴趣,可查看其 源码。
4. 示例项目
以下是一个完整的示例,演示如何使用 espressif/led_strip 组件并使用其 RMT 后端来点亮 微雪 ESP32-S3-Zero 迷你开发板 的板载 WS2812 LED,并让其在红、绿两色之间循环切换。
ESP32-S3-Zero 引脚图
4.1 创建项目
创建一个项目。如果不清楚如何操作,请参考 从模板创建项目。
4.2 添加 espressif/led_strip 组件到项目
-
搜索 "led_strip" 组件(espressif/led_strip)
-
复制右侧的指令:
idf.py add-dependency "espressif/led_strip^3.0.1~1"
-
点击
打开 ESP-IDF 终端,粘贴命令。
-
此外,还需要在代码中包含相应的头文件并调用组件文档和文件夹中提供的函数。详见代码部分。
4.3 示例代码
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "led_strip.h"
static const char *TAG = "example";
#define BLINK_GPIO 21 // LED 连接的 GPIO 引脚
static led_strip_handle_t led_strip; // LED 灯带句柄
static void configure_led(void)
{
ESP_LOGI(TAG, "Configuring addressable LED on GPIO %d", BLINK_GPIO);
// LED 灯带通用配置
led_strip_config_t strip_config = {
.strip_gpio_num = BLINK_GPIO, // 设置 GPIO 引脚
.max_leds = 1, // 设置 LED 数量
.color_component_format = LED_STRIP_COLOR_COMPONENT_FMT_RGB, // 设置颜色格式
};
// RMT 后端特定配置
led_strip_rmt_config_t rmt_config = {
.resolution_hz = 10 * 1000 * 1000, // RMT 分辨率,10MHz
.flags.with_dma = false, // 禁用 DMA
};
// 创建 LED 灯带对象
ESP_ERROR_CHECK(led_strip_new_rmt_device(&strip_config, &rmt_config, &led_strip));
led_strip_clear(led_strip); // 初始状态下清空灯带
}
void app_main(void)
{
configure_led(); // 配置 LED
while (1)
{
ESP_LOGI(TAG, "Set LED color to RED");
led_strip_set_pixel(led_strip, 0, 255, 0, 0); // 设置为红色
led_strip_refresh(led_strip); // 刷新灯带使颜色生效
vTaskDelay(pdMS_TO_TICKS(1000)); // 延时 1000 毫秒
ESP_LOGI(TAG, "Clear LED color");
led_strip_clear(led_strip); // 清空灯带,熄灭 LED
vTaskDelay(pdMS_TO_TICKS(1000)); // 延时 1000 毫秒
ESP_LOGI(TAG, "Set LED color to GREEN");
led_strip_set_pixel(led_strip, 0, 0, 255, 0); // 设置为绿色
led_strip_refresh(led_strip); // 刷新灯带使颜色生效
vTaskDelay(pdMS_TO_TICKS(1000)); // 延时 1000 毫秒
ESP_LOGI(TAG, "Clear LED color");
led_strip_clear(led_strip); // 清空灯带,熄灭 LED
vTaskDelay(pdMS_TO_TICKS(1000)); // 延时 1000 毫秒
}
}
4.4 构建并烧录
-
配置烧录选项
首先,在构建和烧录之前,请务必检查并设置正确的目标设备、串口和烧录方式。参考 第2节 运行示例 - 1.3 配置项目 。
-
点击
一键自动依次执行构建、烧录和监视这三个步骤。
-
烧录完成后,可以看到开发板上的 LED 开始闪烁。同时,串口监视器会启动并输出如下日志信息:
4.5 代码解析
1. 包含头文件
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "led_strip.h"
freertos/FreeRTOS.h
和freertos/task.h
: FreeRTOS 核心头文件,提供了任务管理功能,如本示例中用于延时的vTaskDelay
函数。esp_log.h
: ESP-IDF 的日志库,用于在终端打印信息,方便调试。led_strip.h
:led_strip
组件的核心头文件,提供了控制 WS2812 LED 的所有 API,如配置、创建、设置颜色和刷新等。
2. 定义全局变量和宏
static const char *TAG = "example";
#define BLINK_GPIO 21 // LED 连接的 GPIO 引脚
static led_strip_handle_t led_strip; // LED 灯带句柄
TAG
: 用于esp_log
日志输出的标签,方便识别日志来源。BLINK_GPIO
: 定义了连接 WS2812 数据线的 GPIO 引脚。微雪 ESP32-S3-Zero 迷你开发板 板载一颗 WS2812B LED,连接在 GPIO 21。led_strip_handle_t led_strip
: 定义一个 LED 灯带句柄。句柄(Handle)是一个指向内部驱动实例的“指针”或“引用”。在初始化后,所有对该灯带的操作(如设置颜色、刷新)都将通过这个句柄来完成。
3. 配置并创建 LED 灯带
这部分代码在 configure_led()
函数中完成,是驱动 WS2812 的关键初始化步骤。
// LED 灯带通用配置
led_strip_config_t strip_config = {
.strip_gpio_num = BLINK_GPIO,
.max_leds = 1,
.color_component_format = LED_STRIP_COLOR_COMPONENT_FMT_RGB,
};
// RMT 后端特定配置
led_strip_rmt_config_t rmt_config = {
.resolution_hz = 10 * 1000 * 1000, // 10MHz
.flags.with_dma = false,
};
// 创建 LED 灯带对象
ESP_ERROR_CHECK(led_strip_new_rmt_device(&strip_config, &rmt_config, &led_strip));
led_strip_clear(led_strip); // 初始状态下清空灯带
- 通用配置 (
led_strip_config_t
):strip_gpio_num
: 指定控制 LED 的 GPIO 引脚。max_leds
: 灯带上 LED 的总数。本示例中开发板只有一个板载 LED,因此设为 1。color_component_format
: 指定颜色分量的格式。LED_STRIP_COLOR_COMPONENT_FMT_RGB
表示颜色数据将按红、绿、蓝的顺序发送。led_strip
组件的默认格式就是LED_STRIP_COLOR_COMPONENT_FMT_GRB
,因此如果你的灯带是常见的 GRB 顺序,可以不设置此参数或直接删除此行,以使用默认配置。
- RMT 后端配置 (
led_strip_rmt_config_t
):resolution_hz
: 设置 RMT 通道的分辨率(时钟频率)。10 MHz 是驱动 WS2812 的常用值,它决定了 RMT 产生脉冲的精度。10MHz 的分辨率意味着每个时钟周期为 1/10,000,000 秒,即 100ns。flags.with_dma
: 是否启用 DMA(直接内存访问)。DMA 可以在不占用 CPU 的情况下传输数据,特别适合驱动大量 LED。由于本例只有一个 LED,数据量很小,因此禁用 DMA (false
) 以节省资源。
- 创建设备实例:
led_strip_new_rmt_device()
: 这是核心函数,它根据提供的通用配置和 RMT 配置来初始化 RMT 外设,并创建一个 LED 灯带驱动实例。成功后,led_strip
句柄将指向这个实例。
- 清空灯带:
led_strip_clear()
: 在初始化后调用此函数,将所有 LED 设置为关闭状态(颜色为 0, 0, 0),确保灯带初始状态是熄灭的。
4. 主循环:控制 LED 颜色
app_main
函数中的 while(1)
循环是程序的主体,负责循环改变 LED 的颜色。
while (1)
{
// 设置为红色
led_strip_set_pixel(led_strip, 0, 255, 0, 0);
led_strip_refresh(led_strip);
vTaskDelay(pdMS_TO_TICKS(1000));
// 熄灭 LED
led_strip_clear(led_strip);
vTaskDelay(pdMS_TO_TICKS(1000));
// ... (重复设置绿色等)
}
led_strip_set_pixel(led_strip, 0, 255, 0, 0)
:- 此函数用于设置单个像素点的颜色,但它只更新内存中的颜色缓冲区,并不会立即将颜色发送给 LED。
- 参数依次为:灯带句柄、LED 索引(从 0 开始)、红色分量、绿色分量、蓝色分量。
led_strip_refresh(led_strip)
:- 这个函数是真正执行数据发送的步骤。它会读取内存缓冲区中的颜色数据,通过 RMT 外设将其编码为精确的时序信号,并发送到 GPIO 引脚,从而更新物理 LED 的颜色。
led_strip_clear(led_strip)
:- 这是一个便捷函数,它会将缓冲区中所有 LED 的颜色设置为 (0, 0, 0),然后自动调用
led_strip_refresh()
来熄灭整个灯带。
- 这是一个便捷函数,它会将缓冲区中所有 LED 的颜色设置为 (0, 0, 0),然后自动调用
vTaskDelay(pdMS_TO_TICKS(1000))
:- FreeRTOS 的延时函数,使当前颜色保持显示 1000 毫秒,然后进入下一个状态。
pdMS_TO_TICKS()
是一个宏,用于将毫秒转换为 FreeRTOS 的系统节拍数(Ticks)。
- FreeRTOS 的延时函数,使当前颜色保持显示 1000 毫秒,然后进入下一个状态。