「Raspberry Pi Pico 2 Wでつくる高速表示お天気時計:SDカード活用とフォント最適化の記録」

「Raspberry Pi Pico 2 Wでつくる高速表示お天気時計:SDカード活用とフォント最適化の記録」


0.使用環境

  • Raspberry Pi Pico 2 W with RP2350
  • MicroPython version='v1.27.0 on 2025-12-09
  • LCD:240x320 2.8インチ ili9341

    1. 導入:なぜいま、お天気時計を改良するのか?

  • 現状の課題: 以前作成したお天気時計において、「時刻更新時に文字の幅が変わって表示がガタつく」という微かな違和感があった。
  • 目指す姿: 人が近づいた時だけ素早く起動し、一瞬で現在時刻と天気を表示する「スマートな時計」を作りたい。

2. 課題①:フォントのガタつきを解消(固定幅フォントの導入)

  • 現象: 可変幅フォントを使用していたため、数字の「1」と「8」で横幅が異なり、更新時に座標がずれる。
  • 解決策:
    • 時計表示用に固定幅フォントを作成し、描画ロジックを最適化。
    • 更新時に不要なゴミが表示されないよう、背景塗りつぶしと描画のタイミングを制御。

3. 課題②:起動の高速化とメモリ節約(SDカードの活用)

  • 背景: バッテリー駆動のため、不要なときは電源OFF、人が来たらONという運用にしたい。起動後の描画を待たせないためには?
  • 実装:
    • サイズが大きいフォントデータやアイコン画像を、フラッシュメモリではなくSDカードへ外出し。
    • 必要なデータを必要な分だけダイレクトに読み出すことで、プログラム領域を圧迫せず、起動直後のレスポンスを確保。
    • os ライブラリとバイナリ読み込みを活用した、高速な描画システムの構築。

4. 本題:フォント・描画システムの最適化(技術的ポイント)

  • データ構造の工夫:
    • LCD Image Converter で作成した.cファイルを.binに変換。
    • 文字とインデックスの対応を JSON で管理し、辞書引きを高速化。
  • 高速化の秘訣:
    • ピクセル単位の描画ではなく、RAWバイナリを一括転送することで描画時間を劇的に短縮(前回の遅延を解消)。
    • SPIクロックの向上とファイルハンドルを保持する運用。

4-1.フォントデータの作成方法

  • LCD Image Converterで作成
    • インストール:About project | LCD Image Converter
    • 使い方
      • 起動画面

      • 初期設定と「文字幅固定」の設定

      • MSゴシック 16x16に設定

      • 16x16固定に設定

        • 固定設定:Monospacedに設定
        • 作成する文字列:デフォルトは半角英数字記号
        • 文字の追加:右上(「Source Font」の右側)の窓に追加する。
        • 文字サイズ:デフォルトはW1 H1。 16フォントを選択しても8x16サイズになっている場合がある。 16x16サイズしたい場合はW/16 H/16とする。
        • デフォルトは半角文字が入っているので、16x16にすると半角文字は隙間が空いてしまいます。この場合は全角文字に換えておいた方が良いです。

        OKをクリックするとFontというデータが作成されます

      • フォントの出力 「File」→「Convert]で、Font.cというファイルが出力されます。


4-2.フォントデータの変換:高速化のための「下準備」

LCD Image Converterが出力した.cファイルは、人間が読める「ソースコード」の状態です。これをそのままPicoで読み込むと、マイコン側で複雑な計算(解釈)が必要になり、表示までの時間がかかってしまいます。

そこで、「あらかじめマイコンが直接読み込める形式(バイナリ)」に変換しておくことで、処理を極限まで省く工夫をしました。

【変換のイメージ:レストランの料理】 * .cファイル(ソースコード)の状態: 材料がテーブルに散らばっている状態。マイコンが食べる前に、一つずつ並べて調理(計算)しなくてはなりません。 * .binファイル(変換後)の状態: 必要な料理がすべてお皿に盛り付けられた状態。マイコンはそのまま「食べる(表示する)」だけで済みます。

