I2C通信で液晶ディスプレイ(LCD)に文字表示。

2024年8月21日

【 当サイトには広告リンクが含まれています。 】


ラズパイPicoW(Raspberry Pi Pico W)に液晶ディスプレイ(LCD)を接続し、I2C通信を使って文字を表示させます。



実験準備

実験に必要な機器と部品を準備します。

機器

「ラズパイPicoWを始めよう。」記事で書きましたラズパイPicoWと、統合開発環境ThonnyとMicroPythonファームウェアをインストールしたRaspi4Bを準備します。


部品

液晶ディスプレイ(LCD)、I2CシリアルI/Fモジュールなど実験に使う部品を準備します。

部 品 名規 格数 量取扱い店(参考)
液晶ディスプレイ(LCD)LCD 1602A1Amazon (セット)
I2CシリアルI/FモジュールLCM1602
ブレッドボードBB-1021秋月電子通商
ジャンパーワイヤメス-メス
(約20cm)
1電子工作ステーション


機器と部品の配線

準備した機器と部品を実際につなぎ、MicroPythonでプログラミングを行い、液晶ディスプレイ(LCD)に文字を表示させます。

配線リスト

ブレッドボードを使って、液晶ディスプレイ(LCD)などをジャンパーワイヤでつなぎます。

GPI6とGP17にI2Cピンを接続しますが、プログラムコードを調整すれば、他のI2Cピンを使うこともできます。

液晶ディスプレイ(LCD)のVCCピンは5Vに接続する必要があります。


