virt_flyのブログ

virt_flyのブログ

フライトシミュレーターソフトのFlightGearで仮想飛行を楽しむブログです。

RP2040 TYPE-C 16MBでTFTにキャラ画像表示

↑RP2040 TYPE-C 16MBを使い、混在する異なるサイズの画像を2インチTFTに連続表示(動画はGIF)

 

16MB使うにはCircuitpython

 

■Micropythonはフラッシュメモリーがいくらあっても2MBと見做す

 

Raspberry Pi Zeroに替えて、Picoを使ってパラパラアニメを作ろうと思ったら、画像はRAWファイルでも12~3枚が限度。無印のPicoのフラッシュメモリーが2MBしかないからです。

 

16MBあると思われるPico互換機のRP2040 TYPE-C 16MBが、以前Aliexpressで購入したまま眠っていましたので、これなら使えそうと考えました。

 

RP2040 TYPE-C 16MBボード

↑フラシュメモリーを16MBもつPico互換のRP2040 TYPE-C 16MB 眼がよく見えなくてハンダがひどいことになってます

 

早速MicropythonをインストールしてRAW画像を入れようとしたところ、12~3枚くらいで入らなくなってしまいました。これじゃPicoと同じ2MBじゃないか、騙されたのではと思ったものですが、どうやら無印Pico用のMicropythonはなべてフラッシュメモリーは2MBと見做すようになっているみたいです。

 

■専用のMicropythonをビルドする手もあるが

 

CircuitPythonを試すか、使えるMicropythonが探しても見つからないので自分でビルドしてつくる他なさそうです。専用のものを作っても管理が大変だし、ビルドは面倒でしょうから、CircuitPythonが使えたら手っ取り早く、無難でしょう。

 

Circuitpythonはあまりプログラミングしてこなかったし久しぶりでどこからダウンロードしてくればいいのかも忘れていたくらいなので、コード作成は生成AIに任せました。

 

RP2040 TYPE-C 16MBと2インチTFTディスプレイ

↑RP2040 TYPE-C 16MBでフラシュメモリー16MB を使うにはCircuitepython が手っ取り早い

 

結果は、Circuitpythonならインストールするだけで16MBを認識しすぐ使えることがわかり、またパラパラアニメも予定の28枚の画像を使って実現できました。

 

■GMT020-02専用のst7789ドライバーは無敵

 

なお、ディスプレイにはクセのある2インチTFT(GMT020-02)を使用しましたので、生成AIがコード作成にもたつきましたが、CircuitpythonとMicropythonでは違いがあるものの、以前紹介した自作専用のst7789ドライバーを示したところ、ハードの特性をつかみ、速やかにコード作成がすすみました。GMT020-02専用のst7789ドライバーはここでも貢献、無敵です。

 

RP2040でホログラム表示(GIF)

↑Pico(正確にはPico互換機)でも疑似透過ディスプレイでホログラムができました(動画はGIF) セリアのクリアキューブが各70㎜の正方体だったので目的にピッタリ

 

■混在するサイズの異なる画像を連続表示

 

愚痴ですが、ChatGPTには振り回されました。CircuitpythonはBMP画像しか扱えない、RAWも使えなくもないがBMPにしろというので用意したのに、途中からBMPはダメだRAW画像にせよと。しかも、以前作成してあったRAW画像がなぜか表示できないので、ChatGPTが用意した変換コードでRAW形式に作り替えたところ、なぜか縦長画像を生成していて、コードをどんなにいじろうともなおらない原因になっていたのです。以前作成してあったRAW画像もいつのまにか表示できるようになっていたり、他にも、Circuitpythonでは画像の回転や反転は不可能と言っていたのに、いつのまにか可能なことがまるわかりのコードを出してきたりと、相手がAIでも不信がわくというもの。

 

結局、コードはCopilotに書かせました。

 

