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

  1. cd $HOME
  2. git clone https://github.com/ziutek/emgo/
  3. cd emgo/egc
  4. go install

然后设置一下环境变量

  1. export EGCC=path_to_arm_gcc # eg. /usr/local/arm/bin/arm-none-eabi-gcc
  2. export EGLD=path_to_arm_linker # eg. /usr/local/arm/bin/arm-none-eabi-ld
  3. export EGAR=path_to_arm_archiver # eg. /usr/local/arm/bin/arm-none-eabi-ar
  4.  
  5. export EGROOT=$HOME/emgo/egroot
  6. export EGPATH=$HOME/emgo/egpath
  7.  
  8. export EGARCH=cortexm0
  9. export EGOS=noos
  10. export EGTARGET=f030x6

更详细的说明可以在 Emgo 官网上找到。

egcPATHgo buildgo installegc$HOME/bin/usr/local/bin

现在,为你的第一个 Emgo 程序创建一个新文件夹,随后把示例中链接器脚本复制过来:

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

最基本程序

main.go
  1. package main
  2.  
  3. func main() {
  4. }

文件编译没有出现任何问题:

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

第一次编译可能会花点时间。编译后产生的二进制占用了 7624 个字节的 Flash 空间(文本 + 数据)。对于一个什么都没做的程序来说,占用的空间有些大。还剩下 8760 字节,可以用来做些有用的事。

不妨试试传统的 “Hello, World!” 程序:

  1. package main
  2.  
  3. import "fmt"
  4.  
  5. func main() {
  6. fmt.Println("Hello, World!")
  7. }

