ディスプレイライブラリ解剖(上級者向け)

初めに

前回の記事で、「ラズパイ pico」でディスプレイに文字を表示させました。
その中で、「LDC]というライブラリを使用しました。
今回の記事は、この「LDC]ライブラリの詳しい解説をしていこうと思います。

ライブラリの解体(上級者向け)

ということで、前回ディスプレイを使うのに「LDC」というライブラリを使用しました。
この「LDC」とは何かというと、Liquid Crystal Displayの略で液晶ディスプレイです

ただし、今回使ったLEDは少し違います。

本来ディスプレイを表示させるのには、16ピン必要です
それが、今回使用したI2Cモジュール(裏についている黒いうもの)を使用することでたったの4ピンで使うことができます。

画像(通信モジュール)


このモジュールを使うのに、必要な必要なLDCライブラリの解説です。
本来は16ピンのところ4ピンで文字を表示させるライブラリの仕組みを見ていきましょう。

import machine
import time

class LCD():
    def __init__(self, addr=None, blen=1):
        sda = machine.Pin(6)
        scl = machine.Pin(7)
        self.bus = machine.I2C(1,sda=sda, scl=scl, freq=400000)
        #print(self.bus.scan())
        self.addr = self.scanAddress(addr)
        self.blen = blen
        self.send_command(0x33) # Must initialize to 8-line mode at first
        time.sleep(0.005)
        self.send_command(0x32) # Then initialize to 4-line mode
        time.sleep(0.005)
        self.send_command(0x28) # 2 Lines & 5*7 dots
        time.sleep(0.005)
        self.send_command(0x0C) # Enable display without cursor
        time.sleep(0.005)
        self.send_command(0x01) # Clear Screen
        self.bus.writeto(self.addr, bytearray([0x08]))

    def scanAddress(self, addr):
        devices = self.bus.scan()
        if len(devices) == 0:
            raise Exception("No LCD found")
        if addr is not None:
            if addr in devices:
                return addr
            else:
                raise Exception(f"LCD at 0x{addr:2X} not found")
        elif 0x27 in devices:
            return 0x27
        elif 0x3F in devices:
            return 0x3F
        else:
            raise Exception("No LCD found")

    def write_word(self, data):
        temp = data
        if self.blen == 1:
            temp |= 0x08
        else:
            temp &= 0xF7
        self.bus.writeto(self.addr, bytearray([temp]))
    
    def send_command(self, cmd):
        # Send bit7-4 firstly
        buf = cmd & 0xF0
        buf |= 0x04               # RS = 0, RW = 0, EN = 1
        self.write_word(buf)
        time.sleep(0.002)
        buf &= 0xFB               # Make EN = 0
        self.write_word(buf)

        # Send bit3-0 secondly
        buf = (cmd & 0x0F) << 4
        buf |= 0x04               # RS = 0, RW = 0, EN = 1
        self.write_word(buf)
        time.sleep(0.002)
        buf &= 0xFB               # Make EN = 0
        self.write_word(buf)
    
    def send_data(self, data):
        # Send bit7-4 firstly
        buf = data & 0xF0
        buf |= 0x05               # RS = 1, RW = 0, EN = 1
        self.write_word(buf)
        time.sleep(0.002)
        buf &= 0xFB               # Make EN = 0
        self.write_word(buf)

        # Send bit3-0 secondly
        buf = (data & 0x0F) << 4
        buf |= 0x05               # RS = 1, RW = 0, EN = 1
        self.write_word(buf)
        time.sleep(0.002)
        buf &= 0xFB               # Make EN = 0
        self.write_word(buf)
    
    def clear(self):
        self.send_command(0x01) # Clear Screen
        
    def openlight(self):  # Enable the backlight
        self.bus.writeto(self.addr,bytearray([0x08]))
        # self.bus.close()
    
    def write(self, x, y, str):
        if x < 0:
            x = 0
        if x > 15:
            x = 15
        if y < 0:
            y = 0
        if y > 1:
            y = 1

        # Move cursor
        addr = 0x80 + 0x40 * y + x
        self.send_command(addr)

        for chr in str:
            self.send_data(ord(chr))
    
    def message(self, text):
        #print("message: %s"%text)
        for char in text:
            if char == '\n':
                self.send_command(0xC0) # next line
            else:
                self.send_data(ord(char))

これが、LDCのライブラリの中身です。
この「LDC」モジュールは様々なディスプレイに対応できるように設計されているので、不要な部分を削ってみました。
それが以下のようになります。

