🔦

M5 Atom Matrix の M5.dis.displaybuff()関数がバグる問題を無理やり解決した話

2022/09/08に公開

はじめに

先日 M5 Atom Lite を購入し、スマートリモコン化して遊んでました。
これがなかなか楽しく、M5 Atom Matrix も追加購入しました。

Matrix は名前の通り、5 × 5 のマトリックス LED を搭載しています。
そのため、合計 25 個の LED に色を指定する必要があり、制御に手間がかかります。

LED の制御

LED を光らせる関数が 2 つあるみたいです。(参考)

  • M5.dis.drawpix(uint8_t Number, CRGB Color)
    引数は LED の番号と色です。
    Atom Lite の場合は LED が一つしか無いため、こちらのほうが使いやすいようです。
    Atom Matrix の場合は LED が 25 個あるため、この関数を 25 回呼ぶことになります。
  • M5.dis.displaybuff(uint8_t *buffptr, int8_t offsetx = 0, int8_t offsety = 0)
    引数は一次元の配列、X 軸方向の移動量、Y 軸方向の移動量です。
    配列は ATOM Pixel TOOL という公式ソフトで作成できます。
    リンク先 の下の方に小さく DL リンクがあります。
    作成した配列を引数として渡すだけでいいため、基本的にこっちを使いやすいようです。

コードの比較

さて、Atom Matrix で LED を光らせるコードを比較してみます。


drawpix

main.cpp
#include <M5Atom.h>

const uint8_t matrix[5][5]=
    {{0,0,0,0,0},
     {0,1,0,1,0},
     {0,0,0,0,0},
     {1,0,0,0,1},
     {0,1,1,1,0}};

CRGB dispColor(uint8_t r, uint8_t g, uint8_t b){
    return (CRGB)((r << 16) | (g << 8) | b);
}

CRGB col[2]=
    {dispColor(0,0,0),
     dispColor(128,0,255)};

void setup() {

    Serial.begin(115200);
    M5.begin(true, false, true);

    for (int i = 0; i < 5; i++){
        for (int j = 0; j < 5; j++){
            M5.dis.drawpix(i*5+j, col[matrix[i][j]]);
        }
    }

}
void loop() {
    delay(500);
    M5.update();
}

displaybuff

main.cpp
#include <M5Atom.h>

const unsigned char matrix[77]=
{
/* width  005 */ 0x05,
/* height 005 */ 0x05,
/* Line   000 */ 0x00,0x00,0x00, 0x00,0x00,0x00, 0x00,0x00,0x00, 0x00,0x00,0x00, 0x00,0x00,0x00, // 
/* Line   001 */ 0x00,0x00,0x00, 0x80,0x00,0xff, 0x00,0x00,0x00, 0x80,0x00,0xff, 0x00,0x00,0x00, // 
/* Line   002 */ 0x00,0x00,0x00, 0x00,0x00,0x00, 0x00,0x00,0x00, 0x00,0x00,0x00, 0x00,0x00,0x00, // 
/* Line   003 */ 0x80,0x00,0xff, 0x00,0x00,0x00, 0x00,0x00,0x00, 0x00,0x00,0x00, 0x80,0x00,0xff, // 
/* Line   004 */ 0x00,0x00,0x00, 0x80,0x00,0xff, 0x80,0x00,0xff, 0x80,0x00,0xff, 0x00,0x00,0x00, // 
};

void setup() {

    Serial.begin(115200);
    M5.begin(true, false, true);

    M5.dis.displaybuff((uint8_t*)matrix);

}
void loop() {
    delay(500);
    M5.update();
}

どうでしょう。明らかに displaybuff のほうが多くのメリットがあります。

  • コーディングが楽
  • ドット絵作成が楽
  • アニメーション作成が楽

今回は静止画ですが、アニメーションとなると drawpix は地獄に思えます。
フレーム数のマトリックスを作って使いたいカラーを指定して、、、
どう考えても displaybuff を使いますね。

問題発生

そういえばどんな絵が表示されるか見てないですね。
それでは見てみましょう。

drawpix


:)

displaybuff


!?!?

あれ??なんで!?
試しにこれでもやってみましょう。

main.cpp
#include <M5Atom.h>

