shonen.hateblo.jp

やったこと,しらべたことを書く.

Windowsアプリケーションから Arduino の OLED ディスプレイを操作する(HSP - 画像送信編)

todo: draft

目標

  • シリアル通信を介してUSBで接続された ArduinoMicro+OLED に画像を表示する.

f:id:m_buyoh:20180130012132j:plain:w400

環境

前提

  • "Adafruit SSD1306" と "Adafruit GFX Library" を使用したサンプルが動作すること.

次の記事を参考にしてください.

qiita.com

特にI2Cアドレスの罠は注意.

PC上で読み込んだ画像をArduinoに転送・表示

  • Adafruitのサンプルによれば,display.drawPixelを使って,特定のピクセルに対して色を設定することが出来る.
  • つまり,1ピクセルずつ丁寧に色を決めていけば,画像は書ける.
  • 画像の転送はどうすればよいか?
  • 転送する画像の横幅は128pxと決め打ちしておくとする.
  • 左上から順に「白黒白白・・・」というデータをシリアル通信を介して送ればよい.
  • ここでは,末尾コードを0,黒を表すコードを1,白を表すコードを2とした.

  • というわけで,とりあえず動くコードを示します.

Arduino側のコード

#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

#define OLED_RESET 4
Adafruit_SSD1306 display(OLED_RESET);

#if (SSD1306_LCDHEIGHT != 64)
#error("Height incorrect, please fix Adafruit_SSD1306.h!");
#endif


void setup() {
  Serial.begin(9600);

  // by default, we'll generate the high voltage from the 3.3v line internally! (neat!)
  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);  // initialize with the I2C addr 0x3D (for the 128x64)
  // init done
  
  // Show image buffer on the display hardware.
  // Since the buffer is intialized with an Adafruit splashscreen
  // internally, this will display the splashscreen.
  display.display();
  delay(2000);
  
  display.clearDisplay();
  display.display();
}

int buffcount = 0;

void loop() {

  while (Serial.available() > 0){
    int c = Serial.read();
    if (c == 0){
      display.display();
      buffcount = 0;
    }
    else{
      int y = buffcount/128;
      int x = buffcount%128;
      display.drawPixel(x, y, c == 1 ? BLACK : WHITE);
      buffcount++;
    }
  }
}

HSP側のコード.2値画像変換パートはこの記事のメインでは無いので適当に流してください.

#include "hspext.as"

#const COMPORT 3

    // 256色モード
    screen 0, 320, 240, 1
    
    // ダイアログ表示
    dialog "jpg;*.jpeg;*.png;*.bmp;*.gif",16
    if stat == 0 : end
    
    // 白と黒の2色のみから成るパレットに書き換え
    repeat 256 : palette cnt,0,0,0,0 : loop
    palette 0,255,255,255,1
    
    // 画像をスクリーン1に読み込み
    buffer 1 : picload refstr
    wx = ginfo_winx
    wy = ginfo_winy
    
    // スクリーン1から0へ縮小コピー
    gsel 0
    pos 0,0 : gzoom 128,128*wy/wx,1,0,0,wx,wy
    
    // 画像処理ここまで
    // ================================
    // シリアルで画像を送信する
    
    onexit *exit
    // シリアル通信に接続
    comopen COMPORT, "baud=9600 parity=N data=8 stop=1"
    if stat : dialog "error: comopen" : end
    
    repeat 64
        y = cnt
        repeat 128
            x = cnt
            
            pget x,y
            if ginfo_r == 0 : c = 1 : else : c = 2  
            // c=1:黒 , c=2:白

            // シリアル通信にピクセルデータを送信            
            computc c 
        loop
    loop
    
    // 末尾データ
    computc 0
    
    dialog "complete"
    
*exit
    comclose
    end

動いた

  • 試してみていかがでしたか?
  • 上で示したコードは無駄な事をたくさんしている.
  • 無駄な要素をすべて列挙し始めたらキリが無いので,『通信』に焦点を当てて改善しよう.

通信の高速化をしよう

computc 10000回は遅い

  • HSP側のプログラムが画像を転送し始めてから完了するまで6.95秒掛かった.
  • computc を大量に呼び出すのはあまり効率的ではない.
  • バッファに送信するデータを蓄えてから,まとめて送信するべき.
  • というわけで,この改善を行ったコードを次に示す.
    • 以下のコードでは,送信するデータを文字列型変数buffに蓄えている.
  • 実行時間は0.5秒まで短縮した.正直がっかりした.
