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

在看過前兩篇後,各位對於如何使用 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 磁碟介面。

Image for post
Image for post

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

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

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

Image for post
Image for post
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 的加持。

Image for post
Image for post
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 腳位

Image for post
Image for post

這裡來稍微談一下 micro:bit 的腳位:P3、P4、P6、P7、P9、P10 是與板子上的 LED 燈陣列共用,所以平常沒事最好別用它們。P5 與 P11 分別和按鈕 A/B 共用,P19 與 P20 是 I2C 腳位。所以正常下能直接使用的腳位就是 。(若要使用 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)
Image for post
Image for post
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() 變成一個 ,一個在背景跑的平行執行緒。結果就是你能看到兩個 LED 燈以不同的速度在閃爍,但不會卡住彼此。

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

func main() {
go led1()
go led2()
delay(100)
}
Image for post
Image for post
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

Image for post
Image for post
Photo by Louis Hansel @shotsoflouis on Unsplash

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

下載 TinyGo drivers

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

go get tinygo.org/x/drivers

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

在 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 腳位,並一樣接上電源/接地線即可。

Image for post
Image for post
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 driver,編譯時就能抓到套件。

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

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

Image for post
Image for post

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

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

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

Written by

Former translator and currently a tech-book editor based in Taiwan. https://krantasblog.blogspot.com

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