不幸的是,这次结果有些糟糕:

  1. $ egc
  2. /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'
  3. /usr/local/arm/bin/arm-none-eabi-ld: region `Flash' overflowed by 10880 bytes
  4. exit status 1

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

fmtstrconvreflectstrconv

闪烁

我们的开发板上有一个与 PA4 引脚和 VCC 相连的 LED。这次我们的代码稍稍长了一些:

  1. package main
  2.  
  3. import (
  4. "delay"
  5.  
  6. "stm32/hal/gpio"
  7. "stm32/hal/system"
  8. "stm32/hal/system/timer/systick"
  9. )
  10.  
  11. var led gpio.Pin
  12.  
  13. func init() {
  14. system.SetupPLL(8, 1, 48/8)
  15. systick.Setup(2e6)
  16.  
  17. gpio.A.EnableClock(false)
  18. led = gpio.A.Pin(4)
  19.  
  20. cfg := &gpio.Config{Mode: gpio.Out, Driver: gpio.OpenDrain}
  21. led.Setup(cfg)
  22. }
  23.  
  24. func main() {
  25. for {
  26. led.Clear()
  27. delay.Millisec(100)
  28. led.Set()
  29. delay.Millisec(900)
  30. }
  31. }
init
system.SetupPLL(8, 1, 48/8)
systick.Setup(2e6)
gpio.A.EnableClock(false)False
led.Setup(cfg)
led.Clear()
led.Set()

编译这个代码:

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

正如你所看到的,这个闪烁程序占用了 2320 字节,比最基本程序占用空间要大。还有 6440 字节的剩余空间。

看看代码是否能运行:

  1. $ openocd -d0 -f interface/stlink.cfg -f target/stm32f0x.cfg -c 'init; program cortexm0.elf; reset run; exit'
  2. Open On-Chip Debugger 0.10.0+dev-00319-g8f1f912a (2018-03-07-19:20)
  3. Licensed under GNU GPL v2
  4. For bug reports, read
  5. http://openocd.org/doc/doxygen/bugs.html
  6. debug_level: 0
  7. adapter speed: 1000 kHz
  8. adapter_nsrst_delay: 100
  9. none separate
  10. adapter speed: 950 kHz
  11. target halted due to debug-request, current mode: Thread
  12. xPSR: 0xc1000000 pc: 0x0800119c msp: 0x20000da0
  13. adapter speed: 4000 kHz
  14. ** Programming Started **
  15. auto erase enabled
  16. target halted due to breakpoint, current mode: Thread
  17. xPSR: 0x61000000 pc: 0x2000003a msp: 0x20000da0
  18. wrote 10240 bytes from file cortexm0.elf in 0.817425s (12.234 KiB/s)
  19. ** Programming Finished **
  20. adapter speed: 950 kHz

在这篇文章中,这是我第一次,将一个短视频转换成动画 PNG。我对此印象很深,再见了 YouTube。 对于 IE 用户,我很抱歉,更多信息请看 apngasm。我本应该学习 HTML5,但现在,APNG 是我最喜欢的,用来播放循环短视频的方法了。

 

STM32F030F4P6

更多的 Go 语言编程

如果你不是一个 Go 程序员,但你已经听说过一些关于 Go 语言的事情,你可能会说:“Go 语法很好,但跟 C 比起来,并没有明显的提升。让我看看 Go 语言的通道和协程!”

接下来我会一一展示:

  1. import (
  2. "delay"
  3.  
  4. "stm32/hal/gpio"
  5. "stm32/hal/system"
  6. "stm32/hal/system/timer/systick"
  7. )
  8.  
  9. var led1, led2 gpio.Pin
  10.  
  11. func init() {
  12. system.SetupPLL(8, 1, 48/8)
  13. systick.Setup(2e6)
  14.  
  15. gpio.A.EnableClock(false)
  16. led1 = gpio.A.Pin(4)
  17. led2 = gpio.A.Pin(5)
  18.  
  19. cfg := &gpio.Config{Mode: gpio.Out, Driver: gpio.OpenDrain}
  20. led1.Setup(cfg)
  21. led2.Setup(cfg)
  22. }
  23.  
  24. func blinky(led gpio.Pin, period int) {
  25. for {
  26. led.Clear()
  27. delay.Millisec(100)
  28. led.Set()
  29. delay.Millisec(period - 100)
  30. }
  31. }
  32.  
  33. func main() {
  34. go blinky(led1, 500)
  35. blinky(led2, 1000)
  36. }
mainblinkymainblinkygpio.Pin
goroutines(tasks)script.ld
  1. ISRStack = 1024;
  2. MainStack = 1024;
  3. TaskStack = 1024;
  4. MaxTasks = 2;
  5.  
  6. INCLUDE stm32/f030x4
  7. INCLUDE stm32/loadflash
  8. INCLUDE noos-cortexm

栈的大小需要靠猜,现在还不用关心这一点。

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

另一个 LED 和协程一共占用了 248 字节的 Flash 空间。

 

STM32F030F4P6

通道

通道是 Go 语言中协程之间相互通信的一种推荐方式。Emgo 甚至能允许通过中断处理来使用缓冲通道。下一个例子就展示了这种情况。

  1. package main
  2.  
  3. import (
  4. "delay"
  5. "rtos"
  6.  
  7. "stm32/hal/gpio"
  8. "stm32/hal/irq"
  9. "stm32/hal/system"
  10. "stm32/hal/system/timer/systick"
  11. "stm32/hal/tim"
  12. )
  13.  
  14. var (
  15. leds [3]gpio.Pin
  16. timer *tim.Periph
  17. ch = make(chan int, 1)
  18. )
  19.  
  20. func init() {
  21. system.SetupPLL(8, 1, 48/8)
  22. systick.Setup(2e6)
  23.  
  24. gpio.A.EnableClock(false)
  25. leds[0] = gpio.A.Pin(4)
  26. leds[1] = gpio.A.Pin(5)
  27. leds[2] = gpio.A.Pin(9)
  28.  
  29. cfg := &gpio.Config{Mode: gpio.Out, Driver: gpio.OpenDrain}
  30. for _, led := range leds {
  31. led.Set()
  32. led.Setup(cfg)
  33. }
  34.  
  35. timer = tim.TIM3
  36. pclk := timer.Bus().Clock()
  37. if pclk < system.AHB.Clock() {
  38. pclk *= 2
  39. }
  40. freq := uint(1e3) // Hz
  41. timer.EnableClock(true)
  42. timer.PSC.Store(tim.PSC(pclk/freq - 1))
  43. timer.ARR.Store(700) // ms
  44. timer.DIER.Store(tim.UIE)
  45. timer.CR1.Store(tim.CEN)
  46.  
  47. rtos.IRQ(irq.TIM3).Enable()
  48. }
  49.  
  50. func blinky(led gpio.Pin, period int) {
  51. for range ch {
  52. led.Clear()
  53. delay.Millisec(100)
  54. led.Set()
  55. delay.Millisec(period - 100)
  56. }
  57. }
  58.  
  59. func main() {
  60. go blinky(leds[1], 500)
  61. blinky(leds[2], 500)
  62. }
  63.  
  64. func timerISR() {
  65. timer.SR.Store(0)
  66. leds[0].Set()
  67. select {
  68. case ch <- 0:
  69. // Success
  70. default:
  71. leds[0].Clear()
  72. }
  73. }
  74.  
  75. //c:__attribute__((section(".ISRs")))
  76. var ISRs = [...]func(){
  77. irq.TIM3: timerISR,
  78. }

与之前例子相比较下的不同:

TIM3timerISRirq.TIM3timerISRblinkyISRsblinkyforrange
leds
APBCLK = AHBCLKAPBCLKAPBCLK

如果 CNT 寄存器增加 1 kHz,那么 ARR 寄存器的值等于更新事件(重载事件)在毫秒中的计数周期。 为了让更新事件产生中断,必须要设置 DIER 寄存器中的 UIE 位。CEN 位能启动时钟。

timer.EnableClock(true)
timerISRirq.TIM3timer.SR.Store(0)

下面的这几行代码:

  1. select {
  2. case ch <- 0:
  3. // Success
  4. default:
  5. leds[0].Clear()
  6. }
default
ISRs//c:__attribute__((section(".ISRs"))).ISRs
blinkyfor
  1. for range ch {
  2. led.Clear()
  3. delay.Millisec(100)
  4. led.Set()
  5. delay.Millisec(period - 100)
  6. }

等价于:

  1. for {
  2. _, ok := <-ch
  3. if !ok {
  4. break // Channel closed.
  5. }
  6. led.Clear()
  7. delay.Millisec(100)
  8. led.Set()
  9. delay.Millisec(period - 100)
  10. }
intstruct{}struct{}{}

让我们来编译一下代码:

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

新的例子占用了 11324 字节的 Flash 空间,比上一个例子多占用了 1132 字节。

timerISRselect

 

STM32F030F4P6

开发板上的 LED 一直没有亮起,说明通道从未出现过溢出。

timer.ARR.Store(700)timer.ARR.Store(200)timerISR

 

STM32F030F4P6

timerISR

第一部分到这里就结束了。你应该知道,这一部分并未展示 Go 中最重要的部分,接口。

协程和通道只是一些方便好用的语法。你可以用自己的代码来替换它们,这并不容易,但也可以实现。接口是Go 语言的基础。这是文章中 第二部分所要提到的.

在 Flash 上我们还有些剩余空间。