Go语言在极小硬件上的运用(二)

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

目前创新互联已为近1000家的企业提供了网站建设、域名、虚拟主机、网站改版维护、企业网站设计、平乐网站维护等服务,公司将坚持客户导向、应用为本的策略,正道将秉承"和谐、参与、激情"的文化,与客户和合作伙伴齐心协力一起成长,共同发展。

STM32F030F4P6

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

并发 Blinky – 回顾

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

 
 
 
 
  1. type LED struct {
  2. pin gpio.Pin
  3. }
  4.  
  5. func (led LED) On() {
  6. led.pin.Clear()
  7. }
  8.  
  9. func (led LED) Off() {
  10. led.pin.Set()
  11. }

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

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

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

 
 
 
 
  1. type PushPullLED struct {
  2. pin gpio.Pin
  3. }
  4.  
  5. func (led PushPullLED) On() {
  6. led.pin.Set()
  7. }
  8.  
  9. func (led PushPullLED) Off() {
  10. led.pin.Clear()
  11. }

请注意,这两种类型都具有相同的方法,它们的工作方式也相同。如果在 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. type LED interface {
  12. On()
  13. Off()
  14. }
  15.  
  16. type PushPullLED struct{ pin gpio.Pin }
  17.  
  18. func (led PushPullLED) On() {
  19. led.pin.Set()
  20. }
  21.  
  22. func (led PushPullLED) Off() {
  23. led.pin.Clear()
  24. }
  25.  
  26. func MakePushPullLED(pin gpio.Pin) PushPullLED {
  27. pin.Setup(&gpio.Config{Mode: gpio.Out, Driver: gpio.PushPull})
  28. return PushPullLED{pin}
  29. }
  30.  
  31. type OpenDrainLED struct{ pin gpio.Pin }
  32.  
  33. func (led OpenDrainLED) On() {
  34. led.pin.Clear()
  35. }
  36.  
  37. func (led OpenDrainLED) Off() {
  38. led.pin.Set()
  39. }
  40.  
  41. func MakeOpenDrainLED(pin gpio.Pin) OpenDrainLED {
  42. pin.Setup(&gpio.Config{Mode: gpio.Out, Driver: gpio.OpenDrain})
  43. return OpenDrainLED{pin}
  44. }
  45.  
  46. var led1, led2 LED
  47.  
  48. func init() {
  49. system.SetupPLL(8, 1, 48/8)
  50. systick.Setup(2e6)
  51.  
  52. gpio.A.EnableClock(false)
  53. led1 = MakeOpenDrainLED(gpio.A.Pin(4))
  54. led2 = MakePushPullLED(gpio.A.Pin(3))
  55. }
  56.  
  57. func blinky(led LED, period int) {
  58. for {
  59. led.On()
  60. delay.Millisec(100)
  61. led.Off()
  62. delay.Millisec(period - 100)
  63. }
  64. }
  65.  
  66. func main() {
  67. go blinky(led1, 500)
  68. blinky(led2, 1000)
  69. }
  70.  

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

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

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

 
 
 
 
  1. led1.methods->On(led1.value)

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

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

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

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

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

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

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

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

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

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

