Adafruit_GFXのデフォルトフォント(glcdfont)について

概要

TFT液晶モジュールにリモコンの操作パネルを描画するためには、図形のほかにフォントも描画してやる必要がある。Adafruit_GFXライブラリには、サンプルとしていくつかのフォントが格納されているが、今回は余計なことを考えずにデフォルトのフォントを使うことにした。

A_PRO_MINI_ILI9341_TFT1
A_PRO_MINI_ILI9341_TFT1

フォントを利用するにあたって、リモコン用に試作した構成を使って、文字表示について調べたりフォントの実体がSRAMではなくフラッシュメモリに格納されているとを確認した話。回路はちょっと前に掲載したものと同様である。

glcdfont.c

デフォルトフォントの実体は、glcdfont.c としてAdafruit_GFXライブラリと一緒に配布されている。glcdfont.cは、ビルド時にライブラリと一緒にコンパイルされリンクされるので、特に指定しなくてもスケッチと共にマイコン側に送り込まれる。

glcdfont.cを開いて見てみると以下のような配列が定義されている。

static const unsigned char font[] PROGMEM = { … }

この配列には、固定長のビットマップフォントの実体が1文字あたり5バイトずつ256行にわたって記述されている。各フォントの大きさが 5 × 7ドットなので、1文字あたり40ビットあれば十分なのである。

この配列には、PROGMEM というキーワードが付いている。このキーワードが付いたデータブロックは、SRAMではなくプログラムと同じフラッシュメモリに配置される。PROGMEMについては後述する。

フォントの表示

まずは以下のようなスケッチを書いて、TFT液晶にフォントを表示してみた。

#include <Adafruit_GFX.h>
#include <Adafruit_ILI9341.h>
#define TFT_RST 6
#define TFT_DC 7
#define TFT_CS 5
Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC);
#define LED_PIN 4

#define TEXT_COLOR  ILI9341_NAVY
#define BG_COLOR    ILI9341_GREENYELLOW
#define BG_COLOR_EVEN    ILI9341_CYAN

void dump_fonts() {
  enum { x_ofs = 4, y_ofs = 8, x_size = 13, y_size = 17 };
  tft.fillScreen(BG_COLOR);
  tft.cp437(true);
  tft.setTextSize(2);
  int i;
  char tmp[5];
  tft.setTextColor(TEXT_COLOR, BG_COLOR_EVEN);
  for(int y = 0; y < 16; y++) {
    if (y == 0) {
      for(i = 0; i < 16; i++) { 
        sprintf(tmp, "%X  ",i);        
        tft.setCursor((i + 1) * x_size + x_ofs * 2, y_ofs);
        tft.print(tmp);
      }
    }
    if (y % 2 == 1)
      tft.setTextColor(TEXT_COLOR, BG_COLOR_EVEN);
    else
      tft.setTextColor(TEXT_COLOR, BG_COLOR);
    for(i = 0; i < 16; i++) {
      if (i == 0) {
        sprintf(tmp, "%X  ", y);        
        tft.setCursor(x_ofs, (y + 1) * y_size + y_ofs);
        tft.print(tmp);
      }
      tft.setCursor((i + 1) * x_size + x_ofs * 2, (y + 1) * y_size + y_ofs);
      sprintf(tmp, "%c ", (y * 16 + i) & 0xff);
      tft.print(tmp);
    }
  }
  tft.drawFastHLine(x_ofs, y_ofs + y_size - 2 , x_size * 17, TEXT_COLOR);
  tft.drawFastVLine(x_ofs + x_size + 1, y_ofs , y_size * 17, TEXT_COLOR);
}

void setup() {
  pinMode(LED_PIN, OUTPUT);
  pinMode(2, INPUT_PULLUP);
  pinMode(TFT_RST, OUTPUT);
  digitalWrite(TFT_RST, 0);  
  delay(10);
  digitalWrite(TFT_RST, 1);  
  delay(10);
  tft.begin();
  tft.setTextWrap(false);
  tft.setRotation(2);
  digitalWrite(LED_PIN, 1); // back-light on.
  dump_fonts();
}
void loop() { }

