Font to Binary Converter ツールを開く

ドットフォントの配列の読み方:行方向・列方向のビットパックと MSB / LSB

公開: · 作者: takushio2525

ドットフォントの配列は、同じ文字でも「どの向きにドットを詰めるか」「1 バイトのどちらの端から詰めるか」で数値がまったく変わります。表示に使うライブラリが期待する並びと違うと、文字が反転したり崩れたりします。ここでは美咲フォントの「A」(8×8)を例に、Font to Binary Converter の出力の読み方と、ライブラリごとに選ぶ設定をまとめます。

美咲フォントの「A」(8×8)。以下の例はすべてこの文字をツールで変換したものです

0 と 1 の表(3 次元配列)

かんたんモードの「C言語(見やすい2D)」は、ドットをそのまま 0 と 1 で並べます。[文字][行][列] の 3 次元配列で、1 が点灯するドットです。

const uint8_t font_data[1][8][8] = {
      // char: "A" (8x8)
      // ...#....
      // ..#.#...
      // ..#.#...
      // ..#.#...
      // ..###...
      // ..#.#...
      // ..#.#...
      // ........
    {
        {0, 0, 0, 1, 0, 0, 0, 0},
        {0, 0, 1, 0, 1, 0, 0, 0},
        {0, 0, 1, 0, 1, 0, 0, 0},
        {0, 0, 1, 0, 1, 0, 0, 0},
        {0, 0, 1, 1, 1, 0, 0, 0},
        {0, 0, 1, 0, 1, 0, 0, 0},
        {0, 0, 1, 0, 1, 0, 0, 0},
        {0, 0, 0, 0, 0, 0, 0, 0}
    }
};

読みやすい反面、1 ドットに 1 バイト(uint8_t)を使うので、8×8 で 64 バイト、16×16 では 256 バイトになります。マイコンで使うときは、次のビットパックで 8 ドットを 1 バイトに詰めるのが普通です。

行方向のビットパック

横 1 行の 8 ドットを 1 バイトに詰めます。1 文字のデータは、上の行から順に並びます。詳細モードの「構造」で「ビットパック(行方向)」を選ぶと、この形になります。

const uint8_t font_data[] = {
  // char: "A" (8x8)
  // ...#....
  // ..#.#...
  // ..#.#...
  // ..#.#...
  // ..###...
  // ..#.#...
  // ..#.#...
  // ........
  0b00010000, 0b00101000, 0b00101000, 0b00101000, 0b00111000, 0b00101000, 0b00101000, 0b00000000
};

MSB first と LSB first

「ビット順」は、行の左端のドットを 1 バイトのどちらの端に置くかを決めます。MSB first は左端を最上位ビット(bit7)に、LSB first は左端を最下位ビット(bit0)に置きます。同じ行 ...#.... でも、MSB first なら 0b00010000(0x10)、LSB first なら 0b00001000(0x08)です。

行ドットMSB firstLSB first
0...#....0x100x08
1..#.#...0x280x14
2..#.#...0x280x14
3..#.#...0x280x14
4..###...0x380x1C
5..#.#...0x280x14
6..#.#...0x280x14
7........0x000x00

幅が 8 の倍数でないとき

1 行に「幅 ÷ 8 の切り上げ」バイトを使い、余ったビットは 0 で埋めます。次の 12×12 の例では 1 行が 2 バイトになり、MSB first なので各行の 2 バイト目の下位 4 ビットは常に 0 です(「A」の形が粗いのは、16×16 用に作られた DotGothic16 を 12 ドットに縮めたためです。フォントは推奨の大きさ、美咲フォントなら 8、DotGothic16 なら 16 で使うときれいに出ます)。

const uint8_t font_data[] = {
  // char: "A" (12x12)
  // ............
  // .....##.....
  // .....##.....
  // .....##.....
  // .....##.....
  // ....####....
  // ....#..#....
  // ....#..#....
  // ....####....
  // ...##...#...
  // ...#....#...
  // ............
  0b00000000, 0b00000000, 0b00000110, 0b00000000, 0b00000110, 0b00000000, 0b00000110, 0b00000000, 0b00000110, 0b00000000, 0b00001111, 0b00000000, 0b00001001, 0b00000000, 0b00001001, 0b00000000,
  0b00001111, 0b00000000, 0b00011000, 0b10000000, 0b00010000, 0b10000000, 0b00000000, 0b00000000
};

