导读:我将展示一个简单的示例,来说明如何定义和使用接口,以及如何利用无处不在的 io.Writer 接口。
本文字数:21486,阅读时长大约:24分钟
https://linux.cn/article-12747-1.html

作者:Michał Derkacz
译者:XianLei Gao
在本文的 第一部分 的结尾,我承诺要写关于接口的内容。我不想在这里写有关接口或完整或简短的讲义。相反,我将展示一个简单的示例,来说明如何定义和使用接口,以及如何利用无处不在的 io.Writer 接口。还有一些关于反射(reflection)半主机(semihosting)的内容。
]
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. func (led LED)On(){
  5. led.pin.Clear()
  6. }
  7. func (led LED)Off(){
  8. led.pin.Set()
  9. }
现在我们可以简单地调用 led.On() 和 led.Off(),这不会再引起任何疑惑了。
在前面的所有示例中,我都尝试使用相同的 漏极开路配置(open-drain configuration)来避免代码复杂化。但是在最后一个示例中,对于我来说,将第三个 LED 连接到 GND 和 PA3 引脚之间并将 PA3 配置为推挽模式(push-pull mode)会更容易。下一个示例将使用以此方式连接的 LED。
但是我们的新 LED 类型不支持推挽配置,实际上,我们应该将其称为 OpenDrainLED,并定义另一个类型 PushPullLED
  1. type PushPullLEDstruct{
  2. pin gpio.Pin
  3. }
  4. func (led PushPullLED)On(){
  5. led.pin.Set()
  6. }
  7. func (led PushPullLED)Off(){
  8. led.pin.Clear()
  9. }
请注意,这两种类型都具有相同的方法,它们的工作方式也相同。如果在 LED 上运行的代码可以同时使用这两种类型,而不必注意当前使用的是哪种类型,那就太好了。接口类型可以提供帮助:
  1. package main
  2. import(
  3. "delay"
  4. "stm32/hal/gpio"
  5. "stm32/hal/system"
  6. "stm32/hal/system/timer/systick"
  7. )
  8. type LED interface{
  9. On()
  10. Off()
  11. }
  12. type PushPullLEDstruct{ pin gpio.Pin}
  13. func (led PushPullLED)On(){
  14. led.pin.Set()
  15. }
  16. func (led PushPullLED)Off(){
  17. led.pin.Clear()
  18. }
  19. func MakePushPullLED(pin gpio.Pin)PushPullLED{
  20. pin.Setup(&gpio.Config{Mode: gpio.Out,Driver: gpio.PushPull})
  21. returnPushPullLED{pin}
  22. }
  23. type OpenDrainLEDstruct{ pin gpio.Pin}
  24. func (led OpenDrainLED)On(){
  25. led.pin.Clear()
  26. }
  27. func (led OpenDrainLED)Off(){
  28. led.pin.Set()
  29. }
  30. func MakeOpenDrainLED(pin gpio.Pin)OpenDrainLED{
  31. pin.Setup(&gpio.Config{Mode: gpio.Out,Driver: gpio.OpenDrain})
  32. returnOpenDrainLED{pin}
  33. }
  34. var led1, led2 LED
  35. func init(){
  36. system.SetupPLL(8,1,48/8)
  37. systick.Setup(2e6)
  38. gpio.A.EnableClock(false)
  39. led1 =MakeOpenDrainLED(gpio.A.Pin(4))
  40. led2 =MakePushPullLED(gpio.A.Pin(3))
  41. }
  42. func blinky(led LED, period int){
  43. for{
  44. led.On()
  45. delay.Millisec(100)
  46. led.Off()
  47. delay.Millisec(period -100)
  48. }
  49. }
  50. func main(){
  51. go blinky(led1,500)
  52. blinky(led2,1000)
  53. }
