【.NET 与树莓派】控制彩色灯带(WS28XX)

彩色灯带,相信不用老周多说,大家都知道,没准你家里的灯墙里面就有。老周的茅屋是早期建造的,所以没有预留的灯槽,明灯的话是不好看的,因此老周家里没使用灯带。不过,像柜子后面,显示器后面,书桌边沿这些地方,可以贴彩色灯带。书架上也贴了一些,因为那个书架是圣诞树形状的,没办法一条灯带贴完,只能把它剪开 N 段,测算好每一段的距离,再用烙铁加线重新焊接起来(嫌麻烦的话可以直接买连接头,不用焊接)。

买灯带时老周没有买驱动器,毕竟一开始老周就计划自己写个程序来控制灯带的色彩。装饰用的灯带,5V 电压足矣。如果灯较多可以考虑独立供电,5V电源比较方便好弄,手机充电头、锂电池+5V稳压板即可轻松解决。其实,两米长度内直接用开发板供电都没啥问题。要是五米一卷的,用树莓派的5V引脚供电没有问题,但用 ESP32 的话,电压好像有点不够,亮度降低。

老周的卧室里面目前使用的装饰灯是用 ESP 32 做的,两颗18650电池供电(需要稳压模块调到5V电压)。主要是看中 ESP32有 WiFi 功能,上面写个 UDP 服务器,通过接收到的命令来切换灯带颜色。

控制命令比较简单,全用文本格式,客户端只要发送满足格式要求的命令到 ESP32 即可。比如,发送

SET+255 125 127

首先识别出“SET+”,后面三个数值用空格分隔,依次表示 RGB 三个值。例如,让灯变成蓝色

SET+0 0 255

=================================================================================

彩色灯带使用 WS28XX 作为IC,常见的像 WS2812B。每个灯珠都可以单独控制,灯带剪开后只需要串联好就行,灯带末端不需要连接回路,WS28XX 自带回路。

WS28XX 的时序其实很简单,+5V 和 GND 是供电接口,剩下一根数据线来传输信号,所以这货是单数据线控制的。当N个灯珠串联起来后,第一个灯珠的 Din 输入信号,Dout 输出信号;第一个灯珠 Dout 联第二个灯珠的 Din;第二个灯珠的 Dout 联第三个灯珠的 Din ……

 

我们都知道,RGB三个值,各为一个字节,8位,三个值合起来 24 位,所以每个灯珠的数据为24位,3个 byte。对于每个二进制位,WS28XX 是通过单周期内高电平的持续时间来判断 0 或 1。

请看下面的高清无码美图。

 

上面这个就是 WS28XX 的时序图。在一个脉冲周期内,如果:

高电平持续时间为 0.4 微秒,低电平持续时间为 0.85 微秒,那么就是0;

高电平持续时间为 0.85 微秒,低电平持续时间为 0.4 微秒,那么就是1。

故一个周期的总时长为 1.25 微秒,即频率为 1000000 / 1.25 = 800 KHz,即 0.8 MHz

请记住这个频率,后面有用。

WS28XX的信号是连续发的,中间不需要停顿;如果出现超过 50 微秒的低电平,那么WS28XX会认为你的信号发送完毕。如果数据线上有数据来就会从第一个灯珠开始进行处理(等于更新整个灯带的数据)。

例如,有四个灯珠串联,每个灯的RGB有24位,那么,你需要向WS28XX连续发送 24 * 4 = 96 位数据,数据发送中不要停顿,所有数据都是连着发的—— 96位连续发,中间不用停顿;全部发完后再输出50微秒左右的低电平表示结束发送。

比如,红色,RGB 是 255,0,0,那么这24位就是:

1111 1111 0000 0000 0000 0000

如果有三个灯珠,第一个灯珠设置为绿色,第二个灯珠为白色,第三个灯珠为蓝色,那么三组RGB为:

【0,255,0】【255,255,255】【0,0,255】。于是,72位二进制为:

0000 0000 1111 1111 0000 0000 1111 1111 1111 1111 1111 1111 0000 0000 0000 0000 1111 1111

在发送的时候,这72位是连续发送的,中间不需要延时,发完后把电平拉低,50 us后WS28XX就会认为你已经发完数据。

 

 正如你所看到的,WS28XX 的通信协议比较简单。但是,问题出在它的时间很短。你如果纯手动写代码来改变电平的高低,要求程序有很高的性能。低配的单片机可能不够快,像树莓派这样的开发板,虽然处理器肯定比单片机快,但是代码传递到系统驱动,再由驱动传到底层硬件。而且每次切换电平需要来回两次通信,花的时间太长,都有可能超出 1.25 us 的周期。

所以,一般不采取直接写GPIO电平的方式通信,而是借助硬件上所支持的协议。能够做这事的硬件协议有俩:

1、PWM。这个估计大家能理解,前面老周说要记住那个通信频率,现在用上了。把PWM频率设为 800 KHz,然后一个周期的时长就是 1.25 us。最后你一定猜到了,发送1时,高电平持续0.85 us,占空比 68%;发送0时,高电平持续0.4 us,占空比 32%。于是呢,不断地改变占空比,就能给WS28XX发信号。许多方案是和DMA一起使用的,就是为了提高速度。.NET Iot 封装的 PWM 不支持 DMA 方式,因此这个方案跳过。

