2018年1月6日土曜日

[SSD1331]SPI画面描画(画面の塗潰し)

前回では初期化処理までしか記載出来なかったので、引き続き描画部分を調べて行きます。下記はターゲットのコードです。

// 色の定義
#define BLACK 0x0000

void setup(void) {
    display.begin();
    display.fillScreen(BLACK);
}

void loop() {
}

今回、調べるのはこの中の、

display.fillScreen(BLACK);

の部分。
以下は、これまでに確認している変数の一覧。

uint8_t _cs10(cs)
uint8_t _rs8 (dc)
uint8_t _sid0
uint8_t _sclk0
uint8_t _rst9(rst)
int16_t _width96(WIDTH)
int16_t _height64(HEIGHT)
uint8_t rotation0
int16_t cursor_y0
int16_t cursor_x0
uint8_t textsize1
uint16_t textcolor0xFFFF
uint16_t textbgcolor0xFFFF
boolean wraptrue
boolean _cp437false
gfxFontNULL

"fillScreen()"は、"Adafruit_GFX.cpp"の中に見つかったので確認して行きます。

void Adafruit_GFX::fillScreen(uint16_t color) {
    // 必要に応じてサブクラスで更新してください!
    fillRect(0, 0, _width, _height, color);
}

uint16_t color2 バイトの符号なし整数 color0x0000(BLACK)

"fillRect()"も同じファイルの中に記載されているので、調べましょう。

void Adafruit_GFX::fillRect(int16_t x, int16_t y, int16_t w, int16_t h,
        uint16_t color) {
    // 必要に応じてサブクラスで更新してください!
    startWrite();
    for (int16_t i=x; i<x+w; i++) {
        writeFastVLine(i, y, h, color);
    }
    endWrite();
}

int16_t x2 バイトの符号付き整数 x0
int16_t y2 バイトの符号付き整数 y0
int16_t w2 バイトの符号付き整数 w96(_width)
int16_t h2 バイトの符号付き整数 h64(_height)
uint16_t y2 バイトの符号なし整数 color0x0000(BLACK)

順番に追いかけます。
"startWrite()"も同じファイル内に見つかりました。

void Adafruit_GFX::startWrite(){
    // 必要に応じてサブクラスで上書きしてください!
}

ん。。何もないですね。
ちなみに、"endWrite()"も先に見ておきます。

void Adafruit_GFX::endWrite(){
    // startWrite が定義されている場合は、サブクラスで上書きしてください!
}

似たようなものですね。これから先に出てくる場合はスルーで良さそうです。

次にfor文のループ箇所。
0〜96までのループで、"writeFastVLine()"を繰り返し実行するようです。
ここから先では実数があった方が良いのでループの数値iを48と固定しておきます。

// (x,y) は最上点; 不明な場合は、呼び出し側の関数はエンドポイントをソートするか、
// 代わりに writeLine() を呼び出す必要があります
void Adafruit_GFX::writeFastVLine(int16_t x, int16_t y,
        int16_t h, uint16_t color) {
    // startWriteが定義されている場合は、サブクラスで上書きしてください!
    // writeLine(x, y, x, y+h-1, color); もしくは writeFillRect(x, y, 1, h, color); でも構いません。
    drawFastVLine(x, y, h, color);
}

int16_t x 2 バイトの符号付き整数 x 48(i)
int16_t y 2 バイトの符号付き整数 y 0
int16_t h 2 バイトの符号付き整数 h 64(_height)
uint16_t color 2 バイトの符号なし整数 color 0x0000(BLACK)

writeからdrawに変わっただけの"drawFastVLine()"を調べます。

// (x,y) は最上点である。 不明な場合は、呼び出し側の関数がエンドポイントをソートするか、
// 代わりに drawLine() を呼び出す必要があります
void Adafruit_GFX::drawFastVLine(int16_t x, int16_t y,
        int16_t h, uint16_t color) {
    // 必要に応じてサブクラスで更新してください!
    startWrite();
    writeLine(x, y, x, y+h-1, color);
    endWrite();
}

int16_t x 2 バイトの符号付き整数 x 48(i)
int16_t y 2 バイトの符号付き整数 y 0
int16_t h 2 バイトの符号付き整数 h 64(_height)
uint16_t color 2 バイトの符号なし整数 color 0x0000(BLACK)

"startWrite()"、"endWrite()"の二つはスルーして、
"writeLine()"を確認。