1 文字のバイト数 = 「幅 ÷ 8 の切り上げ」× 高さ。8×8 なら 8 バイト、12×12 なら 24 バイト、16×16 なら 32 バイトです。

列方向のビットパック

縦 1 列の 8 ドットを 1 バイトに詰めます。1 文字のデータは、左の列から順に並びます。ビット順は、MSB first なら上端のドットが bit7、LSB first なら上端が bit0 です。次は LSB first の例です。

const uint8_t font_data[] = {
  // char: "A" (8x8)
  // ...#....
  // ..#.#...
  // ..#.#...
  // ..#.#...
  // ..###...
  // ..#.#...
  // ..#.#...
  // ........
  0b00000000, 0b00000000, 0b01111110, 0b00010001, 0b01111110, 0b00000000, 0b00000000, 0b00000000
};

左端を 0 として列を数えると、列 2(「A」の左の縦線)は 1〜6 行目が点灯しているので、LSB first では bit1〜bit6 が 1 の 0b01111110 になります。列 3(頂点と横棒がある列)は 0 行目と 4 行目が点灯していて、LSB first では 0b00010001、MSB first にすると 0b10001000 です。

const uint8_t font_data[] = {
  // char: "A" (8x8)
  // ...#....
  // ..#.#...
  // ..#.#...
  // ..#.#...
  // ..###...
  // ..#.#...
  // ..#.#...
  // ........
  0b00000000, 0b00000000, 0b01111110, 0b10001000, 0b01111110, 0b00000000, 0b00000000, 0b00000000
};

高さが 8 を超えるとき

1 列に「高さ ÷ 8 の切り上げ」バイトを使い、列ごとに「上の 8 ドット → 下の 8 ドット」の順で並びます。16×16 なら、0 列目の上・0 列目の下・1 列目の上… の順で 32 バイトです。

const uint8_t font_data[] = {
  // char: "あ" (16x16)
  // ......#.........
  // ......#.........
  // ..############..
  // ..############..
  // ......#.....#...
  // ......#######...
  // ....#########...
  // ...####...####..
  // ..##..#..##..##.
  // .##...#..#....#.
  // .#....####....#.
  // .#....###.....#.
  // .#...###.....##.
  // .#######....##..
  // ..###..#.####...
  // .........###....
  0x00, 0x00, 0x00, 0x3E, 0x0C, 0x63, 0x8C, 0x61, 0xCC, 0x60, 0xCC, 0x30, 0xFF, 0x3F, 0x6C, 0x7C,
  0x6C, 0x0C, 0x6C, 0xC7, 0xEC, 0xC1, 0xEC, 0xC0, 0xFC, 0x60, 0x8C, 0x31, 0x00, 0x1F, 0x00, 0x00
};

1 文字のバイト数 = 幅 ×「高さ ÷ 8 の切り上げ」。

SSD1306 の画面バッファとの関係

SSD1306 の表示メモリは、縦 8 ドットを 1 バイト(最下位ビットが上)にしたものが横に 128 バイト並んだ「ページ」を、128×64 なら 8 段重ねた形です。Adafruit_SSD1306 の画面バッファも同じ並びで、座標 (x, y) のドットは buffer[x + (y / 8) * WIDTH] の bit (y & 7) に入ります(Adafruit_SSD1306.cpp の drawPixel)。

そのため、列方向・LSB first・高さ 8 の出力は、1 ページぶんの並びとそのまま同じです。高さ 16 の出力は、上の 8 ドットと下の 8 ドットが列ごとに交互に並んでいるので、ページに書き込むときは次のように並べ替えます(Adafruit_SSD1306 の getBuffer() で画面バッファに直接書く例です)。

// 列方向・LSB の 16×16(1 列 2 バイト: 上の 8 ドット、下の 8 ドット)を、
// 画面バッファ(縦 8 ドット × 横 128 列の「ページ」が 8 段)の page0 段目・x0 列目から書き込む
void putGlyph16(uint8_t x0, uint8_t page0, const uint8_t *glyph) {
  uint8_t *buf = display.getBuffer();
  for (uint8_t page = 0; page < 2; page++) {
    for (uint8_t x = 0; x < 16; x++) {
      buf[(page0 + page) * 128 + x0 + x] = pgm_read_byte(glyph + x * 2 + page);
    }
  }
}

