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

开发 后端
在本系列的 第一 和 第二 部分中讨论的大多数示例都是以某种方式闪烁的 LED。起初它可能很有趣,但是一段时间后变得有些无聊。让我们做些更有趣的事情……让我们点亮更多的 LED!

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 Pill 或 STM32F4-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 下。

让我们结束这段冗长的介绍并转到代码:

  1. package main
  2.  
  3. import (
  4. "delay"
  5. "math/rand"
  6. "rtos"
  7.  
  8. "led"
  9. "led/ws281x/wsuart"
  10.  
  11. "stm32/hal/dma"
  12. "stm32/hal/gpio"
  13. "stm32/hal/irq"
  14. "stm32/hal/system"
  15. "stm32/hal/system/timer/systick"
  16. "stm32/hal/usart"
  17. )
  18.  
  19. var tts *usart.Driver
  20.  
  21. func init() {
  22. system.SetupPLL(8, 1, 48/8)
  23. systick.Setup(2e6)
  24.  
  25. gpio.A.EnableClock(true)
  26. tx := gpio.A.Pin(9)
  27.  
  28. tx.Setup(&gpio.Config{Mode: gpio.Alt})
  29. tx.SetAltFunc(gpio.USART1_AF1)
  30.  
  31. d := dma.DMA1
  32. d.EnableClock(true)
  33.  
  34. tts = usart.NewDriver(usart.USART1, d.Channel(2, 0), nil, nil)
  35. tts.Periph().EnableClock(true)
  36. tts.Periph().SetBaudRate(3000000000 / 1390)
  37. tts.Periph().SetConf2(usart.TxInv)
  38. tts.Periph().Enable()
  39. tts.EnableTx()
  40.  
  41. rtos.IRQ(irq.USART1).Enable()
  42. rtos.IRQ(irq.DMA1_Channel2_3).Enable()
  43. }
  44.  
  45. func main() {
  46. var rnd rand.XorShift64
  47. rnd.Seed(1)
  48. rgb := wsuart.GRB
  49. strip := wsuart.Make(24)
  50. black := rgb.Pixel(0)
  51. for {
  52. c := led.Color(rnd.Uint32()).Scale(127)
  53. pixel := rgb.Pixel(c)
  54. for i := range strip {
  55. strip[i] = pixel
  56. tts.Write(strip.Bytes())
  57. delay.Millisec(40)
  58. }
  59. for i := range strip {
  60. strip[i] = black
  61. tts.Write(strip.Bytes())
  62. delay.Millisec(20)
  63. }
  64. }
  65. }
  66.  
  67. func ttsISR() {
  68. tts.ISR()
  69. }
  70.  
  71. func ttsDMAISR() {
  72. tts.TxDMAISR()
  73. }
  74.  
  75. //c:__attribute__((section(".ISRs")))
  76. var ISRs = [...]func(){
  77. irq.USART1: ttsISR,
  78. irq.DMA1_Channel2_3: ttsDMAISR,
  79. }

导入部分

与前面的示例相比,导入部分中的新内容是 rand/math 包和带有 led/ws281x 子树的 led 包。 led 包本身包含 Color 类型的定义。 led/ws281x/wsuart 定义了 ColorOrderPixel 和 Strip 类型。

我想知道如何使用 image/color 中的 Color 或 RGBA 类型,以及如何以它将实现 image.Image 接口的方式定义 Strip。 但是由于使用了 gamma 校正 和 大开销的 color/draw 包,我以简单的方式结束:

  1. type Color uint32
  2. 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 像素的初始化条带。它等效于:

  1. strip := make(wsuart.Strip, 24)
  2. strip.Clear()

其余代码使用随机颜色绘制类似于 “Please Wait…” 微调器的内容。

strip 切片充当帧缓冲区。 tts.Write(strip.Bytes()) 将帧缓冲区的内容发送到环。

中断

该程序由处理中断的代码组成,与先前的 UART 示例 中的代码相同。