この「下準備」を済ませることで、起動した瞬間にパッと画面が表示される、あの軽快なレスポンスが実現できました。

【具体的な変換手順】 1. Pythonスクリプトによる抽出: .cファイルの中から、実際の点情報(ドットのON/OFF)が書かれた部分だけを抜き出します。 2. バイナリデータ化(.bin): 抜き出したデータを「1バイトの列」としてファイルに書き出します。 3. インデックス管理(.json): 「どの文字がどこにあるか」という場所情報を別ファイル(JSON)で保存します。これにより、マイコンは「〇〇という文字が欲しければ、ファイルの中の◯◯バイト目を見ればいい」と迷わずアクセスできるようになります。

これにより、Picoの限られたメモリを消費することなく、巨大な漢字フォントセットであっても一瞬で表示できるようになりました。

[変換プログラム:「.c」ファイル→「.json」ファイル] 「.c」ファイルからバイナリの「.json」ファイルへ変換するpythonプログラムを下記に示します。 PCで下記プログラム実行し事前に.jsonファイル(フォントデータのインデックスを記載したファイル)を作成しておきます。 このインデックスを使用することで、フォントデータ内の表示したいフォントを探す時間が短縮できるので、高速に表示できるようになります。

import re
import json

def generate_font_map_clean(c_file_path, json_output_path):
    with open(c_file_path, 'r', encoding='utf-8') as f:
        content = f.read()

    # 1. image_data で始まる行だけを対象にする(tImage は無視)
    # image_data_Font..._0xXX を探す
    pattern = r"static const uint8_t (image_data_.*?_0x([0-9a-fA-F]+))"
    matches = re.findall(pattern, content)
    
    # 2. 辞書を作成(インデックスを 0 からの連番にする)
    char_map = {}
    for i, match in enumerate(matches):
        full_name, hex_code = match
        char_val = int(hex_code, 16)
        
        # 制御文字などはchr()変換できない場合があるためスキップや保護
        try:
            char = chr(char_val)
            # 既に登録済みならスキップ(重複対策)
            if char not in char_map:
                char_map[char] = i 
        except:
            continue
        
    with open(json_output_path, 'w', encoding='utf-8') as f:
        json.dump(char_map, f, ensure_ascii=False, indent=4)
    
    print(f"抽出完了: {len(char_map)} 文字登録しました。")

font_name = "Font_time"

# 実行
generate_font_map_clean(font_name + '.c', font_name + '_map.json')

[変換プログラム:「.c」ファイル→「.bin」ファイル] 「.c」ファイルからバイナリの「.bin」ファイルへ変換するpythonプログラムを下記に示します。 PCで下記プログラム実行し事前に.binファイルを作成しておきます。データをバイナリ化することで、データサイズを抑え、LCDへの表示を高速化することができます。

import re

def convert_c_to_raw(c_file_path, output_bin_path, char_color, bg_color):
    # RGB565変換関数
    # 0xFFFF(白)を想定した簡易版
    # 0xR G B → 0xRRRRRGGGGGGBBBBB に変換
    def to_rgb565(color):
        # color が 0xFFFF のような場合、そのまま2バイトに
        return color.to_bytes(2, 'big')

    char_color_bytes = to_rgb565(char_color)
    bg_color_bytes = to_rgb565(bg_color)

    with open(c_file_path, 'r', encoding='utf-8') as f:
        content = f.read()

    # 修正: image_data_... の後の { から } までの間で、0x で始まる数値のみを抽出
    # ASCIIアートコメント "//" があっても無視するようにします
    pattern = r"static const uint8_t image_data_.*?\[.*?\] = \{(.*?)\};"
    matches = re.findall(pattern, content, re.DOTALL)

    with open(output_bin_path, 'wb') as f_out:
        for match in matches:
            # 0x で始まる文字列だけを抽出する
            hex_strings = re.findall(r"0x[0-9a-fA-F]{2}", match)
            
            for hex_str in hex_strings:
                byte_val = int(hex_str, 16)
                # 1バイト(8ビット)を8画素に展開
                for i in range(8):
                    # ビットが立っていれば文字色
                    # --- 修正後 ---
                    if (byte_val >> (7 - i)) & 1:
                        f_out.write(bg_color_bytes)   # 反転させるので背景色
                    else:
                        f_out.write(char_color_bytes) # 反転させるので文字色
    print(f"変換完了: {output_bin_path}")