TFTモジュール用他のI/Oポートの初期化やバックライトの点灯などについては以前と同様。実行すると、以下のように表示される。

glcdfont / cp437 = true;

横軸が左から右に下位4ビット、縦軸が上から下に上位4ビットのマトリクスになっており、glcdfontに含まれている256種類の文字を表示している。なお、0x00と0xffはいずれも空白で0x20と同じ。

制御文字の領域までいろいろなキャラクタが含まれており、ちょっとしたリモコンのボタンを書くには良さそうである。

かつてglcdfont.cにあった問題点

先ほどのスケッチの16行目に以下のように書いた。

tft.cp437(true);

この行により、コードページ437(CP437)のキャラクタセットを指定している。CP437は、むかしPC-DOSのプログラムで使われていた、今となっては懐かしいキャラクタセットの一つである。もともと、glcdfontはCP437用に作られたものだと思うのだけど、わざわざ cp437(true); としなければならない理由は、そうしないと文字が1つ欠落し以降のコードがずれてしまうためである。

ためしに、tft.cp437(false);  として実行してみると、以下のようになる。

glcdfont / cp437 = false;

一見すると同じに見えるのだが、0xB0以降のコードとキャラクタの対応がずれてしまってCP437ではなくなっている。

このことは、Adafruit_GFX.cpp の中のコメントでも言及されている。想像するに、開発当初に0xB0に対応するキャラクタ(Light shade) を入れ忘れてしまい、次の Medium shade が0xB0になって以降もずれてしまった、ということではないかな。

現在配布されているglcdfont.cには最初の図のように0xB0にLight Shadeキャラクタが入っている。ただ、Adafruit_GFX.cpp内には、あえて tft.cp437(true);  と書かない限り2番目の図のようなキャラクタセットになるようなプログラムが書かれている。修正以前のキャラクタコードを使ったスケッチが多数存在しているから、ということらしい。

CP437の各キャラクタについては、https://www.ascii-codes.com/ を参照した。

PROGMEMとデータの配置

上に載せたスケッチをArduino IDEでビルドすると、メッセージ領域には以下のようなメモリ情報が表示された。

最大30720バイトのフラッシュメモリのうち、スケッチが10884バイト(35%)を使っています。
最大2048バイトのRAMのうち、グローバル変数が115バイト(5%)を使っていて、ローカル変数で1933バイト使うことができます。

「グローバル変数」はわずか115バイトしか使っていないようである。Arduino ではスケッチで定義した配列などのデータは RAM に置かれるはずなのだが、glcdfont 用の1280バイト ( 5 × 256バイト)はここに含まれていない。PROGMEMキーワードの指定により、コード領域と同じフラッシュメモリに配置されたようである。確認するために、avr-nm.exe というツールを使って、ビルド結果のelfファイルから各シンボルの配置を見てみた。

avr-nmの実行

avr-nm.exeは、うちのWindows PCでは “C:\Program Files (x86)\Arduino\hardware\tools\avr\bin” に格納されている。このフォルダにはAVRマイコン向けの各種ビルドツールや転送ツールなどが格納されている。

※ 標準インストールした、Arduino 1.8.3を前提としている。

このツールは、コンパイルされリンクされたオブジェクトファイル(ELFファイル : Executable and Linkable Format) からシンボルとそのアドレスおよびサイズを抜き出してリスト表示してくれる。

Arduino IDEでビルドしたオブジェクトファイルは Windowsの場合、C:\Users\{user-name}\AppData\Local\Temp フォルダ内の、”arduino_build_NNNNNN”(Nは数字) という名前のビルドフォルダに格納されている。
このフォルダは、Arduino IDEを開始しスケッチを開いてビルドするごとに作成されるようなので、ビルド直後に”arduino_buid*”を検索してやれば、注目したいスケッチに対応するフォルダはすぐに見つかるだろう。

