前言
因為工作的關係,目前正在學習Python GUI的設計,剛好手上有之前買的STM32開發版,於是就翻了之前買的小玩具來接下去玩。 由於我還不太了解ChibiOS的使用方法,於是我開始求助ChatGPT。嘗試用問答的方式來寫出控制WS2812的程式。 沒想到在產出程式的那一步,ChatGPT因為產出錯的程式碼而導致後續無法運作。在後面有邀糗ChatGPT重新產生一遍程式碼,但光靠我現在的知識量,很難讓他產生出正確的程式。
而且想到要花更多時間邊Debug邊找出正確的寫法,可能這個連假就這樣子過去了…QQ
後續我改變想法:先使用Arduino Core嘗試寫出可以動的Demo,先把基本的輪子(框架)造出來,後續要移植到其他的生態系(ex: ChibiOS)會方便許多。
使用元件
- Arduino Uno R3
這是義大利原廠在2018年出的版本,但其實功能上沒什麼改變,所以用藍色的那一塊也可以。 - WS2812B
我買的是這種環形的LED,如果要用長條型的,一樣可以使用。


WS2812控制方式
WS2812B的控制方式是採用OneWire發送訊號的方式來控制LED內部暫存器,進而讓LED發出對應的光芒。

但因為他的波形非常的短暫,就可以想到的作法,操作方式會有很多種:
- Firmware Delay(效率會不好,建議僅用在初期開發)
- Timer 計時器(效率佳)
- Timer + DMA(沒試過,但相較於前兩個應該會好很多)
- Etc…
當然,現在有很多大神/團隊把上面複雜的操作方式包成Library (函式庫),如果要快速成型的話,可以直接到Github / Google搜一圈,找一個符合你的需求來使用即可。 我使用的是FastLED這個函式庫,在這裡感謝製作這個函式庫的大神們。

架構圖

- 使用者會透過GUI調整WS2812的顏色與各種燈泡的亮滅,GUI會透過UART方式傳送「指令」至Arduino Uno / 其他MCU,再透過封裝好的Library控制WS2812。
- 指令方面,目前先規劃為6碼(意即6個8位元的資料)來當作GUI與Uno的溝通橋樑。後續會再根據需求擴充。指令格式如下:

