在本文的 第一部分 的结尾,我承诺要写关于接口的内容。我不想在这里写有关接口或完整或简短的讲义。相反,我将展示一个简单的示例,来说明如何定义和使用接口,以及如何利用无处不在的 io.Writer
接口。还有一些关于 反射 reflection 和 半主机 semihosting 的内容。
]
接口是 Go 语言的重要组成部分。如果你想了解更多有关它们的信息,我建议你阅读《高效的 Go 编程》 和 Russ Cox 的文章。
并发 Blinky – 回顾
当你阅读前面示例的代码时,你可能会注意到一中打开或关闭 LED 的反直觉方式。 Set
方法用于关闭 LED,Clear
方法用于打开 LED。这是由于在 漏极开路配置 open-drain configuration 下驱动了 LED。我们可以做些什么来减少代码的混乱?让我们用 On
和 Off
方法来定义 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
接口,它有两个方法: On
和 Off
。 PushPullLED
和 OpenDrainLED
类型代表两种驱动 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
选项,必须按下复位按钮才能运行程序。
看来,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 LEDs 或 1-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!\r\n")
}
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 芯片。
你还需要一些终端仿真程序(我更喜欢 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!\r\n")
为
io.WriteString(tts, "Hello, World!\r\n")
此外,你需要将 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
我们可以打印两种情况下按其大小排序的所有符号。通过过滤和分析获得的数据(awk
,diff
),我们可以找到大约 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
方法,但它被编译进来了,与 DisableRx
、RxDMAISR
、EnableRx
以及上面未提及的其他方法一样。不幸的是,如果你为接口赋值了一些内容,就需要它的完整方法集(包含所有依赖项)。对于使用大多数方法的大型程序来说,这不是问题。但是对于我们这种极简的情况而言,这是一个巨大的负担。
我们已经接近 MCU 的极限,但让我们尝试打印一些数字(你需要在 import
部分中用 strconv
替换 io
包):
func main() {
a := 12
b := -123
tts.WriteString("a = ")
strconv.WriteInt(tts, a, 10, 0, 0)
tts.WriteString("\r\n")
tts.WriteString("b = ")
strconv.WriteInt(tts, b, 10, 0, 0)
tts.WriteString("\r\n")
tts.WriteString("hex(a) = ")
strconv.WriteInt(tts, a, 16, 0, 0)
tts.WriteString("\r\n")
tts.WriteString("hex(b) = ")
strconv.WriteInt(tts, b, 16, 0, 0)
tts.WriteString("\r\n")
}
与使用 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("\r\n")
strconv.WriteInt(tts, b, 10, 6, ' ')
tts.WriteString("\r\n")
strconv.WriteInt(tts, b, 10, 6, '0')
tts.WriteString("\r\n")
strconv.WriteInt(tts, b, 10, 6, '.')
tts.WriteString("\r\n")
strconv.WriteInt(tts, b, 10, -6, ' ')
tts.WriteString("\r\n")
strconv.WriteInt(tts, b, 10, -6, '0')
tts.WriteString("\r\n")
strconv.WriteInt(tts, b, 10, -6, '.')
tts.WriteString("\r\n")
}
下面是它的输出:
-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!\r\n")
采用以下伪 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' 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