【電子工作】SO1602AW(キャラクタディスプレイ)を使ってみる

今回はI2C接続の16x2有機ELキャラクターディスプレイSO1602AWWB-UC-WBを使ってみます。

秋月電子で1580(税込み)でした。
[有機ＥＬキャラクタディスプレイモジュール　１６×２行　白色 | 秋月電子通商](https://akizukidenshi.com/catalog/g/gP-08277/)


- [取扱説明書](https://akizukidenshi.com/download/ds/akizuki/so1602awwb-uc-wb-u_akizuki_manu.pdf)
- [データシート](http://www.kyohritsu.jp/eclib/OTHER/DATASHEET/LED/sp1602awwbuawbu.pdf)

# ■ ピン


<img width="1047" alt="スクリーンショット 2023-09-19 1.01.16.png (356.1 kB)" src="https://img.esa.io/uploads/production/attachments/5809/2023/09/19/19598/65710a91-aee6-4d03-8512-0db68c96c7c2.png">


1. VSS: ラズパイのGND端子に接続
電源グランド
1. VDD: ラズパイの3.3V端子に接続
電源
1. /CS: VSSに接続
1. SA0: GNDもしくはVDDに接続
I2Cスレーブアドレス。この端子がLOWなら`0x3C` 、 HIGHなら `0x3D` 
※ 今回はLOWに接続
1. 接続しない
1. 接続しない
1. SCL: ラズパイのSCL端子に接続
I2Cクロック入力
1. SDA in: ラズパイのSDAに接続
I2Cのデータ入力
1. SDA out: ラズパイのSDAに接続
I2Cのデータ出力
1. 以降は接続しない

# ■ データの送信

データの送信は「コントロールバイト」と「データバイト」の2つを送信します。
「コントロールバイト」は送信するデータのメタデータで、「データバイト」が送信するデータの本体です。

「コントロールバイト」には以下のフラグをもたせます。

- bit 7 `Co` : データバイトが複数の場合は1、1組のみの場合は0を設定
- bit6 `D/C#` : データバイトがコマンドの場合は `0` 、 データの場合は `1`

※ 基本的にデータバイトを複数送ることはしないので、コマンドの場合は `0x00` 、データ書込みの場合は `0x40` をコントロールバイトに指定することになります。

<img width="742" alt="スクリーンショット 2023-09-18 23.26.33.png (73.5 kB)" src="https://img.esa.io/uploads/production/attachments/5809/2023/09/18/19598/f1c73797-803f-410d-ada1-5d1078c74d35.png">


# ■ 利用できる文字

SO1602AWでは下記の文字が利用できます。
「データバイト」に文字に対応するアドレスを格納して、SO1602AWに送信することで液晶に表示されます。
※ 例えば `a` は `0b01000001` (0x41) 、`ア` は `0b10110001` (0xB1) となります。

<img width="980" alt="スクリーンショット 2023-09-19 0.35.45.png (223.8 kB)" src="https://img.esa.io/uploads/production/attachments/5809/2023/09/19/19598/08876e5a-bdfd-445c-a7c8-0f58837f3b4d.png">


# ■ 液晶表示 DDRAMアドレス

液晶の各セルにはアドレスが紐付けられています。 (これをDDRAMアドレスと呼ぶようです)
SO1602AWではカーソルという概念があり、このカーソルを任意のDDRAMアドレスに移動して表示したい文字を送信すると、指定したDDRAMアドレスに対応するセルに文字を表示することができます。
※ カーソルの移動は後述の「コマンド」で行います。
カーソルは1文字表示するごとに自動でインクリメントするので、表示ごとにいちいち設定し直す必要はありません。データを送信する前に表示開始位置(例えば `0x00` (1行目先頭))に設定しておけばOKです。

<img width="1233" alt="スクリーンショット 2023-09-19 0.36.39.png (46.8 kB)" src="https://img.esa.io/uploads/production/attachments/5809/2023/09/19/19598/82cea586-1e89-4f45-b3a6-ab171e4f3715.png">

# ■ よく使うコマンド

SO1602AWはコマンドを利用してデバイスの動作設定を行います。
この設定が微妙にややこしいので、少し解説します。

このコマンドには、 `IS` , `RE` , `SD`という3つのレジスタが存在し、このレジスタの状態で利用できるコマンドが異なります。
※ `IS` , `RE` , `SD` はいずれも `0` か `1` を値に取ります。

めんどくさいことに、これら3つのレジスタを変更するコマンドは1つにまとまっておらず、レジスタを変更するコマンドは、レジスタの状態によって実行できたりできなかったりします。
※ 例えば `SD` レジスタを設定するコマンドは `IS=0, RE=1, SD=0` の状態でないと利用できません、、、 (何だこの仕様)

 なので、コマンドは `IS` , `RE` , `SD` の状態ごとに解説していきます。

## IS=X, RE=X, SD=0  (Xは任意の値)

| コマンド名 | bit 7 | bit 6 | bit 5 | bit 4 | bit 3 | bit 2 | bit 1 | bit 0 | 説明 |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| Clear Display | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | すべてのDDRAMに `0x20` (スペース) を書き込み、<br> DDRAMアドレスを `0x00` (1行目先頭) に設定する |


## IS=X, RE=0, SD=0 

| コマンド名 | bit 7 | bit 6 | bit 5 | bit 4 | bit 3 | bit 2 | bit 1 | bit 0 | 説明 |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| Return Home | 0 | 0 | 0 | 0 | 0 | 0 | 1 | * | DDRAMアドレスを `0x00` (1行目先頭) に設定する |
| Entry Mode Set | 0 | 0 | 0 | 0 | 0 | 1 | I/D | S | データ書き込み時のカーソルの移動方向の設定 |
| Display ON/OFF | 0 | 0 | 0 | 0 | 0 | D | C | B | ディスプレイ・カーソル・ブリンク(カーソルの点滅)のON/OFFの設定 |
| Function Set | 0 | 0 | 1 | * | N | DH | RE(0) | IS | `RE` を `0` に、 `IS` を任意の値に設定できるコマンド。<br>その他設定表示設定も兼ねている。 |

※ `*` は任意の値

- Entry Mode Set 
    - `I/D`:
      (1) カーソルが右に移動しDDRAMアドレスがインクリメント (default)
      (0) カーソルが左に移動しDDRAMアドレスがデクリメント
    - `S`: ディスプレイ全体のシフト
      (1) I/D=1ならディスプレイ全体が右にシフト。I/D=0なら左にシフト。
      (0) ディスプレイのシフトを行わない (default)
- Display ON/OFF
    - `D`: ディスプレイ
      (1) ON
      (0) OFF (default)
    - `C`: カーソル表示
      (1) ON
      (0) OFF (default)
    - `B`: ブリンク(カーソルの点滅)表示
      (1) ON
      (0) OFF (default)
- Function Set
    - N: 表示行数制御
      (1) 2行表示
      (0) 1行表示
    - DH: 2行高フォント制御
      (1) 2行分の高さで1文字を表示 (DDRAMは0x00 - 0x27まで利用可能)
      (0) 通常表示
    - RE: 拡張命令用レジスタ
      (0) 固定
    - IS: 拡張命令用レジスタ
      (1) 利用する
      (0) 利用しない

## IS=0, RE=0, SD=0

| コマンド名 | bit 7 | bit 6 | bit 5 | bit 4 | bit 3 | bit 2 | bit 1 | bit 0 | 説明 |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| Cursor or Display Shift | 0 | 0 | 0 | 1 | S/C | R/L | * | * | カーソル(もしくは画面)をどうシフトするかを設定 |
- Cursor or Display Shift
     - `S/C` : 画面とカーソルどちらをシフトするかを選択
       (1) 画面をシフト (文字が画面を流れるように表示される)
       (0) カーソルをシフト 
     - `R/L` : 左右どちらにシフトするかを選択
       (1) 右にシフト
       (0) 左にシフト

## IS=0, RE=X, SD=0

| コマンド名 | bit 7 | bit 6 | bit 5 | bit 4 | bit 3 | bit 2 | bit 1 | bit 0 | 説明 |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| Set DDRAM Address | 1 | AC6 | AC5 | AC4 | AC3 | AC2 | AC1 | AC0 | カーソルを指定したDDRAMアドレスに移動します。 |
| Write Data | D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 | （DDRAM/CGRAM) にデータを書き込む |

## IS=0, RE=1, SD=0

| コマンド名 | bit 7 | bit 6 | bit 5 | bit 4 | bit 3 | bit 2 | bit 1 | bit 0 | 説明 |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| Function Set | 0 | 0 | 1 | * | N | DH | RE(1) | REV | `RE` を `1` に設定できるコマンド。<br>その他設定表示設定も兼ねている。 |
| OLED Characterization | 0 | 1 | 1 | 1 | 1 | 0 | 0 | SD | `SD` を任意の値に設定できるコマンド | 
| Double Height(4-line) / Display-dot shift | 0 | 0 | 0 | 1 | UD2 | UD1 | * | DH' | 選択した行だけディスプレイシフトを行う設定の有効化・無効化 |


- Function Set
    - `N`: 表示行数制御
      (1) 2行表示
      (0) 1行表示
    - `DH`: 2行高フォント制御
      (1) 2行分の高さで1文字を表示(5 x 16ドット) (DDRAMは0x00 - 0x27まで利用可能)
      (0) 通常表示 (5 x 8ドット)
    - `RE`: 拡張命令用レジスタ
      (1) 固定
    - `REV`: 表示ドットの反転設定
      (1) 反転する
      (0) 反転しない
- OLED Characterization (IS=0, RE=1, SD=0)
    - `SD`: 拡張命令用レジスタ
      (1) 利用する
      (0) 利用しない
- Double Height(4line)/Display-dot shift (IS=0, RE=1, SD=0)
      - `UD2,UD2`: あまり関係ないので `11` (default)
      - `DH'`: 選択した行だけディスプレイシフトを行う設定の有効・無効
        (1) 有効化 (ディスプレイシフト)
        (0) 無効化 (スクロール) (default)


## IS=1, RE=1, SD=0

| コマンド名 | bit 7 | bit 6 | bit 5 | bit 4 | bit 3 | bit 2 | bit 1 | bit 0 | 説明 |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| Shift / Scroll Enable | 0 | 0 | 0 | 1 | DS4/HS4 | DS3/HS3 | DS2/HS2 | DS1/HS1 | 選択した行(1 ~ 4)のみを選択的にディスプレイシフトさせる設定 |

- Shift / Scroll Enable
      - DS/HS: 
        - `DS4/HS4 ~ DS1/HS1` : `DH'=1` の場合は、行毎のディスプレイシフトを有効化。1111 (default)
        ※ `DH'=0` の場合は、行毎の水平スクロールを有効化。1111 (default)

# 実装

## 1. 固定表示してみる

画面をシフトさせずに固定の文字列を表示します

<img width="500" alt="disp1.jpg (529.3 kB)" src="https://img.esa.io/uploads/production/attachments/5809/2023/09/20/19598/e0e4c999-3215-4a48-9091-16dc2c24a7c5.jpg">

実際にテキストを表示させるコードは以下になります。基本的にはカーソルを動かして、テキストを書き込むだけですね。

```python
def main(pi, i2c_handler):
    """
    固定表示
    """
    write_command(pi, i2c_handler, 0b00101000)  # bit2 (1: Double height, 0: Single height) (Function Set)
    # IS=0, RE=0, SD=0

    l1 = b"Temp  Pres Hum"
    for char in l1:
        write_data(pi, i2c_handler, char)

    ddram_addr = 0b00100000  # 2行目の先頭 (0x20)
    write_command(pi, i2c_handler, 0b10000000 | ddram_addr)  # Set DDRAM RAM Address
    l2 = b"24.34 1012 50.35"
    for ch in l2:
        write_data(pi, i2c_handler, ch)
```

全文

```python
from time import sleep
import pigpio

def write_data(pi, i2c_handler, data: int):
    control_byte = 0b01000000  # コントロールバイト: データ書込みは bit6=1
    pi.i2c_write_device(i2c_handler, bytes([control_byte, data]))
    sleep(0.001)


def write_command(pi, i2c_handler, command: int):
    control_byte = 0b00000000  # コントロールバイト: コマンドは bit6=0
    pi.i2c_write_device(i2c_handler, bytes([control_byte, command]))
    sleep(0.05)


def init(pi, i2c_handler):
    """初期化処理"""
    # Clear Display (IS=X, RE=X, SD=0)
    #   全DDRAMに0x20を書き込み、DDRAMを0x00(1行目先頭)に設定
    #   0 0 0 0 0 0 0 1
    write_command(pi, i2c_handler, 0b00000001)

    # Return Home (IS=X, RE=0, SD=0)
    #   DDRAMを0x00(1行目先頭)に設定
    #   0 0 0 0 0 0 1 *
    write_command(pi, i2c_handler, 0b00000010)

    # Display ON/OFF Control
    #   ディスプレイ・カーソル・ブリンクのON/OFFを設定
    #   0 0 0 0 1 D C B
    #   - D: ディスプレイ (1)
    #   - C: カーソル表示 (0)
    #   - B: ブリンク表示 (0)
    write_command(pi, i2c_handler, 0b00001100)

    # Entry Mode Set (IS=0, RE=0, SD=0)
    #   データ書き込み時のカーソルの移動方向の設定
    #   0 0 0 0 0 1 I/D S
    #   - I/D: カーソルが右に移動しDDRAMアドレスがインクリメント (1)
    #   - S  : ディスプレイ全体のシフトさせない (0)
    write_command(pi, i2c_handler, 0b00000110)  # シフト設定をデフォルト値に

    # Function Set (IS=0, RE=1, SD=0)
    #   機能設定
    #   0 0 0 1 * N DH RE(0) REV
    #   - N: 2行表示 (1)
    #   - DH: 2行高表示を利用しない (0)
    #   - RE: (1)
    #   - REV: ディスプレイを反転表示しない (0)
    write_command(pi, i2c_handler, 0b00101010)  # IS=0, RE=1, SD=0

    # OLED Characterization (IS=0, RE=1, SD=0)
    #   0 1 1 1 1 0 0 SD
    #   - SD: (1)
    write_command(pi, i2c_handler, 0b01111001)  # IS=0, RE=1, SD=1

    # Set Contrast Control
    #   コントラスト設定。
    #   2バイトコマンドなので、このあとに輝度設定バイト(0x00 - 0xFF)を送信
    #   1 0 0 0 0 0 0 1
    write_command(pi, i2c_handler, 0b10000001)  # コントラストセット
    write_command(pi, i2c_handler, 0b11111111)  # 輝度 max

    # OLED Characterization (IS=0, RE=1, SD=0)
    #   0 1 1 1 1 0 0 SD
    #   - SD: (0)
    write_command(pi, i2c_handler, 0b01111000)  # IS=0, RE=1, SD=0

    # Function Set (IS=X, RE=0, SD=0)
    #   機能設定
    #   0 0 0 1 * N DH RE(0) IS
    #   - N: 2行表示 (1)
    #   - DH: 2行高表示を利用しない (0)
    #   - RE: (0)
    #   - IS: (0)
    write_command(pi, i2c_handler, 0b00101000)  # IS=0, RE=0, SD=0



def off(pi, i2c_handler):
    """終了処理"""
    write_command(pi, i2c_handler, 0b01111000)  # SD=1 (OLED Characterization)
    write_command(pi, i2c_handler, 0b00101000)  # RE=0, IS=0 (Function Set)
    write_command(pi, i2c_handler, 0b00000001)  # Clear Display
    write_command(pi, i2c_handler, 0b00000010)  # Return Home
    write_command(pi, i2c_handler, 0b00001000)  # Display, cursor, blink = OFF



def main(pi, i2c_handler):
    """
    固定表示
    """
    write_command(pi, i2c_handler, 0b00101000)  # bit2 (1: Double height, 0: Single height) (Function Set)
    # IS=0, RE=0, SD=0

    l1 = b"Temp  Pres Hum"
    for char in l1:
        write_data(pi, i2c_handler, char)

    ddram_addr = 0b00100000  # 2行目の先頭 (0x20)
    write_command(pi, i2c_handler, 0b10000000 | ddram_addr)  # Set DDRAM RAM Address
    l2 = b"24.34 1012 50.35"
    for ch in l2:
        write_data(pi, i2c_handler, ch)


if __name__ == "__main__":
    pi = pigpio.pi()
    if not pi.connected:
        raise Exception("pigpio connection faild...")

    i2c_bus = 1
    i2c_address = 0x3C  # SA0=L (SA0=Hの場合は0x3D)  (i2cdetect 1コマンドで確認)
    i2c_flags = 0x0
    i2c_handler = pi.i2c_open(i2c_bus, i2c_address)
    try:
        init(pi, i2c_handler)
        main(pi, i2c_handler)
        sleep(10)
    finally:
        off(pi, i2c_handler)
        sleep(1)
        pi.i2c_close(i2c_handler)
        pi.stop()
```

## 2. 固定文字列を画面シフトさせて表示

1行20文字以内の文字列を、画面をシフトさせて、右から左へ流れるように表示させます。

<img width="500" alt="disp2.gif (1.9 MB)" src="https://img.esa.io/uploads/production/attachments/5809/2023/09/20/19598/18263f03-5e07-4fbf-a9dd-711c0f37aaac.gif">

カーソルを動かして文字列を書き込むまでは1と同様です。
書込み後にディスプレイを左にシフトする処理をループします。
※ 表示できる文字は1行16文字ですが、20文字までデータとして保持しておけるようです。
※ main 以外は同じなので省略します。

```python

def main(pi, i2c_handler):
    """
    繰り返し表示
    """
    # 設定

    l1 = b"Temp  Pres Hum"  # 1行 20byteまで保持できる
    for char in l1:
        write_data(pi, i2c_handler, char)

    ddram_addr = 0b00100000  # 2行目の先頭 (0x20)
    write_command(pi, i2c_handler, 0b10000000 | ddram_addr)  # Set DDRAM RAM Address
    l2 = b"24.34 1012 50.35"
    for ch in l2:
        write_data(pi, i2c_handler, ch)  # 0xB1 = ア
    
    while (True):
        # Cursor or Display Shift (IS=0, RE=0, SD=0)
        #   カーソル(ディスプレイ)位置の移動設定
        #   0 0 0 1 S/C R/L * *
        #   - S/C: ディスプレイをシフト (1)
        #   - R/L: 左にシフト (0)
        write_command(pi, i2c_handler, 0b00011000)
        sleep(0.5)
```

## 3. 長い文字列を画面シフトさせながら表示 (2行同時シフト)

20文字に収まりきらない長い文字列を、画面をシフトさせて、右から左へ流れるように表示させます。

<img width="500" alt="disp3.gif (1.9 MB)" src="https://img.esa.io/uploads/production/attachments/5809/2023/09/20/19598/1eebb643-8622-4e86-8653-d0fc739d3203.gif">

`Entry Mode Set` でディスプレイのシフトを有効化し、「カーソルを先頭に移動」 -> 「20文字書き込む」を繰り返します。
表示文字列を20で割り切れる文字数に調整するときれいに表示できます。
※ main 以外は同じなので省略します。

```python
def main(pi, i2c_handler):
    """
    画面をシフトさせて長い文字列を表示
    画面のシフトは2行同時
    """
    l1 = b"Text:"
    for char in l1:
        write_data(pi, i2c_handler, char)
    sleep(1)

    # Entry Mode Set (IS=0, RE=0, SD=0)
    #   データ書き込み時のカーソルの移動方向の設定
    #   0 0 0 0 0 1 I/D S
    #   - I/D: カーソルが右に移動しDDRAMアドレスがインクリメント (1)
    #   - S  : ディスプレイ全体のシフトさせる (1)
    write_command(pi, i2c_handler, 0b00000111)


    l2 = b"If there was an error the number of bytes read will be less than zero (and will contain the error code)."
    l2 += (b" " * (len(l2) % 20))

    idx = 0
    while (True):
        for char in l2:
            if (idx % 20 == 0):  # 20文字ごとにDDRAMをリセット
                # Set DDRAM RAM Address (IS=0, RE=X, SD=0)
                #   DDRAMアドレスを設定
                #   1 N N N N N N N
                #   - N: アドレスを指定 1行目(0x00 - 0x0F) 2行目(0x20 - 0x2F)
                write_command(pi, i2c_handler, 0b10100000)  # 2行目の先頭 (0x20)

            write_data(pi, i2c_handler, char)
            sleep(0.1)
            idx += 1

```

## 4. 長い文字列を画面シフトさせながら表示 (1行だけシフト)

20文字に収まりきらない長い文字列を、画面をシフトさせて、右から左へ流れるように表示させます。
3との違いは2行目だけ画面をシフトさせているところです。

<img width="500" alt="disp4.gif (1.9 MB)" src="https://img.esa.io/uploads/production/attachments/5809/2023/09/20/19598/feb43b12-e722-441b-af07-235e48b84324.gif">

基本的には3と同じですが、  `Double Height(4line)/Display-dot shift` と `Shift / Scroll Enable` 設定を利用して、特定の行だけ画面のシフトを有効化しています。
※ main 以外は同じなので省略します。

```python
def main(pi, i2c_handler):
    """
    スクロール表示 (1行目固定)
    ※ 長文の表示で利用
    """
    l1 = b"Text:"
    for char in l1:
        write_data(pi, i2c_handler, char)
    sleep(1)

    # Entry Mode Set (IS=0, RE=0, SD=0)
    #   データ書き込み時のカーソルの移動方向の設定
    #   0 0 0 0 0 1 I/D S
    #   - I/D: カーソルが右に移動しDDRAMアドレスがインクリメント (1)
    #   - S  : ディスプレイ全体のシフトさせる (1)
    write_command(pi, i2c_handler, 0b00000111)

    write_command(pi, i2c_handler, 0b00101010)  # IS=0, RE=1, SD=0

    # Double Height(4line)/Display-dot shift (IS=0, RE=1, SD=0)
    #   0 0  0 1 UD2 UD1 * DH'
    #   - UD2,UD2: あまり関係ないので 11 (default)
    #   - DH': 選択した行だけディスプレイシフトを行う設定を有効化 (1)
    write_command(pi, i2c_handler, 0b00011101)

    write_command(pi, i2c_handler, 0b00101001)  # IS=1, RE=0, SD=0
    write_command(pi, i2c_handler, 0b00101010)  # IS=1, RE=1, SD=0

    # Shift / Scroll Enable (IS=1, RE=1, SD=0)
    #   0 0 0 1 DS4/HS4 DS3/HS3 DS2/HS2 DS1/HS1
    #   - DS/HS: 2行目のディスプレイシフトを有効化
    write_command(pi, i2c_handler, 0b00010010)  # display shift enable line2

    write_command(pi, i2c_handler, 0b00101000)  # IS=0, RE=0, SD=0

    try:
        l2 = b"If there was an error the number of bytes read will be less than zero (and will contain the error code)."
        l2 += (b" " * (len(l2) % 20))
        idx = 0
        while (True):
            for char in l2:
                if (idx % 20 == 0):  # 20文字ごとにDDRAMをリセット
                    # Set DDRAM RAM Address (IS=0, RE=X, SD=0)
                    #   DDRAMアドレスを設定
                    #   1 N N N N N N N
                    #   - N: アドレスを指定 1行目(0x00 - 0x0F) 2行目(0x20 - 0x2F)
                    write_command(pi, i2c_handler, 0b10100000)  # 2行目の先頭 (0x20)

                write_data(pi, i2c_handler, char)
                sleep(0.1)
                idx += 1
    finally:
        write_command(pi, i2c_handler, 0b00101010)  # IS=0, RE=1, SD=0

        # Double Height(4line)/Display-dot shift (IS=0, RE=1, SD=0)
        # 設定リセット
        write_command(pi, i2c_handler, 0b00011101)

        write_command(pi, i2c_handler, 0b00101001)  # IS=1, RE=0, SD=0
        write_command(pi, i2c_handler, 0b00101010)  # IS=1, RE=1, SD=0
        

        # Shift / Scroll Enable (IS=1, RE=1, SD=0)
        # 設定リセット
        write_command(pi, i2c_handler, 0b00011111)

        write_command(pi, i2c_handler, 0b00101000)  # IS=0, RE=0, SD=0
```

# 参考

https://nobita-rx7.hatenablog.com/entry/27983030