// Bresenham's algorithm - thx wikpedia
void Adafruit_GFX::writeLine(int16_t x0, int16_t y0, int16_t x1, int16_t y1,
        uint16_t color) {
    int16_t steep = abs(y1 - y0) > abs(x1 - x0);
    if (steep) {
        _swap_int16_t(x0, y0);
        _swap_int16_t(x1, y1);
    }

    if (x0 > x1) {
        _swap_int16_t(x0, x1);
        _swap_int16_t(y0, y1);
    }

    int16_t dx, dy;
    dx = x1 - x0;
    dy = abs(y1 - y0);

    int16_t err = dx / 2;
    int16_t ystep;

    if (y0 < y1) {
        ystep = 1;
    } else {
        ystep = -1;
    }

    for (; x0<=x1; x0++) {
        if (steep) {
            writePixel(y0, x0, color);
        } else {
            writePixel(x0, y0, color);
        }
        err -= dy;
        if (err < 0) {
            y0 += ystep;
            err += dx;
        }
    }
}

int16_t x0 2 バイトの符号付き整数 x0 48(i)
int16_t y0 2 バイトの符号付き整数 y0 0
int16_t x1 2 バイトの符号付き整数 x1 48(i)
int16_t y1 2 バイトの符号付き整数 y1 0+64(_height)-1
uint16_t color 2 バイトの符号なし整数 color 0x0000(BLACK)
int16_t steep 2 バイトの符号付き整数 steep abs(y1 - y0) = 63 > abs(x1 - x0) = 0
int16_t dx 2 バイトの符号付き整数 dx x1 - x0
int16_t dy 2 バイトの符号付き整数 dy abs(y1 - y0)
int16_t err 2 バイトの符号付き整数 err dx / 2
int16_t ystep 2 バイトの符号付き整数 ystep

これは基本的な関数ですが、念のため掲載しておきます。

abs()

[Math]

説明
数値の絶対値を計算する

構文
abs(x)

パラメータ
x: 数値

戻り値
x: xが0以上の場合
-x:xが0より小さい場合

Notes and Warnings
abs()関数が実装されているため、角カッコ内の他の関数の使用は避けてください。結果が正しくない可能性があります。
abs(a++); // これを避ける - 不正な結果となります

abs(a); // これを代わりに使用する -
a++; // 他の[Math]関数は外に置くようにします

次のマクロは、引数の二値を入れ替えるためだけのものです。

#define _swap_int16_t(a, b) { int16_t t = a; a = b; b = t; }

ここで、コードを追って見ます。

"steep = abs(y1 - y0) > abs(x1 - x0)"
代入しているだけなのですが、気になるのは比較演算子">"かと思います。
比較演算子の条件が成立するとtrue(1)が返り、不成立だとfalse(blank)が返る単純なものです。
abs(63 - 0) > abs(48 - 48) = 63 > 0なので、"1"です。

以上により、
"if (steep) { ~ }"
の条件分岐が成立するので、
"_swap_int16_t(x0, y0);"
"_swap_int16_t(x1, y1);"
をマクロに当嵌め、
x0 = 0、y0 = 48、
x1 = 63、y1 = 48となります。

"if ( x0 > x1 ) { ~ }"の条件分岐は不成立なのでスルー。

dx = x1 - x0 = 63 - 0 = 63
dy = abs( y1 - y0 ) = abs( 48 - 48 ) = 0
err = dx / 2 = 63 / 2 = 31.5

"if ( y0 < y1 ) { ystep = 1; } else { ystep = -1; }"
( y0 < y1 ) = ( 48 < 48 )条件分岐は不成立なので、ystep = -1。

"for (; x0<=x1; x0++) { ~ }"
初期値はX0=0なので省略されていますが、X0が0〜63までのループ処理。
ループ内部では、
"if (steep) { writePixel(y0, x0, color); } else { writePixel(x0, y0, color); }"
条件分岐が成立するので、
"writePixel(y0, x0, color);"
が実行される。

"err -= dy"は、err = err - dy = 31.5 - 0 なので変化なし。

続く"if ( err < 0 ) { ~ }"も不成立になるのでスルー

以上で動作確認は終了。と言うことで、"writePixel()"を調査。

void Adafruit_GFX::writePixel(int16_t x, int16_t y, uint16_t color){
    // startWriteが定義されている場合は、サブクラスで上書きしてください!
    drawPixel(x, y, color);
}

int16_t x2 バイトの符号付き整数 x48(i)
int16_t y2 バイトの符号付き整数 y0
uint16_t color2 バイトの符号なし整数 color0x0000(BLACK)

