LoginSignup
9
4

More than 1 year has passed since last update.

はじめてNerves(5) SPIを使ったA/D変換結果をGPIOのPWMでLチカ

Last updated at Posted at 2020-02-18

前回まで GPIO を扱ってきました。そろそろデジタル入出力にも飽きてきたのでアナログ値をやり取りしたくなってきます。今回は SPI を用いてアナログ入出力をやってみます。実は SPI を使うのは初めての経験です。はてどうなるか。

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

準備

今回はアナログな値で遊ぶためにハードウェアとソフトウェアの両方に追加を行います。

ハードウェアは、アナログ入力用の10kΩの半固定抵抗と、その値をデジタルに変換するための A/DコンバータIC をブレッドボードに以下を追加してラズパイと接続します。あとアナログ出力用に LED をラズパイの GPIO に接続します。

ソフトウェアは Elixir Circuits の SPI を使います。

アナログ入力用ハードウェア

ツマミ付きの可変抵抗を使ってアナログ値を入力できるようにします。今回は可変抵抗がなかったので半固定抵抗を使ってます。可変抵抗か半固定抵抗かの違いは、普段からグリグリ回すのか、たまにしか回さないかの違いで、電気的には同じものです。どちらも固定抵抗の両端に相当する2つの端子の他に、中間的な抵抗値になる端子を1つ持ってて、この中間的な抵抗値というのがツマミを回す角度によって変化します。

このアナログ値をデジタル値に変換して読み取るのですが、抵抗値そのものを読み取るのではなく電圧を読み取らせます。具体的には、固定抵抗部分の両端に0.0Vと3.3Vをかけておくと、可変抵抗部分の端子にはツマミの回した角度によって 0.0〜3.3V の電圧が出るので、それを測定してデジタル化します。

私は秋月電子で 10kΩの半固定抵抗A/DコンバータIC MCP3208 を購入しました。半固定抵抗は以下の写真で1辺が10mmぐらいの大きさです。ピンとピンとの間がブレッドボードのピン間の間隔と同じ 2.54mm です。

MCP3208 は以下の写真で、長辺が20mmぐらいの大きさです。
MCP3208

  • 半固定抵抗 10kΩ
    • 固定抵抗部の一端: 3.3V
    • 可変部: MCP3208 の第1ピンへ
    • 固定抵抗部のもう一端: 0V (GND)
  • A/D コンバータ MCP3208
    1. CH0: 半固定抵抗の可変部より
    2. CH1: 繋がない
    3. CH2: 繋がない
    4. CH3: 繋がない
    5. CH4: 繋がない
    6. CH5: 繋がない
    7. CH6: 繋がない
    8. CH7: 繋がない
    9. $Digital\ Ground$: 0V(ラズパイ入出力 6ピンなど)
    10. $\overline{CS}$/SHDN: SPI0 CE0(ラズパイ入出力 24ピン)
    11. $D_{IN}$: SPI0 MOSI(ラズパイ入出力 19ピン)
    12. $D_{OUT}$: SPI0 MISO(ラズパイ入出力 21ピン)
    13. $Clock$: SPIO SCLK(ラズパイ入出力 23ピン)
    14. $Analog\ Ground$: 0V(ラズパイ入出力 6ピンなど)
    15. $V_{REF}$: 3.3V(ラズパイ入出力 1ピンなど)
    16. $V_{DD}$: 3.3V(ラズパイ入出力 1ピンなど)

図は「MCP3208 ラズパイ」とかで検索して見てください。私はカラー図解 最新 Raspberry Piで学ぶ電子工作 作って動かしてしくみがわかる (ブルーバックス) の137〜138ページの第6.3.2節「半固定抵抗を用いた回路」を参照しました。

ラズパイのSPIに関するピンは Raspberry Pi Pinout の SPI タグ でご覧ください。
alt

信号に関する細かい話