2、SPI,这个方案是目前最优秀方案。.NET Iot 封装的库也是采用 SPI 协议。这是个巧妙利用,将SPI的 MOSI 口与灯带的 Din 连接,让 SPI 时序来控制灯珠。

为啥不用 IIC (I2C)呢?因为 IIC 要从机地址啊,我们又不是真的使用 IIC 设备,所以这里不适合。根据官方的示例,使用SPI时要设置以下几个参数:

a、速率 2400 KHz,正好是 800 K 的三倍。于是乎,SPI 中 3 个二进制位对应WS28XX中 1 个二进制位。要发送 1 ,SPI 写入二进制 110;要发送 0 ,SPI 写入二进制 100。一个灯珠的数据是3字节24位,那么 SPI 就得写入72位(9个字节)。

 b、SPI 模式选择 Mode0。

c、数据长度为8位。

因为树莓派是支持硬件 SPI 的,所以 SPI 方案是没有问题的。若开发板没有硬件 SPI(靠软件模拟),就不能使用SPI方案了,速度跟不上。

 

 =======================================================================

知道原理后,咱们可以动手做了。

第1步,引用 Iot.Device.Bindings 包(Nuget),依赖包 System.Device.Gpio 会自动引入,所以不用管它。

第2步,using 以下命名空间:

using System.Device.Spi;
using System.Drawing;
using Iot.Device.Graphics;
using Iot.Device.Ws28xx;

第3步,初始化 SpiDevice 对象。

SpiConnectionSettings setting = new(0)
{
    Mode = SpiMode.Mode0,
    ClockFrequency = 2400_000,  //注意速率
    DataBitLength = 8
};

using SpiDevice spidev = SpiDevice.Create(setting);

把 ClockFrequency 设置为 2400,000 hz,原因前面解释过。

第4步,老周买的灯带是 WS2812B 芯片的,所以,实例化 WS28XX 时用 Ws2812b 类,大部分灯带都是这个。

// 30表示使用30个灯珠,这个按实际来传值,要用到50个灯珠,就传50
const int LED_NUM = 30;
Ws28xx leds = new Ws2812b(spidev, LED_NUM);

老周现用来测试的灯带是五米长的,150 个灯,但为了省电,只选择了 30 个灯做实验,你可以根据实际情况改代码。咱们这里用的灯带,所以只考虑宽度,高度忽略;如果用的是点阵屏,则要考虑高度,即几行几列。

 

第5步,准备七种常用颜色,待会咱们做色彩轮换效果。

Color[] colors =
{
    Color.Blue,         // 蓝色
    Color.Yellow,       // 黄色
    Color.White,        // 白色
    Color.Red,          // 红色
    Color.Green,        // 绿色
    Color.Pink,         // 粉色
    Color.Orange        // 橙色
};

 

第6步,色彩轮换,七种颜色轮着显示。

// Color数组中正在使用的索引
int theIndex = 0;

// 进入循环,开始溜灯
while (true)
{
    // 当表示索引的值超出数组范围后,重回0
    if (theIndex > colors.Length - 1)
    {
        theIndex = 0;
    }
    // 获取位图对象
    BitmapImage image = leds.Image;
    // 循环修改每个灯珠的颜色
    for (int x = 0; x < LED_NUM; x++)
    {
        // 更新像素点(对应一个灯珠)
        image.SetPixel(x, 0, colors[theIndex]);
        // 调用更新,才会刷新灯带显示
        leds.Update();
        Thread.Sleep(10);
    }
    theIndex++;
    Thread.Sleep(1000);
}

可以把一个灯珠视为一个像素,先从 Image 属性中获得对 BitmapImage 对象的引用,然后用 SetPixel 方法来设置每个灯的颜色。这里因为用的是灯带,所以 y 坐标都是 0,仅改变 x 坐标上的值。

修改完颜色后,如果想灯珠马上更新,请调用 Update 方法。

最后,强调一下:接线时,树莓派的 MOSI 接灯带的数据接口(Din 或 Di)。

 

 如果SPI不能用,请执行 sudo raspi-config,然后进入“Interface Options”,确认 SPI 总线启用。

 

 看看最终效果。

【补充】如果出现程序运行几秒钟就“卡死”的情况,那是SPI被系统降频了所致。若遇到此问题,可以在 config.txt 文件中修改 GPU 超频参数。

sudo nano /boot/config.txt

加上这两行:

core_freq=250
core_freq_min=250

或者

core_freq=500
core_freq_min=250

这样配置后,至少保证了 GPU 内核在空闲时的频率不低于 250 MHz。

 

除了变色、流水,还有比较常用的特效是颜色渐变。颜色渐变算法我们可以自己写,老周会在下一篇水文中演示颜色渐变效果。 

 

posted @ 2021-12-14 17:30  东邪独孤  阅读(4323)  评论(8编辑  收藏  举报