2021年9月27日月曜日

タイムコーダーを作る【3】RaspberryPi4+I2CネイティブOLEDを使う

前回、I2Cインターフェースで接続したOLEDで作成したタイムコーダーを今回はI2Cにネイティブ対応したOLEDで作り直します。I2Cインターフェースモジュールを使用した場合、通信速度が上がらず、計測間隔が長くなっていたのを改善します。また、LCD1602のようなキャラクターOLEDを使うことで、ビットマップ型のSDD1332よりも表示間隔が短くできるので、今回の用途にはキャラクタータイプの方が適しています。


前回の話:

タイムコーダーを作る【2】RaspberryPi4+I2C+OLEDで表示高速化

タイムコーダーを作る【1】 RaspberryPi4+I2Cで液晶LCD1602を使う時は 3.3V版を使う

のつづき

■前提とする環境

RaspberryPi4
RaspberryPi W/WH
Python3
SO1602AWWB-UC-WB-U <Sunlike Display Tech. Corp.> (秋月電子)

 

■I2CインターフェールモジュールからネイティブOLEDに変更した

I2Cインターフェースモジュールは、LCDに送るメッセージを4ビットに分割して、コントロール信号も別に操作する必要があり、さらにI2Cのアドレスも送るのでLCDメッセージ1バイトを送信するのになんと12バイトが必要となっていました。
これを、OLEDのICがI2Cを直接解釈できるならば、このオーバーヘッドが大きく改善できるはずです。

そして、見つけたのが…

SOC1602F <SHENZHEN SURENOO TECHNOLOGY CO.,LTD.> AliExpress

SOC1602F

SOC1602F-open



SO1602AWWB-UC-WB-U <Sunlike Display Tech. Corp.> 秋月電子

の2つでした。

SOC1602F 

SOC1602Fはピンの配置が短辺側についており、配線の取り回しの良さから一番の本命だったのですが、どうやっても動作せず、I2Cデバイス一覧でも出てきたり出てこなくなったり、メッセージを送ってもウントもスントも言わず、2つ購入していたのにも関わらずどちらも同じ症状。ネットで解決策を探るも解決策はおろか動作実績さえ見つけられない。プリント基板の配線も多層基板で配線を追いきれず、お手上げ状態となり諦めました。こちらはまた日を改めて再度試してみたいところです。

SO1602AWWB-UC-WB-U

 今回、秋月電子さんのSO1602AWWB-UC-WB-Uをためしたところ、一発で動作したのでこちらを利用することにしました。回路的にはどちらのモジュールもほとんど同じで、OLEDモジュール(画面)とそのコントローラーは同じUS2066を使用しています。実は基板上にはほとんど回路が有りません。                                                    
 SOC1602Fもエポキシ基板上の回路も大したものは乗っていないので実は簡単なことで改善できそうなんですが私の実力では解明できませんでした。
RaspberyPiで動作出来た方、是非アドバイスをお願いいたします。

今回は、キャラクターOLEDでネイティブでI2Cを解釈できる、SO1602AWWB-UC-WB-Uを使用して作り直してみます。

■制作・回路

秋月さんのリファレンス回路をもとに作成します。

注意点は、RaspberryPiでOLEDを使用する時外部電電源が必要になります。RaspberryPi上の3.3V出力は絶対定格が50mAと低く、全点灯したときのOLEDの最大消費電流が80mAなので、定格を超えてしまいます。これが、LCD(SC1602BBWB-XA-GB-G)の消費電流がパネル3.0mA、バックライト20mA前後で有ることを考えると結構大きな電流を消費しているのがわかります。
今回、不足分を補うため外部電源として手元にあった3.3VなACアダプターを利用し、ブレッドボードに供給しています。


■ソースコード

I2Cネイティブで動作するUS2066では、今までのプログラムが大幅に簡略化出来ます。
また、視認性や速度が重要となるならUS2066を使用したキャラクターOLEDモジュールは良い選択肢になります。
ソースコードはライブラリとそれを呼び出すプログラムとの2つで構成されます。
今回時計プログラムを掲載していますが、他の記事のGNSS/GPSを使用した内部時計の同期を使えば電波時計になります。