const unsigned char matrix[77]=
{
/* width  005 */ 0x05,
/* height 005 */ 0x05,
/* Line   000 */ 0xff,0x00,0x00, 0xff,0x00,0x00, 0xff,0x00,0x00, 0xff,0x00,0x00, 0xff,0x00,0x00, // 
/* Line   001 */ 0xff,0x00,0x00, 0xff,0x00,0x00, 0xff,0x00,0x00, 0xff,0x00,0x00, 0xff,0x00,0x00, // 
/* Line   002 */ 0xff,0x00,0x00, 0xff,0x00,0x00, 0xff,0x00,0x00, 0xff,0x00,0x00, 0xff,0x00,0x00, // 
/* Line   003 */ 0xff,0x00,0x00, 0xff,0x00,0x00, 0xff,0x00,0x00, 0xff,0x00,0x00, 0xff,0x00,0x00, // 
/* Line   004 */ 0xff,0x00,0x00, 0xff,0x00,0x00, 0xff,0x00,0x00, 0xff,0x00,0x00, 0xff,0x00,0x00, // 
};

void setup() {

    Serial.begin(115200);
    M5.begin(true, false, true);

    M5.dis.displaybuff((uint8_t*)matrix);

}
void loop() {
    delay(500);
    M5.update();
}

配列を見ても全部赤になりそうな気がするけど、、、

赤が一個もない、、、

色々しらべたけど、、、

ぜんぜん分かりませんでした。
赤と緑が逆になる現象は結構ヒットしましたが

自分の環境だと drawpix だと問題ないからどうやら違うみたい。

そして解決へ

drawpix は使えるなら displaybuff を自作すればいいんじゃない?
と考えて作ってみました。

関数の仕組みを考える

配列の構造

ATOM Pixel TOOL で作成される配列は

const unsigned char matrix[77]=
{
/* width  005 */ 0x05,
/* height 005 */ 0x05,
/* Line   000 */ 0x00,0x00,0x00, 0x00,0x00,0x00, 0x00,0x00,0x00, 0x00,0x00,0x00, 0x00,0x00,0x00, // 
/* Line   001 */ 0x00,0x00,0x00, 0x80,0x00,0xff, 0x00,0x00,0x00, 0x80,0x00,0xff, 0x00,0x00,0x00, // 
/* Line   002 */ 0x00,0x00,0x00, 0x00,0x00,0x00, 0x00,0x00,0x00, 0x00,0x00,0x00, 0x00,0x00,0x00, // 
/* Line   003 */ 0x80,0x00,0xff, 0x00,0x00,0x00, 0x00,0x00,0x00, 0x00,0x00,0x00, 0x80,0x00,0xff, // 
/* Line   004 */ 0x00,0x00,0x00, 0x80,0x00,0xff, 0x80,0x00,0xff, 0x80,0x00,0xff, 0x00,0x00,0x00, // 
};

matrix[0] X 軸のテクセル数
matrix[1] Y 軸のテクセル数
matrix[pos*3 + 2] ~ [pos*3 + 4] LED の番号 pos の R ~ B
となっているようです。

ということは
横軸の配列数は matrix[0] * 3
縦軸の配列数は matrix[1]
どこかのピクセルの色は三次元ベクトル (matrix[pos], matrix[pos+1], matrix[pos+2]) で取得できることになります。

これで各ピクセルの pos がわかれば絵を書けることがわかりました。

pos を考える

pos について考えるうえで matrix[0], matrix[1] がどうしても邪魔です。
とりあえずここでは消えてもらいましょう。

const unsigned char matrix[75]=
{
/* width  005    0x05, */
/* height 005    0x05, */
/* Line   000 */ 0x00,0x00,0x00, 0x00,0x00,0x00, 0x00,0x00,0x00, 0x00,0x00,0x00, 0x00,0x00,0x00, // 
/* Line   001 */ 0x00,0x00,0x00, 0x80,0x00,0xff, 0x00,0x00,0x00, 0x80,0x00,0xff, 0x00,0x00,0x00, // 
/* Line   002 */ 0x00,0x00,0x00, 0x00,0x00,0x00, 0x00,0x00,0x00, 0x00,0x00,0x00, 0x00,0x00,0x00, // 
/* Line   003 */ 0x80,0x00,0xff, 0x00,0x00,0x00, 0x00,0x00,0x00, 0x00,0x00,0x00, 0x80,0x00,0xff, // 
/* Line   004 */ 0x00,0x00,0x00, 0x80,0x00,0xff, 0x80,0x00,0xff, 0x80,0x00,0xff, 0x00,0x00,0x00, // 
};

