目录

    • 引言
    • 使用SPI + DMA 方式实现思路分析
      • 1. 查看WS2812的datasheet手册
      • 2. 根据官方的led_strip组件的方式,自己手把手实现一遍
      • 3.完整的程序(实现霓虹灯效果)

引言

  参考官方提供的led_strip组件使用 SPI + DMA 方式驱动WS2812 RGB灯的实现思路,只有明白实现的思路,方能将其移植到各个平台使用),至于官方提供的led_strip组件我就不在此分析了,大家可以通过终端输入 idf.py add-dependency "espressif/led_strip^2.0.0" 命令下载该组件源码。

使用SPI + DMA 方式实现思路分析

1. 查看WS2812的datasheet手册

  通过手册,了解如何驱动ws2812 RGB灯模块的。下面是我从手册中截取的内容:
在这里插入图片描述
划红线提到,ws2812支持多个级联,每个ws2812会截取24bit数据,其他数据会往下发送给下一级的ws2812,如此类推。这种情况datasheet手册中有提到,如下图所示:
在这里插入图片描述
而datasheet中提到关于每个bit数据0码和1码的时序波形要求,如下图所示:
在这里插入图片描述

2. 根据官方的led_strip组件的方式,自己手把手实现一遍

  首先需要知道,官方提供的led_strip组件使用的SPI频率为2.5MHz,也是每个bit占用的时间是400ns=0.4us,而led_strip组件是使用3个SPI的bit数据表示一个ws2812的bit数据的,也就是说led_strip组件发送一个ws2812的bit数据的时间是3个SPI bit的时间(3x0.4us=1.2us),可以看出led_strip组件发送的一个ws2812的bit数据的时间并不满足>=1.25us,其实大家不要太在意这个,使用3个SPI bit表示一个ws2812的bit数据是经过大量测试验证,是可行、可靠的。如果大家觉得担心,是可以用一个字节的SPI数据来表示一个ws2812的bit数据的,那么8 x (spi clock) >= 1.25 , 也就是spi clock >=0.156us SPI时钟频率带6.4MHz才可行。
  废话不多说,下面是我使用官方提供的led_strip组件使用的SPI频率为2.5MHz方式画的发送一个GRB数据的时序波形图:
在这里插入图片描述
  通过我提供的时序波形图,大家也应该对使用2.5MHz,3个SPI表示一个ws2812的bit数据有清晰的认识了,ws2812的1码通过SPI发送3个bit数据110表示,而ws2812的0码通过SPI发送3个bit数据100表示;也可知道一个ws2812的GRB数据有三个字节,每个字节表示一种颜色,而一种也是需要3字节的SPI数据去实现。那么怎么将这3字节的数据转成将要发送的SPI数据呢?实现代码如下:
在这里插入图片描述
上图的程序已经有很详细的注解了,这里就不再讲解了。

3.完整的程序(实现霓虹灯效果)

/*#######################################################################################################*/
/*                      start   使用ESP32S3 SPI的API函数做的WS2812 LED灯带驱动程序)                      */
/*#######################################################################################################*/
/**
 * @file spi_ws2812.c
 * @brief 使用SPI控制WS2812 LED的ESP32-S3优化实现
 * @note 基于ESP-IDF v5.1 开发,硬件平台:ESP32-S3
 */
#include "driver/spi_master.h"
#include "soc/spi_periph.h"
#include "hal/spi_hal.h"
#include "driver/gpio.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

/* 宏定义 ----------------------------------------------------------------*/
//   #define TAG "WS2812"                                // 日志标签
#define STRIP_LED_GPIO_PIN       48                 // WS2812数据引脚(必须支持SPI MOSI功能)
#define STRIP_LED_NUMBERS        1                  // LED数量
#define SPI_HOST                 SPI2_HOST          // 使用的SPI控制器(ESP32-S3 SPI2支持高速传输)
#define SPI_CLOCK_SPEED_HZ       (2.5 * 1000 * 1000)// SPI时钟频率(需匹配WS2812时序要求)
#define SPI_DMA_CHANNEL          SPI_DMA_CH_AUTO    // 自动选择DMA通道
#define SPI_TRANS_QUEUE_SIZE     4                  // SPI传输队列深度
#define BYTES_PER_PIXEL          3                  // 每个像素的字节数(GRB格式)
#define BITS_PER_COLOR_BIT       3                  // 每个颜色位对应的SPI数据位数

/* 全局变量 --------------------------------------------------------------*/
static uint8_t *pixel_buf = NULL;                   // LED数据缓冲区(DMA要求内存对齐)
static spi_device_handle_t spi_device;              // SPI设备句柄

/* 函数声明 --------------------------------------------------------------*/
static void ws2812_encode_color(uint8_t color, uint8_t *buffer);

/**
 * @brief SPI控制器初始化
 * @note 配置SPI总线参数并初始化DMA传输
 */
