🐱

Pinot Font(PFN)で日本語表示(C/C++)

に公開

マイクロコントローラ(以下、マイコン)で日本語を表示したい場合、かつてはX Window SystemのK16フォントなどのFixed Fontを持ってきたり、(もはや知る人が少ない)DOS/VのFONTX形式のフォントを持ってきたりしてましたが、ちょっと扱いづらい。マイコンのプログラミング環境もUTF-8になって久しく(ですよね?)、そんな環境ではPinot Font(PFN)(以下、PFNフォント)が扱いやすいよなあと思っていました。

きっと誰かがすでにとっくに、C/C++で使えるようにしてくれているんじゃないかと思いながら、機会があってCで扱うライブラリを作成したので公開しておきます。ライブラリ本体のpfnfont.cpp/pfnfont.hはマイコンに依存していない(と思う)ので、どんなマイコンであっても、C++が使えるなら利用可能だと思います。

Arduino用のライブラリは、Githubのリポジトリに公開しています。こちらRelaseからZIPファイルをダウンロードしてArduino IDEにインストールしてください。

PFNFontクラスの使い方

PFNFontクラスを利用するにはフォントとコールバック関数を用意する必要があります。

PFNフォントの用意

PFNフォントについては、オリジナルのアーティクルに詳細な解説があります。フォントをPFN形式に変換するためのツールも公開されていますし、東雲フォントをPFNフォントに変換したバイナリも公開されています。変換済みの東雲フォントを使わせていただくのが、手っ取り早いでしょう。12/14/16pxの3種類があるので、たいていの表示デバイスに適合します。

PFNFontクラスは、PFNフォントがオンメモリにあることを前提にしています。16pxの東雲フォントで232KBほどですから、今どきのマイコンなら無理なくROMに格納できるでしょう。

Raspberry Pi Picoシリーズ向けのサンプルコードでは、東雲14pxフォントを .incbinを使ってフラッシュメモリ領域に埋め込んでいます。GNU ASの疑似命令 .incbinが使えない開発環境ならば、ヘッダファイル化してコンパイルするのが、もっとも手軽かつ確実です。

Gemini君にフォントファイルをヘッダファイル化するPythonコードを書いてもらったので、こいつを使ってヘッダファイルにするといいでしょう。

import sys

# python convert_font.py pfnfont.pfn font_data > font.h

if len(sys.argv) < 3:
    print("Usage: python convert_font.py <input_file.pfn> <array_name>")
    sys.exit(1)

input_file = sys.argv[1]
array_name = sys.argv[2]

try:
    with open(input_file, "rb") as f:
        print(f"const unsigned char {array_name}[] = {{")
        byte = f.read(1)
        count = 0
        while byte:
            print(f"0x{byte.hex()}", end=", ")
            byte = f.read(1)
            count += 1
            if count % 16 == 0:
                print("") # 16バイトごとに改行
        print("\n};")
        print(f"const unsigned int {array_name}_len = {count};")

except FileNotFoundError:
    print(f"Error: File '{input_file}' not found.")

コールバック関数

PFNFontクラスは1文字デコードするたびにコールバック関数を呼びます。コールバック関数には文字のビットマップを格納した構造体 font_gryph_t *が渡されるので、コールバック関数で日本語を表示したいデバイスにビットマップを描けばいいわけです。このような仕掛けなので、PFNFontクラスは日本語を表示するデバイスやマイコンを問わず利用できます。

コールバック関数の例はサンプルdraw_font_to_terminal()を見てもらえばいいかとは思いますが、典型的にはこんな感じになろうかと思います。

// コールバック関数
void draw_font_to_display(font_gryph_t *gryph, int x, int y) {
    for (int r = 0; r < gryph->height; r++) {
        for (int c = 0; c < gryph->width; c++) {
            // ビットマップからピクセルデータを取得
            int bit_index = r * gryph->width + c;
            int byte_index = bit_index / 8;
            int bit_offset = 7 - (bit_index % 8);
            if ((gryph->bitmap[byte_index] >> bit_offset) & 1) {
                // 1なら表示デバイスの該当座標に点を打つ
                draw_pixel(x+c, y+r, forward_color);
            } else {
                draw_pixel(x+c, y+r, back_color);
            }
        }
    }
}

PFNFontクラスを使う

こんな感じに日本語をあなたのディスプレイに描画します。

#include "pfnfont.h"
#include "font.h"
...
// font_data: PFNフォントデータの先頭アドレス
// font_data_len: PFNフォントデータのサイズ
// draw_font_to_display: コールバック関数
PFNFont *pfn = new PFNFont(font_data, font_data_len, draw_font_to_display);
pfn->draw_string( u8"こんにちは世界!", 0, 0);

PFNフォント雑感