#include "hspext.as"
#const COMPORT 3

    screen 0, 320, 240, 1
    
    dialog "jpg;*.jpeg;*.png;*.bmp;*.gif",16
    if stat == 0 : end
    
    repeat 256 : palette cnt,0,0,0,0 : loop
    palette 0,255,255,255,1
    
    buffer 1 : picload refstr
    wx = ginfo_winx
    wy = ginfo_winy
    
    gsel 0
    pos 0,0 : gzoom 128,128*wy/wx,1,0,0,wx,wy
    
    // ================================
    // シリアルで画像を送信する
    
    onexit *exit
    comopen COMPORT, "baud=9600 parity=N data=8 stop=1"
    if stat : dialog "error: comopen" : end
 
    sdim buff,10000
    
    p = 0
    repeat 64
        y = cnt
        repeat 128
            x = cnt
            
            pget x,y
            if ginfo_r == 0 : c = 1 : else : c = 2
    
            // c=1:黒 , c=2:白
            
            poke buff,p,c
            // computc c
            p += 1
        loop
    loop
    
    comput buff
    computc 0
    
    dialog "complete"
    
*exit
    comclose
    end

データ圧縮アイデア

  • ここから先はアルゴリズムな話になります.よろしくお願いします.
  • データの転送に使用する文字は3種類.
  • しかし,256種類(8bit)使えるはずである.
  • もっと効率化できるはず.

データ圧縮その1

  • ビット単位で載せる方法.
  • ピクセルは2値しか取らない(1bit).
  • 8bitなら,8ピクセル分のデータを送信することが出来るはずである.
int posy = 0, posx = 0;

void loop() {

  while (Serial.available() > 0){
    int c = Serial.read();
    for (int i = 0; i < 8; ++i){
       display.drawPixel(posx+i, posy, c & (1 << (7-i)) ? WHITE : BLACK);
    }
    posx += 8;
    if (posx == 128){
      posx = 0; ++posy;
    }
    if (posy == 64){
      posy = 0;
      display.display();
    }
  }
}
  • HSP側のコードを示す前に,HSP特有の融通の利かなさについて.
  • C言語等多くの言語では文字列の末尾に\0を付け加えて終端を表すことが多い.
  • HSPcomput命令でも同様で,\0をデータの終端として認識する.
  • よって,バッファの途中に\0を含む場合,その\0がデータの終端として認識されてしまう.
  • computcを使えば解決するが,先程の通り,なるべく使いたくはない.
  • そこで,バッファの途中に\0を含む場合,0にならないようランダムにビットを立てている.
    sdim buff,10000
    
    p = 0
    i = 0
    bit = 0
    repeat 64
        y = cnt
        repeat 128
            x = cnt
            
            pget x,y
            if ginfo_r == 0 : c = 0 : else : c = 1
    
            bit = (bit << 1) | c
            i += 1
            
            if i == 8 {
                poke buff,p,bit
                p += 1
                bit = 0
                i = 0
            }
        loop
    loop
    
    // 文字コード0を含む文字列はcomputでは送信できない…
    // 無理やり0以外の数値に書き換えている
    repeat p
        if peek(buff,cnt)==0 : poke buff,cnt,1<<rnd(8)
    loop
    
    comput buff
    
    dialog "complete"
  • この改善で,効率は8倍になるはず.

データ圧縮その2

  • 色の連続数をデータとする手法.一般に連長圧縮と呼ばれる.
  • 「0が20個」「1が12個」「0が40個」…というデータを送る
  • 問題は,同じ色が256個以上連続するケース.
  • この場合,「0が255個」「1が0個」「0がX個」とすれば良い.
  • 前述の通り,hspextの都合から0は送信出来ないので,0の代わりに255を使用する.
  • すなわち,同じ色が260個の場合は254,255,6となる.
int pos = 0;
int col = 0;

void loop() {

  while (Serial.available() > 0){
    int c = Serial.read();
    if (c == 0){
      display.display();
      pos = 0;
      continue;
    }else if (c != 255){
      for (; c; --c, ++pos)
        display.drawPixel(pos&127, (pos>>7)&63, col ? WHITE : BLACK);
    }
    col ^= 1;
  }

}
    sdim buff,10000
    
    p = 0
    count = 0
    last = -1
    
    repeat 64
        y = cnt
        repeat 128
            x = cnt
            
            pget x,y
            if ginfo_r == 0 : col = 0 : else : col = 1
    
            if last == -1 {
                last = col
                if col == 1 {
                    poke buff,p,255 : p += 1
                }
            }
            if col != last {
                poke buff,p,count : p += 1
                count = 0
                last = col
            }
            if count == 254{
                poke buff,p,count : p += 1
                poke buff,p,255   : p += 1
                count = 0
            }
            count += 1
        loop
    loop
    
    if 0 < count{
        poke buff,p,count : p += 1
        last = last ^ 1
    }
    
    if last == 1 {
        poke buff,p,255 : p += 1
    }
    
    comput buff
    computc 0
    
    dialog "complete"
  • 効率は 約2倍~254倍.

さいご

  • 「無駄な事をたくさんしている」と書いたが,本気で頑張るなら改善点は多い.
  • hsppgetは遅い
    • mrefを使う
  • hspcomputで0が使えないのは痛手
    • WindowsAPIを直接扱うようにする
  • Adafruit_GFXが重い
    • 実行速度はそこまで大したことは無い.コード内にフォントを持つため,生成されるコードがとても大きくなる.
    • ライブラリを書き換え,不要な命令を削除する.あるいは,I2C通信を頑張って書く.