Go 最小硬件编程(第一部分)

我们能够让 Go 在多低的配置下运行并做一些实用的事情呢?

最近我购买了这个特别便宜的开发板:

购买它,我基于以下三个理由:第一,我(作为一个程序员)从未搞过 STM32F0 系列的开发板;第二,STM32F10x 系列的板子已经很陈旧了,STM32F0 系列的 MCU 十分便宜,有更新的外设,并有很多改进和 bug 修复;第三,我选择这个系列中最低配的是为了本文,这会让整个事情变得妙趣横生。

硬件

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 或是 7 段数码管显示就足够了。但是,在第二部分中,我将会尝试使用 strconv 包去格式化并在 UART 上打印一些数字或文本。

闪烁

我们的开发板有一个 LED 连接于 PA4 引脚和 VCC。这次我们编写多一点代码:

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,这样就提供 48 MHz 的系统频率。 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 是我喜欢的展现循环短视频的方式了。

更多 Go 编程

如果你不是一个 Go 的程序员,但是你已经听过 Go 语言的一些事情,你可能会说:“这种语法很好,但是相较于 C 没有明显的提升。给我展示 Go 语言channelsgoroutines!” 下面是代码:

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 在一个新的 goroutine 中启动第一个 blinky 函数,这样两个 LED 同时 并行 运行。有必要提一下,gpio.Pin 类型支持并发访问在同一 GPIO 口的不同引脚。 Emgo 仍然还有许多缺点。其中一个就是你必须提前对 goroutines(tasks)指定一个最大数值。是时候编辑一下 script.Id 了:

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 和 goroutine 花费了 248 字节的 Flash 空间。

Channels

Channels 是 Go 中 goroutines 之间通信 最好的方式。Emgo 做的更多,它允许通过 中断处理 去使用 缓冲 channels。下面的例子实际展示了这种情况。

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 方法处理 irp.TIM3 中断。
  4. 新增的容量为 1 的缓冲 channel 用于 timerISRblinky 协程之间进行通信。
  5. ISRs 数组作为中断向量表,是更大的异常向量表的一部分。
  6. blinky 的 for 语句 被替换为 range 语句。 为了方便,所有的 LED 或者其引脚都被集中放入 leds 数组中。除此之外,所有的引脚都已经在它们被配置为输出之前设置为已知的初始状态(高电平)。 在这个例子中,我们想计时器以 1kHz 跳动。为了配置 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 寄存器中的所有事件标志让 IRQ 到 NVIC 无效。根据经验规则一般是在处理程序开始时,立即清除中断标志,因为 IRQ 无效会有延时。这就阻止了不明所以的再次调用处理器的情况。为了完全放心,清除读序列应该被运行,但是在我们的例子中,清理一下就足够了。 以下代码:
    select {
    case ch <- 0:
     // Success
    default:
     leds[0].Clear()
    }
    

    是使用 Go 的方式在一个 channel 上非阻塞地发送消息。没有一个中断处理程序能够在等待 channel 中的空闲空间。如果 channel 满了,执行 default,那么开发板上 LED 被点亮,直到下一次中断。 ISRs 数组包含中断向量。//c:__attribute__((section(".ISRs"))) 会造成连接器将会把它插入到 .ISRs section 中。 新的 blinky 的 for 循环:

    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)
    }
    

    注意,在这个例子中,我们对从 channel 中接收到的值不感兴趣。我们只在意这里能够接收到东西就行。我们可以通过声明 channel 的元素类型给予它空的结构体表达式 struct{} 而不是 int,同时发送 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 字节。 使用当前的时序,两个 blinky goroutines 从 channel 消费的速度比 timerISR 发送给它的速度快得多。因此,它们同时等待新数据到来,你可以观察到 Go规范 所要求的 select 的随机性。

    开发板上的 LED 总是关闭的,因此 channel 没有出现溢出。 让我们来加快发送的速度,改变 timer.ARR.Store(700)timer.ARR.Store(200)。现在 timerISR 每秒发送 5 条数据,但是两个接收者每秒同时只能接收 4 条消息。

    正如你所看到的,timerISR 点亮了黄色 LED,意味着在 channel 中没有空间了。 到这里,我完成了本文的第一部分。你应该清除这一部分没有为你展示 Go 语言中最重要的东西,接口。 Goroutines 和 channels 是很棒很便捷的语法。你可以用你自己的代码替换它们 - 这不容易但是可行。接口是 Go 的本质,这就是我将在本文的第二部分开始的内容。 我们仍然有空闲的 Flash 空间。