今回はあまり高い精度を必要としないアナログ値をあつかうので結線を雑にやってます。より精度を高くするには

  • Analog GND と半固定の GND を近くなるように配線する
  • $V_{REF}$ と半固定の電源側を近くなるように配線する
  • アナログ系とデジタル系とをできるだけ鑑賞しないようにする、例えば…
    • $V_{REF}$ および Analog GND のペアと、$V_{DD}$ および Digital GND のペアと、をできるだけ独立させる

などということをします。今回は神経質になる必要はありません。あと MCP3208 の入力にはシングルエンド(single end)と疑似差動(pseudo differential)の2種類の入力があり、後者はコモンモードノイズを若干減らせるような記述がデータシートにありますが、今回は結線が十分短いのでシングルエンドで接続します。

アナログ出力用ハードウェア

アナログといいつつ、今回はデジタル出力でアナログ出力を模倣します。具体的には PWM で LED を駆動してそれっぽい光らせ方をします。これまで使った LED とは別に用意することにします。今度は GPIO 25 を使います。以下の図で GPIO は 18 に代えて 25 を、抵抗は 100Ωに代えて 330オームを使います。

alt

ハードウェアのできあがり

前回までのハードウェアに追加したので大変狭苦しくなりました。写真のブレッドボードで、上半分(18行目まで)が前回までに作成した部分、下半分が今回作成した部分です。

ここで配線の色は以下のように色分けしています。

  • 0V (GND):黒
  • 3.3V:橙
  • 5.0V:赤
  • ラズパイ方向への入力:白
  • ラズパイからの出力:黃

ソフトウェア

SPI の操作用に Elixir Circuits SPI を使います。

なお、この公式ページに A/D コンバータの回路がありますが MCP3002 という別のチップ用ですので、このままでは MCP3208 には使えません。同じくサンプルコードがありますが、このままでは動きません。これから A/D コンバータを調達する場合は MCP3002 を用意して、この公式ドキュメントをそのまま使うという手もあります。

さて、SPI を有効にするのに以下を mix.exs に追加して mix deps.get コマンドを実行してください。

mix.exs
  ...
  defp deps do
    [
      ... # 途中省略
      {:circuits_spi, "~> 0.1"},
    ]
  end
  ...

正しくインストールできているかとりあえずの試験をする

ハードウェアとソフトウェアが準備できたら mix firmware して ./upload.shmix firmware.burn で SD カードを焼いてラズパイを再起動してください。

すでに突っ込んであるソフトウェアが動くかもしれませんが、それは放っておいてラズパイに ssh (かシリアルコンソール)で接続してください。そして iex で手動で Circuits.SPI.bus_names/0 関数を実行してみてください。

iex> Circuits.SPI.bus_names()
["spidev0.0", "spidev0.1"]

のように空でないリストが返れば、ハードウェアとソフトウェアがおおよそ正しくインストールされています。

A/D コンバータの出力を SPI 経由で取得する

では準備ができたので、まずはアナログ入力で遊んでみます。

SPI で A/D コンバータの出力を取り込む

GPIO 同様に GenServer で SPI の入出力を取り扱います。ここでも単独のプロセスとして動くようにします。

  • SpiInOut.start_link/2: SPIインタフェースをオープンする
    • SPI IF の論理名を ATOM で与える
    • SPI の物理インタフェースを文字列で与える
  • SpiInOut.send_receive/2 SPIインタフェースにコマンドを出力して結果を受け取る
    • SPI IF の論理名を ATOM で与える
    • SPI に与えるコマンド(8bit の整数倍で)
      • 返ってくる値は、与えたコマンドの長さと同じになる
  • SpiInOut.stop/1: SPI インタフェースをクローズする
    • SPI IF の論理名を ATOM で与える
lib/exineris3b/spiinout.ex
defmodule SpiInOut do
  @behaviour GenServer
  require Circuits.SPI
  require Logger

  def start_link(pname, spi_name) do
    Logger.debug("#{__MODULE__} start_link: #{inspect(pname)}, #{spi_name} ")
    GenServer.start_link(__MODULE__, spi_name, name: pname)
  end

  def send_receive(pname, bitstring) do
    GenServer.call(pname, {:transfer, bitstring})
  end

  def stop(pname), do: GenServer.stop(pname)

  @impl GenServer
  def init(spi_name) do
    Logger.debug("#{__MODULE__} init_open: #{spi_name} ")
    Circuits.SPI.open(spi_name) # {:ok, ref} が返るのを期待
  end

  @impl GenServer
  def handle_call({:transfer, bitstring}, _from, spiref) do