void ws2812_spi_init(void)
{
    /* 内存分配 ----------------------------------------------------------*/
    // 计算缓冲区总大小:LED数量 × 每像素字节数 × 每字节SPI数据量
    const size_t buf_size = STRIP_LED_NUMBERS * BYTES_PER_PIXEL * BITS_PER_COLOR_BIT;
    
    // 分配DMA兼容内存(必须使用内部SRAM)
    pixel_buf = heap_caps_calloc(1, buf_size, MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL);
    if (!pixel_buf) {
        ESP_LOGE(TAG, "DMA内存分配失败!请求大小:%d字节", buf_size);
        return;
    }

    /* SPI总线配置 -------------------------------------------------------*/
    spi_bus_config_t bus_cfg = {
        .mosi_io_num = STRIP_LED_GPIO_PIN,  // MOSI引脚连接WS2812 DIN
        .miso_io_num = -1,                  // 禁用MISO
        .sclk_io_num = -1,                  // 禁用SCLK(仅MOSI输出)
        .quadwp_io_num = -1,                // 禁用QSPI WP
        .quadhd_io_num = -1,                // 禁用QSPI HD
        .max_transfer_sz = buf_size,        // 最大传输长度匹配缓冲区
        .flags = SPICOMMON_BUSFLAG_MASTER,  // 主模式
    };

    // 初始化SPI总线
    esp_err_t ret = spi_bus_initialize(SPI_HOST, &bus_cfg, SPI_DMA_CHANNEL);
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "SPI总线初始化失败!错误码:0x%x", ret);
        heap_caps_free(pixel_buf);
        return;
    }

    /* SPI设备配置 -------------------------------------------------------*/
    spi_device_interface_config_t dev_cfg = {
        .clock_speed_hz = SPI_CLOCK_SPEED_HZ,   // 2.5MHz时钟(对应400ns周期)
        .mode = 0,                              // SPI模式0(CPOL=0, CPHA=0)
        .spics_io_num = -1,                     // 禁用CS引脚
        .queue_size = SPI_TRANS_QUEUE_SIZE,     // 传输队列深度
        .flags = SPI_DEVICE_NO_DUMMY,           // 无虚位数据
    };

    // 添加SPI设备
    ret = spi_bus_add_device(SPI_HOST, &dev_cfg, &spi_device);
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "SPI设备添加失败!错误码:0x%x", ret);
        spi_bus_free(SPI_HOST);
        heap_caps_free(pixel_buf);
        return;
    }

    /* 信号反向配置(根据硬件连接需要)--- 什么意思呢?简单来说原本设置输出颜色是纯绿色的,如果反向过来就是纯红色加纯蓝色的混合颜色了*/
    // 部分WS2812需要反向信号,通过GPIO矩阵配置
    // esp_rom_gpio_connect_out_signal(STRIP_LED_GPIO_PIN, 
    //                               spi_periph_signal[SPI_HOST].spid_out, 
    //                               true,  // 使能信号反向
    //                               false);

    ESP_LOGI(TAG, "SPI初始化完成,时钟频率:%.1fMHz", 
           (float)SPI_CLOCK_SPEED_HZ / 1e6);
}

/**
 * @brief 将单个颜色字节编码为3字节SPI数据
 * @param color 颜色值(0-255)
 * @param buffer 目标缓冲区(需3字节空间)
 * 
 * 编码规则:
 * - 每个颜色bit扩展为3个SPI bit
 * - 0 → 100 (0x4)
 * - 1 → 110 (0x6)
 * 
 * 示例:
 * color=0xFF(二进制11111111) → 
 * 编码后:0x66 0x66 0x66(每个bit转为0x6)
 */
static void ws2812_encode_color(uint8_t color, uint8_t *buffer)
{
    uint32_t packed = 0;
    
    // 从最高位开始处理(WS2812要求MSB优先)
    for(int i = 7; i >= 0; i--) {
        uint8_t bit = (color >> i) & 0x01;
        // 每个颜色bit打包为3个SPI bit
        packed |= (bit ? 0x06 : 0x04) << (3 * i);
    }

    // 将24位数据拆分为3个字节
    buffer[0] = (packed >> 16) & 0xFF;  // 高8位
    buffer[1] = (packed >> 8)  & 0xFF;  // 中8位
    buffer[2] = packed & 0xFF;          // 低8位
    // ESP_LOGI(TAG, "packed=%#X   [0]%#X   [1]%#X  [2]%#X", packed, buffer[0], buffer[1], buffer[2]);
}


/**
 * @brief 设置单个LED颜色
 * @param index LED索引(0开始)
 * @param green 绿色分量(0-255)
 * @param red   红色分量(0-255)
 * @param blue  蓝色分量(0-255)
 * @note WS2812使用GRB颜色顺序
 */
