これまでの記事で電流・電圧を測るためのハードウェアとその計測値を取得するI2Cデバイスへのアクセスの方法を説明しました。今回はプログラムでどんどん継続的に計測値を収集するプログラムを作成してみましょう。
計測対象が一つだけの場合の計測
まずはとにかく簡単に動くプログラムを目指します。このプログラムは計測対象が一つの場合にしか使えません。複数の計測対象がある場合については後に出てきます。
ハードウェア
ここでは毎度おなじみ 前々回説明した絶縁型回路 で試します。
上が被計測用の Raspberry Pi 4 で、下が計測用のBeagleBone Green です。間にあるのが Strawberry Linux 製の絶縁型計測基板 INA226PRCiso です。計測側と被計測側のどこで電気的に絶縁されてるかはわかりますね。
ソフトウェア
ようやくプログラムです。このプログラムは I2C バスを自分でオープンして reference を掴んでしまうので、使う I2C のバスを専有してしまいます。ですので同じバスにはいくつも I2C デバイスをつなぐことが出来ません。
宣言・定数
まずは宣言類から。
defmodule Ina226.Alone do
use GenServer
require Logger
@i2c_bus "i2c-2" # for BBB/BBG, use i2c-1 for RPi
@interval 1_000 # measurement interval (ms)
@curr_coef 0.1 # coefficient for raw current data to real mili ampere
@volt_coef 1.25 # coefficient for raw voltage data to real mili volt
プロセスにして走らせるのに GenServer
を使います。あと定期的な記録として Logger
を使います。きちんと DB にためたい方は ETS
や Mnesia
を使うと良いでしょう。
定数としては I2C のバス、記録をとる間隔、INA226PRCiso でとれる生データを SI 単位系に変換する係数を用意します。
プロセスの起動
ここではプロセスを作るのに GenServer
を用います。
最初の関数は Ina226.Alone.start_link(65)
のように I2C デバイスのスレーブアドレスを引数に持ちます。呼び出されると I2C バスをオープンして、その reference とスレーブアドレスをプロセスの状態として持ち回ります。
この Ina226.Alone.start_link/1
から呼び出される init/1
コールバックでは set_interval/1
関数でインタバルタイマの設定を行います。今回は 1000ms ごとに記録を取るようにします。
set_interval/1
は、引数で指定された時間待って、自分自身のプロセスに :wakeup
メッセージを送るように仕掛けます。このメッセージによって、後で出てくる handle_info/2
関数が起動されます。
def start_link(i2c_addr) do
Logger.debug("#{__MODULE__} start_link:")
{:ok, i2c_ref} = Circuits.I2C.open(@i2c_bus)
GenServer.start_link(__MODULE__, {i2c_ref, i2c_addr})
end
@impl GenServer
def init(state) do
set_interval(@interval)
{:ok, state}
end
defp set_interval(ms) do
Process.send_after(self(), :wakeup, ms) # every ms mili seconds
end
定期的にお仕事をする関数
上の set_interval/1
関数により、プロセスに :wakeup
というメッセージが送られてきます。これを受け取るのが handle_info/1
関数です。ここで計測と記録の動作をさせます。
実際の計測は後で出てくる get_all/2
関数で行い、その結果を「電流、電圧、電力」のタプルで受け取ります。単位は $mA, mV, mW$ です。その結果を Logger.info/1
でログに吐くようにします。必要な作業が終わったら再びタイマをセットして終わります。
@impl GenServer
def handle_info(:wakeup, state = {i2c_ref, i2c_addr}) do
{ma, mv, mw} = get_all(i2c_ref, i2c_addr)
Logger.info("#{__MODULE__}: #{ma} mA, #{mv} mV, #{mw} mW")
set_interval(@interval)
{:noreply, state}
end
計測プログラム
INA226 とのやりとりは read_curr/2
関数と read_volt/2
関数で行います。この具体的な動作については 前回記事 をご覧ください。
handle_info/2
から呼び出される get_all/2
関数は read_curr/2
と read_volt/2
からの電流値・電圧値から電力を算出して小数点以下をトリミングしてタプルとして返します。
defp get_all(ref, addr) do
ma = read_curr(ref, addr) |> round
mv = read_volt(ref, addr) |> round
mw = ma * mv /1000 |> Float.round(1)
{ma, mv, mw}
end
defp read_curr(ref, addr) do
{:ok, <<val::signed-integer-size(16)>>} = Circuits.I2C.write_read(ref, addr, <<1>>, 2)
val * @curr_coef
end
defp read_volt(ref, addr) do
{:ok, <<val::unsigned-integer-size(16)>>} = Circuits.I2C.write_read(ref, addr, <<2>>, 2)
val * @volt_coef
end
end
実行
以上をコンパイルしてターゲットの BeagleBone Green にダウンロードします。ターゲットはダウンロードが終わると自動的にリブートするので ssh
でログインします。
$ mix firmware # コンパイルしてファームウェアを構築する
$ mix upload # ないしは ./upload.sh あるいは mix.burn で SDカードに焼く
$ ssh nerves.local # リブートしたらログインする
ログインしたら、まずは結果を見るために RingLogger.attach/0
関数でロガーを有効にします。その後で Ina226.Alone.start_link/1
でプロセスを立ち上げます。引数は I2C デバイスのスレーブアドレスです。
iex(1)> RingLogger.attach
:ok
iex(2)> Ina226.Alone.start_link(65)
04:57:27.992 [debug] Elixir.Ina226.Alone start_link:
{:ok, #PID<0.1218.0>}
あとはおよそ1000ミリ秒ごとに計測値が出てきます。この例では、ラズパイ4の電源が最初はオフで途中でオンにした様子を示しています。
04:57:29.048 [info] Elixir.Ina226.Alone: 0 mA, 0 mV, 0.0 mW
04:57:30.103 [info] Elixir.Ina226.Alone: 0 mA, 0 mV, 0.0 mW
04:57:31.157 [info] Elixir.Ina226.Alone: 0 mA, 0 mV, 0.0 mW
04:57:32.211 [info] Elixir.Ina226.Alone: 372 mA, 4979 mV, 1852.2 mW
04:57:33.266 [info] Elixir.Ina226.Alone: 367 mA, 4989 mV, 1831.0 mW
04:57:34.320 [info] Elixir.Ina226.Alone: 367 mA, 4990 mV, 1831.3 mW
iex(3)> RingLogger.detach
:ok
iex(4)>
ログ表示を止めるには RingLogger.detach/0
関数を使います。表示が止まるだけでログには出力され続けています。
自動でプロセスを起動する
上ではプロセスを手動で開始しました。BeagleBone が起動すると同時にプロセスが立ち上がるようにするには Application
モジュールを使います。
すでに雛形が用意されているので、その中の children/1
関数を編集します。
def children(_target) do
[
# Children for all targets except host
# Starts a worker by calling: Ina226.Worker.start_link(arg)
{Ina226.Alone, 65}, # この行を追加。このカンマはあってもなくても可。
]
end
このように記述しておくと、ターゲットホストの Nerves が立ち上がると Ina226.Alone.start_link(65)
が自動的に起動され、ログに出力され続けます。
iex(1)> RingLogger.attach
:ok
04:11:40.135 [info] Elixir.Ina226.Alone: 0 mA, 0 mV, 0.0 mW
04:11:41.190 [info] Elixir.Ina226.Alone: 200 mA, 4968 mV, 993.6 mW
04:11:42.244 [info] Elixir.Ina226.Alone: 365 mA, 4990 mV, 1821.3 mW
04:11:43.299 [info] Elixir.Ina226.Alone: 366 mA, 4993 mV, 1827.4 mW
計測対象が2つ以上の場合のプログラム
さてここまでのプログラムでは計測対象が一つでした。これはプロセスが I2C をオープンしたときに reference を掴んでしまって、他のプロセスが同一の I2C バスにアクセスできなくなるからです。そこで I2C バスのアクセスまで考えたプログラムを作ってみます。
ハードウェア
計測対象を2つにします。ここではこれまでに出てきた2つの計測ボードを使います。若干回路が複雑ですが以下が接続されています。オレンジ色の破線が電気的に分離されている境界です。
- ラズパイ4
- 本体:電源はINA226PRCiso から供給されている
- INA226PRCiso:ラズパイ4の電源の計測をする
- 電源は電気的に分離されていて、ラズパイ0やI2C側とは独立になっている
- I2C は電気的に分離されていて、ラズパイ0側と接地が共通になっている
- ラズパイ0
- 本体:電源はINA226PRC から供給されている
- INA226PRC:ラズパイ0の電源の計測をする
- I2C は電気的に分離されおらずラズパイ0の電源と接地が共通である
- I2Cバス: Seeed Studio の Grove を使ってます
- ラズパイ0の I2C マスタ
- INA226PRC の I2Cスレーブ(アドレス64)
- INA226PRCiso の I2Cスレーブ(アドレス65)
I2Cバスへの複数センサの接続
同一の I2C バスには異なるスレーブアドレスのデバイスしか接続できません。今回は 64 と 65 です。INA226は電源ON時のピン接続でスレーブアドレスが決まります。 同一バスに接続する INA226 はこの機能を使って異なるスレーブアドレスにして下さい。INA226は最大で16センサを一つのバスにぶら下げることが可能です。
ソフトウェア
I2Cバス(マスタ)ごとのオープンと、I2Cスレーブへのアクセスがあり、全体でバス(マスタ)が一つであるのに対してスレーブが複数ぶら下がる構造になります。今回は以下のようにプロセスを構成しました。
- Workerモジュール: I2Cバスをオープンして各スレーブ用プロセスに reference を渡す
- InOutモジュール: 各スレーブに対するアクセスやロギングを処理する
以下、先ほどとは逆にブートしてから呼び出される順番に説明します。
ブート時の自動起動を行う Application モジュール
計測用のI2Cスレーブが複数になるので、スレーブアドレスをリストにして渡すように改変します。
def children(_target) do
[
# Children for all targets except host
# Starts a worker by calling: Ina226.Worker.start_link(arg)
{Ina226.Worker, [64, 65]}, # 複数のI2Cスレーブアドレスをリストで渡す
# {Ina226.Alone, 65},
]
end
I2C マスタに対してプロセスになる Worker モジュール
Application モジュールから Ina226.Worker.start_link/1
が呼び出されます。引数にスレーブアドレスリストが渡ってきます。このプロセスの大事なところは、I2Cバスをオープンするのはここだけであるというところです。
I2Cをオープンした後は、GenServer のコールバック関数 init/1
において、渡されたリストの要素である各スレーブアドレスに対して InOut モジュールをプロセスとして起動します。
プロセス名をバス名とスレーブアドレス番号から生成して区別できるようにします。GenServer の start_link/3
関数の name:
オプションで名前をつけるには、名前をATOMにする必要があるため bus <> "x" <> Integer.to_string(addr, 16) |> String.to_atom
という回りくどいことをしています。これによりスレーブアドレスが 64 の場合には :"i2c-1x40"
というプロセス名用の ATOM が pname にバインドされます。
defmodule Ina226.Worker do
use GenServer
require Logger
@i2c_bus "i2c-1" # i2c-1 for Rpi, i2c-2 for BBB/BBG
def start_link(i2c_addr_list) do
{:ok, ref} = Circuits.I2C.open(@i2c_bus)
Logger.debug("#{__MODULE__}, start_link:")
GenServer.start_link(__MODULE__, {@i2c_bus, i2c_addr_list, ref})
end
@impl GenServer
def init({bus, addr_list, ref}) do
Enum.each(addr_list, fn(addr) ->
Logger.debug("#{__MODULE__}, #{inspect({bus, ref, addr})}")
pname = bus <> "x" <> Integer.to_string(addr, 16) |> String.to_atom
Ina226.InOut.start_link(pname, ref, addr)
end)
{:ok, []}
end
end
I2C スレーブごとにプロセスになる InOut モジュール
これの中身は先程の Alone
モジュールとほとんど一緒です。ただし、I2Cスレーブごとにプロセスが出来ますのでプロセスを区別するためにプロセス名を受け取ります。また、I2Cバスをオープンする代わりに Worker プロセスから渡された reference を使います。さらに、ログに表示した際に複数のI2Cスレーブを区別できるように、ログにスレーブアドレスも出力するようにします。
defmodule Ina226.InOut do
use GenServer
require Logger
@interval 1_000 # measurement interval (ms)
@i2c_delay 25 # delay for waiting conversion (ms)
@curr_coef 0.1 # coefficient for raw current data to real mili ampere
@volt_coef 1.25 # coefficient for raw voltage data to real mili volt
def start_link(pname, i2c_ref, i2c_addr) do
Logger.debug("#{__MODULE__} start_link: #{inspect(pname)}")
GenServer.start_link(__MODULE__, {i2c_ref, i2c_addr}, name: pname)
end
@impl GenServer
def init(state) do
set_interval(@interval)
{:ok, state}
end
defp set_interval(ms) do
Process.send_after(self(), :wakeup, ms) # every ms mili seconds
end
@impl GenServer
def handle_info(:wakeup, state = {i2c_ref, i2c_addr}) do
{ma, mv, mw} = get_all(i2c_ref, i2c_addr)
Logger.info("#{__MODULE__}: I2C_addr #{i2c_addr}, #{ma} mA, #{mv} mV, #{mw} mW")
set_interval(@interval)
{:noreply, state}
end
defp get_all(ref, addr) do
ma = read_curr(ref, addr) |> round
mv = read_volt(ref, addr) |> round
mw = ma * mv /1000 |> Float.round(1)
{ma, mv, mw}
end
defp read_curr(ref, addr) do
{:ok, <<val::signed-integer-size(16)>>} = Circuits.I2C.write_read(ref, addr, <<1>>, 2)
val * @curr_coef
end
defp read_volt(ref, addr) do
{:ok, <<val::unsigned-integer-size(16)>>} = Circuits.I2C.write_read(ref, addr, <<2>>, 2)
val * @volt_coef
end
end
実行
SD に焼いてブートして Nerves にログインします。立ち上がると同時にプロセスが起動していますのでログへの記録は始まっています。RingLogger.attach/0
でログを見てみます。
以下では、スレーブアドレス 64 にラズパイ0の電源の電流・電圧・電力が、スレーブアドレス65にラズパイ4の電源の電流・電圧・電力がログに出力されているのがわかります。ラズパイ0は計測している(I2Cマスタの)ホストですので、記録が開始されると同時に電源供給がなされています。ラズパイ4は途中で電源を入れています。
iex(1)> RingLogger.attach
:ok
14:53:20.334 [info] Elixir.Ina226.InOut: I2C_addr 64, 124 mA, 5334 mV, 661.4 mW
14:53:20.485 [info] Elixir.Ina226.InOut: I2C_addr 65, 0 mA, 0 mV, 0.0 mW
14:53:21.388 [info] Elixir.Ina226.InOut: I2C_addr 64, 125 mA, 5343 mV, 667.9 mW
14:53:21.539 [info] Elixir.Ina226.InOut: I2C_addr 65, 0 mA, 0 mV, 0.0 mW
14:53:22.442 [info] Elixir.Ina226.InOut: I2C_addr 64, 125 mA, 5341 mV, 667.6 mW
14:53:22.593 [info] Elixir.Ina226.InOut: I2C_addr 65, 0 mA, 1 mV, 0.0 mW
14:53:23.496 [info] Elixir.Ina226.InOut: I2C_addr 64, 123 mA, 5343 mV, 657.2 mW
14:53:23.648 [info] Elixir.Ina226.InOut: I2C_addr 65, 398 mA, 4920 mV, 1958.2 mW
14:53:24.550 [info] Elixir.Ina226.InOut: I2C_addr 64, 125 mA, 5340 mV, 667.5 mW
14:53:24.702 [info] Elixir.Ina226.InOut: I2C_addr 65, 396 mA, 4926 mV, 1950.7 mW
14:53:25.604 [info] Elixir.Ina226.InOut: I2C_addr 64, 128 mA, 5343 mV, 683.9 mW
14:53:25.756 [info] Elixir.Ina226.InOut: I2C_addr 65, 396 mA, 4928 mV, 1951.5 mW
iex(2)> RingLogger.detach
:ok
iex(3)>
ラズパイ0とラズパイ4で結構消費電力が違いますね。あとACアダプタのDC5V出力も結構違いますね。
まとめ
直流の電流・電圧・電力計測を Nerves でやってみました。INA226PRCiso やその兄弟ボードを用意しておけば、電流・電圧・電力を計測したいなと思ったらすぐに計測が可能になりますので、普段から用意しておきたいです。
I2C の制御方法
GPIOのオープン・クローズは GPIO 番号ごとに行うので、GPIO ごとにプロセスを作ってそれの referece をプロセスで握っていればよかったので比較的見通しが良かったです。
これに対して、I2C の方はバス単位のオープン・クローズになるので、マスタを司るプロセスとスレーブを司るプロセスを別にする必要がありました。今回は INA226 しかアクセスしない前提でしたので若干簡略化しています。これがさらに別の I2C スレーブをバスにつなぐとなると、I2Cバスをオープンしたプロセスに名前をつけて、I2Cバスにアクセスしたいプロセスがその名前とスレーブ番号でアクセスするような構成を作る必要があります。
これは Elixir.Circuits
ライブラリ自体には該当する機能がありませんので、自分でさらに汎用的なライブラリを作って起きたいところです。
参考文献
- はじめてNerves(0) ElixirによるIoTフレームワークNervesがとにかく動くようになるためのリンク集
- 組込みに欠かせない Elixir でのビットの扱い方
- Elixir official document, Kernel.SpecialForms
- Elixir official document, Kernel.SpecialForms, ::/2
- Elixir official document, Kernel.SpecialForms, <<>>/1
- はじめてNerves(10) I2C デバイス INA226 で電流・電圧・電力を測ってみる (1/3)
- はじめてNerves(11) I2C デバイス INA226 で電流・電圧・電力を測ってみる (2/3)
- Texas Instruments INA226
- Strawberry Linux
- Strawberry Linux 製 INA226PRC
- Strawberry Linux 製 INA226PRCiso