Arduino 程式
初始化
#include <Arduino.h>
#include <FastLED.h>
#include <string.h>
#define SERIAL_BYTE 6
#define LED_PIN 2
#define LED_NUM 12
unsigned long mill = 0;
char serial_bytes[SERIAL_BYTE] = {};
bool set_led = false;
CRGB leds[LED_NUM];
- 前三行會引入需要的函式庫,讓編譯器知道我們後續需要的函式。分別為:
- Arduino Core函式庫
Arduino.h
- 以及FastLED函式庫
FastLED.h
- 標準函式庫
String.h
- Arduino Core函式庫
- 之後會透過
#define
定義三個巨集,分別是:- 指令的長度
SERIAL_BYTE
- WS2812接的PIN腳位置
LED_PIN
- 串接WS2812的個數
LED_NUM
- 指令的長度
- 再來會宣告四個變數,並將它們都初始化:
- 取代
delay()
的millis(詳細在這裡有介紹過) - 指令字元陣列
serial_bytes
(後續都是HEX進行操控,用char
即可) - 一個操作鎖
set_led
:作用是在控制LED的時候不會因為其他指令送進來而被干擾 - CRGB結構的陣列
leds
:裡面記載每顆LED的顏色配置。
- 取代
setup()
void setup()
{
FastLED.addLeds<WS2812, LED_PIN, GRB>(leds, LED_NUM);
FastLED.clearData();
FastLED.show();
Serial.begin(115200);
Serial.setTimeout(100); // 100ms
}
- 首先使用
FastLED.addLeds()
增加LED配置,在泛型裡面填入LED的型號,LED的腳位與顏色定義。之後在括號裡面填入接WS2812 的腳位。- 為了怕每次上電都會有前一次的記錄造成顯示不正常,在這裡我使用
FastLED.clearData()
清掉記憶體內的值,並使用FastLED.show()
顯示新的狀態。
- 為了怕每次上電都會有前一次的記錄造成顯示不正常,在這裡我使用
- 之後使用
Serial.begin()
打開UART通訊功能,括號裡面填入你想設定的鮑率。- 由於會很快速的輸入指令,為了怕後續沒收到,這裡使用
Serial.setTimeout()
設定最久的讀取時間為100ms。
- 由於會很快速的輸入指令,為了怕後續沒收到,這裡使用
剛買的WS2812沒意外都會是GRB方式配置,在撰寫這部分的時候一定要去查看該LED的應用手冊/說明書。
loop()
void loop()
{
set_led = ReadSerial();
if(set_led)
{
SetupLED();
}
}
- 這邊我使用自己事先寫好的
ReadSerial()
讀取指令,並且回傳一個布林值,代表有無收到新的指令。 - 如果有收到指令,那會跳至
SetupLED()
進行更改LED的操作,以此循環。
ReadSerial()
bool ReadSerial()
{
unsigned long cmill = millis();
if (cmill - mill > 10)
{
mill = cmill;
// read serial bytes
if (Serial.readBytes(serial_bytes, SERIAL_BYTE) == SERIAL_BYTE)
{
if (serial_bytes[5] == 0x79)
{
Serial.flush();
return true;
}
}
}
return false;
}
- 使用
Serial.readBytes()
讀取透過UART輸入進來的資料- 如果接收到的資料長度等於事先設定的長度,且最後的校驗碼為正確,則會清除Serial裡面的Buffer,並且回傳
True
,反之回傳False
- 如果接收到的資料長度等於事先設定的長度,且最後的校驗碼為正確,則會清除Serial裡面的Buffer,並且回傳
- 在這裡用到之前使用Timer計時來取代Delay的技巧,讓執行更有效率(詳細在這裡有介紹過)
SetupLED()
void SetupLED()
{
if(serial_bytes[0] == 0x01 && serial_bytes[5] == 0x79) // Turn On
{
if (serial_bytes[1] == 0x00) // R
{
for(int i = 0; i < LED_NUM; i++)
leds[i] = CRGB(255, 0, 0);
FastLED.show();
}
else if (serial_bytes[1] == 0x01) // G
{
for(int i = 0; i < LED_NUM; i++)
leds[i] = CRGB(0, 255, 0);
FastLED.show();
}
else if (serial_bytes[1] == 0x02) // B
{
for(int i = 0; i < LED_NUM; i++)
leds[i] = CRGB(0, 0, 255);
FastLED.show();
}
else if (serial_bytes[1] == 0x03) // Custom
{
for(int i = 0; i < LED_NUM; i++)
leds[i] = CRGB(serial_bytes[2], serial_bytes[3], serial_bytes[4]);
FastLED.show();
}
}
else if (serial_bytes[0] == 0x00 && serial_bytes[5] == 0x79) // Turn Off
{
FastLED.clearData();
FastLED.show();
}
}
- 這裡判斷接收進來的資料欄位,透過
CRGB()
去設定每個LED的顏色,之後使用FastLED.show()
顯示出對應的顏色。
GUI 程式
GUI我使用Python與Flet框架來撰寫,目前的功能還算簡單,畢竟還僅僅是驗證這些功能可不可行。