void ws2812_set_one_pixel(uint32_t index, uint8_t green, uint8_t red, uint8_t blue)
{
    // 计算数据起始位置
    uint32_t offset = index * BYTES_PER_PIXEL * BITS_PER_COLOR_BIT;
    
    // 清空旧数据(可选,根据是否需要保留之前的数据)
    memset(pixel_buf + offset, 0, BYTES_PER_PIXEL * BITS_PER_COLOR_BIT);

    // 编码各颜色分量
    ws2812_encode_color(green, &pixel_buf[offset + 0]);  // G分量
    ws2812_encode_color(red,   &pixel_buf[offset + 3]);  // R分量
    ws2812_encode_color(blue,  &pixel_buf[offset + 6]);  // B分量
}

/**
 * @brief 刷新所有LED显示
 * @note 发送数据后需要至少50μs的低电平作为复位信号
 */
void ws2812_all_pixel_refresh(void)
{
    spi_transaction_t trans = {
        .length = STRIP_LED_NUMBERS * BYTES_PER_PIXEL * BITS_PER_COLOR_BIT * 8, // 总bit数
        .tx_buffer = pixel_buf,
        .rx_buffer = NULL,
    };

    // 提交传输请求
    esp_err_t ret = spi_device_transmit(spi_device, &trans);
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "SPI传输失败!错误码:0x%x", ret);
    }

    // 必须的复位延时(>50μs)
    // esp_rom_delay_us(60);    // 因为是创建的任务线程里面跑,任务内已经有任务颜色函数,一般都说1us起步的
}

/**
 * @brief 清除所有LED显示
 */
void ws2812_all_pixel_clear(void)
{
    // 填充0数据
    memset(pixel_buf, 0, STRIP_LED_NUMBERS * BYTES_PER_PIXEL * BITS_PER_COLOR_BIT);
    
    // 立即刷新显示
    ws2812_all_pixel_refresh();
}


/**
 * @brief 将HSV颜色空间转换为RGB颜色空间(优化版,纯整数运算)
 * @param h 色相(0-359度),输入时会被自动归一化
 * @param s 饱和度(0-255)
 * @param v 亮度(0-255)
 * @param r 红色分量输出指针(0-255)
 * @param g 绿色分量输出指针(0-255)
 * @param b 蓝色分量输出指针(0-255)
 * @note 优化点:消除浮点运算,避免数据溢出,处理负数色相
 */
void hsv2rgb(int h, uint8_t s, uint8_t v, uint8_t *r, uint8_t *g, uint8_t *b)
{
    /*----------- 色相归一化 -----------*/
    h %= 360;                    // 初步归一化到-359~359
    if (h < 0) h += 360;         // 处理负数,确保h在0-359范围内

    /*----------- 色相分区计算 -----------*/
    const int hh_int = h / 60;   // 色相区域编号(0-5)
    const int h_mod60 = h % 60;  // 色相在区域内的余数(0-59)

    /*----------- 中间变量计算 -----------*/
    // 类型提升防止溢出,所有中间计算使用32位无符号整数
    const uint32_t s_u32 = s;    // 饱和度扩展为32位
    const uint32_t v_u32 = v;    // 亮度扩展为32位

    // 计算p = v * (255 - s) / 255
    const uint8_t p = (v_u32 * (255 - s)) / 255;

    // 计算q和t的分子部分(基于整数运算的公式推导)
    const uint32_t q_num = v_u32 * (15300U - s_u32 * h_mod60);     // 15300 = 255 * 60
    const uint32_t t_num = v_u32 * (15300U - s_u32 * (60 - h_mod60));

    // 除以15300(等价于原式的除以255和浮点运算部分)
    const uint8_t q = q_num / 15300;
    const uint8_t t = t_num / 15300;

    /*----------- 根据色相区域分配RGB值 -----------*/
    switch (hh_int) {
        case 0: *r = v; *g = t; *b = p; break; // 红主导,绿过渡,蓝最低
        case 1: *r = q; *g = v; *b = p; break; // 绿主导,红过渡,蓝最低
        case 2: *r = p; *g = v; *b = t; break; // 绿主导,蓝过渡,红最低
        case 3: *r = p; *g = q; *b = v; break; // 蓝主导,绿过渡,红最低
        case 4: *r = t; *g = p; *b = v; break; // 蓝主导,红过渡,绿最低
        case 5: *r = v; *g = p; *b = q; break; // 红主导,蓝过渡,绿最低
        default: *r = *g = *b = 0;             // 异常情况(理论上不会触发)
    }
}

void app_main()
{
    // 彩虹渐变效果实现
    uint8_t hue = 0;  // 色相值(0-359)
    while(1)
    {
       for(int i = 0; i < 1; i++) {
           uint8_t red, green, blue;
           hsv2rgb(hue % 360, 255, 255, &red, &green, &blue); // 全饱和度和亮度
           ws2812_set_one_pixel(i, green, red, blue);
       }
       hue += 1;  // 色相递增步长(控制渐变速度)
       ws2812_all_pixel_refresh();
       vTaskDelay(pdMS_TO_TICKS(30)); // 30ms刷新间隔
    }
}
/*#######################################################################################################*/
/*                        end   使用ESP32S3 SPI的API函数做的WS2812 LED灯带驱动程序)                       */
/*#######################################################################################################*/

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