時計プログラム 
[us2066clock.py]
#! /usr/bin/env python3

import smbus, time
import datetime
import us2066

#get us2006_lib into oled(Instance)
oled = us2066.Us2066(0x3c)
oled.brightness(255)

#write strings data(1st row)
now = datetime.datetime.now()

#write strings data(2nd row)
micro = 1000000
pos_micro = 0
while True:
    now = datetime.datetime.now()

    if micro > now.microsecond:
        oled.pos(0, 0)
        current = now.strftime("%y%m%d %H:%M:%S")
        oled.print(current)  # date times
        oled.pos(pos_micro, 1)

    micro = now.microsecond
    if pos_micro == 0:
        oled.pos(pos_micro, 1)

    str_milli = str(micro)[0:3]
    oled.print(str_milli)  #write strings
    pos_micro = (pos_micro + 3)
    if pos_micro >= 15:
        pos_micro = 0
    time.sleep(0.00)


#モニタの表示アドレス(2行16列の場合)
#     [1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16]
# 1行目:[0x00][0x01][0x02][0x03]・・・[0x0F]
# 2行目:[0x20][0x21][0x22][0x23]・・・[0x2F]



ライブラリ本体
同じフォルダに保存してご利用ください。
[us2066.py]
# -*- coding: utf-8 -*-
#I2C Interface library
#  For US2066 OLED/PLED Segment/Common Driver with Controller
#
#SOC series
# SUNLIKE SO1602A,SO2002A,SO2004A,
# SO1602AWWB-UC-WB-U
#  not work: Surenoo SOC1602F

import smbus
import time
name = 'us2066'

