ラズベリーパイとpythonでキャラクタディスプレイ（WinSTAR OLED 20x4）を制御する

# はじめに
![APC_0419.JPG](https://qiita-image-store.s3.amazonaws.com/0/149049/6ee92e20-f56f-12f5-4301-7d84604eab64.jpeg)
　二年前に引越ししたとき、自分の部屋にNTP同期＋室温度計のついた時計が欲しいと思って自作した時計が今回のネタ元です。  
　デジット（共立エレショップ）で売ってるWinSTAR社のOLEDディスプレイの起動時の挙動が安定せずおかしな表示をすることが多くて、出来るだけラズパイをリブートしないという運用でカバー（笑）してたのですが、再調査したところ現象が再発しなくなったので記録を残します。  



# 不安定なWinSTAR OLED
![IMG_7046.GIF](https://qiita-image-store.s3.amazonaws.com/0/149049/adfbe4b9-5be3-f635-ce61-d52b020f2cde.gif)
　ラズパイ1Bに4bitモードでGPIOにつないで、このWinSTAR OLEDに付いているコントローラ（W0100）のデータシートの通りに組んで何度かプログラムを実行すると多分こうなります（笑）  
　これはデバッグ時に何度も試したいときに致命的で、電源を落とさないと回復しません。  
　ラズパイの起動直後は発生しにくく、デバッグやなにかでプログラムをリロードすると大抵発生します。また、ラズパイ自体を再起動したり物理的にOLEDの接続をやりなおしても発生しやすくて、接続を断ってしばらく放置して接続しなおしてあげると発生しにくくなります。

## 対策その1
　WinSTAR OLEDに付いているコントローラ（W0100）のデータシートの最後に対策が書いています。  
　4bitモードの場合、4bitで5回0の転送を行うことで転送の不一致をリセット出来る（かなり要約）らしい。この対策はプログラムの再起動時に効果があると思います。  

## 対策その2
　探してたら[この方](http://www.mumpk.com/tech/2014/05/03/arduino-winstar-oled/)から[この方](http://www.a-phys.eng.osaka-cu.ac.jp/hosoda-g/site1/GLCD.html#sec5_tsuiho)へのページを見つけて試しました。VDDとVSSの間を330ohmから1Kohmで接続します。要するに+5Vを抵抗でGNDに落としてます。電源断後、OLED側にあるコンデンサに残る電流を放流することで、内部記憶を開放します。この対策は電源断・再入時に効果があると思います。  
　私は手持ちをゴソゴソして見つけた990ohmをOLED側のピン出しの部分にハンダ付けしました。  
![APC_0420.JPG](https://qiita-image-store.s3.amazonaws.com/0/149049/b77b6ad1-ca7f-bff6-bed4-a5fa16185fea.jpeg)

## 対策結果
　私はこの対策で治まりました。これで治らなければ[この方](http://www.a-phys.eng.osaka-cu.ac.jp/hosoda-g/site1/GLCD.html#sec5_tsuiho)の[添付ファイル](http://www.a-phys.eng.osaka-cu.ac.jp/hosoda-g/site1/WinStarOLED2.txt)を読んでセラミックコンデンサかESRコンデンサでノイズ対策を行う予定でしたが今のところそこまでは不要なようです。

# WinSTAR OLED 20x4表示モデルを使ったPythonによるサンプルコード
　Pythonでこのディスプレイを制御します。  
　ラズパイ側への接続はGPIOが並んでいて取り出しやすい部分を選んでます。  
　OLEDは4bitモード、日本語キャラクタ、カーソル無し設定で使います（ほかの設定は初期化部分を読んでください）。  
　W0100には4ラインモードがなく、実機で確認すると2ラインモードで1ライン目の制御電文で21文字目を指定すると実際の2ライン目に表示、2ライン目の制御電文で21文字目を指定すると4ライン目を表示します。  
　このOLEDの20x4のモデルにはR/Wの接続が無くOLEDからのレスポンスを受けられません。よって実機確認でのウェイトを入れています。私はタイミングチャートの読み方を覚えてないので実機が動いたらそれでいいやーレベルです。  

　あとは文字は文字コードを分解して4bitづつ送信する、というところが理解出来れば余裕です。  

```python
#!/usr/bin/python

import RPi.GPIO as GPIO
import time

# ***************************
# GPIO (WinSTAR OLED Display) settings
# The wiring for the WinSTAR OLED is as follows:
# 1 : GND
# 2 : 5V
# 3 : NC
# 4 : RS (Register Select)
# 5 : R/W (Read Write)       - TO GND (It doesn't wired by WinSTAR 20x4 model.)
# 6 : Enable or Strobe
# 7 : Data Bit 0             - NOT USED
# 8 : Data Bit 1             - NOT USED
# 9 : Data Bit 2             - NOT USED
# 10: Data Bit 3             - NOT USED
# 11: Data Bit 4
# 12: Data Bit 5
# 13: Data Bit 6
# 14: Data Bit 7
# 15: NC
# 16: NC

# Maximum characters per line
WRITE_LINE_WIDTH = 20

# OLED RAM Line Address
WRITE_LINE_1 = 0x80
WRITE_LINE_2 = 0xC0
WRITE_LINE_3 = 0x94
WRITE_LINE_4 = 0xD4

# Relationship wiring between GPIO pin and OLED pin.
# Pin position of OLED seen from RaspberryPi.
OLED_RS = 7
OLED_E = 8
OLED_D4 = 25
OLED_D5 = 24
OLED_D6 = 23
OLED_D7 = 18

# Constants
WRITE_MODE_CHR = True
WRITE_MODE_CMD = False


def initialize_gpio():
    GPIO.setwarnings(False)
    # Use BCM GPIO numbers
    GPIO.setmode(GPIO.BCM)
    GPIO.setup(OLED_E, GPIO.OUT)
    GPIO.setup(OLED_RS, GPIO.OUT)
    GPIO.setup(OLED_D4, GPIO.OUT)
    GPIO.setup(OLED_D5, GPIO.OUT)
    GPIO.setup(OLED_D6, GPIO.OUT)
    GPIO.setup(OLED_D7, GPIO.OUT)
    GPIO.output(OLED_E, False)
    GPIO.output(OLED_RS, False)
    GPIO.output(OLED_D4, False)
    GPIO.output(OLED_D5, False)
    GPIO.output(OLED_D6, False)
    GPIO.output(OLED_D7, False)



def initialize_winstar_oled():
    # ws0010 4bit mode initialized
    time.sleep(0.5)

    # Synchronization function for an 4-bit use
    set4_bit(0, 0, 0, 0, WRITE_MODE_CMD)
    set4_bit(0, 0, 0, 0, WRITE_MODE_CMD)
    set4_bit(0, 0, 0, 0, WRITE_MODE_CMD)
    set4_bit(0, 0, 0, 0, WRITE_MODE_CMD)
    set4_bit(0, 0, 0, 0, WRITE_MODE_CMD)

    # Function set
    set4_bit(0, 0, 1, 0, WRITE_MODE_CMD)  # start?
    set4_bit(0, 0, 1, 0, WRITE_MODE_CMD)  # 0 0 1 DL (4bit mode)
    set4_bit(1, 0, 0, 0, WRITE_MODE_CMD)  # N F FT1 FT0 (2line?, 5x8dot, english-japanese font)
    time.sleep(0.1)  # Instead of BUSY check

    # Displey mode
    set4_bit(0, 0, 0, 0, WRITE_MODE_CMD)  # 0 0 0 0
    set4_bit(1, 1, 0, 0, WRITE_MODE_CMD)  # 1 D C B (Disp on, Cursor off, Blink off)
    time.sleep(0.1)  # Instead of BUSY check

    # Displey clear
    set4_bit(0, 0, 0, 0, WRITE_MODE_CMD)  # 0 0 0 0
    set4_bit(0, 0, 0, 1, WRITE_MODE_CMD)  # 0 0 0 1
    time.sleep(0.1)  # Instead of BUSY check

    # Return home
    set4_bit(0, 0, 0, 0, WRITE_MODE_CMD)  # 0 0 0 0
    set4_bit(0, 0, 1, 0, WRITE_MODE_CMD)  # 0 0 1 0
    time.sleep(0.1)  # Instead of BUSY check

    # Entry mode set
    set4_bit(0, 0, 0, 0, WRITE_MODE_CMD)  # 0 0 0 0
    set4_bit(0, 1, 1, 0, WRITE_MODE_CMD)  # 0 1 I/D S (Inclemental, not shift)
    time.sleep(0.1)  # Instead of BUSY check


def set4_bit(d7, d6, d5, d4, mode):
    GPIO.output(OLED_RS, mode)

    GPIO.output(OLED_D4, False)
    GPIO.output(OLED_D5, False)
    GPIO.output(OLED_D6, False)
    GPIO.output(OLED_D7, False)

    GPIO.output(OLED_D4, d4)
    GPIO.output(OLED_D5, d5)
    GPIO.output(OLED_D6, d6)
    GPIO.output(OLED_D7, d7)

    time.sleep(0.00000006)
    GPIO.output(OLED_E, True)
    time.sleep(0.00000006)
    GPIO.output(OLED_E, False)
    time.sleep(0.00000006)

    # 注：四苦八苦してた頃のものなので統一するか削除可能かも…？
    if mode:
        time.sleep(0.00004)
    else:
        time.sleep(0.00152)

#**************************************:
# utility functions
def set_8bit(bits, mode):
    # High bits
    set4_bit(bits & 0x80 == 0x80, bits & 0x40 == 0x40,
             bits & 0x20 == 0x20, bits & 0x10 == 0x10, mode)
    # Low bits
    set4_bit(bits & 0x08 == 0x08, bits & 0x04 == 0x04,
             bits & 0x02 == 0x02, bits & 0x01 == 0x01, mode)


def write_line(message, line):
    message = message.ljust(WRITE_LINE_WIDTH, " ")
    set_8bit(line, WRITE_MODE_CMD)
    for i in range(WRITE_LINE_WIDTH):
        set_8bit(ord(message[i]), WRITE_MODE_CHR)


def clear_line(line):
    set_8bit(line, WRITE_MODE_CMD)
    for _ in range(WRITE_LINE_WIDTH):
        set_8bit(0, WRITE_MODE_CHR)


def fill_line(fill, line):
    set_8bit(line, WRITE_MODE_CMD)
    for _ in range(WRITE_LINE_WIDTH):
        set_8bit(fill, WRITE_MODE_CHR)


def clear_display():
    set_8bit(0x01, WRITE_MODE_CMD)

# init
initialize_gpio()
initialize_winstar_oled()


# sample
clear_display()
time.sleep(0.1)
write_line("Hello, OLED world!", WRITE_LINE_1)
time.sleep(0.1)
GPIO.cleanup()

```

冒頭の写真のアプリそのものは[github](https://github.com/marksard/oledclockservice)に公開しています。  

# 資料

* [WinSTAR page](https://www.winstar.com.tw/products/oled-module/oled-character-display/oled-20x4.html)
* [OLED datasheet](https://www.winstar.com.tw/uploads/files/6eb7552c6c9c656f690c8206dc964e19.pdf)
* [WS0010 datasheet](http://blog.digit-parts.com/pdf/ws0010.pdf)
* [デジットブログ: WinSTAR社OLEDディスプレイモジュール入荷！](http://blog.digit-parts.com/archives/51779891.html)
* [合資会社 満福のブログ: Winstar OLED WEH001602AをArduinoで使う](http://www.mumpk.com/tech/2014/05/03/arduino-winstar-oled/)
* [波動物理工学研究室: 種々の液晶表示器（有機EL：俗称OLEDも含む）制御のページ](http://www.a-phys.eng.osaka-cu.ac.jp/hosoda-g/site1/GLCD.html#sec5_tsuiho)