今回はラズパイのGPIOを用いて外部に用意したLEDをチカります。できるだけ汎用性高くして再利用性とか可読性とかを上げたいと思ってます。さあどうなるか。
これは Nerves 1.5 (Elixir 1.9) を用いてます。
#準備
Nerves の準備がまだの方は はじめてNerves(1) 電源ONでLチカアプリを起動させる をご覧ください。この記事の まずはターゲットのファームウェアを焼けるようにする までは同じです。このあとの ターゲット用のプログラムを書く の章から道が分かれますので、この直前までやってください。ただし今回のプロジェクト名は exineris3b でやります。
ハードウェアを接続する
今回はラズパイについているLEDではなく、外部に回路を作ってそれを制御します。ココからハードウェアをどんどん追加していきます。今回やる内容は LED と抵抗とタクトスイッチをブレッドボード上に接続して使います。
ラズベリーパイの外部入出力用の信号は基板の脇に立ってる40ピンのコネクタでやりとりします。どのピンにどんな信号が出ているかは [Raspberri Pi Pinout] (https://pinout.xyz) などをご覧ください。以下の図はこの抜粋で数字だけ書いてあるのが GPIO の番号を示しています。
これらのハードウェアをどう組んでいくかあまり詳しくない方は、例えば カラー図解 最新 Raspberry Piで学ぶ電子工作 作って動かしてしくみがわかる (ブルーバックス) をごらんください。これに出てくるような回路を Nerves で制御していこうかなと考えています(どこまで忠実にやるかは別として)。なお、これ用に [Raspberry Piで学ぶ電子工作 パーツセット] (http://akizukidenshi.com/catalog/g/gK-10852/) もありますのでご参考まで。
今回はまず、ラズベリーパイの GPIO18 の出力を LED の点灯用に使います。これは Circuits.GPIO の例に従っています。
なお、この図では電流制限抵抗が 100Ω になっていますが、私が使った回路では 330Ω を使いました。
GPIO 用ののライブラリを読み込む設定
今回は GPIO というインタフェースを用います。これには Circuits.GPIO ライブラリを使います。前回同様に、このライブラリを使うのに依存関係を mix.exs
ファイルに指定します。
...
defp deps do
[
... # 途中省略
{:circuits_gpio, "~> 0.4"},
]
end
...
これを書いたのちに mix deps.get
コマンドを投入してください。
なお、前回の nerves.leds
の記述が残っていても構いません。
New:
circuits_gpio 0.4.4
* Getting circuits_gpio (Hex package)
とか出てきて読み込まれたのがわかります。沢山出てくるメッセージの一部ですので注意して見てください。
LEDが1個のLチカ
では早速Lチカです。ただし GenServer をきちんとつかうことにしますので、そうやすやすとは点きません。モジュールの構造は以下のようにします。
- Application: Nerves システムが立ち上がった時にここが呼ばれるので、ここから必要な関数を呼び出すようにしておくと、ブート時に自動的にプログラムが走るようになります
- Worker: Application から呼び出される関数を定義します。これは Application から Supervisor 監視下で実行されるプロセスとして動きますので GenServer を用いて記述します
- LedBlink: 特定の GPIO に接続された LED を点滅させるためのモジュールです
- GpioInOut: GPIO 経由での入出力を司ります。GPIO のピンごとに GenServer でプロセスを立てて、GPIO に対するアクセスは必ずこのプロセスを経由するようにします。
自動起動の設定をする
電源オンでプログラムが立ち上がるように lib/プロジェクト名/application.ex
に記述をします。ここでは lib/exineris3b/application.ex
に1行を加えます。
def children(_target) do
[
{Exineris3b.Worker, [:led1]},
]
end
こうすると Exineris3b.Worker
モジュールの start_link/1
関数を :led1
という引数で起動します。その際にプロセスを Supervisor の監視下においてくれます。
GPIO制御用モジュール GpioInOut
では GenServer で GPIO を制御するモジュールを書いてみます。あとで IO 数が増えたときも改変しなくて済むように出来るだけ汎用的に書いてみます。なれないうちはなんでこんな面倒なことをしないとならないのだろうと思われるかもしれません。今回書いてみたら GenServer の基本的な書き方が一通り入っているのでちょうど良いサンプルになってるかと思います。
-
start_link/3
は指定された番号の GPIO を指定された入出力方向でオープンします。 - gpio_no は GPIO の番号
- in_out は入出力を :in か :out で指定
- pname は他のプロセスと区別しやすいように適当なプロセス名を
-
write/2
GPIOピンへの出力をします - pname は start_link/3 で指定したプロセス名
- val は何を出力するかを 0 か 1 で指定(0 で Low レベルに、1 で High レベル)
-
read/1
で GPIO ピンからの入力をします。返り値にプロセス名と入力の値のペアが入ります。 - pname は start_link/3 で指定したプロセス名を指定
-
stop/1
使っていた GPIO をクローズして、プロセスを終了します。 - pname は start_link/3 で指定したプロセス名を指定します。
defmodule GpioInOut do
@behaviour GenServer
require Circuits.GPIO
require Logger
def start_link(pname, gpio_no, in_out) do
GenServer.start_link(__MODULE__, {gpio_no, in_out}, name: pname)
end
def write(pname, val) do
GenServer.cast(pname, {:write, val})
end
def read(pname) do
GenServer.call(pname, :read)
end
def stop(pname) do
GenServer.stop(pname)
end
@impl GenServer
def init({gpio_no, in_out}) do
Logger.debug("#{__MODULE__} init: #{gpio_no}")
Circuits.GPIO.open(gpio_no, in_out) # {:ok, ref} が返るのを期待
end
@impl GenServer
def handle_cast({:write, val}, gpioref) do
Circuits.GPIO.write(gpioref, val)
{:noreply, gpioref}
end
@impl GenServer
def handle_call(:read, _from, gpioref) do
{:reply, {:ok, Circuits.GPIO.read(gpioref)}, gpioref}
end
@impl GenServer
def terminate(reason, gpioref) do
Circuits.GPIO.close(gpioref)
reason
end
LEDをチカらせる LedBlink
では準備が整ったところでGPIO18につないでいるLEDが点滅するモジュールを書きます。ポイントは led_blink/2
関数でプロセス名とGPIO番号を指定するところです。これで、以降はプロセス名であれこれ出来るようになります。
-
LedBlink.init/4
は指定した LED を点滅させます - gpio_name は GPIO に名前をつけます。ピン番号で物理的に直に指定しないで済むようにこれで論理名をつけます
- gpio_no は LED を点滅させる GPIO の物理ピンを指定します
- on_delay は LED が点灯している時間をミリ秒で指定します
- off_delay は LED が消灯している時間をミリ秒で指定します
関数名に init
とつけていますが、これは GenServer のコールバック関数ではありません。
defmodule LedBlink do
require Logger
require GpioInOut
def init(gpio_name, gpio_no, on_delay, off_delay) do
Logger.debug("#{__MODULE__}: led_blink #{gpio_name} #{gpio_no} start...")
GpioInOut.start_link(gpio_name, gpio_no, :output)
loop(gpio_name, on_delay, off_delay)
end
defp loop(gpio_name, on_delay, off_delay) do
Logger.debug("#{__MODULE__}: led_blink #{gpio_name} on...")
GpioInOut.write(gpio_name, 1)
Process.sleep(on_delay)
Logger.debug("#{__MODULE__}: led_blink #{gpio_name} off...")
GpioInOut.write(gpio_name, 0)
Process.sleep(off_delay)
loop(gpio_name, on_delay, off_delay)
end
end
メイン関数 Worker
Worker 関数は Application から呼び出されます。これ、明示していませんが Supervisor の下で管理されるプロセスとして動きます。
-
start_link/1
起動用です。Exineris3b.Application
より呼ばれます。引数はWorker
モジュール内のどの動作をさせるかを選択させるために用います。 -
init/1
はstart_link/1
のコールバックで、ここではLED点滅用の関数を呼びます
なお、本来は @behaviour GenServer
と書くべきと思いますが Nerves でうまく動かないので use GenServer
で代用しています。これ、もう少し調べて後日ご報告します。
defmodule Exineris3b.Worker do
require Logger
require LedBlink
use GenServer
def start_link(state \\ []) do
GenServer.start_link(__MODULE__, state, name: __MODULE__)
end
def init(state = [:led1]) do
Logger.debug("#{__MODULE__}: single led blink start...")
LedBlink.init(:led_no0, 18, 1000, 1000)
{:ok, state}
end
def init(state) do # Application.children/1 の引数を間違うとここに来る
Logger.debug("#{__MODULE__}: No such operation defined!! state: #{inspect(state)}")
{:error, state}
end
ブート時に自動で立ち上げる Application.ex
ブート後にターゲットにログインして iex から起動もできますが、ここでは自動で立ち上げてみます。Exineris3b.Application.children/1
に以下の様に記述します。コメントが長いですが、プログラムとして意味があるのは最後の一行です。
def children(_target) do
[
# Children for all targets except host
# Starts a worker by calling: Exineris3b.Worker.start_link(arg)
# {Exineris3b.Worker, arg},
# {Exineris3b.Worker, []},
# {Worker, []}
{Exineris3b.Worker, [:led1]},
]
end
実行してみる
ここまで準備したら mix firmware
してコンパイルしてください。ファームウェアができたら mix firmware.burn
で SD に焼いてラズパイをブートします1。
リブートしてくると LED が点滅します。ターゲットのコンソールに入れるなら RingLogger.attach
コマンドを実行すると LED の on/off に従ってログが出力されているのが見えます。
なお、自動で立ち上がらなかったらログインして iex で RingLogger.attach したあとに Exineris3b.Worker.start_link([:led1])
と入力して手動で実行できるか確認してください。
LEDが2個のLチカ
1つのLEDのLチカができたので、2つにしましょう。2つめのLEDはGPIO23(ラズパイのピン16番)を使います2。
2つの点滅を独立したプロセスにする
LEDが1つのときの Worker.ex を以下と書いたので、
LedBlink.init(:led_no0, 18, 1000, 1000)
LEDが2つになるならこんな風に書きたくなります。
LedBlink.init(:led_no0, 18, 200, 200)
LedBlink.init(:led_no1, 23, 400, 600)
が、これでは動きません。最初のLEDの制御に行ったっきりになるので2番目のLEDが制御されないのです。ここでは両方を独立した並行プロセスとして別々に起動する必要があります。いろんな書き方が思いつきますが、ここではシンプルになるように Task を使ってみます。
Task.async(fn -> LedBlink.init(:led_no0, 18, 200, 200) end)
Task.async(fn -> LedBlink.init(:led_no1, 23, 400, 600) end)
こうのように記述すると双方のLEDが独立したプロセスで制御されます。改めて Worker.ex の全体を書き直すとこうなります。
defmodule Exineris3b.Worker do
require Logger
require LedBlink
use GenServer
def start_link(state \\ []) do
GenServer.start_link(__MODULE__, state, name: __MODULE__)
end
def init(state = [:led1]) do
Logger.debug("#{__MODULE__}: single led blink start...")
LedBlink.init(:led_no0, 18, 1000, 1000)
{:ok, state}
end
def init([:led2] = state) do
Logger.debug("#{__MODULE__}: double led blink start...")
Task.async(fn -> LedBlink.init(:led_no0, 18, 200, 200) end)
Task.async(fn -> LedBlink.init(:led_no1, 23, 400, 600) end)
{:ok, state}
end
def init(state) do
Logger.debug("#{__MODULE__}: No such operation defined!! state: #{inspect(state)}")
{:error, state}
end
呼び出すプロセスがLED 2個用になるように Application.ex
の方も書き換えます。
def children(_target) do
[
{Exineris3b.Worker, [:led2]},
]
end
関数引数のパターンマッチ
ここで LED が 1個のときと 2個のときとを Exineris3b.Worker.start_link/1
への引数で区別するため init/1
関数の仮引数に以下のように記述してみました。これどちらもパターンマッチで動きます。マッチしてから state 変数に束縛するか、一旦 state に束縛してからマッチさせるかが違いますが、望む引数があったら実行してそうでなかったらスルーして次のマッチに行くという動作は同様に起こります。
def init(state = [:led1]) do
...
end
def init([:led2] = state) do
...
end
実行
あとのファイルは変える必要がありません。mix firmware して SD カードに焼いて、ラズパイをリブートしてみてください。
まとめ
ラズパイのGPIOを使ってNervesでLチカをやりました。プロセスが Supervisor 下で管理され、かつ各LEDが独立したプロセスで制御されるようにしてみました。これがモジュラリティ高く記述されていて、今後どれぐらいスマートにスムーズに拡張していけるのか検証していきたいです。
参考文献
- Nerves Circuits.GPIO
- Elixir GenServer
- Elixir Task
- はじめてなElixir(18) Taskにも触ってみる
- はじめてな Elixir(19) GenServer で定期的なお仕事をする(まだ終わってない編)
- はじめてNerves(0) ElixirによるIoTフレームワークNervesがとにかく動くようになるためのリンク集
- はじめてNerves(1) 電源ONでLチカアプリを起動させる