ドットフォントの配列の読み方:行方向・列方向のビットパックと MSB / LSB
ドットフォントの配列は、同じ文字でも「どの向きにドットを詰めるか」「1 バイトのどちらの端から詰めるか」で数値がまったく変わります。表示に使うライブラリが期待する並びと違うと、文字が反転したり崩れたりします。ここでは美咲フォントの「A」(8×8)を例に、Font to Binary Converter の出力の読み方と、ライブラリごとに選ぶ設定をまとめます。
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 first | LSB first |
|---|---|---|---|
| 0 | ...#.... | 0x10 | 0x08 |
| 1 | ..#.#... | 0x28 | 0x14 |
| 2 | ..#.#... | 0x28 | 0x14 |
| 3 | ..#.#... | 0x28 | 0x14 |
| 4 | ..###... | 0x38 | 0x1C |
| 5 | ..#.#... | 0x28 | 0x14 |
| 6 | ..#.#... | 0x28 | 0x14 |
| 7 | ........ | 0x00 | 0x00 |
幅が 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 first | Adafruit_GFX.cpp(b & 0x80) |
Adafruit_GFX の drawXBitmap | 行方向 | LSB first | 同(b & 0x01) |
U8g2 の drawXBMP | 行方向 | LSB first | u8g2_bitmap.c(mask = 1 から左へ) |
U8g2 の drawBitmap | 行方向 | MSB first | 同(mask = 128 から)。PROGMEM の配列は読めない |
MicroPython framebuf の MONO_HLSB | 行方向 | MSB first | modframebuf.c(7 - (x & 0x07)) |
MicroPython framebuf の MONO_HMSB | 行方向 | LSB first | 同(x & 0x07) |
MicroPython framebuf の MONO_VLSB(SSD1306 のドライバが画面に使う形式) | 列方向・高さ 8 | LSB first | 同(y & 0x07) |
LedControl の setRow | 行方向・幅 8 | MSB first | LedControl.cpp(B10000000 >> column) |
LedControl の setColumn | 列方向・高さ 8 | MSB first | 同(value >> (7-row)) |
| SSD1306 の画面バッファに直接書く | 列方向・高さ 8 | LSB first | Adafruit_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 文字だけのアイコンなどに使います。