ふつうは行方向のデータを drawBitmap で描けば並びを気にせずに済みます(「Arduino と OLED で日本語を表示する」)。画面バッファに直接書くのは、描く処理を軽くしたいときなどです。

ライブラリごとの設定

よく使う描画の関数・形式と、ツールで選ぶ設定の対応です。右の列は、各ライブラリのソースで読み方を確かめた箇所です。

描き方構造ビット順ソースで確かめた箇所
Adafruit_GFX の drawBitmap行方向MSB firstAdafruit_GFX.cpp(b & 0x80)
Adafruit_GFX の drawXBitmap行方向LSB first同(b & 0x01)
U8g2 の drawXBMP行方向LSB firstu8g2_bitmap.c(mask = 1 から左へ)
U8g2 の drawBitmap行方向MSB first同(mask = 128 から)。PROGMEM の配列は読めない
MicroPython framebuf の MONO_HLSB行方向MSB firstmodframebuf.c(7 - (x & 0x07))
MicroPython framebuf の MONO_HMSB行方向LSB first同(x & 0x07)
MicroPython framebuf の MONO_VLSB(SSD1306 のドライバが画面に使う形式)列方向・高さ 8LSB first同(y & 0x07)
LedControl の setRow行方向・幅 8MSB firstLedControl.cpp(B10000000 >> column)
LedControl の setColumn列方向・高さ 8MSB first同(value >> (7-row))
SSD1306 の画面バッファに直接書く列方向・高さ 8LSB firstAdafruit_SSD1306.cpp(1 << (y & 7))

この表のとおりに各ライブラリが配列を読んだとき、ツールのドット絵と同じ形に戻ることを、この記事の出力例で確かめています。なお、行方向の形式はどれも、幅が 8 の倍数でない行をバイトの区切りまで埋めて読むので、ツールの出力(余りを 0 で埋める)とそのまま合います。

かんたんモードのプリセットは、それぞれ次の設定です。

  • Arduino / C言語・MicroPython / bytes: 行方向・MSB first・16 進
  • C言語(見やすい2D)・Python リスト・JSON: 0 と 1 の 3 次元配列
  • 記号アート(# と .): 数値ではなく、確かめる用のドット絵

MicroPython で使う例

「MicroPython / bytes」を選ぶと、1 文字ぶんを 1 つのリストにした形で出力します。

font_data = [
    # char: "A" (8x8)
    [
        0x10, 0x28, 0x28, 0x28, 0x38, 0x28, 0x28, 0x00
    ]
]

行方向・MSB first なので、1 文字ぶんを bytearray にして MONO_HLSB の FrameBuffer を作れば、SSD1306 のドライバ(micropython-lib の ssd1306)の blit で描けます。

from machine import Pin, I2C
import framebuf
import ssd1306

# ESP32 の例。SCL・SDA のピンはボードに合わせて変える
i2c = I2C(0, scl=Pin(22), sda=Pin(21))
oled = ssd1306.SSD1306_I2C(128, 64, i2c)

# ここにツールの出力(font_data = [...])を貼る

glyph = framebuf.FrameBuffer(bytearray(font_data[0]), 8, 8, framebuf.MONO_HLSB)
oled.blit(glyph, 0, 0)
oled.show()

そのほかの設定

  • 0/1 反転: 点灯と消灯を入れ替えます。白地に黒い文字で描きたいときや、0 のビットで点灯する表示器に使います。
  • 表記(10 進数・16 進数・2 進数): 値は同じで、書き方だけが変わります。2 進の 0b… は C++14 からの書き方で、Arduino(GCC)ではそのまま使えます。C では C23 から標準になり、それより前は GCC などの拡張です。
  • 文字コメントを挿入・ASCIIアートも含める: 各文字の前に // char: "A" (8x8) とドット絵のコメントを入れます。配列のどこが何の文字かを確かめるのに便利です。
  • 2次元配列 (行×列): 1 文字目だけを [行][列] の配列で出します。1 文字だけのアイコンなどに使います。