Go 語言在極小硬體上的運用(一)

2019-09-24 21:03:00

Go 語言,能在多低下的設定上執行並行揮作用呢?

我最近購買了一個特別便宜的開發板:

STM32F030F4P6

我購買它的理由有三個。首先,我(作為程式設計師)從未接觸過 STM320 系列的開發板。其次,STM32F10x 系列使用也有點少了。STM320 系列的 MCU 很便宜,有更新一些的外設,對系列產品進行了改進,問題修復也做得更好了。最後,為了這篇文章,我選用了這一系列中最低設定的開發板,整件事情就變得有趣起來了。

硬體部分

STM32F030F4P6 給人留下了很深的印象:

  • CPU: Cortex M0 48 MHz(最低設定,只有 12000 個邏輯閘電路)
  • RAM: 4 KB,
  • Flash: 16 KB,
  • ADC、SPI、I2C、USART 和幾個定時器

以上這些採用了 TSSOP20 封裝。正如你所見,這是一個很小的 32 位系統。

軟體部分

如果你想知道如何在這塊開發板上使用 Go 程式設計,你需要反複閱讀硬體規範手冊。你必須面對這樣的真實情況:在 Go 編譯器中給 Cortex-M0 提供支援的可能性很小。而且,這還僅僅只是第一個要解決的問題。

我會使用 Emgo,但別擔心,之後你會看到,它如何讓 Go 在如此小的系統上盡可能發揮作用。

在我拿到這塊開發板之前,對 stm32/hal 系列下的 F0 MCU 沒有任何支援。在簡單研究參考手冊後,我發現 STM32F0 系列是 STM32F3 削減版,這讓在新埠上開發的工作變得容易了一些。

如果你想接著本文的步驟做下去,需要先安裝 Emgo

cd $HOMEgit clone https://github.com/ziutek/emgo/cd emgo/egcgo install

然後設定一下環境變數

export EGCC=path_to_arm_gcc      # eg. /usr/local/arm/bin/arm-none-eabi-gccexport EGLD=path_to_arm_linker   # eg. /usr/local/arm/bin/arm-none-eabi-ldexport EGAR=path_to_arm_archiver # eg. /usr/local/arm/bin/arm-none-eabi-arexport EGROOT=$HOME/emgo/egrootexport EGPATH=$HOME/emgo/egpathexport EGARCH=cortexm0export EGOS=noosexport EGTARGET=f030x6

更詳細的說明可以在 Emgo 官網上找到。

要確保 egc 在你的 PATH 中。 你可以使用 go build 來代替 go install,然後把 egc 複製到你的 $HOME/bin/usr/local/bin 中。

現在,為你的第一個 Emgo 程式建立一個新資料夾,隨後把範例中連結器指令碼複製過來:

mkdir $HOME/firstemgocd $HOME/firstemgocp $EGPATH/src/stm32/examples/f030-demo-board/blinky/script.ld .

最基本程式

main.go 檔案中建立一個最基本的程式:

package mainfunc main() {}

檔案編譯沒有出現任何問題:

$ egc$ arm-none-eabi-size cortexm0.elf   text    data     bss     dec     hex filename   7452     172     104    7728    1e30 cortexm0.elf

第一次編譯可能會花點時間。編譯後產生的二進位制佔用了 7624 個位元組的 Flash 空間(文字 + 資料)。對於一個什麼都沒做的程式來說,占用的空間有些大。還剩下 8760 位元組,可以用來做些有用的事。

不妨試試傳統的 “Hello, World!” 程式:

package mainimport "fmt"func main() {    fmt.Println("Hello, World!")}

不幸的是,這次結果有些糟糕:

$ egc/usr/local/arm/bin/arm-none-eabi-ld: /home/michal/P/go/src/github.com/ziutek/emgo/egpath/src/stm32/examples/f030-demo-board/blog/cortexm0.elf section `.text' will not fit in region `Flash'/usr/local/arm/bin/arm-none-eabi-ld: region `Flash' overflowed by 10880 bytesexit status 1

“Hello, World!” 需要 STM32F030x6 上至少 32KB 的 Flash 空間。

