Arduino と OLED(SSD1306)で日本語を表示する:美咲フォント 8×8 を配列にして drawBitmap で描く
メニューの項目や「こんにちは」のような決まった言葉だけを OLED に出したいなら、日本語フォントを丸ごと入れなくても、使う文字のドットだけを配列で持てば足ります。美咲フォントなら 1 文字 8 バイトなので、5 文字でも 40 バイトです。この記事では、Font to Binary Converter でその配列を作り、Arduino Uno と SSD1306 の OLED に表示するまでを説明します。
この方法が向いている場面
- 向いている: 表示する文字が前もって決まっているとき(メニュー、状態の表示、あいさつ、単位など)。使う文字の分のデータだけで済むので、フラッシュの小さいマイコンでも収まります。
- 向いていない: 受け取った文章をそのまま表示するなど、どの文字が来るか分からないとき。この場合は、フォント全体を持つ表示ライブラリを使うほうが確実です。
用意するもの
- Arduino Uno(ATmega328P)など。この記事のスケッチは、Arduino IDE 同梱の arduino-cli で Uno 向けにコンパイルできることを確かめています
- SSD1306 を使った 128×64 ドットの OLED モジュール(I2C 接続)。Uno では SDA を A4、SCL を A5 につなぎます
- Arduino IDE のライブラリマネージャで入れる「Adafruit SSD1306」と「Adafruit GFX Library」(依存する Adafruit BusIO も一緒に入れます)
配列を作る
- Font to Binary Converter を開き、「かんたん」モードのまま使います
- 「1 変換したい文字」に
こんにちはと入力します - 「2 見た目」のフォントで「Misaki Gothic (8x8)」を選びます(ドットサイズも 8 になります)
- 「3 出力フォーマット」で「Arduino / C言語」を選び、出力の「コピー」を押します
出力は次のようになります(ツールで実際に出したものです)。
const uint8_t font_data[] PROGMEM = {
// char: "こ" (8x8)
// ........
// .#####..
// ........
// ........
// .#......
// #.......
// .######.
// ........
0x00, 0x7C, 0x00, 0x00, 0x40, 0x80, 0x7E, 0x00,
// char: "ん" (8x8)
// ...#....
// ...#....
// ..#.....
// ..##....
// .#..#.#.
// .#..#.#.
// #....#..
// ........
0x10, 0x10, 0x20, 0x30, 0x4A, 0x4A, 0x84, 0x00,
// char: "に" (8x8)
// #.......
// #..###..
// #.......
// #.......
// #..#....
// #.#.....
// #..####.
// ........
0x80, 0x9C, 0x80, 0x80, 0x90, 0xA0, 0x9E, 0x00,
// char: "ち" (8x8)
// ..#.....
// #######.
// ..#.....
// .#.###..
// .##...#.
// .....#..
// ..###...
// ........
0x20, 0xFE, 0x20, 0x5C, 0x62, 0x04, 0x38, 0x00,
// char: "は" (8x8)
// #...#...
// #.#####.
// #...#...
// #...#...
// #.###...
// ##..##..
// #.###.#.
// ........
0x88, 0xBE, 0x88, 0x88, 0xB8, 0xCC, 0xBA, 0x00
};
1 文字ぶんは、コメントのドット絵(# が点灯)と 8 個の数値です。数値 1 つが横 1 行の 8 ドットで、左端のドットが最上位ビット(0x80)に入っています。たとえば「こ」の 2 行目 .#####.. は 2 進で 01111100、16 進で 0x7C です。PROGMEM が付いているので、配列は SRAM ではなくフラッシュメモリに置かれます。
スケッチ
出力を貼り付けたスケッチの全体です。画面の左上から 5 文字を横に並べて描きます。
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
// 128×64 ドット・I2C 接続の OLED。リセットピンは使わない(-1)
Adafruit_SSD1306 display(128, 64, &Wire, -1);
// ↓ Font to Binary Converter の出力をそのまま貼る
const uint8_t font_data[] PROGMEM = {
// char: "こ" (8x8)
// ........
// .#####..
// ........
// ........
// .#......
// #.......
// .######.
// ........
0x00, 0x7C, 0x00, 0x00, 0x40, 0x80, 0x7E, 0x00,
// char: "ん" (8x8)
// ...#....
// ...#....
// ..#.....
// ..##....
// .#..#.#.
// .#..#.#.
// #....#..
// ........
0x10, 0x10, 0x20, 0x30, 0x4A, 0x4A, 0x84, 0x00,
// char: "に" (8x8)
// #.......
// #..###..
// #.......
// #.......
// #..#....
// #.#.....
// #..####.
// ........
0x80, 0x9C, 0x80, 0x80, 0x90, 0xA0, 0x9E, 0x00,
// char: "ち" (8x8)
// ..#.....
// #######.
// ..#.....
// .#.###..
// .##...#.
// .....#..
// ..###...
// ........
0x20, 0xFE, 0x20, 0x5C, 0x62, 0x04, 0x38, 0x00,
// char: "は" (8x8)
// #...#...
// #.#####.
// #...#...
// #...#...
// #.###...
// ##..##..
// #.###.#.
// ........
0x88, 0xBE, 0x88, 0x88, 0xB8, 0xCC, 0xBA, 0x00
};
const uint8_t CHAR_W = 8; // 1 文字の幅(ドット)
const uint8_t CHAR_H = 8; // 1 文字の高さ(ドット)
const uint8_t BYTES_PER_CHAR = (CHAR_W + 7) / 8 * CHAR_H; // 1 文字のバイト数(8×8 なら 8)
const uint8_t CHAR_COUNT = sizeof(font_data) / BYTES_PER_CHAR;
void setup() {
// 128×64 のモジュールの多くは I2C アドレス 0x3C(0x3D のものもある)
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
for (;;) {} // 初期化に失敗したら止める(配線とアドレスを確かめる)
}
display.clearDisplay();
for (uint8_t i = 0; i < CHAR_COUNT; i++) {
// i 文字目のデータの先頭を渡して、8×8 のビットマップとして描く
display.drawBitmap(i * CHAR_W, 0, font_data + i * BYTES_PER_CHAR, CHAR_W, CHAR_H, SSD1306_WHITE);
}
display.display(); // ここで初めて画面に送られる
}
void loop() {}
drawBitmapの 3 番目の引数に、font_data + i * BYTES_PER_CHAR(i 文字目のデータの先頭)を渡しています。const uint8_tの配列を渡すと、drawBitmapはフラッシュから読み出す処理(pgm_read_byte)で読むので、PROGMEM の配列をそのまま渡せます。display.display()を呼ぶまで、画面は変わりません。drawBitmapは画面バッファに描くだけです。- Adafruit_SSD1306 は
begin()のときに、128×64 なら 1,024 バイトの画面バッファを確保します。Uno の SRAM は 2,048 バイトしかないので、フォントのデータを PROGMEM でフラッシュに置いておくのが大事です。このスケッチをコンパイルすると、グローバル変数は 344 バイトでした(画面バッファは実行時に確保されるので、この数字には入りません)。
なぜこの設定で合うのか
Adafruit_GFX の drawBitmap は、ビットマップを上の行から順に読み、1 行を「(幅 + 7) / 8」バイトとして、各バイトの最上位ビット(0x80)から左→右へ描きます(ライブラリの Adafruit_GFX.cpp の drawBitmap でこの処理を確かめられます)。「Arduino / C言語」の出力は次の設定なので、この読み方とそのまま一致します。
| 設定 | 値 | 意味 |
|---|---|---|
| 構造 | ビットパック(行方向) | 横に並んだ 8 ドットを 1 バイトに詰める |
| ビット順 | MSB first | 左端のドットを最上位ビットに入れる |
| 表記 | 16進数 | 書き方だけの違いで、10 進・2 進にしても値は同じ |
幅が 8 の倍数でないとき(たとえば 12 ドット)は 1 行が 2 バイトになり、余ったビットは 0 で埋まります。drawBitmap も 1 行を「(幅 + 7) / 8」バイトとして読むので、このときもそのまま渡せます。スケッチの BYTES_PER_CHAR も同じ式で計算しています。並び方の詳しい説明は「出力の読み方」にまとめています。
文字を大きく表示する
8×8 の文字は、128×64 の画面に 1 行 16 文字入る大きさで、小さく感じることがあります。大きくする方法は 2 つあります。
DotGothic16(16×16)で作り直す
フォントで「DotGothic16 (16x16)」を選ぶと、ドットサイズが 16 になり、1 文字 32 バイトの配列になります。スケッチの CHAR_W と CHAR_H を 16 に変えれば、BYTES_PER_CHAR は自動で 32 になり、同じ drawBitmap で描けます。
8×8 のデータのまま、1 ドットを 2×2 に広げて描く
データを増やさずに大きく見せたいときは、1 ドットを fillRect で 2×2 の四角として描きます。上のスケッチに次の関数を足し、drawBitmap の代わりに drawChar2x(i * 16, 0, font_data + i * 8) のように呼びます。
// 1 ドットを 2×2 に広げて描く(8×8 の文字を 16×16 の大きさで表示する)
void drawChar2x(int16_t x, int16_t y, const uint8_t *glyph) {
for (uint8_t row = 0; row < 8; row++) {
uint8_t bits = pgm_read_byte(glyph + row); // 1 行ぶん(bit7 が左端)
for (uint8_t col = 0; col < 8; col++) {
if (bits & (0x80 >> col)) {
display.fillRect(x + col * 2, y + row * 2, 2, 2, SSD1306_WHITE);
}
}
}
}
この関数は、配列の読み方(1 バイト = 1 行、bit7 が左端)をそのまま書いたものです。3 倍・4 倍にしたいときは、2 のところを変えます。
U8g2 を使う場合
U8g2 ライブラリで描くときは、drawXBMP を使います。drawXBMP は XBM 形式、つまり各バイトの最下位ビットが左端のデータを読みます(u8g2_bitmap.c の u8g2_DrawHXBMP)。そのため、ツールで「詳細」モードに切り替え、「ビット順」を「LSB first」にして出力し直します。
#include <U8g2lib.h>
#include <Wire.h>
// 128×64・I2C 接続の SSD1306。F はバッファを全画面ぶん持つ版
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);
// ↓ ビット順を LSB first にして出力したもの
const uint8_t font_data[] PROGMEM = {
// char: "こ" (8x8)
// ........
// .#####..
// ........
// ........
// .#......
// #.......
// .######.
// ........
0x00, 0x3E, 0x00, 0x00, 0x02, 0x01, 0x7E, 0x00,
// char: "ん" (8x8)
// ...#....
// ...#....
// ..#.....
// ..##....
// .#..#.#.
// .#..#.#.
// #....#..
// ........
0x08, 0x08, 0x04, 0x0C, 0x52, 0x52, 0x21, 0x00,
// char: "に" (8x8)
// #.......
// #..###..
// #.......
// #.......
// #..#....
// #.#.....
// #..####.
// ........
0x01, 0x39, 0x01, 0x01, 0x09, 0x05, 0x79, 0x00,
// char: "ち" (8x8)
// ..#.....
// #######.
// ..#.....
// .#.###..
// .##...#.
// .....#..
// ..###...
// ........
0x04, 0x7F, 0x04, 0x3A, 0x46, 0x20, 0x1C, 0x00,
// char: "は" (8x8)
// #...#...
// #.#####.
// #...#...
// #...#...
// #.###...
// ##..##..
// #.###.#.
// ........
0x11, 0x7D, 0x11, 0x11, 0x1D, 0x33, 0x5D, 0x00
};
void setup() {
u8g2.begin();
u8g2.clearBuffer();
for (uint8_t i = 0; i < sizeof(font_data) / 8; i++) {
u8g2.drawXBMP(i * 8, 0, 8, 8, font_data + i * 8);
}
u8g2.sendBuffer();
}
void loop() {}
-
U8g2 には
drawBitmapもありますが、この関数はフラッシュから読み出す処理を通らず、ポインタの先をそのまま読みます。そのため、Uno などの AVR で PROGMEM に置いた配列には使えません。PROGMEM の配列はdrawXBMPで描きます。 -
コンストラクタ名に
_F_が付く版は画面バッファ 1,024 バイトを最初から確保するので、このスケッチのグローバル変数は 1,556 バイト(Uno の SRAM の 75%)でした。メモリが足りなくなったら、_1_や_2_が付くページバッファ版に替える方法があります(描き方がfirstPage()/nextPage()のループに変わります)。
うまく表示されないとき
| 症状 | 確かめること |
|---|---|
| 何も表示されない | I2C アドレス(多くは 0x3C、モジュールによっては 0x3D)、配線(Uno は SDA が A4・SCL が A5)、display.display() の呼び忘れ |
| 文字が左右反転して見える | ビット順。drawBitmap は MSB first、drawXBMP は LSB first のデータを読む |
| 文字が斜めにずれて崩れる | ツールで選んだドットサイズと、スケッチの CHAR_W・CHAR_H が揃っているか |
| 違う文字が出る | 配列の文字は入力した順に並ぶ。コメントの char: で順番を確かめる |
| 動きが不安定・勝手に再起動する | SRAM の不足。配列に PROGMEM が付いているか、ほかに大きな配列を SRAM に置いていないか |
美咲フォントについて
美咲フォントは、門真なむ氏が作った 8×8 ドットの日本語ビットマップフォントで、JIS 第一・第二水準の漢字を収めています。一部の記号を除いてすべての文字が 7×7 ドットの範囲に収まっているので、文字を隙間なく並べても字間と行間が空きます(配布元の説明より)。上の出力でも、どの文字も右端の列と一番下の行が空いています。
Font to Binary Converter に同梱しているのは「美咲ゴシック第2」(misaki_gothic_2nd.ttf)で、半角文字は 3×7 ドット、全角の仮名は 7×7 ドットをいっぱいに使う字形です。美咲フォントは作者の定めるライセンスで配布されています。作品や製品に使うときは、配布元のライセンスの説明を確かめてください。
LED マトリクスに出したいときは「8×8 LED マトリクスに文字を出す」を参考にしてください。