有機EL ( OLED ) SSD1306 を再検証してみました ( I2C 通信用 )

ESP32 ( ESP-WROOM-32 )

連続描画するスケッチ入力

では、上記の8×8 pixel の絵を連続して表示させてみます。
ソースコード内で描画ドットを90度回転させて、縦のセグメントを水平方向にビット変換させています。
以下のスケッチを入力してみてください。

【ソースコード】 (※無保証 ※PCの場合、ダブルクリックすればコード全体を選択できます)

#include "Wire.h"
 
const uint8_t ADDRES_OLED =  0x3C;
const int SDA_OLED =  5;
const int SCL_OLED =  4;
const uint32_t Frequensy_OLED = 400000; //Max=400kHz
 
uint8_t DotB1[8]={
  0b11111111,
  0b00000111,
  0b11111111,
  0b00000011,
  0b00000101,
  0b00001001,
  0b00010001,
  0b00100001
};
 
void setup() {
  Serial.begin(115200);
 
  SSD1306_Init(); //OLED ssd1306 初期化
  delay(10);
  Clear_Display_All();
  Display_Pic();
}
 
void loop() {
 
}
 
void SSD1306_Init(){
  Wire.begin(SDA_OLED, SCL_OLED);
  Wire.setClock(Frequensy_OLED);
  delay(100);
 
  Wire.beginTransmission(ADDRES_OLED);
    Wire.write(0b10000000); //control byte, Co bit = 1 (1byte only), D/C# = 0 (command) Max=31byte
      Wire.write(0xAE); //display off
    Wire.write(0b00000000); //control byte, Co bit = 0 (continue), D/C# = 0 (command) 
      Wire.write(0xA8); //Set Multiplex Ratio  0xA8, 0x3F
        Wire.write(0b00111111); //64MUX
    Wire.write(0b00000000); //control byte, Co bit = 0 (continue), D/C# = 0 (command) 
      Wire.write(0xD3); //Set Display Offset 0xD3, 0x00
        Wire.write(0x00);
    Wire.write(0b10000000); //control byte, Co bit = 1 (1byte only), D/C# = 0 (command) 
      Wire.write(0x40); //Set Display Start Line 0x40
    Wire.write(0b10000000); //control byte, Co bit = 1 (1byte only), D/C# = 0 (command)
      Wire.write(0xA1); //Set Segment re-map 0xA0/0xA1
    Wire.write(0b10000000); //control byte, Co bit = 1 (1byte only), D/C# = 0 (command)
      Wire.write(0xC8); //Set COM Output Scan Direction 0xC0,/0xC8
    Wire.write(0b00000000); //control byte, Co bit = 0 (continue), D/C# = 0 (command) 
      Wire.write(0xDA); //Set COM Pins hardware configuration 0xDA, 0x02
        Wire.write(0b00010010);
    Wire.write(0b00000000); //control byte, Co bit = 0 (continue), D/C# = 0 (command) 
      Wire.write(0x81); //Set Contrast Control 0x81, default=0x7F
        Wire.write(255); //0-255
    Wire.write(0b10000000); //control byte, Co bit = 1 (1byte only), D/C# = 0 (command)
      Wire.write(0xA4); //Disable Entire Display On
    Wire.write(0b10000000); //control byte, Co bit = 1 (1byte only), D/C# = 0 (command)
      Wire.write(0xA6); //Set Normal Display 0xA6, Inverse display 0xA7
    Wire.write(0b00000000); //control byte, Co bit = 0 (continue), D/C# = 0 (command) 
      Wire.write(0xD5); //Set Display Clock Divide Ratio/Oscillator Frequency 0xD5, 0x80
        Wire.write(0b10000000);
    Wire.write(0b00000000); //control byte, Co bit = 0 (continue), D/C# = 0 (command) 
      Wire.write(0x20); //Set Memory Addressing Mode
        Wire.write(0x10); //Page addressing mode
  Wire.endTransmission();
  Wire.beginTransmission(ADDRES_OLED);
    Wire.write(0b00000000); //control byte, Co bit = 0 (continue), D/C# = 0 (command) 
      Wire.write(0x22); //Set Page Address
        Wire.write(0); //Start page set
        Wire.write(7); //End page set
    Wire.write(0b00000000); //control byte, Co bit = 0 (continue), D/C# = 0 (command) 
      Wire.write(0x21); //set Column Address
        Wire.write(0); //Column Start Address
        Wire.write(127); //Column Stop Address
    Wire.write(0b00000000); //control byte, Co bit = 0 (continue), D/C# = 0 (command) 
      Wire.write(0x8D); //Set Enable charge pump regulator 0x8D, 0x14
        Wire.write(0x14);
    Wire.write(0b10000000); //control byte, Co bit = 1 (1byte only), D/C# = 0 (command) 
      Wire.write(0xAF); //Display On 0xAF
  Wire.endTransmission();
}
//*******************************************
void Display_Pic(){
  int i,j,m,n;
  uint8_t b = 0, dummy = 0;
 
  for(i=0; i<8; i++){
    Wire.beginTransmission(ADDRES_OLED);
      Wire.write(0b10000000); //control byte, Co bit = 1 (1byte only), D/C# = 0 (command) Max=31byte
        Wire.write(0xB0 | i); //set page start address←垂直開始位置はここで決める(B0~B7)
      Wire.write(0b00000000);
        Wire.write(0x21); //set Column Address
          Wire.write(0); //Column Start Address←水平開始位置はここで決める(0~127)
          Wire.write(127); //Column Stop Address 画面をフルに使う
    Wire.endTransmission();
 
    for(j=0; j<16; j++){//column = 8byte x 16 ←8バイト毎に水平に連続で16個表示
      Wire.beginTransmission(ADDRES_OLED);
        Wire.write(0b01000000); //control byte, Co bit = 0 (continue), D/C# = 1 (data) Max=31byte
 
        for(m=7; m>=0; m--){
          for(n=0; n<8; n++){ //描画1バイトを縦列に変換
            dummy = ( DotB1[n] >> m ) & 0x01;
            if(dummy > 0){
              b = b | (dummy << n);
            }
          }
          Wire.write(b); //SSD1306のGDRAM にデータ書き込み
          b = 0;
        }
 
      Wire.endTransmission(); //これが送信されて初めてディスプレイに表示される
      delay(500);
    }
 
  }
}
//**************************************************
void Clear_Display_All(){
  uint8_t i, j, k;
 
  for(i = 0; i < 8; i++){//Page(0-7)
    Wire.beginTransmission(ADDRES_OLED);
      Wire.write(0b10000000); //control byte, Co bit = 1 (1byte only), D/C# = 0 (command)
        Wire.write(0xB0 | i); //set page start address(B0~B7)
      Wire.write(0b00000000);
        Wire.write(0x21); //set Column Address
          Wire.write(0); //Column Start Address(0-127)
          Wire.write(127); //Column Stop Address(0-127)
    Wire.endTransmission();
 
    for(j = 0; j < 16; j++){//column = 8byte x 16
      Wire.beginTransmission(ADDRES_OLED);
      Wire.write(0b01000000); //control byte, Co bit = 0 (continue), D/C# = 1 (data)
      for(k = 0; k < 8; k++){ //continue to 31byte
        Wire.write(0x00);
      }
      Wire.endTransmission();
    }
  }
}

