SPI驱动WS2812全彩LED

接口通信格式

WS2812/WS2812B LED 使用 24 bit 数据调节 RGB 色彩, 每个 bit 都是通过(一个高电平 + 一个低电平)表示的.

 

 

根据手册

• 0 表示为一个短的(0.35 µs)高电平加一个长的(0.90 µs)低电平

• 1 表示为一个长的(0.90 µs)高电平加一个短的(0.35 µs)低电平

• 单个 bit 信号周期, 高低电平时长合计为 1.25 µs

• 发送超过 24 bit 信号后, 之前输入的信号会依次传递给串行的下一个 WS2812 LED

• 控制器发送数据前需要保持低电平超过 50 µs(又称为 RESET), 用于通知 WS2812 开始接收数据.

根据上面的信息, 对单颗LED发送数据, 需要的时间为
24 × 1.25 µ s + 50 µ s = 80 µ s 24 × 1.25 µs + 50 µs = 80 µs24×1.25µs+50µs=80µs
对于8颗LED, 需要的时间为
8 × 24 × 1.25 µ s + 50 µ s = 290 µ s 8 × 24 × 1.25 µs + 50 µs = 290 µs8×24×1.25µs+50µs=290µs

实际的通信时间间隔要求

当传输信号时, 高低电平时间间隔如果不符合手册要求, 差距较大时LED会不工作(不亮), 在间隔接近但是不完全满足时, LED会出现显示错乱, 色彩乱跳等.

Tim “cpldcpu” 做过一系列实验 https://cpldcpu.wordpress.com/2014/01/14/light_ws2812-library-v2-0-part-i-understanding-the-ws2812/ 验证过时间间隔的边界, 发现这些要求实际上相当宽松:

• 触发RESET只需要 9 µs (比手册要求的 50 µs 小很多)

• 一个 bit 的周期至少需要 1.25 µs, 但是不能超过 9 µs, 因为这样容易触发RESET

• 0 的高电平时间, 手册要求是 0.35 µs, 实际上可以短至 62.5 ns , 但是不能长于 0.50 µs

• 1 的高电平时间, 手册要求是 0.65 µs, 实际上长度可以几乎跨越整个 bit 周期, 但是不能短于 0.625 µs

SPI驱动时的bit数选择

对于输出固定长度的电平组合, SPI是最简单的方式. 可以使用 SPI, 通过控制其中的数据值与 WS2812 通信, 而时间间隔控制则需要通过控制 SPI 的时钟以及每次发送的 bit 数量实现, 根据Controlling WS2812(B) leds using STM32 HAL SPI 的计算, 通过对比多种 bit 数的时间要求, 发现使用 bit 数越多, 兼容性越好, MCU越容易实现. 因此可以使用默认的 8bit SPI 通信.

对于 PY32F002A/PY32F003/PY32F030, 因为最高频率是48MHz, 所以当SPI分频为8, 16时, 分别对应 6MHz, 3MHz, 在工作范围内; 对于 PY32F040/PY32F071/PY32F072, 最高频率是72MHz, 当SPI分频为8, 16, 32时, 分别对应 9MHz, 4.5MHz, 2.25MHz, 都在工作范围内.

设计思路

由于控制高低电平占比(ws2812 bit)的时间需要多个SPI bit,故SPI发送的长度肯定大于通信的内容,即用多个SPI bit去模拟一个ws2812 bit。常见的SPI传输是8bit,但也可配置为12、16等比特。但为了DMA搬运数据时方便对齐,故配置为8bit。

在SPI 8bit的数据长度下,我们采用2,4,8,12,16的SPI bit去模拟一个ws2812 bit。比如可以得出一下组合(实际上的ws2812时序并没有数据手册上那么严格,且不同厂家的配置不同):

用4个SPI bit去模拟,则用SPI bit表示ws2812 bit0为:1000;bit1可表示为1110。且一个SPI bit在250~420ns均可。
用8个SPI bit去模拟,则用SPI bit表示ws2812 bit0为:1100 0000;bit1可表示为1111 1100。且一个SPI bit在120~210ns均可。
当然不同比例的SPI bit可以调整出不同的SPI时间,具体根据SPI的配置时间。8个SPI bit能将时间调整得更精细,而4个SPI bit更省RAM。具体怎么配置还要和SPI的速度匹配。故我采用4个SPI bit模式。那么SPI的通信速率为2.4Mbps~4Mbps。

 实现

用4个SPI bit去模拟,则用SPI bit表示ws2812
bit0为:1000bit1可表示为1110。即

LOW=0x08

HIGH=0x0E

首先将SPI调整发送模式数据长度大小端分频系数CPHA

#define  WSLEDNUM    8                                                // 定义灯的数量
uint8_t  wsFillMap[4] = {0x88, 0x8E, 0xE8, 0xEE};                     // 这是一个哈希表 00=0x88 01=0x8E 10=0xE8 11=0xEE
uint8_t  ws2812Buffer[WSLEDNUM*12+2] = {0};                            // 数组缓冲区

void setOnePixRGB(uint8_t R, uint8_t G, uint8_t B, uint16_t index)
{
    uint8_t i;
    uint8_t *bufHead = ws2812Buffer + (12 * index);                    // 通过bufHead确定该像素的首地址,减少后面计算
    
    for (i = 0; i < 4; ++i)                                            // 每次位移两位,通过哈希表来填充缓存区(当然你可以写一个4位的哈希表,就不用位移了)。并同时为GRB赋值,减少循环次数。
    {
            bufHead[0+i] = wsFillMap[(G >> (6 - 2 * i)) & 0x03];
            bufHead[4+i] = wsFillMap[(R >> (6- 2 * i)) & 0x03];
            bufHead[8+i] = wsFillMap[(B >> (6 - 2 * i)) & 0x03];
    }
}

void flushWs2812(void)                                                // 刷新函数,多发两个低电平稳定电平(心里安慰,其实没用)
{
    HAL_SPI_Transmit_DMA(&hspi1, ws2812Buffer, WSLEDNUM*12+2);
}

这段代码实现了刷新一个灯的缓冲区,并通过哈希表减少运算量。最后通过DMA发送缓冲区数据即更新所有灯的状态。

注意,同时因为下拉的存在,所以不需要发送很多的0来发送RESET,但必须需保证两帧间隔大于文档中的RESET码时间。

 

posted @ 2025-06-16 16:45  Sean_hn  阅读(454)  评论(0)    收藏  举报