Michał Derkacz 发布的文章

在本系列的 第一第二 部分中讨论的大多数示例都是以某种方式闪烁的 LED。起初它可能很有趣,但是一段时间后变得有些无聊。让我们做些更有趣的事情……

…让我们点亮更多的 LED!

STM32F030F4P6

WS281x LED

WS281x RGB LED(及其克隆品)非常受欢迎。你可以以单个元素购买、链成长条或组装成矩阵、环或其他形状。

WS2812B

它们可以串联连接,基于这个事实,你可以只用 MCU 的单个引脚就可以控制一个很长的 LED 灯条。不幸的是,它们的内部控制器使用的物理协议不能直接适用于你在 MCU 中可以找到的任何外围设备。你必须使用 位脉冲 bit-banging 或以特殊方式使用可用的外设。

哪种可用的解决方案最有效取决于同时控制的 LED 灯条数量。如果你必须驱动 4 到 16 个灯条,那么最有效的方法是 使用定时器和 DMA(请不要忽略这篇文章末尾的链接)。

如果只需要控制一个或两个灯条,请使用可用的 SPI 或 UART 外设。对于 SPI,你只能在发送的一个字节中编码两个 WS281x 位。由于巧妙地使用了起始位和停止位,UART 允许更密集的编码:每发送一个字节 3 位。

我在 此站点 上找到了有关 UART 协议如何适用于 WS281x 协议的最佳解释。如果你不懂波兰语,这里是 英文翻译

基于 WS281x 的 LED 仍然是最受欢迎的,但市场上也有 SPI 控制的 LED:APA102SK9822。关于它们的三篇有趣的文章在这里:123

LED 环

市场上有许多基于 WS2812 的环。我有一个这样的:

WS2812B

它具有 24 个可单独寻址的 RGB LED(WS2812B),并暴露出四个端子:GND、5V、DI 和 DO。通过将 DI(数据输入)端子连接到上一个的 DO(数据输出)端子,可以链接更多的环或其他基于 WS2812 的东西。

让我们将这个环连接到我们的 STM32F030 板上。我们将使用基于 UART 的驱动程序,因此 DI 应连接到 UART 接头连接器上的 TXD 引脚。 WS2812B LED 需要至少 3.5V 的电源。 24 个 LED 会消耗大量电流,因此在编程/调试期间,最好将环上的 GND 和 5V 端子直接连接到 ST-LINK 编程器上可用的 GND 和 5V 引脚:

WS2812B

我们的 STM32F030F4P6 MCU 和整个 STM32 F0、F3、F7、L4 系列具有 F1、F4、L1 MCU 不具备的一项重要功能:它可以反转 UART 信号,因此我们可以将环直接连接到 UART TXD 引脚。如果你不知道我们需要这种反转,那么你可能没有读过我上面提到的 文章

因此,你不能以这种方式使用流行的 Blue PillSTM32F4-DISCOVERY。使用其 SPI 外设或外部反相器。有关使用 SPI 的 NUCLEO-F411RE,请参见 圣诞树灯 项目作为 UART + 逆变器的示例或 WS2812示例

顺便说一下,大多数 DISCOVERY 板可能还有一个问题:它们在 VDD = 3V 而不是 3.3V 的情况下工作。 对于高 DI,WS281x 至少要求电源电压 * 0.7。如果是 5V 电源,则为 3.5V;如果是 4.7V 电源,则为 3.3V;可在 DISCOVERY 的 5V 引脚上找到。如你所见,即使在我们的情况下,第一个 LED 的工作电压也低于规格 0.2V。对于 DISCOVERY 板,如果供电 4.7V,它将工作在低于规格的 0.3V 下;如果供电 5V,它将工作在低于规格 0.5V 下。

让我们结束这段冗长的介绍并转到代码:

package main

import (
    "delay"
    "math/rand"
    "rtos"

    "led"
    "led/ws281x/wsuart"

    "stm32/hal/dma"
    "stm32/hal/gpio"
    "stm32/hal/irq"
    "stm32/hal/system"
    "stm32/hal/system/timer/systick"
    "stm32/hal/usart"
)

var tts *usart.Driver

func init() {
    system.SetupPLL(8, 1, 48/8)
    systick.Setup(2e6)

    gpio.A.EnableClock(true)
    tx := gpio.A.Pin(9)

    tx.Setup(&gpio.Config{Mode: gpio.Alt})
    tx.SetAltFunc(gpio.USART1_AF1)

    d := dma.DMA1
    d.EnableClock(true)

    tts = usart.NewDriver(usart.USART1, d.Channel(2, 0), nil, nil)
    tts.Periph().EnableClock(true)
    tts.Periph().SetBaudRate(3000000000 / 1390)
    tts.Periph().SetConf2(usart.TxInv)
    tts.Periph().Enable()
    tts.EnableTx()

    rtos.IRQ(irq.USART1).Enable()
    rtos.IRQ(irq.DMA1_Channel2_3).Enable()
}

func main() {
    var rnd rand.XorShift64
    rnd.Seed(1)
    rgb := wsuart.GRB
    strip := wsuart.Make(24)
    black := rgb.Pixel(0)
    for {
        c := led.Color(rnd.Uint32()).Scale(127)
        pixel := rgb.Pixel(c)
        for i := range strip {
            strip[i] = pixel
            tts.Write(strip.Bytes())
            delay.Millisec(40)
        }
        for i := range strip {
            strip[i] = black
            tts.Write(strip.Bytes())
            delay.Millisec(20)
        }
    }
}

