Linux中國

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

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 $HOME
git clone https://github.com/ziutek/emgo/
cd emgo/egc
go install

然後設置一下環境變數

export EGCC=path_to_arm_gcc      # eg. /usr/local/arm/bin/arm-none-eabi-gcc
export EGLD=path_to_arm_linker   # eg. /usr/local/arm/bin/arm-none-eabi-ld
export EGAR=path_to_arm_archiver # eg. /usr/local/arm/bin/arm-none-eabi-ar

export EGROOT=$HOME/emgo/egroot
export EGPATH=$HOME/emgo/egpath

export EGARCH=cortexm0
export EGOS=noos
export EGTARGET=f030x6

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

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

現在,為你的第一個 Emgo 程序創建一個新文件夾,隨後把示例中鏈接器腳本複製過來:

mkdir $HOME/firstemgo
cd $HOME/firstemgo
cp $EGPATH/src/stm32/examples/f030-demo-board/blinky/script.ld .

最基本程序

main.go 文件中創建一個最基本的程序:

package main

func 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 main

import "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 bytes
exit status 1

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

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

閃爍

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

package main

import (
    "delay"

    "stm32/hal/gpio"
    "stm32/hal/system"
    "stm32/hal/system/timer/systick"
)

var led gpio.Pin

func 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 v2
For bug reports, read
        http://openocd.org/doc/doxygen/bugs.html
debug_level: 0
adapter speed: 1000 kHz
adapter_nsrst_delay: 100
none separate
adapter speed: 950 kHz
target halted due to debug-request, current mode: Thread 
xPSR: 0xc1000000 pc: 0x0800119c msp: 0x20000da0
adapter speed: 4000 kHz
** Programming Started **
auto erase enabled
target halted due to breakpoint, current mode: Thread 
xPSR: 0x61000000 pc: 0x2000003a msp: 0x20000da0
wrote 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.Pin

func 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/f030x4
INCLUDE stm32/loadflash
INCLUDE 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 main

import (
    "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:
    // Success
default:
    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 上我們還有些剩餘空間。

via: https://ziutek.github.io/2018/03/30/go_on_very_small_hardware.html

作者:Michał Derkacz 譯者:wenwensnow 校對:wxy

本文由 LCTT 原創編譯,Linux中國 榮譽推出


本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive

對這篇文章感覺如何?

太棒了
0
不錯
0
愛死了
0
不太好
0
感覺很糟
0
雨落清風。心向陽

    You may also like

    Leave a reply

    您的郵箱地址不會被公開。 必填項已用 * 標註

    此站點使用Akismet來減少垃圾評論。了解我們如何處理您的評論數據

    More in:Linux中國