LoginSignup
10
2

More than 3 years have passed since last update.

はじめてNerves(2) GenServer を使ってLチカをする

Last updated at Posted at 2020-02-06

今回はラズパイのGPIOを用いて外部に用意したLEDをチカります。できるだけ汎用性高くして再利用性とか可読性とかを上げたいと思ってます。さあどうなるか。

これは Nerves 1.5 (Elixir 1.9) を用いてます。

準備

Nerves の準備がまだの方は はじめてNerves(1) 電源ONでLチカアプリを起動させる をご覧ください。この記事の まずはターゲットのファームウェアを焼けるようにする までは同じです。このあとの ターゲット用のプログラムを書く の章から道が分かれますので、この直前までやってください。ただし今回のプロジェクト名は exineris3b でやります。

ハードウェアを接続する

今回はラズパイについているLEDではなく、外部に回路を作ってそれを制御します。ココからハードウェアをどんどん追加していきます。今回やる内容は LED と抵抗とタクトスイッチをブレッドボード上に接続して使います。

ラズベリーパイの外部入出力用の信号は基板の脇に立ってる40ピンのコネクタでやりとりします。どのピンにどんな信号が出ているかは Raspberri Pi Pinout などをご覧ください。以下の図はこの抜粋で数字だけ書いてあるのが GPIO の番号を示しています。
alt

これらのハードウェアをどう組んでいくかあまり詳しくない方は、例えば カラー図解 最新 Raspberry Piで学ぶ電子工作 作って動かしてしくみがわかる (ブルーバックス) をごらんください。これに出てくるような回路を Nerves で制御していこうかなと考えています(どこまで忠実にやるかは別として)。なお、これ用に Raspberry Piで学ぶ電子工作 パーツセット もありますのでご参考まで。

今回はまず、ラズベリーパイの GPIO18 の出力を LED の点灯用に使います。これは Circuits.GPIO の例に従っています。
alt

なお、この図では電流制限抵抗が 100Ω になっていますが、私が使った回路では 330Ω を使いました。

GPIO 用ののライブラリを読み込む設定

今回は GPIO というインタフェースを用います。これには Circuits.GPIO ライブラリを使います。前回同様に、このライブラリを使うのに依存関係を mix.exs ファイルに指定します。

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行を加えます。

lib/exineris3b/application.ex
  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 で指定したプロセス名を指定します。
lib/exineris3b/gpioinout.ex
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 のコールバック関数ではありません。

ledblink.ex
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/1start_link/1 のコールバックで、ここではLED点滅用の関数を呼びます

なお、本来は @behaviour GenServer と書くべきと思いますが Nerves でうまく動かないので use GenServer で代用しています。これ、もう少し調べて後日ご報告します。

lib/exineris3b/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(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 に以下の様に記述します。コメントが長いですが、プログラムとして意味があるのは最後の一行です。

lib/exineris3b/application.ex
  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 に従ってログが出力されているのが見えます。

out.gif

なお、自動で立ち上がらなかったらログインして 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 を以下と書いたので、

lib/exineris/worker.ex
    LedBlink.init(:led_no0, 18, 1000, 1000)

LEDが2つになるならこんな風に書きたくなります。

lib/exineris/worker.ex
    LedBlink.init(:led_no0, 18, 200, 200)
    LedBlink.init(:led_no1, 23, 400, 600)

が、これでは動きません。最初のLEDの制御に行ったっきりになるので2番目のLEDが制御されないのです。ここでは両方を独立した並行プロセスとして別々に起動する必要があります。いろんな書き方が思いつきますが、ここではシンプルになるように Task を使ってみます。

lib/exineris/worker.ex
    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 の全体を書き直すとこうなります。

lib/exineris3b/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 の方も書き換えます。

lib/exineris3b/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 カードに焼いて、ラズパイをリブートしてみてください。

out.gif

まとめ

ラズパイのGPIOを使ってNervesでLチカをやりました。プロセスが Supervisor 下で管理され、かつ各LEDが独立したプロセスで制御されるようにしてみました。これがモジュラリティ高く記述されていて、今後どれぐらいスマートにスムーズに拡張していけるのか検証していきたいです。

参考文献


  1. ネットワーク周りを上手にセットアップしているなら、もちろん mix firmware.gen.scriptupload.sh ファイルを生成して ./upload.sh IPアドレス で焼いていただいて構いません。 

  2. たまたま1つ目のLEDが12番ピンで、GND挟んで隣だったのでこれにしてます。 

10
2
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
2