func ttsISR() {
    tts.ISR()
}

func ttsDMAISR() {
    tts.TxDMAISR()
}

//c:__attribute__((section(".ISRs")))
var ISRs = [...]func(){
    irq.USART1:          ttsISR,
    irq.DMA1_Channel2_3: ttsDMAISR,
}

导入部分

与前面的示例相比,导入部分中的新内容是 rand/math 包和带有 led/ws281x 子树的 led 包。 led 包本身包含 Color 类型的定义。 led/ws281x/wsuart 定义了 ColorOrderPixelStrip 类型。

我想知道如何使用 image/color 中的 ColorRGBA 类型,以及如何以它将实现 image.Image 接口的方式定义 Strip。 但是由于使用了 gamma 校正 和 大开销的 color/draw 包,我以简单的方式结束:

type Color uint32
type Strip []Pixel

使用一些有用的方法。然而,这种情况在未来可能会改变。

init 函数

init 函数没有太多新颖之处。 UART 波特率从 115200 更改为 3000000000/1390 ≈ 2158273,相当于每个 WS2812 位 1390 纳秒。 CR2 寄存器中的 TxInv 位设置为反转 TXD 信号。

main 函数

XorShift64 伪随机数生成器用于生成随机颜色。 XORSHIFT 是目前由 math/rand 包实现的唯一算法。你必须使用带有非零参数的 Seed 方法显式初始化它。

rgb 变量的类型为 wsuart.ColorOrder,并设置为 WS2812 使用的 GRB 颜色顺序(WS2811 使用 RGB 顺序)。然后用于将颜色转换为像素。

wsuart.Make(24) 创建 24 像素的初始化条带。它等效于:

strip := make(wsuart.Strip, 24)
strip.Clear()

其余代码使用随机颜色绘制类似于 “Please Wait…” 微调器的内容。

strip 切片充当帧缓冲区。 tts.Write(strip.Bytes()) 将帧缓冲区的内容发送到环。

中断

该程序由处理中断的代码组成,与先前的 UART 示例 中的代码相同。

让我们编译并运行:

$ egc
$ arm-none-eabi-size cortexm0.elf
   text    data     bss     dec     hex filename
  14088     240     204   14532    38c4 cortexm0.elf
$ openocd -d0 -f interface/stlink.cfg -f target/stm32f0x.cfg -c 'init; program cortexm0.elf; reset run; exit'

我跳过了 openocd 的输出。下面的视频显示了该程序的工作原理:

让我们做些有用的事情...

第一部分 的开头,我曾问过:“Go 能深入到多低层,而还能做一些有用的事情?”。 我们的 MCU 实际上是一种低端设备(8 比特的人可能会不同意我的看法),但到目前为止,我们还没有做任何有用的事情。

所以... 让我们做些有用的事情... 让我们做个时钟!

在互联网上有许多由 RGB LED 构成的时钟示例。让我们用我们的小板子和 RGB 环制作自己的时钟。我们按照下面的描述更改先前的代码。

导入部分

删除 math/rand 包,然后添加 stm32/hal/exti

全局变量

添加两个新的全局变量:btnbtnev

var (
    tts   *usart.Driver
    btn   gpio.Pin
    btnev rtos.EventFlag
)

它们将用来处理那些用于设置时钟的 “按钮”。我们的板子除了重置之外没有其他按钮,但是如果没有它,我们仍然可以通过某种方式进行管理。

init 函数

将这段代码添加到 init 函数:

btn = gpio.A.Pin(4)

btn.Setup(&gpio.Config{Mode: gpio.In, Pull: gpio.PullUp})
ei := exti.Lines(btn.Mask())
ei.Connect(btn.Port())
ei.EnableFallTrig()
ei.EnableRiseTrig()
ei.EnableIRQ()

rtos.IRQ(irq.EXTI4_15).Enable()

在内部 上拉电阻 pull-up resistor 启用的情况下,将 PA4 引脚配置为输入。它已连接至板载 LED,但这不会妨碍任何事情。更重要的是它位于 GND 引脚旁边,所以我们可以使用任何金属物体来模拟按钮并设置时钟。作为奖励,我们还有来自板载 LED 的其他反馈。

我们使用 EXTI 外设来跟踪 PA4 状态。它被配置为在发生任何更改时都会产生中断。

btnWait 函数

定义一个新的辅助函数:

func btnWait(state int, deadline int64) bool {
    for btn.Load() != state {
        if !btnev.Wait(1, deadline) {
            return false // timeout
        }
        btnev.Reset(0)
    }
    delay.Millisec(50) // debouncing
    return true
}

它等待 “按钮” 引脚上的指定状态,但只等到最后期限出现。这是稍微改进的轮询代码:

for btn.Load() != state {
    if rtos.Nanosec() >= deadline {
        // timeout
    }
}

我们的 btnWait 函数不是忙于等待 statedeadline,而是使用 rtos.EventFlag 类型的 btnev 变量休眠,直到有事情发生。你当然可以使用通道而不是 rtos.EventFlag,但是后者便宜得多。

main 函数

我们需要全新的 main 函数:

func main() {
    rgb := wsuart.GRB
    strip := wsuart.Make(24)
    ds := 4 * 60 / len(strip) // Interval between LEDs (quarter-seconds).
    adjust := 0
    adjspeed := ds
    for {
        qs := int(rtos.Nanosec() / 25e7) // Quarter-seconds since reset.
        qa := qs + adjust

        qa %= 12 * 3600 * 4 // Quarter-seconds since 0:00 or 12:00.
        hi := len(strip) * qa / (12 * 3600 * 4)

        qa %= 3600 * 4 // Quarter-seconds in the current hour.
        mi := len(strip) * qa / (3600 * 4)

        qa %= 60 * 4 // Quarter-seconds in the current minute.
        si := len(strip) * qa / (60 * 4)

        hc := led.Color(0x550000)
        mc := led.Color(0x005500)
        sc := led.Color(0x000055)

        // Blend the colors if the hands of the clock overlap.
        if hi == mi {
            hc |= mc
            mc = hc
        }
        if mi == si {
            mc |= sc
            sc = mc
        }
        if si == hi {
            sc |= hc
            hc = sc
        }

        // Draw the clock and write to the ring.
        strip.Clear()
        strip[hi] = rgb.Pixel(hc)
        strip[mi] = rgb.Pixel(mc)
        strip[si] = rgb.Pixel(sc)
        tts.Write(strip.Bytes())

        // Sleep until the button pressed or the second hand should be moved.
        if btnWait(0, int64(qs+ds)*25e7) {
            adjust += adjspeed
            // Sleep until the button is released or timeout.
            if !btnWait(1, rtos.Nanosec()+100e6) {
                if adjspeed < 5*60*4 {
                    adjspeed += 2 * ds
                }
                continue
            }
            adjspeed = ds
        }
    }
}

我们使用 rtos.Nanosec 函数代替 time.Now 来获取当前时间。这样可以节省大量的闪存,但也使我们的时钟变成了不知道日、月、年的老式设备,最糟糕的是它无法处理夏令时的变化。

我们的环有 24 个 LED,因此秒针的显示精度可以达到 2.5 秒。为了不牺牲这种精度并获得流畅的运行效果,我们使用 1/4 秒作为基准间隔。半秒就足够了,但四分之一秒更准确,而且与 16 和 48 个 LED 配合使用也很好。

红色、绿色和蓝色分别用于时针、分针和秒针。这允许我们使用简单的“逻辑或操作”进行颜色混合。我们 Color.Blend 方法可以混合任意颜色,但是我们闪存不多,所以我们选择最简单的解决方案。

我们只有在秒针移动时才重画时钟。

btnWait(0, int64(qs+ds)*25e7)

上面的这行代码等待的正是那一刻,或者是按钮的按下。

每按一下按钮就会把时钟向前调一调。按住按钮一段时间会加速调整。

中断

定义新的中断处理程序:

func exti4_15ISR() {
    pending := exti.Pending() & 0xFFF0
    pending.ClearPending()
    if pending&exti.Lines(btn.Mask()) != 0 {
        btnev.Signal(1)
    }
}

并将 irq.EXTI4_15: exti4_15ISR 条目添加到 ISR 数组。

该处理程序(或中断服务程序)处理 EXTI4\_15 IRQ。 Cortex-M0 CPU 支持的 IRQ 明显少于其较大的同类兄弟处理器,因此你经常可以看到一个 IRQ 被多个中断源共享。在我们的例子中,一个 IRQ 由 12 个 EXTI 线共享。

exti4\_15ISR 读取所有挂起的位,并从中选择 12 个更高的有效位。接下来,它清除 EXTI 中选中的位并开始处理它们。在我们的例子中,仅检查第 4 位。 btnev.Signal(1) 引发 btnev.Wait(1, deadline) 唤醒并返回 true

你可以在 Github 上找到完整的代码。让我们来编译它:

$ egc
$ arm-none-eabi-size cortexm0.elf
   text    data     bss     dec     hex filename
  15960     240     216   16416    4020 cortexm0.elf

这里所有的改进只得到 184 个字节。让我们再次重新构建所有内容,但这次在 typeinfo 中不使用任何类型和字段名:

$ cd $HOME/emgo
$ ./clean.sh
$ cd $HOME/firstemgo
$ egc -nf -nt
$ arm-none-eabi-size cortexm0.elf
   text    data     bss     dec     hex filename
  15120     240     216   15576    3cd8 cortexm0.elf

现在,有了千字节的空闲空间,你可以改进一些东西。让我们看看它是如何工作的:

我不知道我是怎么精确打到 3:00 的!?

以上就是所有内容!在第 4 部分(本系列的结束)中,我们将尝试在 LCD 上显示一些内容。(LCTT 译注:然而烂尾了,第三篇写于 2018 年,整个博客当年就停更了。)


via: https://ziutek.github.io/2018/05/03/go_on_very_small_hardware3.html

作者:Michał Derkacz 选题:lujun9972 译者:gxlct008 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

在本文的 第一部分 的结尾,我承诺要写关于接口的内容。我不想在这里写有关接口或完整或简短的讲义。相反,我将展示一个简单的示例,来说明如何定义和使用接口,以及如何利用无处不在的 io.Writer 接口。还有一些关于 反射 reflection 半主机 semihosting 的内容。

STM32F030F4P6]

接口是 Go 语言的重要组成部分。如果你想了解更多有关它们的信息,我建议你阅读《高效的 Go 编程》 和 Russ Cox 的文章

并发 Blinky – 回顾

当你阅读前面示例的代码时,你可能会注意到一中打开或关闭 LED 的反直觉方式。 Set 方法用于关闭 LED,Clear 方法用于打开 LED。这是由于在 漏极开路配置 open-drain configuration 下驱动了 LED。我们可以做些什么来减少代码的混乱?让我们用 OnOff 方法来定义 LED 类型:

type LED struct {
    pin gpio.Pin
}

func (led LED) On() {
    led.pin.Clear()
}

func (led LED) Off() {
    led.pin.Set()
}

现在我们可以简单地调用 led.On()led.Off(),这不会再引起任何疑惑了。

在前面的所有示例中,我都尝试使用相同的 漏极开路配置 open-drain configuration 来避免代码复杂化。但是在最后一个示例中,对于我来说,将第三个 LED 连接到 GND 和 PA3 引脚之间并将 PA3 配置为 推挽模式 push-pull mode 会更容易。下一个示例将使用以此方式连接的 LED。

但是我们的新 LED 类型不支持推挽配置,实际上,我们应该将其称为 OpenDrainLED,并定义另一个类型 PushPullLED

type PushPullLED struct {
    pin gpio.Pin
}

func (led PushPullLED) On() {
    led.pin.Set()
}

func (led PushPullLED) Off() {
    led.pin.Clear()
}

请注意,这两种类型都具有相同的方法,它们的工作方式也相同。如果在 LED 上运行的代码可以同时使用这两种类型,而不必注意当前使用的是哪种类型,那就太好了。 接口类型可以提供帮助:

package main

import (
    "delay"

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

type LED interface {
    On()
    Off()
}

type PushPullLED struct{ pin gpio.Pin }

func (led PushPullLED) On()  {
    led.pin.Set()
}

func (led PushPullLED) Off() {
    led.pin.Clear()
}

func MakePushPullLED(pin gpio.Pin) PushPullLED {
    pin.Setup(&gpio.Config{Mode: gpio.Out, Driver: gpio.PushPull})
    return PushPullLED{pin}
}

type OpenDrainLED struct{ pin gpio.Pin }

func (led OpenDrainLED) On()  {
    led.pin.Clear()
}

func (led OpenDrainLED) Off() {
    led.pin.Set()
}

func MakeOpenDrainLED(pin gpio.Pin) OpenDrainLED {
    pin.Setup(&gpio.Config{Mode: gpio.Out, Driver: gpio.OpenDrain})
    return OpenDrainLED{pin}
}

var led1, led2 LED

func init() {
    system.SetupPLL(8, 1, 48/8)
    systick.Setup(2e6)

    gpio.A.EnableClock(false)
    led1 = MakeOpenDrainLED(gpio.A.Pin(4))
    led2 = MakePushPullLED(gpio.A.Pin(3))
}

func blinky(led LED, period int) {
    for {
        led.On()
        delay.Millisec(100)
        led.Off()
        delay.Millisec(period - 100)
    }
}

func main() {
    go blinky(led1, 500)
    blinky(led2, 1000)
}

我们定义了 LED 接口,它有两个方法: OnOffPushPullLEDOpenDrainLED 类型代表两种驱动 LED 的方式。我们还定义了两个用作构造函数的 Make*LED 函数。这两种类型都实现了 LED 接口,因此可以将这些类型的值赋给 LED 类型的变量:

led1 = MakeOpenDrainLED(gpio.A.Pin(4))
led2 = MakePushPullLED(gpio.A.Pin(3))

在这种情况下, 可赋值性 assignability 在编译时检查。赋值后,led1 变量包含一个 OpenDrainLED{gpio.A.Pin(4)},以及一个指向 OpenDrainLED 类型的方法集的指针。 led1.On() 调用大致对应于以下 C 代码:

led1.methods->On(led1.value)

如你所见,如果仅考虑函数调用的开销,这是相当廉价的抽象。

但是,对接口的任何赋值都会导致包含有关已赋值类型的大量信息。对于由许多其他类型组成的复杂类型,可能会有很多信息:

$ egc
$ arm-none-eabi-size cortexm0.elf
   text    data     bss     dec     hex filename
  10356     196     212   10764    2a0c cortexm0.elf

如果我们不使用 反射,可以通过避免包含类型和结构字段的名称来节省一些字节:

$ egc -nf -nt
$ arm-none-eabi-size cortexm0.elf
   text    data     bss     dec     hex filename
  10312     196     212   10720    29e0 cortexm0.elf

生成的二进制文件仍然包含一些有关类型的必要信息和关于所有导出方法(带有名称)的完整信息。在运行时,主要是当你将存储在接口变量中的一个值赋值给任何其他变量时,需要此信息来检查可赋值性。

我们还可以通过重新编译所导入的包来删除它们的类型和字段名称:

$ cd $HOME/emgo
$ ./clean.sh
$ cd $HOME/firstemgo
$ egc -nf -nt
$ arm-none-eabi-size cortexm0.elf
   text    data     bss     dec     hex filename
  10272     196     212   10680    29b8 cortexm0.elf

让我们加载这个程序,看看它是否按预期工作。这一次我们将使用 st-flash 命令:

$ arm-none-eabi-objcopy -O binary cortexm0.elf cortexm0.bin
$ st-flash write cortexm0.bin 0x8000000
st-flash 1.4.0-33-gd76e3c7
2018-04-10T22:04:34 INFO usb.c: -- exit_dfu_mode
2018-04-10T22:04:34 INFO common.c: Loading device parameters....
2018-04-10T22:04:34 INFO common.c: Device connected is: F0 small device, id 0x10006444
2018-04-10T22:04:34 INFO common.c: SRAM size: 0x1000 bytes (4 KiB), Flash: 0x4000 bytes (16 KiB) in pages of 1024 bytes
2018-04-10T22:04:34 INFO common.c: Attempting to write 10468 (0x28e4) bytes to stm32 address: 134217728 (0x8000000)
Flash page at addr: 0x08002800 erased
2018-04-10T22:04:34 INFO common.c: Finished erasing 11 pages of 1024 (0x400) bytes
2018-04-10T22:04:34 INFO common.c: Starting Flash write for VL/F0/F3/F1_XL core id
2018-04-10T22:04:34 INFO flash_loader.c: Successfully loaded flash loader in sram
 11/11 pages written
2018-04-10T22:04:35 INFO common.c: Starting verification of write complete
2018-04-10T22:04:35 INFO common.c: Flash written and verified! jolly good!

我没有将 NRST 信号连接到编程器,因此无法使用 -reset 选项,必须按下复位按钮才能运行程序。

Interfaces

看来,st-flash 与此板配合使用有点不可靠(通常需要复位 ST-LINK 加密狗)。此外,当前版本不会通过 SWD 发出复位命令(仅使用 NRST 信号)。软件复位是不现实的,但是它通常是有效的,缺少它会将会带来不便。对于 板卡程序员 board-programmer 来说 OpenOCD 工作得更好。

UART

UART( 通用异步收发传输器 Universal Aynchronous Receiver-Transmitter )仍然是当今微控制器最重要的外设之一。它的优点是以下属性的独特组合:

  • 相对较高的速度,
  • 仅两条信号线(在 半双工 half-duplex 通信的情况下甚至一条),
  • 角色对称,
  • 关于新数据的 同步带内信令 synchronous in-band signaling (起始位),
  • 在传输 words 内的精确计时。

这使得最初用于传输由 7-9 位的字组成的异步消息的 UART,也被用于有效地实现各种其他物理协议,例如被 WS28xx LEDs1-wire 设备使用的协议。

但是,我们将以其通常的角色使用 UART:从程序中打印文本消息。

package main

import (
    "io"
    "rtos"

    "stm32/hal/dma"
    "stm32/hal/gpio"
    "stm32/hal/irq"
    "stm32/hal/system"
    "stm32/hal/system/timer/systick"
    "stm32/hal/usart"
)

var tts *usart.Driver

func init() {
    system.SetupPLL(8, 1, 48/8)
    systick.Setup(2e6)

    gpio.A.EnableClock(true)
    tx := gpio.A.Pin(9)

    tx.Setup(&gpio.Config{Mode: gpio.Alt})
    tx.SetAltFunc(gpio.USART1_AF1)
    d := dma.DMA1
    d.EnableClock(true)
    tts = usart.NewDriver(usart.USART1, d.Channel(2, 0), nil, nil)
    tts.Periph().EnableClock(true)
    tts.Periph().SetBaudRate(115200)
    tts.Periph().Enable()
    tts.EnableTx()

    rtos.IRQ(irq.USART1).Enable()
    rtos.IRQ(irq.DMA1_Channel2_3).Enable()
}

func main() {
    io.WriteString(tts, "Hello, World!
")
}

func ttsISR() {
    tts.ISR()
}

func ttsDMAISR() {
    tts.TxDMAISR()
}

//c:__attribute__((section(".ISRs")))
var ISRs = [...]func(){
    irq.USART1:          ttsISR,
    irq.DMA1_Channel2_3: ttsDMAISR,
}

你会发现此代码可能有些复杂,但目前 STM32 HAL 中没有更简单的 UART 驱动程序(在某些情况下,简单的轮询驱动程序可能会很有用)。 usart.Driver 是使用 DMA 和中断来减轻 CPU 负担的高效驱动程序。

STM32 USART 外设提供传统的 UART 及其同步版本。要将其用作输出,我们必须将其 Tx 信号连接到正确的 GPIO 引脚:

tx.Setup(&gpio.Config{Mode: gpio.Alt})
tx.SetAltFunc(gpio.USART1_AF1)

在 Tx-only 模式下配置 usart.Driver (rxdma 和 rxbuf 设置为 nil):

tts = usart.NewDriver(usart.USART1, d.Channel(2, 0), nil, nil)

我们使用它的 WriteString 方法来打印这句名言。让我们清理所有内容并编译该程序:

$ cd $HOME/emgo
$ ./clean.sh
$ cd $HOME/firstemgo
$ egc
$ arm-none-eabi-size cortexm0.elf
  text       data        bss        dec        hex    filename
  12728        236        176      13140       3354    cortexm0.elf

要查看某些内容,你需要在 PC 中使用 UART 外设。

请勿使用 RS232 端口或 USB 转 RS232 转换器!

STM32 系列使用 3.3V 逻辑,但是 RS232 可以产生 -15 V ~ +15 V 的电压,这可能会损坏你的 MCU。你需要使用 3.3V 逻辑的 USB 转 UART 转换器。流行的转换器基于 FT232 或 CP2102 芯片。

UART

你还需要一些终端仿真程序(我更喜欢 picocom)。刷新新图像,运行终端仿真器,然后按几次复位按钮:

$ 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: 0x080016f4 msp: 0x20000a20
adapter speed: 4000 kHz
** Programming Started **
auto erase enabled
target halted due to breakpoint, current mode: Thread
xPSR: 0x61000000 pc: 0x2000003a msp: 0x20000a20
wrote 13312 bytes from file cortexm0.elf in 1.020185s (12.743 KiB/s)
** Programming Finished **
adapter speed: 950 kHz
$
$ picocom -b 115200 /dev/ttyUSB0
picocom v3.1

port is        : /dev/ttyUSB0
flowcontrol    : none
baudrate is    : 115200
parity is      : none
databits are   : 8
stopbits are   : 1
escape is      : C-a
local echo is  : no
noinit is      : no
noreset is     : no
hangup is      : no
nolock is      : no
send_cmd is    : sz -vv
receive_cmd is : rz -vv -E
imap is        :
omap is        :
emap is        : crcrlf,delbs,
logfile is     : none
initstring     : none
exit_after is  : not set
exit is        : no

Type [C-a] [C-h] to see available commands
Terminal ready
Hello, World!
Hello, World!
Hello, World!

每次按下复位按钮都会产生新的 “Hello,World!”行。一切都在按预期进行。

要查看此 MCU 的 双向 bi-directional UART 代码,请查看 此示例

io.Writer 接口

io.Writer 接口可能是 Go 中第二种最常用的接口类型,仅次于 error 接口。其定义如下所示:

type Writer interface {
    Write(p []byte) (n int, err error)
}

usart.Driver 实现了 io.Writer,因此我们可以替换:

tts.WriteString("Hello, World!
")

io.WriteString(tts, "Hello, World!
")

此外,你需要将 io 包添加到 import 部分。

io.WriteString 函数的声明如下所示:

func WriteString(w Writer, s string) (n int, err error)

如你所见,io.WriteString 允许使用实现了 io.Writer 接口的任何类型来编写字符串。在内部,它检查基础类型是否具有 WriteString 方法,并使用该方法代替 Write(如果可用)。

让我们编译修改后的程序:

$ egc
$ arm-none-eabi-size cortexm0.elf
   text    data     bss     dec     hex filename
  15456     320     248   16024    3e98 cortexm0.elf

如你所见,io.WriteString 导致二进制文件的大小显着增加:15776-12964 = 2812 字节。 Flash 上没有太多空间了。是什么引起了这么大规模的增长?

使用这个命令:

arm-none-eabi-nm --print-size --size-sort --radix=d cortexm0.elf

我们可以打印两种情况下按其大小排序的所有符号。通过过滤和分析获得的数据(awkdiff),我们可以找到大约 80 个新符号。最大的十个如下所示:

> 00000062 T stm32$hal$usart$Driver$DisableRx
> 00000072 T stm32$hal$usart$Driver$RxDMAISR
> 00000076 T internal$Type$Implements
> 00000080 T stm32$hal$usart$Driver$EnableRx
> 00000084 t errors$New
> 00000096 R $8$stm32$hal$usart$Driver$$
> 00000100 T stm32$hal$usart$Error$Error
> 00000360 T io$WriteString
> 00000660 T stm32$hal$usart$Driver$Read

因此,即使我们不使用 usart.Driver.Read 方法,但它被编译进来了,与 DisableRxRxDMAISREnableRx 以及上面未提及的其他方法一样。不幸的是,如果你为接口赋值了一些内容,就需要它的完整方法集(包含所有依赖项)。对于使用大多数方法的大型程序来说,这不是问题。但是对于我们这种极简的情况而言,这是一个巨大的负担。

我们已经接近 MCU 的极限,但让我们尝试打印一些数字(你需要在 import 部分中用 strconv 替换 io 包):

func main() {
    a := 12
    b := -123

    tts.WriteString("a = ")
    strconv.WriteInt(tts, a, 10, 0, 0)
    tts.WriteString("
")
    tts.WriteString("b = ")
    strconv.WriteInt(tts, b, 10, 0, 0)
    tts.WriteString("
")

    tts.WriteString("hex(a) = ")
    strconv.WriteInt(tts, a, 16, 0, 0)
    tts.WriteString("
")
    tts.WriteString("hex(b) = ")
    strconv.WriteInt(tts, b, 16, 0, 0)
    tts.WriteString("
")
}

与使用 io.WriteString 函数的情况一样,strconv.WriteInt 的第一个参数的类型为 io.Writer

$ egc
/usr/local/arm/bin/arm-none-eabi-ld: /home/michal/firstemgo/cortexm0.elf section `.rodata' will not fit in region `Flash'
/usr/local/arm/bin/arm-none-eabi-ld: region `Flash' overflowed by 692 bytes
exit status 1

这一次我们的空间超出的不多。让我们试着精简一下有关类型的信息:

$ cd $HOME/emgo
$ ./clean.sh
$ cd $HOME/firstemgo
$ egc -nf -nt
$ arm-none-eabi-size cortexm0.elf
   text    data     bss     dec     hex filename
  15876     316     320   16512    4080 cortexm0.elf

很接近,但很合适。让我们加载并运行此代码:

a = 12
b = -123
hex(a) = c
hex(b) = -7b

Emgo 中的 strconv 包与 Go 中的原型有很大的不同。它旨在直接用于写入格式化的数字,并且在许多情况下可以替换沉重的 fmt 包。 这就是为什么函数名称以 Write 而不是 Format 开头,并具有额外的两个参数的原因。 以下是其用法示例:

func main() {
    b := -123
    strconv.WriteInt(tts, b, 10, 0, 0)
    tts.WriteString("
")
    strconv.WriteInt(tts, b, 10, 6, ' ')
    tts.WriteString("
")
    strconv.WriteInt(tts, b, 10, 6, '0')
    tts.WriteString("
")
    strconv.WriteInt(tts, b, 10, 6, '.')
    tts.WriteString("
")
    strconv.WriteInt(tts, b, 10, -6, ' ')
    tts.WriteString("
")
    strconv.WriteInt(tts, b, 10, -6, '0')
    tts.WriteString("
")
    strconv.WriteInt(tts, b, 10, -6, '.')
    tts.WriteString("
")
}

下面是它的输出:

-123
  -123
-00123
..-123
-123
-123
-123..

Unix 流 和 莫尔斯电码 Morse code

由于大多数写入的函数都使用 io.Writer 而不是具体类型(例如 C 中的 FILE ),因此我们获得了类似于 Unix stream 的功能。在 Unix 中,我们可以轻松地组合简单的命令来执行更大的任务。例如,我们可以通过以下方式将文本写入文件:

echo "Hello, World!" > file.txt

> 操作符将前面命令的输出流写入文件。还有 | 操作符,用于连接相邻命令的输出流和输入流。

多亏了流,我们可以轻松地转换/过滤任何命令的输出。例如,要将所有字母转换为大写,我们可以通过 tr 命令过滤 echo 的输出:

echo "Hello, World!" | tr a-z A-Z > file.txt

为了显示 io.Writer 和 Unix 流之间的类比,让我们编写以下代码:

io.WriteString(tts, "Hello, World!
")

采用以下伪 unix 形式:

io.WriteString "Hello, World!" | usart.Driver usart.USART1

下一个示例将显示如何执行此操作:

io.WriteString "Hello, World!" | MorseWriter | usart.Driver usart.USART1

让我们来创建一个简单的编码器,它使用莫尔斯电码对写入的文本进行编码:

type MorseWriter struct {
    W io.Writer
}

func (w *MorseWriter) Write(s []byte) (int, error) {
    var buf [8]byte
    for n, c := range s {
        switch {
        case c == '\n':
            c = ' ' // Replace new lines with spaces.
        case 'a' <= c && c <= 'z':
            c -= 'a' - 'A' // Convert to upper case.
        }
        if c < ' ' || 'Z' < c {
            continue // c is outside ASCII [' ', 'Z']
        }
        var symbol morseSymbol
        if c == ' ' {
            symbol.length = 1
            buf[0] = ' '
        } else {
            symbol = morseSymbols[c-'!']
            for i := uint(0); i < uint(symbol.length); i++ {
                if (symbol.code>>i)&1 != 0 {
                    buf[i] = '-'
                } else {
                    buf[i] = '.'
                }
            }
        }
        buf[symbol.length] = ' '
        if _, err := w.W.Write(buf[:symbol.length+1]); err != nil {
            return n, err
        }
    }
    return len(s), nil
}

type morseSymbol struct {
    code, length byte
}

//emgo:const
var morseSymbols = [...]morseSymbol{
    {1<<0 | 1<<1 | 1<<2, 4}, // ! ---.
    {1<<1 | 1<<4, 6},        // " .-..-.
    {},                      // #
    {1<<3 | 1<<6, 7},        // $ ...-..-

    // Some code omitted...

    {1<<0 | 1<<3, 4},        // X -..-
    {1<<0 | 1<<2 | 1<<3, 4}, // Y -.--
    {1<<0 | 1<<1, 4},        // Z --..
}

你可以在 这里 找到完整的 morseSymbols 数组。 //emgo:const 指令确保 morseSymbols 数组不会被复制到 RAM 中。

现在我们可以通过两种方式打印句子:

func main() {
    s := "Hello, World!
"
    mw := &MorseWriter{tts}

    io.WriteString(tts, s)
    io.WriteString(mw, s)
}

我们使用指向 MorseWriter &MorseWriter{tts} 的指针而不是简单的 MorseWriter{tts} 值,因为 MorseWriter 太大,不适合接口变量。

与 Go 不同,Emgo 不会为存储在接口变量中的值动态分配内存。接口类型的大小受限制,相当于三个指针(适合 slice )或两个 float64(适合 complex128)的大小,以较大者为准。它可以直接存储所有基本类型和小型 “结构体/数组” 的值,但是对于较大的值,你必须使用指针。

让我们编译此代码并查看其输出:

$ egc
$ arm-none-eabi-size cortexm0.elf
   text    data     bss     dec     hex filename
  15152     324     248   15724    3d6c cortexm0.elf
Hello, World!
.... . .-.. .-.. --- --..--   .-- --- .-. .-.. -.. ---.

终极闪烁

Blinky 是等效于 “Hello,World!” 程序的硬件。一旦有了摩尔斯编码器,我们就可以轻松地将两者结合起来以获得终极闪烁程序:

package main

import (
    "delay"
    "io"

    "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, Speed: gpio.Low}
    led.Setup(&cfg)
}

type Telegraph struct {
    Pin   gpio.Pin
    Dotms int // Dot length [ms]
}

func (t Telegraph) Write(s []byte) (int, error) {
    for _, c := range s {
        switch c {
        case '.':
            t.Pin.Clear()
            delay.Millisec(t.Dotms)
            t.Pin.Set()
            delay.Millisec(t.Dotms)
        case '-':
            t.Pin.Clear()
            delay.Millisec(3 * t.Dotms)
            t.Pin.Set()
            delay.Millisec(t.Dotms)
        case ' ':
            delay.Millisec(3 * t.Dotms)
        }
    }
    return len(s), nil
}

func main() {
    telegraph := &MorseWriter{Telegraph{led, 100}}
    for {
        io.WriteString(telegraph, "Hello, World! ")
    }
}

// Some code omitted...

在上面的示例中,我省略了 MorseWriter 类型的定义,因为它已在前面展示过。完整版可通过 这里 获取。让我们编译它并运行:

$ egc
$ arm-none-eabi-size cortexm0.elf
   text    data     bss     dec     hex filename
  11772     244     244   12260    2fe4 cortexm0.elf

Ultimate Blinky

反射

是的,Emgo 支持 反射reflect 包尚未完成,但是已完成的部分足以实现 fmt.Print 函数族了。来看看我们可以在小型 MCU 上做什么。

为了减少内存使用,我们将使用 半主机 semihosting 作为标准输出。为了方便起见,我们还编写了简单的 println 函数,它在某种程度上类似于 fmt.Println

package main

import (
    "debug/semihosting"
    "reflect"
    "strconv"

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

var stdout semihosting.File

func init() {
    system.SetupPLL(8, 1, 48/8)
    systick.Setup(2e6)

    var err error
    stdout, err = semihosting.OpenFile(":tt", semihosting.W)
    for err != nil {
    }
}

type stringer interface {
    String() string
}

func println(args ...interface{}) {
    for i, a := range args {
        if i > 0 {
            stdout.WriteString(" ")
        }
        switch v := a.(type) {
        case string:
            stdout.WriteString(v)
        case int:
            strconv.WriteInt(stdout, v, 10, 0, 0)
        case bool:
            strconv.WriteBool(stdout, v, 't', 0, 0)
        case stringer:
            stdout.WriteString(v.String())
        default:
            stdout.WriteString("%unknown")
        }
    }
    stdout.WriteString("
")
}

type S struct {
    A int
    B bool
}

func main() {
    p := &S{-123, true}

    v := reflect.ValueOf(p)

    println("kind(p) =", v.Kind())
    println("kind(*p) =", v.Elem().Kind())
    println("type(*p) =", v.Elem().Type())

    v = v.Elem()

    println("*p = {")
    for i := 0; i < v.NumField(); i++ {
        ft := v.Type().Field(i)
        fv := v.Field(i)
        println("  ", ft.Name(), ":", fv.Interface())
    }
    println("}")
}

semihosting.OpenFile 函数允许在主机端打开/创建文件。特殊路径 :tt 对应于主机的标准输出。

println 函数接受任意数量的参数,每个参数的类型都是任意的:

func println(args ...interface{})

可能是因为任何类型都实现了空接口 interface{}println 使用 类型开关 打印字符串,整数和布尔值:

switch v := a.(type) {
case string:
    stdout.WriteString(v)
case int:
    strconv.WriteInt(stdout, v, 10, 0, 0)
case bool:
    strconv.WriteBool(stdout, v, 't', 0, 0)
case stringer:
    stdout.WriteString(v.String())
default:
    stdout.WriteString("%unknown")
}

此外,它还支持任何实现了 stringer 接口的类型,即任何具有 String() 方法的类型。在任何 case 子句中,v 变量具有正确的类型,与 case 关键字后列出的类型相同。

reflect.ValueOf(p) 函数通过允许以编程的方式分析其类型和内容的形式返回 p。如你所见,我们甚至可以使用 v.Elem() 取消引用指针,并打印所有结构体及其名称。

让我们尝试编译这段代码。现在让我们看看如果编译时没有类型和字段名,会有什么结果:

$ egc -nt -nf
$ arm-none-eabi-size cortexm0.elf
   text    data     bss     dec     hex filename
  16028     216     312   16556    40ac cortexm0.elf

闪存上只剩下 140 个可用字节。让我们使用启用了半主机的 OpenOCD 加载它:

$ openocd -d0 -f interface/stlink.cfg -f target/stm32f0x.cfg -c 'init; program cortexm0.elf; arm semihosting enable; reset run'
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: 0x08002338 msp: 0x20000a20
adapter speed: 4000 kHz
** Programming Started **
auto erase enabled
target halted due to breakpoint, current mode: Thread
xPSR: 0x61000000 pc: 0x2000003a msp: 0x20000a20
wrote 16384 bytes from file cortexm0.elf in 0.700133s (22.853 KiB/s)
** Programming Finished **
semihosting is enabled
adapter speed: 950 kHz
kind(p) = ptr
kind(*p) = struct
type(*p) =
*p = {
   X. : -123
   X. : true
}

如果你实际运行此代码,则会注意到半主机运行缓慢,尤其是在逐字节写入时(缓冲很有用)。

如你所见,*p 没有类型名称,并且所有结构字段都具有相同的 X. 名称。让我们再次编译该程序,这次不带 -nt -nf 选项:

$ egc
$ arm-none-eabi-size cortexm0.elf
   text    data     bss     dec     hex filename
  16052     216     312   16580    40c4 cortexm0.elf

现在已经包括了类型和字段名称,但仅在 main.go 文件中 main 包中定义了它们。该程序的输出如下所示:

kind(p) = ptr
kind(*p) = struct
type(*p) = S
*p = {
   A : -123
   B : true
}

反射是任何易于使用的序列化库的关键部分,而像 JSON 这样的序列化 算法 物联网 IoT 时代也越来越重要。

这些就是我完成的本文的第二部分。我认为有机会进行第三部分,更具娱乐性的部分,在那里我们将各种有趣的设备连接到这块板上。如果这块板装不下,我们就换一块大一点的。


via: https://ziutek.github.io/2018/04/14/go_on_very_small_hardware2.html

作者:Michał Derkacz 译者:gxlct008 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

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中国 荣誉推出