【解説】

●1行目:
Arduino core for ESP32 または ESP8266 標準の I2C ライブラリインクルードです。

●6行目:
I2C通信の最大周波数 400kHz としています。

●22行目:
32-84行で関数化した、SSD1306 初期化です。

●24行目:
122-144行で関数化した、ディスプレイ消去です。
ただ単に 0x00 を画面一杯に表示するようにしているだけです。

●25行目:
86-120行で関数化しているものです。

●33行目:
Wire.begin(); だけの場合はマイコンチップデフォルトの SDA, SCL ピンに設定されますが、今後いろいろな電子工作をやる場合には固定されると不都合なので、ここでピン設定できるようにしました。
今後、いろいろなことをやりたい場合には結構重要です。

●37-83行:
ここは、SSD1306 データシートの初期化フローチャートに従った手順でコマンドを実行しています。
先ほども述べたように、Wire.beginTransmission 関数の後は 31バイトまでしか送信できませんので、68行で一旦区切っています。

●86-120行:
ここで、ディスプレイに描画させています。
92-93行で水平位置のページ番号を0-8で指定して、94-97行で水平開始位置をセグメント単位で指定しています。
100-117行で、実際のデータを送信していますが、コントロールバイト以外では連続して 31byte しか送れないので、8byteごとに区切って送信しています。
105-110行で、描画1バイトを縦列のセグメントに変換していますので、重要なところです。
115行目のWire.endTransmission 関数が送られて初めてディスプレイに表示されます。