シンプルなコードではつまらないので、RAW画像は240x240と240x320の両サイズ混在で連続表示を可能にするものとしました。そのため、大きなサイズの画像を表示した後に小さな画像を表示すると、小さくてカバーできず大きな画像の残像がはみだして覗き見苦しくなります。これを覆い隠すため、大きい画像と小さい画像との間に背景画像を挟んで表示させることにしますが、画像が変わるたびにいちいちすべて背景画像を描いていてはちらつきの元。したがって、小さい画像の表示の前に小さな画像を表示していた場合は間に背景画像は描かない、もちろん大きな画像を表示する場合にはその前の背景画像の描画は不要なので省くようにコードを記述しました。冒頭のGIFを参照ください。背景画像に歩き回るキャラクターの背景色(暗灰色)を使用したので、うまい具合にはみ出し部分の見分けはつかなくなっています。

 

import board
import busio
import digitalio
import time

# ============================
# ST7789 Driver
# ============================

class ST7789:
  def __init__(self, spi, width, height, reset, dc, cs):
    self.spi = spi
    self.width = width
    self.height = height

    self.reset = digitalio.DigitalInOut(reset)
    self.dc = digitalio.DigitalInOut(dc)
    self.cs = digitalio.DigitalInOut(cs)

    self.reset.direction = digitalio.Direction.OUTPUT
    self.dc.direction = digitalio.Direction.OUTPUT
    self.cs.direction = digitalio.Direction.OUTPUT

    self.cs.value = True
    self.dc.value = False

  def cmd(self, c):
    self.cs.value = False
    self.dc.value = False
    self.spi.write(bytes([c]))
    self.cs.value = True

  def data(self, d):     self.cs.value = False
    self.dc.value = True
    if isinstance(d, int):
      self.spi.write(bytes([d]))
    else:
      self.spi.write(d)
    self.cs.value = True

  def reset_display(self):
    self.reset.value = False
    time.sleep(0.05)
    self.reset.value = True
    time.sleep(0.05)

  def init(self):
    self.reset_display()

    self.cmd(0x11)
    time.sleep(0.12)

    self.cmd(0x36)
    self.data(0x00) #デフォルト
    #self.data(0xC0) #180度回転
    #self.data(0x40) #左右反転
    #self.data(0x80) #上下反転
    #self.data(0x20) #90度回転

    self.cmd(0x3A)
    self.data(0x55)

    self.cmd(0x21)
    self.cmd(0x29)
    time.sleep(0.02)

  def window(self, x0, y0, x1, y1):
    self.cmd(0x2A)
    self.data(x0 >> 8)
    self.data(x0 & 255)
    self.data(x1 >> 8)
    self.data(x1 & 255)

    self.cmd(0x2B)
    self.data(y0 >> 8)
    self.data(y0 & 255)
    self.data(y1 >> 8)
    self.data(y1 & 255)

    self.cmd(0x2C)


  def draw_raw_320_stream(self, filename):     # 240x320 全面表示(rowstart補正不要)
    self.window(0, 0, 239, 319)

    bufsize = 4096

    self.cs.value = False
    self.dc.value = True

    with open(filename, "rb") as f:
      while True:
        buf = f.read(bufsize)
        if not buf:
          break
        self.spi.write(buf)

    self.cs.value = True

  # 240x240 RAW を rowstart=40 に表示(ストリーミング)
  def draw_raw_240_stream(self, filename, rowstart=40):
    y0 = rowstart
    y1 = rowstart + 239

    self.window(0, y0, 239, y1)

    bufsize = 4096

    self.cs.value = False
    self.dc.value = True

    with open(filename, "rb") as f:
      while True:
        buf = f.read(bufsize)
        if not buf:
          break
        self.spi.write(buf)

    self.cs.value = True

  # 連続ループ再生(RAMを使わない)
  def play_sequence_stream(self, filelist, delay_ms=200, rowstart=40):
    while True:
      for filename in filelist:
       #self.draw_raw_240_stream(filename, rowstart=rowstart)
       self.draw_raw_320(filename)
       time.sleep(delay_ms / 1000)


# ============================
# Main Program
# ============================

spi = busio.SPI(clock=board.GP18, MOSI=board.GP19)
while not spi.try_lock():
  pass
spi.configure(baudrate=40000000, polarity=0, phase=0)

tft = ST7789(
  spi,
  240, 320, # ← パネルは 240×320
  reset=board.GP20,
  dc=board.GP16,
  cs=board.GP17
)