ラズパイPicoW液晶ディスプレイ(LCD備 考
GP16SDA
GP17SCL
VBUS (5V)VCC
GNDGND


配線図



液晶ディスプレイ(LCD)とI2CシリアルI/Fモジュールは、ブレッドボード内部の配線で接続します。


外部ライブラリの保存

LCDと通信をするために、GitHubユーザー(T-622)が開発したライブラリ、 lcd_api.py」と「 pico_i2c_lcd.pyの2種類のモジュールを組み合わせて使います。

外部モジュールは、GitHubから直接ダウンロードして入手するか、コマンドを使ってインストールする方法が一般的ですが、今回は入手したコードをThonnyのエディタに貼りつけて、指定のフォルダ(ディレクトリ)に保存します。

libフォルダ(ディレクトリ)の作成

MicroPythonは外部モジュールをインポートする際、ラズパイPicoWの「lib」フォルダ(ディレクトリ)を参照します。

「lib」フォルダ(ディレクトリ)が存在しない場合は、次の手順で新たに作成します。

Raspi4BのThonnyを起動後、メニューの「表示」をクリックして、ダイアログから「ファイル」を選択します。



「Raspberry pi Pico」エリアのルート(/)フォルダ(ディレクトリ)を右クリックし、ダイアログを表示させます。



表示されたダイアログで「新しいディレクトリ…」をクリックします。




「新しいディレクトリ…」ダイアログの「新しいディレクトリ名を入力 /」に「lib」を入力し、「OK」をクリックします。



「lib」フォルダが追加されていることを確認後、「ファイルX」をクリックしてファイルを終了します。



lcd_api.pyモジュールの保存

Raspi4BのThonnyを起動し、次のコードをコピーして「エディタ」にペーストします。

ラズパイPicoWの「lib」フォルダ(ディレクトリ)に、ファイル名「 lcd_api.py」として保存します。

ファイル名は変えないでください。


# forked from https://github.com/T-622/RPI-PICO-I2C-LCD/
import time

class LcdApi:
    
    # Implements the API for talking with HD44780 compatible character LCDs.
    # This class only knows what commands to send to the LCD, and not how to get
    # them to the LCD.
    #
    # It is expected that a derived class will implement the hal_xxx functions.
    #
    # The following constant names were lifted from the avrlib lcd.h header file,
    # with bit numbers changed to bit masks.
    
    # HD44780 LCD controller command set
    LCD_CLR             = 0x01  # DB0: clear display
    LCD_HOME            = 0x02  # DB1: return to home position

    LCD_ENTRY_MODE      = 0x04  # DB2: set entry mode
    LCD_ENTRY_INC       = 0x02  # DB1: increment
    LCD_ENTRY_SHIFT     = 0x01  # DB0: shift

    LCD_ON_CTRL         = 0x08  # DB3: turn lcd/cursor on
    LCD_ON_DISPLAY      = 0x04  # DB2: turn display on
    LCD_ON_CURSOR       = 0x02  # DB1: turn cursor on
    LCD_ON_BLINK        = 0x01  # DB0: blinking cursor

    LCD_MOVE            = 0x10  # DB4: move cursor/display
    LCD_MOVE_DISP       = 0x08  # DB3: move display (0-> move cursor)
    LCD_MOVE_RIGHT      = 0x04  # DB2: move right (0-> left)

    LCD_FUNCTION        = 0x20  # DB5: function set
    LCD_FUNCTION_8BIT   = 0x10  # DB4: set 8BIT mode (0->4BIT mode)
    LCD_FUNCTION_2LINES = 0x08  # DB3: two lines (0->one line)
    LCD_FUNCTION_10DOTS = 0x04  # DB2: 5x10 font (0->5x7 font)
    LCD_FUNCTION_RESET  = 0x30  # See "Initializing by Instruction" section

    LCD_CGRAM           = 0x40  # DB6: set CG RAM address
    LCD_DDRAM           = 0x80  # DB7: set DD RAM address

    LCD_RS_CMD          = 0
    LCD_RS_DATA         = 1

    LCD_RW_WRITE        = 0
    LCD_RW_READ         = 1

    def __init__(self, num_lines, num_columns):
        self.num_lines = num_lines
        if self.num_lines > 4:
            self.num_lines = 4
        self.num_columns = num_columns
        if self.num_columns > 40:
            self.num_columns = 40
        self.cursor_x = 0
        self.cursor_y = 0
        self.implied_newline = False
        self.backlight = True
        self.display_off()
        self.backlight_on()
        self.clear()
        self.hal_write_command(self.LCD_ENTRY_MODE | self.LCD_ENTRY_INC)
        self.hide_cursor()
        self.display_on()

    def clear(self):
        # Clears the LCD display and moves the cursor to the top left corner
        self.hal_write_command(self.LCD_CLR)
        self.hal_write_command(self.LCD_HOME)
        self.cursor_x = 0
        self.cursor_y = 0

    def show_cursor(self):
        # Causes the cursor to be made visible
        self.hal_write_command(self.LCD_ON_CTRL | self.LCD_ON_DISPLAY |
                               self.LCD_ON_CURSOR)

    def hide_cursor(self):
        # Causes the cursor to be hidden
        self.hal_write_command(self.LCD_ON_CTRL | self.LCD_ON_DISPLAY)

    def blink_cursor_on(self):
        # Turns on the cursor, and makes it blink
        self.hal_write_command(self.LCD_ON_CTRL | self.LCD_ON_DISPLAY |
                               self.LCD_ON_CURSOR | self.LCD_ON_BLINK)

    def blink_cursor_off(self):
        # Turns on the cursor, and makes it no blink (i.e. be solid)
        self.hal_write_command(self.LCD_ON_CTRL | self.LCD_ON_DISPLAY |
                               self.LCD_ON_CURSOR)

    def display_on(self):
        # Turns on (i.e. unblanks) the LCD
        self.hal_write_command(self.LCD_ON_CTRL | self.LCD_ON_DISPLAY)

    def display_off(self):
        # Turns off (i.e. blanks) the LCD
        self.hal_write_command(self.LCD_ON_CTRL)

    def backlight_on(self):
        # Turns the backlight on.
        
        # This isn't really an LCD command, but some modules have backlight
        # controls, so this allows the hal to pass through the command.
        self.backlight = True
        self.hal_backlight_on()

    def backlight_off(self):
        # Turns the backlight off.

        # This isn't really an LCD command, but some modules have backlight
        # controls, so this allows the hal to pass through the command.
        self.backlight = False
        self.hal_backlight_off()

    def move_to(self, cursor_x, cursor_y):
        # Moves the cursor position to the indicated position. The cursor
        # position is zero based (i.e. cursor_x == 0 indicates first column).
        self.cursor_x = cursor_x
        self.cursor_y = cursor_y
        addr = cursor_x & 0x3f
        if cursor_y & 1:
            addr += 0x40    # Lines 1 & 3 add 0x40
        if cursor_y & 2:    # Lines 2 & 3 add number of columns
            addr += self.num_columns
        self.hal_write_command(self.LCD_DDRAM | addr)

    def putchar(self, char):
        # Writes the indicated character to the LCD at the current cursor
        # position, and advances the cursor by one position.
        if char == '\n':
            if self.implied_newline:
                # self.implied_newline means we advanced due to a wraparound,
                # so if we get a newline right after that we ignore it.
                pass
            else:
                self.cursor_x = self.num_columns
        else:
            self.hal_write_data(ord(char))
            self.cursor_x += 1
        if self.cursor_x >= self.num_columns:
            self.cursor_x = 0
            self.cursor_y += 1
            self.implied_newline = (char != '\n')
        if self.cursor_y >= self.num_lines:
            self.cursor_y = 0
        self.move_to(self.cursor_x, self.cursor_y)

    def putstr(self, string):
        # Write the indicated string to the LCD at the current cursor
        # position and advances the cursor position appropriately.
        for char in string:
            self.putchar(char)

    def custom_char(self, location, charmap):
        # Write a character to one of the 8 CGRAM locations, available
        # as chr(0) through chr(7).
        location &= 0x7
        self.hal_write_command(self.LCD_CGRAM | (location << 3))
        self.hal_sleep_us(40)
        for i in range(8):
            self.hal_write_data(charmap[i])
            self.hal_sleep_us(40)
        self.move_to(self.cursor_x, self.cursor_y)

    def hal_backlight_on(self):
        # Allows the hal layer to turn the backlight on.
        # If desired, a derived HAL class will implement this function.
        pass

    def hal_backlight_off(self):
        # Allows the hal layer to turn the backlight off.
        # If desired, a derived HAL class will implement this function.
        pass

    def hal_write_command(self, cmd):
        # Write a command to the LCD.
        # It is expected that a derived HAL class will implement this function.
        raise NotImplementedError

    def hal_write_data(self, data):
        # Write data to the LCD.
        # It is expected that a derived HAL class will implement this function.
        raise NotImplementedError

    def hal_sleep_us(self, usecs):
        # Sleep for some time (given in microseconds)
        time.sleep_us(usecs)


pico_i2c_lcd.pyモジュールの保存

Raspi4BのThonnyを起動し、次のコードをコピーして「エディタ」にペーストします。

ラズパイPicoWの「lib」フォルダ(ディレクトリ)に、ファイル名「pico_i2c_lcd.py」として保存します。

ファイル名は変えないでください。


# forked from https://github.com/T-622/RPI-PICO-I2C-LCD/
import utime
import gc

from lcd_api import LcdApi
from machine import I2C

# PCF8574 pin definitions
MASK_RS = 0x01       # P0
MASK_RW = 0x02       # P1
MASK_E  = 0x04       # P2

SHIFT_BACKLIGHT = 3  # P3
SHIFT_DATA      = 4  # P4-P7

class I2cLcd(LcdApi):
    
    #Implements a HD44780 character LCD connected via PCF8574 on I2C

    def __init__(self, i2c, i2c_addr, num_lines, num_columns):
        self.i2c = i2c
        self.i2c_addr = i2c_addr
        self.i2c.writeto(self.i2c_addr, bytes([0]))
        utime.sleep_ms(20)   # Allow LCD time to powerup
        # Send reset 3 times
        self.hal_write_init_nibble(self.LCD_FUNCTION_RESET)
        utime.sleep_ms(5)    # Need to delay at least 4.1 msec
        self.hal_write_init_nibble(self.LCD_FUNCTION_RESET)
        utime.sleep_ms(1)
        self.hal_write_init_nibble(self.LCD_FUNCTION_RESET)
        utime.sleep_ms(1)
        # Put LCD into 4-bit mode
        self.hal_write_init_nibble(self.LCD_FUNCTION)
        utime.sleep_ms(1)
        LcdApi.__init__(self, num_lines, num_columns)
        cmd = self.LCD_FUNCTION
        if num_lines > 1:
            cmd |= self.LCD_FUNCTION_2LINES
        self.hal_write_command(cmd)
        gc.collect()

    def hal_write_init_nibble(self, nibble):
        # Writes an initialization nibble to the LCD.
        # This particular function is only used during initialization.
        byte = ((nibble >> 4) & 0x0f) << SHIFT_DATA
        self.i2c.writeto(self.i2c_addr, bytes([byte | MASK_E]))
        self.i2c.writeto(self.i2c_addr, bytes([byte]))
        gc.collect()
        
    def hal_backlight_on(self):
        # Allows the hal layer to turn the backlight on
        self.i2c.writeto(self.i2c_addr, bytes([1 << SHIFT_BACKLIGHT]))
        gc.collect()
        
    def hal_backlight_off(self):
        #Allows the hal layer to turn the backlight off
        self.i2c.writeto(self.i2c_addr, bytes([0]))
        gc.collect()
        
    def hal_write_command(self, cmd):
        # Write a command to the LCD. Data is latched on the falling edge of E.
        byte = ((self.backlight << SHIFT_BACKLIGHT) |
                (((cmd >> 4) & 0x0f) << SHIFT_DATA))
        self.i2c.writeto(self.i2c_addr, bytes([byte | MASK_E]))
        self.i2c.writeto(self.i2c_addr, bytes([byte]))
        byte = ((self.backlight << SHIFT_BACKLIGHT) |
                ((cmd & 0x0f) << SHIFT_DATA))
        self.i2c.writeto(self.i2c_addr, bytes([byte | MASK_E]))
        self.i2c.writeto(self.i2c_addr, bytes([byte]))
        if cmd <= 3:
            # The home and clear commands require a worst case delay of 4.1 msec
            utime.sleep_ms(5)
        gc.collect()

    def hal_write_data(self, data):
        # Write data to the LCD. Data is latched on the falling edge of E.
        byte = (MASK_RS |
                (self.backlight << SHIFT_BACKLIGHT) |
                (((data >> 4) & 0x0f) << SHIFT_DATA))
        self.i2c.writeto(self.i2c_addr, bytes([byte | MASK_E]))
        self.i2c.writeto(self.i2c_addr, bytes([byte]))
        byte = (MASK_RS |
                (self.backlight << SHIFT_BACKLIGHT) |
                ((data & 0x0f) << SHIFT_DATA))      
        self.i2c.writeto(self.i2c_addr, bytes([byte | MASK_E]))
        self.i2c.writeto(self.i2c_addr, bytes([byte]))
        gc.collect()


LCDの I2Cアドレスを確認

Raspi4BのThonnyを起動し、次のコードを「エディタ」に入力するか、リストをコピーしてペーストします。


#モジュールの読み込み(インポート)
from machine import Pin, I2C

#I2Cを利用するため、オブジェクト(i2c)を作成
i2c = I2C(0,sda=Pin(16), scl=Pin(17), freq=400000)

#I2Cのアドレス確認(16進数)
for addr in i2c.scan():   
    print("I2C 16進法アドレス: ", hex(addr))


Thonnyの「F5」キーを押して、プログラムを実行します。

「I2C 16進法アドレス: 0x27」がThonnyのシェルに表示されます。

LCDの I2Cアドレスは「0x27」であることが確認できます。


確認後、Thonnyの「Ctrl + F2」キーを押して、停止します。


文字表示プログラム

画面上のどこに文字を表示させたいかを選択し、液晶ディスプレイにメッセージを送ると、文字が表示されます。

文字を表示したい場所を指定しなければ、最初に空いているスペースに、文字が表示されます。

文字表示プログラムは「Hello, World!」を1行目に3秒間表示した後、表示を消去します。

続いて「Welcome To」を1行目に表示して、3秒間経過後、2行目に「ラズパイ Pico W」を表示します。

Raspi4BのThonnyを起動し、次のコードを「エディタ」に入力するか、リストをコピーしてペーストします。


#モジュールの読み込み(インポート)
from machine import Pin, I2C
from pico_i2c_lcd import I2cLcd
from time import sleep


#I2Cを利用するため、オブジェクト(i2c)を作成
i2c = I2C(0,sda=Pin(16), scl=Pin(17), freq=400000)


#LCDのパラメータを設定
ADR = 0x27
ROW = 2
COL = 16


#LCDを利用するため、オブジェクト(lcd)を作成
lcd = I2cLcd(i2c, ADR, ROW, COL)


#画面を消去
lcd.clear()
#LCDに文字を表示
lcd.putstr("Hello World!")
#3秒間待つ
sleep(3)
lcd.clear()


#LCDに文字を表示
lcd.putstr("Welcome to")
sleep(3)
        

#カタカナ文字(ラズパイ)を作成
ch=[
#ラ
bytearray([0x0E, 0x00, 0x1F, 0x01, 0x01, 0x02, 0x04, 0x00]),
#ス
bytearray([0x00, 0x1F, 0x01, 0x02, 0x04, 0x0A, 0x11, 0x00]),
#"
bytearray([0x04, 0x12, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00]),       
#ハ    
bytearray([0x00, 0x04, 0x02, 0x11, 0x11, 0x11, 0x11, 0x00]),
#。
bytearray([0x1C, 0x14, 0x1C, 0x00, 0x00, 0x00, 0x00, 0x00]),
#イ
bytearray([0x01, 0x02, 0x04, 0x0C, 0x14, 0x04, 0x04, 0x00])
]


#LCDにカタカナ文字(ラズパイ)を表示
for i in range(len(ch)):
    lcd.custom_char(i, ch[i])
    lcd.move_to(i, 1)
    lcd.putchar(chr(i))
    
    
#LCDに文字を表示
lcd.putstr(" Pico W")


#5秒後にLCD消灯
sleep(5)
lcd.backlight_off()
lcd.display_off()


Thonnyの「F5」キーを押して、文字表示プログラムを実行します。

文字表示プログラムは「Hello, World!」を1行目に3秒間表示した後、表示を消去します。

続いて「Welcome To」を1行目に表示して、3秒間経過後、2行目に「ラズパイ Pico W」を表示します。

表示終了後、5秒後にLCDが消灯します。



確認後、Thonnyの「Ctrl + F2」キーを押して、停止します。


任意のファイル名でラズパイPicoWに保存します。(ここでは「3_6i2clcd.py」で保存しました。)


プログラムの仕組み

LCDとどのようにやり取りするのか、プログラムの動きを簡単に見ていきます。

▶️モジュールの読み込み(インポート)

LCDとI2C通信するために必要なモジュールを読み込みます。

from machine import Pin, I2C
from pico_i2c_lcd import I2cLcd
from time import sleep


▶️I2Cを利用するため、オブジェクト(i2c)を作成

LCDとI2C接続するGP16とGP17を初期化します。

i2c = I2C(0,sda=Pin(16), scl=Pin(17), freq=400000)


項 目説 明
0I2Cのチャンネル番号(ラズパイPicoWの場合は0または1)
sda=Pin(16)SDA(シリアルデータ)として使うGPピン番号
scl=Pin(17)SCL(シリアルクロック)として使うGPピン番号
freq=400000通信速度(400000bps=400kbps)


▶️LCDのパラメータを設定

LCDディスプレイのI2Cアドレス(0x27)と表示範囲(2行16列)を設定します。

ADR = 0x27
ROW = 2
COL = 16


▶️LCDを利用するため、オブジェクト(lcd)を作成

LCDに文字を表示させるため、I2cLcd() 関数を使ってオブジェクト(lcd)を作成し、i2c オブジェクト、アドレス(ADR)、行数(ROW)と列数(COL)を引数として渡します。

lcd = I2cLcd(i2c, ADR, ROW, COL)


▶️画面を消去

新しい文字を表示する前に、clear()関数を使って画面の内容を消去します。

lcd.clear()


▶️LCDに文字を表示

単純な文字表示は、lcd オブジェクトの putstr() 関数を使って、表示したい文字を引数として渡します。

引数として渡された文字列 「Hello World!」は、ディスプレイの上の段(1行目)の左隅から表示されます。

lcd.putstr("Hello World!")


▶️3秒間待つ

メッセージを3秒間画面に表示した後、画面の内容を消去します。

sleep(3)
lcd.clear()


▶️LCDに文字を表示

「Welcome to」を、ディスプレイの上の段(1行目)の左隅から3秒間表示します。

lcd.putstr("Welcome to")
sleep(3)


▶️カタカナ文字(ラズパイ)を作成

カタカナ文字や絵文字等の特殊文字は、「lcd.putstr("ラズパイ")」関数では表示することができません。

16×2のLCDには、文字を表示できるブロックが32個あり、各ブロックは5×8の小さなピクセルで構成されています。

各ピクセルの状態を定義することで、特殊文字を表示することができ、そのためには、各ピクセルの状態を保持するバイト配列変数を作成する必要があります。

バイト配列変数は、8個の特殊文字を0~7の添え字を持つ、LCDのメモリーに保存することができます。

特殊文字の作成は、「LCD Custom Character Generator」を利用しました。

LCD Custom Character Generatorの「Pixels」をクリックして文字を作成すると、ピクセルの状態が8個の16進数に変換されますので、値をコピーしてプログラムを作成します。

カタカナの「ラ」は、次のように作成します。(文字の形はこちらの記事を参考にしました。)


コピーした値をバイト配列にし、配列をリストにして文字列として扱います。

ch=[
#ラ
bytearray([0x0E, 0x00, 0x1F, 0x01, 0x01, 0x02, 0x04, 0x00]),
#ス
bytearray([0x00, 0x1F, 0x01, 0x02, 0x04, 0x0A, 0x11, 0x00]),
#"
bytearray([0x04, 0x12, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00]),       
#ハ    
bytearray([0x00, 0x04, 0x02, 0x11, 0x11, 0x11, 0x11, 0x00]),
#。
bytearray([0x1C, 0x14, 0x1C, 0x00, 0x00, 0x00, 0x00, 0x00]),
#イ
bytearray([0x01, 0x02, 0x04, 0x0C, 0x14, 0x04, 0x04, 0x00])
]


▶️LCDにカタカナ文字(ラズパイ)を表示

リスト化したカタカナ文字(ラズパイ)をfor文を使ってLCDに表示します。

最初にLCDのメモリーに表示したい文字情報を、LCDのメモリーに保存する必要があります。

保存は、lcdオブジェクトのcustom_char()関数を使い、メモリーの番地を指定する添え字と、文字のバイト配列を引数として渡します。

lcd.custom_char(i, ch[i])


文字を表示する位置の設定は、lcdオブジェクトのmove_to()関数を使い、列の位置(0~15)と、行の位置(0は上段、1は下段)を引数として渡します。

lcd.move_to(i, 1)


添え字で指定されたメモリー内のバイト配列をchr型に変換し、putchar()関数に引数として渡すことで、指定した位置に文字が表示されます。

lcd.putchar(chr(i))


▶️LCDに文字を表示

「Pico W」を、カタカナ文字(ラズパイ)の後ろに追加表示します。

lcd.putstr(" Pico W")


▶️5秒後にLCD消灯

メッセージを5秒間画面に表示した後、LCDのバックライトを消して、LCDをOFFにします。

sleep(5)
lcd.backlight_off()
lcd.display_off()


まとめ

ラズパイPicoW(Raspberry Pi Pico W)に液晶ディスプレイ(LCD)を接続し、I2C通信を使って文字を表示させることができました。

MicroPythonに標準で提供されているライブラリ(標準ライブラリ)の他に、サードパーティーから提供されているライブラリ(外部ライブラリ)の保存と使い方を学習しました。

英数、記号文字以外の文字(カタカナ等の特殊文字)を作成し、表示する方法を学習しました。