#    Logger.debug("#{__MODULE__} :transfer #{inspect(bitstring)} ")
    {:reply, Circuits.SPI.transfer(spiref, bitstring), spiref}
  end

  @impl GenServer
  def terminate(reason, spiref) do
    Logger.debug("#{__MODULE__} terminate: #{inspect(reason)}")
    Circuits.SPI.close(spiref)
    reason
  end
end

SPIが動くか基本的な試験をする

呆れるほどシンプルです。では試験をしてみます。まずは、ラズパイのブート後に何か勝手に動かないように application.ex を変更しておきます。

lib/exineris3b/application.ex
  def children(_target) do
    [
      # none
    ]
  end

これで mix firmware して mix firmware.burnupload.sh で SD にファームウェアを焼いて、そしてラズパイをリブートします。リブートしたら ssh でログインします。

iex(1)> RingLogger.attach # ロガーを起動
:ok
iex(2)> {:ok, spiref} = SpiInOut.start_link(:spi0, "spidev0.0") # SPIインタフェースをオープンする

04:00:03.261 [debug] Elixir.SpiInOut start_link: spi0, spidev0.0 

04:00:03.261 [debug] Elixir.SpiInOut init_open: spidev0.0 
{:ok, #PID<0.1091.0>}
iex(3)> SpiInOut.send_receive(:spi0, <<0x06, 0x00, 0x00>>) # 半固定抵抗を適当な位置にして読み込み指令を送って返ってきた値をみる
{:ok, <<0, 3, 124>>}
iex(4)> SpiInOut.send_receive(:spi0, <<0x06, 0x00, 0x00>>) # 半固定抵抗を目一杯左に回して読み込み指令を送って返ってきた値をみる
{:ok, <<0, 0, 0>>}
iex(5)> SpiInOut.send_receive(:spi0, <<0x06, 0x00, 0x00>>) # 半固定抵抗を目一杯右に回して読み込み指令を送って返ってきた値をみる
{:ok, <<0, 15, 255>>}

半固定抵抗を回した位置に応じて値が変わります。最後を見ると返ってきてるのは 0b111111111111 です。ちゃんとアナログ値が12bitのデジタル値になって返ってきてるようです。なおここで24bitを返り値として得ていますが、有効なのは下位12bitです。上位12bitは無視してください。

SPI で MCP3208 に与える指令の意味

今回は MCP3208 に与える指示を24bitで <<0x06, 0x00, 0x00>> と決め打ちで書いています。そして返ってきた値の下位12bitを使うようにしています。これについて大まかに説明します。詳細は MCP3208 Datasheet の21ページ 6.0 Applications Information を読んでください。

今回のプログラムでは以下の意味を MCP3208 への指示として与えています。

  • スタートビット:以下の表で S の bit、必ず1
  • アナログ入力方式:以下の表で D の bit、シングルエンドを指示するのに1
  • SPIのチャンネル:以下の表で CBA の 3bit、今回は0チャンネルを指示するのに000
MSB<->LSB 76543210 76543210 76543210
MCP3208の入力 00000SDC BAXXXXXX XXXXXXXX
今回の指定 00000110 00000000 00000000

上の表で X は Don't care すなわち0でも1でもどちらでも良いです。今回は 0 にしてあります。このビットストリームが <<0b00000110, 0b00000000, 0b00000000>> すなわち <<0x06, 0x00, 0x00>> です。同様に SPI の第7チャンネルをシングルエンドで使いたいのであれば <<0b00000111, 0b11000000, 0b00000000>> すなわち <<0x07, 0xc0, 0x00>> を使います。本来であれば、シングルエンドの入力方式にするかどうかと SPI のどのチャンネルを使うのかを引数で指定するようにするのが正しいプログラミングでしょう。今回はサボりました。

そして出力の方は以下の表のように最初の11bitが不定(以下の表で?表記:0か1かに意味がない)で、そのあとに1bitの 0 があり、それに続く12bit(表で L〜A)が意味のある値です。

MSB<->LSB 76543210 76543210 76543210
MCP3208の出力 ???????? ???0LKJI HGFEDCBA

なお、表で MSB (Most Significant Bit) が上位側を LSB (Least Significant Bit) が下位側を示しています。

半固定抵抗の値を連続的に読んでみる

基本的な入出力が出来たのでもう少し複雑にします。上では無効な上位12bitも読んでいたのでそれをカットするようにして、あとは半固定抵抗の値が連続的に取れるようにします。

Pwm モジュールは後でLEDをPWMで点灯するつもりなので、このような名前にしました。が、今は PWM はまだ手をつけないで SPI でのデータのやり取りだけにします。Pwm.read_reg/3 関数は以下の引数を持ちます。

  • オープンした SPI の ref 値(すでにどこかでオープンしていることが前提)
  • SPI でデバイス(今回は MCP3208)に与える指令
  • ループを回る間隔(デフォルトは 50ms)

この関数は SPI 経由で MCP3208 に指令を与えて、その返り値の必要な部分である下位12bitを切り出して、それをログ出力する… を繰り返します。

lib/exineris3b/pwm.ex
defmodule Pwm do
  require Logger
  require GpioInOut
  require SpiInOut

  def read_reg(spi_name, inst, interval \\ 50) do
    {:ok, val} = SpiInOut.send_receive(spi_name, inst)
    <<_::size(12), val12bit::size(12)>> = val
    Logger.debug("#{__MODULE__} #{spi_name}: #{inspect(val12bit)} #{0..div(val12bit,256) |> Enum.map(fn _n -> "=" end)}")
    Process.sleep(interval)
    read_reg(spi_name, inst, interval)
  end

Woker モジュールは SPI をオープンして、あとは上で定義した関数を呼び出します。

lib/exineris3b/worker.ex
defmodule Exineris3b.Worker do
  use GenServer
  require Logger

  def start_link(state \\ []) do
    GenServer.start_link(__MODULE__, state, name: __MODULE__)
  end

  def init(state = [:readreg]) do
    Logger.debug("#{__MODULE__}: Read register start...")
    Pwm.read_reg(:spi0, <<0x06, 0x00, 0x00>>, 1000)
    {:ok, state}
  end
end

Application モジュールに以下を記述して上の Worker.start_link/1 関数を呼び出します。

lib/exineris3b/application.ex
  def children(_target) do
    [
      {Exineris3b.Worker, [:readreg]},
    ]
  end

これをコンパイルして SD カードを焼いてラズパイを再起動します。ラズパイにログインして RingLogger.attach すると半固定抵抗の位置が 1000ms ごとにログに表示されます。

outb.gif

半固定抵抗の位置に応じて LED を PWM で点灯させる

アナログ入力ができたのでアナログ出力をやります。アナログ入力と行っても正確には A/D 変換後の値の入力であるのと同様に、今回のアナログ出力は正確には PWM (Pulse Width Moduration) というデジタル出力手法でアナログ値を模倣するような出力です。具体的には GPIO からの出力を細かく ON/OFF して LED が中間の明るさで光るようにします。

以下は半固定抵抗の値を取得して、それに応じて PWM で LED を点滅させる関数です。半固定抵抗の位置は A/D 変換後に 0〜4095 の 12bit で出力されます。今回はこの上位4bitを使います。0だったら(12bit で言うと 0〜255 だったら)完全に消灯、15だったら(12bit で言うと 3840〜4095だったら)完全に点灯、その間なら値に応じた割合で点滅するようにしました。引数は以下のようになってます。

  • オープンした SPI の名前を atom で
  • SPI デバイスに対する指令をビットストリームで
  • オープンした LED 用の GPIO の名前を atom で
  • ループを回る周期をmsで(省略時のデフォルトが 50ms)
lib/exineris3b/pwm.ex
  def potentioled(spi_name, spi_inst, led_name, interval \\ 50) do
    {:ok, val} = SpiInOut.send_receive(spi_name, spi_inst) # SPI で半固定抵抗の値を持ってくる
    <<_::size(12), upper4bit::size(4), _::size(8)>> = val # 必要なのはA/D変換後の12bitのうちの上位4bit
    on_time  = div(upper4bit * interval, 15) # LED を ON にする時間の計算
    off_time = div((15 - upper4bit) * interval, 15) # OFF にする時間の計算
    Logger.debug("#{__MODULE__} #{spi_name}: #{on_time}-#{off_time} #{0..upper4bit |> Enum.map(fn _n -> "=" end)}")
    GpioInOut.write(led_name, :true)  # LED を ON にする
    Process.sleep(on_time)            # 計算した時間だけ ON 状態で待つ
    GpioInOut.write(led_name, :false) # LED を OFF にする
    Process.sleep(off_time)           # 計算した時間だけ OFF 状態で待つ
    potentioled(spi_name, spi_inst, led_name, interval) # 無限ループ
  end

これを呼び出す Worker モジュールの関数が以下です。SPI と LED をそれぞれオープンして上の関数に渡します。周期は動作がわかりやすいように 1000ms と長くしてます。

lib/exineris3b/worker.ex
defmodule Exineris3b.Worker do
#  @behaviour GenServer # does not work with Nerves correctly
  use GenServer
  require Logger

  def start_link(state \\ []) do
    GenServer.start_link(__MODULE__, state, name: __MODULE__)
  end

  def init(state = [:potentioled]) do
    Logger.debug("#{__MODULE__}: PWM LED with potentiometer start...")
    GpioInOut.start_link(:led_pwm, 25, :output)
    SpiInOut.start_link(:spi0, "spidev0.0")
    Pwm.potentioled(:spi0, <<0x06, 0x00, 0x00>>, :led_pwm, 1000)
    {:ok, state}
  end```

ブート時に自動的に機能するように以下を設定します。ここで potentio とは、半固定抵抗を potentiometer というところから来ています。

lib/exineris3b/application.ex
  def children(_target) do
    [
      {Exineris3b.Worker, [:potentioled]},
    ]
  end

これを実行したのが次の動画です。

out4.gif

PWM の周期を変えてみる

上で Pwm.potentioled(:spi0, <<0x06, 0x00, 0x00>>, :led_pwm, 1000) とやった例は、第4引数が周期を指定しており、SPI からの半固定抵抗の読み込みから次の読み込みまで、点灯と消灯の合計が 1000ms (1Hz) になるようにしてます。これですといわゆるLチカ風になります。この周期を変えると同じ PWM でも随分と違った感じになります。

これを Pwm.potentioled(:spi0, <<0x06, 0x00, 0x00>>, :led_pwm) にすると周期がデフォルトの 50ms (20Hz) になります。若干のチラつきがわかりますね。

out2.gif

さらに短い周期にして Pwm.potentioled(:spi0, <<0x06, 0x00, 0x00>>, :led_pwm, 10) で間隔が 10ms (100Hz) でやったのが次です。

out2.gif

これで LED の ON/OFF は意識されなくなり、点灯しっぱなしで明るさが変わっているように感じます。こうなるとアナログ出力していると言っても良いような状態です。

SPI デバイスに与える指令と返り値の処理をまとめる

実は上の例は「もくもく会」でやった成果です。時間内に切りの良いところまで到達するのに「とにかくアナログで入出力ができる」のを優先しました。のでプログラミングの美しさが犠牲になっています。あとから見て、どうにもみっともないので改善してみました。

SPI デバイスへの指令と返り値の処理を一箇所に記述する

上の例では Worker モジュールで SPI デバイスへ指示する24bitを記述しているのに、それから返ってきた 24bit から必要な部分を切り出す部分は Pwm モジュール内に記述しています。これはどちらも MCP3208 というデバイスに依存しているので、プログラム中の同じ場所に記述するのが望ましいでしょう。

以下では Pwm モジュールには具体的な SPI の動作に関係するところは記載せずに、SPI への指示も返り値の処理も Worker モジュールから渡される引数で受け取って処理するように書いてみた例です。

半固定抵抗の位置を読むやや綺麗なプログラム

まず、元の Pwm.readreg 関数に代えて Pwm.read_potentio/4 関数を作ってみた例です。

  • オープンした SPI の名前を atom で
  • SPI デバイスへの指示をビットストリームで渡す
  • SPI デバイスからの返り値をどう処理するかを関数で渡す
  • 周期を ms で

ここで第3引数が pickup_bs という関数を渡すことに注意してください。SPI デバイスからの値を rec_bs 変数に受け取って pickup_bs.(rec_bs) で必要な部分を取り出すようにしています。

lib/exineris3b/pwm.ex
  def read_potentio(spi_name, inst_bs, pickup_bs, interval \\ 50) do
    {:ok, rec_bs} = SpiInOut.send_receive(spi_name, inst_bs) # SPI デバイスとのやりとり
    val12bit = pickup_bs.(rec_bs) # SPI デバイスからの返り値から必要な部分を取り出す
    Logger.debug("#{__MODULE__} #{spi_name}: #{inspect(val12bit)} #{0..div(val12bit,256) |> Enum.map(fn _n -> "=" end)}")
    Process.sleep(interval)
    read_potentio(spi_name, inst_bs, pickup_bs, interval)
  end

この必要な部分を取り出す関数は以下の &split12_12/1 という風に記述して渡します

lib/exineris3b/worker.ex
  def init([:read3208] = state) do
    Logger.debug("#{__MODULE__}: Read register start...")
    SpiInOut.start_link(:spi0, "spidev0.0")
    Pwm.read_potentio(:spi0, <<0x06, 0x00, 0x00>>, &split12_12/1)
    {:ok, state}
  end

  defp split12_12(<<_::size(12), latter12bit::size(12)>>) do
    latter12bit
  end

この split12_12/1 関数は24bitのストリームを受け取って、下位12bitを返す関数です。ここでは defp を使って関数を定義してますが & 記法を使うとこのモジュールを越えて他のモジュールに渡すことができます。これは今回の発見でした。

それにしても Elixir ではビットストリームもパターンマッチが使えるので本当に綺麗に書くことができます。IoT 向けな言語といえましょう。あんまり綺麗に短く書けるので、どうもわざわざ名前付き関数にするのが大仰にも感じます。そういう向きには以下のように関数への引数部分に匿名関数で渡すという手もあります。

lib/exineris3b/worker.ex
  def init([:read3208] = state) do
    Logger.debug("#{__MODULE__}: Read register start...")
    SpiInOut.start_link(:spi0, "spidev0.0")
    Pwm.read_potentio(:spi0, <<0x06, 0x00, 0x00>>,
      fn(<<_::size(12), latter12bit::size(12)>>) -> latter12bit end)
    {:ok, state}
  end

綺麗にしたプログラムで改めて LED を PWM で光らせる

では同様に SPI デバイスとのやりとりを一箇所にまとめた版のLチカプログラムを作ります。今度は下位12bitではなく中間部の4bitですので若干複雑になります。

  • オープンした SPI の名前
  • SPI デバイスへの指令をビットストリーム
  • SPI デバイスからの返り値を処理する関数
  • オープンした LED 用 GPIO の名前
  • 周期を ms で
lib/exineris3b/pwm.ex
  def potentio_led(spi_name, spi_inst, spi_pickup, led_name, interval \\ 50) do
    {:ok, rec_bs} = SpiInOut.send_receive(spi_name, spi_inst)
    upper4bit = spi_pickup.(rec_bs)
    on_time  = div(upper4bit * interval, 15)
    off_time = div((15 - upper4bit) * interval, 15)
    Logger.debug("#{__MODULE__} #{spi_name}: #{on_time}-#{off_time} #{0..upper4bit |> Enum.map(fn _n -> "=" end)}")
    GpioInOut.write(led_name, :true)
    Process.sleep(on_time)
    GpioInOut.write(led_name, :false)
    Process.sleep(off_time)
    potentio_led(spi_name, spi_inst, spi_pickup, led_name, interval)
  end

pickup_middle/4 関数は、ビットストリーム bs の上位 l ビットと下位 n ビットを捨てて、その中間の m ビットを返す関数です。これを使って pickup_middle4/1 関数を、ビットストリームの13番目から4ビットを取り出す関数として定義して、それを Pwm.potentio_led/5 関数の第3引数として渡しています。

lib/exineris3b/worker.ex
  def init([:potentio_led] = state) do
    Logger.debug("#{__MODULE__}: PWM LED with potentiometer start...")
    GpioInOut.start_link(:led_pwm, 25, :output)
    SpiInOut.start_link(:spi0, "spidev0.0")

    Pwm.potentio_led(:spi0, <<0x06, 0x00, 0x00>>, &pickup_middle4/1, :led_pwm)
    {:ok, state}
  end

  defp pickup_middle4(bs), do: pickup_middle(bs, 12, 4, 8)

  defp pickup_middle(bs, l, m, n) do
    <<_::size(l), mid_bs::size(m), _::size(n)>> = bs
    mid_bs
  end

自動起動するために以下を記述します。

lib/exineris3b/application.ex
  def children(_target) do
    [
      {Exineris3b.Worker, [:potentio_led]},
    ]
  end

これで先程同様に半固定抵抗の位置に応じて LED が強く光るプログラムができました。

平行に AND/OR ロジックも実行させてみる

ここまでのプログラムは前回まで同様に GenServer が司るプロセスで実現されています。ですので、前回のLチカは並行して動作させることが可能です(使ってる GPIO が全部別であることが必要です)。Worker モジュールを以下のように書き換えると、前回の はじめてNerves(4) 独立したプロセスでボタチカをする で作成した AND/OR のロジック回路と今回の PWM Lチカが同時に動きます。

lib/exineris3b/worker.ex
defmodule Exineris3b.Worker do
  use GenServer
  require Logger

  def start_link(state \\ []) do
    GenServer.start_link(__MODULE__, state, name: __MODULE__)
  end

  def init([:potentio_led] = state) do
    Logger.debug("#{__MODULE__}: PWM LED with potentiometer start...")
    GpioInOut.start_link(:button_a, 17, :input)
    GpioInOut.start_link(:button_b, 22, :input)
    GpioInOut.start_link(:led_x, 18, :output)
    GpioInOut.start_link(:led_y, 23, :output)
    GpioInOut.start_link(:led_pwm, 25, :output)
    SpiInOut.start_link(:spi0, "spidev0.0")

    Task.async(fn -> LogicLed.logical_and(:button_a, :button_b, :led_x) end)
    Task.async(fn -> LogicLed.logical_or(:button_a, :button_b, :led_y) end)
    Pwm.potentio_led(:spi0, <<0x06, 0x00, 0x00>>, &pickup_middle4/1, :led_pwm)
    {:ok, state}
  end

  defp pickup_middle4(bs), do: pickup_middle(bs, 12, 4, 8)

  defp pickup_middle(bs, l, m, n) do
    <<_::size(l), mid_bs::size(m), _::size(n)>> = bs
    mid_bs
  end
end

手間ひまかけてキッチリ綺麗に書いておくと、こういうとき素直に動いてくれて大変うれしいですね。

まとめ

今回は ラズパイ3B 上の Nerves で SPI インタフェースを使ってみました。これを用いて半固定抵抗の位置の検出をしました。さらに GPIO 出力による LED を PWM でアナログ風に強弱をつけて発光させてみました。

今回も GenServer を用いて I/O の操作を独立したプロセスで実行するようにしました。これにより、以前に作ったプログラムを平行に動作させることが可能であることを確認できました。

謝辞

今回は【第17回清流elixir勉強会】Elixirもくもく会&今年の方針を決める会【2020年初開催】でもくもくして作りました。このような機会を与えてくれた清流Elixirのみなさんに感謝します。

参考文献

9
4
3

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
9
4