📘 Raspberry Pi Pico で LCD 表示を高速化!
― 1bit フォント生成と Renderer クラスによる高速描画 ―
Raspberry Pi Pico で LCD に大きなフォントを描画すると、 起動直後の描画がワンテンポ遅い という問題がありました。
今回、この問題を根本から解決するために、 フォントを 1bit 化し、JSON + BIN 形式で扱う新方式 を導入し、さらに フォント描画を Renderer クラスに統合 することで、描画処理を大幅に高速化しました。
結果として、 起動直後の描画がほぼ即時に! という大きな改善が得られました。
この記事では、
- 前回方式の問題点
- 今回の改善内容
- 1bit フォント生成プログラム
- Renderer クラスによる描画処理
- 実際の表示例
をまとめて紹介します。
🟥 1. 前回方式の問題点
前回は .c 形式のフォントデータを XglcdFont で読み込んでいました。
Raspberry pi pico 2 WでLCD表示 - tomtomst【電子工作 DIY】
■ 問題:起動直後の描画が遅い
特に 32px〜72px の大きなフォントでは、 起動後 0.5〜1秒ほど描画が遅れる という現象が発生していました。
■ 原因:フォントファイルの読み込みが重い
.c フォントはサイズが大きい
- 起動時に毎回ファイル I/O が発生
- RAM 展開も重い
- 結果として描画開始が遅延
🟦 2. 今回の改善方針
✔ 改善方針
- フォントを 1bit 化し、JSON + BIN に分離して高速ロードする
- 描画処理を Renderer クラスに統合し、LCD クラスをシンプル化する
✔ この方式のメリット
- 1bit 化でデータサイズが大幅に縮小
- JSON(メタ情報)+ BIN(ビットマップ)で高速読み込み
- 起動直後の描画がほぼ即時
- 可変幅フォントに対応
- 記号位置補正も柔軟に可能
- 描画処理が Renderer に集約され、保守性が向上
🟩 3. 図解:新しいフォント方式の全体像
■ フォント生成の流れ(PC 側)

■ Pico 側の描画の流れ

