SSD1306 の pixel 配置について
SSD1306 は、Segment/Common Driverコントローラというものです。
グラフィックディスプレイには、Pixel ( ドット )単位で明るさや色を指定できるものがありますが、SSD1306 はあくまでセグメント単位で表示するものです。
ですから、ライブラリを使わずに、自分で一からプログラミングしようとして、普通のグラフィックディスプレイと同じ感覚でやろうとすると、まったくうまくいきません。
それはどういうことかというと、128×96 pixel の内訳は、下図の様にまずページという単位で区切られています。
購入当初のデフォルト状態のページ番号は下図の様な並びになっています。
注意していただきたいのは、基板のシルク印刷から見ると、上下逆になっています。
一般的には、上左端が原点( 0, 0 )になってほしいですよね。
これを直す方法は後で述べます。
そして、このページは128個のセグメント ( segment ) で構成されています。
そのセグメント ( segment ) は下図の様に8個の pixel ( ドット )で構成されています。
グラフィックディスプレイならば、この pixel 一個一個に対してデータを割り当てられると思いますよね。
でも、SSD1306 はセグメント単位でしか書き込みできません。
つまり、セグメント0に 10011011 というビットを書き込むと上図のように 1 の部分が点灯し、0 の部分が消灯します。
SSD1306 の難しいところは、このセグメントを水平方向に変換( Remap )できないことです。
つまり、常にセグメントは縦方向なのです。
これは 8×8 pixel のフォントを表示する場合には好都合ですが、直線や点を描く場合にはとても難しい処理を考えなければなりません。
縦の線は簡単ですが、同じページ内に水平線を重ねたい場合や、ページを跨いだ斜め線を描きたい場合は面倒です。
ということで、グラフィカルな線や点を描く方法は後日、別記事で紹介したいと思います。
では、まず、以前の記事でも紹介した、以下のようなビット列を表示させたい場合を考えます。
11111111 00000111 11111111 00000011 00000101 00001001 00010001 00100001
これを普通に送信させて下図の様に表示させたいですよね。
では、以下のようなスケッチを入力してみてください。
これは、ESP32 および ESP8266 共通です。
【ソースコード】 (※無保証 ※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);//※このバイトも含め、以後、合計32byteまで送信できる Wire.write(0b10000000); //control byte, Co bit = 1, D/C# = 0 (command) Wire.write(0xAE); //display off Wire.write(0b00000000); //control byte, Co bit = 0, D/C# = 0 (command) Wire.write(0xA8); //Set Multiplex Ratio 0xA8, 0x3F Wire.write(0b00111111); //64MUX Wire.write(0b00000000); //control byte, Co bit = 0, D/C# = 0 (command) Wire.write(0xD3); //Set Display Offset 0xD3, 0x00 Wire.write(0x00); Wire.write(0b10000000); //control byte, Co bit = 1, D/C# = 0 (command) Wire.write(0x40); //Set Display Start Line 0x40 Wire.write(0b10000000); //control byte, Co bit = 1, D/C# = 0 (command) Wire.write(0xA0); //Set Segment re-map 0xA0/0xA1 Wire.write(0b10000000); //control byte, Co bit = 1, D/C# = 0 (command) Wire.write(0xC0); //Set COM Output Scan Direction 0xC0,/0xC8 Wire.write(0b00000000); //control byte, Co bit = 0, 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, 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, D/C# = 0 (command) Wire.write(0xA4); //Disable Entire Display On Wire.write(0b00000000); //control byte, Co bit = 0, D/C# = 0 (command) Wire.write(0xA6); //Set Normal Display 0xA6, Inverse display 0xA7 Wire.write(0b00000000); //control byte, Co bit = 0, 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, 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, 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, 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, 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, D/C# = 0 (command) Wire.write(0xAF); //Display On 0xAF Wire.endTransmission(); } //******************************************* void Display_Pic(){ Wire.beginTransmission(ADDRES_OLED); Wire.write(0b10000000); //control byte, Co bit = 1, D/C# = 0 (command) Wire.write(0xB0); //set page start address←垂直開始ページはここで決める(B0~B7) Wire.write(0b00000000); //control byte, Co bit = 0, D/C# = 0 (command) Wire.write(0x21); //set Column Address Wire.write(0); //Column Start Address←水平開始位置はここで決める(0~127) Wire.write(127); //Column Stop Address 画面をフルに使う Wire.endTransmission(); Wire.beginTransmission(ADDRES_OLED); //この後は Max 31byteまで Wire.write(0b01000000); //control byte, Co bit = 0 (continue), D/C# = 1 (data) for(int i=0; i<8; i++){ Wire.write(DotB1[i]); //SSD1306のGDRAM にデータ書き込み } Wire.endTransmission(); //これが送信されて初めてディスプレイに表示される } //************************************************** 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, D/C# = 0 (command) Wire.write(0xB0 | i); //set page start address(B0~B7) Wire.write(0b00000000); //control byte, Co bit = 0, D/C# = 0 (command) 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(); } } }
では、コンパイル書き込み実行させてみてください。
すると、下図の様に表示されます。
デフォルト状態ではこのようになるので、Remap コマンドを使って左上端を始点にしたいと思います。
それは、次のようなコマンドです。
上記のスケッチの場合、49行と51行のコマンドになります。
49行目:Set Segment Re-map
これは実際は水平反転するコマンドになります。
●制御コマンド0xA0 を送信した場合
●制御コマンド0xA1 を送信した場合
51行目:Set COM Output Scan Direction
これは、実際は垂直反転するコマンドになります。
●制御コマンド0xC0 を送信した場合
●制御コマンド0xC8 を送信した場合
制御コマンド 0xA1 , 0xC8 を送信した場合
ということで、左上端を始点(0 , 0)にしたい場合、制御コマンド 0xA1 と 0xC8 を送信すればよいということになります。
ただ、先ほども述べたように、セグメントを水平方向に変換できません。
ですから、そうしたい場合には Arduino IDE 上のプログラムを作って変換することになります。
その方法は次のスケッチを参照してください。
コメント
こんにちは。PIC Basic PRO環境とPIC18FシリーズでSSD1306を使おうと試行錯誤していました。こちらの記事とデータシートをにらめっこして、思い通りに表示させる事ができるようになりました。大変参考になりました。ありがとうございます。
TMさん
記事をご覧いただき、ありがとうございます。
PIC使いの方にもこの記事が役立ってくれたことは、ちょっと嬉しいです。
苦労した甲斐がありました。
SPIを使った場合の記事も楽しみにしています!よろしくお願いします!
ノラネコさん
記事をご覧いただき、ありがとうございます。
あ、そう言えば、SSD1306 の SPI もありましたね。
すっかり忘れていました。
今、取組中の課題が多くて、かなり後回しになってしまいそうです。
スミマセン
m(_ _)m
まだ勘違いされているようですね。
Coビットの意味を全然理解されていない。
にゃ さん
コメント頂き有難うございます。
そうですか・・・。
しばらく、しばらくSSD1306は使っていなかったので、今取り込み中の作業が終わったら見直してみます。
しばらく時間がかかりますが・・・。
勘違いばかりでダメダメですね・・・。
にゃ さん
もうすっかりこの使い方を忘れていて、思い出すのに時間がかかりました。
今、改めて自分の文章を読んでみると、Co bit のところが曖昧で何か変ですね。
Google 翻訳も中途半場です。
今、私が思い出したところによると、Co_bit=0 の場合は、その後に続くデータバイトを、コントロールバイト無しに連続して送信でき、Co_bit=1 ならば、その後のデータバイトは1バイトのみと解釈しました。
例として、
Brightnessdisplay off などは、制御コマンドが1バイトだけなので、その場合は Co_bit=1 にするという感じでしょうか。実際にGDDRAM へデータバイトを連続で書き込む場合は、Co_bit=0 にして、コントロールバイト1バイトの後、連続でデータバイトのみ送るという感じだと解釈しています。
実際にそれで問題無く動作しているのですが、これでも間違えていますか??
何分、英語の解釈が苦手なもので・・・。
にゃ さん
ソースコードなどを全て修正してみました。
ご指摘の通り、私の間違えと勘違いでした。
ご指摘されなかったら、問題無く動作していたので、延々と気付かなかったと思います。
修正はしたものの、まだ間違えていたらご指摘いただければと思います。
まだまだ素人でした。
この度はありがとうございました。
m(_ _)m
ライブラリ無しでの表示、大変興味深く読ませていただきました。
ところで、SPI用の記事は公開されていますでしょうか?
yoshitsune さん
記事をご覧いただき、ありがとうございます。
残念ながら、SSD1306についてはSPI用の記事はございません。
この記事は4年以上前の古い記事で、当時の記憶が大分薄れてきていますが、恐らく、グラフィックが扱いにくいSSD1306をSPIで動かすよりも、SSD1331を使った方が断然扱いやすかったので、SSD1306のSPIはスルーしたんだと思います。
はじめまして.
現在大学生で,初めてのIotデバイス制作を始めたのですが,今回のモニターの制御が難しく,難航していたのですがこの記事に完全に助けられました.
デバイス構成が完全に同じで,すべてが書いてありました.
細かな内部動作まで解説していただきとても感謝しています.
これからも拝見させていただきます!
ありがとうございました.
吾輩は猫さん
かなり古い記事ですが、ご覧いただきありがとうございます。
そう言って頂けるとめちゃめちゃうれしいで~す!!!
今やArduino core ESP32ではらびやんさん作成のLovyan GFXという超優秀なライブラリがあるので、それでESP32やESP8266でSSD1306を制御する方が遙かに高速で高機能ですよ。
ESP32界隈ではそのライブラリが標準になっているので、試してみて下さい。
(^^)
参考になりました。ありがとうございます!
Adafruitのライブラリが結構メモリを食っていたので凄く助かりました。
自分でSSD1306のライブラリを作ってみようと思います。
じゅさん
記事をご覧いただき、ありがとうございます。
久々のコメント投稿で嬉しくなっちゃいました。
ただ、この記事は2017年10月に投稿したもので、素人の手探りでいろいろと穴だらけです。
今やESP32界隈ではらびやんさん作成のLovyanGFXライブラリが洗練されていて超高速表示でおススメですよ~。