ILI9341 LCD を使う

更新日: 2026.08.25

概要

以前ILI9341_240_320 2.5" LCD で LCDをESP32で制御する例を紹介しました。今回はRaspberry PIで制御してみたいと思います。 ”LCDへの表示” ”タッチセンサ使用” の順に説明して行います。目標は "自作製品のちょっとしたユーザインターフェイスとして使用する" です。

ILI9341 LCD

回路の作成

Schematic circuit of raspberry-pi & LCD
  • Raspberry PIのヘッダーピンとLCDのピンを上図の様に接続します。
  • LCDとタッチセンサの制御はSPIで行います。チップセレクトは別に、制御線は共通にしています。

OSのインストールと前準備

"Paspberry PI Zero W" に "Legacy 32bit light(Bookworm no Desktop)"をRaspberry PI Imagerで セットアップしています。OSインストール後下記の前準備を行います。

  • アップデートとアップグレイド
    • sudo apt update
    • sudo apt full-upgrade -y
  • SPI通信の有効化
    • raspi-config で「Interface Options」で SPI を有効にする。
  • Pi.GPIO: Raspberry PiのGPIOピン制御。spidev: SPI通信制御。 Pillow (PIL): 画像処理・描画のインストール
    • sudo apt install python3-rpi.gpio python3-spidev python3-pil
  • 注:今回使用したRas PI Zero固有の問題と思いますがspi.open(0, 1)が正常に通信出来ませんでした。
    • CS2(GPIO8)の不具合が原因でした。そこで、CS2をGPIO8からGPIO22に変更しています。
    • "/boot/firmware/config.txt" の一番下( "[all]"の下)に 項目の下記を追加。
      • "dtoverlay=spi0-2cs,cs0_pin=8,cs1_pin=22"

LDCへの表示

確認コード

LCDの画面全体を "赤” ”緑” ”青” に塗り潰すコードです。

lcd_test.py python
import time import RPi.GPIO as GPIO import spidev # ========================================== # ピン配置設定 (BCM番号) # ========================================== LED_PIN = 23 DC_PIN = 24 # Data/Command 切替ピン RST_PIN = 25 # リセットピン CS_PIN = 8 # 液晶用CSピン # 画面サイズ WIDTH = 240 HEIGHT = 320 # RPi.GPIOの設定 GPIO.setwarnings(False) GPIO.setmode(GPIO.BCM) GPIO.setup(DC_PIN, GPIO.OUT) GPIO.setup(RST_PIN, GPIO.OUT) GPIO.setup(CS_PIN, GPIO.OUT) GPIO.setup(LED_PIN, GPIO.OUT) GPIO.output(LED_PIN, GPIO.HIGH) # SPIの初期化 (Bus 0, Device 0) spi = spidev.SpiDev() spi.open(0, 0) spi.max_speed_hz = 16000000 # 16MHz spi.mode = 0 def send_command(cmd): GPIO.output(DC_PIN, GPIO.LOW) # DC=0: コマンド GPIO.output(CS_PIN, GPIO.LOW) spi.writebytes([cmd]) GPIO.output(CS_PIN, GPIO.HIGH) def send_data(data): GPIO.output(DC_PIN, GPIO.HIGH) # DC=1: データ GPIO.output(CS_PIN, GPIO.LOW) if isinstance(data, list): spi.writebytes(data) else: spi.writebytes([data]) GPIO.output(CS_PIN, GPIO.HIGH) def reset_display(): GPIO.output(RST_PIN, GPIO.HIGH) time.sleep(0.05) GPIO.output(RST_PIN, GPIO.LOW) time.sleep(0.05) GPIO.output(RST_PIN, GPIO.HIGH) time.sleep(0.15) def init_ili9341(): print("ILI9341を初期化中...") reset_display() # ILI9341の標準的な初期化シーケンス send_command(0xEF) send_data([0x03, 0x80, 0x02]) send_command(0xCF) send_data([0x00, 0xC1, 0x30]) send_command(0xED) send_data([0x64, 0x03, 0x12, 0x81]) send_command(0xE8) send_data([0x85, 0x00, 0x78]) send_command(0xCB) send_data([0x39, 0x2C, 0x00, 0x34, 0x02]) send_command(0xF7) send_data([0x20]) send_command(0xEA) send_data([0x00, 0x00]) send_command(0xC0) send_data([0x23]) send_command(0xC1) send_data([0x10]) send_command(0xC5) send_data([0x3E, 0x28]) send_command(0xC7) send_data([0x86]) send_command(0x36) # Memory Access Control (向き設定) send_data([0x48]) # 横向き send_command(0x3A) # Pixel Format Set send_data([0x55]) # 16bit / pixel (RGB565) send_command(0x11) # Sleep Out time.sleep(0.12) send_command(0x29) # Display ON time.sleep(0.05) print("初期化完了") def set_window(x0, y0, x1, y1): send_command(0x2A) send_data([x0 >> 8, x0 & 0xFF, x1 >> 8, x1 & 0xFF]) send_command(0x2B) send_data([y0 >> 8, y0 & 0xFF, y1 >> 8, y1 & 0xFF]) send_command(0x2C) def fill_color(color_high, color_low): """ 画面全体を指定したRGB565の色で塗りつぶす """ set_window(0, 0, WIDTH - 1, HEIGHT - 1) GPIO.output(DC_PIN, GPIO.HIGH) GPIO.output(CS_PIN, GPIO.LOW) # spidevの制限(4096バイト)を回避するためブロックごとに分割送信 block_pixels = 512 color_data = [color_high, color_low] * block_pixels total_pixels = WIDTH * HEIGHT sent_pixels = 0 while sent_pixels < total_pixels: chunk = min(block_pixels, total_pixels - sent_pixels) if chunk == block_pixels: spi.writebytes(color_data) else: spi.writebytes([color_high, color_low] * chunk) sent_pixels += chunk GPIO.output(CS_PIN, GPIO.HIGH) def main(): try: init_ili9341() print("赤色で塗りつぶします") fill_color(0xF8, 0x00) # 赤 (Red: 0xF800) time.sleep(2) print("緑色で塗りつぶします") fill_color(0x07, 0xE0) # 緑 (Green: 0x07E0) time.sleep(2) print("青色で塗りつぶします") fill_color(0x00, 0x1F) # 青 (Blue: 0x001F) time.sleep(2) except KeyboardInterrupt: print("\n終了します") finally: spi.close() GPIO.cleanup() if __name__ == "__main__": main()
  • 8~11行:制御用ピンの定義
  • 25行:バックライトオン。画面が光ります。
  • 28~31行:SPIの設定。
    • MAX速度は24MHz位。余裕を見て16MHzに設定
    • LCD用には転送速度の速い "spi.open(0, 0)" を使用します。
    • モードは ”0”を使用。
  • 34~38行:LCDにコマンドを送信
  • 41~48行:LCDにデータを送信
  • 60行から:LCDの初期化
    • 62行:LCDをリセット
    • 65行から:ILI9341の標準的な初期化シーケンス
      • 88,89行:LCDの向きを設定。今回は横向き(0x48)
      • 94,97行:Sleep解除とDisplayオン。
  • 136行から:これがメインプログラム。画面を 赤 緑 青 に塗り潰していいます。