🟧 4. 1bit フォント生成プログラム(PC 側)
以下が今回作成したフォント生成スクリプトです。
・「FONT_SIZE = 72」とところで、フォントのサイズを指定しています。
・「CHARS = "%.0123456789:℃"」のところで変換したい文字を指定しています。
from PIL import Image, ImageFont, ImageDraw
import json
FONT_PATH = r"C:\Windows\Fonts\meiryo.ttc"
FONT_SIZE = 72
CHARS = "%.0123456789:℃"
# 記号位置補正(必要に応じて調整)
ADJUST = {
":": (+1, +5),
}
def render_char(ch, font, ascent):
img = Image.new("L", (200, 200), 0)
draw = ImageDraw.Draw(img)
draw.text((0, 0), ch, 255, font=font)
bbox = img.getbbox()
if bbox is None:
return None, 0
img = img.crop(bbox)
top = bbox[1]
y_offset = top - ascent
img = img.point(lambda p: 255 if p > 128 else 0, mode="1")
return img, y_offset
def img_to_1bit_bytes(img):
w, h = img.size
data = bytearray()
for y in range(h):
byte = 0
bit_count = 0
for x in range(w):
pixel = img.getpixel((x, y))
if pixel == 255:
byte |= (1 << bit_count)
bit_count += 1
if bit_count == 8:
data.append(byte)
byte = 0
bit_count = 0
if bit_count != 0:
data.append(byte)
return data
font = ImageFont.truetype(FONT_PATH, FONT_SIZE)
ascent, descent = font.getmetrics()
font_meta = {"chars": {}}
all_bytes = bytearray()
offset = 0
for ch in CHARS:
img, y_offset = render_char(ch, font, ascent)
if img is None:
continue
w, h = img.size
bytes_ = img_to_1bit_bytes(img)
font_meta["chars"][ch] = {
"width": w,
"height": h,
"y_offset": y_offset,
"data_offset": offset,
"data_length": len(bytes_),
}
offset += len(bytes_)
all_bytes.extend(bytes_)
file_name = "font_" + str(FONT_SIZE) + "_1bit"
with open(file_name+".json", "w", encoding="utf-8") as f:
json.dump(font_meta, f, ensure_ascii=False, indent=2)
with open(file_name+".bin" , "wb") as f:
f.write(all_bytes)
print("生成完了:",file_name)
🟨 5. Pico 側:フォントデータ読み込み(XglcdFontVar)
フォントデータは JSON + BIN を読み込み、XglcdFontVar が保持します。
class XglcdFontVar:
def __init__(self, meta, data):
self.meta = meta
self.data = data
def get_letter(self, ch):
info = self.meta["chars"][ch]
bitmap = self.data[info["data_offset"]: info["data_offset"] + info["data_length"]]
return bitmap, info["width"], info["height"], info["y_offset"]
読み込み関数:
import ujson
def font_read(font_file):
with open(font_file+".json") as f:
meta = ujson.load(f)
with open(font_file+".bin", "rb") as f:
data = bytearray(f.read())
return XglcdFontVar(meta, data)
font72 = font_read("fonts/font_72_1bit")
font48 = font_read("fonts/font_48_1bit")
font32 = font_read("fonts/font_32_1bit")
font16 = font_read("fonts/font_16_1bit")
🟦 6. Renderer クラス:描画処理を一元化
フォント描画処理を Renderer に統合しました。
class FontRenderer:
def __init__(self, lcd, font, font_size, char_width_map=None):
self.lcd = lcd
self.font = font
self.font_size = font_size
self.char_width_map = char_width_map or {}
def draw_letter(self, x, baseline_y, ch, color, background=0,
landscape=False, rotate_180=False):
bitmap, w, h, y_offset = self.font.get_letter(ch)
bytes_per_row = (w + 7) // 8
buf = bytearray(w * h * 2)
src_index = 0
dst_index = 0
for yy in range(h):
bit_mask = 1
for xx in range(w):
pixel = color if (bitmap[src_index] & bit_mask) else background
buf[dst_index] = (pixel >> 8) & 0xFF
buf[dst_index + 1] = pixel & 0xFF
dst_index += 2
bit_mask <<= 1
if bit_mask == 0x100:
bit_mask = 1
src_index += 1
src_index = (yy + 1) * bytes_per_row
if rotate_180:
new_buf = bytearray(len(buf))
num_pixels = len(buf) // 2
for i in range(num_pixels):
new_idx = (num_pixels - 1 - i) * 2
old_idx = i * 2
new_buf[new_idx], new_buf[new_idx + 1] = buf[old_idx], buf[old_idx + 1]
buf = new_buf
draw_y = baseline_y + y_offset
self.lcd.fill_rectangle(x, draw_y, self.font_size, self.font_size, background)
if landscape:
draw_y -= w
self.lcd.block(x, draw_y, x + h - 1, draw_y + w - 1, buf)
else:
self.lcd.block(x, draw_y, x + w - 1, draw_y + h - 1, buf)
return w, h
def draw_text(self, x, baseline_y, text, color, spacing=0):
cursor_x = x
default_width = int(self.font_size / 2)
for ch in text:
w, h = self.draw_letter(cursor_x, baseline_y, ch, color)
w2 = self.char_width_map.get(ch, default_width)
cursor_x += w2 + spacing
🟩 7. 実際の描画例
# Renderer の準備
font72_renderer = FontRenderer(display, font72, 72, char_width_map={":": 18})
font32_renderer = FontRenderer(display, font32, 32)
# 年月日・時刻の取得
weekday, weekdays, now_day, now_time, now_hour = get_now_data()
# 年月日表示(32px)
font32_renderer.draw_text(0, 32, now_day, 0xFFFF)
# 時刻表示(72px)
font72_renderer.draw_text(8, 120, now_time, 0xFFFF, spacing=3)
🟫 8. 改善の効果
| 項目 |
改善前 |
改善後 |
| 起動直後の描画開始 |
1〜2秒遅れる |
ほぼ即時 |
| フォントデータサイズ |
大きい |
1bit 化で大幅削減 |
| 描画速度 |
やや遅い |
高速 |
| 可変幅フォント |
非対応 |
対応 |
| 記号位置補正 |
難しい |
JSON で柔軟に管理 |
| コード構造 |
分散 |
Renderer に集約され保守性向上 |
🟩 9. まとめ
今回の改善で、 フォント生成 → 軽量化 → 高速描画 → クラス統合 という一連の流れが完成しました。
- 1bit フォントでデータサイズを削減
- JSON + BIN で高速ロード
- 可変幅フォント&記号補正に対応
- Renderer クラスで描画処理を一元化
- 起動直後の LCD 描画が劇的に高速化
以前の起動 動作

修正後の起動 動作