UTF-32コードポイント並びにフォントビットマップが並べてあるので、UTF-8環境でとっても扱いやすいフォント形式ですけど、フォントブロック内のコードポイント長が4バイト固定でなく1、2、4バイトの3通りあるのが、ちょっと厄介な感じですね。ボンクラなので、コードポイントを取ってくるあんまりいい方法が思いつかないという。

また、結果としてビットマップが4バイトアラインされていないので、Armなど多くのCPUで厄介な感じになります。PFNFontクラスではフォントビットマップをアラインされたメモリブロックにコピーしてから、コールバックに渡してます。こうすることで、コールバック関数側ではビットマップに安全にアクセスできるように配慮しました。

なお、このリポジトリに、Arduino用のライブラリ形式のソースと、Arduino IDEにインストール可能なZIPアーカイブを公開しておきました。Arduinoユーザーは、こちらを利用してください。

Raspberry Pi Picoシリーズ+ST7789 LCDで高速日本語表示を行うサンプル

Raspberry Pi PicoシリーズのSPIに、ST7789を搭載したLCDモジュールを接続し、DMAを使って高速な日本語表示を行うサンプルも用意しました。ただし、ご存知の通り、ST77xxはモジュール製品ごとに初期化が異なる場合がある難点があります。したがって、このコードのままで、市販されているST7789搭載LCDモジュールのすべてが動作するかどうかはわかりません。必要に応じて、初期化コードを変える必要があるかもしれないということを承知しておいてください。

参考までに、このコードはAmazonで売られている解像度240×240ドットのモジュール製品でテストしています。この製品なら動くはずです。

このサンプルコードでは、1行分のバッファを用意して、PFNFontクラスを使ってバッファに1行分の文字をレンダリング後、バッファをDMAでディスプレイに転送しています。バッファをダブルにしているので、複数行を表示させるときには文字のレンダリングとSPIディスプレへの書き込みが並列化され高速に表示できることが特徴です。

表示例

当初の目論見では、1文字単位にDMAで転送するつもりでした(PFNFontクラスもその想定で設計)。しかし、ST77xxは描画ウィンドウをセットするコマンド実行後や、DMA転送後に数十μ秒程度のウェイトを入れないとハングアップしてしまうことが判明。1文字単位で転送を行うと逆に遅くなってしまうんですよね。仕方なく、行単位の転送としました(うーむ)。

ところで、これまでコード生成AIの類による自動コード補完でコードを書いたことはなく、せいぜいAIはチャットや最終チェックにしか使ってませんでした。しかし、今回のPFNFontクラスとjpfont_2_lcdは、お試しでGemini Code Assist(無料版)のコード補完を有効にして書いてみました。最初はAIのコード補完が気持ち悪く邪魔と感じたものの、慣れてくるとGemini Code Assistがこちらの意図を汲んでコードを生成してくれる印象を受けるようになって、AIのコード支援が急速に世間に定着してるのも納得できましたね。

もっとも、Gemini Code Assistのせいで余計な手間が生じたところもありました(結果的には自分の頭が悪いだけだがAIのせいにしておく)。レンダリング後のバッファをDMAで転送するサイズの計算に次のような感じのコードを生成したんですね。

dma_channel_set_trans_count(dma_ch, strlen(str) * pfn->get_max_font_width() * pfn->get_max_font_height(), true);

「いやいやUTF-8なんだからstrlen(str)はないしpfn->get_max_font_width()を掛けても横幅は出ねえよ」と。そこで、PFNFont::draw_string()がレンダリングした幅を返すように改造して修正したのだけれども(当然)表示できない。そんなの当たり前で、ラインバッファにフォントを書くとき次のようにTFT_WIDTHで折り返してるのだから1行分バッファ全体をLCDに転送しないとまともな表示なるわけがない。

line_buf[r * TFT_WIDTH + (x + c)] = font_color;

Gemini Code Assistのstrlen(str)* pfn->get_max_font_width()に引っかかってしまって無駄に悩んでしまった。AIのコード補完を使っていても、自分で考えるのを止めてしまうと、このようなことも起きるってことでしょう。勉強になりました。

また、これは少し前に分かったことですが、Gemini Code AssistはPicoシリーズのDMAのコードが混ざると、チャットに返答しなくなるようです。無料版だからかもしれませんが、RP2xxxのDMA関連のコードの学習が浅いからかもしれません。AIはなかなか便利なものだが、全面的に頼るのは難しそうですね。

最後に個人的な宣伝ですが、日経BP社様からRaspberry Pi Picoシリーズの純正C SDKのガイドブックを出版させていただくことになりました。PFNFontクラスや、LCDに日本語をDMAで描画するサンプルは本来、この書籍に掲載する予定だったネタです。残念ながらというか、毎度のように最後はお尻に火がついた状態で書き上げなければならなくなり、PFNFontクラスや、LCDに日本語表示は掲載に間に合いませんでした。自由に利用してもらって、何かの役に立てていただければ幸いです。

Discussion