コンパイル書き込み実行

では、上記のスケッチをコンパイル書き込み実行させてみてください。
以下の動画のように表示されればOKです。

では、次の実験として、上記のスケッチの 101-102行目を99行目へ移動して、115行目を118行目に移動してみてください。

そして、下図の様に「ツール」メニューの Core Debug Level “Verbose”  ( ESP32 の場合 )にしてみてください。

そして、シリアルモニターを 115200bps で起動し、コンパイル書き込み実行をしてみてください。

いかがでしょうか?
まともに表示されないと思います。

そして、シリアルモニターにはこんなメッセージが出てくると思います。
(※ESP32 の場合)

[W][esp32-hal-i2c.c:231] i2cWrite(): Ack Error! Addr: 3c

つまり、スレーブデバイスのSSD1306 から アクノリッジビット( ACK )が返って来ないというメッセージです。
これからも、Wire.beginTransmission関数の後は、やはり 31byte までということが分かると思います。
このことは十分頭に入れておいてプログラミングしていかねばなりませんね。

まとめ

どうでしょうか?

この SSD1306 というディスプレイドライバをグラフィックディスプレイと勘違いして使用すると、とても難しいデバイスだということが分かると思います。
これは文字に特化した方が遙かに使いやすいです。
それでも、ページという制限があるので、16×16 pixel の文字を表示させるにもちょっと難しくなります。

なんでこの使いにくいデバイスがこんなにも流行ってしまったんだろう・・・。
流行らせた原因は私にもあると思いますが、当時はAmazon で売っている小型のOLED がこれしか無かったというのもありますね。

ということで、捨てることができず、無視できなくなってきたデバイスですので、この際、極めてみたいと思った次第です。

因みに、何度も申し上げますが、私はアマチュアなので、間違えていることがあるかも知れません。
その場合はコメント等でご連絡いただけると助かります。

次回はもっと発展させて、文字表示やグラフィカルな表示方法を紹介する予定です。

ということで、今回はここまでです。

ではまた・・・。

コメント

  1. TM より:

    こんにちは。PIC Basic PRO環境とPIC18FシリーズでSSD1306を使おうと試行錯誤していました。こちらの記事とデータシートをにらめっこして、思い通りに表示させる事ができるようになりました。大変参考になりました。ありがとうございます。

    • mgo-tec mgo-tec より:

      TMさん

      記事をご覧いただき、ありがとうございます。

      PIC使いの方にもこの記事が役立ってくれたことは、ちょっと嬉しいです。
      苦労した甲斐がありました。

  2. ノラネコ より:

    SPIを使った場合の記事も楽しみにしています!よろしくお願いします!

    • mgo-tec mgo-tec より:

      ノラネコさん

      記事をご覧いただき、ありがとうございます。

      あ、そう言えば、SSD1306 の SPI もありましたね。
      すっかり忘れていました。

      今、取組中の課題が多くて、かなり後回しになってしまいそうです。
      スミマセン
      m(_ _)m

  3. にゃ。 より:

    まだ勘違いされているようですね。
    Coビットの意味を全然理解されていない。

    • mgo-tec mgo-tec より:

      にゃ さん

      コメント頂き有難うございます。

      そうですか・・・。
      しばらく、しばらくSSD1306は使っていなかったので、今取り込み中の作業が終わったら見直してみます。
      しばらく時間がかかりますが・・・。
      勘違いばかりでダメダメですね・・・。

    • mgo-tec mgo-tec より:

      にゃ さん

      もうすっかりこの使い方を忘れていて、思い出すのに時間がかかりました。
      今、改めて自分の文章を読んでみると、Co bit のところが曖昧で何か変ですね。
      Google 翻訳も中途半場です。

      今、私が思い出したところによると、Co_bit=0 の場合は、その後に続くデータバイトを、コントロールバイト無しに連続して送信でき、Co_bit=1 ならば、その後のデータバイトは1バイトのみと解釈しました。

      例として、Brightness display off などは、制御コマンドが1バイトだけなので、その場合は Co_bit=1 にするという感じでしょうか。
      実際にGDDRAM へデータバイトを連続で書き込む場合は、Co_bit=0 にして、コントロールバイト1バイトの後、連続でデータバイトのみ送るという感じだと解釈しています。
      実際にそれで問題無く動作しているのですが、これでも間違えていますか??
      何分、英語の解釈が苦手なもので・・・。

    • mgo-tec mgo-tec より:

      にゃ さん

      ソースコードなどを全て修正してみました。
      ご指摘の通り、私の間違えと勘違いでした。

      ご指摘されなかったら、問題無く動作していたので、延々と気付かなかったと思います。
      修正はしたものの、まだ間違えていたらご指摘いただければと思います。
      まだまだ素人でした。
      この度はありがとうございました。
      m(_ _)m

  4. yoshitsune より:

    ライブラリ無しでの表示、大変興味深く読ませていただきました。
    ところで、SPI用の記事は公開されていますでしょうか?

    • mgo-tec mgo-tec より:

      yoshitsune さん

      記事をご覧いただき、ありがとうございます。

      残念ながら、SSD1306についてはSPI用の記事はございません。
      この記事は4年以上前の古い記事で、当時の記憶が大分薄れてきていますが、恐らく、グラフィックが扱いにくいSSD1306をSPIで動かすよりも、SSD1331を使った方が断然扱いやすかったので、SSD1306のSPIはスルーしたんだと思います。

  5. 吾輩は猫 より:

    はじめまして.
    現在大学生で,初めてのIotデバイス制作を始めたのですが,今回のモニターの制御が難しく,難航していたのですがこの記事に完全に助けられました.

    デバイス構成が完全に同じで,すべてが書いてありました.
    細かな内部動作まで解説していただきとても感謝しています.

    これからも拝見させていただきます!
    ありがとうございました.

    • mgo-tec mgo-tec より:

      吾輩は猫さん

      かなり古い記事ですが、ご覧いただきありがとうございます。
      そう言って頂けるとめちゃめちゃうれしいで~す!!!

      今やArduino core ESP32ではらびやんさん作成のLovyan GFXという超優秀なライブラリがあるので、それでESP32やESP8266でSSD1306を制御する方が遙かに高速で高機能ですよ。
      ESP32界隈ではそのライブラリが標準になっているので、試してみて下さい。
      (^^)

  6. じゅ より:

    参考になりました。ありがとうございます!
    Adafruitのライブラリが結構メモリを食っていたので凄く助かりました。
    自分でSSD1306のライブラリを作ってみようと思います。

    • mgo-tec mgo-tec より:

      じゅさん

      記事をご覧いただき、ありがとうございます。
      久々のコメント投稿で嬉しくなっちゃいました。

      ただ、この記事は2017年10月に投稿したもので、素人の手探りでいろいろと穴だらけです。
      今やESP32界隈ではらびやんさん作成のLovyanGFXライブラリが洗練されていて超高速表示でおススメですよ~。

タイトルとURLをコピーしました