tft.init() # 表示したい RAW 画像のリスト files = [
  ("digit4.raw", 320),
  ("image01.raw", 240),
  ("digit3.raw", 320),
  ("image02.raw", 240),
  ("image03.raw", 240),
  ("image04.raw", 240),
  ("image05.raw", 240),
  ("digit2.raw", 320),
  ("digit1.raw", 320),
  ("digit0.raw", 320),
  ("digit_blank.raw", 320),
  ("image06.raw", 240)
]

# --- 背景を1回だけ描く ---
tft.draw_raw_320_stream("back_gray.raw")

previous_size = None

while True:
  for filename, size in files:

    # --- 背景描画の条件 ---
    # 1. 今回の画像が 240x320 の場合 → 描かない
    # 2. 今回が 240x240 で、前回が 240x320 の場合 → 描く
    # 3. 今回が 240x240 で、前回も 240x240 の場合 → 描かない(ちらつき防止)

    if size == 320:
      tft.draw_raw_320_stream(filename)
    else:
        # 前回が320 → 残像が出るので背景を描く
        tft.draw_raw_320_stream("back_gray.raw")

      # 240x240 を rowstart=40 で描く
      tft.draw_raw_240_stream(filename, rowstart=40)

    previous_size = size
    time.sleep(0.2)

このコードは、CopilotならびにChatGPTの助けを得て作成しました。

Pico Wで実現したニキシー管風単管時計

↑Pico Wに置き換えが成功したニキシー管風単管時計

 

1.3インチCS無しカラーIPS液晶

Raspberry Pi Pico Wで、クセつよ2インチTFT液晶を用いてニキシー管風時計がまがりなりにも実現できたのに気をよくして、1.3インチCS無しカラーIPS液晶でも実現に挑みました。

 

もともとRaspberry Pi Zero Wで作製したニキシー管風時計に用いたディスプレイは、この1.3インチ液晶(st7789)でした。ただ、工作物に組み込まれていて、ばらすことになっては面倒なので、先に2インチTFT液晶を試したものでした。

 

 

 

■このケースでも2回に1回の電源の入れ直しが必要

 

1.3インチ用に作成したpythonプログラムは、一発で時計を動かすことができました。しかし、後になって気付いたのですが、なんと2インチTFT液晶(GMT020-02)同様、2回に1回は電源を入れなおさないと時計が動き出さないのです。原因はわかりませんが、こうなると、液晶側でなくPico W側が怪しくなってきます。AliExpressにて安価で購入したものなので、純正品でなく互換品の可能性が大であることが関係しているのかもしれません。

 

■画像の反転、回転も可

 

次いで問題となったのは、Raspberry Pi Zero W用に作成していたニキシー管風時計では、当該液晶を工作の都合で逆さまに固定することにしていたため、画像が倒立状態になってしまったことです。当初は、どこかでミスったのでしょう。上下反転や左右反転でも画像が表示がされずノイズが入ったりして、MADCTLが効かないと思い込んだのですが、ソフトウエアで反転はできたものの、描画に時間がかかって瞬時に加増転換できず時計としては使い物にならないことから、今一度MADCTLを試したところ反転、回転ができることがわかりました。なお、上下の位置のズレが生じていましたが、位置調整で解消が可能なものでした。

 

180度回転で倒立は解消。また左右反転により、ペッパーズ・ゴーストの仕組みを応用した疑似透過ディスプレイによるホログラム時計も簡単に実現できました。

 

Pico Wで実現したニキシー管風単管時計

↑2インチTFT液晶(手目)、1.3インチIPS液晶(奥)に表示をさせたニキシー管風単管時計

 

■Pico Wに置き換えが成功したニキシー管風単管時計

 

2回に1回の時計の起動の問題はあるものの、Pi Zero W用にニキシー管ぽく工作した単管時計がPico Wで動かすことが出来ました。置き換え成功です。なお、同じmain.pyで1.3インチと2インチの両方の液晶で時計が動きます。両者で表示位置に若干の違い、すなわち上下に多少ズレがありますが、液晶画面をはみだすものではありません。

 

以下にコードを示します。液晶用のドライバーは不要です。ピン接続はコードを見てください。画像ファイルは、0~9の数字を表すものと時刻表示の区切りをわかりやすくするためのブランク画像の計11枚を使用しています。

 

