はじめてNerves(7) I2C で液晶表示する

これまで GPIO, SPI とやってきました。今回は I2C をやってみます。Nerves 1.5 (Elixir 1.9) を用いてます。

# ハードウェア
Circuits.I2C を用いて $I^2C$ デバイスを制御するモジュールを書きます。今回は I2C スレーブデバイスへの出力だけで表示ができます。ですので、I2Cスレーブデバイスからの読み込みはしません。[^I2Cslave]

[^I2Cslave]: 厳密にはエラーをチェックするのに ST7032 からの読み込みも可能です。今回はそこまで信頼性の必要な表示ではないのと、エラー出ても対して何もできないのでスルーすることにしました。

ハードウェアはラズパイ0W向けに秋月電子通商で売ってる [IoT学習HATキット](http://akizukidenshi.com/catalog/g/gK-14568/) を用います。ラズパイ0W向けですが、今回はラズパイ3Bで用います。

![K-14568](http://akizukidenshi.com/img/goods/C/K-14568.jpg)

これに AQM0802A-FLW-GBW という8文字×2行の液晶と、ST7032 という液晶ドライバチップが用いられています。この ST7032 は $I^2C$ で制御することが出来ます。

##I2Cが見えているかチェックする

まずは Elixir の Circuits.I2C を取り込みましょう。依存関係に以下を追加します。

```mix.exs
  defp deps do
    [
      ...
      {:circuits_i2c, "~> 0.1"},
      ...
    ]
  end
```

これで mix deps.get して mix firmware して SDカード焼いてリブートしてください。マシンに ssh して Nerves から I2C のインタフェースが見えているかチェックします。以下のようにバス名やスレーブデバイス番号が見えればひとまず大丈夫です。

```exs

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 を名前で呼べるようにしておきます。

```exs:lib/exineris3b/i2cinout.ex
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行で、これに決め打ちしたプログラムです。

```exs:lib/exineris3b/st7302lcd8x2.ex
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 から自動で起動しないようにしておきます。

```exs:lib/exineris3b/application.ex
  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!" と印字

```exs
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
```

出力した結果は以下のようになります。

![IMG_7290.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260320/0eef5daf-3423-14f5-9c55-9bbc788f2b7b.jpeg)

これだけの関数ですとちょっと不便ですので、多少工夫した表示用の関数を用意したいところです。

# ブート時に表示する
せっかくなのでちょっと使ってみます。ラズパイは単体ではロクに入出力がないので、ちゃんとブートしてきたのかどうかがにわかにはわかりません。ブート時に一言言うようにします。

以下はバックライトを点灯して文字列を表示するモジュールです。動作したらすぐに資源を解放します。ただ解放してもそのままの状態なので、このあと何かアクションするまでは光ったまま文字列が見えてる状態です。

```exs:lib/exineris3b/startnotice.ex
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 への引数で指定します。

```exs:lib/exineris3b/application.ex
  def children(_target) do
    [
      {Exineris3b.StartNotice, ["Exineris", "kochi.ex"]},
      {YourFavoriteModule, [args]}, ＃ 起動する本体プログラム
    ]
  end
```


# まとめ
今回は I2C を用いて液晶ディスプレイの表示をしてみました。デバイスに対する書き込みだけしか使ってませんので、いずれ I2C の読み込みもしてみます。

# 参考文献

- [Elixir Circuits - I2C](https://hexdocs.pm/circuits_i2c/readme.html#getting-started)
- [Circuits.I2C](https://hexdocs.pm/circuits_i2c/Circuits.I2C.html)
- [ST7032 Datasheet](http://akizukidenshi.com/download/ds/sitronix/st7032.pdf)
- [秋月電子通商 IoT学習HATキット (Rspberry Pi Zero WH用](http://akizukidenshi.com/catalog/g/gK-14568/)
- [I2C接続小型キャラクタLCDモジュール 8×2行（バックライト付き）](http://akizukidenshi.com/catalog/g/gP-09422/)
- [PICマイコン入門－実践](https://tool-lab.com/category/make/pic-practical-use/)
  - [第42回 液晶モジュール制御コマンド 〜動作設定編〜](https://tool-lab.com/pic-practice-42/)
  - [第43回 液晶モジュール制御コマンド 〜文字表示編〜](https://tool-lab.com/pic-practice-43/)
  - [第44回 液晶モジュール動作確認プログラム](https://tool-lab.com/pic-practice-44/)
- [はじめてNerves(0) ElixirによるIoTフレームワークNervesがとにかく動くようになるためのリンク集](https://qiita.com/kikuyuta/items/9dee85c32004c8e827e7)