2019年9月30日月曜日

OLED 0.91inch 128x32dot datasheetを読む読む (SSD1306)

OLED displayをarduinoに取り付けたい.

中華通販から買った@¥150ぐらいのがこれ.128x32dotらしい.IFはI2Cである.
繋げば動くArduinoであるから、どこかのスケッチをDLすれば動くのだろう.しかし疑問はいろいろある.I2Cアドレスは何番なのか? キャラクタROMを搭載しているのか? などなど....

以下ではこいつをArduino+I2Cで動かす方法を解説する.

-----
netを徘徊すると、SSD1306というOLED driver ICが検索にひっかかるのだが、どうやらSSD1306とは下写真の「表示部+IC」をひっくるめてSSD1306であるらしい.
そして、最初の写真にあるブルーのプリント基板はSSD1306とは無関係だという点も知っておく必要があるだろう.ブルー基板の役割は何か?
・SSD1306にはI2C以外にもIF形式が用意されているのだが、ブルー基板によってI2C IFに固定されている
・ブルー基板の回路図はこれらしい.3.3V REGが描かれている.
てなことを考えつつ、SSD1306 datasheetを読んでみた.
datasheetはこれみたい.  → こちら
ただしこのリンク先は128x64dotを解説しているのでGraphic RAMのアロケーションを読み替える努力を要するかもしれない.RAMの下半分には書けるけど表示されないとかなんとか.

それでは、DDS1306 datasheetから気になったところを抜き出してゆく.

【電源】
IC電源        VDD = 1.65~3.3V
OLED電源    VCC = 7~15V
いきなりなんだこりゃ? OLEDは3.3Vじゃないんだ? OLEDの高電圧をどうやって生成しているのだろう? SSD1306には、キャパシタを使った電圧ダブラが内蔵されているらしいよ.

【IF】
詳述はしないが、こんなにたくさんある.6800なんか懐かしいよ.作ってたの高校生の時だもん.
IFの選択は外部ピンBS[2:0]で行う.BS設定はブルー基板上で固定されている.
6800               BS[2:0]=100
8080               BS[2:0]=110
3線SPI            BS[2:0]=001
4線SPI            BS[2:0]=000
I2C                  BS[2:0]=010    ←今回のはこれ

【I2Cアドレス】
011110x
xの部分は外部ピンのSA0で決まる.
すなわちこのどっちか.
0111101 = 3D
0111100 = 3C
ブルー基板にSA0ピンが在るのかどうか? だが回路図を見ても、現物を見ても、SA0と思しき信号線は見えない.だとすると、このOLEDは決め打ちで3Dか3Cのどちらかなのだろう.それでどっちなのかは、動かしてみなくちゃ判らないのだろう.動かしてみたところ、3Cであることが確認された.

