學 Go 玩創客,TinyGo 初體驗(3):使用 BBC micro:bit 與 TinyGo drivers 驅動周邊元件,Part I

Photo by Alex Holyoake on Unsplash

上篇:學 Go 玩創客,TinyGo 初體驗(2):Arduino Uno/Nano 的數位與類比信號控制

在看過前兩篇後,各位對於如何使用 TinyGo 來控制開發板,應該已有基本概念了。然而,如同我們在第一篇結尾說的,8 位元的 Arduino Uno/Nano 因為記憶體太小,以至於很難做更進一步的應用,比如使用以 32 位元開發板為目標的 TinyGo drivers。

目前 TinyGo 支援的 32 位元開發板以 SAMD21/51 及 nRF52 微控制器為大宗,但這類板子在台灣能買到的不多,通常也較貴。有意思的是,當中其實有個特例,而且出身令人有點意想不到 — — 2016 年針對兒童程式教育而推出、使用 nRF51(Cortex M0+)處理器的 BBC micro:bit

Why micro:bit?

micro:bit 在全球各地區都有經銷,因此這塊板子在台灣的小學程式課程也相當熱門。微軟與 Python 基金會分別提供了線上版的 MakeCode 積木編輯器(TypeScript)和 MicroPython 編輯器,此外也有大量來自對岸的 micro:bit 擴充教材出現。

不過,micro:bit 可不是小朋友的專利;它的規格(16 KB SRAM、256 KB 快閃記憶體)看似普通,但拿來運行 TinyGo 就綽綽有餘,而且還內建有按鈕、LED 燈陣列及加速計/磁力計。此外,它採用 KL26 微處理器來提供(模擬的)USB 磁碟介面。

micro:bit 在設計時,就是希望能簡化上傳程式的過程,好讓兒童也能輕鬆設定 micro:bit。因此,它採用了 ARM mbed 設計的獨特雙晶片系統。

micro:bit 上的另一個微處理器 KL26 會藉由稱為 DAL(Device Abstraction Layer)的韌體,在電腦上產生出一個貌似 USB 磁碟機的東西,而使用者只要將他們從線上編輯器下載的 .hex 檔拖曳或貼到該磁碟區,KL26 就會將程式傳給主處理器 nRF51。如此一來,使用者根本不需安裝任何驅動程式,而且只要複製貼上就行了。

(當然,這種拖曳動作對中低年級的小學生來說,其實還是有點吃力的 XD。不過這就不是本文的討論重點了。)

Photo by Annie Spratt on Unsplash

在使用 TinyGo 時,micro:bit 這種硬體架構帶來了幾個意想不到的好處:

  1. 既然不需像其他開發板一樣透過序列埠上傳程式,你可以一邊開著序列埠終端機讀 micro:bit 的 UART 輸出,一邊透過命令提示字元或系統終端機寫入新程式。
  2. 不須安裝額外的燒錄程式。
  3. 甚至,在使用 tinygo flash 指令上傳程式時,你還不必指定 -port 參數。(就目前筆者在 Windows 上測試是如此。)除非是你同時接了一些其他板子,無法燒錄時會要求你指定 port。

除了 micro:bit 以外,只有 Adafruit 家的 SAMD21/51 開發板能做到非常接近的效果;SAMD21/51 內建有 USB 介面(正常下你必須按 reset 兩次才能進入燒錄模式),而 Adafruit 在這些板子上安裝的 UF2 bootloader 能直接模擬出 USB 碟(真的 USB 碟),讓使用者能同樣以拖曳. uf2 編譯檔的方式上傳之。當然,這樣就又回到之前的老問題:在台灣買 Adafruit 產品不容易,而且貴上太多了。

就筆者目前試用幾種板子玩 TinyGo 的經驗,micro:bit 用起來意外地方便。這再加上 micro:bit 相對容易購買、硬體規格也不錯的優點,筆者認為 micro:bit 跟 TinyGo 幾乎堪稱絕配。此外,不管是舊版的 v1.3b 或新的 v1.5,板子上的加速計與羅盤感測器也都有 TinyGo drivers 的加持。

Photo by Mohammad Metri on Unsplash

目前 TinyGo 對 micro:bit 還不支援的地方

  1. 無法使用藍芽或「廣播」。
  2. 沒有 ADC(類比輸入)。
  3. 有驅動程式能用來控制其 5x5 LED 矩陣,但還沒有能顯示文字的字庫。

準備材料

你需要一片 BBC micro:bit,以及一塊 micro:bit 用的腳位擴充板(edge connector)。你不需購買那種很貴、裝有電池或一堆感測器的擴充板,只要能將所有腳位轉成針腳即可。

筆者使用的是 SparkFun micro:bit Breakout,這擴充板可直接插在 400 孔麵包板上。當然,目前市面上也有許多中國或台灣製的便宜腳位擴充板。

要注意的是,SparkFun 擴充板重新排列了 micro:bit 的腳位順序,而一般腳位擴充板並不會這麼做。此外這篇我大部分的接線圖沒有特別畫出麵包板,畢竟如何接線不是本文的討論重點。

