前回は GPIO の出力のみ使いました。今回はタクトスイッチを入力として使ってみます。で、ボタンを押してLEDを点灯させるのをなんというのでしょうか。Lチカはボタンの反応を使う印象がないので、ボタンを押すとチカっと点くということで「ボタチカ」と名付けました。
これは Nerves 1.5 (Elixir 1.9) を用いてます。
#準備
まず、出力のハードウェアは前回同様、つまり GPIO 18 と GPIO 23 に LED をつないだラズパイ3Bを使います。ラズパイの入出力ピンを再掲しておきます。
どのピンにどんな信号が出ているかは [Raspberri Pi Pinout] (https://pinout.xyz) などをご覧ください。
タクトスイッチを追加する
ラズベリーパイの GPIO17 の入力をタクトスイッチのON/OFFの検出に使います。
出来上がるとこんな感じです。LEDの電流制限抵抗はここでは 330Ω を用いています。ピンはコネクタ上では以下を使っています。
- 2: 5V
- 6: GND (0V)
- 11: GPIO 17
- 12: GPIO 18
入出力用の基本ソフトウェア
ソフトウェアも前回同様 GpioInOut モジュールをそのまま使います。念のため一式を以下に掲載しておきます。
defmodule GpioInOut do
@behaviour GenServer
require Circuits.GPIO
require Logger
def start_link(pname, gpio_no, in_out) do
Logger.debug("#{__MODULE__} start_link: #{pname}, #{gpio_no}, #{in_out} ")
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_open: #{gpio_no}, #{in_out} ")
Circuits.GPIO.open(gpio_no, in_out) # {:ok, ref} が返るのを期待
end
@impl GenServer
def handle_cast({:write, val}, gpioref) do
Logger.debug("#{__MODULE__} :write #{val} ")
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
Logger.debug("#{__MODULE__} terminate: #{inspect(reason)}")
Circuits.GPIO.close(gpioref)
reason
end
end
ボタン1つに LED 1つ (logical NOT)
まず最初は 1入力 1出力で、ボタンの状態に合わせてLEDが点灯するように作ります。ただし「ボタンを押すとLEDが消灯する」ようにします。これはラズパイがブートしてきた時に何もしなくてもLEDが点いてくれる方が動作がわかりやすいからです。
ボタンを押すと LED が消えるようにする
前回作った buttonled.ex を拡張していきます。以下は拡張した部分のみを書いています。
button_not/5
関数は Worker モジュールから呼ばれる関数で、以下の引数を持ちます。
- button_name: ボタン用の GPIO の論理名
- button_no: ボタン用の GPIO 番号
- led_name: LED 用の GPIO の論理名
- led_no: LED 用の GPIO 番号
- interval: ループをどれぐらいの間隔で回すか (ms)
button_not/5
では、使う GPIO をオープンして、すぐに not_loop/3
関数を呼び出してループを回します。ループでは以下を行います。
- ボタンの状態を取る(0: 押してる、1: 押してない)
- それを使って LED に表示する
- 指定された時間だけ待つ
- 自分自身を呼び出す(無限ループする)
ボタンの出力は負論理であることに注意してください。つまり「ボタンを押す(アクティブ)」な状態が 0 に対応しています。これはハードウェア屋さんには比較的自然ですが、なれてないと戸惑うかもしれません。これに対して今回使ってる LED 出力は「1を出力した時に点灯する(アクティブになる)」正論理です。ボタン出力が負論理で LED が正論理ですので、ボタン出力を特に加工しないでLEDに渡すと「離した時に点灯、押した時に消灯」が実現できます。
defmodule ButtonLed do
require Logger
require GpioInOut
def button_not(button_name, button_no, led_name, led_no, interval) do
Logger.debug("#{__MODULE__}: button_led #{button_name} #{button_no} #{led_name} #{led_no} #{interval} start...")
GpioInOut.start_link(button_name, button_no, :input)
GpioInOut.start_link(led_name, led_no, :output)
not_loop(button_name, led_name, interval)
end
defp not_loop(button_name, led_name, interval) do
{:ok, val} = GpioInOut.read(button_name)
GpioInOut.write(led_name, val)
Logger.debug("#{__MODULE__}, button = #{val}")
Process.sleep(interval)
not_loop(button_name, led_name, interval)
end
end
その他の関数
Worker からは button_not/5
を呼び出すようにします。ここでは GPIO 17 のボタン情報を読み、GPIO 18 の LED に出力を行うようにしてます。ループを回る際に毎度 100ms 停止します。
def init([:button_not] = state) do
Logger.debug("#{__MODULE__}: single led by one button start...")
ButtonLed.not(:button, 17, :led, 18, 100)
{:ok, state}
end
電源 ON で自動的に動くように、Application から Worker を呼び出します。
def children(_target) do
[
{Exineris3b.Worker, [:button_not]},
]
end
実行
あとは mix firmware して SD カードに焼いてリブートです。
LED を2個とも点灯させてみる (Logical Not Not)
もう一方の LED も点けないとちょっとさびしいです。もう一個の LED を、元の LED とは逆に点灯させてみます。これは先程の関数にちょっと書き足すだけで実現できます。関数名は not とそうでないのを両方持つという意味で button_not_not/7
と命名しました。引数の意味は以下です。
- button_name: ボタン用の GPIO の論理名
- button_no: ボタン用の GPIO 番号
- ボタンを押すと消える方の LED
- led0_name: LED 用の GPIO の論理名
- led0_no: LED 用の GPIO 番号
- ボタンを押すと点く方の LED
- led1_name: LED 用の GPIO の論理名
- led1_no: LED 用の GPIO 番号
- interval: ループをどれぐらいの間隔で回すか (ms)
def button_not_not(button_name, button_no, led0_name, led0_no, led1_name, led1_no, interval) do
GpioInOut.start_link(button_name, button_no, :input)
GpioInOut.start_link(led0_name, led0_no, :output)
GpioInOut.start_link(led1_name, led1_no, :output)
not_not_loop(button_name, led0_name, led1_name, interval)
end
defp not_not_loop(button_name, led0_name, led1_name, interval) do
{:ok, val} = GpioInOut.read(button_name)
GpioInOut.write(led0_name, val)
GpioInOut.write(led1_name, 1-val)
Logger.debug("#{__MODULE__}, button = #{val}")
Process.sleep(interval)
not_not_loop(button_name, led0_name, led1_name, interval)
end
Worker からは button_not_not/7
を呼び出すようにします。ここでは GPIO 17 のボタン情報を読み、GPIO 18 と GPIO 23 の LED に出力を行うようにしてます。ループを回る間隔は 100ms だとボタンの操作に対してちょっと遅れるような体感があったので 50ms と短くしてみました。
def init([:button_not_not] = state) do
Logger.debug("#{__MODULE__}: double leds by one button start...")
ButtonLed.button_not_not(:button, 17, :led0, 18, :led1, 23, 50)
{:ok, state}
end
電源ONでの自動実行の記述もします。
def children(_target) do
[
{Exineris3b.Worker, [:button_not_not]},
]
end
例によって mix firmware して SD カードに焼いてリブートさせてください。実行させるとこんな感じです。
ワンショットトリガー (One Shot Trigger)
ボタンの状態がそのまま LED に反映されるだけだとあまりにも工夫がないので、ちょっとひねってみましょう。ボタンを押すと、ボタンを離してもしばらく点灯して、それから LED が消灯するような関数を作成してみます。
時限で LED を光らせる
ロジック回路ではこのような動作をワンショットトリガーと呼ぶので関数名も button_oneshot/6
としています1。引数は以下です。
- button_name: ボタン用の GPIO の論理名
- button_no: ボタン用の GPIO 番号
- led_name: LED 用の GPIO の論理名
- led_no: LED 用の GPIO 番号
- delay: LED が点灯し続ける時間 (ms)
- interval: ループをどれぐらいの間隔で回すか (ms)
- ただし指定しない場合は 50ms をデフォルト値とする
最後の引数は、毎度指定するのも面倒なので省略可能にしてみました。このため関数を1つ定義しただけで button_oneshot/5
も同時に定義したことになります。
def button_oneshot(button_name, button_no, led_name, led_no, delay, interval \\ 50) do
GpioInOut.start_link(button_name, button_no, :input)
GpioInOut.start_link(led_name, led_no, :output)
oneshot_loop(button_name, led_name, div(delay, interval), interval)
end
defp oneshot_loop(button_name, led_name, count, interval, prev \\ 1, timer \\ 0) do
# Logger.debug("#{__MODULE__}, #{count}, #{prev}, #{timer}")
# Logger.debug("#{0..timer |> Enum.map(&(to_string(rem(&1,10))))}")
Logger.debug("#{0..timer |> Enum.map(fn _n -> "*" end)}")
{:ok, now} = GpioInOut.read(button_name)
timer = if (now == 0) and (prev == 1), do: count, else: timer
timer = if (timer > 0), do: timer - 1, else: 0
GpioInOut.write(led_name, (if (timer > 0), do: 1, else: 0))
Process.sleep(interval)
oneshot_loop(button_name, led_name, count, interval, now, timer)
end
ループ中の動作
今度のループ関数 oneshot_loop/6
はちょっとばかり複雑です。まずは引数から。
- button_name: ボタン用の GPIO の論理名(ループ中に変化しない)
- led_name: LED 用の GPIO の論理名(ループ中に変化しない)
- count: LED が点灯し続けるループ回数(ループ中に変化しない)
- interval: ループを回る時間(ループ1回についての待ち時間、ループ中に変化しない)
- これは delay(ms) が interval(ms) の何倍かで決まる
- interval: ループをどれぐらいの間隔で回すか (ms)
- prev: 直前のボタンの状態
- 初期状態ではボタンが押されてないとする(デフォルト値: 1)
- timer: LEDを消灯するまでループを回る回数
- 初期状態では LED の消灯用の遅延タイマが起動していないとする(デフォルト値: 0)
ループ内の動作はこんな感じになります。
- ボタンの状態を取る(0: 押してる、1: 押してない)
- ボタンが押されたなら、すなわち直前に押されてなくて今押されたなら、タイマを起動する。
- タイマの起動は timer に count を束縛することで行う
- タイマが切れてなければ(まだ timer 値が 0 になってないなら)タイマを減ずる
- タイマが切れてなければ LED を点灯し、タイマが切れたら(timer が 0 なら)LED を消灯する
- 指定された時間だけ待つ
- 自分自身を呼び出す(無限ループする)
Elixir ならでわの if な記述
ここではどの if
も関数として使ってます。Elixir やってると if
を制御構造として使うのがなんだかはばかられるのでこうしてみました。これが綺麗なのかと言われるとどうでしょう、ちょっとわかりません。
その他の関数
これを Worker から呼び出します。以下の設定では、ボタンをおした直後から1秒 (1000ms) LED がつきっぱなしになります。そのときボタンをさっさと離そうが、押しっぱなしにしようが「押した直後から1秒光って消灯する」という動作をします。光ってる最中に、ボタンを離してもう一度押すと、押し直してから1秒経つまで光っぱなしです。
なお、もう一つの LED は前回の「LED 点滅関数」を呼び出すことで、50ms光って200ms消えるというのを延々と繰り返すようにしてみました。これらの関数は独立したプロセスとして動作します。
def init([:button_oneshot] = state) do
Logger.debug("#{__MODULE__}: one shot led by one button start...")
Task.async(fn -> LedBlink.init(:led_no0, 18, 50, 200) end)
Task.async(fn -> ButtonLed.button_oneshot(:button, 17, :led, 23, 1000) end)
# Task.async(fn -> ButtonLed.button_oneshot(:button, 17, :led, 23, 10000, 1000) end) # デバッグ用
{:ok, state}
end
例によって自動起動のプログラムはこちら。
def children(_target) do
[
{Exineris3b.Worker, [:button_oneshot]},
]
end
実行
例によって例によって mix firmware して SD カードに焼いてリブートさせてください。こんな風に動きます。
リセット付きワンショットトリガー (One Shot Trigger with Reset)
入力を複数にしてみます。ボタンを2つにしてみましょう。先ほどの LED GPIO 23 の向かい側のピンに新たにボタンを追加します。GPIO の 22 番になります。
先ほどは一旦 LED が点灯すると時間が経過するまで消しようがなかったです。新しいボタンで強制的に消灯できるようにしましょう。
新しいボタンで強制消灯
新しい関数は botton_one_shot_reset/8
です。引数多いですね。
- button_on_name: トリガボタン用の GPIO の論理名
- button_on_no: トリガボタン用の GPIO 番号
- button_off_name: 強制消灯用ボタン用の GPIO の論理名
- button_off_no: 強制消灯用ボタン用の GPIO 番号
- led_name: LED 用の GPIO の論理名
- led_no: LED 用の GPIO 番号
- delay: LED が点灯し続ける時間 (ms)
- interval: ループをどれぐらいの間隔で回すか (ms)
最後の引数は、指定しない場合は 50ms をデフォルト値としますので、これまた関数を1つ定義しただけで botton_one_shot_reset/7
関数も同時に定義したことになります。
def button_oneshot_reset(button_on_name, button_on_no, button_off_name, button_off_no, led_name, led_no, delay, interval \\ 50) do
GpioInOut.start_link(button_on_name, button_on_no, :input)
GpioInOut.start_link(button_off_name, button_off_no, :input)
GpioInOut.start_link(led_name, led_no, :output)
oneshot_loop_reset(button_on_name, button_off_name, led_name, div(delay, interval), interval)
end
defp oneshot_loop_reset(button_on_name, button_off_name, led_name, count, interval, prev \\ 1, timer \\ 0) do
{:ok, now} = GpioInOut.read(button_on_name)
{:ok, reset} = GpioInOut.read(button_off_name)
# Logger.debug("#{__MODULE__}, #{count}, #{prev}, #{timer}")
# Logger.debug("#{__MODULE__}, #{now}, #{prev}, #{reset}, #{timer}")
Logger.debug("#{0..timer |> Enum.map(fn _n -> "*" end)}")
timer = if (now == 0) and (prev == 1), do: count, else: timer
timer = if (timer > 0) and (reset == 1), do: timer - 1, else: 0
GpioInOut.write(led_name, (if (timer > 0), do: 1, else: 0))
Process.sleep(interval)
oneshot_loop_reset(button_on_name, button_off_name, led_name, count, interval, now, timer)
end
GPIO 入力の増加に対して処理が増えています。本質的に変えたのは
timer = if (timer > 0) and (reset == 1), do: timer - 1, else: 0
の行です。これは reset が 1 の場合、すなわち GPIO 22 のボタンが押されてない場合は前回同様に timer の減算処理をします。reset が 0 の場合、すなわち GPIO 22 ボタンが押されていると timer の値にかかわらず値を 0 にして LED を消灯させます。
その他の関数
例によって Application モジュール、Worker モジュール、から呼び出します。
def init([:button_oneshot_reset] = state) do
Logger.debug("#{__MODULE__}: one shot led by one button start...")
Task.async(fn -> ButtonLed.button_oneshot_reset(:button_on, 17, :button_off, 22, :led, 23, 2500) end)
{:ok, state}
end
今回は点灯時間があまり短いとリセットを試すのがせわしなくなるのでタイマは 2.5 秒ほどに伸ばします。
def children(_target) do
[
{Exineris3b.Worker, [:button_oneshot_reset]},
]
end
実行
例によって例によって例によって mix firmware して SD カードに焼いてリブートさせてください。こんな風に動きます。
Elixir ならでわのパイプとEnumを使った記述
ログ出力に使ってる 0..timer |> Enum.map(fn _n -> "*" end)
も Elixir っぽいです。タイマの値に相当する長さの "****"
文字列を生成します。コメントアウトしてるログ出力の 0..timer |> Enum.map(&(to_string(rem(&1,10))))
は "*"
の代わりに1の位の数字を出します。こういうのは綺麗にかけた感じがして気分が良いです。RingLogger.attach してお楽しみください。なお、Logger の引数ではなく、文字列として値が欲しい場合は |> Enum.join()
してやれば一つの文字列となります。
iex(1)> require Logger
Logger
iex(2)> timer = 20
20
iex(3)> 0..timer |> Enum.map(fn _n -> "*" end)
["*", "*", "*", "*", "*", "*", "*", "*", "*", "*", "*", "*", "*", "*", "*", "*", "*", "*", "*", "*", "*"]
iex(4)> Logger.debug("#{0..timer |> Enum.map(fn _n -> "*" end)}")
01:54:35.520 [debug] *********************
:ok:
iex(5)> 0..timer |> Enum.map(fn _n -> "*" end) |> Enum.join
"*********************"
iex(6)> Logger.debug("#{0..timer |> Enum.map(&(to_string(rem(&1,10))))}")
01:55:24.941 [debug] 012345678901234567890
:ok
iex(7)> 0..timer |> Enum.map(&(to_string(rem(&1,10)))) |> Enum.join
"012345678901234567890"
動いてるラズパイに ssh して RingLogger.attach すると以下のようにログが見えます。なおこれを綺麗に見るときは、他の Logger.debug を落としておいてください。
まとめ
GPIO での入力・出力の両方を使ってみました。ロジック回路ふうな動作をするプログラム、今回はワンショットトリガを作ってみました。
参考文献
- Nerves Circuits.GPIO
- はじめてNerves(0) ElixirによるIoTフレームワークNervesがとにかく動くようになるためのリンク集
- はじめてNerves(1) 電源ONでLチカアプリを起動させる
- はじめてNerves(2) GenServer を使ってLチカをする
-
アナログ電子回路では同様の動作をする回路を単安定マルチバイブレータと呼びます。 ↩