您尚未登录。

楼主 # 2022-08-19 14:33:11

HonestQiao
会员
注册时间: 2022-08-03
已发帖子: 5
积分: 191

【代码分享】MC3172大战WS2812B

WS2812概览
WS2812B是非常常见的一种带有全彩LED控制IC的光源,应用场合非常多。
很多开发板上,有一颗WS2812B灯珠供开发者调用,如ESP32-C3-DevKitM-1开发板:(有的板子可能不带有)
esp32-c3-devkitm-1-v1-isometric.png

多颗灯珠,也能够串联到一起使用,组成形式多种多样,如:
WS2812B形态.png

在实际应用中,组成灯带的形式也非常常用:
.png


WS2812的控制
不管WS2812B组成何种形式,都只需要一根线进行控制即可:
.jpeg

控制设备根据要控制的灯珠数量,一次发送所需长度的控制数据,然后第一颗灯珠把第一段控制数据截取掉,然后把剩下的向后传递,依次直到所有的灯珠都能收到自己对应的控制数据。要控制的灯珠数量,可少于整体实际灯珠数量,但必须从第一颗顺序开始,例如只控制第一颗,或者前两颗。
.png
这种控制方式,看过人体蜈蚣的同学,是不是感觉有内味了:(((

每一颗灯珠,都是全彩LED,控制数据的顺序为三原色GRB,每一种颜色由8bits构成,也就是0~255,三种合起来有1600万种颜色,当然大多数可能我们分辨不出来。
也就是说,控制一颗灯珠,需要24bits的数据:
.png

WS2812的时序定义
然而,然而,此处的bit,和我们从控制设备发送过来的控制数据中的二进制bit(通俗来说),不能一一对应。
它有着自己的时序规则定义:
.png

控制数据的发送规则
我们传输控制数据的时候,需要控制传输引脚,根据以上时序,形成特定的高低电平保持时间,才会被控制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颗灯珠:
W4-2.jpg
W4-1.jpg

24颗灯环:
W4-3.jpg
W4-4.jpg


代码分享
具体的代码,分享到了: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的速率设置关系,并使用逻辑分分析仪进行了分析:
.png
由于SPI发送数据前后,idle为高电平,所以在实际数据发送前后,发送了50 bits的RST,隔离开来,避免干扰。
在大佬刘工手把手指导下,经过多番调试大战,最终搞定了WS2812B。


另外,还要感谢群里Memory大佬的帮助,提供了200Mhz晶振,让我的板子得以高速运行。

以上只是一次小小的原理学习尝试,分享的内容也非常的基础,如有不当之处,还请各位大佬多多指正,一定好好学习,天天向上!


附注:
本文中,关于WS2812B的资料,可以查看下面的PDF文档:
WS2812B_cn.pdf

最近编辑记录 HonestQiao (2022-08-19 17:12:53)

离线

楼主 #2 2022-08-19 17:24:22

HonestQiao
会员
注册时间: 2022-08-03
已发帖子: 5
积分: 191

Re: 【代码分享】MC3172大战WS2812B

离线

楼主 #4 2022-08-19 18:51:10

HonestQiao
会员
注册时间: 2022-08-03
已发帖子: 5
积分: 191

Re: 【代码分享】MC3172大战WS2812B

sven1234 说:

大佬威武。
多来几路就可以做个LED显示屏了。

>>> 12个可配置通信接口,可按需配置为UASRT或SPI或USB或CAN总线形式

大胆搞起!!!

离线

页脚

工信部备案:粤ICP备20025096号 Powered by FluxBB

感谢为中文互联网持续输出优质内容的各位老铁们。 QQ: 516333132, 微信(wechat): whycan_cn (哇酷网/挖坑网/填坑网) service@whycan.cn