class Us2066:
    i2c = None
    address = None

    def __init__(self, address):
        print("us2066.__init__()", address)
        self.i2c = smbus.SMBus(1)
        self.set_address(address)
        self.init()
        self.off()
        self.clear()
        self.home()
        self.on()

    def init(self):
        #LCD Intialize
        self.command(0x2A)         
        self.command(0x71)        
        self.command(0x00)          #Disable Internal Regulator
        self.command(0x28)          #

    def set_address(self, address):
        print("set_address()", address)
        self.address = address

    def wait_busy(self):
        busy = 0x80
        while busy & 0x80:
            busy = self.i2c.read_byte_data(self.address, 0x00)
            time.sleep(0)

    def command(self, code):
        #print("command()", code)
        self.i2c.write_byte_data(self.address, 0x00, code)
        #time.sleep(0.002)

    def print(self, message):
       #print("write()", message)
       mojilist = []
       for char in message:
            mojilist += self.CHAR_TABLE[char]
            #time.sleep(0.00002)
       self.wait_busy()
       self.i2c.write_i2c_block_data(self.address, 0x40, mojilist)

    def off(self):
        self.wait_busy()
        self.command(0x08)          #Display Off

    def on(self):
        self.wait_busy()
        self.command(0x0c)          # Diplay ON = 1100b

    def clear(self):
        self.wait_busy()
        self.command(0x01)          #Clear =1b

    def home(self):
        self.wait_busy()
        self.command(0x02)          #Return home =2b

    def pos(self, xxx, yyy):
        self.wait_busy()
        self.command(0x80 + 0x20 * yyy + xxx)

    def brightness(self,  bright):
        self.wait_busy()
        self.command(0x80)        # set RE=1
        self.command(0x2A)

        self.command(0x80)        # set SD=1
        self.command(0x79)

        self.command(0X81)        # set Brightness
        self.command(bright)

        self.command(0x80)        # set SD=0
        self.command(0x78)

        self.command(0x80)        # set RE=0
        self.command(0x28)

    CHAR_TABLE = {
        u'†': [0x11],
        u'§': [0x12],
        u'¶': [0x13],
        u'Γ': [0x14],
        u'Δ': [0x15],
        u'θ': [0x16],
        u'Λ': [0x17],
        u'Ξ': [0x18],
        u'Π': [0x19],
        u'Σ': [0x1a],
        u'Φ': [0x1c],
        u'Ψ': [0x1d],
        u'Ω': [0x1e],
        u'α': [0x1f],

        u' ': [0x20],
        u'!': [0x21],
        u'"': [0x22],
        u'#': [0x23],
        u'$': [0x24],
        u'%': [0x25],
        u'&': [0x26],
        u"'": [0x27],
        u'(': [0x28],
        u')': [0x29],
        u'*': [0x2a],
        u'+': [0x2b],
        u',': [0x2c],
        u'-': [0x2d],
        u'.': [0x2e],
        u'/': [0x2f],

        u'0': [0x30],
        u'1': [0x31],
        u'2': [0x32],
        u'3': [0x33],
        u'4': [0x34],
        u'5': [0x35],
        u'6': [0x36],
        u'7': [0x37],
        u'8': [0x38],
        u'9': [0x39],
        u':': [0x3a],
        u';': [0x3b],
        u'<': [0x3c],
        u'=': [0x3d],
        u'>': [0x3e],
        u'?': [0x3f],

        u'@': [0x40],
        u'A': [0x41],
        u'B': [0x42],
        u'C': [0x43],
        u'D': [0x44],
        u'E': [0x45],
        u'F': [0x46],
        u'G': [0x47],
        u'H': [0x48],
        u'I': [0x49],
        u'J': [0x4a],
        u'K': [0x4b],
        u'L': [0x4c],
        u'M': [0x4d],
        u'N': [0x4e],
        u'O': [0x4f],

        u'P': [0x50],
        u'Q': [0x51],
        u'R': [0x52],
        u'S': [0x53],
        u'T': [0x54],
        u'U': [0x55],
        u'V': [0x56],
        u'W': [0x57],
        u'X': [0x58],
        u'Y': [0x59],
        u'Z': [0x5a],
        u'[': [0x5b],
        u'¥': [0x5c],
        u']': [0x5d],
        u'^': [0x5e],
        u'_': [0x5f],

        u'`': [0x60],
        u'a': [0x61],
        u'b': [0x62],
        u'c': [0x63],
        u'd': [0x64],
        u'e': [0x65],
        u'f': [0x66],
        u'g': [0x67],
        u'h': [0x68],
        u'i': [0x69],
        u'j': [0x6a],
        u'k': [0x6b],
        u'l': [0x6c],
        u'm': [0x6d],
        u'n': [0x6e],
        u'o': [0x6f],

        u'p': [0x70],
        u'q': [0x71],
        u'r': [0x72],
        u's': [0x73],
        u't': [0x74],
        u'u': [0x75],
        u'v': [0x76],
        u'w': [0x77],
        u'x': [0x78],
        u'y': [0x79],
        u'z': [0x7a],
        u'{': [0x7b],
        u'|': [0x7c],
        u'}': [0x7d],
        u'→': [0x7e],
        u'←': [0x7f],

        u'。': [0xa1],
        u'「': [0xa2],
        u'」': [0xa3],
        u'、': [0xa4],
        u'・': [0xa5],
        u'ヲ': [0xa6],
        u"ァ": [0xa7],
        u'ィ': [0xa8],
        u'ゥ': [0xa9],
        u'ェ': [0xaa],
        u'ォ': [0xab],
        u'ャ': [0xac],
        u'ュ': [0xad],
        u'ョ': [0xae],
        u'ッ': [0xaf],

        u'ー': [0xb0],
        u'ア': [0xb1],
        u'イ': [0xb2],
        u'ウ': [0xb3],
        u'エ': [0xb4],
        u'オ': [0xb5],
        u'カ': [0xb6],
        u'キ': [0xb7],
        u'ク': [0xb8],
        u'ケ': [0xb9],
        u'コ': [0xba],
        u'サ': [0xbb],
        u'シ': [0xbc],
        u'ス': [0xbd],
        u'セ': [0xbe],
        u'ソ': [0xbf],

        u'タ': [0xc0],
        u'チ': [0xc1],
        u'ツ': [0xc2],
        u'テ': [0xc3],
        u'ト': [0xc4],
        u'ナ': [0xc5],
        u'ニ': [0xc6],
        u'ヌ': [0xc7],
        u'ネ': [0xc8],
        u'ノ': [0xc9],
        u'ハ': [0xca],
        u'ヒ': [0xcb],
        u'フ': [0xcc],
        u'ヘ': [0xcd],
        u'ホ': [0xce],
        u'マ': [0xcf],

        u'ミ': [0xd0],
        u'ム': [0xd1],
        u'メ': [0xd2],
        u'モ': [0xd3],
        u'ヤ': [0xd4],
        u'ユ': [0xd5],
        u'ヨ': [0xd6],
        u'ラ': [0xd7],
        u'リ': [0xd8],
        u'ル': [0xd9],
        u'レ': [0xda],
        u'ロ': [0xdb],
        u'ワ': [0xdc],
        u'ン': [0xdd],
        u'゛': [0xde],
        u'゜': [0xdf],

        u''': [0xf0],
        u'"': [0xf1],
        u'°': [0xf2],
        u'×': [0xf7],
        u'÷': [0xf8],
        u'≧': [0xf9],
        u'≦': [0xfa],
        u'≪': [0xfb],
        u'≫': [0xfc],
        u'≠': [0xfd],
        u'√': [0xfe],
        u' ̄': [0xff],

        u'ガ': [0xb6, 0xde],
        u'ギ': [0xb7, 0xde],
        u'グ': [0xb8, 0xde],
        u'ゲ': [0xb9, 0xde],
        u'ゴ': [0xba, 0xde],
        u'ザ': [0xbb, 0xde],
        u'ジ': [0xbc, 0xde],
        u'ズ': [0xbd, 0xde],
        u'ゼ': [0xbe, 0xde],
        u'ゾ': [0xbf, 0xde],
        u'ダ': [0xc0, 0xde],
        u'ヂ': [0xc1, 0xde],
        u'ヅ': [0xc2, 0xde],
        u'デ': [0xc3, 0xde],
        u'ド': [0xc4, 0xde],
        u'バ': [0xca, 0xde],
        u'ビ': [0xcb, 0xde],
        u'ブ': [0xcc, 0xde],
        u'ベ': [0xcd, 0xde],
        u'ボ': [0xce, 0xde],
        u'パ': [0xca, 0xdf],
        u'ピ': [0xcb, 0xdf],
        u'プ': [0xcc, 0xdf],
        u'ペ': [0xcd, 0xdf],
        u'ポ': [0xce, 0xdf],
    }



アメグラ2号のブログさんを参考にさせていただきました

■結果

やはりI2Cにネイティブで対応したUS2066は高速で、ディスプレイの書き換えが1msec弱となりました。しかし、1msecでは逆にシャッター速度が遅いときに読めなくなります。表題の写真に掲載しているものは、視認性を上げるために遅くさせています。4msecあたりの間隔で書き換えを行っているのが見て取れるかと思います。
上記の写真では、下段の数字が3桁ごとのmsec時間を表示しています。
わざわざ3桁づつ横に出力しているのは、カメラのシャッターの開放時間と書き換え時間が重なると文字が重なり読めなくなるためです。
この写真では、2段め左から3桁づつ読み
324
3**
312
316
320
と読めます。
読みにくかった3**は4msec毎に書き換えされていることから
328
あたりと推測できます。
なので現在時刻は。
2021/9/24 20:04:56.328-331
と推測できます。

かなり精度よくOLEDから時刻を読むことができるようになったので、次はGNSS/GPSからの時刻を精度良くRaspberryPiに反映させることが重要となってきました。

そうなってくると、GNSS/GPSユニットから送られてくる時刻情報と、実際の時刻とのずれを計測して補正する必要があります。
現状そこまで精度が必要ないので、次期課題としておきます。



2021年9月22日水曜日

タイムコーダーを作る【2】RaspberryPi4+I2C+OLEDで表示高速化

前回、液晶で作成したタイムコーダーを今回はOLEDで作り直します。

やはり液晶では書き換え速度が遅く電波時計にしかならず、ビデオカメラに写し込んでそのビデオの撮影時刻を正確に掴むところまでには至りませんでした。

前回の話:

タイムコーダーを作る[1] RaspberryPi4+I2Cで液晶LCD1602を使う時は 3.3V版を使う

のつづき

■前提とする環境

RaspberryPi4
RaspberryPi W/WH
Python3
SOC1602A OLED3.3V WS0010/HD44780互換 
I2CアダプターPCF1574(I2C/HD44780変換インターフェース)




■LCDからOLEDに変更した

前回LCDでは液晶の特性上どうしても書き換え速度が上がらず、3fpsぐらいの速度しかでませんでした。そこで、それよりも遥かに速い書き換え速度が期待できるOLEDなキャラクターディスプレイを使うことにしました。
店頭やネット販売でOLEDディスプレイが購入できますが、販売実績が良いものはだいたいSDD1322を使用したビットマップディスプレイです。文字サイズを自由に変更したり、図形を書いたり、写真のようなものを表示したりもできます。しかし、全画面のビットマップデータを毎回送信することになるので高速な書き換えを必要とする用途には向きません。実は、最初にSDD1322をつかってはみたのですが、ビットマップデータの送信に時間がかかることが判明し利用を中止しました。
やはり速いのはキャラクターディスプレイです。文字を表すアスキーコードを送るだけなので、一文字を送るのに8bit送信するだけです。
OLEDでキャラクターディスプレイを探したところ次のものを見つけました。


これに、I2CアダプターPCF1574をはんだ付けして使用します。

■OLEDキャラクターデバイスでミリ秒表示の時計を作ってみた

冒頭の画像が実際に動作しているところです。
1行目は日時を表示しています。2行目はmSecを表す3桁を連続して表示するようにしています。

画面上に表示されている2行目を読むと

728

739

751

692

716

この映像から現在時刻は20:11:02.751以降というのがわかります。

692から順に差分を読むと

24

12

11

12

最悪値を除くと書き換え間隔は12mSec辺りというのがわかります。なので現在時刻は

20:11:02.751~763

辺りにあると思われます。

前回のタイムレコーダーの書き換え間隔が約300mSecだったので、それよりも遥かに精度が良くなりました。

実用上では問題無いレベルにはなりました。しかし、それでも精度にもう少し余裕がほしいところです。ちなみに、動画のフレームレートが60fpsなら、1フレーム当たり16.6mSecとなります。せめて4mSec程度の精度は欲しくなります。


■ソースコード

今回SOC1602Aを利用するに当たり、Pythonのライブラリi2clcdを使用しました。しかしながら、そのままでは動作しなかったので、いくらか手を加えました。
公開していただいたSiYu Wuさん ありがとう!
謝謝 SiYu Wu!
[i2coledclockp4.py]
# -*- coding: utf-8 -*-
# !/usr/bin/python
import os
import time
from time import gmtime, strftime
import datetime
#import i2coled
import i2coled

lcd = i2coled.i2coled()
lcd.init()
lcd.print(os.path.basename(__file__))
print(os.path.basename(__file__))
time.sleep(1)


lcd.clear()
now = datetime.datetime.now()
strnow = now.strftime("%y%m%d %H:%M:%S.")
print(strnow)
lcd.return_home()
time.sleep(0.01)
lcd.print(strnow)

digit_f = 0
second_prev = -1
while(True):

    #### current time
    now = datetime.datetime.now()
    if now.second != second_prev:
        second_prev = now.second
        lcd.clear()
        strnow = now.strftime("%y%m%d %H:%M:%S.")
        lcd.return_home()
        time.sleep(0.01)
        lcd.print(strnow)

    str_f = now.strftime("%f")
    str_f4 = str_f[:3]
    lcd.move_cursor(1, digit_f)
    digit_f = (digit_f + 3) % 15
    #lcd.return_home()
    time.sleep(0)
    lcd.print(str_f4)

    time.sleep(0)


[i2coled.py]

# -*- coding: utf-8 -*-
"""
Custumized for OLED SOC1602A
    https://engineering.purdue.edu/477grp3/Files/refs/SOC1602A.pdf

LCD1602/2002/2004 I2C adapter driver for Raspberry Pi or other devices

Copyright (C) 2019 SiYu Wu <wu.siyu@hotmail.com>. All Rights Reserved.

"""
__author__ = 'Nomura Yukiaki <>'
__author__ = 'SiYu Wu <wu.siyu@hotmail.com>'

import smbus
import time

name = 'i2coled'

# Note for developers
#
# I2C byte:   [H ------------------------ L]
#             [    data    ]  [  ctrl_bits ]
# PCA8574:    P7  P6  P5  P4  P3  P2  P1  P0
# LCD1602:    D7  D6  D5  D4  BT  E   R/W RS

# Define some device constants
LCD_DAT = 0x01  # Mode - Sending data
LCD_CMD = 0x00  # Mode - Sending command

# LINE_1 = 0x80   # LCD RAM address for the 1st line
# LINE_2 = 0xC0   # LCD RAM address for the 2nd line
# LINE_3 = 0x94   # LCD RAM address for the 3rd line
# LINE_4 = 0xD4   # LCD RAM address for the 4th line
LCD_LINES = (0x80, 0xC0, 0x94, 0xD4)

# Character code for custom characters in CGRAM
CGRAM_CHR = (b'\x00', b'\x01', b'\x02', b'\x03', b'\x04', b'\x05', b'\x06', b'\x07')


class i2coled():
    def __init__(self, i2c_bus=1, i2c_addr=0x27, lcd_width=16):
        """
        initialize the connection with the LCD

        i2c_bus:    the smbus where the LCD connected to,
                    for Raspberry Pi, it should be 1 or 0 (depending on the model)
        i2c_addr:   I2C address of the adapter, usually 0x27, 0x20 or 0x3f
        lcd_width:  the width of the LCD, e.g. 16 for LCD1602, 20 for LCD2002/2004
        """
        self._bus = smbus.SMBus(i2c_bus)
        time.sleep(0.1)
        self._i2c_addr = i2c_addr
        self._lcd_width = lcd_width

        self._backlight = True
        self._write_data = 0x00
        self._read_data = 0x00
        

    def _i2c_write(self, data):
        """write one byte to I2C bus"""
        self._write_data = data
        self._bus.write_byte(self._i2c_addr, data)

    def _i2c_read(self):
        """read one byte from I2C bus"""
        data = self._bus.read_byte(self._i2c_addr)
        return data

    def _write_en(self):
        """proform a high level pulse to EN"""

        time.sleep(0)
        self._i2c_write(self._write_data | 0b00000100)
        time.sleep(0)
        self._i2c_write(self._write_data & ~0b00000100)
        time.sleep(0)

    def _read_en(self):
        """proform a high level pulse to EN"""

        time.sleep(0)
        self._i2c_write(self._write_data | 0b00000100)
        time.sleep(0.00018) #tACC68@p11
        data_H = self._i2c_read()
        self._i2c_write(self._write_data & ~0b00000100)
        time.sleep(0)

        self._i2c_write(self._write_data | 0b00000100)
        time.sleep(0.00018) #tACC68@p11
        data_L = self._i2c_read()
        self._i2c_write(self._write_data & ~0b00000100)
        time.sleep(0)

        data = (data_H & 0xF0) + (data_L >> 4)
        return data

    def write_byte(self, data, mode):
        """write one byte to LCD"""

        data_H = (data & 0xF0) | self._backlight * 0x08 | mode
        data_L = ((data << 4) & 0xF0) | self._backlight * 0x08 | mode

        self._i2c_write(data_H)
        self._write_en()

        self._i2c_write(data_L)
        self._write_en()

        time.sleep(0.0001)

    def read_byte(self, command):
        self._i2c_write(command)
        data = self._read_en()

        return data

    def wait_busy(self):
        busy = 0x80
        while busy & 0x80:
            busy = self.read_byte(0x02) #BT:0 E:0 Read:1 RS:0  ReadBusyFlag
            time.sleep(0)

    def init(self):
        """
        Initialize the LCD
         SOC1602A.pdf Page 21, 4-bit mode Initialization
        """
        time.sleep(0.001)       #Wait for power stabilization
        # setting LCD data interface to 8 bit
        # Function Set
        self._i2c_write(0x30)
        self._write_en()
        time.sleep(0.0041)
        self._i2c_write(0x30)
        self._write_en()
        time.sleep(0.0001)
        self._i2c_write(0x30)
        self._write_en()
        time.sleep(0.0001)


        # setting LCD data interface to 4 bit
        # Function Set  from instruction of developers manual
        self._i2c_write(0x20)
        self._write_en()
        time.sleep(0.0041)
        self._i2c_write(0x20)
        self._write_en()
        time.sleep(0.0001)
        self._i2c_write(0x80)
        self._write_en()
        time.sleep(0.0001)
        #self.wait_busy()

        # Display Off
        self.write_byte(0x08, LCD_CMD)
            #self._i2c_write(0x00)
            #self._write_en()
            #self._i2c_write(0x80)
            #self._write_en()

        #Display Clear
        self.clear()

        #Entry Mode Set
        self.write_byte(0x06, LCD_CMD)
            #self._i2c_write(0x00)
            #self._write_en()
            #self._i2c_write(0x60)
            #self._write_en()

        #Home Command
        self.return_home()

        #Display ON
        self.write_byte(0x0C, LCD_CMD)


    def init_org(self):
        """
        Initialize the LCD
        """

        # setting LCD data interface to 4 bit
        self._i2c_write(0x30)
        self._write_en()
        time.sleep(0.0041)
        self._i2c_write(0x30)
        self._write_en()
        time.sleep(0.0001)
        self._i2c_write(0x30)
        self._write_en()
        time.sleep(0.0001)
        self._i2c_write(0x20)
        self._write_en()

        self.write_byte(0x28, LCD_CMD)    # 00101000 Function set: interface 4bit, 2 lines, 5x8 font
        self.write_byte(0x0C, LCD_CMD)    # 00001100 Display ON/OFF: display on, cursor off, cursor blink off
        self.write_byte(0x06, LCD_CMD)    # 00000110 Entry Mode set: cursor move right, display not shift
        self.clear()

    def clear(self):
        """
        Clear the display and reset the cursor position
        """
        self.write_byte(0x01, LCD_CMD)
        time.sleep(0.002)

    def set_backlight(self, on_off):
        """
        Set whether the LCD backlight is on or off
        """
        self._backlight = on_off
        i2c_data = (self._write_data & 0xF7) + self._backlight * 0x08
        self._i2c_write(i2c_data)

    def set_cursor(self, cursor_visible, cursor_blink):
        """
        Set whether the cursor is visible and whether it will blink
        """
        cmd = 0x0C + cursor_visible * 0x02 + cursor_blink * 0x01
        self.write_byte(cmd, LCD_CMD)

    def move_cursor(self, line, column):
        """
        Move the cursor to a new posotion

        line:   line number starts from 0
        column: column number starts from 0
        """
        cmd = LCD_LINES[line] + column
        self.write_byte(cmd, LCD_CMD)

    def shift(self, direction='RIGHT', move_display=False):
        """
        Move the cursor and display left or right

        direction:      could be 'RIGHT' (default) or 'LEFT'
        move_display:   move the entire display and cursor, or only move the cursor
        """
        direction = 0x04 if direction == 'RIGHT' else 0x00
        cmd = 0x10 + direction + move_display * 0x08
        self.write_byte(cmd, LCD_CMD)

    def return_home(self):
        """
        Reset cursor and display to the original position.
        """
        self.write_byte(0x02, LCD_CMD)
        time.sleep(0.002)

    def write_CGRAM(self, chr_data, CGRAM_solt=0):
        """
        Write a custom character to CGRAM

        chr_data:     a tuple that stores the character model data
        CGRAM_solt:   int from 0 to 7 to determine where the font data is written

        NOTICE: re-setting the cursor position after calling this method, e.g.

        lcd.write_CGRAM((0x10, 0x06, 0x09, 0x08, 0x08, 0x09, 0x06, 0x00), 2)
        lcd.move_cursor(1, 0)
        lcd.print(b'New char: ' + i2coled.CGRAM_CHR[2])
        """
        cmd = 0x40 + CGRAM_solt * 8
        self.write_byte(cmd, LCD_CMD)

        for dat in chr_data:
            self.write_byte(dat, LCD_DAT)

    def print(self, text):
        """
        Print a string at the current cursor position

        text:   bytes or str object, str object will be encoded with ASCII
        """
        if isinstance(text, str):
            text = text.encode('ascii')

        for b in text:
            self.write_byte(b, LCD_DAT)

    def print_line(self, text, line, align='LEFT'):
        """
        Fill a whole line of the LCD with a string

        text:   bytes or str object, str object will be encoded with ASCII
        line:   line number starts from 0
        align:  could be 'LEFT' (default), 'RIGHT' or 'CENTER'
        """

        if isinstance(text, str):
            text = text.encode('ascii')

        text_length = len(text)
        if text_length < self._lcd_width:
            blank_space = self._lcd_width - text_length
            if align == 'LEFT':
                text = text + b' ' * blank_space
            elif align == 'RIGHT':
                text = b' ' * blank_space + text
            else:
                text = b' ' * (blank_space // 2) + text + b' ' * (blank_space - blank_space // 2)
        else:
            text = text[:self._lcd_width]

        self.write_byte(LCD_LINES[line], LCD_CMD)
        self.print(text)



オリジナルのソースコードでは初期化に失敗することが有りましたので、ELM by ChaNさんの推奨する初期化方式を参考に修正いたしました。
初期化の方針としては、いきなり4bitモードで初期化するのではなく、一度8bitモードに確実に変更したあと、4bitモードへあらためて変更するという方法です。4bitモードの半バイト送信状態で4bitモード初期化をかけようとすると失敗するようです。

■なぜ遅いのか さらなる高速化を図るには

このOLEDの書き換え間隔は12mSec辺りでした。では、限界値はどこにあるのか。

RaspberryPi4のI2Cのクロックは最大で400kbps標準で100kbpsです。デフォルト値の100kbpsの場合、3桁の数字を送るのにかかる時間は、概算で

3桁 x 8ビット  (1 / 100,000bps) = 0.24mSec

 1mSecごとの表示も可能なはずです。

 なぜ遅いのか。HD44780互換インターフェースでは、8ビットと4ビット幅のデータ転送に対応しており、I2Cでも8ビットの送信が出来ます。そのまま接続できそうですが、8ビット全てをデータバスに接続していまうと、HD44780(LCD)のRS,R/W,E信号線のコントロールが出来ません。そのため、HD44780の4ビットバスモードを利用し、データは4ビット幅で送信し、のこり4ビットでRS,R/W,E信号線をコントールしています。


なので、同じ3桁を表示するためには、

RaspberryPi4から送信する8ビットはそれぞれ


一桁目上位4ビット送信
EN High
EN Low
一桁目下位4ビット送信
EN High
EN Low 
二桁目上位4ビット送信
EN High
EN Low
二桁目下位4ビット送信
EN High
EN Low
三桁目上位4ビット送信
EN High
EN Low
三桁目下位4ビット送信
EN High
EN Low

となります。I2Cを使用しI2C-HD44780インターフェースアダプターを使用すると、1バイト送信するためには6バイト必要です。しかも、それぞれ1バイト送信するのに、I2Cのアドレスも送信しないといけないのでさらに1バイト必要となり、なんと12バイト必要となります。
さらに、先の3桁の送信には36バイト。さらに文字の表示位置も毎回指定するのでさらに6バイト合計42バイト必要になります。

    42byte x 8bit x 10uSec = 2880uSec = 3.36mSec

3.36mSec必要になります。
実際にはI2Cのスタートコンディション、ACK受信、ストップコンディション等の処理がかかるのでこれ以上の時間が必要となっています。

ということで、さらなる高速化を図るには、I2C変換モジュールがボトルネックになっているので、OLEDをI2Cで直接接続されているキャラクターモジュールが必要となります。

そして、見つけたのが…

SOC1602F <SHENZHEN SURENOO TECHNOLOGY CO.,LTD.>
SO1602AWWB-UC-WB-U <Sunlike Display Tech. Corp.>

I2Cインターフェースを持つOLEDキャラクターモジュール!HD44780インターフェースへ変換せず、I2Cで直接コマンド操作できるので遥かに早く動作するはずです。

いずれもドライバーICはUS2066。

次回、I2Cネイティブ動作のOLEDを使ってさらなる高速化を図ります。


タイムコーダーを作る【3】RaspberryPi4+I2CネイティブOLEDを使う