【I2C Write/Read】
I2Cの1アクションはこんなバイト並びでアクセスするんだってさ.
まず知るべきなのは2byte目であろう.
2byte目のbit構成は、{ Co, D/C#, 000000 } である.
Co=0       → 後続はdataだけである意味
D/C#=0   → 後続はcommandである意味
D/C#=1   → 後続はGDRAM dataである意味(アドレスauto increment)
やや意味不明.SSD1306をいろいと動かしてみても、Coがどう働いているのかは不明だ.

飽くまでもやってみた結果なのでわたしの勘違いかもしれないが、I2Cでcommandを与えるやり方と、GDRAMへdataを書くやり方には違いがあるようだ.両者の例をまず示す.

<commandを与えるやり方>
  Wire.beginTransmission(0x3c);    I2C address
  Wire.write(0x00);     Co=0  C/D=0の意味
  Wire.write(0xd5);   複数個のcommands
  Wire.write(0x80);      ↓
  Wire.write(0x8d);      ↓
  Wire.write(0x14);      ↓
  Wire.write(0xaf);       ↓
  Wire.endTransmission();

<GDRAM dataを書くやり方>
  Wire.beginTransmission(0x3c);   I2C address
  Wire.write(0x40);    Co=0  C/D=1の意味
  Wire.write(0x33);    GDRAMへ1byte書く、GDRAMアドレスはインクリされる
  Wire.endTransmission();

違いの一つ目は、コントロールbyteが00か40かである.これはおまじないみたいなものなので無問題である.
違いの二つ目は、commandは連続した複数bytesを連続送信できたが、GDRAMはI2Cの1動作毎に1byteしか送信できないみたいだ.やってみてそうだった.Coを変えてみても結果は同じであった.なんか腑に落ちない.

【VRAMなのか?】
8bitマイコン時代に在ったようなVRAM+キャラクタROM構成ではない.
128x64dotのベタなグラフィックRAMがあるのみ.
したがって文字を表示させたければユーザーがdot絵を描く必要がある.

【GDRAM構成】
SSD1306は128x64dotのGDRAMを内蔵している.
縦横の呼び名は、横をSEG0-SEG127、縦をCOM0-COM63 と称する.
それに加えて、8個のCOMを束ねてPAGEと称する.PAGE0~PAGE7がある.
1つのPAGEは画面上で128x8dotの横長区画になるのは想像がつくだろう.
↓ホストCPUからGDRAMに1byteを書き込んだとき、その8bitが画面上にどのように表示されるのかには注意が必要だ.その8bitはPAGEの縦一列に対応するのである.なんだか扱いがめんどくさい気がしてくる.この図はPAGE単体の拡大図だ.
↓SSD1306は128x64dot GDRAMを内蔵しているが、ここで扱う128x32dot OLEDにはその半分しか表示されない.この図のように.
もっとも、必ずしもこの図のように上半分だけとは限らない.GDRAMの表示開始lineを指定する機能があるので表示エリアを上下に移動させることは可能だ.(コマンド40-7f)

↓まとめるとこのようになる.縦8bit画素に相当する1byteがGDRAMアクセスの最小単位である.書き込みアドレスは自動インクリされるので、0byte目~511byte目まで書き込めば128x32dotの表示エリアを埋め尽くせるという仕組みだ.
ただし、SSD1306はアドレスインクリメント範囲を任意に設定できるので、上図のベタな展開はそういった機能を使わないケースにおいてである.

【コマンド】
使いそうなコマンドとして以下をリストしておくなり.

表示開始行
byte数:1
command: 40h ~ 7Fh      40hならCOM0、7FhならCOM63

縦ライン数
byte数:2
command:{ A8h, 31 }
128x32dot OLEDなので32lineなので31をセットしておく.この数がアンマッチだとscrollしたときにおかしくなってしまうのだと思うんだ.

表示反転
byte数:1
command:A0h     通常
command:A1h     左右反転
command:C0h     通常
command:C8h     上下反転

COM config
byte数:2
command:{ DAh, 00 }
リセット値が01なのでなんか嫌なので00にしておく.機能はよくわからない.

scrollする/しない
byte数:1
command:2Eh      scrollしない
command:2Fh      scrollする

GDRAM write address increment range
byte数:8
command:{ 20h, 00, 21h, 00, 127, 22h, 0, 3 }
GDRAMへ書き込む際のアドレスは自動インクリメントされる.そのアドレス範囲を指定するコマンドである.3つのコマンドの合体である.
下図のように、GDRAMの0番地→511番地まで自動インクリされたくば上のコマンドを発すればいい.このコマンドによって、513個目のdataは0番地に戻る.結果としてGDRAMの下半分には書き込めなくなる.OLEDが128x32dotなのでそれでいいのだ.
コントラスト
byte数:2
command:{ 81H, N }
N=0~255  (reset value 127)

内蔵RC発振器
byte数:2
command:{ D5h, 80h }     この設定だと370kHzで発振するらしい

内蔵チャージポンプ
byte数:2
command:{ 8Dh, 14h }     OLED用高電圧発生回路をオンにする

SLEEP/ACTIVE
byte数:1
command:AEH     OFF、sleep
command:AFH     ON、active

コマンドはこれらの他にもあるけど、reset valueで放置しとけばいい感じ.
また、scroll commandがいろいろあるが、興味ないので割愛する.

-----
以上の知見をもとに、べたーっとcodingしてみた. →こちら

追記:asciiで文字列表示できるようにしたのもつくってみた.こっちの方が参考になるかもしれない   →こちら

トラブルシュート: 立ち上がりの遅い電源をつかったらトラブル発生.Arduinoの起動時間がOLEDよりも早くて、OLEDがまだ寝ぼけているうちにsetup()が済んでしまってOLEDがクルクルパーになってしまった.対策のため、setup()の先頭にdelay(1000)を入れとくのがオススメよ.こんなことってよくあるよね.(リンク先のsourceにdelayは入ってない)


↓動かした環境はこんなもの.いろんな基板がついてるけど、本件に関係するのはarduino nanoとOLEDだけである.
↓プログラムを動かすとOLEDに数字が表示される.それだけ.8x8dot文字だと老眼にはきつい文字サイズである.もっと大きな表示にしなくちゃいけないね.

以下はsource code.

#include <Wire.h>        I2Cを動かすライブラリ
#define OLED_ADRS 0x3c         OLED基板のI2Cアドレスは3Ch

自作した0~9のキャラクタ
byte font[] = { 
  0x3e,0x41,0x41,0x41,0x41,0x3e,0x00,0x00, // 0
  0x00,0x00,0x42,0x7f,0x40,0x00,0x00,0x00, // 1
  0x42,0x61,0x51,0x49,0x49,0x46,0x00,0x00, // 2
  0x22,0x41,0x49,0x49,0x49,0x36,0x00,0x00, // 3 
  0x18,0x14,0x12,0x7f,0x10,0x10,0x00,0x00, // 4
  0x2f,0x49,0x49,0x49,0x49,0x30,0x00,0x00, // 5
  0x3e,0x49,0x49,0x49,0x49,0x32,0x00,0x00, // 6
  0x03,0x41,0x21,0x11,0x09,0x07,0x00,0x00, // 7
  0x36,0x49,0x49,0x49,0x49,0x36,0x00,0x00, // 8
  0x26,0x49,0x49,0x49,0x49,0x3e,0x00,0x00 };  // 9

ーーー中略ーーー

数字をOLEDに表示するルーチン
1文字で8x8dotなので8bytesを送信する.
GDRAMへ1byite送信するためにI2Cへ3bytes送信している.
void oled_numeric(int n)
{
 for(int i=0;i<8;i++){
  Wire.beginTransmission(OLED_ADRS);   アドレス送信
  Wire.write(0x40);           GDRAMデータである
  Wire.write(font[n*8+i]);    font送信
  Wire.endTransmission();
 }
}

OLEDをクリアする.ゼロをGDRAM全域に書き込む.
void oled_clear()
{
  for(int i=0;i<512;i++){
    Wire.beginTransmission(OLED_ADRS);
    Wire.write(0x40);
    Wire.write(0);
    Wire.endTransmission();
  }
}

起動時に一度呼ばれるルーチン
各種初期化
void setup() {
  Wire.begin();       I2C初期化

  SSD1306の初期設定.コマンドは上で説明したとおり.
  Wire.beginTransmission(OLED_ADRS); // transmit to device 3d
  Wire.write(0x00);  // below are commands
  Wire.write(0x40);  // display start line (40-7f)
  Wire.write(0xa8);  // line number
  Wire.write(0x1f);  // line number 32  
  Wire.write(0xa0);  // left-right non inverting
  Wire.write(0xc0);  // up-down non inverting
  Wire.write(0xda);  // COM config
  Wire.write(0x00);  // COM config normal
  Wire.write(0x2e);  // stop scroll
  Wire.write(0x20);  // addressing mode
  Wire.write(0x00);  // addressing mode horizontal
  Wire.write(0x21);  // addressing range SEG0-SEG127
  Wire.write(0);
  Wire.write(127);
  Wire.write(0x22);  // addressing range PAGE0-PAGE3
  Wire.write(0);
  Wire.write(3);
  Wire.write(0x81);  // contrast
  Wire.write(0xff);  // contrast max
  Wire.write(0xd5);  // set OSC
  Wire.write(0x80);  // 370kHz
  Wire.write(0x8d);  // enable charge pump
  Wire.write(0x14);  // charge pump
  Wire.write(0xaf);  // OLED ON
  Wire.endTransmission();     // stop transmitting

  oled_clear();
}

ループルーチン
OLEDに0~9を0.5秒毎に表示する
void loop() {
  if(oled_number==9) oled_number=0;
  else oled_number++;
  oled_numeric(oled_number);

  delay(500);
}


かしこ

0 件のコメント:

コメントを投稿