import machine
import time
class LCD_easy():
    def __init__(self,addr=None,blen=1):
        sda=machine.Pin(0)
        scl=machine.Pin(1)
        self.bus=machine.I2C(0,sda=sda,scl=scl,freq=400000)
        self.addr=self.scanAddress(addr)
        self.blen=blen
        #ディスプレイの指定(2行の5x7どっと)
        self.send_command(0x28)
        time.sleep(0.005)
        #コンソールを表示しないコマンド
        self.send_command(0x0c)
        time.sleep(0.005)
        #スクリーンをクリアするコマンド
        self.send_command(0x01)
        time.sleep(0.005)
        self.bus.writeto(self.addr,bytearray([0x08]))
        
    def scanAddress(self,addr):
        devices=self.bus.scan()
        if len(devices)==0:
            raise Exception('No LCD found')
        if addr is not None:
            if addr in devices:
                return addr
            else:
                raise Exception(f'LCD at 0x{addr:2x}not found')
        elif 0x27 in devices:
            return 0x27
        elif 0x3F in devices:
            return 0x3F
        else:
            raise Exception('No LCD found')
    def write_word(self,data):
        temp=data
        if self.blen==1:
            temp |=0x08
        else:
            temp &=0xf7
        self.bus.writeto(self.addr,bytearray([temp]))
    def send_command(self,cmd):
        buf=cmd & 0xF0
        buf |=0x04
        self.write_word(buf)
        time.sleep(0.002)
        buf &=0xFB
        self.write_word(buf)
        
        buf=(cmd & 0x0F) <<4
        buf |=0x04
        self.write_word(buf)
        time.sleep(0.002)
        buf &= 0xFB
        self.write_word(buf)
    def send_data(self, data):
        # Send bit7-4 firstly
        buf = data & 0xF0
        buf |= 0x05               # RS = 1, RW = 0, EN = 1
        self.write_word(buf)
        time.sleep(0.002)
        buf &= 0xFB               # Make EN = 0
        self.write_word(buf)

        # Send bit3-0 secondly
        buf = (data & 0x0F) << 4
        buf |= 0x05               # RS = 1, RW = 0, EN = 1
        self.write_word(buf)
        time.sleep(0.002)
        buf &= 0xFB               # Make EN = 0
        self.write_word(buf)
    def clear(self):
        self.send_command(0x01)
    def openlight(self):
        self.bus.writeto(self.addr,bytearray([0x08]))
    def message(self, text):
        #print("message: %s"%text)
        for char in text:
            if char == '\n':
                self.send_command(0xC0) 
            else:
                self.send_data(ord(char))

lcd=LCD_easy()
string='kouduki\n'
lcd.message(string)
string='lab'
lcd.message(string)

こんな感じになります。
ということで大きく分けて5つで解説していきます。

ブロックごとに解説!

__init__

まずは、「__init__」についてです。
これは、特殊メソッドでクラスが呼び出しされると初期値を渡します。

では、詳しく見ていきます。
下記のプログラムも見ながら、みてみてください。

def __init__(self,addr=None,blen=1):
        sda=machine.Pin(0)
        scl=machine.Pin(1)
        self.bus=machine.I2C(0,sda=sda,scl=scl,freq=400000)
        self.addr=self.scanAddress(addr)
        self.blen=blen
        #ディスプレイの指定(2行の5x7どっと)
        self.send_command(0x28)
        time.sleep(0.005)
        #コンソールを表示しないコマンド
        self.send_command(0x0c)
        time.sleep(0.005)
        #スクリーンをクリアするコマンド
        self.send_command(0x01)
        time.sleep(0.005)
        self.bus.writeto(self.addr,bytearray([0x08]))

__init__()の中にあるselfといれることで、どこからでもこの関数の中にある値にアクセスできるようになります。
そして、addr=Noneとは最初は空の状態で何も入れないということを表しています。
blenはデフォルトで1が入っていて、特定の条件下で0か1に変化します。
このディスプレイでは変更する必要がないので変わっていません。

SDA-,SCL-これらは、I2C通信(2本の線を使った通信方式)で使用するピンのを定義します。
sdaとは、データを送る線。sclはクロック線と言ってタイミングを計るものです。
sdaに電気が流れているか、いないかで0か1を渡します。
sclは今からデータ送るよ!と教える線です。

self.bus=machine.I2C(0,sda=sda,scl=scl,freq=400000)

これは、I2Cの通信の定義をします。()の最初の0はI2Cのどの線を使うかを定義するものです
以下の画像のI2C0かI2C1をつなぐかによって、変わります。
sda,sclは最初に定義したものを使います。

ここにピンの配線図

self.addr=self.scanAddress(addr)

これは、scanAddress関数を実行して帰ってきた値をaddrに入れます

self.blen=blen

これは値をそのまま代入します。

self.send_command(0x28)
time.sleep(0.005)

これは、send_command関数を用いて、ディスプレイに0x28(2進数で0010 1000)を送ります。
この0x28とは、ディスプレイに2行の5×7ドットディスプレイだよね?
と確認するためのコマンドです。ほかにも3行のディスプレイや1行のディスプレイの確認コマンドも存在しますが、今回使ったのが、2行ディスプレイなので、ほかのコマンドは削除しました。
その後、読み込みの時間5ms待機します。

self.send_command(0x0c),self.send_command(0x01)

これらは、それぞれカーソルを表示しない、画面をクリアにするコマンドを送るものです。
このコマンドは、ディスプレイのほうで決まっています。
データシートなどを確認すると、コマンドについて詳しく書いています。

