TinyGoのgoroutineによる非同期Lチカ

Lチカとはマイコンの基本とされる制御でLEDのON/OFFを繰り返しチカチカと点滅させるプログラムである。複数のLEDでLチカを実装するのは意外に難しいが、TinyGoのgoroutineを使えば容易に実現できる。
この記事ではハードウェアに[zero-kb02](https://github.com/sago35/keyboards)を使用した実装例を紹介する。

![IMG_1731.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1256483/1bac07ac-41e7-434f-31d1-16956454fcb8.gif)

## ハードウェア構成

zero-kb02はRP2040チップを搭載したキーボードである。今回は、このキーボードをマイコンボードとして利用する。外部出力されているGPIO14とGPIO15にLEDを接続した構成で使用した。同じRP2040を使用しているRaspberry Pi Picoなどでも同様に動作する。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1256483/06f570d1-ed80-cab1-4d13-d9cc55c6772d.png)

## 普通のLチカ

まずGPIO14に接続されているLEDを200msecごとに点灯と消灯を繰り返してみる。よく入門記事で紹介されているLチカのコードである。他の言語でも、おおむね似たようなコードになる。

```go
package main

import (
	"machine"
	"time"
)


func main() {
	led := machine.GPIO14
	led.Configure(machine.PinConfig{Mode: machine.PinOutput})
	for {
		led.High()
		time.Sleep(time.Millisecond * 200)
		led.Low()
		time.Sleep(time.Millisecond * 200)
	}
}
```

## 非同期Lチカ(タイマー監視)

前述のLチカに加えて、もう一つLEDを接続し300msecで点滅させることを考える。いくつか方法はあるが簡単なのは次のコードだ。タイマーの経過時間を常時監視、それぞれのタイミングでOn/Offを行っている。

```go
package main

import (
    "machine"
    "time"
)

func toggle(pin *machine.Pin) {
    if pin.Get() == false {
        pin.Set(true) // 点灯
    } else {
        pin.Set(false) // 消灯
    }
}

func main() {
    // GPIO14とGPIO15を出力モードに設定
    ledA := machine.GPIO14
    ledB := machine.GPIO15

    ledA.Configure(machine.PinConfig{Mode: machine.PinOutput})
    ledB.Configure(machine.PinConfig{Mode: machine.PinOutput})

    var lastToggleA time.Time
    var lastToggleB time.Time

    for {
        // 200msecごとにLED Aを点灯/消灯
        if time.Since(lastToggleA) >= 200*time.Millisecond {
            toggle(&ledA) 
            lastToggleA = time.Now() // ledAの最後の切り替え時刻を更新
        }

        // 300msecごとにLED Bを点灯/消灯
        if time.Since(lastToggleB) >= 300*time.Millisecond {
            toggle(&ledB) 
            lastToggleB = time.Now() // ledBの最後の切り替え時刻を更新
        }
    }
}
```

LED AとLED Bの処理が分離していない。開発が進み他の処理も加わると瞬く間にコードが複雑化する。しかもこのコードは処理を伴う無限ループでポーリング処理による電力消費が大きい。

## 非同期Lチカ(goroutine)

TinyGoでは[時空魔法](https://qiita.com/rdon_key/items/9e0f03e11ff902f7f0b4)goroutineが実装されているので、この程度であればシンプルなコードで実現できる。



```go
package main

import (
        "machine"
        "time"
)
var ledA = machine.GPIO14
var ledB = machine.GPIO15

func processLedA(){
        for {
                ledA.High()
                time.Sleep(time.Millisecond * 200)
                ledA.Low()
                time.Sleep(time.Millisecond * 200)
        }
}

func processLedB(){
        for {
                ledB.High()
                time.Sleep(time.Millisecond * 300)
                ledB.Low()
                time.Sleep(time.Millisecond * 300)
        }
}

func main() {
        ledA.Configure(machine.PinConfig{Mode: machine.PinOutput})
        ledB.Configure(machine.PinConfig{Mode: machine.PinOutput})

        go processLedA()
        go processLedB()

        select{} //プログラムの終了を防ぐ
}
```

関数processLedAとprocessLedBが独立してLチカを行う構成により、コードの見通しが良く、新しい機能の追加も容易になっている。
一見奇妙に見える最後の`select{}`は、main関数が終了してしまうことを防ぐために使用してる。Go言語では、main関数が終了するとプログラム全体が終了してしまいgoroutineも強制終了されるのだ。case文を持たないselect文は永久待ち状態になるので、goroutineが継続して実行されることが保証される。

## 最後に
この記事ではTinyGoによるマイコン制御はgoroutineによりシンプルに非同期処理が実現できることを示した。他にもコンパイル言語であるTinyGoはマイコン制御と相性がいい。著者は厳密な型付けによる安全性や高速な実行速度、メモリ効率の良さが気に入っている。より詳しい情報は[TinyGo](https://tinygo.org/)を参照してほしい。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1256483/69e8669a-bdf5-f8c8-d79f-87bd4eb7f3a9.png)