让我们编译并运行:

  1. $ egc
  2. $ arm-none-eabi-size cortexm0.elf
  3. text data bss dec hex filename
  4. 14088 240 204 14532 38c4 cortexm0.elf
  5. $ 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

全局变量

添加两个新的全局变量:btn 和 btnev

  1. var (
  2. tts *usart.Driver
  3. btn gpio.Pin
  4. btnev rtos.EventFlag
  5. )

它们将用来处理那些用于设置时钟的 “按钮”。我们的板子除了重置之外没有其他按钮,但是如果没有它,我们仍然可以通过某种方式进行管理。

init 函数

将这段代码添加到 init 函数:

  1. btn = gpio.A.Pin(4)
  2.  
  3. btn.Setup(&gpio.Config{Mode: gpio.In, Pull: gpio.PullUp})
  4. ei := exti.Lines(btn.Mask())
  5. ei.Connect(btn.Port())
  6. ei.EnableFallTrig()
  7. ei.EnableRiseTrig()
  8. ei.EnableIRQ()
  9.  
  10. rtos.IRQ(irq.EXTI4_15).Enable()

在内部上拉电阻pull-up resistor启用的情况下,将 PA4 引脚配置为输入。它已连接至板载 LED,但这不会妨碍任何事情。更重要的是它位于 GND 引脚旁边,所以我们可以使用任何金属物体来模拟按钮并设置时钟。作为奖励,我们还有来自板载 LED 的其他反馈。

我们使用 EXTI 外设来跟踪 PA4 状态。它被配置为在发生任何更改时都会产生中断。

btnWait 函数

定义一个新的辅助函数:

  1. func btnWait(state int, deadline int64) bool {
  2. for btn.Load() != state {
  3. if !btnev.Wait(1, deadline) {
  4. return false // timeout
  5. }
  6. btnev.Reset(0)
  7. }
  8. delay.Millisec(50) // debouncing
  9. return true
  10. }

它等待 “按钮” 引脚上的指定状态,但只等到最后期限出现。这是稍微改进的轮询代码:

  1. for btn.Load() != state {
  2. if rtos.Nanosec() >= deadline {
  3. // timeout
  4. }
  5. }

我们的 btnWait 函数不是忙于等待 state 或 deadline,而是使用 rtos.EventFlag 类型的 btnev 变量休眠,直到有事情发生。你当然可以使用通道而不是 rtos.EventFlag,但是后者便宜得多。

main 函数

我们需要全新的 main 函数:

  1. func main() {
  2. rgb := wsuart.GRB
  3. strip := wsuart.Make(24)
  4. ds := 4 * 60 / len(strip) // Interval between LEDs (quarter-seconds).
  5. adjust := 0
  6. adjspeed := ds
  7. for {
  8. qs := int(rtos.Nanosec() / 25e7) // Quarter-seconds since reset.
  9. qa := qs + adjust
  10.  
  11. qa %= 12 * 3600 * 4 // Quarter-seconds since 0:00 or 12:00.
  12. hi := len(strip) * qa / (12 * 3600 * 4)
  13.  
  14. qa %= 3600 * 4 // Quarter-seconds in the current hour.
  15. mi := len(strip) * qa / (3600 * 4)
  16.  
  17. qa %= 60 * 4 // Quarter-seconds in the current minute.
  18. si := len(strip) * qa / (60 * 4)
  19.  
  20. hc := led.Color(0x550000)
  21. mc := led.Color(0x005500)
  22. sc := led.Color(0x000055)
  23.  
  24. // Blend the colors if the hands of the clock overlap.
  25. if hi == mi {
  26. hc |= mc
  27. mc = hc
  28. }
  29. if mi == si {
  30. mc |= sc
  31. sc = mc
  32. }
  33. if si == hi {
  34. sc |= hc
  35. hc = sc
  36. }
  37.  
  38. // Draw the clock and write to the ring.
  39. strip.Clear()
  40. strip[hi] = rgb.Pixel(hc)
  41. strip[mi] = rgb.Pixel(mc)
  42. strip[si] = rgb.Pixel(sc)
  43. tts.Write(strip.Bytes())
  44.  
  45. // Sleep until the button pressed or the second hand should be moved.
  46. if btnWait(0, int64(qs+ds)*25e7) {
  47. adjust += adjspeed
  48. // Sleep until the button is released or timeout.
  49. if !btnWait(1, rtos.Nanosec()+100e6) {
  50. if adjspeed < 5*60*4 {
  51. adjspeed += 2 * ds
  52. }
  53. continue
  54. }
  55. adjspeed = ds
  56. }
  57. }
  58. }

