Linux中國

Go 語言在極小硬體上的運用(二)

在本文的 第一部分 的結尾,我承諾要寫關於介面的內容。我不想在這裡寫有關介面或完整或簡短的講義。相反,我將展示一個簡單的示例,來說明如何定義和使用介面,以及如何利用無處不在的 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!rn")
}

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!rn")

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

此外,你需要將 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("rn")
    tts.WriteString("b = ")
    strconv.WriteInt(tts, b, 10, 0, 0)
    tts.WriteString("rn")

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

與使用 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("rn")
    strconv.WriteInt(tts, b, 10, 6, ' ')
    tts.WriteString("rn")
    strconv.WriteInt(tts, b, 10, 6, '0')
    tts.WriteString("rn")
    strconv.WriteInt(tts, b, 10, 6, '.')
    tts.WriteString("rn")
    strconv.WriteInt(tts, b, 10, -6, ' ')
    tts.WriteString("rn")
    strconv.WriteInt(tts, b, 10, -6, '0')
    tts.WriteString("rn")
    strconv.WriteInt(tts, b, 10, -6, '.')
    tts.WriteString("rn")
}

下面是它的輸出:

-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!rn")

採用以下偽 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 &apos;a&apos; <= c && c <= &apos;z&apos;:
            c -= &apos;a&apos; - &apos;A&apos; // Convert to upper case.
        }
        if c < &apos; &apos; || &apos;Z&apos; < c {
            continue // c is outside ASCII [&apos; &apos;, &apos;Z&apos;]
        }
        var symbol morseSymbol
        if c == &apos; &apos; {
            symbol.length = 1
            buf[0] = &apos; &apos;
        } else {
            symbol = morseSymbols[c-&apos;!&apos;]
            for i := uint(0); i < uint(symbol.length); i++ {
                if (symbol.code>>i)&1 != 0 {
                    buf[i] = &apos;-&apos;
                } else {
                    buf[i] = &apos;.&apos;
                }
            }
        }
        buf[symbol.length] = &apos; &apos;
        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!rn"
    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 &apos;.&apos;:
            t.Pin.Clear()
            delay.Millisec(t.Dotms)
            t.Pin.Set()
            delay.Millisec(t.Dotms)
        case &apos;-&apos;:
            t.Pin.Clear()
            delay.Millisec(3 * t.Dotms)
            t.Pin.Set()
            delay.Millisec(t.Dotms)
        case &apos; &apos;:
            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, &apos;t&apos;, 0, 0)
        case stringer:
            stdout.WriteString(v.String())
        default:
            stdout.WriteString("%unknown")
        }
    }
    stdout.WriteString("rn")
}

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, &apos;t&apos;, 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 &apos;init; program cortexm0.elf; arm semihosting enable; reset run&apos;
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中國 榮譽推出


本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive

對這篇文章感覺如何?

太棒了
0
不錯
0
愛死了
0
不太好
0
感覺很糟
0
雨落清風。心向陽

    You may also like

    Leave a reply

    您的郵箱地址不會被公開。 必填項已用 * 標註

    此站點使用Akismet來減少垃圾評論。了解我們如何處理您的評論數據

    More in:Linux中國