対象読者
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);
}