我们使用 rtos.Nanosec 函数代替 time.Now 来获取当前时间。这样可以节省大量的闪存,但也使我们的时钟变成了不知道日、月、年的老式设备,最糟糕的是它无法处理夏令时的变化。

我们的环有 24 个 LED,因此秒针的显示精度可以达到 2.5 秒。为了不牺牲这种精度并获得流畅的运行效果,我们使用 1/4 秒作为基准间隔。半秒就足够了,但四分之一秒更准确,而且与 16 和 48 个 LED 配合使用也很好。

红色、绿色和蓝色分别用于时针、分针和秒针。这允许我们使用简单的“逻辑或操作”进行颜色混合。我们 Color.Blend 方法可以混合任意颜色,但是我们闪存不多,所以我们选择最简单的解决方案。

我们只有在秒针移动时才重画时钟。

  1. btnWait(0, int64(qs+ds)*25e7)

上面的这行代码等待的正是那一刻,或者是按钮的按下。

每按一下按钮就会把时钟向前调一调。按住按钮一段时间会加速调整。

中断

定义新的中断处理程序:

  1. func exti4_15ISR() {
  2. pending := exti.Pending() & 0xFFF0
  3. pending.ClearPending()
  4. if pending&exti.Lines(btn.Mask()) != 0 {
  5. btnev.Signal(1)
  6. }
  7. }

并将 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 上找到完整的代码。让我们来编译它:

  1. $ egc
  2. $ arm-none-eabi-size cortexm0.elf
  3. text data bss dec hex filename
  4. 15960 240 216 16416 4020 cortexm0.elf

这里所有的改进只得到 184 个字节。让我们再次重新构建所有内容,但这次在 typeinfo 中不使用任何类型和字段名:

  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. 15120 240 216 15576 3cd8 cortexm0.elf

现在,有了千字节的空闲空间,你可以改进一些东西。让我们看看它是如何工作的: 

 

我不知道我是怎么精确打到 3:00 的!?

以上就是所有内容!在第 4 部分(本系列的结束)中,我们将尝试在 LCD 上显示一些内容。(LCTT 译注:然而烂尾了,第三篇写于 2018 年,整个博客当年就停更了。)

 

责任编辑:庞桂玉 来源: Linux中国
相关推荐

2019-09-25 17:46:13

Go语言编程语言

2020-10-24 18:00:28

Go语言编程语言

2020-10-25 06:30:48

Go语言编程语言

2020-08-12 08:51:19

Go语言Concurrency后台

2024-05-10 08:36:40

Go语言对象

2023-12-13 07:19:01

微服务架构Golang

2014-04-09 09:32:24

Go并发

2021-09-17 10:51:01

Linuxlspci命令

2010-01-27 13:53:40

强大的CC++编译器

2013-07-30 09:23:43

VMwareGoogle云平台

2013-07-30 09:27:32

Go云计算语言

2012-10-08 09:25:59

GoGo语言开发语言

2012-11-21 11:48:23

i-NVMM加密密码

2018-03-12 22:13:46

GO语言编程软件

2012-05-04 11:28:41

虚拟化KVM高性能

2023-08-15 08:01:07

Go 语言排序

2022-05-31 16:00:46

Go 编程语言复制文件Go 标准库

2023-12-30 10:22:57

Go语言函数开发

2014-10-31 09:48:36

Go语言

2023-04-20 17:26:40

FreeDOSC 语言
点赞
收藏

51CTO技术栈公众号