マルツ製キャラクタLCDモジュールをRaspberry Pi Zeroで遊ぶ


# はじめに

あちらこちらの箱に散在していたパーツ類を整理していたら、かなり前に購入した「マルツパーツ館 16文字x2行表示 キャラクタLCDモジュール」が出てきた。購入したことすら記憶に残っていなかったが、デュポンケーブルでRaspberry Pi Zeroに直接接続したところ、他に何もしなくても簡単に表示できたので、もう少し遊んでみようと思う。

つい先日、[aitendo製LCDの記事](https://qiita.com/nak435/items/08ee7fe9e230c06f9821)をアップしたが、今回の記事とは関係なくLCDつながりも単なる偶然。


# 1. マルツパーツ館

![取扱説明書](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283293/f0b3fd45-7a89-c14c-2fa0-e474ef652ebc.png)


「○にツ」のマルツパーツ館のロゴが入った取扱説明書と一緒に出てきた。ピンヘッダをハンダ付けしたものの、そのまま放置しいつしか記憶からも消え去ったと思われる。
「マルツパーツ館」をネットで検索すると、[2014年10月に現**マルツ**にリニューアルしたというブログ](http://marutsuparts.blog74.fc2.com/blog-entry-1078.html)がヒットした。今もロゴは「○にツ」ですね。
<div style=“text-align:center; margin-right:auto; margin-left:auto;”><img alt="MARUTSU" src="https://www.marutsu.co.jp/contents/shop/marutsu/favicon.ico">　　　　<img width="180" alt="MI2CLCD-01" src="https://www.marutsu.co.jp/contents/shop/marutsu/img/goods/020/137795/0000000200188607_2.jpg"></div>

「MI2CLCD-01」を検索すると、[現在のマルツの商品](https://www.marutsu.co.jp/pc/i/137795/)がヒットする。おそらく同じLCDモジュールだと思われる。


# 2. Raspberry Pi Zeroに接続

取説には、バスライン（SDA/SCL）は、
・ ハンダジャンパで基板上の2.2KΩのプルアップを有効化できる
・ 外付けする場合は、2.2〜4.7KΩの抵抗でプルアップすること
と、説明があり、もしかして、Raspberry Pi の1.8KΩのプルアップでも動作するかも？と半信半疑でデュポンケーブルでRaspberry Pi Zeroに接続したところ、すんなり表示できた。

使用したプログラムは、[aitendo製LCDの記事で使ったもの](https://qiita.com/nak435/items/08ee7fe9e230c06f9821#プログラムは既出のarduinoスケッチをpythonに書き直しただけだ)そのもの。ピンアサインは以下の通り。（GNDが交差しなければ、1・3・5・7・9と一直線。惜しい）

|LCDモジュール|Raspberry Pi<br>(ピン番号)|
|:-:|:-:|
|VDD|3V3  (1)|
|GND|GND  (9)|
|SDA|BCM2 (3)|
|SCL|BCM3 (5)|
|RST|BCM4 (7)|


# 3. サイズがRaspberry Pi Zeroと同じ

このLCDを見つけて手にしたときに、すぐに思ったのが「サイズがRaspberry Pi Zeroと同じじゃ？」。基板同士を重ねてみるとほぼ同じ。
マルツのサイトにサイズが載っていたので、並べるとホントに同じです。
![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283293/653515e9-c4af-de70-78ea-387694c24385.png)
このLCDモジュールができた頃は、Raspberry Pi Zeroはまだ存在していないと思うので、偶然なんでしょうね。専用HATを自作しようかと思うほどジャストサイズ。


# 4. 遊んでみる

# (a) アイコンを表示する

このLCDは、9種類のアイコンを表示できるので試してみた。

アイコンを表示する関数は以下の通り。念のため、adrは0〜15、bitは0〜31の範囲にマスクしている。

```python
def lcd_setIcon(adr, bit):
  lcd_cmd(0x39)
  lcd_cmd(0x40 | (adr & 0xF))
  lcd_data(bit & 0x1F)
  lcd_cmd(0x38)
```

この**adr**と**bit**で表示できる**アイコン**はどれ？、取説にもST7032のデータシートにも記載がない。「st7032 アイコン」で検索すると、ストロベリー・リナックスが販売している「[I2C低電圧キャラクタ液晶モジュール（１６ｘ２行）](https://strawberry-linux.com/catalog/items?code=27001)」の参考資料「[アプリケーションノート](https://strawberry-linux.com/pub/i2c_lcd-an001.pdf)」がヒットし詳しく載っていた。ストロベリー・リナックスのLCDはBO1602B、マルツのLCDのBO1602Dと、ほぼ一緒なんでしょうね。下表に引用させていただきました。
![icon adr/bit](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283293/b9bf9013-fa03-0c29-36c5-b93dd487f099.png)
例えば、バッテリー満タンのアイコンを表示したい場合は、adr=0xD、bit=0x1Eを指定します。消す場合は bit=0。

<img width="360" alt="IMG_9800.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283293/c1ce73ba-be40-8d07-f88d-23104dca37cb.png">

上の写真のようにちゃんと表示できました。昔のガラケーを思いださせる懐かしいアイコンです。
バッテリー充電中をイメージしたアイコンを設定してみた。↓それっほく見えますか？ 
![charging.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283293/5aaf3eb3-13cc-fc74-6acb-b56df8d4f942.gif)
本来ならタイマーイベントで処理するところを、サボって単純ループで実装しています。

```python:バッテリー充電中
while True:
  for bit in [0x02, 0x12, 0x0a, 0x06]:
    lcd_setIcon(0xd, bit)
    time.sleep(0.5)
    lcd_setIcon(0xd, 0)
    time.sleep(0.5)
```


# (b) CGRAMにビットパターンを定義して表示する

（動画の）上の写真でアイコンの行の下に1行空いて、2行目にTM、十字架、§・・・が表示されていますが、本当は、1行目に下表に示した左端の図形が表示されるはずなんですが・・・（ST7032データシートから引用）
![スクリーンショット 2022-04-18 21.45.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283293/c76b6cc3-c0ee-8e23-ae2f-44e9b33ccb14.png)
ですが、これは、OPR1=1＆OPR2=1 の場合で、何も表示されていないということは、このLCDはOPR1=0＆OPR2=0であることを意味していると思われ、この場合は、CGRAMに好きな図形を定義して表示させることができるようです。

1文字分のビットパターンは、横5ドッド x 縦8ライン。縦の8ライン目はカーソルの表示でも使われるので、0にしておくのが無難でしょう。つまり、5x7ドット図形。

CGRAMにビットパターンを設定する関数は以下の通り。adrは0x0〜0xF、patternは1バイトx8の配列で指定します。1バイトの下5ビットで1ラインのデータを指定し、それを8行分。

```python
def lcd_setCGRAM(adr, pattern):
  adr &= 0xF
  lcd_cmd(0x40 | (adr << 3))
  for char in pattern:
    lcd_data(char)
```

```python:使用例（文字コード=5に上向き矢印）
# ↑
lcd_setCGRAM(0x5, [0b00100, 0b01110, 0b10101, 0b00100, \
                   0b00100, 0b00100, 0b00100, 0b00000])
```

では、上表の0x0〜0xFの16個のビットパターンを自分で定義して表示させてみます。しかし、

```python
patterns = [[0b00010, 0b00101, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b00000], \
            [0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b10100, 0b01000, 0b00000], \
            [0b00000, 0b00000, 0b01010, 0b10101, 0b10101, 0b01010, 0b00000, 0b00000], \
            [0b11111, 0b10001, 0b10010, 0b10100, 0b11000, 0b10000, 0b00000, 0b00000], \
            [0b00001, 0b00001, 0b00101, 0b01001, 0b11111, 0b01000, 0b00100, 0b00000], \
            [0b00100, 0b01110, 0b10101, 0b00100, 0b00100, 0b00100, 0b00100, 0b00000], \
            [0b00100, 0b00100, 0b00100, 0b00100, 0b10101, 0b01110, 0b00100, 0b00000], \
            [0b00000, 0b00100, 0b00010, 0b11111, 0b00010, 0b00100, 0b00000, 0b00000], \
            [0b00000, 0b00100, 0b01000, 0b11111, 0b01000, 0b00100, 0b00000, 0b00000], \
            [0b11111, 0b10000, 0b10000, 0b10000, 0b10000, 0b10000, 0b10000, 0b00000], \
            [0b11111, 0b00001, 0b00001, 0b00001, 0b00001, 0b00001, 0b00001, 0b00000], \
            [0b10000, 0b10000, 0b10000, 0b10000, 0b10000, 0b10000, 0b11111, 0b00000], \
            [0b00001, 0b00001, 0b00001, 0b00001, 0b00001, 0b00001, 0b11111, 0b00000], \
            [0b00000, 0b00000, 0b00000, 0b01100, 0b01100, 0b00000, 0b00000, 0b00000], \
            [0b01110, 0b10011, 0b10101, 0b10011, 0b10101, 0b10101, 0b01110, 0b00000], \
            [0b01110, 0b11011, 0b10101, 0b10111, 0b10101, 0b11011, 0b01110, 0b00000]]
for adr in range(16):
  lcd_setCGRAM(adr, patterns[adr])
```

<img width="360" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283293/221567d8-78a5-11de-cd86-a59c6867fd88.png">

なぜか、後半の8文字分が繰り返し表示されるだけ？？

データシートをよく読むと、
![スクリーンショット 2022-04-18 23.12.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283293/0ce460ba-19ea-f16b-e7fd-bf3d46118f93.png)
OPR1=0＆OPR2=0は8文字のようです。前半の8文字の設定に変えたら下記写真の通り。16文字使えないのはちょっとガッカリ。OPR1=1＆OPR2=0も同じ8文字なので、こっちかもしれない。（16文字だと思っていたが、別なコントローラICの仕様だったようだ）

<img width="370" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283293/bab5db3e-52a1-9493-31e4-56e10405a48f.png">


# (c) CGRAMを動的に書き換えたら表示はどうなる？

前項の16個の図形のビットパターンのうち、前半の8個分をCGRAMに定義して、その文字を表示させた状態で後半の8個分をCGRAMに再定義すると、表示済みの文字はどうなるのか実験してみた。
想定される動作としては、次のどちらか。
1. CGRAMの再定義によって、表示済みの文字も書き変わる
1. 表示済みの文字は変わらず、新しく書いた文字が再定義後の図形になる

もし後者なら、使い道が広がるが、おそらく前者ではないかと予想する。
確認するプログラムは以下の通り。①最初に前半の8個分をCGRAMに定義、②1行目に文字コード0x0〜0x7の文字を出力。③後半の8個分をCGRAMに再定義。ここで1行目の表示に変化があるか ないか。④2行目に文字コード0x0〜0x7の文字を出力。1行目と2行目の文字に差が出るか 出ないか。

```python
patterns = [[0b00010, 0b00101, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b00000], \
            [0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b10100, 0b01000, 0b00000], \
            [0b00000, 0b00000, 0b01010, 0b10101, 0b10101, 0b01010, 0b00000, 0b00000], \
            [0b11111, 0b10001, 0b10010, 0b10100, 0b11000, 0b10000, 0b00000, 0b00000], \
            [0b00001, 0b00001, 0b00101, 0b01001, 0b11111, 0b01000, 0b00100, 0b00000], \
            [0b00100, 0b01110, 0b10101, 0b00100, 0b00100, 0b00100, 0b00100, 0b00000], \
            [0b00100, 0b00100, 0b00100, 0b00100, 0b10101, 0b01110, 0b00100, 0b00000], \
            [0b00000, 0b00100, 0b00010, 0b11111, 0b00010, 0b00100, 0b00000, 0b00000], \
            [0b00000, 0b00100, 0b01000, 0b11111, 0b01000, 0b00100, 0b00000, 0b00000], \
            [0b11111, 0b10000, 0b10000, 0b10000, 0b10000, 0b10000, 0b10000, 0b00000], \
            [0b11111, 0b00001, 0b00001, 0b00001, 0b00001, 0b00001, 0b00001, 0b00000], \
            [0b10000, 0b10000, 0b10000, 0b10000, 0b10000, 0b10000, 0b11111, 0b00000], \
            [0b00001, 0b00001, 0b00001, 0b00001, 0b00001, 0b00001, 0b11111, 0b00000], \
            [0b00000, 0b00000, 0b00000, 0b01100, 0b01100, 0b00000, 0b00000, 0b00000], \
            [0b01110, 0b10011, 0b10101, 0b10011, 0b10101, 0b10101, 0b01110, 0b00000], \
            [0b01110, 0b11011, 0b10101, 0b10111, 0b10101, 0b11011, 0b01110, 0b00000]]
# ①
for adr in range(8):
  lcd_setCGRAM(adr, patterns[adr])
# ②
lcd_cmd(0x01) # clear display
for char in range(8):
  lcd_data(char)
print("first 8 CGRAM")
time.sleep(5)

# ③
for adr in range(8, 16):
  lcd_setCGRAM(adr & 0x7, patterns[adr])
# ④
lcd_move(0x40) # move next line
for char in range(8):
  lcd_data(char)
print("last 8 CGRAM")
```

やはり予想した通りの↓結果であった。しかし、瞬時に描き変わるのでこれはこれで使い道があるかも。
<img width="360" alt="IMG_9815.JPG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283293/7ce2d357-167a-e705-551a-d9d946a6fdc5.jpeg">




# 5. 実用的なプログラムを書く

サイズ感がフィットしているので、**Raspberry Pi Zero WH**に常設し普段使いしようと思い、以下のプログラムを書くことにした。

1. （IPアドレス取得後に）自身のホスト名とIPアドレスを表示
1. バッテリーアイコンを使って、CPU使用率を表示（1秒間隔で更新）<br><img width="180" alt="CPU使用率" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283293/e454bead-4e65-54fc-07c6-81aab9ed7222.png">
1. 数分経過したら、ホスト名とIPアドレスの代わりに、現在時刻とCPU使用率を数字で表示
1. GPIOに取り付けたスイッチを押すことで、「ホスト名・IPアドレス」表示と「現在時刻・CPU使用率」表示を切り替え

<details><summary>ここを<b>クリック</B>すると、プログラム全体を<b>表示する</b></summary><div>

```python
import os
import psutil
import smbus
import time
import datetime
import RPi.GPIO as GPIO
import signal
import threading

BUSY = False
STOP = False

def handle_exit(signal_number, stack):
  global STOP
  STOP = True
  lcd_clear()
  lcd_puts("bye!")
  GPIO.cleanup()
  print("bye!")
  exit()

def ip_adr():
  cmd = "ifconfig | grep 192.168 "
  result = os.popen(cmd).read().split()
  return "" if len(result) == 0 else result[1]

def hostname():
  cmd = "hostname"
  result = os.popen(cmd).read().split()
  return result[0] + ".local"

LAST_CPU = 0.0
def cpu_usage():
  global LAST_CPU
  LAST_CPU = psutil.cpu_percent(interval=1)
  return LAST_CPU

def timer_scheduler():
  global STOP
  if not STOP:
    t = threading.Timer(1, timer_scheduler)
    t.start()
  cpu = cpu_usage()

i2c = smbus.SMBus(1) # 1 is bus number

signal.signal(signal.SIGTERM, handle_exit)
signal.signal(signal.SIGINT, handle_exit)

def delay(ms):
  time.sleep(ms / 1000)

def lcd_cmd(cmd):
  global BUSY
  BUSY = True
  i2c.write_byte_data(i2cadr, 0x00, cmd)
  delay(2)
  BUSY = False

def lcd_data(char):
  global BUSY
  BUSY = True
  i2c.write_byte_data(i2cadr, 0x40, char)
  BUSY = False
  delay(2)

def lcd_init():
  # reset
  delay(500)
  GPIO.setup(resetPin, GPIO.OUT)
  GPIO.output(resetPin, False)
  delay(10)
  GPIO.output(resetPin, True)
  delay(10)
  # LCD initialize
  delay(40)
  lcd_cmd(0x38) # function set
  lcd_cmd(0x39) # function set
  lcd_cmd(0x14) # interval osc
  lcd_cmd(0x70 | (contrast & 15)) # contrast low
  lcd_cmd(0x5c | (contrast >> 4 & 3)) # contrast high / icon / power
  lcd_cmd(0x6c) # follower control
  delay(300)
  lcd_cmd(0x38) # function set
  lcd_cmd(0x0c) # display on
  delay(2)    

def lcd_puts(str):
  charSet = []
  for char in str:
    charSet.append(ord(char))
  i2c.write_i2c_block_data(i2cadr, 0x40, charSet)

def lcd_setCGRAM(adr, pattern):
  adr &= 0xF
  lcd_cmd(0x40 | (adr << 3))
  for char in pattern:
    lcd_data(char)

def lcd_move2ndline():
  lcd_cmd(0x80 | 0x40)

def lcd_setIcon(adr, bit):
  lcd_cmd(0x39) # function set for Extended
  lcd_cmd(0x40 | (adr & 0xF))
  lcd_data(bit & 0x1F)
  lcd_cmd(0x38) # function set for NORMAL

def lcd_resetAllIcons():
  for adr in [0x0, 0x2, 0x4, 0x6, 0x7, 0x9, 0xB, 0xD, 0xF]:
    lcd_setIcon(adr, 0)

def lcd_clear():
  lcd_cmd(0x01) # clear display

def lcd_output(line1, line2=""):
  lcd_clear()
  lcd_puts(line1)
  if len(line2) == 0:
      return
  lcd_move2ndline()
  lcd_puts(line2)

def hostname_ipadr():
  lcd_output(hostname(), ip_adr())

def cpuUsage_clock():
  global LAST_CPU
  cpu = '{:>16}'.format(f'{LAST_CPU:.1f}%')
  now = datetime.datetime.now()
  now_str = now.strftime('/%d %H:%M:%S')
  if now.month >= 10:
    now_str = chr(now.month - 10) + now_str    
  else:
    now_str = str(now.month) + now_str
  lcd_output(cpu, '{:>16}'.format(now_str))

GPIO.setmode(GPIO.BOARD)
resetPin = 7
i2cadr = 0x3e
contrast = 40

lcd_init()

m10_11_12 = [[0b10010, 0b10101, 0b10101, 0b10101, 0b10101, 0b10010, 0b00000, 0b00000], \
             [0b10010, 0b10010, 0b10010, 0b10010, 0b10010, 0b10010, 0b00000, 0b00000], \
             [0b10010, 0b10101, 0b10001, 0b10010, 0b10100, 0b10111, 0b00000, 0b00000]]

for adr in range(3):
  lcd_setCGRAM(adr, m10_11_12[adr])

def setCPUusageIcon():
  global LAST_CPU
  cpu = LAST_CPU
  if cpu < 25.0:
    lcd_setIcon(0x0D, 0x02)
  elif cpu < 50.0:
    lcd_setIcon(0x0D, 0x12)
  elif cpu < 75.0:
    lcd_setIcon(0x0D, 0x1A)
  elif cpu < 95.0:
    lcd_setIcon(0x0D, 0x1E)
  else:
    for _ in range(2):
      lcd_setIcon(0x0D, 0x1E)
      delay(300)
      lcd_setIcon(0x0D, 0)
      delay(300)
    lcd_setIcon(0x0D, 0x1E)

t = threading.Thread(target = timer_scheduler)
t.start()

lcd_output("Please wait soon", " for network act")
time.sleep(1)
while len(ip_adr()) == 0:
  setCPUusageIcon()
  time.sleep(1)

hostname_ipadr()

for _ in range(180):
  setCPUusageIcon()
  time.sleep(1)


sw_pin = 37
GPIO.setup(sw_pin, GPIO.IN, pull_up_down=GPIO.PUD_UP)

CLOCK = True
def clock_or_hostname():
  global CLOCK
  if CLOCK:
    cpuUsage_clock()
  else:
    hostname_ipadr()

def handle_button(pin):
  global CLOCK, BUSY
  CLOCK = not CLOCK
  if not BUSY:
    clock_or_hostname()

GPIO.add_event_detect(sw_pin, GPIO.FALLING, handle_button, bouncetime=1000)

while True:
  clock_or_hostname()
  setCPUusageIcon()
  time.sleep(1)

```
</div></details>

適当な負荷を掛けるのが難しいので、すぐに100%になってしまうけど、ちゃんと表示できていそう。また、スイッチを押すと表示がちゃんと切り替わりますね。
<iframe height="270" width="480" src="https://www.youtube.com/embed/oh2v8AkE9AQ?rel=0" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

:::note warn
もし、上の埋め込み動画が表示されていない場合は、<b>[こちら](https://www.youtube.com/v/oh2v8AkE9AQ)</b>からどうぞ。
:::

これを`/etc/rc.local`から起動するように設定した。


# 6. マウントベースとスペーサを設計

Raspberry Pi ZeroとこのLCDを重ねて取り付けるためのベースとスペーサを設計した。ピンヘッダとデュポンケーブルが高さ方向の幅をとるので、HATのようにピッタとはいかずLCDが浮いた取り付けとなってしまうが仕方ない。
![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283293/085a8cb4-0897-8676-3a7d-a5c3eda29da0.png)

さっそく3DPRで出力して上の動画で使っていますが、真上から撮影しているので見えないですね。斜めからの写真を載せておきます。

<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283293/37a5e861-403c-e7e1-e1a5-b290a5d94919.png">

このLCDにバックライトがないのが残念!!

_基板にはバックライトLED~~用の制限抵抗や~~の給電方法を選択するためのジャンパランド、スルーホールまでも用意されている。
ストロベリー・リナックは同サイズの液晶にバックライト付きのLCDを販売しているので、それをマルツの基板に移し替える!?。てか、マルツもバックライト付きをラインナップすればいいのに・・・_

**2022.4.21訂正**
　マルツの基板にはバックライトLEDの制限抵抗（チップ抵抗）をハンダ付けするためのランドが用意されていますが、抵抗自体がハンダ付けされているわけではありません。


# おわりに

今回は**ST7032i**について調べLCD出力に関して勉強するよい機会となりました。また、普段余り使っていない**python**で色々な処理の書き方なども勉強になリました。
1000円足らずでこんなに遊べるなんて、ホント便利な時代になりました。みなさんに感謝です。
この記事も何かの参考になれば幸いです。

**以上**