16. WS2812 RGB LED
📝本节您将学习 WS2812B 单总线 RGB LED 的通信协议和驱动方法,利用 PWM + DMA 实现高效的全彩灯光控制。
🏆本章目标
1️⃣ 理解 WS2812B 单总线通信协议的时序要求
2️⃣ 掌握 PWM + DMA 驱动 WS2812B 的实现原理
3️⃣ 实现单色控制和 RGB 颜色混合
4️⃣ 实现流水灯、呼吸灯等灯效
16.1 WS2812B 介绍
WS2812B 是一款集成了驱动电路的智能 RGB LED,单根数据线即可控制颜色。多颗 WS2812B 可级联,每颗独立寻址。
基本参数
| 参数 | 规格 |
|---|---|
| 工作电压 | 3.5~5.3V(推荐 5V) |
| 通信方式 | 单总线(NRZ 编码) |
| 颜色深度 | 24-bit(R8 + G8 + B8) |
| 数据速率 | 800kHz |
| 级联方式 | DOUT → 下一颗 DIN |
天巧星板载 4 颗 WS2812B,数据引脚为 PB26(TIMA1 CCP0)。
级联结构
MCU PB26 ──→ LED0 DIN → LED0 DOUT ──→ LED1 DIN → ... → LED3 DOUT每颗 LED 从数据流中截取前 24 bit 作为自己的颜色数据,将剩余数据传递给下一颗。
16.2 通信协议
WS2812B 使用 NRZ(Non-Return-to-Zero)编码,通过高电平持续时间区分 bit 0 和 bit 1:
| 编码 | 高电平时间(T_H) | 低电平时间(T_L) | 总周期 |
|---|---|---|---|
| 0 码 | 220~380ns | 580~1000ns | 1.25μs |
| 1 码 | 580~1000ns | 220~420ns | 1.25μs |
| Reset | — | >280μs | 数据锁存 |
简化理解(800kHz,周期 1.25μs = 1250ns):
- 0 码:高电平约 350ns + 低电平约 900ns
- 1 码:高电平约 700ns + 低电平约 550ns
- Reset:持续低电平超过 280μs,触发所有 LED 锁存显示
数据格式
每颗 LED 需要 24 bit 数据,顺序为 GRB(注意不是 RGB):
[G7 G6 G5 G4 G3 G2 G1 G0] [R7 R6 R5 R4 R3 R2 R1 R0] [B7 B6 B5 B4 B3 B2 B1 B0]
绿色 8-bit 红色 8-bit 蓝色 8-bit2
高位先发(MSB first)。
16.3 驱动方案:PWM + DMA
由于 WS2812B 的时序精度要求高(纳秒级),使用 CPU 软件模拟(位操作 + delay)在中断等不确定因素下容易出错。最可靠的方案是 PWM + DMA:
- 定时器(TIMA1) 配置为 800kHz PWM 输出(周期 1.25μs)
- DMA 自动从内存缓冲区逐个加载占空比值到定时器比较寄存器
- 占空比大 = 高电平时间长 = 1 码;占空比小 = 高电平时间短 = 0 码
时序参数(80MHz 主频)
定时器周期 = 80MHz / 800kHz = 100 ticks(实际出厂固件用 105)
| 编码 | 比较值(占空比) | 高电平时间 |
|---|---|---|
| 0 码 | 35 | 35/80MHz ≈ 437ns |
| 1 码 | 70 | 70/80MHz ≈ 875ns |
⚠️重要:需要 80MHz 主频
WS2812B 驱动需要精确的纳秒级时序。默认 32MHz 主频下定时器分辨率不足(每 tick 31.25ns),无法满足要求。
必须配置为 80MHz 主频(HFXT + PLL),参考第 3 章时钟配置。如果 CPUCLK_FREQ 为 32000000,WS2812B 将无法正常工作。
16.4 驱动实现
16.4.1 数据结构
#include "ti_msp_dl_config.h"
#include <string.h>
#define WS2812B_NUM 4 // LED 数量
#define WS2812B_BITS 24 // 每颗 LED 24 bit
#define WS2812B_RESET 50 // Reset 码长度(低电平周期数)
#define WS2812B_T0H 35 // 0 码占空比
#define WS2812B_T1H 70 // 1 码占空比
#define WS2812B_PERIOD 105 // PWM 周期
// DMA 缓冲区:每个 bit 对应一个 PWM 周期的占空比值
// 总长度 = LED数 × 24bit + Reset 周期数
#define DMA_BUF_SIZE (WS2812B_NUM * WS2812B_BITS + WS2812B_RESET)
static uint8_t ws2812_dma_buf[DMA_BUF_SIZE];
// RGB 颜色存储
typedef struct {
uint8_t r, g, b;
} WS2812_Color_t;
static WS2812_Color_t ws2812_colors[WS2812B_NUM];2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
16.4.2 颜色转换为 DMA 数据
// 将 RGB 颜色数组转换为 DMA 缓冲区
static void ws2812_update_buffer(void)
{
uint16_t idx = 0;
for (uint8_t led = 0; led < WS2812B_NUM; led++) {
// GRB 顺序,高位先发
uint8_t grb[3] = {
ws2812_colors[led].g,
ws2812_colors[led].r,
ws2812_colors[led].b
};
for (uint8_t byte = 0; byte < 3; byte++) {
for (int8_t bit = 7; bit >= 0; bit--) {
ws2812_dma_buf[idx++] = (grb[byte] & (1 << bit))
? WS2812B_T1H : WS2812B_T0H;
}
}
}
// Reset 段:全部为 0(低电平)
memset(&ws2812_dma_buf[idx], 0, WS2812B_RESET);
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
16.4.3 DMA 发送
// 启动一次 WS2812B 数据发送
void ws2812_send(void)
{
ws2812_update_buffer();
// 配置 DMA:从缓冲区 → 定时器比较寄存器
DL_DMA_setSrcAddr(DMA, WS2812_DMA_CH,
(uint32_t)&ws2812_dma_buf[0]);
DL_DMA_setDestAddr(DMA, WS2812_DMA_CH,
(uint32_t)&TIMA1->COUNTERREGS.CC_01[0]); // CCP0 比较寄存器
DL_DMA_setTransferSize(DMA, WS2812_DMA_CH, DMA_BUF_SIZE);
DL_DMA_enableChannel(DMA, WS2812_DMA_CH);
// 启动定时器
DL_TimerA_startCounter(TIMA1);
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
16.4.4 初始化
void ws2812_init(void)
{
// 默认全灭
memset(ws2812_colors, 0, sizeof(ws2812_colors));
memset(ws2812_dma_buf, 0, sizeof(ws2812_dma_buf));
}2
3
4
5
6
16.5 颜色控制 API
// 设置单颗 LED 颜色
void ws2812_set_color(uint8_t index, uint8_t r, uint8_t g, uint8_t b)
{
if (index >= WS2812B_NUM) return;
ws2812_colors[index].r = r;
ws2812_colors[index].g = g;
ws2812_colors[index].b = b;
}
// 设置所有 LED 为同一颜色
void ws2812_set_all(uint8_t r, uint8_t g, uint8_t b)
{
for (uint8_t i = 0; i < WS2812B_NUM; i++) {
ws2812_colors[i].r = r;
ws2812_colors[i].g = g;
ws2812_colors[i].b = b;
}
}
// 全灭
void ws2812_clear(void)
{
ws2812_set_all(0, 0, 0);
ws2812_send();
}
// 设置亮度(全局缩放)
void ws2812_set_brightness(uint8_t brightness)
{
// brightness: 0~255
for (uint8_t i = 0; i < WS2812B_NUM; i++) {
ws2812_colors[i].r = ws2812_colors[i].r * brightness / 255;
ws2812_colors[i].g = ws2812_colors[i].g * brightness / 255;
ws2812_colors[i].b = ws2812_colors[i].b * brightness / 255;
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
16.6 灯效实现
16.6.1 流水灯(彩虹)
// HSV 转 RGB(简化版,H: 0~359, S/V: 0~255)
void hsv_to_rgb(uint16_t h, uint8_t s, uint8_t v, uint8_t *r, uint8_t *g, uint8_t *b)
{
uint8_t region = h / 60;
uint8_t remainder = (h % 60) * 255 / 60;
uint8_t p = v * (255 - s) / 255;
uint8_t q = v * (255 - (s * remainder / 255)) / 255;
uint8_t t = v * (255 - (s * (255 - remainder) / 255)) / 255;
switch (region) {
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;
default: *r = v; *g = p; *b = q; break;
}
}
// 彩虹流水灯效果
void ws2812_effect_rainbow(uint16_t offset)
{
for (uint8_t i = 0; i < WS2812B_NUM; i++) {
uint16_t hue = (offset + i * 360 / WS2812B_NUM) % 360;
uint8_t r, g, b;
hsv_to_rgb(hue, 255, 128, &r, &g, &b);
ws2812_set_color(i, r, g, b);
}
ws2812_send();
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
16.6.2 呼吸灯
// 单色呼吸效果
void ws2812_effect_breathe(uint8_t r, uint8_t g, uint8_t b, uint8_t phase)
{
// phase: 0~255 对应一个完整呼吸周期
uint8_t brightness;
if (phase < 128)
brightness = phase * 2; // 渐亮
else
brightness = (255 - phase) * 2; // 渐暗
ws2812_set_all(r * brightness / 255,
g * brightness / 255,
b * brightness / 255);
ws2812_send();
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16.6.3 跑马灯
// 单颗 LED 依次点亮
void ws2812_effect_chase(uint8_t r, uint8_t g, uint8_t b, uint8_t pos)
{
for (uint8_t i = 0; i < WS2812B_NUM; i++) {
if (i == pos % WS2812B_NUM)
ws2812_set_color(i, r, g, b);
else
ws2812_set_color(i, 0, 0, 0);
}
ws2812_send();
}2
3
4
5
6
7
8
9
10
11
16.7 完整实验
#include "ti_msp_dl_config.h"
int main(void)
{
SYSCFG_DL_init(); // 必须配置为 80MHz!
ws2812_init();
uint16_t hue_offset = 0;
while (1) {
// 彩虹流水效果
ws2812_effect_rainbow(hue_offset);
hue_offset = (hue_offset + 2) % 360;
delay_cycles(4000000); // 50ms 更新一次(80MHz)
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
16.8 常见问题
| 问题 | 原因 | 解决 |
|---|---|---|
| LED 全不亮 | 主频不是 80MHz | 检查时钟配置 |
| 颜色不对(偏色) | GRB 顺序搞反了 | 确认发送顺序为 G-R-B |
| 第一颗正常,后面乱 | Reset 不够长 | 增加 WS2812B_RESET 值 |
| 闪烁不稳定 | DMA 中断与其他中断冲突 | 提高 DMA 中断优先级 |
| 只能亮白色 | 占空比参数不对 | 确认 T0H=35, T1H=70, Period=105 |
16.9 知识总结
| 内容 | 说明 |
|---|---|
| 协议 | NRZ 单总线,800kHz |
| 数据顺序 | GRB,高位先发,24-bit/LED |
| 驱动方案 | PWM + DMA(精确时序,零 CPU 干预) |
| 主频要求 | 必须 80MHz |
| 天巧星引脚 | PB26(TIMA1 CCP0) |
| LED 数量 | 4 颗,级联 |
下一章将学习无线串口模块的使用,实现天巧星与其他设备的无线数据通信。