【.NET 與樹莓派】控制彩色燈帶(WS28XX)

語言: CN / TW / HK

彩色燈帶,相信不用老周多說,大家都知道,沒準你家裡的燈牆裡面就有。老周的茅屋是早期建造的,所以沒有預留的燈槽,明燈的話是不好看的,因此老周家裡沒使用燈帶。不過,像櫃子後面,顯示器後面,書桌邊沿這些地方,可以貼彩色燈帶。書架上也貼了一些,因為那個書架是聖誕樹形狀的,沒辦法一條燈帶貼完,只能把它剪開 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 匯流排啟用。

看看最終效果。

除了變色、流水,還有比較常用的特效是顏色漸變。顏色漸變演算法我們可以自己寫,老週會在下一篇水文中演示顏色漸變效果。