micro:bit 腳位

這裡來稍微談一下 micro:bit 的腳位:P3、P4、P6、P7、P9、P10 是與板子上的 LED 燈陣列共用,所以平常沒事最好別用它們。P5 與 P11 分別和按鈕 A/B 共用,P19 與 P20 是 I2C 腳位。所以正常下能直接使用的腳位就是 P0-P2、P8、P12–16。(若要使用 SPI,就會用到 P13–15。)

micro:bit 腳位的輸出電壓為 3.3V,最大輸出電流 5 mA(因此就算不用電阻,直接接 LED 也絕不會燒壞)。3V 腳位可輸出的電流至多 90 mA,這對一般外接元件而言夠用了,但要拿來驅動馬達之類的裝置就會比較困難。

可讀類比信號的腳位為 P0 至 P4,可惜目前 TinyGo 尚未實作 micro:bit 的類比信號讀取功能。

共時性 LED 控制

第一個範例先回頭來看 Blinky 範例的一個變形版 — — 這回我們要控制 2 個外接 LED 以不同部的速度閃爍。這正是 Go 語言最出名的特色之一:concurrency(共時性)。這也是先前在使用 Arduino Uno 時所無法使用的功能。

  1. LED 1 正極 -> P1
  2. LED 2 正極 -> P2
  3. LED 負極 -> G (Gnd)
package mainimport (
"machine"
"time"
)
func main() {
go led1()
led2()
}
func led1() {
led := machine.P1
led.Configure(machine.PinConfig{Mode: machine.PinOutput})
for {
led.High()
delay(500)
led.Low()
delay(500)
}
}
func led2() {
led := machine.P2
led.Configure(machine.PinConfig{Mode: machine.PinOutput})
for {
led.High()
delay(400)
led.Low()
delay(400)
}
}
func delay(t uint16) {
time.Sleep(time.Duration(1000000 * uint32(t)))
}

假如要上傳的原始檔為 main.go(其位置請參考本系列第一篇),命令提示字元或終端機的指令即為

tinygo flash -target microbit main

你真想要的話,還是可以指定 -port:

tinygo flash -target microbit -port COMX main

上面的範例修改自 TinyGo 官方的 blinky2。要注意 TinyGo 定義了 machine.PX 來代表腳位 PX,這反映了 TinyGo 如何能針對腳位規格不同的板子來調整 machine 套件的內容。

在此有兩個函式,各自都會建立一個 led 腳位物件,並在無窮迴圈中閃爍它。然而,在 main() 中 go led1() 這句會把 led1() 變成一個 Goroutine,一個在背景跑的平行執行緒。結果就是你能看到兩個 LED 燈以不同的速度在閃爍,但不會卡住彼此。

其實 main() 自己就是個主要的 Goroutine。如果你想把 led1() 和 led2() 都變成 Goroutine 並在背景一起跑的話,main() 後面得加個小小的延遲,否則這些背景執行緒還來不及開始就結束了(因此也就根本不會執行):

func main() {
go led1()
go led2()
delay(100)
}
Photo by JOSHUA COLEMAN on Unsplash

補:目前要在 Arduino Uno 上跑 goroutine 也是可行的,但上傳程式時必須多加一個參數來啟用該功能(因為預設是關閉的):

tinygo flash -scheduler coroutines -target arduino -port COMX main

當然要注意的是 Uno 或 Nano 跑 Goroutine 的時間間隔可能會非常的…不準確(笑)所以最好還是在 32 位元的板子上使用吧。

不使用 time.Sleep 和共時性的延遲方法

如果不使用前面的辦法,另一個讓 LED 燈不同步閃爍的可能解法是用 time.Since() 來判斷程式執行後過了多少時間:

package mainimport (
"machine"
"time"
)
func main() {
led1 := machine.P1
led2 := machine.P2
led1.Configure(machine.PinConfig{1})
led2.Configure(machine.PinConfig{1})
ledSwitch1 := true
ledSwitch2 := true
start1 := time.Now()
start2 := time.Now()
for {
if time.Since(start1) >= time.Millisecond*500 {
led.Set(ledSwitch1)
ledSwitch1 = !ledSwitch1
start1 = time.Now()
}
if time.Since(start2) >= time.Millisecond*400 {
led.Set(ledSwitch2)
ledSwitch2 = !ledSwitch2
start2 = time.Now()
}
}
}

time.Start() 會計算現在的時間(板子的開機時間)跟 start 變數的差距,於是這樣就能判斷什麼時候要做點事情。

可惜的是,time.Since() 在 AVR 開發板上無法傳回正確的值,所以這招沒法用在 Arduino Uno/Nano 上。

使用 TinyGo drivers

Photo by Louis Hansel @shotsoflouis on Unsplash

接下來我們會用到 TinyGo drivers。這是一系列周邊電子元件的 TinyGo 驅動程式,同樣以 Go 語言撰寫而成,總數還不算多,但已經足夠我們體驗了。

下載 TinyGo drivers

