概要
CH32V+MounRiver Studioで開発するのがとても肌に合うので何でもかんでもMounRiverで出来るように自分でドライバを整えていこうという試みの中の今回はOLEDディスプレイの制御をやってみたいと思います。
大まかなやり方
- I2CでSSD1306を初期化
- ビデオメモリに画像を描画
- I2CでSSD1306にデータ流し込み
やることはシンプルなのですが、SSD1306の仕組みが分かるまでは何をして良いのやら分からなかったりします。
今回は、GitHubにあるAdafruitのArduino用のコードを見ながら必要な部分だけを移植…しようとしていたらChatGPTがサクッと動くものを作ってくれたのでそれを元にチューニングしてみました。ChatGPT優秀すぎてやばい。
抑えておくべきSSD1306の仕様
・基本制御は「コマンド書き込み」と「データ書き込み」
・「コマンド書き込み」は2バイト固定
・「データ書き込み」はバースト書き込み可能(オートインクリメント)。
「コマンド書き込み」は、スレーブアドレスを送った後、
0x00を送って、次にコマンドを1バイト送ります。
ということで、コマンドを1バイト送るための関数 SSD1306_WriteCommand を実装します。
ChatGPTが書いてくれたまんまです。
#define SSD1306_ADDR 0x3C << 1
#define SSD1306_COMMAND 0x00
#define SSD1306_DATA 0x40
// SSD1306にコマンド送信
void SSD1306_WriteCommand(uint8_t cmd) {
I2C_GenerateSTART(I2C1, ENABLE);
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));
I2C_Send7bitAddress(I2C1, SSD1306_ADDR, I2C_Direction_Transmitter);
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));
I2C_SendData(I2C1, SSD1306_COMMAND);
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
I2C_SendData(I2C1, cmd);
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
I2C_GenerateSTOP(I2C1, ENABLE);
}
これを使って初期化を行います。
下記はChatGPTが書いたコードのままですが、実際には最後の「Display ON」の前に内部メモリをきれいにしておかないと、初期状態のメモリ内容(不定値)がノイズとして表示されます。
とはいえほぼそのまま使えるコードを出してくれました。
// SSD1306 初期化
void SSD1306_Init(void) {
Delay_Ms(100); // 電源オン待ち
SSD1306_WriteCommand(0xAE); // Display OFF
SSD1306_WriteCommand(0xA4); // Entire Display ON (resume)
SSD1306_WriteCommand(0xD5); SSD1306_WriteCommand(0x80); // Set Clock
SSD1306_WriteCommand(0xA8); SSD1306_WriteCommand(0x3F); // Multiplex
SSD1306_WriteCommand(0xD3); SSD1306_WriteCommand(0x00); // Display Offset
SSD1306_WriteCommand(0x40); // Set Start Line
SSD1306_WriteCommand(0x8D); SSD1306_WriteCommand(0x14); // Charge Pump ON
SSD1306_WriteCommand(0x20); SSD1306_WriteCommand(0x00); // Horizontal Addressing
SSD1306_WriteCommand(0xA1); // Segment Remap
SSD1306_WriteCommand(0xC8); // COM Output Scan Direction
SSD1306_WriteCommand(0xDA); SSD1306_WriteCommand(0x12); // COM Pins
SSD1306_WriteCommand(0x81); SSD1306_WriteCommand(0xCF); // Contrast
SSD1306_WriteCommand(0xD9); SSD1306_WriteCommand(0xF1); // Pre-charge
SSD1306_WriteCommand(0xDB); SSD1306_WriteCommand(0x40); // VCOM Detect
SSD1306_WriteCommand(0xA6); // Normal Display
SSD1306_WriteCommand(0xAF); // Display ON
}
ここまでやったら「データ書き込み」で画面が更新されるようになります。
そして分かりにくい「データ書き込み」ですが、基本は「ドットを打ちたい位置にカーソルを合わせてデータを書き込む」です。
このときの「データ」は1バイトで、縦方向8画素分の情報になっています。
下記画像は「顧客が本当に必要だった情報」です(笑)
データシートにこの図を載せてくれればかなり理解が早かったのに…と思いました。
※黄色と青色のOLEDは上側16ラインが黄色、下側48ラインが青色になっています。
左上の緑のブロックが「カーソル」だと思って下さい。これは左上にカーソルがある状態です。
「コマンド書き込み」で、0xB0、0x00, 0x10を送るとこの左上の位置にカーソルが来ます。
この状態で「データ書き込み」に例えば0xFFを書き込むとカーソルの8画素が全て点灯します。
上側がLSBなので、例えば0x0Fを書き込むと上の4画素が点灯して下の4画素が消灯します。
というわけで、コマンドでカーソルを動かしてデータを送る、というのが基本の流れになります。
ChatGPTが吐き出したコードも載せておきます。
// カーソル位置設定(ページ=Y方向, col=X方向)
void SSD1306_SetCursor(uint8_t page, uint8_t col) {
SSD1306_WriteCommand(0xB0 + page); // Page Start Address
SSD1306_WriteCommand(0x00 + (col & 0x0F)); // Lower Column
SSD1306_WriteCommand(0x10 + ((col >> 4) & 0x0F)); // Higher Column
}
// SSD1306にデータ送信(画面に出力)
void SSD1306_WriteData(uint8_t data) {
I2C_GenerateSTART(I2C1, ENABLE);
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));
I2C_Send7bitAddress(I2C1, SSD1306_ADDR, I2C_Direction_Transmitter);
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));
I2C_SendData(I2C1, SSD1306_DATA);
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
I2C_SendData(I2C1, data);
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
I2C_GenerateSTOP(I2C1, ENABLE);
}
ChatGPTが吐き出してくれたのはここまでで、このままでも十分使えるのですが、これだとWriteData1回で1バイト(8画素)しか書き込めないので効率が悪いです。なのでもっとたくさんのデータをいっぺんに書き込めるようにしてみました。
「バースト転送に対応して」とChatGPTにお願いしたらこれを書いてくれました。らくちん。
void SSD1306_WriteDataBurst(uint8_t *buf, uint16_t len) {
I2C_GenerateSTART(I2C1, ENABLE);
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));
I2C_Send7bitAddress(I2C1, SSD1306_ADDR, I2C_Direction_Transmitter);
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));
I2C_SendData(I2C1, SSD1306_DATA); // Control Byte: Data Mode
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
for (uint16_t i = 0; i < len; i++) {
I2C_SendData(I2C1, buf[i]);
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
}
I2C_GenerateSTOP(I2C1, ENABLE);
}
これを使うことで画面全体を一発で再描画できます。
uint8_t vram[1024]; //128 * 64 / 8 = 1024
//全ドットを点灯
for(i=0; i<1024; i++){
vram[i] = 0xFF;
}
SSD1306_WriteDataBurst(vram, 1024);
オシロスコープで確認したところ、1回の全画面更新にかかる時間は約23.8ミリ秒でした(I2Cの速度は400kHz)。
1バイトの転送に9クロック(8bit+Ack)かかるので、1/400k * 9 * 1024 = 0.0232304なので、ほぼ理論値に近いです。
描画だけであれば43.4Hz出ることになるわけなのでフレームレートも十分ですね。
まとめ
とりあえずここまで出来てしまえば後はこのスクリーンをどのように使うかという話しだけなので、自分の使いたい機能をプログラムしていけば良いということになります。Adafruitのライブラリには5x7ピクセルのフォントがあって文字出力がやりやすくなったりしています。ただ、実際このサイズで使うとかなり小さくて見づらいので、2倍くらいにしたフォントを用意したほうが実用的だろうと思います。これはこれから作っていこうと思います。
もうちょっと整ったらライブラリ公開する予定です。いつになるやら分かりませんが。