なんだか元どおりになってる気もしますが理由があるのでしょう。
この値をさらに"drawPixel()"で実行するわけですが、こちらは"Adafruit_SSD1331.cpp"の中で見つけられます。

void Adafruit_SSD1331::drawPixel(int16_t x, int16_t y, uint16_t color)
{
  if ((x < 0) || (x >= width()) || (y < 0) || (y >= height())) return;

  // 回転を確認し、必要に応じてピクセルを周りに移動する
  switch (getRotation()) {
  case 1:
    gfx_swap(x, y);
    x = WIDTH - x - 1;
    break;
  case 2:
    x = WIDTH - x - 1;
    y = HEIGHT - y - 1;
    break;
  case 3:
    gfx_swap(x, y);
    y = HEIGHT - y - 1;
    break;
  }

  goTo(x, y);

  // データの設定
  *rsportreg |= rspin;
  *csportreg &= ~ cspin;

  spiwrite(color >> 8);
  spiwrite(color);

  *csportreg |= cspin;
}

int16_t x2 バイトの符号付き整数 x48(i)
int16_t y2 バイトの符号付き整数 y0
uint16_t color2 バイトの符号なし整数 color0x0000(BLACK)

コードを追いながら細かい箇所を調べて行きます。

まずは、
" if ((x < 0) || (x >= width()) || (y < 0) || (y >= height())) return;"

上のコードに含まれる関数を探していると、
"Adafruit_GFX.cpp"に"width()"、"height()"がありました。

// ディスプレイのサイズを返します(現在の回転ごとに)
int16_t Adafruit_GFX::width(void) const {
    return _width;
}

int16_t Adafruit_GFX::height(void) const {
    return _height;
}

初期化処理で取得した"_width"、"_height"の値を返しているだけですね。

との情報から、
  • ( x < 0 ) = ( 48 < 0 ) : false
  • ( x >= width() ) = ( 48 >= 96 ) : false
  • ( y < 0 ) = ( 0 < 0 ) : false
  • ( y >= height() ) = ( 0 >= 64 ) : false
となって、いずれにも該当しないのでスルー。

次は、"switch (getRotation()) { ~ }"の"getRotation()"です。

uint8_t Adafruit_GFX::getRotation(void) const {
    return rotation;
}

これらも初期化処理で取得した"rotation"の値を返しているだけです。

switch...case

[Control Structure]

説明
if文と同様に、switch caseはプログラマが様々な条件で実行すべき異なるコードを指定できるようにすることで、プログラムの流れを制御します。特に、switchステートメントは、変数の値をcaseステートメントで指定された値と比較します。変数の値と値が一致するcaseステートメントが見つかると、そのcaseステートメントのコードが実行されます。

breakキーワードはswitchステートメントを終了し、通常は各ケースの終わりに使用されます。breakステートメントがなければ、switchステートメントはブレークまたはswitchステートメントの終わりに達するまで、次の式の実行を続行します( "fall-through")。


構文
switch (var) {
  case label1:
    // ステートメント
    break;
  case label2:
    // ステートメント
    break;
  default:
    // ステートメント
}

パラメータ
var: さまざまなケースと比較する値 
使用できるデータ型: int, char 
label1, label2: 定数 
使用できるデータ型: int, char 


戻り値
Nothing

次に"Adafruit_SSD1331.h"で"gfx_swap()"のマクロを発見。

#define gfx_swap(a, b) { uint16_t t = a; a = b; b = t; }

内容としては、先の"_swap_int16_t()"とほぼ同じです。
唯一異なるのは、こちらは"符号なし整数"の取り扱いになってる点でしょうか。

情報も集まったところで、コードの調査に戻ります。
"rotation"の値は、0なので"getRotation()"も0になり、"case"の条件に該当するものがありません。よって、"switch (getRotation()) { ~ }"はスルーとなります。

続く、"goTo(x, y);"での引数は前処理がスルーだったので、変化なく引き継がれます。とりあえず、"goTo()"で何が行われるのか見て見ましょう。

"goTo()"は、"Adafruit_SSD1331.cpp"に記載されていました。

void Adafruit_SSD1331::goTo(int x, int y) {
  if ((x >= WIDTH) || (y >= HEIGHT)) return;

  // xとy座標を設定する
  writeCommand(SSD1331_CMD_SETCOLUMN);
  writeCommand(x);
  writeCommand(WIDTH-1);

  writeCommand(SSD1331_CMD_SETROW);
  writeCommand(y);
  writeCommand(HEIGHT-1);
}

