ESP32S3:参考官方提供的led_strip组件使用 SPI + DMA 方式驱动WS2812 RGB灯的实现思路 (实现各个平台移植使用该方式)
参考官方提供的led_strip组件使用 SPI + DMA 方式驱动WS2812 RGB灯的实现思路 (实现各个平台移植使用该方式)
目录
-
- 引言
- 使用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灯带驱动程序) */
/*#######################################################################################################*/
更多推荐



所有评论(0)