【ニキシー管風単管時計のPico用main.py(画像180度回転)】

 

from machine import Pin, SPI
import network, ntptime, time

SSID = "Wi-Fiネットワーク名"
PASSWORD = "パスワード"

BLACK = 0x0000

spi = SPI(
  0,
  baudrate=10000000,
  polarity=1,
  phase=1,
  sck=Pin(18),
  mosi=Pin(19),
  miso=None
)

dc = Pin(21, Pin.OUT)
rst = Pin(20, Pin.OUT)
cs = Pin(17, Pin.OUT)

# ---- SPI立ち上がり安定化 ----
def spi_warmup():
  cs(0); dc(1)
  spi.write(b"\x00")
  cs(1)
  time.sleep_ms(2)

def cmd(c):
  cs(0); dc(0)
  spi.write(bytearray([c]))
  cs(1)

def data(d):
  cs(0); dc(1)
  spi.write(bytearray([d]))
  cs(1)

def reset_display():
  rst(0); time.sleep_ms(80)
  rst(1); time.sleep_ms(150)

def init():
  cmd(0x11); time.sleep_ms(120)
  cmd(0x36); data(0xC0)
  cmd(0x3A); data(0x55)
  cmd(0x21)
  cmd(0x29); time.sleep_ms(20)

def init_stable():
  spi_warmup()
  reset_display()
  time.sleep_ms(60)
  init()
  time.sleep_ms(60)
  init()

def window(x0, y0, x1, y1):
  cmd(0x2A)
  data(x0 >> 8); data(x0 & 255)
  data(x1 >> 8); data(x1 & 255)

  cmd(0x2B)
  data(y0 >> 8); data(y0 & 255)
  data(y1 >> 8); data(y1 & 255)

  cmd(0x2C)

