Go語(yǔ)言在極小硬件上的運(yùn)用(一)
Go 語(yǔ)言,能在多低下的配置上運(yùn)行并發(fā)揮作用呢?
我最近購(gòu)買了一個(gè)特別便宜的開發(fā)板:
STM32F030F4P6
我購(gòu)買它的理由有三個(gè)。首先,我(作為程序員)從未接觸過 STM320 系列的開發(fā)板。其次,STM32F10x 系列使用也有點(diǎn)少了。STM320 系列的 MCU 很便宜,有更新一些的外設(shè),對(duì)系列產(chǎn)品進(jìn)行了改進(jìn),問題修復(fù)也做得更好了。最后,為了這篇文章,我選用了這一系列中最低配置的開發(fā)板,整件事情就變得有趣起來(lái)了。
硬件部分
STM32F030F4P6 給人留下了很深的印象:
- CPU: Cortex M0 48 MHz(最低配置,只有 12000 個(gè)邏輯門電路)
- RAM: 4 KB,
- Flash: 16 KB,
- ADC、SPI、I2C、USART 和幾個(gè)定時(shí)器
以上這些采用了 TSSOP20 封裝。正如你所見,這是一個(gè)很小的 32 位系統(tǒng)。
軟件部分
如果你想知道如何在這塊開發(fā)板上使用 Go 編程,你需要反復(fù)閱讀硬件規(guī)范手冊(cè)。你必須面對(duì)這樣的真實(shí)情況:在 Go 編譯器中給 Cortex-M0 提供支持的可能性很小。而且,這還僅僅只是第一個(gè)要解決的問題。
我會(huì)使用 Emgo,但別擔(dān)心,之后你會(huì)看到,它如何讓 Go 在如此小的系統(tǒng)上盡可能發(fā)揮作用。
在我拿到這塊開發(fā)板之前,對(duì) stm32/hal 系列下的 F0 MCU 沒有任何支持。在簡(jiǎn)單研究參考手冊(cè)后,我發(fā)現(xiàn) STM32F0 系列是 STM32F3 削減版,這讓在新端口上開發(fā)的工作變得容易了一些。
如果你想接著本文的步驟做下去,需要先安裝 Emgo
cd $HOME
git clone https://github.com/ziutek/emgo/
cd emgo/egc
go install
然后設(shè)置一下環(huán)境變量
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
更詳細(xì)的說明可以在 Emgo 官網(wǎng)上找到。
要確保 egc
在你的 PATH
中。 你可以使用 go build
來(lái)代替 go install
,然后把 egc
復(fù)制到你的 $HOME/bin
或 /usr/local/bin
中。
現(xiàn)在,為你的第一個(gè) Emgo 程序創(chuàng)建一個(gè)新文件夾,隨后把示例中鏈接器腳本復(fù)制過來(lái):
mkdir $HOME/firstemgo
cd $HOME/firstemgo
cp $EGPATH/src/stm32/examples/f030-demo-board/blinky/script.ld .
最基本程序
在 main.go
文件中創(chuàng)建一個(gè)最基本的程序:
package main
func main() {
}
文件編譯沒有出現(xiàn)任何問題:
$ egc
$ arm-none-eabi-size cortexm0.elf
text data bss dec hex filename
7452 172 104 7728 1e30 cortexm0.elf
第一次編譯可能會(huì)花點(diǎn)時(shí)間。編譯后產(chǎn)生的二進(jìn)制占用了 7624 個(gè)字節(jié)的 Flash 空間(文本 + 數(shù)據(jù))。對(duì)于一個(gè)什么都沒做的程序來(lái)說,占用的空間有些大。還剩下 8760 字節(jié),可以用來(lái)做些有用的事。
不妨試試傳統(tǒng)的 “Hello, World!” 程序:
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}
不幸的是,這次結(jié)果有些糟糕:
$ 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
包強(qiáng)制包含整個(gè) strconv
和 reflect
包。這三個(gè)包,即使在精簡(jiǎn)版本中的 Emgo 中,占用空間也很大。我們不能使用這個(gè)例子了。有很多的應(yīng)用不需要好看的文本輸出。通常,一個(gè)或多個(gè) LED,或者七段數(shù)碼管顯示就足夠了。不過,在第二部分,我會(huì)嘗試使用 strconv
包來(lái)格式化,并在 UART 上顯示一些數(shù)字和文本。
閃爍
我們的開發(fā)板上有一個(gè)與 PA4 引腳和 VCC 相連的 LED。這次我們的代碼稍稍長(zhǎng)了一些:
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
函數(shù)用來(lái)初始化和配置外設(shè)。
system.SetupPLL(8, 1, 48/8)
用來(lái)配置 RCC,將外部的 8 MHz 振蕩器的 PLL 作為系統(tǒng)時(shí)鐘源。PLL 分頻器設(shè)置為 1,倍頻數(shù)設(shè)置為 48/8 =6,這樣系統(tǒng)時(shí)鐘頻率為 48MHz。
systick.Setup(2e6)
將 Cortex-M SYSTICK 時(shí)鐘作為系統(tǒng)時(shí)鐘,每隔 2e6 次納秒運(yùn)行一次(每秒鐘 500 次)。
gpio.A.EnableClock(false)
開啟了 GPIO A 口的時(shí)鐘。False
意味著這一時(shí)鐘在低功耗模式下會(huì)被禁用,但在 STM32F0 系列中并未實(shí)現(xiàn)這一功能。
led.Setup(cfg)
設(shè)置 PA4 引腳為開漏輸出。
led.Clear()
將 PA4 引腳設(shè)為低,在開漏設(shè)置中,打開 LED。
led.Set()
將 PA4 設(shè)為高電平狀態(tài),關(guān)掉LED。
編譯這個(gè)代碼:
$ egc
$ arm-none-eabi-size cortexm0.elf
text data bss dec hex filename
9772 172 168 10112 2780 cortexm0.elf
正如你所看到的,這個(gè)閃爍程序占用了 2320 字節(jié),比最基本程序占用空間要大。還有 6440 字節(jié)的剩余空間。
看看代碼是否能運(yùn)行:
$ 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
在這篇文章中,這是我第一次,將一個(gè)短視頻轉(zhuǎn)換成動(dòng)畫 PNG。我對(duì)此印象很深,再見了 YouTube。 對(duì)于 IE 用戶,我很抱歉,更多信息請(qǐng)看 apngasm。我本應(yīng)該學(xué)習(xí) HTML5,但現(xiàn)在,APNG 是我最喜歡的,用來(lái)播放循環(huán)短視頻的方法了。
STM32F030F4P6
更多的 Go 語(yǔ)言編程
如果你不是一個(gè) Go 程序員,但你已經(jīng)聽說過一些關(guān)于 Go 語(yǔ)言的事情,你可能會(huì)說:“Go 語(yǔ)法很好,但跟 C 比起來(lái),并沒有明顯的提升。讓我看看 Go 語(yǔ)言的通道和協(xié)程!”
接下來(lái)我會(huì)一一展示:
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)
}
代碼改動(dòng)很小: 添加了第二個(gè) LED,上一個(gè)例子中的 main
函數(shù)被重命名為 blinky
并且需要提供兩個(gè)參數(shù)。 main
在新的協(xié)程中先調(diào)用 blinky
,所以兩個(gè) LED 燈在并行使用。值得一提的是,gpio.Pin
可以同時(shí)訪問同一 GPIO 口的不同引腳。
Emgo 還有很多不足。其中之一就是你需要提前規(guī)定 goroutines(tasks)
的最大執(zhí)行數(shù)量。是時(shí)候修改 script.ld
了:
ISRStack = 1024;
MainStack = 1024;
TaskStack = 1024;
MaxTasks = 2;
INCLUDE stm32/f030x4
INCLUDE stm32/loadflash
INCLUDE noos-cortexm
棧的大小需要靠猜,現(xiàn)在還不用關(guān)心這一點(diǎn)。
$ egc
$ arm-none-eabi-size cortexm0.elf
text data bss dec hex filename
10020 172 172 10364 287c cortexm0.elf
另一個(gè) LED 和協(xié)程一共占用了 248 字節(jié)的 Flash 空間。
STM32F030F4P6
通道
通道是 Go 語(yǔ)言中協(xié)程之間相互通信的一種推薦方式。Emgo 甚至能允許通過中斷處理來(lái)使用緩沖通道。下一個(gè)例子就展示了這種情況。
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,
}
與之前例子相比較下的不同:
- 添加了第三個(gè) LED,并連接到 PA9 引腳(UART 頭的 TXD 引腳)。
- 時(shí)鐘(
TIM3
)作為中斷源。 - 新函數(shù)
timerISR
用來(lái)處理irq.TIM3
的中斷。 - 新增容量為 1 的緩沖通道是為了
timerISR
和blinky
協(xié)程之間的通信。 ISRs
數(shù)組作為中斷向量表,是更大的異常向量表的一部分。blinky
中的for
語(yǔ)句被替換成range
語(yǔ)句。
為了方便起見,所有的 LED,或者說它們的引腳,都被放在 leds
這個(gè)數(shù)組里。另外,所有引腳在被配置為輸出之前,都設(shè)置為一種已知的初始狀態(tài)(高電平狀態(tài))。
在這個(gè)例子里,我們想讓時(shí)鐘以 1 kHz 的頻率運(yùn)行。為了配置 TIM3 預(yù)分頻器,我們需要知道它的輸入時(shí)鐘頻率。通過參考手冊(cè)我們知道,輸入時(shí)鐘頻率在 APBCLK = AHBCLK
時(shí),與 APBCLK
相同,反之等于 2 倍的 APBCLK
。
如果 CNT 寄存器增加 1 kHz,那么 ARR 寄存器的值等于更新事件(重載事件)在毫秒中的計(jì)數(shù)周期。 為了讓更新事件產(chǎn)生中斷,必須要設(shè)置 DIER 寄存器中的 UIE 位。CEN 位能啟動(dòng)時(shí)鐘。
時(shí)鐘外設(shè)在低功耗模式下必須啟用,為了自身能在 CPU 處于休眠時(shí)保持運(yùn)行: timer.EnableClock(true)
。這在 STM32F0 中無(wú)關(guān)緊要,但對(duì)代碼可移植性卻十分重要。
timerISR
函數(shù)處理 irq.TIM3
的中斷請(qǐng)求。timer.SR.Store(0)
會(huì)清除 SR 寄存器里的所有事件標(biāo)志,無(wú)效化向 NVIC 發(fā)出的所有中斷請(qǐng)求。憑借經(jīng)驗(yàn),由于中斷請(qǐng)求無(wú)效的延時(shí)性,需要在程序一開始馬上清除所有的中斷標(biāo)志。這避免了無(wú)意間再次調(diào)用處理。為了確保萬(wàn)無(wú)一失,需要先清除標(biāo)志,再讀取,但是在我們的例子中,清除標(biāo)志就已經(jīng)足夠了。
下面的這幾行代碼:
select {
case ch <- 0:
// Success
default:
leds[0].Clear()
}
是 Go 語(yǔ)言中,如何在通道上非阻塞地發(fā)送消息的方法。中斷處理程序無(wú)法一直等待通道中的空余空間。如果通道已滿,則執(zhí)行 default
,開發(fā)板上的LED就會(huì)開啟,直到下一次中斷。
ISRs
數(shù)組包含了中斷向量表。//c:__attribute__((section(".ISRs")))
會(huì)導(dǎo)致鏈接器將數(shù)組插入到 .ISRs
節(jié)中。
blinky
的 for
循環(huán)的新寫法:
for range ch {
led.Clear()
delay.Millisec(100)
led.Set()
delay.Millisec(period - 100)
}
等價(jià)于:
for {
_, ok := <-ch
if !ok {
break // Channel closed.
}
led.Clear()
delay.Millisec(100)
led.Set()
delay.Millisec(period - 100)
}
注意,在這個(gè)例子中,我們不在意通道中收到的值,我們只對(duì)其接受到的消息感興趣。我們可以在聲明時(shí),將通道元素類型中的 int
用空結(jié)構(gòu)體 struct{}
來(lái)代替,發(fā)送消息時(shí),用 struct{}{}
結(jié)構(gòu)體的值代替 0,但這部分對(duì)新手來(lái)說可能會(huì)有些陌生。
讓我們來(lái)編譯一下代碼:
$ egc
$ arm-none-eabi-size cortexm0.elf
text data bss dec hex filename
11096 228 188 11512 2cf8 cortexm0.elf
新的例子占用了 11324 字節(jié)的 Flash 空間,比上一個(gè)例子多占用了 1132 字節(jié)。
采用現(xiàn)在的時(shí)序,兩個(gè)閃爍協(xié)程從通道中獲取數(shù)據(jù)的速度,比 timerISR
發(fā)送數(shù)據(jù)的速度要快。所以它們?cè)谕瑫r(shí)等待新數(shù)據(jù),你還能觀察到 select
的隨機(jī)性,這也是 Go 規(guī)范所要求的。
STM32F030F4P6
開發(fā)板上的 LED 一直沒有亮起,說明通道從未出現(xiàn)過溢出。
我們可以加快消息發(fā)送的速度,將 timer.ARR.Store(700)
改為 timer.ARR.Store(200)
。 現(xiàn)在 timerISR
每秒鐘發(fā)送 5 條消息,但是兩個(gè)接收者加起來(lái),每秒也只能接受 4 條消息。
STM32F030F4P6
正如你所看到的,timerISR
開啟黃色 LED 燈,意味著通道上已經(jīng)沒有剩余空間了。
第一部分到這里就結(jié)束了。你應(yīng)該知道,這一部分并未展示 Go 中最重要的部分,接口。
協(xié)程和通道只是一些方便好用的語(yǔ)法。你可以用自己的代碼來(lái)替換它們,這并不容易,但也可以實(shí)現(xiàn)。接口是Go 語(yǔ)言的基礎(chǔ)。
在 Flash 上我們還有些剩余空間。