我没有将 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 LEDs 或 1-wire 设备使用的协议。

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

 
 
 
 
  1. package main
  2.  
  3. import (
  4. "io"
  5. "rtos"
  6.  
  7. "stm32/hal/dma"
  8. "stm32/hal/gpio"
  9. "stm32/hal/irq"
  10. "stm32/hal/system"
  11. "stm32/hal/system/timer/systick"
  12. "stm32/hal/usart"
  13. )
  14.  
  15. var tts *usart.Driver
  16.  
  17. func init() {
  18. system.SetupPLL(8, 1, 48/8)
  19. systick.Setup(2e6)
  20.  
  21. gpio.A.EnableClock(true)
  22. tx := gpio.A.Pin(9)
  23.  
  24. tx.Setup(&gpio.Config{Mode: gpio.Alt})
  25. tx.SetAltFunc(gpio.USART1_AF1)
  26. d := dma.DMA1
  27. d.EnableClock(true)
  28. tts = usart.NewDriver(usart.USART1, d.Channel(2, 0), nil, nil)
  29. tts.Periph().EnableClock(true)
  30. tts.Periph().SetBaudRate(115200)
  31. tts.Periph().Enable()
  32. tts.EnableTx()
  33.  
  34. rtos.IRQ(irq.USART1).Enable()
  35. rtos.IRQ(irq.DMA1_Channel2_3).Enable()
  36. }
  37.  
  38. func main() {
  39. io.WriteString(tts, "Hello, World!\r\n")
  40. }
  41.  
  42. func ttsISR() {
  43. tts.ISR()
  44. }
  45.  
  46. func ttsDMAISR() {
  47. tts.TxDMAISR()
  48. }
  49.  
  50. //c:__attribute__((section(".ISRs")))
  51. var ISRs = [...]func(){
  52. irq.USART1: ttsISR,
  53. irq.DMA1_Channel2_3: ttsDMAISR,
  54. }
  55.  

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

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

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

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

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

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

 
 
 
 
  1. $ cd $HOME/emgo
  2. $ ./clean.sh
  3. $ cd $HOME/firstemgo
  4. $ egc
  5. $ arm-none-eabi-size cortexm0.elf
  6. text data bss dec hex filename
  7. 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)。刷新新图像,运行终端仿真器,然后按几次复位按钮:

 
 
 
 
  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: 0x080016f4 msp: 0x20000a20
  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: 0x20000a20
  18. wrote 13312 bytes from file cortexm0.elf in 1.020185s (12.743 KiB/s)
  19. ** Programming Finished **
  20. adapter speed: 950 kHz
  21. $
  22. $ picocom -b 115200 /dev/ttyUSB0
  23. picocom v3.1
  24.  
  25. port is : /dev/ttyUSB0
  26. flowcontrol : none
  27. baudrate is : 115200
  28. parity is : none
  29. databits are : 8
  30. stopbits are : 1
  31. escape is : C-a
  32. local echo is : no
  33. noinit is : no
  34. noreset is : no
  35. hangup is : no
  36. nolock is : no
  37. send_cmd is : sz -vv
  38. receive_cmd is : rz -vv -E
  39. imap is :
  40. omap is :
  41. emap is : crcrlf,delbs,
  42. logfile is : none
  43. initstring : none
  44. exit_after is : not set
  45. exit is : no
  46.  
  47. Type [C-a] [C-h] to see available commands
  48. Terminal ready
  49. Hello, World!
  50. Hello, World!
  51. Hello, World!

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

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

io.Writer 接口

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

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

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

 
 
 
 
  1. tts.WriteString("Hello, World!\r\n")

 
 
 
 
  1. io.WriteString(tts, "Hello, World!\r\n")

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

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

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

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

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

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

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