我们定义了 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. 10356196212107642a0c cortexm0.elf
如果我们不使用 反射,可以通过避免包含类型和结构字段的名称来节省一些字节:
  1. $ egc -nf -nt
  2. $ arm-none-eabi-size cortexm0.elf
  3. text data bss dec hex filename
  4. 103121962121072029e0 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. 102721962121068029b8 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,id0x10006444
  7. 2018-04-10T22:04:34 INFO common.c: SRAM size:0x1000 bytes (4KiB),Flash:0x4000 bytes (16KiB)in pages of 1024 bytes
  8. 2018-04-10T22:04:34 INFO common.c:Attempting to write10468(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:StartingFlashwritefor 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!
我没有将 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. import(
  3. "io"
  4. "rtos"
  5. "stm32/hal/dma"
  6. "stm32/hal/gpio"
  7. "stm32/hal/irq"
  8. "stm32/hal/system"
  9. "stm32/hal/system/timer/systick"
  10. "stm32/hal/usart"
  11. )
  12. var tts *usart.Driver
  13. func init(){
  14. system.SetupPLL(8,1,48/8)
  15. systick.Setup(2e6)
  16. gpio.A.EnableClock(true)
  17. tx := gpio.A.Pin(9)
  18. tx.Setup(&gpio.Config{Mode: gpio.Alt})
  19. tx.SetAltFunc(gpio.USART1_AF1)
  20. d := dma.DMA1
  21. d.EnableClock(true)
  22. tts = usart.NewDriver(usart.USART1, d.Channel(2,0),nil,nil)
  23. tts.Periph().EnableClock(true)
  24. tts.Periph().SetBaudRate(115200)
  25. tts.Periph().Enable()
  26. tts.EnableTx()
  27. rtos.IRQ(irq.USART1).Enable()
  28. rtos.IRQ(irq.DMA1_Channel2_3).Enable()
  29. }
  30. func main(){
  31. io.WriteString(tts,"Hello, World!\r\n")
  32. }
  33. func ttsISR(){
  34. tts.ISR()
  35. }
  36. func ttsDMAISR(){
  37. tts.TxDMAISR()
  38. }
  39. //c:__attribute__((section(".ISRs")))
  40. varISRs=[...]func(){
  41. irq.USART1: ttsISR,
  42. irq.DMA1_Channel2_3: ttsDMAISR,
  43. }
你会发现此代码可能有些复杂,但目前 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. 12728236176131403354 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. OpenOn-ChipDebugger0.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. **ProgrammingStarted**
  15. auto erase enabled
  16. target halted due to breakpoint, current mode:Thread
  17. xPSR:0x61000000 pc:0x2000003a msp:0x20000a20
  18. wrote 13312 bytes fromfile cortexm0.elf in1.020185s(12.743KiB/s)
  19. **ProgrammingFinished**
  20. adapter speed:950 kHz
  21. $
  22. $ picocom -b 115200/dev/ttyUSB0
  23. picocom v3.1
  24. port is:/dev/ttyUSB0
  25. flowcontrol : none
  26. baudrate is:115200
  27. parity is: none
  28. databits are :8
  29. stopbits are :1
  30. escape is: C-a
  31. localechois:no
  32. noinit is:no
  33. noreset is:no
  34. hangup is:no
  35. nolock is:no
  36. send_cmd is: sz -vv
  37. receive_cmd is: rz -vv -E
  38. imap is:
  39. omap is:
  40. emap is: crcrlf,delbs,
  41. logfile is: none
  42. initstring : none
  43. exit_after is:notset
  44. exitis:no
  45. Type[C-a][C-h] to see available commands
  46. Terminal ready
  47. Hello,World!
  48. Hello,World!
  49. Hello,World!
每次按下复位按钮都会产生新的 “Hello,World!”行。一切都在按预期进行。
要查看此 MCU 的 双向(bi-directional) UART 代码,请查看 此示例
io.Writer 接口
io.Writer 接口可能是 Go 中第二种最常用的接口类型,仅次于 error 接口。其定义如下所示:
  1. type Writerinterface{
  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(wWriter, 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. 15456320248160243e98 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. tts.WriteString("a = ")
  5. strconv.WriteInt(tts, a,10,0,0)
  6. tts.WriteString("\r\n")
  7. tts.WriteString("b = ")
  8. strconv.WriteInt(tts, b,10,0,0)
  9. tts.WriteString("\r\n")
  10. tts.WriteString("hex(a) = ")
  11. strconv.WriteInt(tts, a,16,0,0)
  12. tts.WriteString("\r\n")
  13. tts.WriteString("hex(b) = ")
  14. strconv.WriteInt(tts, b,16,0,0)
  15. tts.WriteString("\r\n")
  16. }
与使用 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. 15876316320165124080 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 MorseWriterstruct{
  2. W io.Writer
  3. }
  4. func (w*MorseWriter)Write(s []byte)(int, error){
  5. var buf [8]byte
  6. for n, c := range s {
  7. switch{
  8. case c =='\n':
  9. c =' '// Replace new lines with spaces.
  10. case'a'<= c && c <='z':
  11. c -='a'-'A'// Convert to upper case.
  12. }
  13. if c <' '||'Z'< c {
  14. continue// c is outside ASCII [' ', 'Z']
  15. }
  16. var symbol morseSymbol
  17. if c ==' '{
  18. symbol.length =1
  19. buf[0]=' '
  20. }else{
  21. symbol = morseSymbols[c-'!']
  22. for i := uint(0); i < uint(symbol.length); i++{
  23. if(symbol.code>>i)&1!=0{
  24. buf[i]='-'
  25. }else{
  26. buf[i]='.'
  27. }
  28. }
  29. }
  30. buf[symbol.length]=' '
  31. if _, err :=w.W.Write(buf[:symbol.length+1]); err !=nil{
  32. return n, err
  33. }
  34. }
  35. return len(s),nil
  36. }
  37. type morseSymbol struct{
  38. code, length byte
  39. }
  40. //emgo:const
  41. var morseSymbols =[...]morseSymbol{
  42. {1<<0|1<<1|1<<2,4},// ! ---.
  43. {1<<1|1<<4,6},// " .-..-.
  44. {},// #
  45. {1<<3|1<<6,7},// $ ...-..-
  46. // Some code omitted...
  47. {1<<0|1<<3,4},// X -..-
  48. {1<<0|1<<2|1<<3,4},// Y -.--
  49. {1<<0|1<<1,4},// Z --..
  50. }
你可以在 这里 找到完整的 morseSymbols 数组。 //emgo:const 指令确保 morseSymbols 数组不会被复制到 RAM 中。
现在我们可以通过两种方式打印句子:
  1. func main(){
  2. s :="Hello, World!\r\n"
  3. mw :=&MorseWriter{tts}
  4. io.WriteString(tts, s)
  5. io.WriteString(mw, s)
  6. }
我们使用指向 MorseWriter&MorseWriter{tts} 的指针而不是简单的 MorseWriter{tts} 值,因为 MorseWriter 太大,不适合接口变量。
与 Go 不同,Emgo 不会为存储在接口变量中的值动态分配内存。接口类型的大小受限制,相当于三个指针(适合 slice )或两个 float64(适合 complex128)的大小,以较大者为准。它可以直接存储所有基本类型和小型 “结构体/数组” 的值,但是对于较大的值,你必须使用指针。
让我们编译此代码并查看其输出:
  1. $ egc
  2. $ arm-none-eabi-size cortexm0.elf
  3. text data bss dec hex filename
  4. 15152324248157243d6c cortexm0.elf
  1. Hello,World!
  2. ......-...-..-----..--.-----.-..-..-..---.
终极闪烁
Blinky 是等效于 “Hello,World!” 程序的硬件。一旦有了摩尔斯编码器,我们就可以轻松地将两者结合起来以获得终极闪烁程序:
  1. package main
  2. import(
  3. "delay"
  4. "io"
  5. "stm32/hal/gpio"
  6. "stm32/hal/system"
  7. "stm32/hal/system/timer/systick"
  8. )
  9. var led gpio.Pin
  10. func init(){
  11. system.SetupPLL(8,1,48/8)
  12. systick.Setup(2e6)
  13. gpio.A.EnableClock(false)
  14. led = gpio.A.Pin(4)
  15. cfg := gpio.Config{Mode: gpio.Out,Driver: gpio.OpenDrain,Speed: gpio.Low}
  16. led.Setup(&cfg)
  17. }
  18. type Telegraphstruct{
  19. Pin gpio.Pin
  20. Dotmsint// Dot length [ms]
  21. }
  22. func (t Telegraph)Write(s []byte)(int, error){
  23. for _, c := range s {
  24. switch c {
  25. case'.':
  26. t.Pin.Clear()
  27. delay.Millisec(t.Dotms)
  28. t.Pin.Set()
  29. delay.Millisec(t.Dotms)
  30. case'-':
  31. t.Pin.Clear()
  32. delay.Millisec(3* t.Dotms)
  33. t.Pin.Set()
  34. delay.Millisec(t.Dotms)
  35. case' ':
  36. delay.Millisec(3* t.Dotms)
  37. }
  38. }
  39. return len(s),nil
  40. }
  41. func main(){
  42. telegraph :=&MorseWriter{Telegraph{led,100}}
  43. for{
  44. io.WriteString(telegraph,"Hello, World! ")
  45. }
  46. }
  47. // Some code omitted...
在上面的示例中,我省略了 MorseWriter 类型的定义,因为它已在前面展示过。完整版可通过 这里 获取。让我们编译它并运行:
  1. $ egc
  2. $ arm-none-eabi-size cortexm0.elf
  3. text data bss dec hex filename
  4. 11772244244122602fe4 cortexm0.elf
Ultimate Blinky
反射
是的,Emgo 支持 反射reflect 包尚未完成,但是已完成的部分足以实现 fmt.Print 函数族了。来看看我们可以在小型 MCU 上做什么。
为了减少内存使用,我们将使用 半主机(semihosting) 作为标准输出。为了方便起见,我们还编写了简单的 println 函数,它在某种程度上类似于 fmt.Println
  1. package main
  2. import(
  3. "debug/semihosting"
  4. "reflect"
  5. "strconv"
  6. "stm32/hal/system"
  7. "stm32/hal/system/timer/systick"
  8. )
  9. var stdout semihosting.File
  10. func init(){
  11. system.SetupPLL(8,1,48/8)
  12. systick.Setup(2e6)
  13. var err error
  14. stdout, err = semihosting.OpenFile(":tt", semihosting.W)
  15. for err !=nil{
  16. }
  17. }
  18. type stringer interface{
  19. String() string
  20. }
  21. func println(args ...interface{}){
  22. for i, a := range args {
  23. if i >0{
  24. stdout.WriteString(" ")
  25. }
  26. switch v := a.(type){
  27. case string:
  28. stdout.WriteString(v)
  29. caseint:
  30. strconv.WriteInt(stdout, v,10,0,0)
  31. casebool:
  32. strconv.WriteBool(stdout, v,'t',0,0)
  33. case stringer:
  34. stdout.WriteString(v.String())
  35. default:
  36. stdout.WriteString("%unknown")
  37. }
  38. }
  39. stdout.WriteString("\r\n")
  40. }
  41. type S struct{
  42. A int
  43. B bool
  44. }
  45. func main(){
  46. p :=&S{-123,true}
  47. v := reflect.ValueOf(p)
  48. println("kind(p) =", v.Kind())
  49. println("kind(*p) =", v.Elem().Kind())
  50. println("type(*p) =", v.Elem().Type())
  51. v = v.Elem()
  52. println("*p = {")
  53. for i :=0; i < v.NumField(); i++{
  54. ft := v.Type().Field(i)
  55. fv := v.Field(i)
  56. println(" ", ft.Name(),":", fv.Interface())
  57. }
  58. println("}")
  59. }
semihosting.OpenFile 函数允许在主机端打开/创建文件。特殊路径 :tt 对应于主机的标准输出。
println 函数接受任意数量的参数,每个参数的类型都是任意的:
  1. func println(args ...interface{})
可能是因为任何类型都实现了空接口 interface{}。 println 使用 类型开关 打印字符串,整数和布尔值:
  1. switch v := a.(type){
  2. case string:
  3. stdout.WriteString(v)
  4. caseint:
  5. strconv.WriteInt(stdout, v,10,0,0)
  6. casebool:
  7. strconv.WriteBool(stdout, v,'t',0,0)
  8. case stringer:
  9. stdout.WriteString(v.String())
  10. default:
  11. stdout.WriteString("%unknown")
  12. }
此外,它还支持任何实现了 stringer 接口的类型,即任何具有 String() 方法的类型。在任何 case 子句中,v 变量具有正确的类型,与 case 关键字后列出的类型相同。
reflect.ValueOf(p) 函数通过允许以编程的方式分析其类型和内容的形式返回 p。如你所见,我们甚至可以使用 v.Elem() 取消引用指针,并打印所有结构体及其名称。
让我们尝试编译这段代码。现在让我们看看如果编译时没有类型和字段名,会有什么结果:
  1. $ egc -nt -nf
  2. $ arm-none-eabi-size cortexm0.elf
  3. text data bss dec hex filename
  4. 160282163121655640ac cortexm0.elf
闪存上只剩下 140 个可用字节。让我们使用启用了半主机的 OpenOCD 加载它:
  1. $ openocd -d0 -f interface/stlink.cfg -f target/stm32f0x.cfg -c 'init; program cortexm0.elf; arm semihosting enable; reset run'
  2. OpenOn-ChipDebugger0.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:0x08002338 msp:0x20000a20
  13. adapter speed:4000 kHz
  14. **ProgrammingStarted**
  15. auto erase enabled
  16. target halted due to breakpoint, current mode:Thread
  17. xPSR:0x61000000 pc:0x2000003a msp:0x20000a20
  18. wrote 16384 bytes fromfile cortexm0.elf in0.700133s(22.853KiB/s)
  19. **ProgrammingFinished**
  20. semihosting is enabled
  21. adapter speed:950 kHz
  22. kind(p)= ptr
  23. kind(*p)=struct
  24. type(*p)=
  25. *p ={
  26. X.:-123
  27. X.:true
  28. }
如果你实际运行此代码,则会注意到半主机运行缓慢,尤其是在逐字节写入时(缓冲很有用)。
如你所见,*p 没有类型名称,并且所有结构字段都具有相同的 X. 名称。让我们再次编译该程序,这次不带 -nt -nf 选项:
  1. $ egc
  2. $ arm-none-eabi-size cortexm0.elf
  3. text data bss dec hex filename
  4. 160522163121658040c4 cortexm0.elf
现在已经包括了类型和字段名称,但仅在 main.go 文件中main 包中定义了它们。该程序的输出如下所示:
  1. kind(p)= ptr
  2. kind(*p)=struct
  3. type(*p)= S
  4. *p ={
  5. A :-123
  6. B :true
  7. }
反射是任何易于使用的序列化库的关键部分,而像 JSON 这样的序列化 算法 在物联网(IoT)时代也越来越重要。
这些就是我完成的本文的第二部分。我认为有机会进行第三部分,更具娱乐性的部分,在那里我们将各种有趣的设备连接到这块板上。如果这块板装不下,我们就换一块大一点的。

作者:Michał Derkacz 译者:gxlct008 校对:wxy
本文由 LCTT 原创编译,Linux中国 荣誉推出
继续阅读