05 December 2025, Friday

ESP8266開発ボードでOLED LCD SSD1306を使う独自ライブラリ

ESP8266開発ボードを用いてI2C接続のOLED LCD SSD1306を扱う方法。数年前には十分理解して使っていたはずだが、長い間使わないとすっかりとプログラミング方法を忘れ去ってしまっていた。

SSD1306にコマンドやグラフィックデータを送信する

20251205-ssd1306-send-data.jpg
SSD1306にデータを送信するI2Cパケット構造

Arduinoスケッチに落とし込むと次のようになる。ここでは、0x01, 0x02 の2バイトのデータを送信する例を書いている。

コマンドを送信する
#define SSD1306_I2C_ADDRESS 0x3C

Wire.beginTransmission(SSD1306_I2C_ADDRESS);
// コントロールバイト : Co=0, D/C#=0(command), 0,0,0,0,0,0 -> 0x40
Wire.write(0x00);
// コマンド 0x01 を送信する
Wire.write(0x01);
// コマンド 0x02 を送信する
Wire.write(0x02);
// .... 以下、必要な分だけコマンドのバイト列を送信する
Wire.endTransmission();
GDDRAMにグラフィックデータを送信する
#define SSD1306_I2C_ADDRESS 0x3C

Wire.beginTransmission(SSD1306_I2C_ADDRESS);
// コントロールバイト : Co=0, D/C#=1(data), 0,0,0,0,0,0 -> 0x40
Wire.write(0x40);
// データ 0x01 を送信する
Wire.write(0x01);
// データ 0x02 を送信する
Wire.write(0x02);
// .... 以下、必要な分だけデータのバイト列を送信する
Wire.endTransmission();

SSD1306の初期化

SSD1306のマニュアルによれば、初期化は次のようにすればよい。

20251205-ssd1306-initdiagram.jpg
SSD1306の初期化ダイアグラム

初期化から電源ONまで、一連の処理をArduinoスケッチに落とし込むと次のような形になる。

初期化から電源ONまで
// I2C送信
void i2c_write_bytes(const uint8_t *array, size_t len) {
  Wire.beginTransmission(SSD1306_I2C_ADDRESS);
  for (size_t i = 0; i < len; i++) {
    Wire.write(array[i]);
  }
  Wire.endTransmission();
}

// 初期化
void ssd1306_init(int width, int height) {
  uint8_t init1[] = {
    0x00,  // コントロールバイト Co=0(multi byte), D/C#=0(command), 0,0,0,0,0,0 -> 0x00
    0xa8,  // Set Multiplex Ratio
    0x1f,  //   (SSD1306_DISPLAY_HEIGHT - 1), 縦32px->0x1f, 縦64px->0x3f, これを越える部分へのGDデータ書き込みは無視される
    0xd3,  // Set Display Offset
    0x00,  //   RESET(default) 0x00
    0x40,  // Set Display Start Line, 0x40 + line
    0xa0,  // Set Segment Re-map, 0xa0=column address 0
    0xc0,  // Set COM Output Scan Direction
    0xda,  // Set COM Pins
    0x02,  //   RESET(default) 0x02
    0x81,  // Set Contrast Control
    0x7f,  //   default contrast = 0x7f
    0xa4,  // Entire display ON
    0xa6,  // Set Normal display
    0xd5,  // Set Display Clock
    0x80   //   RESET(default) 0x80
  };
  // ここまでのコマンドデータを一気に送信する
  i2c_write_bytes(init1, sizeof(init1));

  uint8_t init2[] = {
    0x00,                       // コントロールバイト Co=0(multi byte), D/C#=0(command), 0,0,0,0,0,0 -> 0x00
    0x20,                       // アドレッシングモード指定
    0x00,                       //   Horizontalモード
    0x21,                       // カラム指定
    0x00,                       //   開始位置(0)
    (uint8_t)(width - 1),       //   終了位置(127) .... 1カラムは1ピクセルの幅
    0x22,                       // ページ指定
    0x00,                       //   開始ページ(0)
    (uint8_t)(height / 8 - 1),  //   終了ページ(3) .... 1ページは8ピクセルの高さ
    0x2E                        // スクロール解除
  };
  // ここまでのコマンドデータを一気に送信する
  i2c_write_bytes(init2, sizeof(init2));
}

// 電源ON
void ssd1306_power_on() {
  uint8_t array[] = {
    0x00,  // コントロールバイト Co=0, D/C#=0
    0x8D,  // チャージポンプ設定
    0x14,  //   チャージポンプOn
    0xAF   // 画面表示On
  };
  i2c_write_bytes(array, sizeof(array));
}

そして、初期化と電源ONのみをするスケッチは次の通りで、その結果画面も示す。

初期化と電源ONのみをするスケッチ
#define SSD1306_I2C_ADDRESS 0x3C
#define SSD1306_DISPLAY_WIDTH 128
#define SSD1306_DISPLAY_HEIGHT 32

#define SSD1306_FONTBITMAP_NORMAL 0
#define SSD1306_FONTBITMAP_INVERT 1
#define SSD1306_FONTBITMAP_UNDERLINE 2

void setup() {
  Serial.begin(115200);
  Wire.begin(4, 5);  // SDA=GPIO4, SCL=GPIO5
  ssd1306_init(SSD1306_DISPLAY_WIDTH, SSD1306_DISPLAY_HEIGHT);
  ssd1306_power_on();
}

20251205-ssd1306-init-noise.jpg
GDDRAMはデフォルトでクリアされないため、画面にはノイズが表示されている

GDDRAMにどのようにデータ送信すればよいのか(最大連続書き込みByte数は?)

画面をクリアするために、GDDRAMに画面幅×行数分のデータを一気に送信できるのか、あるいはデータを小分けにする必要があるのか確かめるため、「画面幅×2」のデータを送信してみた。

「画面幅×2」のデータを送信するスケッチ
void setup() {
  Serial.begin(115200);
  Wire.begin(4, 5);  // SDA=GPIO4, SCL=GPIO5
  ssd1306_init(SSD1306_DISPLAY_WIDTH, SSD1306_DISPLAY_HEIGHT);
  ssd1306_power_on();

  // カーソルを1行目の画面中央に置く
  ssd1306_set_cursor(SSD1306_DISPLAY_WIDTH/2, 1, 0);
  Wire.beginTransmission(SSD1306_I2C_ADDRESS);
  Wire.write(0x40);  // コントロールバイト : Co=0(multi byte), D/C#=1(data), 0,0,0,0,0,0 -> 0x40
  // 画面幅×2のデータ送信 (1バイトの送信データは縦8px*横1pxに相当)
  for (int i = 0; i < SSD1306_DISPLAY_WIDTH*2; i++) {
    Wire.write(0x00);
  }
  Wire.endTransmission();
}


// カーソル設定
void ssd1306_set_cursor(int column, int char_width_px, int height_page) {
  uint8_t array[] = {
    0x00,                                               // コントロールバイト Co=0, D/C#=0
    (uint8_t)((column * char_width_px) & 0x0F),         // カラム下位4ビット (Set Lower Column Start Address)
    (uint8_t)(((column * char_width_px) >> 4) | 0x10),  // カラム上位4ビット (Set Higher Column Start Address)
    (uint8_t)(height_page | 0xB0)                       // ページ指定 = 行指定(0,1,2...) (Set Page Start Address)
  };
  i2c_write_bytes(array, sizeof(array));
}

結果は次の通りで、送信できる最大データサイズは「画面幅 - 1px」であった。それ以上のデータはGDDRAMに書き込まれないため、初期化時に残存しているノイズ画面が表示されたままとなる。

20251205-ssd1306-init-datalimit.jpg
SSD1306にデバイス横幅の2倍の上書きデータを送信したときの状態

画面をクリアする

したがって、画面全体をクリアする関数を次のように作成した。

画面全体をクリアする関数のスケッチ
void ssd1306_clear_display(int width, int height, int flag_invert) {
  // ページ(縦8pxで1行分)を上から下へ処理する
  for (int j = 0; j <= height / 8; j++) {
    // ページ(行)の先頭(左端)にカーソルを移動
    ssd1306_set_cursor(0, 1, j);
    Wire.beginTransmission(SSD1306_I2C_ADDRESS);
    Wire.write(0x40);  // コントロールバイト : Co=0(multi byte), D/C#=1(data), 0,0,0,0,0,0 -> 0x40
    // 画面左端から右端まで「1px縦ライン」ずつ書き込んでいく
    for (int i = 0; i < width; i++) {
      // GDDRAMへの書き込みは(経験則で)最大127bytesのため、十分余裕を持って32bytesごとにデータ転送する
      if(i%32){
        Wire.endTransmission();
        Wire.beginTransmission(SSD1306_I2C_ADDRESS);
        Wire.write(0x40);  // コントロールバイト : Co=0(multi byte), D/C#=1(data), 0,0,0,0,0,0 -> 0x40
      }
      if (flag_invert == SSD1306_FONTBITMAP_NORMAL) Wire.write(0x00);
      else Wire.write(0xff);
    }
    Wire.endTransmission();
  }
}

画面をクリアするスケッチを次のように作って試してみた。

画面をクリアするスケッチ
void setup() {
  Serial.begin(115200);
  Wire.begin(4, 5);  // SDA=GPIO4, SCL=GPIO5
  ssd1306_init(SSD1306_DISPLAY_WIDTH, SSD1306_DISPLAY_HEIGHT);
  ssd1306_power_on();
}

// 2秒毎に、全点灯・全消灯でクリアを行う
void loop() {
  ssd1306_clear_display(SSD1306_DISPLAY_WIDTH, SSD1306_DISPLAY_HEIGHT, SSD1306_FONTBITMAP_INVERT);
  delay(2000);
  ssd1306_clear_display(SSD1306_DISPLAY_WIDTH, SSD1306_DISPLAY_HEIGHT, SSD1306_FONTBITMAP_NORMAL);
  delay(2000);
}

20251205-ssd1306-clear.jpg

ASCII(英数)テキスト出力

8px*8pxのASCIIテキストのビットマップデータをGithubより借用した。

8px*8pxのASCIIテキストのビットマップデータ
#define FONT_WIDTH 8

// 8x8ピクセルのASCII文字ビットマップ(各Byteデータは横1px縦8px, MSBが下端LSBが上端ピクセル)
// フォントデータ出典・複写
// https://github.com/greiman/SSD1306Ascii/blob/master/src/fonts/font8x8.h
const uint8_t font_bitmap[] PROGMEM = {
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // space
  0x00, 0x00, 0x00, 0x00, 0x5F, 0x00, 0x00, 0x00,  // !
  0x00, 0x00, 0x00, 0x03, 0x00, 0x03, 0x00, 0x00,  // "
  0x00, 0x24, 0x7E, 0x24, 0x24, 0x7E, 0x24, 0x00,  // #
  0x00, 0x2E, 0x2A, 0x7F, 0x2A, 0x3A, 0x00, 0x00,  // $
  0x00, 0x46, 0x26, 0x10, 0x08, 0x64, 0x62, 0x00,  // %
  0x00, 0x20, 0x54, 0x4A, 0x54, 0x20, 0x50, 0x00,  // &
  0x00, 0x00, 0x00, 0x04, 0x02, 0x00, 0x00, 0x00,  // '
  0x00, 0x00, 0x00, 0x3C, 0x42, 0x00, 0x00, 0x00,  // (
  0x00, 0x00, 0x00, 0x42, 0x3C, 0x00, 0x00, 0x00,  // )
  0x00, 0x10, 0x54, 0x38, 0x54, 0x10, 0x00, 0x00,  // *
  0x00, 0x10, 0x10, 0x7C, 0x10, 0x10, 0x00, 0x00,  // +
  0x00, 0x00, 0x00, 0x80, 0x60, 0x00, 0x00, 0x00,  // ,
  0x00, 0x10, 0x10, 0x10, 0x10, 0x10, 0x00, 0x00,  // -
  0x00, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x00,  // .
  0x00, 0x40, 0x20, 0x10, 0x08, 0x04, 0x00, 0x00,  // /

  0x3C, 0x62, 0x52, 0x4A, 0x46, 0x3C, 0x00, 0x00,  // 0
  0x44, 0x42, 0x7E, 0x40, 0x40, 0x00, 0x00, 0x00,  // 1
  0x64, 0x52, 0x52, 0x52, 0x52, 0x4C, 0x00, 0x00,  // 2
  0x24, 0x42, 0x42, 0x4A, 0x4A, 0x34, 0x00, 0x00,  // 3
  0x30, 0x28, 0x24, 0x7E, 0x20, 0x20, 0x00, 0x00,  // 4
  0x2E, 0x4A, 0x4A, 0x4A, 0x4A, 0x32, 0x00, 0x00,  // 5
  0x3C, 0x4A, 0x4A, 0x4A, 0x4A, 0x30, 0x00, 0x00,  // 6
  0x02, 0x02, 0x62, 0x12, 0x0A, 0x06, 0x00, 0x00,  // 7
  0x34, 0x4A, 0x4A, 0x4A, 0x4A, 0x34, 0x00, 0x00,  // 8
  0x0C, 0x52, 0x52, 0x52, 0x52, 0x3C, 0x00, 0x00,  // 9
  0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, 0x00,  // :
  0x00, 0x00, 0x80, 0x64, 0x00, 0x00, 0x00, 0x00,  // ;
  0x00, 0x00, 0x10, 0x28, 0x44, 0x00, 0x00, 0x00,  // <
  0x00, 0x28, 0x28, 0x28, 0x28, 0x28, 0x00, 0x00,  // =
  0x00, 0x00, 0x44, 0x28, 0x10, 0x00, 0x00, 0x00,  // >
  0x00, 0x04, 0x02, 0x02, 0x52, 0x0A, 0x04, 0x00,  // ?

  0x00, 0x3C, 0x42, 0x5A, 0x56, 0x5A, 0x1C, 0x00,  // @
  0x7C, 0x12, 0x12, 0x12, 0x12, 0x7C, 0x00, 0x00,  // A
  0x7E, 0x4A, 0x4A, 0x4A, 0x4A, 0x34, 0x00, 0x00,  // B
  0x3C, 0x42, 0x42, 0x42, 0x42, 0x24, 0x00, 0x00,  // C
  0x7E, 0x42, 0x42, 0x42, 0x24, 0x18, 0x00, 0x00,  // D
  0x7E, 0x4A, 0x4A, 0x4A, 0x4A, 0x42, 0x00, 0x00,  // E
  0x7E, 0x0A, 0x0A, 0x0A, 0x0A, 0x02, 0x00, 0x00,  // F
  0x3C, 0x42, 0x42, 0x52, 0x52, 0x34, 0x00, 0x00,  // G
  0x7E, 0x08, 0x08, 0x08, 0x08, 0x7E, 0x00, 0x00,  // H
  0x00, 0x42, 0x42, 0x7E, 0x42, 0x42, 0x00, 0x00,  // I
  0x30, 0x40, 0x40, 0x40, 0x40, 0x3E, 0x00, 0x00,  // J
  0x7E, 0x08, 0x08, 0x14, 0x22, 0x40, 0x00, 0x00,  // K
  0x7E, 0x40, 0x40, 0x40, 0x40, 0x40, 0x00, 0x00,  // L
  0x7E, 0x04, 0x08, 0x08, 0x04, 0x7E, 0x00, 0x00,  // M
  0x7E, 0x04, 0x08, 0x10, 0x20, 0x7E, 0x00, 0x00,  // N
  0x3C, 0x42, 0x42, 0x42, 0x42, 0x3C, 0x00, 0x00,  // O

  0x7E, 0x12, 0x12, 0x12, 0x12, 0x0C, 0x00, 0x00,  // P
  0x3C, 0x42, 0x52, 0x62, 0x42, 0x3C, 0x00, 0x00,  // Q
  0x7E, 0x12, 0x12, 0x12, 0x32, 0x4C, 0x00, 0x00,  // R
  0x24, 0x4A, 0x4A, 0x4A, 0x4A, 0x30, 0x00, 0x00,  // S
  0x02, 0x02, 0x02, 0x7E, 0x02, 0x02, 0x02, 0x00,  // T
  0x3E, 0x40, 0x40, 0x40, 0x40, 0x3E, 0x00, 0x00,  // U
  0x1E, 0x20, 0x40, 0x40, 0x20, 0x1E, 0x00, 0x00,  // V
  0x3E, 0x40, 0x20, 0x20, 0x40, 0x3E, 0x00, 0x00,  // W
  0x42, 0x24, 0x18, 0x18, 0x24, 0x42, 0x00, 0x00,  // X
  0x02, 0x04, 0x08, 0x70, 0x08, 0x04, 0x02, 0x00,  // Y
  0x42, 0x62, 0x52, 0x4A, 0x46, 0x42, 0x00, 0x00,  // Z
  0x00, 0x00, 0x7E, 0x42, 0x42, 0x00, 0x00, 0x00,  // [
  0x00, 0x04, 0x08, 0x10, 0x20, 0x40, 0x00, 0x00,  // backslash
  0x00, 0x00, 0x42, 0x42, 0x7E, 0x00, 0x00, 0x00,  // ]
  0x00, 0x08, 0x04, 0x7E, 0x04, 0x08, 0x00, 0x00,  // ^
  0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x00,  // _

  0x3C, 0x42, 0x99, 0xA5, 0xA5, 0x81, 0x42, 0x3C,  // `
  0x00, 0x20, 0x54, 0x54, 0x54, 0x78, 0x00, 0x00,  // a
  0x00, 0x7E, 0x48, 0x48, 0x48, 0x30, 0x00, 0x00,  // b
  0x00, 0x00, 0x38, 0x44, 0x44, 0x44, 0x00, 0x00,  // c
  0x00, 0x30, 0x48, 0x48, 0x48, 0x7E, 0x00, 0x00,  // d
  0x00, 0x38, 0x54, 0x54, 0x54, 0x48, 0x00, 0x00,  // e
  0x00, 0x00, 0x00, 0x7C, 0x0A, 0x02, 0x00, 0x00,  // f
  0x00, 0x18, 0xA4, 0xA4, 0xA4, 0xA4, 0x7C, 0x00,  // g
  0x00, 0x7E, 0x08, 0x08, 0x08, 0x70, 0x00, 0x00,  // h
  0x00, 0x00, 0x00, 0x48, 0x7A, 0x40, 0x00, 0x00,  // i
  0x00, 0x00, 0x40, 0x80, 0x80, 0x7A, 0x00, 0x00,  // j
  0x00, 0x7E, 0x18, 0x24, 0x40, 0x00, 0x00, 0x00,  // k
  0x00, 0x00, 0x00, 0x3E, 0x40, 0x40, 0x00, 0x00,  // l
  0x00, 0x7C, 0x04, 0x78, 0x04, 0x78, 0x00, 0x00,  // m
  0x00, 0x7C, 0x04, 0x04, 0x04, 0x78, 0x00, 0x00,  // n
  0x00, 0x38, 0x44, 0x44, 0x44, 0x38, 0x00, 0x00,  // o

  0x00, 0xFC, 0x24, 0x24, 0x24, 0x18, 0x00, 0x00,  // p
  0x00, 0x18, 0x24, 0x24, 0x24, 0xFC, 0x80, 0x00,  // q
  0x00, 0x00, 0x78, 0x04, 0x04, 0x04, 0x00, 0x00,  // r
  0x00, 0x48, 0x54, 0x54, 0x54, 0x20, 0x00, 0x00,  // s
  0x00, 0x00, 0x04, 0x3E, 0x44, 0x40, 0x00, 0x00,  // t
  0x00, 0x3C, 0x40, 0x40, 0x40, 0x3C, 0x00, 0x00,  // u
  0x00, 0x0C, 0x30, 0x40, 0x30, 0x0C, 0x00, 0x00,  // v
  0x00, 0x3C, 0x40, 0x38, 0x40, 0x3C, 0x00, 0x00,  // w
  0x00, 0x44, 0x28, 0x10, 0x28, 0x44, 0x00, 0x00,  // x
  0x00, 0x1C, 0xA0, 0xA0, 0xA0, 0x7C, 0x00, 0x00,  // y
  0x00, 0x44, 0x64, 0x54, 0x4C, 0x44, 0x00, 0x00,  // z
  0x00, 0x08, 0x08, 0x76, 0x42, 0x42, 0x00, 0x00,  // {
  0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x00,  // |
  0x00, 0x42, 0x42, 0x76, 0x08, 0x08, 0x00, 0x00,  // }
  0x00, 0x00, 0x04, 0x02, 0x04, 0x02, 0x00, 0x00   // ~
};

そして、この8x8フォントを使ってテキスト表示する関数を次のように作成。

8x8フォントを使ってテキスト表示を行う関数
// フォント取得
// 8x8のフォントビットマップを配列に入れて返す
void get_font_bitmap(char c, int flag_invert, uint8_t *out) {
  int char_code = (int)c - 0x20;
  for (int i = 0; i < 8; i++) {
    out[i] = pgm_read_byte(&font_bitmap[char_code * 8 + i]);
    if (flag_invert == SSD1306_FONTBITMAP_INVERT) {
      out[i] ^= 0xFF;
    }
    if (flag_invert == SSD1306_FONTBITMAP_UNDERLINE) {
      out[i] ^= 0x80;
    }
  }
}

// 文字列表示
void ssd1306_print(const char *str, int flag_invert) {
  Wire.beginTransmission(SSD1306_I2C_ADDRESS);
  Wire.write(0x40);
  for (size_t i = 0; i < strlen(str); i++) {
    uint8_t buf[8];
    get_font_bitmap(str[i], flag_invert, buf);
    for (int k = 0; k < 8; k++) Wire.write(buf[k]);
  }
  Wire.endTransmission();
}

void setup() {
  Serial.begin(115200);
  Wire.begin(4, 5);  // SDA=GPIO4, SCL=GPIO5
  ssd1306_init(SSD1306_DISPLAY_WIDTH, SSD1306_DISPLAY_HEIGHT);
  ssd1306_power_on();

  ssd1306_clear_display(SSD1306_DISPLAY_WIDTH, SSD1306_DISPLAY_HEIGHT, SSD1306_FONTBITMAP_NORMAL);
  ssd1306_set_cursor(0, FONT_WIDTH, 0);
  ssd1306_print("ESP8266 test ...", SSD1306_FONTBITMAP_NORMAL);
  ssd1306_set_cursor(0, FONT_WIDTH, 1);
  ssd1306_print("Invert Text", SSD1306_FONTBITMAP_INVERT);
  ssd1306_set_cursor(0, FONT_WIDTH, 2);
  ssd1306_print("Underline Text", SSD1306_FONTBITMAP_UNDERLINE);
  ssd1306_set_cursor(0, FONT_WIDTH, 3);
  ssd1306_print("01234567890123456789", SSD1306_FONTBITMAP_NORMAL);
}

このプログラムを実行した結果が次の通り。

20251205-ssd1306-text.jpg