PIL (Python Imaging Library)

描画用のライブラリーとして "PIL" を使います。このライブラリーを使うと点、線、図形、文字など を簡単に描くことが出来ます。使い方は、以下の通り。

  1. ベースとなるLCDの大きさと同じ画像を用意
  2. ImageDraw.Draw() を使って描画オブジェクトを作成
  3. 線、図形、文字などを描画するメソッドを実行

出来上がった画像を丸ごとLCDに転送し表示させます。下記は描画のサンプルプログラム

pil_test.py python
import time import RPi.GPIO as GPIO import spidev # ========================================== # ピン配置設定 (BCM番号) # ========================================== LED_PIN = 23 DC_PIN = 24 # Data/Command 切替ピン RST_PIN = 25 # リセットピン CS_PIN = 8 # 液晶用CSピン (GPIO 8 = CE0) # 画面サイズ WIDTH = 320 HEIGHT = 240 # RPi.GPIOの設定 GPIO.setwarnings(False) GPIO.setmode(GPIO.BCM) GPIO.setup(DC_PIN, GPIO.OUT) GPIO.setup(RST_PIN, GPIO.OUT) GPIO.setup(CS_PIN, GPIO.OUT) GPIO.setup(LED_PIN, GPIO.OUT) GPIO.output(LED_PIN, GPIO.HIGH) # SPIの初期化 (Bus 0, Device 0) spi = spidev.SpiDev() spi.open(0, 0) spi.max_speed_hz = 16000000 # 16MHz spi.mode = 0 def send_command(cmd): GPIO.output(DC_PIN, GPIO.LOW) # DC=0: コマンド GPIO.output(CS_PIN, GPIO.LOW) spi.writebytes([cmd]) GPIO.output(CS_PIN, GPIO.HIGH) def send_data(data): GPIO.output(DC_PIN, GPIO.HIGH) # DC=1: データ GPIO.output(CS_PIN, GPIO.LOW) if isinstance(data, list): spi.writebytes(data) else: spi.writebytes([data]) GPIO.output(CS_PIN, GPIO.HIGH) def reset_display(): GPIO.output(RST_PIN, GPIO.HIGH) time.sleep(0.05) GPIO.output(RST_PIN, GPIO.LOW) time.sleep(0.05) GPIO.output(RST_PIN, GPIO.HIGH) time.sleep(0.15) def init_ili9341(): print("ILI9341を初期化中...") reset_display() # ILI9341の標準的な初期化シーケンス send_command(0xEF) send_data([0x03, 0x80, 0x02]) send_command(0xCF) send_data([0x00, 0xC1, 0x30]) send_command(0xED) send_data([0x64, 0x03, 0x12, 0x81]) send_command(0xE8) send_data([0x85, 0x00, 0x78]) send_command(0xCB) send_data([0x39, 0x2C, 0x00, 0x34, 0x02]) send_command(0xF7) send_data([0x20]) send_command(0xEA) send_data([0x00, 0x00]) send_command(0xC0) send_data([0x23]) send_command(0xC1) send_data([0x10]) send_command(0xC5) send_data([0x3E, 0x28]) send_command(0xC7) send_data([0x86]) send_command(0x36) # Memory Access Control (向き設定) send_data([0x28]) # 横向き send_command(0x3A) # Pixel Format Set send_data([0x55]) # 16bit / pixel (RGB565) send_command(0x11) # Sleep Out time.sleep(0.12) send_command(0x29) # Display ON time.sleep(0.05) print("初期化完了") def set_window(x0, y0, x1, y1): send_command(0x2A) send_data([x0 >> 8, x0 & 0xFF, x1 >> 8, x1 & 0xFF]) send_command(0x2B) send_data([y0 >> 8, y0 & 0xFF, y1 >> 8, y1 & 0xFF]) send_command(0x2C) def display_image(image): """PillowのImageオブジェクトをRGB565に変換してILI9341に転送する""" set_window(0, 0, WIDTH - 1, HEIGHT - 1) pixels = image.getdata() buf = bytearray() for r, g, b in pixels: color16 = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3) buf.append(color16 >> 8) buf.append(color16 & 0xFF) GPIO.output(DC_PIN, GPIO.HIGH) GPIO.output(CS_PIN, GPIO.LOW) chunk_size = 4096 for i in range(0, len(buf), chunk_size): spi.writebytes(list(buf[i : i + chunk_size])) GPIO.output(CS_PIN, GPIO.HIGH) from PIL import Image, ImageDraw, ImageFont def main(): # LCDの初期化 init_ili9341() # 1. ベース(幅320 x 高さ240)を作成 img = Image.new("RGB", (WIDTH, HEIGHT), color="black") # 2. 描画オブジェクトを作成 draw = ImageDraw.Draw(img) # 3. 描画の例 # ① 四角形を描く (左上x, 左上y, 右下x, 右下y) draw.rectangle([40, 40, 120, 120], outline="blue", fill="lightblue", width=3) # ② 円(楕円)を描く (左上x, 左上y, 右下x, 右下y) draw.ellipse([160, 40, 240, 120], outline="red", fill="pink", width=3) # ③ 直線を引く (始点x, 始点y, 終点x, 終点y) draw.line([40, 170, 280, 170], fill="green", width=5) # ④ 文字を書く (x座標, y座標, テキスト, 文字の色) draw.text((40, 190), "Hello, Pillow!", fill="white") # LCDに画像を転送し表示 display_image(img) if __name__ == "__main__": main()

