一、WS2812B芯片说明
WS2811B概述
数据协议采用单线归零码的通讯方式,芯片在上电复位以后,DIN端接受从控制器传输过来的数据,首先送过来的24bit数据被第一个芯片提取后,送到芯片内部的数据锁存器,剩余的数据经过内部整形处理电路整形放大后通过DO端口开始转发输出给下一个级联的芯片,每经过一个芯片的传输,信号减少24bit。芯片采用自动整形转发技术,使得该芯片的级联个数不受信号传送的限制,仅仅受限信号传输速度要求。
芯片内部的数据锁存器根据接收到的24bit数据,在OUTR、OUTG、OUTB控制端产生不同的占空比控制信号, 等待DIN端输入RESET信号时,所有芯片同步将接收到的数据送到各个段,芯片将在该信号结束后重新接收的数据,在接收完开始的24bit数据后,通过DO口转发数据口,芯片在没有接收到RESET码前,OUTR、OUTG、OUTB管脚原输出保持不变,当接受到280μs以上低电平RESET码后,芯片将刚才接收到的24bit PWM数据脉宽输出到OUTR、OUTG、OUTB引脚上。
数据传输协议
数据传输时间
时序图及连接方法
数据传输方法
注意:这里的RESET码,在每一次发送灯组颜色信号之前都要发送一遍RESET信号,否则会产生时序错误,导致灯组显示异常
24bit数据结构
- 每一个灯需要 8 bits(1 byte) 的数据 (8个1时最亮、8个0时不亮),所以一颗 ws2812 共需要24 bits(3 bytes) **(24个1时最亮、24个0时不亮)**的数据。
- 灯组的24bit数据的发送顺序是根据芯片手册定义的,可能是RGB,也可能是GRB。
WS2812驱动的几种方式
1.GPIO普通输出方式模拟时序
直接翻转IO口产生时序,这种方式最为简单易用,只需要控制延时的时间,就可以从产生0和1码,它需要占用系统资源。
2.通过SPI控制
使用 SPI 数据传输产生时序只需要控制在合适的波特率,在传输不同数据的时候,可以产生符合要求的0和1码,这种方式需要等同于使用了一个SPI设备。
3.使用 Timer+PWM+DMA 产生时序
定时器 TIM 用以产生一个固定周期的PWM,DMA用以改变PWM 的占空比。如图,DMA通过不断的搬运数据到定时器调节占空比的CCR寄存器,实现ws2812时序的产生,在STM32中,通过配置外设可实现:定时器每产生一次溢出事件(即计数完成),就请求一次DMA搬运一个数据(长度:字节/半字/字可选),所以用户只需要将数据排列在数组里,就可以产生所需要的时序。方法是直接建立一个大的数组,存放所有灯珠的数据,然后启动DMA传输。
二、RGB颜色原理
1.颜色基础
RGB概念
RGB指的是R(red)红色、G(green)绿色、B(blue)蓝色,三种颜色。
所有的颜色都可以用这三种颜色配出来。通常情况下,RGB各有256级亮度,用数字表示为从0、1、2…直到255。按照计算,256级的RGB色彩总共能组合出约1678万种色彩,即256×256×256=16777216。通常也被简称为1600万色或千万色。也称为24位色(2的24次方)
RGB格式的内部排列
2.颜色网站链接
3.RGB的256级亮度
4.颜色模型
表示颜色的模型HSV,H:色调、S:饱和度、V:亮度。只要H和S不变就能够保证颜色不改变,从而调整亮度。首先将RGB空间颜色转换成HSV空间颜色,然后调节V的值,调整之后,再将HSV空间颜色转回到RGB空间颜色这样就实现了RGB亮度的调节。
1)HSV模型概述
RGB图像与相机传感器输出的原始数据相对应,HSV图像则与我们人类的直观视觉更相符。HSV图像也包含相同尺寸的三通道数据:H通道、S通道、V通道。下面分别介绍这三个通道:
H通道:H通道的像素值表示色调,取值范围0~360,我们可以把这个取值范围理解为角度,也即一个闭环的取值范围,如下图:
S通道:S通道的像素值表示图像的饱和度。饱和度是指图片彩色的纯度——图像的混合颜色越少,其饱和度越高,直观看起来就越鲜艳鲜明、视觉效果越强烈;反之图像的混合颜色越多,其饱和度越低,视觉效果越弱。S通道像素值的取值范围是0~1,值越大表示饱和度越高。
比如在所有可视色彩中纯红色的饱和度是最高的,也即纯红色看起来最鲜艳,但是如果在纯红色中混入其它颜色,那么其饱和度将会降低,这时看起来就没那么鲜艳了。
V通道:V通道像素值表示图像的明亮程度,取值范围也是0~1,值越大表示越亮。
2)RGB与HSV图像互相转换原理
RGB图像、HSV图像中各通道像素值的取值范围,对于图像中任意坐标点,其取值范围如下:
将HSV像素值转换到0~255之间:
3)RGB转HSV原理
对于图像中任意坐标点,其RGB颜色空间为(R,G,B),HSV颜色空间为(H,S,V),首先需要将R、G、B值转换到0~1之间:
然后计算H、S、V值:
如果计算得到的H值小于0,将该值再加上360,得到最终的H值:H=H+360
由于Opencv需要做HSV图像的可视化,因此最后还需要将各个值转换到0~255之间:
4)HSV转RGB原理
对于图像中任意坐标点,其RGB颜色空间为(R,G,B),HSV颜色空间为(H,S,V)。首先将可视化图像的H、S、V值分别转换到0~360, 0 ~1, 0 ~1的范围:
那么R、G、B的计算如以下公式,其中floor表示向下取整运算:
5)模型转换代码实现
// 求最大值、最小值函数
static float min(float a, float b, float c)
{
float m;
m = a < b ? a : b;
return (m < c ? m : c);
}
static float max(float a, float b, float c)
{
float m;
m = a > b ? a : b;
return (m > c ? m : c);
}
//结构体定义
typedef struct
{
unsigned char R;
unsigned char G;
unsigned char B;
unsigned char L;
}RGB_Color_TypeDef;
typedef struct
{
float H;
float S;
float V;
}HSV_Color_TypeDef;
//函数实现
static void RGB_TO_HSV(RGB_Color_TypeDef* input,HSV_Color_TypeDef* output)
{
float r,g,b,minRGB,maxRGB,deltaRGB;
r = input->R/255.0f;
g = input->G/255.0f;
b = input->B/255.0f;
minRGB = min(r,g,b);
maxRGB = max(r,g,b);
output->V = maxRGB;
deltaRGB = maxRGB - minRGB;
if(maxRGB != 0.0f)
output->S = deltaRGB / maxRGB;
else
output->S = 0.0f;
if (output->S <= 0.0f)
{
output->H = 0.0f;
}
else
{
if (r == maxRGB)
{
output->H = (g-b)/deltaRGB;
}
else
{
if (g == maxRGB)
{
output->H = 2.0f + (b-r)/deltaRGB;
}
else
{
if (b == maxRGB)
{
output->H = 4.0f + (r-g)/deltaRGB;
}
}
}
output->H = output->H * 60.0f;
if (output->H < 0.0f)
{
output->H += 360;
}
output->H /= 360;
}
}
static void HSV_TO_RGB(HSV_Color_TypeDef* input,RGB_Color_TypeDef* output)
{
float R,G,B;
int k;
float aa,bb,cc,f;
if (input->S <= 0.0f)
R = G = B = input->V;
else
{
if (input->H == 1.0f)
input->H = 0.0f;
input->H *= 6.0f;
k = (int)floor(input->H);
f = input->H - k;
aa = input->V * (1.0f - input->S);
bb = input->V * (1.0f - input->S * f);
cc = input->V * (1.0f -(input->S * (1.0f - f)));
switch(k)
{
case 0:
R = input->V;
G = cc;
B =aa;
break;
case 1:
R = bb;
G = input->V;
B = aa;
break;
case 2:
R =aa;
G = input->V;
B = cc;
break;
case 3:
R = aa;
G = bb;
B = input->V;
break;
case 4:
R = cc;
G = aa;
B = input->V;
break;
case 5:
R = input->V;
G = aa;
B = bb;
break;
}
}
output->R = (unsigned char)(R * 255);
output->G = (unsigned char)(G * 255);
output->B = (unsigned char)(B * 255);
}
//亮度调节函数
//step:亮度调节参数,范围1-100
void Adjust_Brightness(unsigned long color,int step)
{
HSV_Color_TypeDef HSV_V;
RGB_Color_TypeDef RGB_V;
uint32_t RGB;
RGB_V.R=color<<16;
RGB_V.G=color<<8;
RGB_V.B=color;
RGB_TO_HSV(&RGB_V,&HSV_V);
RGB_V.L += step;
if(RGB_V.L <= 0)
{
RGB_V.L = 1;
}else if(RGB_V.L >= 100)
{
RGB_V.L = 100;
}
HSV_V.V = RGB_V.L /100.0;
HSV_TO_RGB(&HSV_V,&RGB_V);
RGB= RGB_V.R<<16 |RGB_V.G <<8 | RGB_V.B; // 合成颜色
return RGB;
}
5.渐变函数
unsigned long ColorToColor(uint8_t Pixel_Len,unsigned long color0, unsigned long color1,unsigned int speed,int step)
{
unsigned char Red0, Green0, Blue0;// 起始三原色
unsigned char Red1, Green1, Blue1;// 结果三原色
int RedMinus, GreenMinus, BlueMinus; // 颜色差(color1 - color0)
unsigned char NStep; // 需要几步
unsigned long RGB[NStep];//RGB渐变颜色数组
float RedStep, GreenStep, BlueStep; // 各色步进值
unsigned long color; // 结果色
unsigned char i,j;
// 绿 红 蓝 三原色分解
Green0= color0>>8;
Red0 = color0>>16;
Blue0 = color0;
Green1= color1>>8;
Red1 = color1>>16;
Blue1 = color1;
// 绿 红 蓝 三原色分解? 若输入的颜色非上述三种
Green0=( color0&0x00FF00)>>8;
Red0= (color0&0xFF0000)>>16;
Blue0= color0;
Green1= (color1&0x00FF00)>>8;
Red1 =(color1&0xFF0000)>>16;
Blue1= color1;
// 计算需要多少步(取差值的最大值)
RedMinus= Red1 - Red0;
GreenMinus = Green1 - Green0;
BlueMinus= Blue1 - Blue0;
//判断两个绝对值大小。。a>b为真,则=a;否则=b
NStep = ( abs0(RedMinus) > abs0(GreenMinus) ) ? abs0(RedMinus):abs0(GreenMinus);
NStep = ( NStep > abs0(BlueMinus) ) ? NStep:abs0(BlueMinus);
// 计算出各色步进值
RedStep= (float)RedMinus/ NStep;
GreenStep = (float)GreenMinus / NStep;
BlueStep = (float)BlueMinus/ NStep;
// 渐变开始
for(i=0; i<NStep; i++)
{
Red1= Red0+ (int)(RedStep* i);
Green1 = Green0 + (int)(GreenStep * i);
Blue1 = Blue0 + (int)(BlueStep* i);
color= Red1<<16 |Green1 <<8 | Blue1; // 合成颜色
//添加自定义函数:将颜色发给各个灯珠
HAL_Delay(speed);//控制渐变速度
}
return color;
三、WS2812B代码实现
1. WS2812B.h
注意:High 和Low的值根据所选STM32芯片的定时器时钟计算,这里的值不做参考。
#define Low (19) //0编码高电平时间
#define High (40) //1编码高电平时间
typedef struct
{
unsigned char R;
unsigned char G;
unsigned char B;
unsigned char L;
}RGB_Color_TypeDef;
typedef struct
{
float H;
float S;
float V;
}HSV_Color_TypeDef;
//R-G-B 定义颜色24bits
//标准色
#define RainbowWHITE 0xFFFFFF // 白色
#define RainbowBLACK 0x000000 // 黑色 (不亮)
#define RainbowRED 0xFF0000 // 红色
#define RainbowORANGE 0xFFA500 // 橙色
#define RainbowYELLOW 0xFFFF00 //黄色
#define RainbowGREEN 0x00FF00 // 绿色
#define RainbowBLUE 0x0000FF // 蓝色
#define RainbowCyan 0x00FFFF //青色
#define RainbowPURPLE 0x800080 //紫色
//灯组参数定义
#define Pixel_NUM 20 //联级灯珠数量
#define RGB_NUM 24 //灯珠RGB数据3*8
#define Lightness 20 //亮度范围:0~100
extern void Color_show_1(int mode,uint8_t Pixel_Len,unsigned long dat,int step);//连续控制几个灯 模式0;逆向,模式1:顺向
extern void Color_show_2(uint8_t Pixel_Len, unsigned long dat,int step);控制某一个灯(顺向)
extern unsigned long ColorToColor(uint8_t Pixel_Len,unsigned long color0, unsigned long color1,unsigned int speed,int step);//颜色渐变函数
extern uint32_t Adjust_Brightness(unsigned long color,int step);
extern void Rainbow_lamp(void);//彩虹灯效
2. WS2812B.c
RGB_Color_TypeDef Color;
const uint32_t WS2812_Rst[300] = {0}; //复位码缓冲区
/*二维数组存放最终PWM输出数组,每一行24个数据代表一个LED*/
uint32_t Pixel_Buf[Pixel_NUM][RGB_NUM];
/*********************************WB2812驱动函数**************************************************/
/*
功能:设定单个RGB LED的颜色,把结构体中RGB的24BIT转换为0码和1码
参数:LedId为LED序号,dat:指定灯的颜色
*/
void RGB_SetColor(uint8_t LedId,unsigned long dat)
{
uint8_t i,j;
RGB_Color_TypeDef Color;
Color.G= dat>>8;
Color.R= dat>>16;
Color.B=dat;
if(LedId > Pixel_NUM)return; // 防止写入ID大于LED总数
for(i=0;i<8;i++)
{
Pixel_Buf[LedId][i] = ( (Color.G & (1 << (7 -i)))? (CODE_1):CODE_0 );//数组某一行0~7转化存放G
}
for(i=8;i<16;i++)
{
Pixel_Buf[LedId][i] = ( (Color.R & (1 << (15-i)))? (CODE_1):CODE_0 );//数组某一行8~15转化存放R
}
for(i=16;i<24;i++)
{
Pixel_Buf[LedId][i] = ( (Color.B & (1 << (23-i)))? (CODE_1):CODE_0 );//数组某一行16~23转化存放B
}
}
/*
功能:发送数组
参数:(&htim1)定时器1,(TIM_CHANNEL_1)通道1,WS2812_Rst:复位码,
*/
extern uint8_t g_Send_ok;
void RGB_SendArray(void)
{
uint8_t i;
if(g_Send_ok == 0)
{
g_Send_ok = 1;
HAL_TIM_PWM_Start_DMA(&htim1, TIM_CHANNEL_1,WS2812_Rst,300);
}
}
/*********************************WB2812基本函数**************************************************/
/*
功能:控制连续的几个灯 0;逆向(靠进按钮),模式1:顺向(远离按钮)
参数:mode为模式选择,0:逆向,1:顺向;
Pixel_Len为LED数量,1-20
dat:指定灯的颜色
*/
void Color_show_1(int mode,uint8_t Pixel_Len, unsigned long dat,int step)
{
int16_t i,j;
uint32_t RGB;
HSV_Color_TypeDef HSV_V;
RGB_Color_TypeDef RGB_V;
RGB_V.R=dat>>16;
RGB_V.G=dat>>8;
RGB_V.B=dat;
RGB_TO_HSV(&RGB_V,&HSV_V);
RGB_V.L = step;
if(RGB_V.L <= 0)
{
RGB_V.L = 1;
}else if(RGB_V.L >= 100)
{
RGB_V.L = 100;
}
HSV_V.V = RGB_V.L /100.0;
HSV_TO_RGB(&HSV_V,&RGB_V);
RGB= RGB_V.R<<16 |RGB_V.G <<8 | RGB_V.B; // 合成颜色
if(mode==0)
{
for(i=0;i<Pixel_Len;i++)
{
RGB_SetColor(i,RGB);
}
RGB_SendArray();
}
else if(mode==1)
{
for(j=0;j<Pixel_Len;j++)
{
RGB_SetColor((Pixel_NUM-1-j),RGB);
}
RGB_SendArray();
}
}
/*
功能:控制某一个灯顺向(远离按钮)
参数:Pixel_Len为顺向第几个灯的序号,灯的顺序:1-20
dat:指定灯的颜色
*/
void Color_show_2(uint8_t Pixel_Len, unsigned long dat,int step)
{
uint32_t RGB;
HSV_Color_TypeDef HSV_V;
RGB_Color_TypeDef RGB_V;
RGB_V.R=dat>>16;
RGB_V.G=dat>>8;
RGB_V.B=dat;
RGB_TO_HSV(&RGB_V,&HSV_V);
RGB_V.L = step;
if(RGB_V.L <= 0)
{
RGB_V.L = 1;
}else if(RGB_V.L >= 100)
{
RGB_V.L = 100;
}
HSV_V.V = RGB_V.L /100.0;
HSV_TO_RGB(&HSV_V,&RGB_V);
RGB= RGB_V.R<<16 |RGB_V.G <<8 | RGB_V.B; // 合成颜色
RGB_SetColor((Pixel_NUM-Pixel_Len),RGB);
RGB_SendArray();
}
//RGB亮度调节
uint32_t Adjust_Brightness(unsigned long color,int step)
{
HSV_Color_TypeDef HSV_V;
RGB_Color_TypeDef RGB_V;
uint32_t RGB;
RGB_V.R=color<<16;
RGB_V.G=color<<8;
RGB_V.B=color;
RGB_TO_HSV(&RGB_V,&HSV_V);
RGB_V.L += step;
if(RGB_V.L <= 0)
{
RGB_V.L = 1;
}else if(RGB_V.L >= 100)
{
RGB_V.L = 100;
}
HSV_V.V = RGB_V.L /100.0;
HSV_TO_RGB(&HSV_V,&RGB_V);
RGB= RGB_V.R<<16 |RGB_V.G <<8 | RGB_V.B; // 合成颜色
return RGB;
}
//彩虹灯效
void Rainbow_lamp(void)
{
int i;
Color_show_1(1,20, BLACK);
HAL_Delay(50);
for(i = 1; i <= 20 ;i++) { //红
Color_show_1(1,i, RainbowRED,Lightness);
HAL_Delay(50);
}
for(i = 1; i <= 20 ;i++) { //橙
Color_show_1(1,i, RainbowORANGE,Lightness);
HAL_Delay(50);
}
for(i = 1; i <= 20 ;i++) { //黄
Color_show_1(1,i, RainbowYELLOW,Lightness);
HAL_Delay(50);
}
for(i = 1; i <= 20 ;i++) { //绿
Color_show_1(1,i, RainbowGREEN,Lightness);
HAL_Delay(50);
}
for(i = 1; i <= 20 ;i++) { //青
Color_show_1(1,i, RainbowCyan,Lightness);
HAL_Delay(50);
}
for(i = 1; i <= 20 ;i++) { //蓝
Color_show_1(1,i, RainbowBLUE,Lightness);
HAL_Delay(50);
}
for(i = 1; i <= 20 ;i++) { //紫
Color_show_1(1,i, RainbowPURPLE,Lightness);
HAL_Delay(50);
}
}
3. main.c
uint8_t g_Send_ok = 0;//DMA发送标志位,0:灯组颜色发送前的复位码;1:发送灯组颜色编码;2:灯组颜色发送完成后的复位码;3:暂停DMA发送并将g_Send_ok置0,准备下一次颜色编码发送。
extern uint32_t WS2812_Rst[300]; //复位码缓冲区
extern uint32_t Pixel_Buf[Pixel_NUM][RGB_NUM];
void HAL_TIM_PWM_PulseFinishedCallback(TIM_HandleTypeDef *htim)
{
if(g_Send_ok==1)
{
HAL_TIM_PWM_Start_DMA(&htim1, TIM_CHANNEL_1,Pixel_Buf,21*24);
g_Send_ok = 2;
}
else if(g_Send_ok == 2)
{
g_Send_ok = 3;
HAL_TIM_PWM_Start_DMA(&htim1, TIM_CHANNEL_1,WS2812_Rst,300);
}
else if(g_Send_ok == 3)
{
g_Send_ok = 0;
HAL_TIM_PWM_Stop_DMA(&htim1, TIM_CHANNEL_1);
}
}
四、总结
-
网上查找资料。
比如借鉴很多其他与本文相关的文章,总之目的只有一个:解决遇到的问题。 -
学会分析芯片手册,找关键,会看图,会分析时序。
比如本次用到的灯珠芯片就是手册有问题,导致灯组的第一个灯显示异常,最后发现其原因是因为每一次发送灯组颜色编码时没有发送复位码,所以导致第一个灯珠接收信号异常;其次灯珠芯片的时序也有问题,从目前网上找到的最新手册上看。
WS2812B芯片手册 -
针对时序芯片的调试一定要会用示波器,实在不行一定要有逻辑分析仪帮助调试,要不然很难查找问题根源。时序真的很麻烦,不是难,是麻烦!
五、PWM波形模拟发送和定时器接收方法
1.PWM波形模拟发送
将下面注释的两条语句打开,则可以只发送一次相同数量的PWM波形;不打开可以一直发送相同数量的PWM波形,每次间隔10S。时间和PWM波占空比可以根据自己的需求进行更改。
#define Led_Delay 10 //状态响应延时
#define PWM_FINISH 20 //定时周期为50ms时,发送完成时间为5s
typedef struct {
uint8_t Pwm_Wave_Num;//PWM波数量(需要扩大两倍)
uint8_t Change_New_Stauts;//新的状态
uint8_t Change_Old_Stauts;//上一个状态
uint8_t Pwm_Wave_Count;PWM波计数
uint8_t Change_Stauts_time;//
uint16_t Pwm_Send_Finish_Count;
}S_LED_CTRL;
if(htim->Instance==TIM1)
{
if(gLed_Ctrl.Pwm_Send_Finish_Count > 0)
gLed_Ctrl.Pwm_Send_Finish_Count--;
else gLed_Ctrl.Change_Stauts_time++;
// if((gLed_Ctrl.Pwm_Wave_Num!=0)&&(gLed_Ctrl.Change_Old_Stauts!=gLed_Ctrl.Change_New_Stauts))
if(gLed_Ctrl.Pwm_Wave_Num!=0)
{
gLed_Ctrl.Pwm_Wave_Count++;
if(gLed_Ctrl.Pwm_Wave_Count==gLed_Ctrl.Pwm_Wave_Num)
{
HAL_GPIO_WritePin(gLed_Ctrl.Dir_Pin.pGpio,gLed_Ctrl.Dir_Pin.Gpio_Pin,GPIO_PIN_RESET);
gLed_Ctrl.Pwm_Wave_Num = 0;
gLed_Ctrl.Pwm_Wave_Count = 0;
// gLed_Ctrl.Change_Old_Stauts= gLed_Ctrl.Change_New_Stauts;
}
else
{
if(gLed_Ctrl.Pwm_Wave_Count%2)
{
HAL_GPIO_WritePin(gLed_Ctrl.Dir_Pin.pGpio,gLed_Ctrl.Dir_Pin.Gpio_Pin,GPIO_PIN_SET);
}
else
{
HAL_GPIO_WritePin(gLed_Ctrl.Dir_Pin.pGpio,gLed_Ctrl.Dir_Pin.Gpio_Pin,GPIO_PIN_RESET);
}
}
}
}
2.定时器接收PWM波
typedef struct {
uint8_t PwmNum_Counter;//PWM计数接收值
uint8_t PwmNum_Record;//PWM计数中间记录值
uint8_t Pwm_Code;//PWM计数最终值
}S_SYSTEM_RUN_CTRL;
void HAL_GPIO_EXTI_Falling_Callback(uint16_t GPIO_Pin)
{
if(GPIO_Pin==GPIO_PIN_4)//PWM波形接收
{
System_Run_Ctrl.PwmNum_Counter++;
TimerPWM_Counter=0;
}
__HAL_GPIO_EXTI_CLEAR_FALLING_IT(GPIO_PIN_4);
}
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance == TIM3)//定时器定时50ms
{
TimerPWM_Counter++;
if((TimerPWM_Counter>3)&&(System_Run_Ctrl.PwmNum_Counter==System_Run_Ctrl.PwmNum_Record))
{
System_Run_Ctrl.Pwm_Code=System_Run_Ctrl.PwmNum_Record;
System_Run_Ctrl.PwmNum_Counter=0;
System_Run_Ctrl.PwmNum_Record=0;
TimerPWM_Counter=0;
}
else
{
System_Run_Ctrl.PwmNum_Record=System_Run_Ctrl.PwmNum_Counter;
}
}
}
参考博客
如何使用STM32F103C8T6驱动WS2812(PWM+DMA)
STM32驱动WS2812B-2020 RGB彩灯(二)
【STM32F4系列】【HAL库】【自制库】WS2812(软件部分)(PWM+DMA)
RGB亮度调节