Nerves Livebook で 7 セグメントディスプレイに文字を表示する

## はじめに

Nerves Livebook で Raspberry Pi 用スターター電子工作キットを楽しんでいます

[SunFounder Raspberry Pi 用のスターター電子工作キット](https://amzn.asia/d/04vHhGKQ)

これまでの記事

- [Nerves Livebook で Raspberry Pi 4 のLチカ](https://qiita.com/RyoWakabayashi/items/8119260ceeea462d6b7b)
- [Nerves Livebook + Delux で簡単Lチカ](https://qiita.com/RyoWakabayashi/items/281afab89249dbf68cd5)
- [MacBook 上の Livebook から Raspberry Pi 上の Livebook に接続してLチカする](https://qiita.com/RyoWakabayashi/private/844da38ff24efa3b923c)
- [Nerves Livebook から GPIO 拡張ボードの LED を点滅させる](https://qiita.com/RyoWakabayashi/private/bda56ee6987b30857266)
- [Nerves Livebook から RGB LED を制御する（ソフトPWMもやってみる）](https://qiita.com/RyoWakabayashi/private/143fda67a1798bd788b7)
- [Nerves Livebook から LED 棒グラフを制御する](https://qiita.com/RyoWakabayashi/items/a6e8b712388d68bdca44)
- [Nerves Livebook でボタンとLEDを連動させる](https://qiita.com/RyoWakabayashi/items/9bf9475d3c5d65cdfa4e)

この記事では 7 セグメントディスプレイに文字を表示します

実装したノートブックはこちら

https://github.com/RyoWakabayashi/elixir-learning/blob/main/livebooks/nerves_livebook/7_segment_display.livemd

## 7 セグメントディスプレイ

7本の棒で文字を表現するディスプレイです

![IMG_1576.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/482c5d89-ce83-3812-8b6d-eb129d720610.jpeg)

実は右下に「.」もあるので LED は 8 個ついています

GPIO のピンを単純に各 LED に繋げれば制御できますが、それだと GPIO のピンをどんどん消費してしまいます

そのため、今回はシフトレジスタとして 74HC595 を使って、3 つのピンだけで 8 つの LED を制御します

## シフトレジスタ

シフトレジスタはシリアル伝送とパラレル伝送を変換します

https://ac-blog.panasonic.co.jp/ja/control/connector/connector/general/20160825

今回の記事では、 74HC595 には順番に信号を送って（シリアル伝送）、そこから 7 セグメントディスプレイには並列に信号を送る（パラレル伝送）ように変換します

```
                    ┌----┐
 - 1 - 0 - 1 - 0 -> |    | - 0 ->
                    | 変 | - 1 ->
                    | 換 | - 0 ->
                    |    | - 1 ->
                    └----┘
```

## 回路の組み立て

オンライン説明書の通りに回路を組みます

https://docs.sunfounder.com/projects/davinci-kit/ja/latest/1.1.4_7-segment_display.html

## ノートブックの実装

Nerves Livebook を開き、以降のコードを実行していきます

### ピンを開く

まず、 GPIO の各ピンを出力用に開きます

```elixir
{:ok, sdi} = Circuits.GPIO.open("GPIO17", :output)
{:ok, rclk} = Circuits.GPIO.open("GPIO18", :output)
{:ok, srclk} = Circuits.GPIO.open("GPIO27", :output)
```

- `sdi`: シリアル伝送を受ける
- `rclk` : 1 にしたタイミングで `sdi` の信号を並列伝送用に設定する
- `srclk` : 1 にしたタイミングで設定されていた信号を並列伝送する

### 2進数をビットに分割する

今回実装する処理の流れは以下のようになっています

- 入力として `00111111` というような2進数を与える
- 入力を `0` `0` `1` `1` `1` `1` `1` `1` というように各桁に分割する
- 各桁（ビット）を 74HC595 に順次送る（`sdi` に各桁の値をセットした後で一瞬 `rclk` を `1` にする）
- 全桁送り終わったら一瞬 `srclk` を `1` にする
- 74HC595 から 7 セグメントディスプレイに各桁の値が並列に送られ、 `1` が送られた LED だけ光る

そのため、まずは 2 進数を一桁ずつに分割します

:::note info
もっと良い実装があるかもしれませんが、 Python 実装を翻訳したものになっています
:::

シフト演算やビットの論理積を行うため、 Bitwise をインポートしておきます

```elixir
import Bitwise
```

例えば以下のようなコードを実行します

```elixir
0b10100001 |> inspect(base: :binary)
```

:::note info
`0b` から始まる値は2進数として解釈されます

inspect(base: :binary) で2進数として表示します（何もしないと10進数に変換して表示されます）
:::


すると、結果は以下のようになります

```elixir
"0b10100001"
```

シフト演算子 `<<<` を使ってみます

```elixir
0b10100001 <<< 1 |> inspect(base: :binary)
```

結果は以下のように右端に `0` が追加され、元のビット列は左にシフトしました

```elixir
"0b101000010"
```

また、 `0b10100001 <<< 2 |> inspect(base: :binary)` とすれば `"0b1010000100"` となり、2桁左にシフトします

シフトしたあと `0b10000000` と論理積を取ってみます

```elixir
(0b10100001 <<< 0 &&& 0b10000000) |> IO.inspect(base: :binary)
(0b10100001 <<< 1 &&& 0b10000000) |> IO.inspect(base: :binary)
(0b10100001 <<< 2 &&& 0b10000000) |> IO.inspect(base: :binary)
(0b10100001 <<< 3 &&& 0b10000000) |> IO.inspect(base: :binary)
(0b10100001 <<< 4 &&& 0b10000000) |> IO.inspect(base: :binary)
(0b10100001 <<< 5 &&& 0b10000000) |> IO.inspect(base: :binary)
(0b10100001 <<< 6 &&& 0b10000000) |> IO.inspect(base: :binary)
(0b10100001 <<< 7 &&& 0b10000000) |> IO.inspect(base: :binary)
```

各出力は以下のようになります

```elixir
0b10000000
0b0
0b10000000
0b0
0b0
0b0
0b0
0b10000000
```

つまり、 `0b10100001` の各桁を左から順に `1` であれば `0b10000000` 、 `0` であれば `0b0` になるように変換しています

この辺りは @kikuyuta さんの記事を参考にしました

https://qiita.com/kikuyuta/items/e200a6208013f38333de

分かりやすく 128 で割ると以下のようになります

```elixir
data = 0b10100001

for bit <- 0..7 do
  div(data <<< bit &&& 0x80, 128)
end
```

実行結果

```elixir
[1, 0, 1, 0, 0, 0, 0, 1]
```

つまり、これによって `0b10100001` を `[1, 0, 1, 0, 0, 0, 0, 1]` に分割できます

### 74HC595 に並列出力を設定する

以下のような関数を定義します

```elixir
shift_out = fn data, sdi, rclk, srclk ->
  for bit <- 0..7 do
    bit = data <<< bit &&& 0x80
    Circuits.GPIO.write(sdi, bit)
    Circuits.GPIO.write(srclk, 1)
    Process.sleep(1)
    Circuits.GPIO.write(srclk, 0)
  end

  Circuits.GPIO.write(rclk, 1)
  Process.sleep(1)
  Circuits.GPIO.write(rclk, 0)
end
```

`data` の各ビットについて、 `sdi` に値をセットし、 `srclk` を一瞬 `1` にして並列伝送用の信号として設定します

全ビット分設定してから `rclk` を一瞬 `1` にして並列伝送を開始します

以下のコードを実行すると、 7 セグメントディスプレイに `0` が表示されます

```elixir
shift_out.(0b00111111, sdi, rclk, srclk)
```

`00111111` の各桁が「7セグメントディスプレイのどの LED を光らせるか」に対応しています

### 16 進数を表示する

16 進数（0〜9とA〜F）の各値を 7 セグメントディスプレイに表示します

`0b00111111` の2進数を `0x3F` のように16進数として表現し、それぞれを 0.5 秒間隔で切り替えます

```elixir
seg_code = [
  0x3F,
  0x06,
  0x5B,
  0x4F,
  0x66,
  0x6D,
  0x7D,
  0x07,
  0x7F,
  0x6F,
  0x77,
  0x7C,
  0x39,
  0x5E,
  0x79,
  0x71
]

seg_code
|> Enum.map(fn code ->
  shift_out.(code, sdi, rclk, srclk)
  Process.sleep(500)
end)
```

![hex.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/0ebc6a94-70fd-3185-90e9-755d0de3a0d9.gif)

### HELLO. を表示する

HELLO. の各文字を表示してみます

L が連続するため、文字の切り替わりが分かるように「何も光らない時間」を 0.2 秒挟むようにしました

```elixir
elixir_code = [
  0b01110110,
  0b01111001,
  0b00111000,
  0b00111000,
  0b00111111,
  0b10000000
]

elixir_code
|> Enum.map(fn code ->
  shift_out.(code, sdi, rclk, srclk)
  Process.sleep(500)
  shift_out.(0b00000000, sdi, rclk, srclk)
  Process.sleep(200)
end)
```

![hello.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/84caaa43-c2b2-7e5f-b879-65ded727550a.gif)

### ピンを閉じる

遊び終わったらピンを閉じておきます

```elixir
Circuits.GPIO.close(sdi)
Circuits.GPIO.close(rclk)
Circuits.GPIO.close(srclk)
```

## まとめ

Nerves Livebook から 7 セグメントディスプレイに文字を表示できました

シフトレジスタの使い方も分かったので、色々応用ができそうです