133行以下が描画部分。

  • 135行:LCDの初期化
  • 138行:ベースを作成
  • 141行:オブジェクトを作成
  • 143~154行:描画
  • 157行:画像データをLCDに転送
    • 110行:画像データをLCDに転送する関数
    • 112行:表示範囲の設定
    • 115~119行:色データをLCDのフォーマットに合わせて変換
    • それ以降:データの転送

コードを実行するとLCDに下記の画像が表示されます。

PIL sample result

LCD制御基本機能とSPIパラメータの微調整

    pil_test_1.py python
    import time import RPi.GPIO as GPIO import spidev # ========================================== # ピン配置設定 (BCM番号) # ========================================== LED_PIN = 23 DC_PIN = 24 # Data/Command 切替ピン RST_PIN = 25 # リセットピン CS_PIN = 8 # 液晶用CSピン (GPIO 8 = CE0) # 画面サイズ WIDTH = 320 HEIGHT = 240 # RPi.GPIOの設定 GPIO.setwarnings(False) GPIO.setmode(GPIO.BCM) GPIO.setup(DC_PIN, GPIO.OUT) GPIO.setup(RST_PIN, GPIO.OUT) GPIO.setup(CS_PIN, GPIO.OUT) GPIO.setup(LED_PIN, GPIO.OUT) GPIO.output(LED_PIN, GPIO.HIGH) # SPIの初期化 (Bus 0, Device 0) spi = spidev.SpiDev() spi.open(0, 0) spi.max_speed_hz = 24000000 # 24MHz spi.mode = 0 def send_command(cmd): GPIO.output(DC_PIN, GPIO.LOW) # DC=0: コマンド GPIO.output(CS_PIN, GPIO.LOW) spi.writebytes([cmd]) GPIO.output(CS_PIN, GPIO.HIGH) def send_data(data): GPIO.output(DC_PIN, GPIO.HIGH) # DC=1: データ GPIO.output(CS_PIN, GPIO.LOW) if not isinstance(data, list): data = [data] # spidevの制限(4096バイト)を回避するため、4096件ずつ分割して送信する chunk_size = 4096 for i in range(0, len(data), chunk_size): chunk = data[i:i + chunk_size] spi.writebytes(chunk) GPIO.output(CS_PIN, GPIO.HIGH) def reset_display(): GPIO.output(RST_PIN, GPIO.HIGH) time.sleep(0.05) GPIO.output(RST_PIN, GPIO.LOW) time.sleep(0.05) GPIO.output(RST_PIN, GPIO.HIGH) time.sleep(0.15) def set_rotation(rotation): """画面の向き(回転・反転)を設定する関数 0: 縦向き (Portrait) 1: 横向き (Landscape - 今回の向き) 2: 縦向き・逆さ (Portrait Reverse) 3: 横向き・逆さ (Landscape Reverse) """ global WIDTH, HEIGHT send_command(0x36) # Memory Access Control if rotation == 0: send_data([0x48]) WIDTH, HEIGHT = 240, 320 elif rotation == 1: send_data([0x28]) # 横向き (MX=0, MY=1, MV=1) WIDTH, HEIGHT = 320, 240 elif rotation == 2: send_data([0x88]) WIDTH, HEIGHT = 240, 320 elif rotation == 3: send_data([0xE8]) # 横向き・逆さ WIDTH, HEIGHT = 320, 240 def init_ili9341(): print("ILI9341を初期化中...") reset_display() # ILI9341の標準的な初期化シーケンス send_command(0xEF) send_data([0x03, 0x80, 0x02]) send_command(0xCF) send_data([0x00, 0xC1, 0x30]) send_command(0xED) send_data([0x64, 0x03, 0x12, 0x81]) send_command(0xE8) send_data([0x85, 0x00, 0x78]) send_command(0xCB) send_data([0x39, 0x2C, 0x00, 0x34, 0x02]) send_command(0xF7) send_data([0x20]) send_command(0xEA) send_data([0x00, 0x00]) send_command(0xC0) send_data([0x23]) send_command(0xC1) send_data([0x10]) send_command(0xC5) send_data([0x3E, 0x28]) send_command(0xC7) send_data([0x86]) send_command(0x36) # Memory Access Control (向き設定) send_data([0x28]) # 横向き send_command(0x3A) # Pixel Format Set send_data([0x55]) # 16bit / pixel (RGB565) send_command(0x11) # Sleep Out time.sleep(0.12) send_command(0x29) # Display ON time.sleep(0.05) print("初期化完了") def back_light(sw): GPIO.output(LED_PIN, sw) def write_data(pixel_data): if isinstance(pixel_data, (bytes, bytearray)): pixel_data = list(pixel_data) send_data(pixel_data) def set_window(x0, y0, x1, y1): send_command(0x2A) send_data([x0 >> 8, x0 & 0xFF, x1 >> 8, x1 & 0xFF]) send_command(0x2B) send_data([y0 >> 8, y0 & 0xFF, y1 >> 8, y1 & 0xFF]) send_command(0x2C) def display_image(image): """PillowのImageオブジェクトをRGB565に変換してILI9341に転送する""" set_window(0, 0, WIDTH - 1, HEIGHT - 1) pixels = image.getdata() buf = bytearray() for r, g, b in pixels: color16 = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3) buf.append(color16 >> 8) buf.append(color16 & 0xFF) GPIO.output(DC_PIN, GPIO.HIGH) GPIO.output(CS_PIN, GPIO.LOW) chunk_size = 4096 for i in range(0, len(buf), chunk_size): spi.writebytes(list(buf[i : i + chunk_size])) GPIO.output(CS_PIN, GPIO.HIGH) from PIL import Image, ImageDraw, ImageFont def main(): init_ili9341() set_rotation(0) img = Image.new("RGB", (WIDTH, HEIGHT), color="black") draw = ImageDraw.Draw(img) draw.rectangle([40, 40, 120, 120], outline="blue", fill="lightblue", width=3) draw.ellipse([160, 40, 240, 120], outline="red", fill="pink", width=3) draw.line([40, 170, 280, 170], fill="green", width=5) draw.text((40, 190), "Hello, Pillow!", fill="white") small_box = (250, 60, 290, 100) draw.ellipse(small_box, outline="black", fill="red", width=2) # LCDに画像を転送し表示 display_image(img) # 1秒ごとに小さい丸の色を赤・青に切り替えて部分更新するループ print("小さい丸の色を点滅させます(Ctrl+Cで終了)") try: while True: for color_name in ["red", "blue"]: # 小さい丸の部分だけを描画する小さなPillow画像(幅40, 高さ40)を作成 box_width = small_box[2] - small_box[0] box_height = small_box[3] - small_box[1] sub_img = Image.new("RGB", (box_width, box_height), color="black") sub_draw = ImageDraw.Draw(sub_img) sub_draw.ellipse([0, 0, box_width - 1, box_height - 1], outline=color_name, fill=color_name) # ピクセルデータをRGB565のバイト列に変換 buf = bytearray() for r, g, b in sub_img.getdata(): color16 = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3) buf.append(color16 >> 8) buf.append(color16 & 0xFF) # 書き換える領域のウィンドウを指定 (ILI9341の座標系に合わせる) set_window(small_box[0], small_box[1], small_box[2] - 1, small_box[3] - 1) # 指定の write_data 関数を使って部分送信 write_data(buf) time.sleep(1.0) except KeyboardInterrupt: print("終了します") finally: GPIO.cleanup() if __name__ == "__main__": main()
  • LCD制御基本機能リスト
  • 関数行説明
    WIDTH = 320, HEIGHT = 24014,15解像度の設定
    send_command(cmd)34コマンドの送信
    send_data(data)41データの送信
    reset_display()57リセット
    set_rotation(rotation)66画面の向き(回転・反転)を設定
    init_ili9341()90初期化
    back_light(sw)133バックライトのオンオフ
    write_data(pixel_data)137部分画面更新
    set_window(x0, y0, x1, y1)144更新範囲指定
    display_image(image)152全画面更新
  • SPIの速度も24MHzに上げて見ました。
    • 30行:spi.max_speed_hz = 24000000
  • 175行:部分更新のサンプルプログラム。
    • 189行以前:初期描画を全画で作成しLCDに転送し表示。
    • 193行以降:丸の色を変えています。
      • 199~201行:40x40の領域を確保しそこに直径40の小丸を描画。
      • 211,214行:更新部分を指定し、その部分のみ更新。
    • 小丸の色が一秒間隔で変わります。
    • 部分変更なので色が変わる時間はかなり短い。これくらいなら許容範囲内です。
  • PIL sample result with partial modification

