這裡有著完全不邪惡,也很不正常的實驗記錄。

寫了一個控制WS2812與Arduino的程式(一)

前言

因為工作的關係,目前正在學習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發出對應的光芒。

但因為他的波形非常的短暫,就可以想到的作法,操作方式會有很多種:

  1. Firmware Delay(效率會不好,建議僅用在初期開發
  2. Timer 計時器(效率佳)
  3. Timer + DMA(沒試過,但相較於前兩個應該會好很多)
  4. 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
  • 之後會透過#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
  • 在這裡用到之前使用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反而會省下很多力氣。

參考資料


已發佈

分類:

,

作者:

標籤:

留言

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *