2
1

GroveBeginnerKit を、C++(Arduino)とRuby(ラズパイ,Rboard) で使う I2C - OLED(SSD1315) 編

Last updated at Posted at 2024-05-07

しまねソフト研究開発センター(略称 ITOC)にいます、東です。

Grove Beginner Kit for Arduino を使ってみる記事の第7回、最後は I2C 接続の OLED ビットマップディスプレイを題材にします。

このレポートでは、今まで通り

「メーカーのデータシートを見て、ICを直接コントロールをすること」

を、方針とします。しかしながら、今までのセンサー類と違いディスプレイコントローラは桁違いに設定項目が多く扱いが難しいです。通常ですと既存ライブラリを使うのが当たり前になると思いますが、学習用の記事という性質上ライブラリは使わずにコントロールします。1

ターゲットは、以下の通り。

OLED ディスプレイ (OLED Display 0.96" (SSD1315))

外観 回路図
grove_OLED.jpeg (省略)

メーカーWiki: Grove - OLED Display 0.96" (SSD1315) より引用)

搭載されている OLEDパネルは、ZJY technology 社製 ZJY-2864KSWPG01、
ディスプレイコントローラ は、SOLOMON SYSTECH 社製 SSD1315 です。

表示デモ

このレポートでは、以下のようにASCII文字表示とビットマップデータの表示をするデモプログラムを作ります。

OLED_Demo.gif

データシートの確認

OLED パネルとディスプレイコントローラの組み合わせで動作するため、双方のデータシートを読む必要があります。

パネルのデータシートは、上記 Wikiページに、[PDF] OLED Module Datasheet として公開されています。ディスプレイコントローラの方は、ICメーカー製品ページから Request を送ってデータシートをもらうポリシーのようですが、適当に探すと野良pdfが落ちていたりするので、それらを参照すれば十分です。

OLEDパネル

  • OLEDパネルはモノクロ2値の 128x64 dots
  • ディスプレイコントローラICの内蔵チャージポンプを使って、OLEDパネルを駆動している(Groveモジュールの回路図とデータシートのサンプル回路図から推定)

ディスプレイコントローラ

  • ディスプレイコントローラ IC 内にフレームバッファを内蔵している
  • I2Cバスを使って、規定されたフォーマットでバイト列を送ることで、コマンドの送信またはフレームバッファ (GDDRAM) に書き込むデータの送信を行う
  • コマンドでは、動作モードの設定、フレームバッファ書き込み開始ポイントの指定などを行う事ができる
  • GDDRAMは8ページ分持っているが、このOLEDパネルを接続する場合は結果的に1画面分しかない
  • 画面の反転表示や、スクロール機能もある(が、当記事では扱わない)

ディスプレイコントローラは、懐かしの 68系、80系 8bit バスも備えていますが、この GroveKit で使えるのは、I2C だけです。
I2Cアドレスは、基板部品面にシルクで 0x78, 0x7A のどちらかに切り替え可能のように書いてありますが、このアドレスは 1bit シフトしたアドレスなので、プログラムに書くときは、0x3C, 0x3D となり、デフォルトでは 0x3C です。
データシートで言うところの1ページは縦8ドットであり、「8ドットより多いピクセル数のディスプレイの場合は、連続したページが表示される」という仕様です。よって、このディスプレイの縦ドット数 64dots ÷ 8dots = 8pages となり、結果的に1画面分しかフレームバッファがありません。

I2C バスデータ仕様

I2Cバスに流すコマンド・データは、最初にコントロールバイト、続いてデータバイトという順番で、連続させることもできるという仕様です。

OLED_I2C_WriteSpec.png
(SSD1315 Datasheet Figure 6-7: I2C-bus data format より)

しかしながら例示もなく、なかなかに読み解くのに苦労しました。
簡単に使うには、以下の事さえ守れば良いです。

  • コマンド送信は、コントロールバイト 0x00 (Co=0, D/C=0) に続いて、コマンドをいくつでも送信できる
  • 表示データの送信は、コントロールバイト 0x40 (Co=0, D/C=1) に続いて、ビットマップデータを連続して送信する

各コマンドに関しては、ひとつひとつ丁寧に説明してあり、わかりやすいです。

フレームバッファ(GDDRAM)と、実際の画面表示との関係

データシートだけでは少し分かりにくいので、データシートの図を少しアレンジした図を示します。

OLED_Fig6-14_arrange.png
(SSD1315 Datasheet Figure 6-14: Enlargement of GDDRAM を筆者がアレンジ)

この図は電源ON時のデフォルト設定によるもので、OLEDモジュールが上図の向きにて、1バイトのデータがY方向8ピクセル(=ドット)の表示に対応します。

任意の位置のピクセルを点灯させるには、まずコマンドによってY方向にあたるページ番号とX方向にあたるセグメント番号を指定し、次に表示データの送信をすることで行います。仕様上、縦8ピクセルが同時に書き換わります。

全体の表示方向などはコマンドによって変更が可能ですが、書き込むバイトとピクセルの関係は固定仕様です。

ICの初期化

OLEDパネルの仕様にあった設定で初期化が必要です。
パネルのデータシート "OEL Display Module.pdf" の、「4.4.2 Vcc Generated by Internal DC/DC Circuit Power up Sequence」 を見ながら、この順番でコマンドを送信します。

OLED_PowerUpSequence.png

以下は、Rubyでの初期化コード例です。(完全なコードは後述します)

  def init()
    init_cmd1 = "\x00\xAE" +    # Display OFF
                "\xD5\x90" +    # Display Clock, OSC Freq  = 1001b
                                #                Div ratio = 0000b (RST)
                "\xA8\x3F" +    # Multiplex ratio = 111111b (RST)
                "\xD3\x00" +    # Display offset = 0 (RST)
                "\x40" +        # Display start line = 0 (RST)
                "\xA0" +        # Segment remap (RST)
                "\xC0" +        # COM output scan direction (RST)
                "\xDA\x12" +    # COM pins H/W conf (RST)
                "\x81\xB0" +    # Contrast = B0
                "\xD9\x22" +    # Pre charge Phase1 = 2 (RST)
                                #            Phase2 = 2 (RST)
                "\xDB\x30" +    # COM select volt level = 30
                "\xA4" +        # Entire display ON (RST)
                "\xA6" +        # Normal/Inverse = Normal(RST)
                "\x20\x01" +    # Memory addressing = Vertical
                "\x2E"          # Deactivate scroll
    @bus.write(@address, init_cmd1)
    clear()
    init_cmd2 = "\x00\x8D\x14" + # Charge pump = Enable 7.5V
                "\xAF"           # Display ON
    @bus.write(@address, init_cmd2)
  end

GDDRAM への書き込み順序を指定するモード、データシートでは Memory Addressing Mode と表記してあるモードを、標準の Page Addressing Mode から、Vertical Addressing Mode に変更します。
ビットマップデータの表示を考えた場合に、X方向Y方向ともに連続したデータの方が自然です。もし標準の Page Addressing Mode や、Horizontal Addressing Mode で、任意サイズ(n × m dots)のビットマップデータの表示を行う場合には、

  縦(Y) 8dots → 横(X) n dots → 縦(Y) 8dots → 横(X) n dots ...

と、XY軸を交互に描く必要があり、データ作成時そのような順番になるようあらかじめ変換するか、もしくは描画時にリアルタイムで変換する必要があります。
変更した、Vertical Addressing Mode ならば、

  縦(Y) m dots → 横(X) n dots

と、XYは反転するものの、シンプルになります。

ビットマップデータの描画

非圧縮ビットマップデータを表示します。
簡単のため、以下の仕様とします。
  * Y-X 方向にスキャンしたデータのみ対応します
  * Y方向のサイズ(= 高さ)は、8の倍数のみ対応とします
  * 表示が画面外になる場合はエラーとします
 
通常ビットマップデータは X-Y 方向にスキャンしてデータを作る事が多いと思いますが、先に述べたハードウェアの構造的制約により、Y-X 方向のデータのみ対応します。
引数で与えられた座標 (x,y) と、画像サイズ (width,height) を元に、カラム (0-127) とページ (0-7) を計算し、それぞれコマンド(0x21, 0x22)でセットの後、ビットマップデータを送信することで表示を行っています。2

  def draw_bitmap( x, y, width, height, data )
    return if x < 0 || x >= WIDTH
    return if y < 0 || y >= HEIGHT

    x1 = x + width - 1
    y1 = (y + height) / 8 - 1
    y /= 8
    return if x1 >= WIDTH
    return if y1 >= HEIGHT / 8

    @bus.write(@address, 0x00, 0x21, x, x1, 0x22, y, y1)
    #                          0x21: Set Column Address
    #                                       0x22: Set Page Address
    @bus.write(@address, 0x40, data)
  end

余談ですが、入門書などでは、ここで使用した即値 0x21, 0x22 などを定数にしてわかりやすく、かつ変更に強いコードを書きましょうと指南されます。

# Ruby
SET_COLUMN_ADDRESS = 0x21
SET_PAGE_ADDRESS = 0x22

/* C */
#define SET_COLUMN_ADDRESS 0x21
#define SET_PAGE_ADDRESS 0x22

ケースバイケースですが、私はこういった ICに対する制御コマンドを記述する場合は、即値をそのまま使いコメントでフォローする事も多いです。このようなケースでは、ICのデータシートが一次情報で、それを見ながらプログラミングをします。そこに定数定義というレイヤーが挟まることが邪魔に感じることと、そもそもICが変更になった場合はコマンド体系も変更になる場合がほとんどなので、変更に強いコードというメリットはありません。

文字列の描画

このOLEDモジュールは、ビットマップデータの表示のみサポートしているので、文字を表示したい場合は別途フォントデータを用意し、ビットマップデータとして描画してやる必要があります。
このレポートでは簡単のため、8×8 dots の ASCII文字のみ(0x20-0x7F) の表示をサポートするものとしてフォントデータ3を用意します。

# フォントデータの用意
  FONT_ASCII_8x8 = "\
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07\x5F\x5F\x00\x00\
\x00\x0B\x07\x00\x0B\x07\x00\x00\x14\x7F\x7F\x14\x7F\x7F\x14\x00\
  (snip)
\x00\x00\x00\x7F\x7F\x00\x00\x00\x00\x41\x41\x77\x3E\x08\x00\x00\
\x01\x01\x01\x01\x01\x01\x01\x01\x00\x00\x06\x0F\x09\x0F\x06\x00".b

エンコードが悪さをしないように、.b をつけておきます。

このフォントデータから、任意の1文字のビットマップデータを取り出すコードを書きます。

  #
  # get 8x8 font bitmap
  #
  def get_font_bitmap( ascii_code )
    if ascii_code < 0x20 || ascii_code > 0x7f
      ascii_code = 0
    else
      ascii_code -= 0x20
    end

    return FONT_ASCII_8x8[ascii_code * 8, 8]
  end

これらを使って、指定した位置 (col, row) から ASCII 文字列を表示するコードを書きます。

  #
  # draw string
  #
  def puts( col, row, str )
    col *= 8
    return if col < 0 || col >= WIDTH
    return if row < 0 || row >= HEIGHT / 8

    @bus.write(@address, 0x00, 0x21, col, 127, 0x22, row, row )
    #                          0x21: Set Column Address (0-127)
    #                                          0x22: Set Page Address (0-7)
    str.each_byte {|code|
      @bus.write(@address, 0x40, get_font_bitmap( code ))
    }
  end

サンプルプログラム

レポート冒頭で示したデモンストレーション表示をするプログラムです。 4

Arduino

モジュールを、I2C 端子に接続します。

クリックでプログラム全体表示
OLED_Display.ino
//  This file is distributed under BSD 3-Clause License.

#include <Wire.h>

const int I2C_ADRS = 0x3C;
const int WIDTH = 128;
const int HEIGHT = 64;
const byte FONT_ASCII_8x8[] =
  "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07\x5F\x5F\x00\x00"
  "\x00\x0B\x07\x00\x0B\x07\x00\x00\x14\x7F\x7F\x14\x7F\x7F\x14\x00"
  "\x26\x6F\x49\x7F\x4B\x7A\x30\x00\x46\x25\x13\x08\x64\x52\x31\x00"
  "\x36\x4F\x4D\x59\x59\x36\x70\x50\x00\x00\x00\x0B\x07\x00\x00\x00"
  "\x00\x00\x1C\x3E\x63\x41\x00\x00\x00\x00\x41\x63\x3E\x1C\x00\x00"
  "\x08\x2A\x1C\x7F\x1C\x2A\x08\x00\x00\x08\x08\x3E\x3E\x08\x08\x00"
  "\x00\xB0\x70\x00\x00\x00\x00\x00\x00\x08\x08\x08\x08\x08\x08\x00"
  "\x00\x00\x60\x60\x00\x00\x00\x00\x40\x20\x10\x08\x04\x02\x01\x00"
  "\x1C\x3E\x61\x41\x43\x3E\x1C\x00\x00\x40\x42\x7F\x7F\x40\x40\x00"
  "\x62\x73\x79\x59\x5D\x4F\x46\x00\x20\x61\x49\x4D\x4F\x7B\x31\x00"
  "\x18\x1C\x16\x13\x7F\x7F\x10\x00\x27\x67\x45\x45\x45\x7D\x38\x00"
  "\x3C\x7E\x4B\x49\x49\x79\x30\x00\x03\x03\x71\x79\x0D\x07\x03\x00"
  "\x36\x4F\x4D\x59\x59\x76\x30\x00\x06\x4F\x49\x49\x69\x3F\x1E\x00"
  "\x00\x00\x00\x66\x66\x00\x00\x00\x00\x00\x00\xB6\x76\x00\x00\x00"
  "\x00\x08\x1C\x36\x63\x41\x00\x00\x00\x14\x14\x14\x14\x14\x14\x00"
  "\x00\x41\x63\x36\x1C\x08\x00\x00\x06\x07\x51\x59\x59\x0F\x06\x00"
  "\x3E\x7F\x41\x59\x55\x5F\x1E\x00\x7C\x7E\x13\x11\x13\x7E\x7C\x00"
  "\x7F\x7F\x49\x49\x49\x7F\x36\x00\x1C\x3E\x63\x41\x41\x63\x22\x00"
  "\x7F\x7F\x41\x41\x63\x3E\x1C\x00\x00\x7F\x7F\x49\x49\x49\x41\x00"
  "\x7F\x7F\x09\x09\x09\x09\x01\x00\x1C\x3E\x63\x41\x49\x79\x79\x00"
  "\x7F\x7F\x08\x08\x08\x7F\x7F\x00\x00\x41\x41\x7F\x7F\x41\x41\x00"
  "\x30\x70\x40\x40\x40\x7F\x3F\x00\x7F\x7F\x18\x3C\x76\x63\x41\x00"
  "\x00\x7F\x7F\x40\x40\x40\x40\x00\x7F\x7F\x0E\x1C\x0E\x7F\x7F\x00"
  "\x7F\x7F\x0E\x1C\x38\x7F\x7F\x00\x3E\x7F\x41\x41\x41\x7F\x3E\x00"
  "\x7F\x7F\x11\x11\x11\x1F\x0E\x00\x3E\x7F\x41\x51\x31\x7F\x5E\x00"
  "\x7F\x7F\x11\x31\x79\x6F\x4E\x00\x26\x6F\x49\x49\x4B\x7A\x30\x00"
  "\x00\x01\x01\x7F\x7F\x01\x01\x00\x3F\x7F\x40\x40\x40\x7F\x3F\x00"
  "\x0F\x1F\x38\x70\x38\x1F\x0F\x00\x7F\x7F\x38\x1C\x38\x7F\x7F\x00"
  "\x63\x77\x3E\x1C\x3E\x77\x63\x00\x00\x07\x0F\x78\x78\x0F\x07\x00"
  "\x61\x71\x79\x5D\x4F\x47\x43\x00\x00\x7F\x7F\x41\x41\x41\x00\x00"
  "\x01\x02\x04\x08\x10\x20\x40\x00\x00\x41\x41\x41\x7F\x7F\x00\x00"
  "\x00\x04\x06\x03\x03\x06\x04\x00\x80\x80\x80\x80\x80\x80\x80\x80"
  "\x00\x01\x03\x06\x0C\x08\x00\x00\x00\x38\x7C\x44\x3C\x7C\x40\x00"
  "\x01\x7F\x7F\x48\x48\x78\x30\x00\x00\x38\x6C\x44\x44\x6C\x28\x00"
  "\x00\x38\x7C\x44\x3F\x7F\x40\x00\x00\x38\x7C\x54\x54\x5C\x08\x00"
  "\x00\x08\x7E\x7F\x09\x03\x02\x00\x00\x58\xE4\xA4\xB4\xB8\x5C\x04"
  "\x00\x7F\x7F\x08\x08\x78\x70\x00\x00\x00\x04\x7D\x7D\x00\x00\x00"
  "\x00\x40\xC0\x80\x80\xFD\x7D\x00\x00\x7F\x7F\x30\x78\x6C\x44\x00"
  "\x00\x00\x01\x7F\x7F\x00\x00\x00\x7C\x7C\x0C\x78\x0C\x7C\x78\x00"
  "\x00\x7C\x78\x04\x04\x7C\x78\x00\x00\x38\x44\x44\x44\x7C\x38\x00"
  "\x00\xFC\xFC\x24\x24\x3C\x18\x00\x18\x3C\x24\x24\xFC\xFC\x80\x00"
  "\x04\x7C\x78\x0C\x04\x0C\x08\x00\x00\x08\x5C\x54\x54\x74\x20\x00"
  "\x04\x04\x3F\x7F\x44\x64\x20\x00\x00\x3C\x7C\x40\x40\x3C\x7C\x00"
  "\x00\x1C\x3C\x60\x60\x3C\x1C\x00\x3C\x7C\x60\x38\x60\x7C\x3C\x00"
  "\x00\x44\x6C\x38\x38\x6C\x44\x00\x00\x4E\x9E\x90\x90\xFE\x7E\x00"
  "\x00\x44\x64\x74\x5C\x4C\x44\x00\x00\x08\x3E\x77\x41\x41\x00\x00"
  "\x00\x00\x00\x7F\x7F\x00\x00\x00\x00\x41\x41\x77\x3E\x08\x00\x00"
  "\x01\x01\x01\x01\x01\x01\x01\x01\x00\x00\x06\x0F\x09\x0F\x06\x00";

const byte BMP_DOG[] =
  "\xFF\xFF\xFF\xFF\x7F\xF0\xFF\xFF\x9F\xEF\xFF\xFF\xCF\xEF\x1F\xFE"
  "\xE7\xEF\xE3\x38\x77\x80\xF8\x27\xB7\x77\xFE\x0F\xFB\x7C\xFF\x7F"
  "\xFD\x7C\xFF\x7F\xFD\x6F\xFF\x1F\xFD\x0F\xFF\xE3\xFD\x0F\xFF\xE3"
  "\xFD\x6F\xFF\x1F\xFD\x7C\xFF\x7F\xF3\x7C\xFF\x7F\xB7\x77\xFE\x0F"
  "\x67\x00\xF8\x27\xEF\x6F\xE3\x38\xCF\xAF\x0F\x9A\x9F\xAF\xFF\xA3"
  "\x3F\xB0\xFF\xBD\xFF\x9F\xFF\x81\xFF\x6F\xFF\x9D\xFF\x77\xFF\xA1"
  "\xFF\xF7\xFE\xBF\xFF\xF7\xFD\x80\xFF\xEF\x03\xFF\xFF\xDF\xFC\xFF"
  "\xFF\x3F\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF";


/*
  SSD1315 へコマンドを送信

  注:Arduinoライブラリの制限により、32bytes以上のデータは送ることができないので、
    呼び出し側で分割して call する。
*/
int oled_send_cmd( const byte *data, int size )
{
  Wire.beginTransmission( I2C_ADRS );
  Wire.write( 0x00 );
  Wire.write( data, size );
  Wire.endTransmission();

  return 0;
}


/*
  SSD1315 のフレームバッファへ書き込み

  注:Arduinoライブラリの制限により、32bytes以上のデータは送ることができないが、
    画像データは任意の位置で切っても問題ないので、この関数内で分割送信する。
*/
int oled_send_fb( const byte *data, int size )
{
  int chunk_size = 30;

  for( int i = 0; i < size; i += chunk_size ) {
    if( i + chunk_size > size ) {
      chunk_size = size - i;
    }

    Wire.beginTransmission( I2C_ADRS );
    Wire.write( 0x40 );
    Wire.write( data + i, chunk_size );
    Wire.endTransmission();
  }

  return 0;
}


/*
  Clear screen.
*/
int oled_clear(void)
{
  oled_send_cmd("\x21\x00\x7f", 3 );
  oled_send_cmd("\x22\x00\x07", 3 );

  for( int i = 0; i < 64; i++ ) {
    Wire.beginTransmission( I2C_ADRS );
    Wire.write( 0x40 );
    for( int j = 0; j < 16; j++ ) {
      Wire.write( 0x00 );
    }
    Wire.endTransmission();
  }

  return 0;
}


/*
  ASCII 文字列の描画
*/
int oled_puts( int col, int row, const char *s )
{
  col *= 8;
  if( col < 0 || col >= WIDTH ) return -1;
  if( row < 0 || row >= HEIGHT / 8 ) return -1;

  byte cmd_c[3] = "\x21\x00\x7f";
  cmd_c[1] = col;
  oled_send_cmd( cmd_c, 3 );

  byte cmd_r[3] = "\x22\x00\x00";
  cmd_r[1] = row;
  cmd_r[2] = row;
  oled_send_cmd( cmd_r, 3 );

  for( int i = 0; i < strlen(s); i++ ) {
    oled_send_fb( FONT_ASCII_8x8 + (s[i] - 0x20) * 8, 8 );
  }

  return 0;
}


/*
  ビットマップデータの描画
*/
int oled_draw_bitmap( int x, int y, int width, int height, const byte *data )
{
  if( x < 0 || x >= WIDTH ) return -1;
  if( y < 0 || y >= HEIGHT ) return -1;

  int x1 = x + width - 1;
  int y1 = (y + height) / 8 - 1;
  y /= 8;
  if( x1 >= WIDTH ) return -1;
  if( y1 >= HEIGHT / 8 ) return -1;

  byte cmd_c[3] = "\x21";
  cmd_c[1] = x;
  cmd_c[2] = x1;
  oled_send_cmd( cmd_c, 3 );

  byte cmd_r[3] = "\x22";
  cmd_r[1] = y;
  cmd_r[2] = y1;
  oled_send_cmd( cmd_r, 3 );

  oled_send_fb( data, width * height / 8 );

  return 0;
}


void setup() {
  Wire.begin();

  // IC初期化
  oled_send_cmd("\xAE", 1);     // Display OFF
  oled_send_cmd("\xD5\x90", 2); // Display Clock, OSC Freq  = 1001b
                                //                Div ratio = 0000b (RST)
  oled_send_cmd("\xA8\x3F", 2); // Multiplex ratio = 111111b (RST)
  oled_send_cmd("\xD3\x00", 2); // Display offset = 0 (RST)
  oled_send_cmd("\x40", 1);     // Display start line = 0 (RST)
  oled_send_cmd("\xA0", 1);     // Segment remap (RST)
  oled_send_cmd("\xC0", 1);     // COM output scan direction (RST)
  oled_send_cmd("\xDA\x12", 2); // COM pins H/W conf (RST)
  oled_send_cmd("\x81\xB0", 2); // Contrast = B0
  oled_send_cmd("\xD9\x22", 2); // Pre charge Phase1 = 2 (RST)
                                //            Phase2 = 2 (RST)
  oled_send_cmd("\xDB\x30", 2); // COM select volt level = 30
  oled_send_cmd("\xA4", 1);     // Entire display ON (RST)
  oled_send_cmd("\xA6", 1);     // Normal/Inverse = Normal(RST)
  oled_send_cmd("\x20\x01", 2); // Memory addressing = Vertical
  oled_send_cmd("\x2E", 1);     // Deactivate scroll

  oled_clear();

  oled_send_cmd("\x8D\x14", 2); // Charge pump = Enable 7.5V
  oled_send_cmd("\xAF", 1);     // Display ON
}


void loop() {
  oled_clear();
  oled_puts( 2, 3, "OLED Display");
  delay( 1000 );

  oled_puts( 0, 0, "16cols x 8rows");
  delay( 500 );
  oled_puts( 6, 1, "Characters");
  delay( 1000 );

  for( int i = 0; i < 8; i++ ) {
    char s[] = {'0' + (i + 1) % 10, 0};
    oled_puts( 0, i, s );
    delay( 100 );
  }
  for( int i = 1; i < 16; i++ ) {
    char s[] = {'0' + (i + 1) % 10, 0};
    oled_puts( i, 7, s );
    delay( 100 );
  }

  delay( 5000 );

  oled_clear();
  oled_puts( 2, 3, "OLED Display");
  delay( 1000 );

  oled_puts( 0, 0, "128x64 dots");
  delay( 500 );
  oled_puts( 6, 1, "Graphics");
  delay( 1000 );

  for( int i = 0; i < 4; i++ ) {
    for( int x = 96; x >= (i*32); x-- ) {
      oled_draw_bitmap( x, 32, 32, 32, BMP_DOG );
      delay( 10 );
    }
    delay( 100 );
  }

  delay( 10000 );
}

I2C送信処理は、ディスプレイコントローラへのコマンド用とビットマップデータ用を分けて、関数化しています。

/*
  SSD1315 へコマンドを送信

  注:Arduinoライブラリの制限により、32bytes以上のデータは送ることができないので、
    呼び出し側で分割して call する。
*/
int oled_send_cmd( const byte *data, int size )
{
  Wire.beginTransmission( I2C_ADRS );
  Wire.write( 0x00 );
  Wire.write( data, size );
  Wire.endTransmission();

  return 0;
}

Arduinoは、I2Cで32bytes以上のデータを一度に送ることができない仕様なので、ビットマップデータのように大きなサイズのデータを送る場合は分割する必要があります。

/*
  SSD1315 のフレームバッファへ書き込み

  注:Arduinoライブラリの制限により、32bytes以上のデータは送ることができないが、
    画像データは任意の位置で切っても問題ないので、この関数内で分割送信する。
*/
int oled_send_fb( const byte *data, int size )
{
  int chunk_size = 30;

  for( int i = 0; i < size; i += chunk_size ) {
    if( i + chunk_size > size ) {
      chunk_size = size - i;
    }

    Wire.beginTransmission( I2C_ADRS );
    Wire.write( 0x40 );
    Wire.write( data + i, chunk_size );
    Wire.endTransmission();
  }

  return 0;
}

Raspberry Pi (CRuby)

モジュールを、I2C 端子に接続します。

クリックでプログラム全体表示
OLED_Display.rb
#  This file is distributed under BSD 3-Clause License.

require "mruby/i2c"

class OLED_SSD1315
  I2C_ADRS = 0x3c
  WIDTH = 128
  HEIGHT = 64
  FONT_ASCII_8x8 = "\
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07\x5F\x5F\x00\x00\
\x00\x0B\x07\x00\x0B\x07\x00\x00\x14\x7F\x7F\x14\x7F\x7F\x14\x00\
\x26\x6F\x49\x7F\x4B\x7A\x30\x00\x46\x25\x13\x08\x64\x52\x31\x00\
\x36\x4F\x4D\x59\x59\x36\x70\x50\x00\x00\x00\x0B\x07\x00\x00\x00\
\x00\x00\x1C\x3E\x63\x41\x00\x00\x00\x00\x41\x63\x3E\x1C\x00\x00\
\x08\x2A\x1C\x7F\x1C\x2A\x08\x00\x00\x08\x08\x3E\x3E\x08\x08\x00\
\x00\xB0\x70\x00\x00\x00\x00\x00\x00\x08\x08\x08\x08\x08\x08\x00\
\x00\x00\x60\x60\x00\x00\x00\x00\x40\x20\x10\x08\x04\x02\x01\x00\
\x1C\x3E\x61\x41\x43\x3E\x1C\x00\x00\x40\x42\x7F\x7F\x40\x40\x00\
\x62\x73\x79\x59\x5D\x4F\x46\x00\x20\x61\x49\x4D\x4F\x7B\x31\x00\
\x18\x1C\x16\x13\x7F\x7F\x10\x00\x27\x67\x45\x45\x45\x7D\x38\x00\
\x3C\x7E\x4B\x49\x49\x79\x30\x00\x03\x03\x71\x79\x0D\x07\x03\x00\
\x36\x4F\x4D\x59\x59\x76\x30\x00\x06\x4F\x49\x49\x69\x3F\x1E\x00\
\x00\x00\x00\x66\x66\x00\x00\x00\x00\x00\x00\xB6\x76\x00\x00\x00\
\x00\x08\x1C\x36\x63\x41\x00\x00\x00\x14\x14\x14\x14\x14\x14\x00\
\x00\x41\x63\x36\x1C\x08\x00\x00\x06\x07\x51\x59\x59\x0F\x06\x00\
\x3E\x7F\x41\x59\x55\x5F\x1E\x00\x7C\x7E\x13\x11\x13\x7E\x7C\x00\
\x7F\x7F\x49\x49\x49\x7F\x36\x00\x1C\x3E\x63\x41\x41\x63\x22\x00\
\x7F\x7F\x41\x41\x63\x3E\x1C\x00\x00\x7F\x7F\x49\x49\x49\x41\x00\
\x7F\x7F\x09\x09\x09\x09\x01\x00\x1C\x3E\x63\x41\x49\x79\x79\x00\
\x7F\x7F\x08\x08\x08\x7F\x7F\x00\x00\x41\x41\x7F\x7F\x41\x41\x00\
\x30\x70\x40\x40\x40\x7F\x3F\x00\x7F\x7F\x18\x3C\x76\x63\x41\x00\
\x00\x7F\x7F\x40\x40\x40\x40\x00\x7F\x7F\x0E\x1C\x0E\x7F\x7F\x00\
\x7F\x7F\x0E\x1C\x38\x7F\x7F\x00\x3E\x7F\x41\x41\x41\x7F\x3E\x00\
\x7F\x7F\x11\x11\x11\x1F\x0E\x00\x3E\x7F\x41\x51\x31\x7F\x5E\x00\
\x7F\x7F\x11\x31\x79\x6F\x4E\x00\x26\x6F\x49\x49\x4B\x7A\x30\x00\
\x00\x01\x01\x7F\x7F\x01\x01\x00\x3F\x7F\x40\x40\x40\x7F\x3F\x00\
\x0F\x1F\x38\x70\x38\x1F\x0F\x00\x7F\x7F\x38\x1C\x38\x7F\x7F\x00\
\x63\x77\x3E\x1C\x3E\x77\x63\x00\x00\x07\x0F\x78\x78\x0F\x07\x00\
\x61\x71\x79\x5D\x4F\x47\x43\x00\x00\x7F\x7F\x41\x41\x41\x00\x00\
\x01\x02\x04\x08\x10\x20\x40\x00\x00\x41\x41\x41\x7F\x7F\x00\x00\
\x00\x04\x06\x03\x03\x06\x04\x00\x80\x80\x80\x80\x80\x80\x80\x80\
\x00\x01\x03\x06\x0C\x08\x00\x00\x00\x38\x7C\x44\x3C\x7C\x40\x00\
\x01\x7F\x7F\x48\x48\x78\x30\x00\x00\x38\x6C\x44\x44\x6C\x28\x00\
\x00\x38\x7C\x44\x3F\x7F\x40\x00\x00\x38\x7C\x54\x54\x5C\x08\x00\
\x00\x08\x7E\x7F\x09\x03\x02\x00\x00\x58\xE4\xA4\xB4\xB8\x5C\x04\
\x00\x7F\x7F\x08\x08\x78\x70\x00\x00\x00\x04\x7D\x7D\x00\x00\x00\
\x00\x40\xC0\x80\x80\xFD\x7D\x00\x00\x7F\x7F\x30\x78\x6C\x44\x00\
\x00\x00\x01\x7F\x7F\x00\x00\x00\x7C\x7C\x0C\x78\x0C\x7C\x78\x00\
\x00\x7C\x78\x04\x04\x7C\x78\x00\x00\x38\x44\x44\x44\x7C\x38\x00\
\x00\xFC\xFC\x24\x24\x3C\x18\x00\x18\x3C\x24\x24\xFC\xFC\x80\x00\
\x04\x7C\x78\x0C\x04\x0C\x08\x00\x00\x08\x5C\x54\x54\x74\x20\x00\
\x04\x04\x3F\x7F\x44\x64\x20\x00\x00\x3C\x7C\x40\x40\x3C\x7C\x00\
\x00\x1C\x3C\x60\x60\x3C\x1C\x00\x3C\x7C\x60\x38\x60\x7C\x3C\x00\
\x00\x44\x6C\x38\x38\x6C\x44\x00\x00\x4E\x9E\x90\x90\xFE\x7E\x00\
\x00\x44\x64\x74\x5C\x4C\x44\x00\x00\x08\x3E\x77\x41\x41\x00\x00\
\x00\x00\x00\x7F\x7F\x00\x00\x00\x00\x41\x41\x77\x3E\x08\x00\x00\
\x01\x01\x01\x01\x01\x01\x01\x01\x00\x00\x06\x0F\x09\x0F\x06\x00".b

  attr_accessor :font_ascii_8x8

  #
  # init instance
  #
  def initialize( i2c_bus, address = I2C_ADRS )
    @bus = i2c_bus
    @address = address
    @font_ascii_8x8 = FONT_ASCII_8x8
  end

  #
  # init controller
  #
  def init()
    init_cmd1 = "\x00\xAE" +    # Display OFF
                "\xD5\x90" +    # Display Clock, OSC Freq  = 1001b
                                #                Div ratio = 0000b (RST)
                "\xA8\x3F" +    # Multiplex ratio = 111111b (RST)
                "\xD3\x00" +    # Display offset = 0 (RST)
                "\x40" +        # Display start line = 0 (RST)
                "\xA0" +        # Segment remap (RST)
                "\xC0" +        # COM output scan direction (RST)
                "\xDA\x12" +    # COM pins H/W conf (RST)
                "\x81\xB0" +    # Contrast = B0
                "\xD9\x22" +    # Pre charge Phase1 = 2 (RST)
                                #            Phase2 = 2 (RST)
                "\xDB\x30" +    # COM select volt level = 30
                "\xA4" +        # Entire display ON (RST)
                "\xA6" +        # Normal/Inverse = Normal(RST)
                "\x20\x01" +    # Memory addressing = Vertical
                "\x2E"          # Deactivate scroll
    @bus.write(@address, init_cmd1)
    clear()
    init_cmd2 = "\x00\x8D\x14" + # Charge pump = Enable 7.5V
                "\xAF"           # Display ON
    @bus.write(@address, init_cmd2)
  end

  #
  # clear screen
  #
  def clear()
    draw_bitmap( 0, 0, WIDTH, HEIGHT, "\x00" * (WIDTH * HEIGHT / 8) )
  end

  #
  # get 8x8 font bitmap
  #
  def get_font_bitmap( ascii_code )
    if ascii_code < 0x20 || ascii_code > 0x7f
      ascii_code = 0
    else
      ascii_code -= 0x20
    end

    return @font_ascii_8x8[ascii_code * 8, 8]
  end

  #
  # draw string
  #
  def puts( col, row, str )
    col *= 8
    return if col < 0 || col >= WIDTH
    return if row < 0 || row >= HEIGHT / 8

    @bus.write(@address, 0x00, 0x21, col, 127, 0x22, row, row )
    #                          0x21: Set Column Address (0-127)
    #                                          0x22: Set Page Address (0-7)
    str.each_byte {|code|
      @bus.write(@address, 0x40, get_font_bitmap( code ))
    }
  end

  #
  # draw bitmap
  #
  def draw_bitmap( x, y, width, height, data )
    return if x < 0 || x >= WIDTH
    return if y < 0 || y >= HEIGHT

    x1 = x + width - 1
    y1 = (y + height) / 8 - 1
    y /= 8
    return if x1 >= WIDTH
    return if y1 >= HEIGHT / 8

    @bus.write(@address, 0x00, 0x21, x, x1, 0x22, y, y1)
    #                          0x21: Set Column Address
    #                                       0x22: Set Page Address
    @bus.write(@address, 0x40, data)
  end
end

BMP_DOG = "\
\xFF\xFF\xFF\xFF\x7F\xF0\xFF\xFF\x9F\xEF\xFF\xFF\xCF\xEF\x1F\xFE\
\xE7\xEF\xE3\x38\x77\x80\xF8\x27\xB7\x77\xFE\x0F\xFB\x7C\xFF\x7F\
\xFD\x7C\xFF\x7F\xFD\x6F\xFF\x1F\xFD\x0F\xFF\xE3\xFD\x0F\xFF\xE3\
\xFD\x6F\xFF\x1F\xFD\x7C\xFF\x7F\xF3\x7C\xFF\x7F\xB7\x77\xFE\x0F\
\x67\x00\xF8\x27\xEF\x6F\xE3\x38\xCF\xAF\x0F\x9A\x9F\xAF\xFF\xA3\
\x3F\xB0\xFF\xBD\xFF\x9F\xFF\x81\xFF\x6F\xFF\x9D\xFF\x77\xFF\xA1\
\xFF\xF7\xFE\xBF\xFF\xF7\xFD\x80\xFF\xEF\x03\xFF\xFF\xDF\xFC\xFF\
\xFF\x3F\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF"


oled = OLED_SSD1315.new( I2C.new )
oled.init
oled.puts( 2, 3, "OLED Display")
sleep 1

oled.puts( 0, 0, "16cols x 8rows")
sleep 0.5
oled.puts( 6, 1, "Characters")
sleep 1

8.times {|i|
  oled.puts( 0, i, (0x30 + (i + 1) % 10).chr )
  sleep 0.1
}
16.times {|i|
  oled.puts( i, 7, (0x30 + (i + 1) % 10).chr )
  sleep 0.1
}
sleep 5

oled.clear()
oled.puts( 2, 3, "OLED Display")
sleep 1

oled.puts( 0, 0, "128x64 dots")
sleep 0.5
oled.puts( 6, 1, "Graphics")
sleep 1

4.times {|i|
  96.downto( i * 32 ) {|x|
    oled.draw_bitmap( x, 32, 32, 32, BMP_DOG )
    sleep 0.01
  }
  sleep 0.1
}

Rubyのライブラリは、バイト長の制限はないので、コマンドもデータも分割することなく、一度に送信できます。加えて、整数(1バイト)、文字列(バイト列)の混在を許しているため、Arduinoのような関数 Wrapper を作ること無く API を直接コールしても十分その意図は通じます。
我々ユーザーは、とても楽にコードを書くことができますね。(その分、ライブラリ作成者が苦労しているわけですが...)

# コマンド送信
# コマンドを示すコントロ−ルバイト(0x00) に引き続き、カラム、ページをセットする
@bus.write(@address, 0x00, 0x21, x, x1, 0x22, y, y1)
#                          0x21: Set Column Address command.
#                                       0x22: Set Page Address command.

# ビットマップデータ送信
# 表示データを示すコントロールバイト(0x40) に引き続き、任意長のビットマップデータを送信する
@bus.write(@address, 0x40, bitmap_data)

RBoard (mruby/c)

モジュールを、I2C 端子に接続します。
CRubyのプログラムから、require 行をコメントアウトしたものがそのまま動きます。

おわりに

ディスプレイ関連は、フォントデータの用意も必要なこともあいまって、どうしてもプログラムが複雑かつ長くなりがちですね。それでも、なんとか自由な表示ができるところまで実現できました。
Rubyのプログラムは、フォントデータが自由なタイミングで替えられるように作ってあるので、フォントデータさえ用意すれば強調文字やセリフありなしのフォントを混在して表示することもできます。
次の課題として、線の描画や縦8ドットおきの制限をソフト的に解消する事などが考えられます。ディスプレイコントローラが持っているフレームバッファは、I2Cではライトオンリーなので、メインメモリにシャドウバッファを設けて重ね合わせを行うなどの処理が必要になりますが、この規模の組込ディスプレイとしては、ちょっと大げさ過ぎるような気がします。今の制限のままで表示を工夫するのが得策かなと思います。

7回に分けて書いた GroveBeginnerKit を使ってみる記事も、今回で最後です。
おつきあいいただきありがとうございました。

  1. ただ、ゼロベースからプログラムを書くよりは動いているものを参考にした方が近道なので、記事執筆にあたり、Wikiページで紹介されている Arduino 用サンプルプログラム(U8g2ライブラリ)の動作も参考にさせてもらっています。

  2. U8g2ライブラリでは、Virtical Addressing Mode にセットしながら、Page Addressing mode 専用のコマンド 0x10-0x17, 0xB0-0xB7 を使って表示位置のコントロールを行っているようです。結果的に意図通り動いてはいるものの、データシート上は間違いです。その他初期化コードにも、このディスプレイコントローラでは無効な設定箇所がありました。

  3. https://fontstruct.com/fontstructions/show/1482734/namco-arcade-raster
    CC BY 3.0 (https://creativecommons.org/licenses/by/3.0/deed.ja) をコンバートして使用。

  4. 犬イラスト引用元
    https://www.ac-illust.com/main/detail.php?id=1504132

2
1
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1