最簡單的下載方式是先安裝 Git,然後在命令提示字元或終端機輸入以下指令:

go get tinygo.org/x/drivers

這會把目前的 TinyGo drivers 最新版本下載到 GOPATH\src(若使用 Windows)或 /home/pi/go/src/(若使用 Linux)底下。將來如果有更新,刪除下載的檔案並重新下載就行了。

筆者在安裝 Go 1.16 之後,發現 TinyGo drivers 與其它套件會被 go get 安裝到 GOPATH\pkg\mod\ 底下。後面我會提一下在匯入套件時需做的額外步驟。

在 TinyGo drivers 下的目錄 examples 就是各驅動程式的範例程式。

NeoPixel(WS2812)RGB LED

又稱 NeoPixel 的 WS2812 RGB LED 燈條是個神奇的

東西,因為它只需用一個腳位就能控制多顆燈,而且還可以串聯,只要電力夠,想接多長就能多長。最重要的是 NeoPixel 用 3.3V 電壓也能點亮,亮度還狂勝普通 LED,是打造互動或藝術裝置很合適的材料。

電子材料行賣的多半是 8 或 12 顆燈的直線或環形模組,但也通常得自己焊接針腳就是了。

  1. Din -> P0
  2. Vcc -> 3V
  3. Gnd -> G

很多模組會有 Dout 腳位,如果你想接更多燈條的話,把 Dout 接到下個模組的 Din 腳位,並一樣接上電源/接地線即可。

package mainimport (
"image/color"
"machine"
"time"
"tinygo.org/x/drivers/ws2812"
)
const (
LED_PIN = machine.Pin(2)
LED_NUM = 12
LED_MAX = 64 // max 255
LED_ROTATE_DELAY = 50 // ms
)
func main() { LED_PIN.Configure(machine.PinConfig{Mode: machine.PinOutput})
neoPixel := ws2812.New(LED_PIN)
var leds [LED_NUM]color.RGBA
change := uint8(LED_MAX / (LED_NUM / 3))
index := [3]uint8{0, uint8(LED_NUM / 3), uint8(LED_NUM / 3 * 2)}
for i := range leds {led_color := [3]uint8{0, 0, 0}
for j := range index {
if abs(int8(uint8(i)-index[j])) <= index[1] {
led_color[j] = LED_MAX_LEVEL - abs(int8(uint8(i)-index[j]))*change
}
}
if uint8(i) >= index[2] {
led_color[0] = LED_MAX_LEVEL - (LED_NUM-uint8(i))*change
}
leds[i] = color.RGBA{R: led_color[0], G: led_color[1], B: led_color[2]}
} neoPixel.WriteColors(leds[:])
time.Sleep(time.Millisecond * 10)
for { var ledsTmp []color.RGBA ledsTmp = leds[1:LED_NUM]
ledsTmp = append(ledsTmp, leds[0])
copy(leds[:], ledsTmp[:])
neoPixel.WriteColors(leds[:])
time.Sleep(time.Millisecond * LED_ROTATE_DELAY)
}}func abs(x int8) uint8 {
if x < 0 {
return uint8(-x)
} else {
return uint8(x)
}
}

可以看到,程式內匯入了 WS2812 的 package。只要你的電腦上有下載 TinyGo drivers,編譯時就能抓到套件。

筆者安裝 Go 1.16 後,TinyGo drivers 的安裝位置改變了。你必須在專案資料夾下(開啟方式見本系列第一篇文章)於終端機輸入以下指令:

go mod init main

(最後一個是你的專案的套件名稱)

然後再輸入一次

go get tinygo.org/x/drivers/

在這之後就能正常抓到套件了。

另一個匯入的套件是 image/color,乃 Go 語言中用來處理色彩用的工具。色彩用 color.RGBA 結構來定義,我們之後還會再看到。

我們在這裡用型別為 color.RGBA 的陣列 colors 定義幾種基本色彩,然後用迴圈不斷切換 NeoPixel 的色彩。設定 NeoPixel 時務必記得,一定要把含有色彩資訊的陣列寫入給燈條(ws.WriteColors(neoLeds[:])),不然色彩的任何改變就不會生效。

此外,程式中的最大亮度只設為 64,因為 NeoPixel 全開起來還蠻刺眼的。另外就是全開太久,也會變燙或是增加燒壞的風險。

如果想增加一點視覺變化,讓燈條會一顆燈一顆燈逐次改變色彩,只要改變寫入資料到燈條的位置跟時間間隔就行了:

        for {
for i := range colors {
for j := range neoLeds {
neoLeds[j] = colors[i]
ws.WriteColors(neoLeds[:])
time.Sleep(time.Millisecond * 100)
}
}
}

--

--

--

Technical writer, former translator and IT editor. My writing power is 100% generated by coffee.

Love podcasts or audiobooks? Learn on the go with our new app.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Alan Wang

Alan Wang

Technical writer, former translator and IT editor. My writing power is 100% generated by coffee.

More from Medium

Mutex with timeout or channels in go

Nil Pointer Deference in Golang

GoReleaser v1.6 — the boring release