其中讀取設定檔的程式碼如下:
start = time.time()
for li in parseData:
while True and li["time"] > 0:
end = time.time()
if round(end - start, 3) >= float(li["time"]):
break
barray = bytearray(li["light"])
self.ser.write(barray)
print("> Run Command: {}s\t{}".format(li["time"], li["light"]))
while (self.ser.writable() == False):
pass
在這裡也同樣用到上面的講過的技巧,利用Timer來去讀取秒數並做對比,之後再去讀取設定檔的指令並送給控制器作LED的輸出。 設定檔格式如下:
[
{
"time": 0,
"light": ["0x01", "0x00", "0x00", "0x00", "0x00", "0x79"]
}
]
之後也會嘗試實現編輯器的功能:若有表演的需求,可以對准MP3的時間點,在相對應的時間來變化LED的顏色與亮度。
過程與問題
ChatGPT
在一開始嘗試使用ChibiOS的時候,發現到ChatGPT產生出來的DMA程式段其實無法正常使用。由於產生出來的程式貌似有些老舊,需要經過一定的調整,將列舉的變數調整成目前ChibiOS版本定義的才可讓編譯器成功編譯。

void ws2812Start(WS2812Driver *ws2812p, const WS2812Config *config) {
uint16_t dma_channel = 0;
dma_channel = dmaStreamAllocate(config->dma, config->dma_stream, NULL, NULL);
dmaStreamSetPeripheral(dma_channel, &config->tim->CCR[config->channel]);
dmaStreamSetMemory0(dma_channel, ws2812p->dma_buffer);
dmaStreamSetTransactionSize(dma_channel, config->num_leds * 24);
// 這段出來的程式碼沒辦法被編譯器編譯
dmaStreamSetMode(dma_channel, DMA_PRIORITY_HIGH | DMA_MEMORY_TO_PERIPHERAL |
DMA_PERIPHERAL_DATA_SIZE_BYTE | DMA_MEMORY_DATA_SIZE_BYTE |
DMA_CIRCULAR | DMA_SxCR_MINC);
timDisableChannel(config->tim, config->channel);
timSetPeriod(config->tim, config->tim_period);
timSetPrescaler(config->tim, config->tim_prescaler);
timEnableChannel(config->tim, config->channel);
dmaStreamEnable(dma_channel);
ws2812p->config = config;
}
但不是說ChatGPT就是垃圾,ChatGPT倒是幫忙我在了解並學期其他領域的時候有一定的貢獻。雖然找出來的東西有些還是有點問題,但只要Google一下,基本上都可以找出正確或者可以使用的解答。 後續我先放棄ChibiOS的方法,先嘗試使用Arduino Uno + FastLED的方案,讓LED亮起來再說。
操控到一半,LED卡住
目前GUI的指令傳送部分沒有做優化,只是單純的指令傳輸,但GUI使用多執行緒方式實現。若動作一快,很容易讓指令傳送單元卡住。
後續我在傳送單元內部多寫了一個變數self._isoperate = False
當作目前有在傳送指令的一道鎖。
def sendSerial(self,
mode: str,
wsval: list = [0, 0, 0]):
# 檢查有無連線至Arduino,以及目前指令有無在傳送。
if (self.ser is not None) and (self.ser.is_open == True) and (self._isoperate == False):
# 將 _isoperate 設置為 True,代表正在傳送
self._isoperate = True
# /// 傳送段部分 ///
# 將 _isoperate 設置為 False,代表傳送完畢
self._isoperate = False
當前端調整拉桿時,若後端的指令還在傳送,則不會做傳送的動作,等到上一道指令都傳送完畢,才會進行下一次指令的傳送。 但,這不會是最好的方法,因為可能傳送的會是一連串讓LED漸變顏色的指令,若發送的動作一快,很容易因為指令還沒傳送完畢,而讓LED變化的不是很順暢。還有很多優化的空間。
結論
當初這個專案只是某天突然想利用ChibiOS來操控看看WS2812,雖然過程中因為ChatGPT正常發揮,以及我對ChibiOS還不是這麼了解,後續改用Arduino Core來實現。但目前已經可以正常的可以根據我想要的模式來讓WS2812呈現對應的亮度。

後續會先將軟體控制端完善,之後會嘗試更改指令部分,可能會參考大部分無人機通用的傳輸協議MavLink來實現。由於MavLink有兩個ID區塊,分別是`SysID`與`CompID`,若未來有要走控制多個LED控制器,使用MavLink反而會省下很多力氣。

發佈留言