Pillow 描画関数

Pillow 描画関数(一部)を下記にまとめます

  • 240×320サイズ、背景色黒(RGB: 0,0,0)の画像を作成
    • image = Image.new("RGB", (240, 320), color=(0, 0, 0))
  • 点(ピクセル)を描画する
    • draw.point([(100, 100)], fill="green")
  • 直線を描画する
    • draw.line([(0, 0), (200, 200)], fill="yellow", width=2)
  • 四角形を描画・塗りつぶす
    • draw.rectangle(xy, fill=None, outline=None, width=1)
      • xy: 四角形の範囲を指定。[(左上X, 左上Y), (右下X, 右下Y)] タプルのリスト(またはタプル)
      • fill: 内部の塗りつぶし色を指定します(RGBタプルや色名など)。
      • outline: 枠線の色を指定します。
      • width: 枠線の太さを指定します(デフォルトは1)
  • 指定した外接する長方形の範囲内に、円または楕円を描画
    • draw.ellipse([(50, 50), (100, 100)], fill=(255, 0, 0))
      • xy: [(x0, y0), (x1, y1)](左上と右下の座標)
      • fill: 塗りつぶしの色(RGBタプル、カラー名など)
      • outline: 枠線の色
      • width: 枠線の太さ
  • 複数点を結んだ多角形を描画する
    • draw.polygon([(50, 10), (10, 90), (90, 90)], fill="purple")
  • フォントの指定(システムにあるフォントファイルパスを指定)
    • font = ImageFont.truetype("/usr/share/fonts/truetype/fonts-japanese-gothic.ttf", 18)
  • 文字を描画 (X座標, Y座標, 文字列, 色, フォント)
    • draw.text((20, 20), "こんにちは", fill=(255, 255, 255), font=font)
  • 画像をファイルとして保存する場合
    • image.save("output.png")
  • ピクセルデータのリストを取得する場合(LCD転送用)
    • pixels = image.getdata()
  • 画像ファイルの表示
    • 画像ファイルを読み込む
      • image_path = "sample.jpg"
      • image = Image.open(image_path)
    • リサイズ(必要に応じて)
      • image = image.resize((WIDTH, HEIGHT))
    • 画像に追加描画する場合(四角を追加)
      • draw = ImageDraw.Draw(image)
      • box_coords = [50, 50, 270, 190]
      • draw.rectangle(box_coords, outline="red", width=3)
    • LCDに書き込み
      • display_image(image):