font_name = 'Font_8x16'

# 実行例(白文字・黒背景にする場合)
# 色は0xFFFF(白)や0x0000(黒)など
convert_c_to_raw(font_name + '.c', font_name + '_raw.bin', 0xFFFF, 0x0000)

作成した「.bin」と「.json」ファイルはPico2Wへ接続するSDカードへ保存しておきます。


4-3. 描画を爆速にする「LCD専用データ変換」の工夫

単にフォントデータをバイナリ(.bin)にするだけでは、十分な高速化は達成できません。実は、LCDに表示する直前の形にデータを加工して保存しておくという、もう一歩踏み込んだ工夫をしています。

【何をしたのか?】 通常、LCD Image Converterが出力するデータは、PCやプログラムが扱いやすい「汎用的な並び順」になっています。しかし、これをそのままLCDに送ると、Pico側で「回転処理」や「バイトの並び替え(ピクセル変換)」という計算が発生し、それが描画のボトルネック(遅延)になります。

そこで、「LCDに送るデータそのもの」を変換スクリプト側で生成することにしました。

  1. LCDの「色データ形式(RGB565)」に変換済みのビット列を保存
    • 通常は描画時に「1bitの情報(点があるか)」を見てから「16bitの色情報」を計算しますが、これを変換段階で済ませておきます。
  2. LCDのメモリマップに直結する並び順で保持
    • LCDには「左上から右下へ」と特定の順番でデータを送り込む必要があります。.binファイルの中身を、LCDが「はい、受け取ったそのままの順番で画面に塗るよ!」と処理できる順番(RAWバイナリ形式)に並び替えて保存しました。

【結果:マイコンはただ「流し込む」だけ】 この工夫により、Picoの役割は「SDカードからデータを読み取って、そのまま通信路(SPI)へ流す」という単なる宅配便の仕事に徹することができるようになりました。 複雑な計算が一切不要になったため、以前とは比較にならない「待ち時間ゼロ」の高速表示が実現したのです。


4-3.フォントデータの表示:Pico2W側の処理

初期設定 - LCDの表示用ライブラリは以前書いたブログ記事と同じ「ili9341」をしようします。 - フォントデータはSDカードへ格納したのでSDカードの設定もここで行っています。 - LCDとSDカードは同じSPI0を使用しています。 - フォントデータをSDカードから読み出せるようにSDカードを「/SD」とマウントしておきます。

import ili9341
from machine import Pin,SPI
import os
from sdcard import SDCard

#SPI設定 SPI_0はLCD用、SPI_1はタッチセンサ、その他用
spi_0 = SPI(0, baudrate=40000000, sck=Pin(18), mosi=Pin(19))
#display = ili9341.Display(spi_0, dc=Pin(17), cs=Pin(16), rst=Pin(20),rotation=270)
display = ili9341.Display(spi_0, dc=Pin(17), cs=Pin(16), rst=Pin(20),rotation=90, width=320, height=240)

display.clear()  # 画面をクリアする
#display.fill_rectangle(0, 0, 320, 240, ili9341.color565(0, 0, 0))  # 黒色で塗りつぶす

SD_spi = SPI(0, baudrate=10000000, polarity=0, phase=0, 
                 sck=Pin(6), mosi=Pin(7), miso=Pin(4))
SD_cs = Pin(5, Pin.OUT)
sd = SDCard(SD_spi, SD_cs)
    
# 念のため一度アンマウントして、確実にマウントし直す
os.mount(sd, '/sd')

