Linux中國

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

在本系列的 第一第二 部分中討論的大多數示例都是以某種方式閃爍的 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中國 榮譽推出


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

對這篇文章感覺如何?

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

    You may also like

    Leave a reply

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

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

    More in:Linux中國