self.bus.writeto(self.addr,bytearray([0x08]))

これはコマンドではなく、データとしてディスプレイに送ります。
0x08はディスプレイのバックライトを制御することができます。

scanadress

続いてscanAdressについて、見ていきましょう。

def scanAddress(self,addr):
        devices=self.bus.scan()
        if len(devices)==0:
            raise Exception('No LCD found')
        if addr is not None:
            if addr in devices:
                return addr
            else:
                raise Exception(f'LCD at 0x{addr:2x}not found')
        elif 0x27 in devices:
            return 0x27
        elif 0x3F in devices:
            return 0x3F

これは、I2Cで通信するデバイスを探すプログラムです。

devices=self.bus.scan()

まずは、ラズパイにつながっているモジュールを探します。
もし、デバイスが見つかったら、devices配列に入れます。
if len(devices)==0:で配列の中身が空だった場合は、以下のプログラムで割り込み処理をしてエラーメッセージを表示します。

if addr is not None:
raise Exception(f'LCD at 0x{addr:2x}not found')

もし、addrが空でない場合は、そのアドレスがdevicesの中にあるか確認します。
含まれている場合は、addrの値を返します。
含まれていない場合は、割り込み処理でエラーメッセージを表示します

elif 0x27 in devices:
return 0x27
elif 0x3F in devices:
return 0x3F

もし、0x27と0x3fがdevicesに含まれている場合は、それらをデフォルトとして値を返します。

write_word

def write_word(self,data):
        temp=data
        if self.blen==1:
            temp |=0x08
        else:
            temp &=0xf7
        self.bus.writeto(self.addr,bytearray([temp]))


blenが1の場合は、orビット演算をして書き込み、
blenが0場合は、andビット演算をして書き込みを行います。

temp=0000 0011
0x08=0000 1000

temp |=0x08
temp=0000 1011

temp &=0x08
temp =0000 0000

orビット演算はどちらかが1の場合に1を入れて、andビット演算の場合はどちらも1の場合に1を入れます。

send_command

モジュールにコマンドを送信するためのコマンドです。

def send_command(self,cmd):
        #前半
        buf=cmd & 0xF0
        buf |=0x04
        self.write_word(buf)#今から送るデータの長さを教える
        time.sleep(0.002)
        buf &=0xFB
        self.write_word(buf)#本物のデータを送る
        #後半
        buf=(cmd & 0x0F) <<4
        buf |=0x04
        self.write_word(buf)#今から送るデータの長さを教える
        time.sleep(0.002)
        buf &= 0xFB
        self.write_word(buf)#本物のデータを送る

コマンドを前半と後半に分けて、それぞれ2回ずつ通信します。
なぜかというと、「今からこれだけのデータ送るよー」と合図をします。
そのあとに、コードを送ります。

なぜ2回に分けるかというと1回で送れるコマンドの量に限りがあるからです。

send_data

def send_data(self, data):
        #前半
        buf = data & 0xF0
        buf |= 0x05               # RS = 1, RW = 0, EN = 1
        self.write_word(buf)
        time.sleep(0.002)
        buf &= 0xFB               # Make EN = 0
        self.write_word(buf)

        #後半
        buf = (data & 0x0F) << 4
        buf |= 0x05               # RS = 1, RW = 0, EN = 1
        self.write_word(buf)
        time.sleep(0.002)
        buf &= 0xFB               # Make EN = 0
        self.write_word(buf)

これも先ほどと同じように前半後半で分けています。
ビット演算のプログラムが違うのは、今から送るお物がコマンドなのかデータなのかを区別するためです。

clear・openlight

def clear(self):
        self.send_command(0x01)
def openlight(self):
        self.bus.writeto(self.addr,bytearray([0x08]))

これは、画面を初期化、バックライトをつけるコマンドです。
このコマンドは、デバイスであらかじめ決められているので、データシートを確認してください。

message

def message(self, text):
        #print("message: %s"%text)
        for char in text:
            if char == '\n':
                self.send_command(0xC0) 
            else:
                self.send_data(ord(char))#アスキーコードに変換して送信

これは、入力された文字を画面に表示させるプログラムです。
まずは、入力されたものをすべてアスキーコードというものに直して、モジュールに送ります。
改行の\nだけは例外なので、特別に定義します。

以下がアスキーコードになります
アルファベットを2進数で表すことができるため、モジュールに2本線だけで送信できるようになります。

ローマ字ASCIIコード(10進数)ASCIIコード(2進数)
A6501000001
B6601000010
C6701000011
D6801000100
E6901000101
F7001000110
G7101000111
H7201001000
アスキーコード表

最後に

どうだったでしょうか?
今回は、前回使ったデバイスのライブラリ解体を行いました!
少し専門的な内容ですね

まだ、自分もシリアル通信をマスターしていないのでかなり苦労しました。
もっと勉強が必要ですね!

ということで、また次回お会いしましょう!

Raspberry Pi Pico Wカテゴリの最新記事