(0)

相关推荐

  • STM32的复用时钟何时开启呢?

    STM32的AFIO时钟真的是在开启引脚复用功能的时候开启吗?其实并不是~ 什么是复用? 我们知道,STM32有很多外设,这些外设的外部引脚都是与GPIO共用的.我们可以通过软件来配置引脚作为GPIO ...

  •  ISP IAP 详解与ISP IAP的实现

    本帖的大致内容分为三部分 1 STM32 IAP方式与ISP方式选择的具体实现操作步骤 2 ISP方式与实现过程 3 IAP方式与实现过程 ISP:In System Programing       ...

  • 浅谈嵌入式软件开发之影响嵌入式MCU中断延迟(Interrupt Latency)的因素总结

    内容提要 引言(何为中断延迟) 1. 存储中断向量表和中断ISR代码的存储器访问速度 2. CPU内核Cache命中(hit)与否 3. 中断ISR代码的复杂程度(是否需要压栈) 4. 中断ISR代码 ...

  • RISC-V其实是反潮流,但是……

    来源:内容由半导体行业观察(ID:icbank)编译自「Erik Engheim」,谢谢. 在1980年代,超级计算机的外观如下图所示.而Cray的半圆形则是80年代超级计算机的代名词.那就是一台超级 ...

  • Go 最小硬件编程(第三部分)

    本系列的第一部分和第二部分中讨论的大多数示例都是以一种或另一种方式闪烁 LED.起初它可能很有趣,但过了一段时间它变得有点无聊.让我们做一些更有趣的事情...... ......让我们点亮更多 LED ...

  • Go 最小硬件编程(第二部分)

    在本文 第一部分 的结尾,我说过要写一下关于 接口 的东西.我不想在这里写一篇完整或是简短的关于接口的讲稿.相反,我将会举一个简单的例子,用以说明如何定义和使用接口,同时知道如何利用通用的 io.Wr ...

  • UG编程第一讲——编程工具讲解

    从零基础学习软件,会建模画图,2D转3D,优化模型结构,出图技巧,到软件基础加工,刀具设置,工序参数设置,加工模板定制以及后处理的安装与制作,加上工厂的各类产品零件的实际加工技巧,经验,加上工厂的各类 ...

  • 发烧攻略硬件篇” 第一集 赢得美女芳心的音响发烧攻略之必杀技!

    发烧攻略硬件篇” 第一集 赢得美女芳心的音响发烧攻略之必杀技!

  • AppInventor+掌控板:为硬件编程(1)

    一直以来都有读者询问是否有硬件控制类的案例,这也是我一直期待涉及的话题,但由于种种原因始终未能付诸行动.就在上个月,张路老师(roadlabs)收到了谢作如老师寄来的一块掌控板,并共同讨论了关于掌控板 ...

  • AppInventor+掌控板:为硬件编程(2)

    我们将开发一个声控灯应用,在安卓手机中,通过语音来控制一个LED灯的开关及亮度.用户对手机说"开灯"."关灯"."亮一点"."暗一 ...

  • 面向对象编程——第一单元回顾与感想

    一.作业结构分析 第一次作业: 类图(真·一类到底) 方法复杂度.类复杂度.类间依赖 第二次作业: 类图 方法复杂度.类复杂度.类间依赖 第三次作业: 类图 方法复杂度.类复杂度.类间依赖 结果一目了 ...

  • C语言对象编程第一弹:封装与抽象

    前言 上次整理了一篇关于面向对象的笔记:<什么是面向对象?>.简单地分享了面向对象的一些基础知识. C语言虽不是面向对象的语言,但也可以使用面向对象的思想来设计我们的程序. C语言 + 面 ...

  • 欧洲本土超级计算硬件迈出第一步!高性能计算RISC-V完成流片,样品测试"Hello World "成功显示!

    2018年欧洲推出了欧洲处理器计划 (EPI),旨在提高欧洲超级计算行业对外国科技公司的独立性.其核心是采用免费和开源的 RISC-V 指令集架构,用于在欧洲境内开发和生产高性能芯片,为欧盟在高性能计 ...