対象読者
IoTに興味があり、C#とRaspberry Pi(Linux環境)の基本的な知識がある方を対象とします。Linuxや電子工作の初歩的な説明は割愛していますので、「Raspberry Piをつかったセンサープログラミング超入門」の記事なども併せて参照してください。
はじめに
前回は、I2C接続でのOLEDディスプレイモジュールの基本的な制御を紹介しました。今回は、ディスプレイに直線や文字を表示してみましょう。
OLEDディスプレイの表示手法
OLEDディスプレイモジュールの画面コントローラーSSD1306の表示仕様は、やや特殊なので、今回はこの仕様をあまり意識せずに、ドット単位の描画やテキスト表示を行う方法を解説します。
ダブルバッファリング
前回説明したように、画面コントローラーのSSD1306では、画面データを縦方向に1バイト(8ビット)単位で、送信する必要があります。単純に0のデータを送信するなら、あまり問題ありませんが、グラフや文字などをドット単位で自由に表示したい場合、その都度1バイト単位のデータを作成するのは困難です。
そのため、直接画像データを転送するのではなく、メモリ上の領域(画面バッファ)を介して画像データを送信するようにします。処理の流れは、次のようになります。
- 画面の解像度と同じビット数の仮想的な領域を、画面バッファとしてメモリ上に作成
- 画面バッファに対して、(仮想的に)描画
- 画面バッファのデータを、画像データとしてまるごと送信
こうすれば、画面データの送信仕様に関係なく、自由に画像データが作成できます。
ちなみにこのように、直接描画メモリに描画(送信)するのではなく、いったん別のメモリ領域に描画を行い、すべて書き終わった後にデータを転送する手法を「ダブルバッファリング」と呼びます。
BitArrayによる画面バッファ
画面バッファとなるメモリ上の領域は、System.Collections.BitArrayクラスを利用することにしました。BitArrayは、ビット値をブール型の配列として管理するクラスです。
前回作成したOledSsd1306クラスに、次のように、画面の解像度(横128ドット縦64ドット)と同じビット数のBitArrayをBitbufferという名前で追加します。ディスプレイモジュールの画面サイズは、他のクラスやコンストラクタでも参照できるように、public staticとして定義しています。
public class OledSsd1306 { // 画面サイズ public static int Width { get; } = 128; public static int Height { get; } = 64; public static int Unit { get; } = 8; // 画面バッファ private readonly BitArray Bitbuffer = new BitArray(Width * Height); ~略~
画面バッファへの描画は、このBitArrayのビットを1にすることになります。ビットを1にするには、BitArrayクラスのSetメソッド、ビットを読み出すにはGetメソッドを用います。
では、画面バッファに1ビットを読み書きするメソッドを定義しましょう。
// 画面バッファに1ビット書き込む public void SetPixel(int x, int y, bool b = true) { Bitbuffer.Set(x + y * Width, b); } // 画面バッファから1ビット読み出す public bool GetPixel(int x, int y) { return Bitbuffer.Get(x + y * Width); } // 画面バッファをすべて0(false)か1(true)にする(1) public void SetAll(bool b = false) { Bitbuffer.SetAll(b); }
BitArrayは、縦も横もない単なる配列ですが、メソッドの引数に縦(0~63)と横(0~127)の位置(座標)を指定して、該当のビットを操作できるようにしました。
また、画面バッファをすべて0か1にするメソッドも作成します。こちらはBitArrayのSetAllメソッドを呼び出すだけです(1)。
なお、すべてのメソッドで引数の範囲チェックなどは省略しています。必要に応じて追加してください。
画面バッファの送信
次に画面バッファの送信、SendBufferメソッドを定義しましょう。画面バッファから、縦方向に8ビット分読み出し(1)、それを1バイトとして送信します。1バイトデータの組み立ては、1を縦の位置に応じてビットシフトした値をORして作成します(2)。
// 画面バッファのデータを送信する public void SendBuffer() { for (int p = 0; p < Height / Unit; p++) { for (int x = 0; x < Width; x++) { byte b = 0; for (int y = 0; y < Unit; y++) // 縦に8ビット読み出す(1) { if (GetPixel(x, y + p * Unit)) // ビット値を読み出す { b |= (byte)(1 << y); // 1バイトデータを組み立てる(2) } } SendData(b); } } }
BitArrayの初期値はすべてfalseなので、画面バッファの初期値はすべて0となります。そのため、OledSsd1306クラスのインスタンス生成後、すぐにSendBufferメソッドを呼び出せば、画面クリアになります。
var oled = new OledSsd1306(fd1); oled.SendBuffer(); // 画面バッファの送信(初期値0なので、画面クリアになる)
直線(ライン)の描画
1ビット(ドット)単位での描画ができましたので、このメソッドを使えば、直線や文字などの描画も可能です。まずは、2つの点の間を直線で描くメソッドを作成してみましょう。
コンピューターグラフィックスでの2点間の直線を描く方法は、DDA(Digital Differential Algorithm)やブレゼンハムのアルゴリズムが有名です(DDAとブレゼンハムのアルゴリズムとの違い)。
上記のDDAを参考にメソッドを作成すると、次のようになります。
// 画面バッファに、始点(x0, y0)~終点(x1, y1)間の直線を描く public void LineDDA(int x0, int y0, int x1, int y1) { var dx = x1 - x0; var dy = y1 - y0; float steps = Math.Max(Math.Abs(dx), Math.Abs(dy)); float xinc = dx / steps; float yinc = dy / steps; float x = x0, y = y0; for (int i = 0; i <= steps; i++) { SetPixel((int)Math.Round(x), (int)Math.Round(y)); x += xinc; y += yinc; } }
この直線のメソッドを利用すると、次のような直線の描画も簡単です。
// 直線のアニメーション for (int i = 0; i < OledSsd1306.Width; i += 8) { oled.LineDDA(i, 0, OledSsd1306.Width -1 - i, OledSsd1306.Height-1); // 直線の描画 oled.SendBuffer(); // 画面データ送信 Thread.Sleep(300); }