まだ分かりづらいので、もっと簡略化してみます。

const unsigned char matrix[25]=
{
/* width  005    0x05, */
/* height 005    0x05, */
/* Line   000 */ 00, 01, 02, 03, 04, // 
/* Line   001 */ 05, 06, 07, 08, 09, // 
/* Line   002 */ 10, 11, 12, 13, 14, // 
/* Line   003 */ 15, 16, 17, 18, 19, // 
/* Line   004 */ 20, 21, 22, 23, 24, // 
};

なにやらパターンが見えてきました。
右に 1 進むと数値が 1 増え、
下に 1 進むと数値が 5 増えています。

横軸を j, 縦軸を i, 数値を pos とすると pos = i * 5 + j となりそうです。

マジックナンバーの 5 がありますが、お気付きの通り width = 5 です。
つまり

pos = i * width + j

となります。

試しに width = 6 のパターンでもやってみましょう。

const unsigned char matrix[30]=
{
/* width  006    0x06, */
/* height 005    0x05, */
/* Line   000 */ 00, 01, 02, 03, 04, 05, // 
/* Line   001 */ 06, 07, 08, 09, 10, 11, // 
/* Line   002 */ 12, 13, 14, 15, 16, 17, // 
/* Line   003 */ 18, 19, 20, 21, 22, 23, // 
/* Line   004 */ 24, 25, 26, 27, 28, 29, // 
};

pos = i * width + j
22 = 3 * 6 + 4

どうやら正しいようです。

ここで displaybuff の引数を見てみましょう。

  • M5.dis.displaybuff(uint8_t *buffptr, int8_t offsetx = 0, int8_t offsety = 0)
    引数は一次元の配列、X 軸方向の移動量、Y 軸方向の移動量です。

この優れた関数は配列の他に、X, Y 軸方向の移動量を指定できるようです。
これもさきほど簡素化した式であれば簡単に適用できます。

n = (i + offsetY) * width + (j + offsetX)

i, j にそれぞれ軸方向の移動量を足してやるだけです。
offsetX をだんだん増やしていくだけで横にスクロールしていく式が完成しました。

横軸は 3 要素を 1 つにまとめて考えたので、
実装段階では pos の計算部分に 3 を掛けてやると辻褄が合いますね。

最後に matrix[0], matrix[1] の分を足してやります。

pos = ((i + offsetY) * width + (j + offsetX)) * 3 + 2

これで完成?

これを実装して横スクロールしてみると、ある条件で思わぬバグに遭遇します。
それは延々とシームレスにループさせる場合です。

つまり、右端のテクセルの右側が左端のスタートになるような場合です。

この画像で考えてみましょう。

0 フレーム目は

最終フレームは

こうなりますね。

そしてメガネをかけるとあら不思議、頭を動かすとそれに合わせて赤いピクセルが移動してるように見えます。

問題なのは、右に 1 進むと数値が 1 増えるということです。
この計算では右端のテクセル (19) の右側が左端のスタート (0) にならないんです。
特に、一番下の行では配列の要素数を超えてしまうことになります。
この問題をテクセルオーバーフローと呼ぶことにします。

これはいけません。
19 の右側は 20 ではなく 0 になるようにしなければなりません。
同様に 39 の右側は 20 です。つまり

こうなって欲しいわけです。

簡略化をもとに戻すのはこの問題を解決してからのほうが良さそうです。
ひとまず思考を戻します。

pos = (i + offsetY) * width * 3 + (j + offsetX) * 3 + 2
pos = (i + offsetY) * width + (j + offsetX)

テクセルオーバーフロー を修正する

横軸の位置を決めるのは j + offsetX であることは先程発見しました。
ではテクセルオーバーフローするのはどこでしょうか。
j + offsetXマトリックスの横軸最大値 を超える場合です。

今回のマトリックスの横軸は 0 ~ 19 の 20 テクセルです。
すなわち マトリックスの横軸最大値 とは 19 つまり width - 1 なので

テクセルオーバーフローするのは

j + offsetX > width - 1 または
j + offsetX >= width と表せます。

先程の画像の 19 の右側を比較してみると
200 にしたいことがわかります。

同様に 39 の右側を比較すると
4020 にしたいことがわかります。

どうやら 20 を引くと良いようです。
20 とはつまり width です。

コードはこうなりますね。

if((j+offsetX) >= width){
    pos -= width;
}