fmt 包強制包含整個 strconvreflect 包。這三個包,即使在精簡版本中的 Emgo 中,佔用空間也很大。我們不能使用這個例子了。有很多的應用不需要好看的文字輸出。通常,一個或多個 LED,或者七段數碼管顯示就足夠了。不過,在第二部分,我會嘗試使用 strconv 包來格式化,並在 UART 上顯示一些數位和文字。

閃爍

我們的開發板上有一個與 PA4 引腳和 VCC 相連的 LED。這次我們的程式碼稍稍長了一些:

package mainimport (    "delay"    "stm32/hal/gpio"    "stm32/hal/system"    "stm32/hal/system/timer/systick")var led gpio.Pinfunc init() {    system.SetupPLL(8, 1, 48/8)    systick.Setup(2e6)    gpio.A.EnableClock(false)    led = gpio.A.Pin(4)    cfg := &gpio.Config{Mode: gpio.Out, Driver: gpio.OpenDrain}    led.Setup(cfg)}func main() {    for {        led.Clear()        delay.Millisec(100)        led.Set()        delay.Millisec(900)    }}

按照慣例,init 函數用來初始化和設定外設。

system.SetupPLL(8, 1, 48/8) 用來設定 RCC,將外部的 8 MHz 振盪器的 PLL 作為系統時鐘源。PLL 分頻器設定為 1,倍頻數設定為 48/8 =6,這樣系統時脈頻率為 48MHz。

systick.Setup(2e6) 將 Cortex-M SYSTICK 時鐘作為系統時鐘,每隔 2e6 次納秒執行一次(每秒鐘 500 次)。

gpio.A.EnableClock(false) 開啟了 GPIO A 口的時鐘。False 意味著這一時鐘在低功耗模式下會被禁用,但在 STM32F0 系列中並未實現這一功能。

led.Setup(cfg) 設定 PA4 引腳為開漏輸出。

led.Clear() 將 PA4 引腳設為低,在開漏設定中,開啟 LED。

led.Set() 將 PA4 設為高電平狀態,關掉LED。

編譯這個程式碼:

$ egc$ arm-none-eabi-size cortexm0.elf   text    data     bss     dec     hex filename   9772     172     168   10112    2780 cortexm0.elf

正如你所看到的,這個閃爍程式佔用了 2320 位元組,比最基本程式佔用空間要大。還有 6440 位元組的剩餘空間。

看看程式碼是否能執行:

$ openocd -d0 -f interface/stlink.cfg -f target/stm32f0x.cfg -c 'init; program cortexm0.elf; reset run; exit'Open On-Chip Debugger 0.10.0+dev-00319-g8f1f912a (2018-03-07-19:20)Licensed under GNU GPL v2For bug reports, read        http://openocd.org/doc/doxygen/bugs.htmldebug_level: 0adapter speed: 1000 kHzadapter_nsrst_delay: 100none separateadapter speed: 950 kHztarget halted due to debug-request, current mode: Thread xPSR: 0xc1000000 pc: 0x0800119c msp: 0x20000da0adapter speed: 4000 kHz** Programming Started **auto erase enabledtarget halted due to breakpoint, current mode: Thread xPSR: 0x61000000 pc: 0x2000003a msp: 0x20000da0wrote 10240 bytes from file cortexm0.elf in 0.817425s (12.234 KiB/s)** Programming Finished **adapter speed: 950 kHz

在這篇文章中,這是我第一次,將一個短視訊轉換成動畫 PNG。我對此印象很深,再見了 YouTube。 對於 IE 使用者,我很抱歉,更多資訊請看 apngasm。我本應該學習 HTML5,但現在,APNG 是我最喜歡的,用來播放回圈短視訊的方法了。

STM32F030F4P6

更多的 Go 語言程式設計

如果你不是一個 Go 程式設計師,但你已經聽說過一些關於 Go 語言的事情,你可能會說:“Go 語法很好,但跟 C 比起來,並沒有明顯的提升。讓我看看 Go 語言的通道和協程!”

接下來我會一一展示:

import (    "delay"    "stm32/hal/gpio"    "stm32/hal/system"    "stm32/hal/system/timer/systick")var led1, led2 gpio.Pinfunc init() {    system.SetupPLL(8, 1, 48/8)    systick.Setup(2e6)    gpio.A.EnableClock(false)    led1 = gpio.A.Pin(4)    led2 = gpio.A.Pin(5)    cfg := &gpio.Config{Mode: gpio.Out, Driver: gpio.OpenDrain}    led1.Setup(cfg)    led2.Setup(cfg)}func blinky(led gpio.Pin, period int) {    for {        led.Clear()        delay.Millisec(100)        led.Set()        delay.Millisec(period - 100)    }}func main() {    go blinky(led1, 500)    blinky(led2, 1000)}

程式碼改動很小: 新增了第二個 LED,上一個例子中的 main 函數被重新命名為 blinky 並且需要提供兩個引數。 main 在新的協程中先呼叫 blinky,所以兩個 LED 燈在並行使用。值得一提的是,gpio.Pin 可以同時存取同一 GPIO 口的不同引腳。

Emgo 還有很多不足。其中之一就是你需要提前規定 goroutines(tasks) 的最大執行數量。是時候修改 script.ld 了:

ISRStack = 1024;MainStack = 1024;TaskStack = 1024;MaxTasks = 2;INCLUDE stm32/f030x4INCLUDE stm32/loadflashINCLUDE noos-cortexm

棧的大小需要靠猜,現在還不用關心這一點。

$ egc$ arm-none-eabi-size cortexm0.elf   text    data     bss     dec     hex filename  10020     172     172   10364    287c cortexm0.elf

另一個 LED 和協程一共佔用了 248 位元組的 Flash 空間。

STM32F030F4P6

通道

通道是 Go 語言中協程之間相互通訊的一種推薦方式。Emgo 甚至能允許通過中斷處理來使用緩衝通道。下一個例子就展示了這種情況。