今回作成した表示プログラム - 実際使用しているのはフォント初期化「(init()」と高速描画「display_string_fast()」です。

class RawFontRenderer:
    def __init__(self, lcd, raw_font_file, width, height):
        self.lcd = lcd
        self.font_file = raw_font_file
        self.w = width
        self.h = height
        # RAWデータは1ピクセル2バイトなので、1文字のサイズは (w * h * 2) バイト
        self.bytes_per_char = width * height * 2
        # 初期化時にファイルを開きっぱなしにする
        self.f = open(raw_font_file, 'rb') 

    def display_char(self, x, y, char_index):
        # 1. 範囲指定(テストのために最小限に)
        # set_windowがうまく動いていない可能性があるので、直接コマンドを叩いてみる
        self.lcd.write_cmd(0x2A) # Column Address Set
        self.lcd.write_data(x.to_bytes(2, 'big') + (x + self.w - 1).to_bytes(2, 'big'))
        self.lcd.write_cmd(0x2B) # Page Address Set
        self.lcd.write_data(y.to_bytes(2, 'big') + (y + self.h - 1).to_bytes(2, 'big'))
        
        # 2. 書き込み
        self.lcd.write_cmd(0x2C) # RAM Write
        
        # ファイルの存在とインデックスの計算チェック
        self.f.seek(char_index * self.bytes_per_char)
        bytes_left = self.bytes_per_char
        
        #while bytes_left > 0:
        #    chunk = self.f.read(min(bytes_left, 512))
        #    self.lcd.write_data(chunk)
        #    bytes_left -= len(chunk)
        
        # 512バイトずつではなく、一気に1文字分全データを書き込む方が速い
        # ただしRAM不足に注意。32x72=4608バイト(約4.5KB)なので、全読みでOK!
        data = self.f.read(self.bytes_per_char)
        self.lcd.write_data(data)

    def display_string(self, text, start_x, y, char_map, spacing=2):
        """
        連続した文字列を高速表示する
        text: 表示したい文字列
        start_x: 開始X座標
        y: 開始Y座標
        char_map: 文字とインデックスの辞書
        spacing: 文字間隔
        """
        current_x = start_x
        for char in text:
            idx = char_map.get(char)
            if idx is not None:
                # 1文字ずつ爆速描画
                self.display_char(current_x, y, idx)
                current_x += (self.w + spacing)
            
            # 画面右端を超えないようにチェック
            if current_x + self.w > 320:
                break

    def display_string_fast(self, text, start_x, y, char_map, spacing=0):
            cursor_x = start_x
            for char in text:
                idx = char_map.get(char)
                if idx is None: continue
                
                # 保持しているファイルポインタを使用
                self.f.seek(idx * self.bytes_per_char)
                glyph = self.f.read(self.bytes_per_char)
                
                # LCDに一括転送
                self.lcd.block(cursor_x, y, cursor_x + self.w - 1, y + self.h - 1, glyph)
                
                cursor_x += (self.w + spacing)

フォントの初期化 - LCDアクセスのSPI設定が、「display」、フォントデータが「 '/sd/Font_8x16_raw.bin'」、フォントサイズが8x16「 8, 16」の場合は下記の用の初期化します。  RawFontRenderer(display, '/sd/Font_8x16_raw.bin', 8, 16)

#--------------------------------------------------------------
# 各サイズのレンダラーを作成
renderer_8x16_raw  = RawFontRenderer(display, '/sd/Font_8x16_raw.bin', 8, 16)
renderer_24x48_raw = RawFontRenderer(display, '/sd/Font_24x48_raw.bin', 24, 48)
renderer_32x32_raw = RawFontRenderer(display, '/sd/Pico2W_32x32_Font_raw.bin', 32, 32)
renderer_48x48_raw = RawFontRenderer(display, '/sd/Pico2W_48x48_Font_raw.bin', 48, 48)
renderer_32x72_raw = RawFontRenderer(display, '/sd/Font_time_raw.bin', 32, 72)

import ujson
def load_char_map(json_path):
    with open(json_path, 'r', encoding='utf-8') as f:
        return ujson.load(f)
  • フォントのインデックス(マップ―データ)は下記のように定義します
import ujson
def load_char_map(json_path):
    with open(json_path, 'r', encoding='utf-8') as f:
        return ujson.load(f)

# フォントマップデータ設定
map_8x16  = load_char_map('/sd/Font_8x16_map.json')
map_24x48 = load_char_map('/sd/Font_24x48_map.json')
map_32x32 = load_char_map('/sd/Pico2W_32x32_Font_map.json')
map_48x48 = load_char_map('/sd/Pico2W_48x48_Font_map.json')
map_32x72 = load_char_map('/sd/Font_time_map.json')

フォントの表示例

    #年月日表示
    renderer_8x16_raw.display_string_fast(now_day, 8, 8, map_8x16,spacing=-0)
    #バージョン表示
    renderer_8x16_raw.display_string_fast(version, 32*8+8 , 0, map_8x16)
    #時刻表示
    renderer_32x72_raw.display_string_fast(now_time, 8, 32, map_32x72)

    #温度の表示
    renderer_24x48_raw.display_string_fast(temp, 32*5+24, 16+4, map_24x48,spacing=-3)
    renderer_48x48_raw.display_string_fast("℃", 32*5+12+24*4, 16+4, map_48x48)
    
    #湿度の表示
    renderer_24x48_raw.display_string_fast(hum, 32*5+24, 16+48, map_24x48,spacing=-3)
    renderer_48x48_raw.display_string_fast("%",32*5+12+24*4, 16+48, map_48x48)

5. 成果:Pico 2 Wで実現した「待ち時間ゼロ」の体験

  • 実際に動作している様子(写真や動画があればベスト)。
  • 電源OFFから時刻が表示されるまでの爆速感。
  • 漢字表記にも対応したことで、情報量が増えより見やすくなったUI。

前期の表示速度(1bitフォント、フラッシュに格納)

表示速度(推測):1文字あたり約40ms

今回の表示速度1(Rawフォント、SDカードに格納)

表示速度(推測):1文字あたり約100ms

今回の表示速度2(Rawフォント、フラッシュに格納)

表示速度(推測):1文字あたり約15ms


6. まとめ

【実験結果:描画速度】 1. 1bitフラッシュ (40ms): 「CPUの計算力不足」 → 計算処理が重く、描画に時間がかかっていた。

  1. Rawフラッシュ (15ms): 「通信速度の限界」 → 描画データを「LCD直結形式」に変換することで、CPU負荷をゼロにし、約2.7倍の高速化に成功。

  2. Raw SDカード (100ms): 「ストレージの物理的応答速度」 → 大容量化のためにSDカードへデータを外出ししたが、外部ストレージ特有の「読み出し待ち」が発生。

以下の表を記事内に挿入すると、技術的な比較が一目でわかります。

フォント構成 転送形式 置場所 描画速度 評価
旧(1bit) 計算型 フラッシュ 40ms 計算が重い
新(Raw) 直接転送 フラッシュ 15ms 最高速
新(Raw) 直接転送 SDカード 100ms 大容量だが低速

結論:

項目 LCD表示速度 フラッシュ容量
Raw化(計算を捨てる) 爆速(15ms) 圧迫
SDカード化(ストレージ外出し) 低速(100ms) 節約

「計算を捨てる(Raw化)」ことで表示は爆速になるが、「データの置き場所(ストレージ)」が速度を決める。リアルタイムな時計にはフラッシュ、大容量な漢字にはSDカードという「適材適所」の設計が重要である。

今回の実験を通じて、『速度』を求めるならフラッシュ直結、『拡張性(大容量)』を求めるならSDカードという、物理的な制約を逆手に取った設計思想が重要だと学びました。現在は速度を優先してフラッシュ版で運用していますが、今後はSDカード版をさらに突き詰め、よく使うデータだけをRAMにキャッシュする『ハイブリッド運用』を目指したいと思います。