縦軸の場合も考え方は同じです。
縦軸の位置を決めるのは (i + offsetY) でした。

マトリックス上では 移動量に width をかける必要があることがわかっているので
コードはこうなります。

if((i+offsetY) >= height){
    pos -= height*width;
}

さて、ようやく簡略化を戻して問題なさそうです。
pos の計算部分に *3 を追加しましょう。

pos = ((i + offsetY) * width + (j + offsetX)) * 3 + 2
if((j+offsetX) >= width){
    pos -= width*3;
}
if((i+offsetY) >= height){
    pos -= height*width*3;
}

関数を作る

引数は displaybuff と "ほぼ" 同じ

"ほぼ" とつけたのは displaybuff のオフセット指定には "クセ" があるからです。
今回の実装ではオフセットにより参照先を右にずらすようにしています。

一方 displaybuff ではテクスチャ側を右にずらすようになっています。
つまり offsetX を増やすと参照先は左にずれていきます。これは使いづらい。

気を取り直して、いままでのアイディアをもとに関数を作ってみました。

CRGB dispColor(uint8_t r, uint8_t g, uint8_t b){
    return (CRGB)((r << 16) | (g << 8) | b);
}
void M5Disp(uint8_t *matrixImage, uint8_t offsetX, uint8_t offsetY)
{
    uint8_t width  = matrixImage[0];
    uint8_t height = matrixImage[1];
    for (int i = 0; i < 5; i++){
        for (int j = 0; j < 5; j++){
            uint pos = ((i+offsetY)*width+(j+offsetX))*3+2;
            if((j+offsetX) >= width){
                pos -= width*3;
            }
            if((i+offsetY) >= height){
                pos -= height*width*3;
            }
            CRGB col = dispColor(matrixImage[pos],
                                 matrixImage[pos+1],
                                 matrixImage[pos+2]);
            M5.dis.drawpix(i*5+j, col);
        }
    }
}

どうせカスタム関数作るなら

どうせならちょっと便利にしたいと思います。
例えば、パラパラ漫画のように表示する絵を次々と切り替えるような場合、
offsetX または offsetY5 ずつ増やさなければなりません。

そういうひと手間がとても嫌いなので、引数を一つ追加して
スクロール <-> パラパラ漫画 のモードを切り替えられるようにしました。

CRGB dispColor(uint8_t r, uint8_t g, uint8_t b){
    return (CRGB)((r << 16) | (g << 8) | b);
}
void M5Disp(uint8_t *matrixImage, uint8_t offsetX, uint8_t offsetY, bool flipBook)
{
    if(flipBook){
        offsetX *= 5;
        offsetY *= 5;
    }
    uint8_t width  = matrixImage[0];
    uint8_t height = matrixImage[1];
    for (int i = 0; i < 5; i++){
        for (int j = 0; j < 5; j++){
            uint pos = ((i+offsetY)*width+(j+offsetX))*3+2;
            if((j+offsetX) >= width){
                pos -= width*3;
            }
            if((i+offsetY) >= height){
                pos -= height*width*3;
            }
            CRGB col = dispColor(matrixImage[pos],
                                 matrixImage[pos+1],
                                 matrixImage[pos+2]);
            M5.dis.drawpix(i*5+j, col);
        }
    }
}

これは便利。とりあえずこれで完成としましょう。
ではサンプルコード全体をどうぞ。

main.cpp
#include <M5Atom.h>

int frame = 0;
const uint8_t frameSize = 20;

