前回、M5StickC PlusのTFTディスプレイに、日本語を表示してみました(下図)。
M5StickC PlusのMCUはESP32なので、WiFi経由でインターネットにアクセスして、HTMLデータなどを取得することができます。最近のHPは、文字コードがunicode(UTF-8)で記述されているので、前回の日本語描画tft_kprint()関数を使うためには、unicode→S-JIS変換が必要となります。
過去に本HPでは、以下の記事に於いてFPGAボードを使い、ハードウェアでunicode⇔S-JIS変換(双方向)を実現しました。unicode⇔S-JIS変換の基本的な考え方については、以下の記事でおさらいしてください。
「#21-10 文字コード変換」
「#22-1 文字コード変換回路」
今回はFPGAボードの力を使用できないM5StickC Plusですので、ソフトウェアでunicode→S-JIS変換(単方向)を実現してみようと思います。
M5StickC PlusのMCUはESP32なので、インターネットからHTMLを取得することが可能です。この時の最大の障壁である、unicodeをクリアしておきたいと思います。
FPGAボード(Moscovium)では、unicode⇔S-JIS双方向変換を実施しました。S-JIS→unicodeの変換テーブルを作っておいて、その逆引きでunicode→S-JIS変換を実行したからです。元々のS-JIS→unicode変換テーブルを順方向参照したら、双方向変換を実行できただけのことです。
インターネット上のデータをハンドリングする目的では、unicode→S-JIS変換が主に必要となります。今回は全てソフトウェアで処理するため、双方向変換は捨てて、unicode→S-JIS単方向変換に集中したいと思います。
こんなくだらない「コード変換」の為に貴重なメモリ資源を浪費するのは耐えられないので、メモリ使用量の削減も目標の一つに挙げておきます。
S-JISに含まれる日本語文字は全て、unicodeの16ビット範囲に存在しています。
従って最も安易な方法は、unicode 16ビット分の全文字コードに対して、対応するS-JISコードをテーブルに保持する方法です(unicode基準変換表)。
「#21-10 文字コード変換」でも議論した通り、この程度(128Kバイト)のメモリは、Arduino DUE (Flash 512Kバイト)や、今回のM5StickC Plus (Flash 1,280Kバイト)にとっては、「何とか確保できる」レベルの話なので、メモリ使用量に目を瞑るなら、これが一番安易な解決方法です。
まずはテーブルサイズの削減を考えて行きましょう。
前節で議論した通り、unicodeを基準とすると、16ビット=65,536文字が対象となるのに対して、第一・第二水準漢字を含むS-JISコードに含まれる文字数は約7,000文字しかありません。
S-JIS文字を基準として、対応するunicodeをペアにしてテーブルを作ると、以下の様になります(S-JIS基準変換表)。
(S-JISコード2バイト+unicode 2バイト)×約7,000文字≒27Kバイト
これをunicodeの昇順で並び変えて配列変数に保持します。
unicode→S-JIS変換を実行する時には、変換したいunicodeをキーとして二分探索すると、最大探索回数log2 (7,000)≒13回で、対応するS-JISコードを知ることができます。
必要なメモリ容量が128Kバイト → 約27Kバイトに削減できました。その代わり、探索回数0回(参照1回のみ) → 13回へ増加しています。つまり、変換は遅くなった、と言う事です。
ようやくエンジンがかかってきました。ここまでは、ほんの肩慣らしです。
前節でテーブルサイズの削減ができたのですが、代わりに探索回数が増加して、変換が遅くなってしまいました。ここから、探索スピードの高速化を考察して行きます。
高速化の手法として、ハッシュ関数を使おうと思います。ハッシュ関数は、要約関数と訳されています。
例えば、unicode(16ビット)を、8ビットに要約するハッシュ関数Hash (x)を考えます。
ハッシュ値=Hash (unicode)
この時、「ハッシュ値のビット数<<unicodeのビット数」の関係になることが要約の条件です。そうでないと「要約」になりませんから。
基本的に、汎用のハッシュ関数と言う物は無くて、扱うデータ系列各々に対して、その都度、適切なハッシュ関数を考案して適応することになります。
今回の例では、16ビット幅のunicodeを、たったの8ビットで要約しようとしているので、必ず重複が発生します。unicodeに対するハッシュ値が、重複しながらも偏らず、なるべく均一にばらつくのが良いハッシュ関数です。
解り易く解説するために、まず、悪い例から示して説明します。
悪いハッシュ関数の例として、「ハッシュ値をunicodeの上位8ビットとする」場合を考えます。C言語風の式を使うと、ハッシュ関数Hash (x)は、以下の様に表すことができます。
Hash (x)=(x>>8)&0xff
ここで、S-JISに対応する文字が存在するunicode値約7,000文字の文字コードをxに代入して、ハッシュ値Hash (x)を求めます。Hash (x)は0〜255の範囲になりますので、各ハッシュ値に、いくつの文字が要約されたかを調べて棒グラフにすると、以下図となります。
「なるべく均一にばらつく」ことが求められるハッシュ関数ですが、一目瞭然、200文字近く重複したハッシュ値がある一方で、1文字も存在しないハッシュ値も多数存在していて、偏っています。
これではダメ。もっと平均的に、256個(8ビット)のハッシュ値に分散してくれないと……。
今度は、「ハッシュ値をunicodeの下位8ビットとする」場合で試してみます。ハッシュ関数は以下の通りです。
Hash (x)=x&0xff
前節と同様に、分布をグラフ化してみると、以下図となりました。
今回は、見るからに均一に分布しています。最大40文字、最小14文字、平均27.1文字と、統計量で見ると偏りがあるようにも思えますが、結構優秀な方だと思います。
※逆の見方をすると、S-JISコードの文字がunicodeの中で、どれだけ無意味にランダムに分散されて配置されているか?が良く解る結果でもあります。
ハッシュ値の計算は0xffでAND演算するだけ、なんて超簡単ですし、今回は、このハッシュ関数を採用します。
なお、一般的には、ハッシュ関数として一様擬似乱数列の計算式を使う場合が多いです。例えば線形合同法とか、M系列(LFSR)等が使われます。一様な擬似乱数列を生成する計算式を使うことで、ハッシュ値を均一に分布させることができるからです。
前節までに、ハッシュ関数によって16ビットのunicodeを8ビットのハッシュ値で要約できました。でもまだ、要約したことによって、どうして探索が高速化できるのか?と言う、つながりが判りませんよね?それは、本節で変換表を作って見ると解ります。
以下図に変換表の模式図を示し、その後で解説を続けます。
変換表:
変換表のデータサイズは以下の通りです。
unsj_lkup_tab[ ]: 3バイト×約7,000文字≒20.5Kバイト
unsj_hash_tab[ ]: 2バイト×257個=514バイト
合計で約21Kバイトです。unicodeの下位8ビットがハッシュ値と一致することで、テーブルunsj_lkup_tab[ ]にunicodeの下位8ビットを記録する必要がなくなり、結果としてデータ量を圧縮できてしまいました。これは嬉しい誤算。
unicode→S-JIS変換する場合は、以下手順となります。
ハッシュ値と文字数の分布表より、1個のハッシュ値に存在する文字数の最大値は40文字でした。従って、上の手順に必要な処理は、参照2回+探索log2 (40)≒6回です。
これでハッシュ関数を使って要約した効果をご理解いただけたでしょうか?あるunicode値を探索するに当たって、ハッシュ値を計算することで、そのunicode値が存在する変換表の範囲を絞り込むことができ、探索回数を減らすことができたのです。
まとめてみましょう。
方式 | メモリコスト | 探索コスト | 備考 |
unicode基準変換表 | 128Kバイト | 参照1回 | 変換は高速。メモリコストは大きい。 |
S-JIS基準変換表 | 27Kバイト | 探索13回 |
unicode基準変換表に比べて、メモリ コストは小さくなったが、探索が遅くなった。 |
ハッシュ方式変換表 | 21Kバイト | 参照2回+探索6回 |
S-JIS基準変換表に比べて、探索が高速化 した上、メモリコストも小さくなった。 |
二分探索の探索回数は、「最大」の回数です。運が良ければ、最初の探索で所望のデータが見つかる場合があるので、最小探索回数は1回です。それも含めて、最大探索回数6回は十分速く、メモリコストも最小なので、ハッシュ方式変換表を採用して良いでしょう。
HTML等の日本語データを処理する場合、符号化方式はUTF-8のはずなので、関数としてはUTF-8→S-JIS変換関数を作ります。
utf82sjis()関数目標仕様:
評価用のサンプルプログラムは、前回(「#23-4 M5StickC Plus日本語表示」)作成したプログラム(m5cp_kprint.ino)に、UTF-8文字列をTFTディスプレイに描画する様に拡張したtft_kprint()関数と、シリアルモニタコマンド【uprn】を追加しました。
評価用プログラムは以下リンクからダウンロードしてください。
評価用プログラム: | |||
m5cp_kprint.ino | 実験用のサンプルプログラム | ||
m5cp_jpn.cpp | 日本語文字列描画・変換関数等 | ||
m5cp_ps.h | ヘッダファイル | ||
m5cp_jpn_font.cpp | フォントファイル(m5cp_jpn_font.olvを実行して生成) | ||
m5cp_jpn_u2s.cpp | unicode→S-JIS変換表(m5cp_jpn_u2s.olvを実行して生成) |
上の全ファイルをダウンロードして、プログラム(m5cp_kprint.ino)と同じディレクトリに放り込んでください。スクリプトを使って生成する2個のファイルも、同じディレクトリに格納します。
コンパイルするには、ハッシュ関数を使った変換表(m5cp_jpn_u2s.cpp)を生成する必要があります。Excel等の表計算ソフトウェアでも頑張れば作成できますが、今回もOlive+のスクリプトで生成します。
変換表生成スクリプト:m5cp_jpn_u2s.olv
※スクリプト(.olv)の実行には拙作Olive+をWindowsにインストールしてください。
コンパイルしてM5StickC Plusに書き込むと、シリアルモニタから以下のコマンドを受け付けています。
追加のシリアルモニタコマンド仕様:
M5StickC PlusをUSBケーブルでPCに接続してください。オールインワンなので、これだけで準備完了です。
こちらの実験用プログラムをコンパイルしてM5StickC Plusに書き込んでください。
以下、実験中の様子です。
以下の実験は、UTF-8で通信する必要があるので、Arduino IDEのシリアルモニタを使います。
実験サンプル:【 】内はシリアルモニタからの送信データ
【init】
OK: init
【uprn,\x14\x14\x14\r\n】
OK: uprint
画面を初期化して、少し大きめ(\x14=20ドット)のサイズに設定しました。制御コードについてはこちらをご参照ください。
【uprn, こんにちは、世界\r\n\r\n】
OK: uprint
シリアルモニタには何も送られてこないので、TFTディスプレイ画面に「こんにちは、世界」と表示されていることを確認してください。成功していると、見た目では何もわからないのですが、Arduino IDEのシリアルモニタはUTF-8で送信してくるので、ちゃんとUTF-8→S-JIS変換関数が機能していなければ、意味不明な文字が描画されるはずです。
【uprn, S-JIS未定義「🐧」文字\r\n\r\n】
OK: uprint
「」内の絵文字はS-JISに存在しないので、変換エラーを示す外字が描画されます。これもTFTディスプレイでご確認ください。
【uprn, 「ハンカク カタカナ」のテスト\r\n\r\n】
OK: uprint
半角カタカナも正しく変換されていることを確認しました。
こちらのm5cp_ps関数群をライブラリ化する方法を記しておきます。
Arduino IDEで新しいライブラリをダウンロードすると、以下のディレクトリに格納されます。
<Documents>\Arduino\libraries
<Documents>は、WindowsのDocumentsディレクトリへのパスを表しています。
自分のライブラリを作るには、「<Documents>\Arduino\libraries\」に、ライブラリのディレクトリを作成します。
バージョン: M5cp-PS Library ver.1.08 | ||
ライブラリディレクトリ: <Documents>\Arduino\libraries\M5cp-PS\ | ||
サブディレクトリ | ファイル | 備考 |
src\ | m5cp_ps.h | ヘッダファイル |
src\ | m5cp_ps.cpp | 追加関数等 |
src\ | m5cp_jpn.cpp | 日本語文字列描画・変換関数等 |
src\ | m5cp_ps_play_mml.cpp | MML演奏・BGM演奏関数等 |
src\ | m5cp_jpn_font.cpp | フォントファイル(m5cp_jpn_font.olvを実行して生成) |
src\ | m5cp_jpn_u2s.cpp | unicode→S-JIS変換表(m5cp_jpn_u2s.olvを実行して生成) |
.\ | library.properties | ライブラリのプロパティ |
.\ | keywords.txt | キーワードの指定(オプション、無くても良い) |
こうしておくと、Arduino IDEの「スケッチメニュー ⇒ ライブラリをインクルード」のメニューの中に、「M5cp-PS」が見つかる様になります。このライブラリ名は、library.propertiesの中で定義しています。ディレクトリ名と一致させていますが、ディレクトリ名ではありません。
これで、インターネットから取得したHTMLファイルなどがUTF-8で符号化されていたとしても、ハンドリングしてTFTディスプレイに日本語文字列を描画できる環境が整いました。