投稿日:2026年1月12日
前回 に引き続きWaveshare 0.71インチ DualEye LCD を検証したいと思います。
部品類はできるだけコンパクトにまとめたいと考え、Raspberry Pi Pico よりも小型のマイコンボードを探していました。
そこで見つけたのが RP2040-Zero です。
標準のPicoと同じRP2040プロセッサを搭載しているため、前回のプログラムも容易に移植できると考え、購入に至りました。
最初にお見せします。
今回作ったものはこちら▼です。

▲2つ購入しました。
サイズはこんな感じです。

▲表面。
電源はUSB Type-C。
RP2040-Zeroの品名とBOOT、RESETスイッチがあります。
GPIOピンはこのような配置になっています。
GP17〜GP25は裏面に配置されています。

▲裏面。
ラズパイのマークが刻印されているプロセッサー。
四角の基盤がGP17〜GP25とGND(接地)。

▲別途購入したピン ヘッダーを適宜に切り、はんだ付けします。

▲はんだ付け。
小さいので慎重に行います。

▲接続しました。
よりコンパクトにしたいのなら、ピンヘッダーではなく直接ケーブルをはんだ付けしても良いと思います。
Pico WHでは、BL1(左目 バックライト)をGP20に。
BL2(右目 リセット)をGP21に繋げました。
今回のRP2040-ZeroにはGP20 と GP21 がピン接続できないので、代わりにGP0 と GP1 を使用します。
改修したMicroPythonになります。
import time
import math
import random
import framebuf
from machine import Pin, SPI
# ==========================================
# 1. GC9D01 ドライバ
# ==========================================
class GC9D01:
def __init__(self, spi, dc_pin, cs_pin, rst_pin, width=160, height=160, rotation=0):
self.spi = spi
self.dc = Pin(dc_pin, Pin.OUT)
self.cs = Pin(cs_pin, Pin.OUT)
self.rst = Pin(rst_pin, Pin.OUT)
self.width = width
self.height = height
self._rotation = rotation
self.init_display()
def write_cmd(self, cmd, data=None):
self.cs(0)
self.dc(0)
self.spi.write(bytearray([cmd]))
if data:
self.dc(1)
self.spi.write(bytearray(data))
self.cs(1)
def write_data(self, data):
self.cs(0)
self.dc(1)
self.spi.write(data)
self.cs(1)
def reset(self):
self.rst(1)
time.sleep(0.01)
self.rst(0)
time.sleep(0.1)
self.rst(1)
time.sleep(0.1)
def init_display(self):
self.reset()
self.write_cmd(0xFE)
self.write_cmd(0xEF)
self.write_cmd(0x80, b'\xFF')
self.write_cmd(0x81, b'\xFF')
self.write_cmd(0x82, b'\xFF')
self.write_cmd(0x83, b'\xFF')
self.write_cmd(0x84, b'\xFF')
self.write_cmd(0x85, b'\xFF')
self.write_cmd(0x86, b'\xFF')
self.write_cmd(0x87, b'\xFF')
self.write_cmd(0x88, b'\xFF')
self.write_cmd(0x89, b'\xFF')
self.write_cmd(0x8A, b'\xFF')
self.write_cmd(0x8B, b'\xFF')
self.write_cmd(0x8C, b'\xFF')
self.write_cmd(0x8D, b'\xFF')
self.write_cmd(0x8E, b'\xFF')
self.write_cmd(0x8F, b'\xFF')
self.write_cmd(0x3A, b'\x05')
self.write_cmd(0xEC, b'\x01')
self.write_cmd(0x74, b'\x02\x0E\x00\x00\x00\x00\x00')
self.write_cmd(0x98, b'\x3E')
self.write_cmd(0x99, b'\x3E')
self.write_cmd(0xB5, b'\x0D\x0D')
self.write_cmd(0x60, b'\x38\x0F\x79\x67')
self.write_cmd(0x61, b'\x38\x11\x79\x67')
self.write_cmd(0x64, b'\x38\x17\x71\x5F\x79\x67')
self.write_cmd(0x65, b'\x38\x13\x71\x5B\x79\x67')
self.write_cmd(0x6A, b'\x00\x00')
self.write_cmd(0x6C, b'\x22\x02\x22\x02\x22\x22\x50')
self.write_cmd(0x6E, b'\x03\x03\x01\x01\x00\x00\x0f\x0f\x0d\x0d\x0b\x0b\x09\x09\x00\x00\x00\x00\x0a\x0a\x0c\x0c\x0e\x0e\x10\x10\x00\x00\x02\x02\x04\x04')
self.write_cmd(0xBF, b'\x01')
self.write_cmd(0xF9, b'\x40')
self.write_cmd(0x9B, b'\x3B')
self.write_cmd(0x93, b'\x33\x7F\x00')
self.write_cmd(0x7E, b'\x30')
self.write_cmd(0x70, b'\x0D\x02\x08\x0D\x02\x08')
self.write_cmd(0x71, b'\x0D\x02\x08')
self.write_cmd(0x91, b'\x0E\x09')
self.write_cmd(0xC3, b'\x1F')
self.write_cmd(0xC4, b'\x1F')
self.write_cmd(0xC9, b'\x1F')
self.write_cmd(0xF0, b'\x53\x15\x0A\x04\x00\x3E')
self.write_cmd(0xF2, b'\x53\x15\x0A\x04\x00\x3A')
self.write_cmd(0xF1, b'\x56\xA8\x7F\x33\x34\x5F')
self.write_cmd(0xF3, b'\x52\xA4\x7F\x33\x34\xDF')
self.set_rotation(self._rotation)
self.write_cmd(0x3A, b'\x05')
self.write_cmd(0xB0, b'\x00')
self.write_cmd(0xB1, b'\x00\x00')
self.write_cmd(0xB4, b'\x00')
self.write_cmd(0x11)
time.sleep(0.2)
self.write_cmd(0x29)
self.write_cmd(0x2C)
def set_rotation(self, rotation):
self._rotation = rotation % 4
rotations = [0xC8, 0x68, 0x08, 0xA8]
self.write_cmd(0x36, bytearray([rotations[self._rotation]]))
def set_window(self, x0, y0, x1, y1):
self.write_cmd(0x2A, bytearray([x0 >> 8, x0 & 0xFF, x1 >> 8, x1 & 0xFF]))
self.write_cmd(0x2B, bytearray([y0 >> 8, y0 & 0xFF, y1 >> 8, y1 & 0xFF]))
self.write_cmd(0x2C)
def display_buffer(self, buffer):
self.set_window(0, 0, self.width - 1, self.height - 1)
self.write_data(buffer)
def fill_screen(self, color):
high = (color >> 8) & 0xFF
low = color & 0xFF
buffer = bytearray([high, low] * 160)
self.set_window(0, 0, self.width - 1, self.height - 1)
for _ in range(self.height):
self.write_data(buffer)
# ==========================================
# 2. 目玉クラス
# ==========================================
class Eye:
def __init__(self, display, shared_buffer, cx, cy, gaze_offset_x=0.0):
self.tft = display
self.buffer = shared_buffer
self.fb = framebuf.FrameBuffer(self.buffer, 160, 160, framebuf.RGB565)
self.cx = cx
self.cy = cy
self.pupil_w = 48
self.pupil_h = 58
self.gaze_offset_x = gaze_offset_x
self.cur_x = 0
self.cur_y = 0
self.WHITE = 0xFFFF
self.BLACK = 0x0000
def draw_filled_ellipse(self, cx, cy, rx, ry, color):
"""塗りつぶされた楕円を描画する"""
for dy in range(-ry, ry + 1):
if ry == 0: break
ratio = (dy / ry) ** 2
if ratio >= 1.0: continue
w = int(rx * math.sqrt(1.0 - ratio))
self.fb.fill_rect(cx - w, cy + dy, w * 2, 1, color)
def update(self, target_x, target_y, eyelid_h=0):
# 視線計算
tx = target_x + self.gaze_offset_x
self.cur_x += (tx - self.cur_x) * 0.2
self.cur_y += (target_y - self.cur_y) * 0.2
# --- 黒目の位置計算 ---
# 1. まず外側に「めいっぱい」動けるようにマージンを大きくマイナスにする
margin_x = -35
margin_y = -10
max_move_x = (160 // 2) - self.pupil_w - margin_x
max_move_y = (160 // 2) - self.pupil_h - margin_y
px = int(self.cx + self.cur_x * max_move_x)
py = int(self.cy + self.cur_y * max_move_y)
# 2. 【ここが修正点】内側(寄り目)に行き過ぎないように制限(クランプ)する
# 内側への移動許容量 (中心から何ピクセルまで寄っていいか)
inward_limit = 10
if self.gaze_offset_x > 0:
# 左目 (Left Display) の場合
# 画面右方向(+X)が「内側/鼻側」
if px > self.cx + inward_limit:
px = self.cx + inward_limit
else:
# 右目 (Right Display) の場合
# 画面左方向(-X)が「内側/鼻側」
if px < self.cx - inward_limit:
px = self.cx - inward_limit
# --- 描画 ---
# 1. 白目 (全画面)
self.fb.fill(self.WHITE)
# 2. 黒目 (楕円)
self.draw_filled_ellipse(px, py, self.pupil_w, self.pupil_h, self.BLACK)
# 3. キャッチライト
catch_r = 8
catch_x = px - 18
catch_y = py - 18
self.draw_filled_ellipse(catch_x, catch_y, catch_r, catch_r, self.WHITE)
# 4. まばたき (白いまぶた + 黒い線)
if eyelid_h > 0:
h_int = int(eyelid_h)
self.fb.fill_rect(0, 0, 160, h_int, self.WHITE)
line_y = h_int
if line_y >= 160:
line_y = 159
self.fb.hline(0, line_y, 160, self.BLACK)
# 転送
self.tft.display_buffer(self.buffer)
# ==========================================
# 3. メイン処理
# ==========================================
def main():
SCK = 10
MOSI = 11
DC = 8
CS_LEFT = 9
RST_LEFT = 12
CS_RIGHT = 13
RST_RIGHT = 15
Pin(0, Pin.OUT).value(1)
Pin(1, Pin.OUT).value(1)
spi = SPI(1, baudrate=60000000, sck=Pin(SCK), mosi=Pin(MOSI))
# 初期化
tft_left = GC9D01(spi, dc_pin=DC, cs_pin=CS_LEFT, rst_pin=RST_LEFT)
tft_right = GC9D01(spi, dc_pin=DC, cs_pin=CS_RIGHT, rst_pin=RST_RIGHT)
tft_left.set_rotation(1)
tft_right.set_rotation(3)
tft_left.fill_screen(0x0000)
tft_right.fill_screen(0x0000)
draw_buffer = bytearray(160 * 160 * 2)
offset_in = 15
offset_down = 10
cross_eye_val = 0.40
eye_l = Eye(tft_left, draw_buffer,
cx=80 + offset_in, cy=80 + offset_down,
gaze_offset_x=cross_eye_val)
eye_r = Eye(tft_right, draw_buffer,
cx=80 - offset_in, cy=80 + offset_down,
gaze_offset_x=-cross_eye_val)
next_move = 0
tx, ty = 0, 0
# まばたき管理変数
blink_state = 0
blink_h = 0
blink_count = 0 # 連続まばたきの残り回数
next_blink = time.ticks_add(time.ticks_ms(), 1000)
while True:
now = time.ticks_ms()
# 視線移動
if now > next_move:
# 大きく動かす設定(外側へ行くための設定)
tx = (random.random() * 2.0) - 1.0
ty = (random.random() * 2.0) - 1.0
next_move = now + random.randint(500, 2500)
if random.random() > 0.7:
tx, ty = 0, 0
# --- ダブルブリンク制御ロジック ---
if blink_state == 0:
if now > next_blink:
blink_state = 1
blink_count = 2
elif blink_state == 1:
blink_h += 320
if blink_h >= 160:
blink_h = 160
blink_state = 2
next_blink = now + 30
elif blink_state == 2:
if now > next_blink:
blink_state = 3
elif blink_state == 3:
blink_h -= 320
if blink_h <= 0:
blink_h = 0
blink_count -= 1
if blink_count > 0:
blink_state = 4
next_blink = now + 80
else:
blink_state = 0
next_blink = now + random.randint(1000, 3000)
elif blink_state == 4:
if now > next_blink:
blink_state = 1
# 描画
eye_l.update(tx, ty, eyelid_h=blink_h)
eye_r.update(tx, ty, eyelid_h=blink_h)
if __name__ == "__main__":
main()▲230、231行目がGP0、GP1への接続の記述。
その他は前回と同じです。
LCDピンとRP2040-Zeroのピンの配置は下表になります。
| LCDピン | RP2040-Zeroの接続先 | 役割 |
|---|---|---|
| VCC | 3V3 | 電源 |
| GND | GND | 接地 |
| DIN | GP11 | SPIデータ |
| CLK | GP10 | SPIクロック |
| CS1 | GP9 | 左目 選択 |
| CS2 | GP13 | 右目 選択 |
| DC | GP8 | コマンド制御 |
| RST1 | GP12 | 左目 リセット |
| RST2 | GP15 | 右目 バックライト |
| BL1 | GP0 | 左目 バックライト |
| BL2 | GP1 | 右目 リセット |
再度お見せします。
今回作ったものはこちら▼です。
現在Blender を習得中です。
下図のようなキャラクターを作っており、上手くできたら3Dプリンターを購入し、今回のDualEyeモジュールを組み込んでみようと思っています。
できればAI機能を搭載し、会話しながら踊ったりさせたいのですが、もう少し先になりそうです。
がんばって、世の中を「あっ!」と驚かせるようなものを作りたいと思います。
最後まで読んでいただき、ありがとうございました。

▲Blenderで制作中のキャラクター。








