これまで GPIO, SPI とやってきました。今回は I2C をやってみます。Nerves 1.5 (Elixir 1.9) を用いてます。
ハードウェア
Circuits.I2C を用いて $I^2C$ デバイスを制御するモジュールを書きます。今回は I2C スレーブデバイスへの出力だけで表示ができます。ですので、I2Cスレーブデバイスからの読み込みはしません。1
ハードウェアはラズパイ0W向けに秋月電子通商で売ってる IoT学習HATキット を用います。ラズパイ0W向けですが、今回はラズパイ3Bで用います。
これに AQM0802A-FLW-GBW という8文字×2行の液晶と、ST7032 という液晶ドライバチップが用いられています。この ST7032 は $I^2C$ で制御することが出来ます。
##I2Cが見えているかチェックする
まずは Elixir の Circuits.I2C を取り込みましょう。依存関係に以下を追加します。
defp deps do
[
...
{:circuits_i2c, "~> 0.1"},
...
]
end
これで mix deps.get して mix firmware して SDカード焼いてリブートしてください。マシンに ssh して Nerves から I2C のインタフェースが見えているかチェックします。以下のようにバス名やスレーブデバイス番号が見えればひとまず大丈夫です。
iex(1)> Circuits.I2C.bus_names
["i2c-1"]
iex(2)> Circuits.I2C.detect_devices
Devices on I2C bus "i2c-1":
* 62
1 devices detected on 1 I2C buses
液晶ディスプレイドライバを I2C で制御する
I/O制御をできるだけ抽象化するために、例によって I2C 入出力を GenServer プロセスで制御して I2C を名前で呼べるようにしておきます。
defmodule I2cInOut do
@behaviour GenServer
require Circuits.I2C
require Logger
def start_link(pname, i2c_bus) do
Logger.debug("#{__MODULE__} start_link: #{inspect(pname)}, #{i2c_bus} ")
GenServer.start_link(__MODULE__, i2c_bus, name: pname)
end
def write(pname, addr, data, retries \\ []) do
GenServer.cast(pname, {:write, addr, data, retries})
end
def read(pname, addr, bytes, retries \\ []) do
GenServer.call(pname, {:read, addr, bytes, retries})
end
def stop(pname), do: GenServer.stop(pname)
@impl GenServer
def init(i2c_name) do
Logger.debug("#{__MODULE__} init_open: #{inspect(i2c_name)} ")
Circuits.I2C.open(i2c_name) # expected to return {:ok, i2cref}
end
@impl GenServer
def handle_cast({:write, addr, data, retries}, i2cref) do
Circuits.I2C.write(i2cref, addr, data, retries)
{:noreply, i2cref}
end
@impl GenServer
def handle_call({:read, addr, bytes, retries}, _from, i2cref) do
{:reply, {:ok, Circuits.I2C.read(i2cref, addr, bytes, retries)}, i2cref}
end
@impl GenServer
def terminate(reason, i2cref) do
Logger.debug("#{__MODULE__} terminate: #{inspect(reason)}")
Circuits.I2C.close(i2cref)
reason
end
end
液晶ドライバIC用の制御プログラムを作る
今回の液晶ディスプレイは ST7302 という LCD ドライバを用いますので、それの制御用のモジュールを書きます。液晶ディスプレイは 8文字×2行で、これに決め打ちしたプログラムです。
defmodule ST7302LCD8x2 do
@behaviour GenServer
require Logger
def start_link(lcd_name, i2c_bus, i2c_addr, gpio_no) do # LCD 初期化
GenServer.start_link(__MODULE__,
{lcd_name, i2c_bus, i2c_addr, gpio_no}, name: lcd_name)
end
def puts(lcd_name, string) do # 文字列表示
GenServer.cast(lcd_name, {:puts, string})
end
def puts(lcd_name, string, row, column) do # row行 column列 に文字列表示
GenServer.cast(lcd_name, {:puts, string, row, column})
end
def clear(lcd_name) do # 表示全クリア
GenServer.cast(lcd_name, :clear)
end
def backlight(lcd_name, on_off) do # バックライト on/off
GenServer.cast(lcd_name, {:backlight, on_off})
end
def stop(lcd_name), do: GenServer.stop(lcd_name)
@impl GenServer
def init({lcd_name, i2c_bus, i2c_addr, gpio_no}) do
i2c_name = to_string(lcd_name) <> ":i2c" |> String.to_atom
gpio_name = to_string(lcd_name) <> ":gpio" |> String.to_atom
I2cInOut.start_link(i2c_name, i2c_bus)
GpioInOut.start_link(gpio_name, gpio_no, :output)
init_lcd(i2c_name, i2c_addr)
{:ok, {i2c_name, i2c_addr, gpio_name}}
end
defp init_lcd(i2c_name, i2c_addr) do
send_command(i2c_name, i2c_addr, 0x38)
send_command(i2c_name, i2c_addr, 0x39)
send_command(i2c_name, i2c_addr, 0x14)
send_command(i2c_name, i2c_addr, 0x78)
send_command(i2c_name, i2c_addr, 0x5e)
send_command(i2c_name, i2c_addr, 0x6a)
Process.sleep(200)
send_command(i2c_name, i2c_addr, 0x38)
send_command(i2c_name, i2c_addr, 0x01)
# send_command(i2c_name, i2c_addr, 0x0c) # カーソルをブリンクしない
send_command(i2c_name, i2c_addr, 0x0f) # カーソルをブリンクする
end
defp send_command(i2c_name, i2c_addr, bytedata) do
I2cInOut.write(i2c_name, i2c_addr, <<0, bytedata>>)
Process.sleep(1)
end
@impl GenServer
def handle_cast({:backlight, on_off}, {i2c_name, i2c_addr, gpio_name}) do
Logger.debug("#{__MODULE__} backlight: #{inspect(gpio_name)}")
GpioInOut.write(gpio_name, on_off)
{:noreply, {i2c_name, i2c_addr, gpio_name}}
end
@impl GenServer
def handle_cast({:puts, string}, {i2c_name, i2c_addr, gpio_name}) do
Logger.debug("#{__MODULE__} backlight: #{inspect(i2c_name)}, #{string}")
to_charlist(string)
|> Enum.map(fn c -> send_char(i2c_name, i2c_addr, c) end)
{:noreply, {i2c_name, i2c_addr, gpio_name}}
end
@impl GenServer
def handle_cast({:puts, string, x, y}, {i2c_name, i2c_addr, gpio_name}) do
Logger.debug("#{__MODULE__} backlight: #{inspect(i2c_name)}, #{string}, #{x}, #{y}")
send_command(i2c_name, i2c_addr, 0x80 + y * 0x40 + x)
:binary.bin_to_list(string)
|> Enum.map(fn c -> send_char(i2c_name, i2c_addr, c) end)
{:noreply, {i2c_name, i2c_addr, gpio_name}}
end
@impl GenServer
def handle_cast(:clear, {i2c_name, i2c_addr, gpio_name}) do
send_command(i2c_name, i2c_addr, 0x01)
{:noreply, {i2c_name, i2c_addr, gpio_name}}
end
defp send_char(i2c_name, i2c_addr, bytedata) do
I2cInOut.write(i2c_name, i2c_addr, <<0x40, bytedata>>)
end
@impl GenServer
def terminate(reason, {i2c_name, _i2c_addr, gpio_name}) do
I2cInOut.stop(i2c_name)
GpioInOut.stop(gpio_name)
reason
end
end
初期化用の init_lcd/2
関数のパラメータは若干適当です。一応、動いていますが、使う方はそのままコピペしないで ST7032 のパラメータをちゃんと調べてパラメータを定めてください。
バイトストリームをバイトリストにする tips
最初文字列(ビットストリーム)をバイトリストにするのに String.to_charlist/1
を使ってました。これ、UTF-8 として印字可能でないと(String.valid?/1
で :true になるようなビットストリームでないと)変換できないので、ST7032 の持ってる特殊キャラクタ(半角カナなど)全ては扱えません。そこで :binary.bin_to_list/1
を使うように変更しています。
動作試験をする
手動で試験を行うために Application から自動で起動しないようにしておきます。
def children(_target) do
[
# {Exineris3b.ST7302LCD8x2, [:hat, "i2c-1", 62, 26]},
# {Exineris3b.Worker2hat, [:led_counter_hat]},
# {Exineris3b.Bootexineris, ["ricc@172.20.10.14"]},
# {Exineris3b.WorkerNaha, [:riccpiot]},
]
mix firmware して SD に焼いてラズパイをブートします。以下を試します。
- LCD の初期化
- バックライトをつける(これは GPIO の操作)
- 0行目に "Hello" と印字
- 1行目のやや右に "World!" と印字
iex(1)> ST7302LCD8x2.start_link(:hat, "i2c-1", 62, 26)
{:ok, #PID<0.1092.0>}
iex(2)> ST7302LCD8x2.backlight(:hat, :true)
:ok
iex(3)> ST7302LCD8x2.puts(:hat, "Hello")
:ok
iex(4)> ST7302LCD8x2.puts(:hat, "World!", 2, 1)
:ok
出力した結果は以下のようになります。
これだけの関数ですとちょっと不便ですので、多少工夫した表示用の関数を用意したいところです。
ブート時に表示する
せっかくなのでちょっと使ってみます。ラズパイは単体ではロクに入出力がないので、ちゃんとブートしてきたのかどうかがにわかにはわかりません。ブート時に一言言うようにします。
以下はバックライトを点灯して文字列を表示するモジュールです。動作したらすぐに資源を解放します。ただ解放してもそのままの状態なので、このあと何かアクションするまでは光ったまま文字列が見えてる状態です。
defmodule Exineris3b.StartNotice do
# @behaviour GenServer # does not work with Nerves correctly
use GenServer
require Logger
def start_link([str0, str1] = state) do
Logger.debug("#{__MODULE__}: #{str0}, #{str1}")
GenServer.start_link(__MODULE__, state, name: __MODULE__)
end
def init([str0, str1] = state) do
ST7302LCD8x2.start_link(:hat, "i2c-1", 62, 26)
ST7302LCD8x2.backlight(:hat, :true)
ST7302LCD8x2.puts(:hat, str0, 0, 0)
ST7302LCD8x2.puts(:hat, str1, 0, 1)
ST7302LCD8x2.stop(:hat)
{:ok, state}
end
end
文字列は application.ex の StartNotice への引数で指定します。
def children(_target) do
[
{Exineris3b.StartNotice, ["Exineris", "kochi.ex"]},
{YourFavoriteModule, [args]}, # 起動する本体プログラム
]
end
まとめ
今回は I2C を用いて液晶ディスプレイの表示をしてみました。デバイスに対する書き込みだけしか使ってませんので、いずれ I2C の読み込みもしてみます。
参考文献
- Elixir Circuits - I2C
- Circuits.I2C
- ST7032 Datasheet
- 秋月電子通商 IoT学習HATキット (Rspberry Pi Zero WH用
- I2C接続小型キャラクタLCDモジュール 8×2行(バックライト付き)
- PICマイコン入門-実践
- はじめてNerves(0) ElixirによるIoTフレームワークNervesがとにかく動くようになるためのリンク集
-
厳密にはエラーをチェックするのに ST7032 からの読み込みも可能です。今回はそこまで信頼性の必要な表示ではないのと、エラー出ても対して何もできないのでスルーすることにしました。 ↩