概要
以前ILI9341_240_320 2.5" LCD で LCDをESP32で制御する例を紹介しました。今回は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の画面全体を "赤” ”緑” ”青” に塗り潰すコードです。
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" を使います。このライブラリーを使うと点、線、図形、文字など を簡単に描くことが出来ます。使い方は、以下の通り。
- ベースとなるLCDの大きさと同じ画像を用意
- ImageDraw.Draw() を使って描画オブジェクトを作成
- 線、図形、文字などを描画するメソッドを実行
出来上がった画像を丸ごとLCDに転送し表示させます。下記は描画のサンプルプログラム
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に下記の画像が表示されます。
LCD制御基本機能とSPIパラメータの微調整
- LCD制御基本機能リスト
- SPIの速度も24MHzに上げて見ました。
- 30行:spi.max_speed_hz = 24000000
- 175行:部分更新のサンプルプログラム。
- 189行以前:初期描画を全画で作成しLCDに転送し表示。
- 193行以降:丸の色を変えています。
- 199~201行:40x40の領域を確保しそこに直径40の小丸を描画。
- 211,214行:更新部分を指定し、その部分のみ更新。
- 小丸の色が一秒間隔で変わります。
- 部分変更なので色が変わる時間はかなり短い。これくらいなら許容範囲内です。
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()
| 関数 | 行 | 説明 |
|---|---|---|
| WIDTH = 320, HEIGHT = 240 | 14,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 | 全画面更新 |
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.rectangle(xy, fill=None, outline=None, 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)
- draw.text((20, 20), "こんにちは", fill=(255, 255, 255), font=font)
- image.save("output.png")
- 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です。
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終了しました ~ $_
キャリブレーション
先ずは下記のコードを実行して下さい。
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()
- コードを実行すると下記の様になります。
- 先ず、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を使った製品を 製作しています。