WS2812概览
WS2812B是非常常见的一种带有全彩LED控制IC的光源,应用场合非常多。
很多开发板上,有一颗WS2812B灯珠供开发者调用,如ESP32-C3-DevKitM-1开发板:(有的板子可能不带有)
多颗灯珠,也能够串联到一起使用,组成形式多种多样,如:
在实际应用中,组成灯带的形式也非常常用:
WS2812的控制
不管WS2812B组成何种形式,都只需要一根线进行控制即可:
控制设备根据要控制的灯珠数量,一次发送所需长度的控制数据,然后第一颗灯珠把第一段控制数据截取掉,然后把剩下的向后传递,依次直到所有的灯珠都能收到自己对应的控制数据。要控制的灯珠数量,可少于整体实际灯珠数量,但必须从第一颗顺序开始,例如只控制第一颗,或者前两颗。
这种控制方式,看过人体蜈蚣的同学,是不是感觉有内味了:(((
每一颗灯珠,都是全彩LED,控制数据的顺序为三原色GRB,每一种颜色由8bits构成,也就是0~255,三种合起来有1600万种颜色,当然大多数可能我们分辨不出来。
也就是说,控制一颗灯珠,需要24bits的数据:
WS2812的时序定义
然而,然而,此处的bit,和我们从控制设备发送过来的控制数据中的二进制bit(通俗来说),不能一一对应。
它有着自己的时序规则定义:
控制数据的发送规则
我们传输控制数据的时候,需要控制传输引脚,根据以上时序,形成特定的高低电平保持时间,才会被控制IC接收。
例如:
假设我们发送0x80,那么对应的二进制为0b10000000;
而MCU上,数字通信数据的发送,本质上是高低电平的规则性变化,那么前述二进制中,发送1的时候,对应高电平,发送0的时候,则对应低电平。
如果以一定的速率发送0b10000000,那么就会形成特定时间的高低电平保持。
经过计算,如果控制设备要在1.25us内发送8bits,那么:
频率 = 1 / (1.25us/8) = 6,400,000 = 6.4M
因此,只要我们的控制设备,以上述速度,发送特定的数据,就能满足上述时序规则。
而发送0b10000000时:
高电平持续时间为1.25us/8 = 0.1563us,在0.25us+-150ns范围内
低电平持续时间为7*1.25us/8 = 1.0938us,在1us+-150ns范围内
那么发送0b10000000时,控制IC就会实际收到0码;
可以选用的控制数据
当然,上述8bits数据中,可以调配开头1和0的个数,只有使得高低电平保持时间,在允许范围内即可。
如果1个1开头,则是0b10000000,对应0x80,在0码时间范围内
如果2个1开头,则是0b11000000,对应0xc0,在0码时间范围内
如果3个1开头,则是0b11100000,对应0xe0,在0码时间范围内
如果4个1开头,则是0b11100000,对应0xe0,无效
如果5个1开头,则是0b11100000,对应0xf8,在1码时间范围内
如果6个1开头,则是0b11100000,对应0xfc,在1码时间范围内
如果7个1开头,则是0b11100000,对应0xfe,在1码时间范围内
我这边的经验数据,取了0x80对应0码,0xf8对应1码。
多颗灯珠的控制
前面说过,控制1颗灯珠,需要24bits,那么,控制设备,只要以上述速度,发送24 Bytes的0x80或0xf8就可以了。
要再同时控制第2颗灯珠,那就再加24 Bytes,一共48 Bytes
要再同时控制第3颗灯珠,那就再加24 Bytes,一共72 Bytes
以此类推,速度允许的情况下,可以控制多达上千颗灯珠。
需要提醒的是,WS2812B一颗灯珠的满亮度工作电流为60ma(GRB各20ma),灯珠少好说,可以用一些开发板的供电,但灯珠一多,很可能带不起来,所以,通常多颗情况下,我都是单独供电的。
控制多颗灯珠,就连续发送24*n Bytes控制数据;那么需要发送新一轮的控制数据,怎么办?
在之前的时序定义中,还有一个RES,我们只需要发送0x00(对应0b00000000),达到50us即可,简单起见,在上述发送速度下,我直接发送了50个0x00
现在,要发送灯珠控制数据了,就连续发送n个24 Bytes的控制数据,让控制IC接收为n个24 bits,并每8 bits对应GRB即可。
如果要发送新一轮的控制数据了,那么就发50 bytes 的0x00即可。
数据解析链
在控制设备发送数据的时候,8个字节,对应WS2812B的8 bits,也就是一种颜色,那我们就在控制设备中,也使用0x00~0xff代表颜色值,并根据其二进制,生成对应的8字节数组,其中0 对应 0x80,1对应0xf8,于是形成了下面的解释链:
控制设备(G=0x01) => 二进制(0b00000001) => 01码字节数组({0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0xf8}) => 通信接口发送 => WS2812B接收 => 01码识别({0, 0, 0, 0, 0, 0, 0, 1}) => 二进制(0b00000001) => 颜色控制值(G=0x01)
通过这个解析链,就形成了完整的闭环。注意,上述解释链中WS2812B接收后解码为二进制和颜色控制值,只是为了方便理解添加的。
选用发送数据的端口
理解了上述数据发送的原理,我们可以选用所有可用的通信端口,来发送数据,用UART、SPI、I2C等都可以。
如果发送的速度为 6.4MHz,一般而言常见的通用通信端口中,只有SPI符合要求。下面的实例,就使用的SPI接口。
在之前的时序规则说明中,给出了容错的范围,选用6.4MHz只是经验数据,是根据标准值1.25us发送8bits数据得到的。
而我的板子运行在200MHz,所以选用6.4MHz,没有任何问题。
数据发送的优化
在容错范围内,发送的数据,形成的高低电平保持时序,符合01码对应的时序要求,那么就可以用。
首发内测群的juice等大佬,直接使用3 bits来形成对应1位01码的时序,那么一种颜色的控制数据,只需要24bits,也就是3 bytes即可,远少于上述6.4MHz方案中的8 bytes,同样所需要的传输速度也就不需要这么高了。
如果感兴趣,可以根据容错范围,进行精心调配,以实现更精确的控制。
实现效果
最终,在感芯刘工的帮助,及群内各位大佬的帮助下,在MC3172上,通过SPi实现了WS2812B的原生控制,具体效果如下:
4颗灯珠:
24颗灯环:
代码分享
具体的代码,分享到了:https://gitee.com/honestqiao/mc3172-ws2812b
对于关键代码进行说明:
#include "../MC3172/MC3172.h"
#include <math.h>
#include "common.h"
#include "test_spi.h"
////////////////////////////////////////////////////////////
#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]))
// RST码
#define TRST 0x00
// 0码
#define TOL 0x80
// 1码
#define TOH 0xf8
// 演示时间
#define DELAY_US_NUM 300000
// 每灯珠bit位数
#define R_BYTE_BITS 24
// 灯珠数量
#define R_NUM 24
// 所有灯珠对应的bit位数
#define R_NUM_BITS R_NUM * R_BYTE_BITS
// RST位发送的个数
#define R_NUM_RESET_BITS 50
// G=0 R=0 B=0对应数据
unsigned char default_tx0[] = {
TOL, TOL, TOL, TOL, TOL, TOL, TOL, TOL,
TOL, TOL, TOL, TOL, TOL, TOL, TOL, TOL,
TOL, TOL, TOL, TOL, TOL, TOL, TOL, TOL};
// G=0x80 R=0x80 B=0x80对应数据
unsigned char default_tx1[] = {
TOH, TOL, TOL, TOL, TOL, TOL, TOL, TOL,
TOH, TOL, TOL, TOL, TOL, TOL, TOL, TOL,
TOH, TOL, TOL, TOL, TOL, TOL, TOL, TOL};
// 多种颜色对应的部分数据
unsigned char default_tx1_colors[] [R_BYTE_BITS]= {
{
TOH, TOL, TOL, TOL, TOL, TOL, TOL, TOL,
TOL, TOL, TOL, TOL, TOL, TOL, TOL, TOL,
TOL, TOL, TOL, TOL, TOL, TOL, TOL, TOL},
{
TOL, TOL, TOL, TOL, TOL, TOL, TOL, TOL,
TOH, TOL, TOL, TOL, TOL, TOL, TOL, TOL,
TOL, TOL, TOL, TOL, TOL, TOL, TOL, TOL},
{
TOL, TOL, TOL, TOL, TOL, TOL, TOL, TOL,
TOL, TOL, TOL, TOL, TOL, TOL, TOL, TOL,
TOH, TOL, TOL, TOL, TOL, TOL, TOL, TOL},
{
TOH, TOL, TOL, TOL, TOL, TOL, TOL, TOL,
TOH, TOL, TOL, TOL, TOL, TOL, TOL, TOL,
TOH, TOL, TOL, TOL, TOL, TOL, TOL, TOL},
{
TOH, TOH, TOH, TOH, TOH, TOH, TOH, TOH,
TOH, TOH, TOH, TOH, TOH, TOH, TOH, TOH,
TOL, TOL, TOL, TOL, TOL, TOL, TOL, TOL},
{
TOL, TOL, TOL, TOL, TOL, TOL, TOL, TOL,
TOH, TOL, TOL, TOL, TOL, TOL, TOL, TOL,
TOH, TOL, TOL, TOL, TOL, TOL, TOL, TOL},
{
TOH, TOL, TOL, TOL, TOL, TOL, TOL, TOL,
TOL, TOL, TOL, TOL, TOL, TOL, TOL, TOL,
TOH, TOL, TOL, TOL, TOL, TOL, TOL, TOL},
};
void GPCOM_SPI_WS2812B2(u32 gpcom_sel)
{
INTDEV_SET_CLK_RST(gpcom_sel,(INTDEV_RUN|INTDEV_IS_GROUP0|INTDEV_CLK_IS_CORECLK_DIV2));
GPCOM_SET_IN_PORT(gpcom_sel,(GPCOM_MASTER_IN_IS_P3));
GPCOM_SET_OUT_PORT(gpcom_sel,( \
GPCOM_P0_OUTPUT_ENABLE|GPCOM_P1_OUTPUT_ENABLE|GPCOM_P2_OUTPUT_ENABLE|GPCOM_P3_OUTPUT_DISABLE| \
GPCOM_P0_IS_HIGH| GPCOM_P1_IS_MASTER_CLK|GPCOM_P2_IS_MASTER_OUT|GPCOM_P3_IS_HIGH
));
GPCOM_SET_COM_MODE(gpcom_sel,(GPCOM_SPI_MASTER_MODE3|GPCOM_TX_MSB_FIRST|GPCOM_RX_MSB_FIRST));
// 设置SPI外设时钟,数据发送速度
GPCOM_SET_COM_SPEED(gpcom_sel,CORE_CLOCK_HZ/INTDEV_CLK_IS_CORECLK_DIV2, 6400000);
while(!(GPCOM_TX_FIFO_EMPTY(gpcom_sel))){};
GPCOM_SET_OVERRIDE_GPIO(gpcom_sel, ( \
GPCOM_P0_OVERRIDE_GPIO| \
GPCOM_P1_OVERRIDE_GPIO| \
GPCOM_P2_OVERRIDE_GPIO| \
GPCOM_P3_OVERRIDE_GPIO|GPCOM_P3_INPUT_ENABLE \
));
u32 t = 0;
// 发送缓冲区
unsigned char default_tx[R_NUM_BITS+R_NUM_RESET_BITS*2+10];
while(1){
u16 idx = 0;
// RESET Bits
for(u16 n=0;n<R_NUM_RESET_BITS;n++) {
default_tx[idx++] = TRST;
}
// 驱动灯珠
for(u16 m=0;m<R_NUM;m++) {
if(t%R_NUM==m) {
for(u16 n=0;n<R_BYTE_BITS;n++) {
default_tx[idx++] = default_tx1_colors[m%7][n];
}
} else {
for(u16 n=0;n<R_BYTE_BITS;n++) {
default_tx[idx++] = default_tx0[n];
}
}
}
// RESET Bits
for(u16 n=0;n<R_NUM_RESET_BITS;n++) {
default_tx[idx++] = TRST;
}
// 设置输出
GPCOM_SET_OUT_PORT(gpcom_sel,( \
GPCOM_P0_OUTPUT_ENABLE|GPCOM_P1_OUTPUT_ENABLE|GPCOM_P2_OUTPUT_ENABLE|GPCOM_P3_OUTPUT_DISABLE| \
GPCOM_P0_IS_HIGH| GPCOM_P1_IS_MASTER_CLK| GPCOM_P2_IS_MASTER_OUT|GPCOM_P3_IS_HIGH
));
// 写入输出缓冲区
for(u16 n=0;n<idx;n++) {
while(GPCOM_TX_FIFO_FULL(gpcom_sel));
GPCOM_PUSH_TX_DATA(gpcom_sel,default_tx[n]);
}
while(!(GPCOM_TX_FIFO_EMPTY(gpcom_sel))){};
// 设置输出引脚低电平
GPCOM_SET_OUT_PORT(gpcom_sel,( \
GPCOM_P0_OUTPUT_ENABLE|GPCOM_P1_OUTPUT_ENABLE|GPCOM_P2_OUTPUT_ENABLE|GPCOM_P3_OUTPUT_DISABLE| \
GPCOM_P0_IS_HIGH| GPCOM_P1_IS_MASTER_CLK| GPCOM_P2_IS_LOW|GPCOM_P3_IS_HIGH
));
delay_us(DELAY_US_NUM);
t++;
}
GPCOM_SET_OUT_PORT(gpcom_sel,( \
GPCOM_P0_OUTPUT_ENABLE|GPCOM_P1_OUTPUT_ENABLE|GPCOM_P2_OUTPUT_ENABLE|GPCOM_P3_OUTPUT_DISABLE| \
GPCOM_P0_IS_HIGH| GPCOM_P1_IS_MASTER_CLK| GPCOM_P2_IS_MASTER_OUT|GPCOM_P3_IS_HIGH
));
}
上述代码中,具体说明如下:
1. 预定义部分:
1). default_tx0[]:定义了一颗灯珠G=0、R=0、B=0的控制字节数组
2). default_tx1[]:定义了一颗灯珠G=0x80、R=0x80、B=0x80的控制字节数组
3). default_tx1_colors[]:定义了7种颜色
二、实际处理部分:
1. 使用t做为自增变量,通过其来确定当前要点亮的灯珠
2. 根据t,生成了控制数据,非当前灯珠使用0码对应的控制数据,当前灯珠则从颜色表中选一个数据,并在发送前后,添加RST码对应的控制数据
3. 发送前,设置P2为MOSI输出,然后发送数据,发送完毕,设置P2为LOW
使用SPI的过程中,在感芯大佬刘工的帮助下,逐步理解了SPI的速率设置关系,并使用逻辑分分析仪进行了分析:
由于SPI发送数据前后,idle为高电平,所以在实际数据发送前后,发送了50 bits的RST,隔离开来,避免干扰。
在大佬刘工手把手指导下,经过多番调试大战,最终搞定了WS2812B。
另外,还要感谢群里Memory大佬的帮助,提供了200Mhz晶振,让我的板子得以高速运行。
以上只是一次小小的原理学习尝试,分享的内容也非常的基础,如有不当之处,还请各位大佬多多指正,一定好好学习,天天向上!
附注:
本文中,关于WS2812B的资料,可以查看下面的PDF文档:
WS2812B_cn.pdf
最近编辑记录 HonestQiao (2022-08-19 17:12:53)
离线
离线
大佬威武。
多来几路就可以做个LED显示屏了。
>>> 12个可配置通信接口,可按需配置为UASRT或SPI或USB或CAN总线形式
大胆搞起!!!
离线