package mainimport (    "delay"    "rtos"    "stm32/hal/gpio"    "stm32/hal/irq"    "stm32/hal/system"    "stm32/hal/system/timer/systick"    "stm32/hal/tim")var (    leds  [3]gpio.Pin    timer *tim.Periph    ch    = make(chan int, 1))func init() {    system.SetupPLL(8, 1, 48/8)    systick.Setup(2e6)    gpio.A.EnableClock(false)    leds[0] = gpio.A.Pin(4)    leds[1] = gpio.A.Pin(5)    leds[2] = gpio.A.Pin(9)    cfg := &gpio.Config{Mode: gpio.Out, Driver: gpio.OpenDrain}    for _, led := range leds {        led.Set()        led.Setup(cfg)    }    timer = tim.TIM3    pclk := timer.Bus().Clock()    if pclk < system.AHB.Clock() {        pclk *= 2    }    freq := uint(1e3) // Hz    timer.EnableClock(true)    timer.PSC.Store(tim.PSC(pclk/freq - 1))    timer.ARR.Store(700) // ms    timer.DIER.Store(tim.UIE)    timer.CR1.Store(tim.CEN)    rtos.IRQ(irq.TIM3).Enable()}func blinky(led gpio.Pin, period int) {    for range ch {        led.Clear()        delay.Millisec(100)        led.Set()        delay.Millisec(period - 100)    }}func main() {    go blinky(leds[1], 500)    blinky(leds[2], 500)}func timerISR() {    timer.SR.Store(0)    leds[0].Set()    select {    case ch <- 0:        // Success    default:        leds[0].Clear()    }}//c:__attribute__((section(".ISRs")))var ISRs = [...]func(){    irq.TIM3: timerISR,}

與之前例子相比較下的不同:

  1. 新增了第三個 LED,並連線到 PA9 引腳(UART 頭的 TXD 引腳)。
  2. 時鐘(TIM3)作為中斷源。
  3. 新函數 timerISR 用來處理 irq.TIM3 的中斷。
  4. 新增容量為 1 的緩衝通道是為了 timerISRblinky 協程之間的通訊。
  5. ISRs 陣列作為中斷向量表,是更大的異常向量表的一部分。
  6. blinky 中的 for 語句被替換成 range 語句。

為了方便起見,所有的 LED,或者說它們的引腳,都被放在 leds 這個陣列裡。另外,所有引腳在被設定為輸出之前,都設定為一種已知的初始狀態(高電平狀態)。

在這個例子裡,我們想讓時鐘以 1 kHz 的頻率執行。為了設定 TIM3 預分頻器,我們需要知道它的輸入時脈頻率。通過參考手冊我們知道,輸入時脈頻率在 APBCLK = AHBCLK 時,與 APBCLK 相同,反之等於 2 倍的 APBCLK

如果 CNT 暫存器增加 1 kHz,那麼 ARR 暫存器的值等於更新事件(過載事件)在毫秒中的計數週期。 為了讓更新事件產生中斷,必須要設定 DIER 暫存器中的 UIE 位。CEN 位能啟動時鐘。

時鐘外設在低功耗模式下必須啟用,為了自身能在 CPU 處於休眠時保持執行: timer.EnableClock(true)。這在 STM32F0 中無關緊要,但對程式碼可移植性卻十分重要。

timerISR 函數處理 irq.TIM3 的中斷請求。timer.SR.Store(0) 會清除 SR 暫存器裡的所有事件標誌,無效化向 NVIC 發出的所有中斷請求。憑藉經驗,由於中斷請求無效的延時性,需要在程式一開始馬上清除所有的中斷標誌。這避免了無意間再次呼叫處理。為了確保萬無一失,需要先清除標誌,再讀取,但是在我們的例子中,清除標誌就已經足夠了。

下面的這幾行程式碼:

select {case ch <- 0:    // Successdefault:    leds[0].Clear()}

是 Go 語言中,如何在通道上非阻塞地傳送訊息的方法。中斷處理程式無法一直等待通道中的空餘空間。如果通道已滿,則執行 default,開發板上的LED就會開啟,直到下一次中斷。

ISRs 陣列包含了中斷向量表。//c:__attribute__((section(".ISRs"))) 會導致連結器將陣列插入到 .ISRs 節中。

blinkyfor 迴圈的新寫法:

for range ch {    led.Clear()    delay.Millisec(100)    led.Set()    delay.Millisec(period - 100)}

等價於:

for {    _, ok := <-ch    if !ok {        break // Channel closed.    }    led.Clear()    delay.Millisec(100)    led.Set()    delay.Millisec(period - 100)}

注意,在這個例子中,我們不在意通道中收到的值,我們只對其接受到的訊息感興趣。我們可以在宣告時,將通道元素型別中的 int 用空結構體 struct{} 來代替,傳送訊息時,用 struct{}{} 結構體的值代替 0,但這部分對新手來說可能會有些陌生。

讓我們來編譯一下程式碼:

$ egc$ arm-none-eabi-size cortexm0.elf   text    data     bss     dec     hex filename  11096     228     188   11512    2cf8 cortexm0.elf

新的例子佔用了 11324 位元組的 Flash 空間,比上一個例子多佔用了 1132 位元組。

採用現在的時序,兩個閃爍協程從通道中獲取資料的速度,比 timerISR 傳送資料的速度要快。所以它們在同時等待新資料,你還能觀察到 select 的隨機性,這也是 Go 規範所要求的。

STM32F030F4P6

開發板上的 LED 一直沒有亮起,說明通道從未出現過溢位。

我們可以加快訊息傳送的速度,將 timer.ARR.Store(700) 改為 timer.ARR.Store(200)。 現在 timerISR 每秒鐘傳送 5 條訊息,但是兩個接收者加起來,每秒也只能接受 4 條訊息。

STM32F030F4P6

正如你所看到的,timerISR 開啟黃色 LED 燈,意味著通道上已經沒有剩餘空間了。

第一部分到這裡就結束了。你應該知道,這一部分並未展示 Go 中最重要的部分,介面。

協程和通道只是一些方便好用的語法。你可以用自己的程式碼來替換它們,這並不容易,但也可以實現。介面是Go 語言的基礎。這是文章中 第二部分所要提到的.

在 Flash 上我們還有些剩餘空間。