タッチセンサ

確認コード

画面にタッチした位置に対応したX,Yの値が表示されればOKです。

touch.py python
import spidev import RPi.GPIO as GPIO # タッチ用として SPI0 のデバイス1を開く spi_touch = spidev.SpiDev() spi_touch.open(0, 1) spi_touch.max_speed_hz = 2000000 # タッチパネル用の速度(約2MHz) spi_touch.mode = 0 INT_PIN = 27 # タッチセンサ割り込みピン # タッチセンサ割り込みピンを入力に設定 GPIO.setwarnings(False) GPIO.setmode(GPIO.BCM) GPIO.setup(INT_PIN, GPIO.IN) def read_touch(): if GPIO.input(INT_PIN) == GPIO.HIGH: return 0, 0 # Y座標の取得 resp_y = spi_touch.xfer2([0xD0, 0x00, 0x00]) raw_y = ((resp_y[1] << 8) | resp_y[2]) >> 3 # X座標の取得 resp_x = spi_touch.xfer2([0x90, 0x00, 0x00]) raw_x = ((resp_x[1] << 8) | resp_x[2]) >> 3 return raw_x, raw_y # テスト用ループ if __name__ == "__main__": print("タッチセンサー読み取り開始...") try: while True: x, y = read_touch() # 値が取れているか確認 if x > 0 and y > 0: print(f"X: {x}, Y: {y}") import time time.sleep(0.05) except KeyboardInterrupt: spi_touch.close() print("終了しました")
  • 5~8行:タッチセンサー用SPIの設定
    • タッチセンサー用SPIとして "spi_touch.open(0, 1)" を使用
    • 転送速度は "2MHz"。速度を上げると正しく動きません。
    • モードは "0"
  • 15行:タッチセンサ割り込みポートを入力に設定
  • 17行:画面にタッチした座標を返すコード
    • spi_touch.xfer2()は画面にタッチしていなくても何かしらの値が帰って来ます。
    • 割り込みピンの状態を確認して下記の様に対応しています。
      • 有効(Low):座標の読み込み。
      • 無効(High):無効(0,0)を返す
  • 34行以降:メインプログラム。
    • LCDの画面にタッチするとモニタに座標が表示されます。
    • これはRAWデータで実際の使用にはキャリブレーションが必要です。
    • ~ $ python3 touch.py タッチセンサー読み取り開始... X: 3075, Y: 2496 X: 3107, Y: 2772 X: 2710, Y: 2043 X: 3216, Y: 1824 X: 3771, Y: 141 X: 3104, Y: 259 ^C終了しました ~ $_