def fill(color):
  window(0, 0, 239, 319)
  hi = color >> 8
  lo = color & 255
  buf = bytearray(512)
  for i in range(0, 512, 2):
    buf[i] = hi
    buf[i+1] = lo
  pixels = 240 * 320
  cs(0); dc(1)
  for _ in range(pixels // 256):
    spi.write(buf)
  cs(1)

def draw_raw_135x240(filename, x, y):
  w = 135
  h = 240
  window(x, y, x + w - 1, y + h - 1)
  cs(0); dc(1)
  with open(filename, "rb") as f:
    while True:
      chunk = f.read(512)
      if not chunk:
        break
      spi.write(chunk)
  cs(1)

# ===== Wi-Fi / NTP =====
def connect_wifi():
  wlan = network.WLAN(network.STA_IF)
  wlan.active(True)
  wlan.config(pm=0xa11140)
  wlan.connect(SSID, PASSWORD)
  print("Connecting Wi-Fi...")
  while not wlan.isconnected():
    time.sleep(0.1)
  print("Connected:", wlan.ifconfig())

def sync_time():
  ntptime.host = "ntp.nict.jp"
  for _ in range(5):
    try:
      ntptime.settime()
      print("NTP synced")
      return
    except:
      time.sleep(1)
  print("NTP failed")

JST = 9 * 3600

def get_digits():
  t = time.localtime(time.time() + JST)
  h, m, s = t[3], t[4], t[5]
  return {
    "H10": h // 10,
    "H1": h % 10,
    "M10": m // 10,
    "M1": m % 10,
    "S10": s // 10,
    "S1": s % 10
  }

center_x = (240 - 135) // 2
center_y = (320 - 240) // 2 + 36

def show_digit(d):
  if d is None:
    draw_raw_135x240("digit_blank.raw", center_x, center_y)
  else:
    draw_raw_135x240(f"digit{d}.raw", center_x, center_y)

sequence = [
  ("H10", 0.7),
  ("H1", 0.7),
  ("BLANK", 0.3),
  ("M10", 0.7),
  ("M1", 0.7),
  ("BLANK", 0.3),
  ("S10", 0.7),
  ("S1", 0.7),
  ("BLANK", 0.8),
]

def main():
  print("INIT...")
  init_stable()
  fill(BLACK)
  draw_raw_135x240("digit0.raw", center_x, center_y)
  print("INIT DONE")

  connect_wifi()
  sync_time()

  while True:
    digits = get_digits()
    for kind, dur in sequence:
      if kind == "BLANK":
        show_digit(None)
      else:
        show_digit(digits[kind])
      time.sleep(dur)
main()

このコードはChatGPT、Copilotの助けを借りて作成しました

 

Raspberry Pi Pico WとTFTディスプレイ

↑Raspberry Pi Pico Wと2インチTFTディスプレイでニキシー管風時計をつくる

クセの強い2インチTFTディスプレイGMT020-02

 

またニキシー管風時計の話かよ、しつこいな、とお思いのことでしょうが、今回はRaspberry Pi Zero WをPico Wに置き換えようというもの。Zero Wは安くても3000円ほどし、Pico Wの方は互換品かも知れませんが半額の1500円台のものがありますから、画像表示で時刻を示すくらいのことにZero Wは使うのはいかにももったいない。思えば、Zero Wでもかつては1300円ほどだったんですが…。

 

■専用ドライバーの設定を踏襲が必須


無印のPicoでなくてPico Wの方を使用したのは、Wi-Fi機能を使って労せず時刻を取得できるから。リアルタイムクロック(RTC)は持っていないし、時刻設定の仕組みを設けるのは少々面倒ですからね。


今回使用したディスプレイは、クセの強い2インチTFT SPIディスプレイのGMT020-02です。Raspberry Pi Zero Wにて、ペッパーズ・ゴーストの仕組みを応用した疑似透過ディスプレイに使いました。Picoでの使用歴は、先の当ブログの「生成AIを御する」にてGMT020-02専用のドライバーを生成AIに作成させた記事で取り上げています。
 

GMT020-02 2インチTFT SPIディスプレイ

↑クセつよのGMT020-02の裏面

 

 

クセつよと言ったように、ST7789Vだとか言われるGMT020-02ですが、かなり特殊なもので今回も結局先に生成AIに作成させた専用ドライバーの設定以外では動かせるものはありませんでした。なお、ドライバーがどれだったかわからなくなっては困るので、今回はドライバーは使わず、main.pyのコード中に設定を取り込みました。

 

■画像はRaw形式、135x240に軽量化が必須


おかげで、何とか画像は表示できたものの、問題は画像ファイルのサイズ。この2インチTFTディスプレイはフルで240x320の画像ファイルが使えますが、そのままのサイズで0~9の数字をあらわす画像ファイルを用意しても、Picoの容量をオーバーしてしまいます。画像を軽量のRAW形式に、サイズも135x240に変換する必要がありました。RAW形式への変換は、生成AIにPythonプログラムを書かせて実行しました。

 

■なぜか2回に1回は電源の入れ直しが必須

 

面倒はその後にまだ控えていました。電源をONにしても1回では時計表示にいたらず、電源を入れなおす必要があったのです。

 

RST の保持時間、初期化順序、SPI の再初期化、コマンド順の調整、遅延の追加、RST 2回、MADCTL の変更、画面クリアの順序変更、電源投入後の待ち時間追加、などとソフトでできそうなことはさまざま行ってみたが改善は見られず、発想を変えて初期化に失敗したら自動リブート(ウォッチドッグタイマーWDTを使う)もだめ、RUNピンを制御してfail_rebootも効果はなく、結局ハードが悪いと結論付けるしかありませんでした。

 

Pico Wへの置き換えにはもう一つの目的がありました。電源のON/OFFだけで時計を立ち上げ、終了させることができるということです。ところが、2回に1回しか時計が動き出さない点では、不都合と言わざるをえません。それでも、電源を入れなおすだけ、それも音声操作でもう一度電源をONにするように言えば済むことですから、少しばかりは我慢すべきかも。

 

■画面が大きいだけにきれい

 

ニキシー管風時計 Raspberry Pi Pico W 2インチTFT

↑画像サイズは135x240と小さいが、結構鮮明できれい

 

時計の起動にまつわる不都合とともに、液晶画面の周囲の白い枠がニキシー管風時計に不似合いなのが少々不満で、黒く塗りつぶしてやりたいところですが、1.3インチの液晶なんかに比べれば画面が大きくて、画像ファイルが135x240にもかかわらず、鮮明なところがきれいで好印象です。