const unsigned char image_atom[302]=
{
/* width  020 */ 0x14,
/* height 005 */ 0x05,
/* Line   000 */ 0x00,0x00,0x00, 0xff,0x00,0x00, 0xff,0x00,0x00, 0xff,0x00,0x00, 0x00,0x00,0x00, 0xff,0xff,0x00, 0xff,0xff,0x00, 0xff,0xff,0x00, 0xff,0xff,0x00, 0xff,0xff,0x00, 0x00,0x00,0x00, 0x00,0xaa,0xff, 0x00,0xaa,0xff, 0x00,0xaa,0xff, 0x00,0x00,0x00, 0x00,0x00,0x00, 0x00,0xff,0x00, 0x00,0x00,0x00, 0x00,0xff,0x00, 0x00,0x00,0x00, // 
/* Line   001 */ 0xff,0x00,0x00, 0x00,0x00,0x00, 0x00,0x00,0x00, 0x00,0x00,0x00, 0xff,0x00,0x00, 0x00,0x00,0x00, 0x00,0x00,0x00, 0xff,0xff,0x00, 0x00,0x00,0x00, 0x00,0x00,0x00, 0x00,0xaa,0xff, 0x00,0x00,0x00, 0x00,0x00,0x00, 0x00,0x00,0x00, 0x00,0xaa,0xff, 0x00,0xff,0x00, 0x00,0x00,0x00, 0x00,0xff,0x00, 0x00,0x00,0x00, 0x00,0xff,0x00, // 
/* Line   002 */ 0xff,0x00,0x00, 0xff,0x00,0x00, 0xff,0x00,0x00, 0xff,0x00,0x00, 0xff,0x00,0x00, 0x00,0x00,0x00, 0x00,0x00,0x00, 0xff,0xff,0x00, 0x00,0x00,0x00, 0x00,0x00,0x00, 0x00,0xaa,0xff, 0x00,0x00,0x00, 0x00,0x00,0x00, 0x00,0x00,0x00, 0x00,0xaa,0xff, 0x00,0xff,0x00, 0x00,0x00,0x00, 0x00,0xff,0x00, 0x00,0x00,0x00, 0x00,0xff,0x00, // 
/* Line   003 */ 0xff,0x00,0x00, 0x00,0x00,0x00, 0x00,0x00,0x00, 0x00,0x00,0x00, 0xff,0x00,0x00, 0x00,0x00,0x00, 0x00,0x00,0x00, 0xff,0xff,0x00, 0x00,0x00,0x00, 0x00,0x00,0x00, 0x00,0xaa,0xff, 0x00,0x00,0x00, 0x00,0x00,0x00, 0x00,0x00,0x00, 0x00,0xaa,0xff, 0x00,0xff,0x00, 0x00,0x00,0x00, 0x00,0x00,0x00, 0x00,0x00,0x00, 0x00,0xff,0x00, // 
/* Line   004 */ 0xff,0x00,0x00, 0x00,0x00,0x00, 0x00,0x00,0x00, 0x00,0x00,0x00, 0xff,0x00,0x00, 0x00,0x00,0x00, 0x00,0x00,0x00, 0xff,0xff,0x00, 0x00,0x00,0x00, 0x00,0x00,0x00, 0x00,0x00,0x00, 0x00,0xaa,0xff, 0x00,0xaa,0xff, 0x00,0xaa,0xff, 0x00,0x00,0x00, 0x00,0xff,0x00, 0x00,0x00,0x00, 0x00,0x00,0x00, 0x00,0x00,0x00, 0x00,0xff,0x00, // 
};

CRGB dispColor(uint8_t r, uint8_t g, uint8_t b){
    return (CRGB)((r << 16) | (g << 8) | b);
}
void M5Disp(uint8_t *matrixImage, uint8_t offsetX, uint8_t offsetY, bool flipBook)
{
    if(flipBook){
        offsetX *= 5;
        offsetY *= 5;
    }
    uint8_t width  = matrixImage[0];
    uint8_t height = matrixImage[1];
    for (int i = 0; i < 5; i++){
        for (int j = 0; j < 5; j++){
            uint pos = ((i+offsetY)*width+(j+offsetX))*3+2;
            if((j+offsetX) >= width){
                pos -= width*3;
            }
            if((i+offsetY) >= height){
                pos -= height*width*3;
            }
            CRGB col = dispColor(matrixImage[pos],
                                 matrixImage[pos+1],
                                 matrixImage[pos+2]);
            M5.dis.drawpix(i*5+j, col);
        }
    }
}

void setup() {

    Serial.begin(115200);
    M5.begin(true, false, true);

    M5Disp((uint8_t*)image_atom, frame, 0, false);

}
void loop() {

    frame++;
    if(frame>=frameSize){
      frame=0;
    }
    
    M5Disp((uint8_t*)image_atom, frame, 0, false);
    
    delay(500);
    M5.update();
}

あとがき

完成してから気づいたんですが、、、

ぼくはいつも Bing で検索してるんですよね。
なんとなくグーグルで検索してみたら同じ症状の記事を発見しました。

M5Atomのライブラリの displaybuff() が変?

どうやら最新の M5Atom ライブラリだと M5.dis.setWidthHeight(5, 5); を追記すると動くけど、R と G が逆になるみたい。

修正されたら displaybuff() を使っても良いかもですね。

GitHubで編集を提案

Discussion