使用这个命令:

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

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

 
 
 
 
  1. > 00000062 T stm32$hal$usart$Driver$DisableRx
  2. > 00000072 T stm32$hal$usart$Driver$RxDMAISR
  3. > 00000076 T internal$Type$Implements
  4. > 00000080 T stm32$hal$usart$Driver$EnableRx
  5. > 00000084 t errors$New
  6. > 00000096 R $8$stm32$hal$usart$Driver$$
  7. > 00000100 T stm32$hal$usart$Error$Error
  8. > 00000360 T io$WriteString
  9. > 00000660 T stm32$hal$usart$Driver$Read

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

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

 
 
 
 
  1. func main() {
  2. a := 12
  3. b := -123
  4.  
  5. tts.WriteString("a = ")
  6. strconv.WriteInt(tts, a, 10, 0, 0)
  7. tts.WriteString("\r\n")
  8. tts.WriteString("b = ")
  9. strconv.WriteInt(tts, b, 10, 0, 0)
  10. tts.WriteString("\r\n")
  11.  
  12. tts.WriteString("hex(a) = ")
  13. strconv.WriteInt(tts, a, 16, 0, 0)
  14. tts.WriteString("\r\n")
  15. tts.WriteString("hex(b) = ")
  16. strconv.WriteInt(tts, b, 16, 0, 0)
  17. tts.WriteString("\r\n")
  18. }

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

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

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

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

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

 
 
 
 
  1. a = 12
  2. b = -123
  3. hex(a) = c
  4. hex(b) = -7b

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

 
 
 
 
  1. func main() {
  2. b := -123
  3. strconv.WriteInt(tts, b, 10, 0, 0)
  4. tts.WriteString("\r\n")
  5. strconv.WriteInt(tts, b, 10, 6, ' ')
  6. tts.WriteString("\r\n")
  7. strconv.WriteInt(tts, b, 10, 6, '0')
  8. tts.WriteString("\r\n")
  9. strconv.WriteInt(tts, b, 10, 6, '.')
  10. tts.WriteString("\r\n")
  11. strconv.WriteInt(tts, b, 10, -6, ' ')
  12. tts.WriteString("\r\n")
  13. strconv.WriteInt(tts, b, 10, -6, '0')
  14. tts.WriteString("\r\n")
  15. strconv.WriteInt(tts, b, 10, -6, '.')
  16. tts.WriteString("\r\n")
  17. }

下面是它的输出:

 
 
 
 
  1. -123
  2. -123
  3. -00123
  4. ..-123
  5. -123
  6. -123
  7. -123..

Unix 流 和 莫尔斯电码Morse code

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

 
 
 
 
  1. echo "Hello, World!" > file.txt

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

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

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

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

 
 
 
 
  1. io.WriteString(tts, "Hello, World!\r\n")

采用以下伪 unix 形式:

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

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

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

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

 
 
 
 
  1. type MorseWriter struct {
  2. W io.Writer
  3. }
  4.  
  5. func (w *MorseWriter) Write(s []byte) (int, error) {
  6. var buf [8]byte
  7. for n, c := range s {
  8. switch {
  9. case c == '\n':
  10. c = ' ' // Replace new lines with spaces.
  11. case 'a' <= c && c <= 'z':
  12. c -= 'a' - 'A' // Convert to upper case.
  13. }
  14. if c < ' ' || 'Z' < c {
  15. continue // c is outside ASCII [' ', 'Z']
  16. }
  17. var symbol morseSymbol
  18. if c == ' ' {
  19. symbol.length = 1
  20. buf[0] = ' '
  21. } else {
  22. symbol = morseSymbols[c-'!']
  23. for i := uint(0); i < uint(symbol.length); i++ {
  24. if (symbol.code>>i)&1 != 0 {
  25. buf[i] = '-'
  26. } else {
  27. buf[i] = '.'
  28. }
  29. }
  30. }
  31. buf[symbol.length] = ' '
  32. if _, err := w.W.Write(buf[:symbol.length+1]); err != nil {
  33. return n, err
  34. }
  35. }
  36. return len(s), nil
  37. }
  38.  
  39. type morseSymbol struct {
  40. code, length byte
  41. }
  42.  
  43. //emgo:const
  44. var morseSymbols = [...]morseSymbol{
  45. {1<<0 | 1<<1 | 1<<2, 4}, // ! ---.
  46. {1<<1 | 1<<4, 6}, // " .-..-.
  47. {}, // #
  48. {1<<3 | 1<<6, 7}, // $ ...-..-
  49.  
  50. // Some code omitted...
  51.  
  52. {1<<0 | 1<<3, 4}, // X -..-
  53. {1<<0 | 1<<2 | 1<<3, 4}, // Y -.--
  54. {1<<0 | 1<<1, 4}, // Z --..
  55. }

你可以在 这里 找到完整的 

当前文章:Go语言在极小硬件上的运用(二)
本文链接:http://www.shufengxianlan.com/qtweb/news42/119342.html

网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等

广告

声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联