int x4 バイトの整数 x48(i)
int y4 バイトの整数 y0
int16_t WIDTH2 バイトの符号付き整数 WIDTH96 ?
int16_t HEIGHT2 バイトの符号付き整数 HEIGHT64 ?

WIDTHとHEIGHTは値を設定している箇所が見当たらないので、詳細がわかりませんが、恐らくWIDTH:96/HEIGHT:64なのだと思います。

内容を見て行きます。まずは、
"if ( ( x >= WIDTH ) || ( y >= HEIGHT ) ) return;"
ですが、
"if ( ( 48 >= 96 ) || ( 0 >= 64 ) ) return;"
と解釈できるので、条件不成立となりスルー。

xとy座標を設定する処理で呼び出されている"writeCommand"の詳細は既に前回出ているので省略して、定数について調べます。

#define SSD1331_CMD_SETCOLUMN 0x15
x 48
WIDTH - 1 95
D/C# Hex D7 D6 D5 D4 D3 D2 D1 D0
0
0
0


15
A[6:0]
B[6:0]
x(48)
WIDTH-1(95)
0
0
0
0
0
0
A6
B6
0
1
0
A5
B5
1
0
1
A4
B4
1
1
0
A3
B3
0
1
1
A2
B2
0
1
0
A1
B1
0
1
1
A0
B0
0
1

列の開始アドレスと終了アドレスの設定
A[6:0] 00d-95d から開始アドレス
B[6:0] 00d-95d の終了アドレス

9.1.1 列アドレスの設定 (15h)
このコマンドは、表示データRAMの列開始アドレスと終了アドレスを指定します。このコマンドは、列アドレスポインタを列開始アドレスに設定します。このポインタは、グラフィック表示データRAM内の現在の読み出し/書き込み列アドレスを定義するために使用されます。コマンドA0hによって水平アドレスインクリメントモードがイネーブルされている場合、1カラムデータの読み書きを完了した後、次のカラムアドレスに自動的にインクリメントされます。列アドレスポインタが終了列アドレスへのアクセスを終了すると、開始列アドレスにリセットされる。

#define SSD1331_CMD_SETROW 0x75
y 0
HEIGHT - 1 63
D/C# Hex D7 D6 D5 D4 D3 D2 D1 D0
0
0
0


75
A[6:0]
B[6:0]
y(0)
HEIGHT-1(63)
0
0
0
0
0
1
A6
B6
0
0
1
A5
B5
0
1
1
A4
B4
0
1
0
A3
B3
0
1
1
A2
B2
0
1
0
A1
B1
0
1
1
A0
B0
0
1

行の開始アドレスと終了アドレスの設定
A[6:0] 00d-95d から開始アドレス
B[6:0] 00d-95d の終了アドレス

9.1.2 行アドレスの設定 (75h)
このコマンドは、表示データRAMの行開始アドレスと終了アドレスを指定します。このコマンドは、行アドレスポインタを行開始アドレスに設定します。このポインタは、グラフィック表示データRAM内の現在の読み出し/書き込み行アドレスを定義するために使用されます。コマンドA0hによって垂直アドレスインクリメントモードがイネーブルされている場合、1行データの読み書きを終了した後、次の行アドレスに自動的にインクリメントされます。 ロウアドレスポインタが終了ロウアドレスへのアクセスを終了すると、ロウアドレスポインタは開始ロウアドレスにリセットされる。

下の図は、例による列アドレスと行アドレスのポインタの移動方法を示しています。列開始アドレスが2に設定され、列終了アドレスが93に設定され、行開始アドレスが1に設定され、行終了アドレスが62に設定されます。水平アドレスインクリメントモードは、コマンドA0hによってイネーブルされます。この場合、グラフィック表示データRAM列のアクセス可能範囲は、列2から列93、行1から行62のみである。さらに、列アドレスポインタは2にセットされ、行アドレスポインタは1にセットされる。1ピクセルのデータの読み取り/書き込みを完了した後、次の読み取り/書き込み操作のために次のRAM位置にアクセスするために、列アドレスが自動的に1ずつ増加します(図21の実線)。列アドレスポインタが終了列93へのアクセスを終了すると、列アドレスポインタは列2にリセットされ、行アドレスは自動的に1だけ増加される(図21の赤文字)。終了行62および終了列93のRAM位置がアクセスされている間、行アドレスは1にリセットされて戻される(図21の青文字)。

