4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-03-07

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

ハードウェア

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

ハードウェアはラズパイ0W向けに秋月電子通商で売ってる IoT学習HATキット を用います。ラズパイ0W向けですが、今回はラズパイ3Bで用います。

K-14568

これに 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 のインタフェースが見えているかチェックします。以下のようにバス名やスレーブデバイス番号が見えればひとまず大丈夫です。


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

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行で、これに決め打ちしたプログラムです。

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 から自動で起動しないようにしておきます。

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!" と印字
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

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

ブート時に表示する

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

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

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 への引数で指定します。

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

まとめ

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

参考文献

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

4
1
0

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?