例えばtest.ino というスケッチをビルドすると、test.ino.elf というオブジェクトファイルが出来上がっている。これを avr-nm にかけるときは、コマンドプロンプトを開いて以下のようにする。

avr-nm.exe -C -n -S test.ino.elf

必要に応じて各ファイルにはフォルダ名を与えること。avr-nmに与えているオプションは以下のような意味をもつ。

  • -C : シンボル名を定義と同様に読みやすく出力する。
  • -n : シンボルのアドレス順にソートする。
  • -S : シンボルがサイズをもつ場合、サイズを出力。

コマンドプロンプトでふつうに実行すると長々とリストが表示されて読みにくいので、出力をテキストファイルにリダイレクトして書き出すなりした方が読みやすい。

省略を混じえながら出力をコピーすると以下のようになっている。

         w serialEventRun()
00000000 W __heap_end
00000000 a __tmp_reg__
....
00000068 T __trampolines_end
00000068 T __trampolines_start
00000068 00000500 t font
00000400 A __LOCK_REGION_LENGTH__
....
0000060e 0000004a t micros
00000658 0000007e t Adafruit_ILI9341::fillRect(int, int, int, int, unsigned int)
000006d6 00000076 T Adafruit_ILI9341::drawFastHLine(int, int, int, unsigned int)
....
00002a4a T _exit
00002a4a W exit
00002a4c t __stop_program
00002a4e A __data_load_start
00002a4e T _etext
00002a84 A __data_load_end
....
00800100 D __data_start
00800100 0000002c d vtable for Adafruit_ILI9341
00800136 B __bss_start
00800136 D __data_end
....
0080013f 00000033 B tft
00800172 00000001 b SPIClass::initialized
00800173 B __bss_end
00800173 N _end
00810000 N __eeprom_end

各行は、アドレス [サイズ] タイプ シンボル の順に並んでいる。タイプTおよびtがテキスト(プログラムコード)領域、Dおよびdが初期値を定義済みのデータ、Bおよびbが未初期化のデータを表している(大文字がexternな外部参照可能シンボル、小文字がstaticな外部参照無しのシンボルのようである)。A,N,Wなどについては、avr-nmのmanを参照のこと。

テキスト領域

先頭の0x00000000からテキスト領域(プログラム領域)が開始しており、この領域のデータ(コード)はフラッシュメモリに格納される。

注目すべきは7行目の 00000068 00000500 t font  という行で、テキスト領域の0x0068から0x500バイトにわたって font という名前のデータが占めている。glcdfont.c に宣言されたフォントデータがPROGMEM指定によってテキスト領域に配置されたことを示している。

このシンボルリストから見ると、__data_load_endの0x00002a84 までがテキスト領域のようで、十進数に直すと 10884となる。Arduino IDEが表示した「スケッチが10884バイト(35%)を使っています。」と一致している。

データ領域

__data_startの0x800100から2KバイトのRAMの終端(0x008008ff)までが マイコン内のSRAM領域となっている。ツールの便宜上? 0x00800000からアドレスがふられているが、マイコン内部のデータ領域は0x0000番地から開始する(プログラム領域とデータ領域のアドレス空間は独立している)。
スケッチやライブラリ内の通常のデータはこの領域に配置される。__data_startから__bss_endの間にAdafruit_ILI9341クラスのvtableやスケッチで宣言したtftといった変数が入っているのが見える。__bss_end以降SRAMのおしまいまでが、ヒープ領域やスタック領域として使われる。

なお、0x0000から0x0100までの256バイトのアドレス領域は、マイコンの各種レジスタとて使われており、データは配置されない。

__data_startから__bss_endまでの大きさは115バイトなので、これも「最大2048バイトのRAMのうち、グローバル変数が115バイト(5%)を使っていて、ローカル変数で1933バイト使うことができます。」に一致している。ここでいう「ローカル変数」には、実行制御に必要なスタック領域、スタック領域に確保されるデータ、malloc() や strdup() を使ってヒープ領域に確保するデータなどが含まれるから、Arduinoがいいと言っても用心深く使った方がよいだろう。