【Nano Framework ESP32篇】WS2812 彩色灯带实验
地球人皆知,许多物联网教程作者的心中都深爱着一灯大师,所以第一个例程总喜欢点灯,高级一点的会来个“一闪一闪亮晶晶”。老周今天要扯的也是和灯有关的,但不单纯地点个灯,那样实在不好玩,缺乏乐趣。老周打算舞个龙灯,哦不,是用 LED 彩色灯带给伙伴们整点炫酷乐子。
说到这LED彩灯,咱们常见到的有两类:
1、一卷一卷的灯带,灯带是软的,底部有背胶,可以随意贴(贴电脑机箱,贴储物柜,贴手办展示盒……);
2、点阵屏,其实跟灯带一个样,只是有个框架,做成矩阵。如 3 * 3、4 * 4 等。
驱动 IC 一般是 WS2812,此货体积甚小,便于寄生于每个灯珠内。所以,每个LED灯珠都可以单独控制。而且 WS2812 允许你把灯珠串联起来,发送给它的数据可以连续设置多个灯珠。颜色由 RGB 控制,即 24 位——设置灯珠颜色要向 WS2812 发送3个字节的数据。
其实,WS2812的驱动协议不那么复杂,随便查查资料就能懂的了。当然,Nano Framework 已经有封装好的驱动,咱们不需要自己写协议。对于 ESP32,有两种驱动方案:
1、SPI 方式,此法各种开发板通用。曾记否?老周写过在树莓派生用 SPI 驱动 WS2812 的水文;
2、RMT 方式,许多 ESP32 模组都支持RMT,可以用它来驱动 WS2812。RMT 说白了就是用来发送和接收红外编码的,比如,电视遥控器、电动马桶遥控器等(空调遥控器好像特殊一些,很多模块都解码不了)。RMT 由于可以在同一周期内设定高、低电平的持续时间,使得它也能用于驱动 WS2812。
实际上,PWM 也可以的,因为一个周期内的占空比也能设定高、低电平的持续时间。只是,在 PWM 连续发生时要频繁地更改占空比,对开发板的速度有要求,还要确保代码执行得够快。Nano Fw 毕竟是封装过的,性能上会有损失的。虽然使用第三方框加会带来一些性能上的削弱,但在应用层可以提升开发效率,就像写普通 .NET 程序那样。两者总是要有一个平衡的。当然了,还是那句话,如果用其他框架做不到的事情,就必须用官方的 idf 了。性能上肯定比脚本语言高的。
好了,基本理论准备完毕,接下来,咱们开始整活。
先来看看如何用 RMT 来驱动 WS2812 的。因为这个比较新奇,所以先介绍它。红外编码协议可能会劝退不少伙伴,但,你不必担心,毕竟咱们这里不是真的用红外通信,只是借助接口协议来给 WS2812 发数据罢了,不会涉及协议编码的。而且,Nano Framework 已经封装好了,用起来很省事,初始化只需要一条 new 语句就完事了。
A、硬件部分,ESP32 模块你可随意,只要有引出相关IO口就行,接口少也不要紧的,因为咱们顶多用一两根线(不含供电)。然后就是RGB灯带或者点阵屏。其实这两者本质上一样。老周这个灯带就是当初写树莓派文章时用的那个,ESP32 也是 3.6 年前买的,放在柜子里吃了几年尘螨。现在拿出来给大伙做演示,居然还能用,这质量可以的。
现在新点的开发板很多是 Type-C 接口的,不过老周这个毕竟是几年前的,是 Micro USB 接口的。啥接口没关系,可能就是找数据线麻烦些,现在很多手机是 C 口的。不过老周家里啥线都有,全都是绿联的。你没看错,老周所有数据线和转换器都是用绿联的。这不是广告,就是质量好。老周家里就是这样的:风扇是美的,因为电机静音;线材绿联的,墙插排插都是公牛的;音响器材铁三角或漫步者……老周不买小米的,小米其实就跟京东京造差不多,万能贴牌。老周记得,买硬盘唯一一次翻车的就是京造的。那个硬盘现在搭在树莓派上,专门放魔法少女动画片。
又扯远了,下面是步骤:
1、启动 VS;
2、新建 Nano 项目,选 Blank Application 项目模板就行了;
3、打开 Nuget 包管理器(方法自己百度),搜索 ws28xx esp32,然后你会找到一个【nanoFramework.Iot.Device.Ws28xx.Esp32】包。没错,它是专为 ESP32 封装,开箱即用(使用时引入 Iot.Device.Ws28xx.Esp32 命名空间)。
4、安装 nanoFramework.Iot.Device.Ws28xx.Esp32 包及相关依赖(一般自动安装);
这里介绍一下几个关键类。首先是 WS 芯片的公共基类 Ws28xx,在实例化时,咱们可以根据所使用的芯片选择派生类:Ws2812b 、Ws2812c、Sk6812 等,WS2812B 和 WS2812C 比较常见。至于你买的灯带是哪个 IC,还真不好说,比如老周这个其实是 WS2812C,可以卖家的商品介绍标注的是 WS2812B。问客服也没用的,多数是一问摇头三不知。客服客服,毫不客气地让你服得三观倒置。
这个可以上机测试,如果灯珠点亮后与你设置的颜色不对,可以换个类,比如,用 WS2812b 的结果不正确,可以换成 WS2812c 来试。WS2812b 和 WS2812c 的红色和绿色好像是反过来的。
接线就简单了,一根接 5V(没5V接口就接 3.3,或者单独供电),数据线只有一根,这里老周用 0 口,即 GPIO 0。你可以随便选其他接口,比如官方示例用的 GPIO 15。
public class Program { const int PxCount = 16; // 有多少个灯珠 const int DelayMS = 10; // 延时多少ms const int DataPin = 0; // 使用哪个IO口 // 入口点 public static void Main() { Ws28xx ws28xx = new Ws2812c(DataPin, PxCount); BitmapImage bmp = ws28xx.Image; //ws28xx.ClockDivider = 2; //ws28xx.ResetCommand = new RmtCommand(1800, false, 1850, false); //ws28xx.OnePulse = new RmtCommand(35, true, 16, false); //ws28xx.ZeroPulse = new(14, true, 34, false); //Debug.WriteLine($"分频:{ws28xx.ClockDivider}"); int i = default; int index1, index2; // 要设置的灯珠索引 while (true) { // 从两边向中间靠拢 for (i = 0; i < PxCount / 2; i++) { // 每次设置两颗灯珠 index1 = i; index2 = PxCount - 1 - i; bmp.SetPixel(index1, 0, Color.Red); bmp.SetPixel(index2, 0, Color.Red); ws28xx.Update(); // 更新 Thread.Sleep(DelayMS); } // 从中间向两侧扩散 for (i = PxCount / 2 - 1; i >= 0; i--) { index1 = i; index2 = PxCount - 1 - i; bmp.SetPixel(index1, 0, Color.Blue); bmp.SetPixel(index2, 0, Color.Blue); // 更新 ws28xx.Update(); Thread.Sleep(DelayMS); } } } }
Ws2812c 类的构造函数有三个参数:1、你用的IO号;2、宽度;3、高度。这里的宽高即灯珠个数,此处老周只点16个灯,多了怕供电不足(其实可以点更多灯珠)。第三个参数默认是1,所以如果高为1可以忽略。这个宽和高啥意思呢?对于灯带来说,你可以认为它永远只有一行,但有 N 列(N 是无限大正整数),即 width = N, height = 1。而点阵屏是矩阵,所以用点阵屏就可以设置宽和高。
Ws28xx类公开 Image 属性,类型为 BitmapImage 类(也是个通用基类)。咱们可以把LED彩灯视作一张位图,每个灯珠就是一个像素。所以,设置某个灯的颜色就要调用 SetPixel 方法,参数是x、y坐标,以及颜色。这个和普通 .NET 程序操作一样。
这个示例,老周实现的效果是:
1、红灯的点亮顺序是从两边往中间靠拢;
2、蓝灯的点亮顺序是从中间向两边扩展。
每当你修改了位图的数据,只是存在内存中,只有调用 WsXXX 实例的 Update 方法才会正式将数据发送出去。
这个点灯算法其实很简单,咱们学过数学,在等差数列中,1+n=2+(n-1)=3+(n-3)。本例中,16个灯,一半就是8,而索引是从0开始的,所以是7,即全序列的索引是 0 到 15,于是得到:0+15=15,1+14=15,2+13=15,3+12=15 …… 7+8=15。每次循环我们就设置相加等于最大索引的那两个灯。这样就能实现从中间向两边展开,顺序反过来就实现从两边向中间收拢。
运行的效果如下图所示。
细心的大伙伴会发现,上面的代码中有几行被注释掉了。这些代码用来设置参数的。不过 WS2812x 类会默认为咱们设置,除非发现默认设置的参数不正确时才要修改,
1、设置分频的分母。
ws28xx.ClockDivider = 2;
ClockDivider 用于设置分频,默认是2。目前支持的时钟是APB(外设的高级总线),频率是 80 MHz,分频2表示除以2=40MHz。也就是一个 Tick 的时间为 1/40000000 = 0.000000025 秒,换算为 0.025 微秒(us)。为什么分频不用 80 呢,这样一 Tick 就是 1 us岂不美哉?因为 WS28XX 的时序很短,比如发送 1 时,高电平持续时间为 0.7 us,低电平持续时间为 0.6 us,总时长在 1.3 us 左右。不过这类IC的时序有很多种版本,时序没有精确的时长。不管怎么说,这时间是要精确到 0.01 us的,所以,分频为80只精确到 1 us 显然不够用。分频为2精确到 0.025 us,基本能对付过去了。
2、设置高、低电平的持续时间。
ws28xx.ResetCommand = new RmtCommand(1800, false, 1850, false); ws28xx.OnePulse = new RmtCommand(35, true, 16, false); ws28xx.ZeroPulse = new(14, true, 34, false);
RmtCommand 类的构造函数是这样的:
RmtCommand(ushort duration1, bool level1, ushort duration2, bool level2)
咱们可以这样理解:一个周期内有两个电平,level1 和 level2,如果是true就是高电平否则低电平;duration1 描述 level1 的持续时间,duration2 描述 level2 的持续时间。假设持续时间是 100,如果分频是80,那么正好是 1us,可是咱们分频是2,就变成 100*0.025 us 了。毕竟分频后会变慢。
接下来看看 SPI 实现。
要安装这几个 Nuget 包:
1、nanoFramework.Iot.Device.Ws28xx
2、nanoFramework.Hardware.Esp32
3、nanoFramework.System.Device.Spi
其他的因为依赖关系会自动安装。
这个 iot 库里面,没有 Ws2812c 类,只有 ws2812b 类,但目前测试来看,能正常使用。
SPI 方案主要是用到了 MOSI 接口,虽然在初始化 SPI 总线时会设置几个引脚,但实际上只连接 MOSI 即可。
// 设置引脚的功能 Configuration.SetPinFunction(23, DeviceFunction.SPI1_MOSI); Configuration.SetPinFunction(19, DeviceFunction.SPI1_MISO); Configuration.SetPinFunction(18, DeviceFunction.SPI1_CLOCK); // 这里有两个参数:第一个是SPI总线ID,第二个是片选引脚,-1表示不使用 SpiConnectionSettings cset = new(1, -1) { Mode = SpiMode.Mode0, ClockFrequency = 2400000, // 通信频率 DataBitLength = 8 }; // 初始化SPI设备 SpiDevice spidev = SpiDevice.Create(cset); Ws28xx ws2812 = new Ws2812b(spidev, PxCount); // 获取图像对象 var bmp = ws2812.Image; int i = default; // 进入循环 while (true) { for(i=0; i<PxCount; i++) { bmp.SetPixel(i, 0, Color.Blue); ws2812.Update(); Thread.Sleep(DelayMS); } for(i=PxCount -1 ; i >= 0;i--) { bmp.SetPixel(i, 0, Color.Red); ws2812.Update(); Thread.Sleep(DelayMS); } for(i=0; i<PxCount; i++) { bmp.SetPixel(i,0,Color.Green); ws2812.Update(); Thread.Sleep(DelayMS); } for(i=PxCount -1 ;i >= 0;i--) { bmp.SetPixel(i, 0, Color.WhiteSmoke); ws2812.Update(); Thread.Sleep(DelayMS); } }
SPI 方式稍麻烦一点,用到的IO口有 18、19、23,而灯带只需连接 23 即可。注意在设置引脚功能时,如果选择 SPI1_MOSI、SPI1_CLOCK 等值,那说明用的是 SPI_1,在实例化 SpiConnectionSettings 对象时,busid 参数就是 1;如果设置功能时使用的是 SPI2_MOSI、SPI2_MISO、SPI2_CLOCK,那么实例化 SpiConnectionSettings 时 busid 是 2。虽然 ESP32 有四路 SPI,但前两路内部保留的,外设只用后两个,即 HSPI 和 VSPI,这两个名字也够奇葩的,其实用起来是一样。这破名字容易使人误认为 VSPI 是虚拟SPI,HSPI 是硬件SPI。
咱们在程序代码中指定的 SPI_1 和 SPI_2 就是 HSPI 和 VSPI。
本示例使用 32 个灯珠,在一轮循环中做四次填充:
1、从头到尾,填充蓝色;
2、从尾到头,填充红色;
3、从头到尾,填充绿色;
4、从尾到头,填充烟白色。
效果如下:
好了,今天就水到这里了。