Col 0 Col 1 Col 2 ..... Col 93 Col 94 Col 95
Row 0
Row 1 Col62,Row93↓


Col2,Row3
Row 3 Col93,Row93→


Col2,Row4↓
: : : : : : : :
Row 61 Col93,Row60→


Col2,Row62↓
Row 62 Col93,Row61→


Col2,Row1↑
Row 63
Figure 21 - 列アドレスポインタと行アドレスポインタの移動の例

"writeCommand(SSD1331_CMD_SETCOLUMN);"
で、列アドレスの指定が可能な状態となります。
続いて、"writeCommand(x);"、"writeCommand(WIDTH-1);"で列の範囲(アドレス)を指定します。ここでは、48〜96が選択されます。
その後の"writeCommand(SSD1331_CMD_SETROW);"では、
行アドレスの指定が可能な状態として、
"writeCommand(y);"、"writeCommand(HEIGHT-1);"で行の範囲(アドレス)を指定します。0〜63が選択範囲。

"goTo()"での処理は以上。

"*rsportreg |= rspin;"
"*csportreg &= ~ cspin;"
この辺りは、前回にも書きましたが再度確認。

Table 8 - シリアルインタフェースの制御端子
Function E R/W# CS# D/C#
Write command Tie low Tie low L L
Write data Tie low Tie low L H

7.2 コマンドデコーダ
このモジュールは、入力をD / C#ピンの入力に基づいてデータまたはコマンドとして解釈する必要があるかどうかを決定します。D/C#ピンがHIGHの場合、データはグラフィック表示データRAM(GDDRAM)に書き込まれます。D/C#がLOWの場合、D0〜D15の入力はコマンドとして解釈され、デコードされて対応するコマンドレジスタに書き込まれます。

以上を参考に確認すると、

"*rsportreg"ポインタ(D/C#)をHIGH、"*csportreg"ポインタ(CS#)をLOW

より、"Write data"に設定されていることがわかります。

次に来る"spiwrite(color >> 8);"では、ビットシフト演算子が出てきます。

>>

[Bitwise Operators]

説明
右シフト演算子>>は、左オペランドのビットを右オペランドによって指定された位置の数だけ右にシフトさせます。

構文
variable >> number_of_bits;

パラメータ
変数: 使用できるデータ型: byte, int, long
number_of_bits: <= 32の数値。許可されるデータ型: int

コード例
int a = 40; // binary: 0000000000101000
int b = a >> 3; // binary: 0000000000000101, or 5 in decimal

ノートと警告
xをyビット(x >> y)だけ右にシフトし、xの最上位ビットが1の場合、動作はxの正確なデータ型に依存します。xがint型である場合、上で議論したように、最高ビットは符号ビットであり、xが負であるか否かを決定する。その場合、秘密の歴史上の理由から、符号ビットは下位ビットにコピーされます:

int x = -16; // binary: 1111111111110000
int y = 3;
int result = x >> y; // binary: 1111111111111110

符号拡張と呼ばれるこの動作は、しばしば必要な動作ではありません。代わりに、ゼロを左からシフトすることをお勧めします。右シフトルールは符号なしのint式では異なりますので、タイプキャストを使用して左からコピーされているものを抑制することができます:

int x = -16; // binary: 1111111111110000
int y = 3;
int result = (unsigned int)x >> y; // binary: 0001111111111110

符号拡張を行わないように注意する場合は、2の累乗で除算する方法として右シフト演算子>>を使用できます。たとえば、次のようにします:
int x = 1000;
int y = x >> 3; // integer division of 1000 by 8, causing y = 125.

行なっているのは、8ビット右にシフトさせているだけです。
color = 0x0000なので、8ビットシフトさせても0です。
正確には、0x00になりますが、あまり意味があるとも思えません。

実験的にcolor = 0xF81Fで試してみましょう。
"1111100000011111"を8ビット右にシフトさせるので、"11111000"となり、
0xF8になります。まぁ、2byteデータを1byteにしてるだけですかね。

その1byteの値を最初に送信し、その後に"spiwrite(color);"で2byte全体を送信しています。

で、最後に"*csportreg |= cspin;"で"*csportreg"ポインタをHIGHに戻して"Write data"を解除しています。

以上で画面描画の調査は終了です。

SSD1331を触る上での基本情報は集まったので、次回からライブラリを使わずにベタなコードでここまでの流れを再現して見たいと思います。


0 件のコメント:

コメントを投稿