キャリブレーション

先ずは下記のコードを実行して下さい。

touch_1.py python
import time import RPi.GPIO as GPIO import spidev from PIL import Image, ImageDraw, ImageFont # ========================================== # ピン配置設定 (BCM番号) # ========================================== LED_PIN = 23 # バックライト制御ピン DC_PIN = 24 # Data/Command 切替ピン RST_PIN = 25 # リセットピン CS_PIN = 8 # 液晶用CSピン INT_PIN = 27 # タッチセンサ割り込みピン # 画面サイズ WIDTH = 320 HEIGHT = 240 # ========================================== # ★ 先ほど取得した4点の実測値 # ========================================== TL_X, TL_Y = 200, 300 # 左上 TR_X, TR_Y = 3690, 300 # 右上 BR_X, BR_Y = 3690, 3820 # 右下 BL_X, BL_Y = 200, 3820 # 左下 # RPi.GPIOの設定 GPIO.setwarnings(False) GPIO.setmode(GPIO.BCM) GPIO.setup(DC_PIN, GPIO.OUT) GPIO.setup(RST_PIN, GPIO.OUT) GPIO.setup(CS_PIN, GPIO.OUT) GPIO.setup(INT_PIN, GPIO.IN) GPIO.setup(LED_PIN, GPIO.OUT) GPIO.output(LED_PIN, GPIO.HIGH) # SPI初期化 LCD spi_lcd = spidev.SpiDev() spi_lcd.open(0, 0) spi_lcd.max_speed_hz = 24000000 # 24MHz spi_lcd.mode = 0 # SPI初期化 Touch panel spi_touch = spidev.SpiDev() spi_touch.open(0, 1) spi_touch.max_speed_hz = 2000000 # 2MHz spi_touch.mode = 0 def send_command(cmd): GPIO.output(DC_PIN, GPIO.LOW) # DC=0: コマンド GPIO.output(CS_PIN, GPIO.LOW) spi_lcd.writebytes([cmd]) GPIO.output(CS_PIN, GPIO.HIGH) def send_data(data): GPIO.output(DC_PIN, GPIO.HIGH) # DC=1: データ GPIO.output(CS_PIN, GPIO.LOW) if not isinstance(data, list): data = [data] # spidevの制限(4096バイト)を回避するため、4096件ずつ分割して送信する chunk_size = 4096 for i in range(0, len(data), chunk_size): chunk = data[i:i + chunk_size] spi_lcd.writebytes(chunk) GPIO.output(CS_PIN, GPIO.HIGH) def reset_display(): GPIO.output(RST_PIN, GPIO.HIGH) time.sleep(0.05) GPIO.output(RST_PIN, GPIO.LOW) time.sleep(0.05) GPIO.output(RST_PIN, GPIO.HIGH) time.sleep(0.15) def set_rotation(rotation): """画面の向き(回転・反転)を設定する関数 0: 縦向き (Portrait) 1: 横向き (Landscape - 今回の向き) 2: 縦向き・逆さ (Portrait Reverse) 3: 横向き・逆さ (Landscape Reverse) """ global WIDTH, HEIGHT send_command(0x36) # Memory Access Control if rotation == 0: send_data([0x48]) WIDTH, HEIGHT = 240, 320 elif rotation == 1: send_data([0x28]) # 横向き (MX=0, MY=1, MV=1) WIDTH, HEIGHT = 320, 240 elif rotation == 2: send_data([0x88]) WIDTH, HEIGHT = 240, 320 elif rotation == 3: send_data([0xE8]) # 横向き・逆さ WIDTH, HEIGHT = 320, 240 def init_ili9341(): print("ILI9341を初期化中...") reset_display() # ILI9341の標準的な初期化シーケンス send_command(0xEF) send_data([0x03, 0x80, 0x02]) send_command(0xCF) send_data([0x00, 0xC1, 0x30]) send_command(0xED) send_data([0x64, 0x03, 0x12, 0x81]) send_command(0xE8) send_data([0x85, 0x00, 0x78]) send_command(0xCB) send_data([0x39, 0x2C, 0x00, 0x34, 0x02]) send_command(0xF7) send_data([0x20]) send_command(0xEA) send_data([0x00, 0x00]) send_command(0xC0) send_data([0x23]) send_command(0xC1) send_data([0x10]) send_command(0xC5) send_data([0x3E, 0x28]) send_command(0xC7) send_data([0x86]) send_command(0x36) # Memory Access Control (向き設定) send_data([0x28]) # 横向き send_command(0x3A) # Pixel Format Set send_data([0x55]) # 16bit / pixel (RGB565) send_command(0x11) # Sleep Out time.sleep(0.12) send_command(0x29) # Display ON time.sleep(0.05) print("初期化完了") def back_light(sw): GPIO.output(LED_PIN, sw) def write_data(pixel_data): if isinstance(pixel_data, (bytes, bytearray)): pixel_data = list(pixel_data) send_data(pixel_data) def set_window(x0, y0, x1, y1): send_command(0x2A) send_data([x0 >> 8, x0 & 0xFF, x1 >> 8, x1 & 0xFF]) send_command(0x2B) send_data([y0 >> 8, y0 & 0xFF, y1 >> 8, y1 & 0xFF]) send_command(0x2C) def display_image(image): """PillowのImageオブジェクトをRGB565に変換してILI9341に転送する""" set_window(0, 0, WIDTH - 1, HEIGHT - 1) pixels = image.getdata() buf = bytearray() for r, g, b in pixels: color16 = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3) buf.append(color16 >> 8) buf.append(color16 & 0xFF) GPIO.output(DC_PIN, GPIO.HIGH) GPIO.output(CS_PIN, GPIO.LOW) chunk_size = 4096 for i in range(0, len(buf), chunk_size): spi_lcd.writebytes(list(buf[i : i + chunk_size])) GPIO.output(CS_PIN, GPIO.HIGH) def read_touch_raw(): if GPIO.input(INT_PIN) == GPIO.HIGH: return 0, 0 try: resp_y = spi_touch.xfer2([0xD0, 0x00, 0x00]) raw_y = ((resp_y[1] << 8) | resp_y[2]) >> 3 resp_x = spi_touch.xfer2([0x90, 0x00, 0x00]) raw_x = ((resp_x[1] << 8) | resp_x[2]) >> 3 return raw_x, raw_y except Exception: return 0, 0 def measure_point(target_name, box_coords): print(f"\n--- 【 {target_name} 】 の赤い四角をタッチして押し続けてください ---") image = Image.new("RGB", (WIDTH, HEIGHT), "black") draw = ImageDraw.Draw(image) draw.rectangle(box_coords, fill="red") draw.text((10, 10), f"Tap: {target_name}", fill="white") display_image(image) samples_x = [] samples_y = [] while True: rx, ry = read_touch_raw() if rx > 0 and ry > 0: samples_x.append(rx) samples_y.append(ry) print(f"計測中... RAW X:{rx}, Y:{ry}") if len(samples_x) >= 20: break time.sleep(0.05) avg_x = sum(samples_x) // len(samples_x) avg_y = sum(samples_y) // len(samples_y) print(f"-> 【{target_name}】 決定! X:{avg_x}, Y:{avg_y}") time.sleep(1) return avg_x, avg_y def calibration(): print("4点キャリブレーションを開始します") try: # 1. 左上 tl_x, tl_y = measure_point("Top-Left", [10, 10, 50, 50]) # 2. 右上 tr_x, tr_y = measure_point("Top-Right", [WIDTH - 50, 10, WIDTH - 10, 50]) # 3. 右下 br_x, br_y = measure_point( "Bottom-Right", [WIDTH - 50, HEIGHT - 50, WIDTH - 10, HEIGHT - 10] ) # 4. 左下 bl_x, bl_y = measure_point("Bottom-Left", [10, HEIGHT - 50, 50, HEIGHT - 10]) print("\n================================") print(" 4点キャリブレーション測定結果 ") print("================================") print(f"TL (左上) -> X:{tl_x}, Y:{tl_y}") print(f"TR (右上) -> X:{tr_x}, Y:{tr_y}") print(f"BR (右下) -> X:{br_x}, Y:{br_y}") print(f"BL (左下) -> X:{bl_x}, Y:{bl_y}") print("================================") image = Image.new("RGB", (WIDTH, HEIGHT), "black") draw = ImageDraw.Draw(image) draw.text( (30, 140), "Measurement Finished!", fill="green" ) display_image(image) time.sleep(5) except KeyboardInterrupt: print("\n終了します") finally: spi_lcd.close() spi_touch.close() GPIO.cleanup() def map_coordinate(raw_x, raw_y): # 4隅の最小・最大値から範囲を割り出す min_x = min(TL_X, BL_X) max_x = max(TR_X, BR_X) min_y = min(TR_Y, BR_Y) max_y = max(TL_Y, BL_Y) # 0.0 〜 1.0 に正規化 fx = (raw_x - min_x) / (max_x - min_x) if (max_x - min_x) != 0 else 0 fy = (raw_y - min_y) / (max_y - min_y) if (max_y - min_y) != 0 else 0 # 座標変換 x = int(fx * WIDTH) y = int(fy * HEIGHT) # 画面内に収める x = max(0, min(x, WIDTH - 1)) y = max(0, min(y, HEIGHT - 1)) return x, y def touch_location(): raw_x, raw_y = read_touch_raw() if raw_x > 0 and raw_y > 0: px, py = map_coordinate(raw_x, raw_y) else: px = py = -1 return px,py def main(): init_ili9341() set_rotation(1) calibration() """ print("\nタッチ座標の読み取りを開始します(終了するには Ctrl+C を押してください)") image = Image.new("RGB", (WIDTH, HEIGHT), color=(255, 255, 255)) draw = ImageDraw.Draw(image) display_image(image) try: while True: px, py = touch_location() # 有効な座標(0以上)が取れている場合のみ表示 if px > -1 and py > -1: print(f"画面座標 -> X:{px}, Y:{py}") time.sleep(0.05) except KeyboardInterrupt: print("\n終了します") finally: spi_lcd.close() spi_touch.close() GPIO.cleanup() """ if __name__ == "__main__": main()
  • コードを実行すると下記の様になります。
    Calibration method of touch sencer
    • 先ず、LCDの1の位置にのみ赤い四角が表示されます。
    • 四角のコーナ(矢印が指している角)にタッチします。
    • 測定が開始されしばらくすると "完了" と表示されます。ここで一度画面からペンを外して下さい
    • 次にLCDの2の位置に赤い四角が表示されます。1と同じ操作を行います。
    • 2,3,4と操作を行うとモニタに測定結果が表示されます(上記右)。
  • これら測定結果をコードの22~25行に代入。
  • 307行をコメント。309,329行のコメントを外してコードを実行します。
  • タッチパネルにタッチするとモニタに補正されたX,Y座標が下記の様に表示されます。
      ~ $ python3 touch_1.py ILI9341を初期化中... 初期化完了 タッチ座標の読み取りを開始します(終了するには Ctrl+C を押してください) 画面座標 -> X:128, Y:70 画面座標 -> X:163, Y:101 画面座標 -> X:137, Y:100 画面座標 -> X:222, Y:110 画面座標 -> X:89, Y:165 画面座標 -> X:124, Y:151 画面座標 -> X:186, Y:131 ^C 終了します
  • 位置が若干ずれる(X,Yの値が正しく無い)場合、22~25行の値を直接いじって調整して下さい。
  • コードのデフォルト値は今回の調整後の値です。参考にして下さい。
  • 293行の "def touch_location():" がタッチした座標を返す関数です。
    • 関数を実行した時にパネルに
      • タッチしている: その位置の座標を返す。X:0~319 y:0~239
      • タッチしていない:x = y = -1 を返す。

最後に

LCDへの書き込みとタッチセンサの読み取りが出来る様になりました。タッチ座標の取得は割り込み 処理を行うのが正しい対応と思いますが、この方法でも十分機能します。自作機器の表示器とユーザインターフェイス として使用したいと思います。

P.S.: "LCDを使ってStandalone Playerを作る"でこのLCDを使った製品を 製作しています。

SINCE 2026