公開日:2021/02/06
title.png

14th Season


#14-7 DUEでTFTシールドのSDカードI/Fを使う

 Arduino DUEでSDカードI/Fを使おうとすると、通常はSAM3X8Eに搭載されたSPIユニットを使用するので、DUEのSPI端子から配線を引き出して使います。

  「#12-6 DUEにSDカードI/Fを接続する

 問題はSPI端子の引き出し位置なのですが、UNO等の機体でICSP端子が配置されたピンヘッダ位置に引き出されているのです(下図)。

a126_spipin.jpg

 これが、何かと使い難い。

 実は、TFTシールド+Arduino MEGAで使う場合には、SOFTWARE_SPIという#defineマクロを有効にしてコンパイルすることで、SDライブラリを使ってTFTシールド上のSDカードI/Fにアクセスすることが出来る(※)みたいなのです。

※手元にMEGAが無いので未確認。SDライブラリのソースコードを解析してみた印象です。

 MEGAで可能なのであれば、DUEだってSOFTWARE_SPIを有効化できるのでは?今回のテーマは、DUE+TFTシールド+SDライブラリで、SOFTWARE_SPIを有効化して、TFTシールド上のSDカードI/Fにアクセスしてみよう、です。

 ただし、今回はCygwinを使ってライブラリのファイルにパッチを当てたり、Arduino IDEの設定ファイルを変更したり、少しハードルが高いので、覚悟してください。本ページでは、記載以上の詳細説明は致しませんので、自力で以下の処理を実行しきる実力がある方のみを対象とします。

SDライブラリの確認

 今回は、ArduinoのSDライブラリにパッチを当てることになります。筆者が使っているSDライブラリはバージョン1.2.3です。異なるバージョンをインストールしてある場合は、以下文中のファイルパス(ディレクトリ)が異なったり、パッチが失敗する場合があります。

 Arduino IDEを起動して、「スケッチメニュー ⇒ ライブラリをインクルード ⇒ ライブラリを管理」を実行します。開いたウインドウで、SDライブラリを探してインストールされているバージョンを確認してください(下図)。

a147_sdlib.png

 バージョンが違う場合、所望のバージョンを選択してインストールしたり、新しいバージョンにアップデートしたりすることが可能です。
 今回確認した時点で、既にバージョン1.2.4がリリースされていました。が、本ページでパッチを当てたのは、バージョン1.2.3ですので、ご注意ください。

 バージョン1.2.3をインストールしてあると、SDライブラリのファイルは以下の場所にあります。

  <User>\Documents\Arduino\libraries\SD\

 <User>はWindowsのユーザディレクトリで、Windowsのエディションや、ログイン名などの設定によって異なります。

パッチを当てる

 SDライブラリ(バージョン1.2.3)にパッチを当てて、DUEでSOFTWARE_SPIを使えるようにします。
 パッチはCygwin上のpatchコマンドで当てますので、こちらを参照の上、WindowsにCygwinとpatchコマンドをインストールしておいてください。

 Cygwinを実行して、適切なシェル(以下説明はtcshを使用した場合です)ウインドウを開き、以下の通り作業を進めてください。

  【 】内はCygwinシェルウインドウからの実行コマンド

 ここで一旦Windowsに戻って、上で作ったディレクトリSD_PATCHをエクスプローラで開いておいてください。そうしたら、以下のファイルをダウンロードして、SD_PATCHディレクトリにコピーします。

  パッチファイル:Sd2Card_due.patch.txt

 更に、Windowsでの作業が続きます。以下のファイルをSD_PATCHディレクトリにコピーします。

  <User>\Documents\Arduino\libraries\SD\src\utility\
    Sd2Card.cpp

 ここまで済んだら、Cygwinに戻ります。

  【 】内はCygwinシェルウインドウからの実行コマンド

 最後のpatchコマンドがエラー(FAILED)無く終了すればOKです。「Sd2Card.cpp」にパッチが当たって、内容が更新されています。Windowsのエクスプローラで「Sd2Card.cpp」を元の場所に上書きコピーしてください。それで終了です。


 もし、以下の様なエラー(FAILED)が発生したら、パッチは失敗しています。SDライブラリのバージョンが異なっていないか、ご自身で「Sd2Card.cpp」を変更して使っていないか、等、確認して原因を取り除いてください。

  【patch < Sd2Card_due.patch.txt】
  patching file Sd2Card.cpp
  Hunk #1 succeeded at 52 with fuzz 1.
  Hunk #2 succeeded at 168 (offset 2 lines).
  Hunk #3 succeeded at 234 (offset 10 lines).
  Hunk #4 FAILED at 322.
  Hunk #5 FAILED at 574.
  Hunk #6 FAILED at 583.
  Hunk #7 succeeded at 631 (offset 27 lines).
  3 out of 7 hunks FAILED -- saving rejects to file Sd2Card.cpp.rej

 patchコマンドが見つからないといった趣旨のエラーが発生した場合は、こちらを参照して、patchコマンドをインストールしてください。

※本HPではCygwinを活用しますが、細かい使い方の説明はしませんので、自力で使える方のみご利用ください。

SOFTWARE_SPIを有効にする

 パッチを当てただけでは、まだ、SOFTWARE_SPIは有効になっていませんので、普通にSDライブラリを使ってしまうと、いつもの様にICSPピンヘッダからSPI端子を引き出さないとアクセスできません。

 コンパイル時に「SOFTWARE_SPI」という#defineマクロを定義しておく必要があります。そのためには、Arduino IDEの設定ファイルを追加/変更する必要があります。

 以下のファイルがあるかどうかを確認してください。

  <User>\AppData\Local\Arduino15\packages\arduino\hardware\sam\1.6.12\
    platform.local.txt

※AppDataディレクトリには不可視属性が付いているので、普通、エクスプローラでは表示されません。エクスプローラの設定で、不可視属性のファイルを表示するオプションを有効にして辿ってください。

 もし、platform.local.txtが無かった場合は、空のテキストファイル📝として作成してください。

 これで全行程終了です。お疲れさまでした。


 以降、DUEを使い、SDライブラリを#includeしてコンパイルすると、常にSOFTWARE_SPIが有効になっています。よってSDライブラリでは、ICSPピンヘッダから配線を引き出しても、SDカードにアクセスできなくなります。
 SOFTWARE_SPIが有効になったことで、以下の通り、UNOと同じピンソケット位置にSOFTWARE_SPI端子が引き出されているためです。

SOFTWARE_SPI端子表
Arduinoピン番号 端子名 備考
D10 SS スレーブセレクト
D11 MOSI マスタアウト・スレーブイン
D12 MISO マスタイン・スレーブアウト
D13 SCK シリアルクロック

 SOFTWARE_SPIが有効な間は、上表のピンソケットからSDカードI/Fに接続して使ってください。TFTシールドを装着すると、上表の端子とTFTシールド上のSDカードI/Fが接続されます。つまり、TFTシールド上のSDカードI/Fを使ってSDカードにアクセスすることが出来ます。

 なお、上で変更した「platform.local.txt」ファイルは、DUEのコンパイル時だけ有効なので、UNOやSTM32等の機体でコンパイルする時は、影響ありません。UNOやSTM32等、DUE以外の機体では、従来通りSPIユニットの出力端子とSDカードI/Fを接続して使えます。
 DUEのSDライブラリでSPIユニットを使ってアクセスしたい場合には、「platform.local.txt」をテキストエディタで開き、「-DSOFTWARE_SPI」を消去してください。

 また、SOFTWARE_SPIは、SDライブラリだけで有効なので、SPIライブラリを直接使う場合には、DUEであってもSPIユニットが使用されます。

 パッチを当てたSDライブラリを削除したり、別バージョンをインストールしたりした場合には、パッチは消えてしまうので、上の処理をやり直しになります。また、バージョンが異なれば、パッチが成功する保証はありません。

SOFTWARE_SPIコード解説

 まず、今回のSOFTWARE_SPIコードがDUEだけで有効になるように、マクロ「__SAM3X8E__」で場合分けしてあります。

  #ifdef SOFTWARE_SPI
   :
   : (共通のコード等)
   :
  #ifdef __SAM3X8E__
   :
   : (DUEで有効なSOFTWARE_SPIコード)
   :
  #else // __SAM3X8E__
   :
   : (元々のSOFTWARE_SPIコード、MEGA用?)
   :
  #endif // __SAM3X8E__
   :
  #endif // SOFTWARE_SPI


 以下の様なマクロを定義してあって、SPI端子の制御や、nop命令1個分(=1/84MHz[s])の時間稼ぎなどを簡略化しています。

  // NOP tune software spi timing
  #define spi_NOP __asm__ __volatile__ ("nop")
  // SCK: D13(PB27/NCS3/TIOB0)
  #define spi_SCK0 REG_PIOB_CODR=(1U<<27)
  #define spi_SCK1 REG_PIOB_SODR=(1U<<27)
  // MOSI: D11(PD7/A17/BA1/TIOA8)
  #define spi_MOSI0 REG_PIOD_CODR=(1U<<7)
  #define spi_MOSI1 REG_PIOD_SODR=(1U<<7)
  // MISO: D12(PD8/A21/NANDALE/TIOB8)
  #define spi_MISO (REG_PIOD_PDSR&(1U<<8))


 端子はこちらで示したピン番号で固定なので、I/Oレジスタを直接叩いて高速化しています。

 SDライブラリの中で、SPI通信は以下2つの関数に集約されているので、この2つをDUE用に作成しました。

  uint8_t spiRec (void)
  {
    int idx;
    int data;

    // no interrupts during byte receive - about 1 us
    noInterrupts ();
    // output pin high - like sending 0XFF
    spi_MOSI1;
    data=0;
    for (idx=0; idx<8; idx++)
    {
      spi_SCK1;
      data <<= 1;
      data |= (spi_MISO)? 1: 0;

      spi_SCK0;
    }
    // enable interrupts
    interrupts ();

    return (data);
  }

  void spiSend (
  uint8_t data)
  {
    int idx;

    // no interrupts during byte send - about 1 us
    noInterrupts ();
    for (idx=0; idx<8; idx++)
    {
      spi_SCK0;
      if (data&0x80)
        spi_MOSI1;
      else
        spi_MOSI0;
      data <<= 1;

      spi_SCK1;
    }
    spi_SCK0;
    // enable interrupts
    interrupts ();
  }


 本来は、クロック(SCK)とデータ(MISO,MOSI)のセットアップやホールド規定を満たす為、間にnop命令を挟み込んでタイミング調整すべきです。
 コンパイル後のアセンブルリストを眺めてみたところ、間に色々な命令が挟まっていて、SCKを10MHz程度でトグルさせることを考えた時には、特にnop命令を挿入する必要なしと判断しました。もし、信号のセットアップやホールドなどのタイミング違反に起因するエラーが発生する場合には、各自、調整してください。

※コンパイラ(GCC)のバージョンが変わって新しい最適化が適用されたりすると、タイミング違反を起こすコードが生成されないとも限りません。本来であれば、インラインアセンブラなどを使って、コードを固定すべきでしょう。その辺りも含めて、以下(↓↓)となります!
※本パッチ(Sd2Card_due.patch.txt)を含めて、本HPで公開するプログラム/回路等は全て無保証です。お使いになる皆様の責任でご利用ください。

実験開始

 DUEにTFTシールドを装着(下図)して、TFTシールド上のSDカードスロットに差したmicroSDカードへアクセスできるかどうか?でテストします。

a121_due_tft.jpg


 従って、使用するのは「#12-6 DUEにSDカードI/Fを接続する」で作成した以下のプログラムで良いでしょう。

  テストプログラム:sd_filer.ino

 上のプログラムで、SOFTWARE_SPIの実力(SDカードのアクセス速度)を評価できるように、【copy】コマンドの実行時間を計測してシリアルモニタに表示する評価用コードを追加しておきました。プログラム先頭付近の「#define SHOW_COPY_TIME」を有効化してから、DUEにコンパイル&書き込んでください。


  実験サンプル:【 】内はシリアルモニタからの送信データ
  【dir】
  OK+ dir /
  SYSTEM~1/
    INDEXE~1  76
  MEMO.TXT  951
  QVGA.BMP  153654
  S8_11K.WAV  2607502
  OK: dir

 ↑microSDカードの中身が表示されましたか?

 ファイルをコピー(【copy】コマンド)すると、コピーに要した時間を表示するようになっています(↓)。

  実験サンプル:【 】内はシリアルモニタからの送信データ
  【copy,MEMO.TXT,TEMP.TMP】
  INFO: copy 0% done.
  INFO: copy 10% done.
  INFO: copy 20% done.
  INFO: copy 30% done.
  INFO: copy 40% done.
  INFO: copy 50% done.
  INFO: copy 60% done.
  INFO: copy 70% done.
  INFO: copy 80% done.
  INFO: copy 90% done.
  OK: copy (7632[ms]/951[byte]) MEMO.TXT TEMP.TMP

 えっ?ビックリするほど遅いです。【copy】コマンド、用意しては見たものの、Arduinoでファイルコピーする機会なんてほぼ無いので、今まで全く気が付きませんでした。たった951バイトのテキストファイルコピーに7.6sもかかってしまいました。
 BMPファイルやWAVファイルなど、サイズの大きなバイナリファイルをコピーしようとすると、ハングしたのかと見まがうほどの時間がかかります。サイズに寄りますが、優に数分間戻ってきません。これは何だ〜?。

ファイルコピー速度の向上

 これまで何度もSDカードI/Fは使っています。BMPファイルの読み出しやWAVファイルの再生など、読み出しに数分かかる様では、全く使い物にならないはずです。つまり、今までの使い方では気にならないアクセス速度を持っていたはず。どうやら、単純にファイルコピー(【copy】コマンド)が遅いだけ、みたいです。

 ファイルコピーは以下の様に実行していました。

  while (fin.available ())
    fout.write (fin.read ());


 入力ファイルfinから1バイト読み出して(fin.read()メソッド)、そのまま出力ファイルfoutへ書き込むだけ(fout.write()メソッド)です。

 SDライブラリの内部処理を解析していないので詳しいことは不明ですが、1バイト毎に2つのファイルを切り替えながらリード/ライトを繰り返す、上のコードが速度低下の原因だろうと推測しました。
 そこで、コピーバッファを確保して、バッファサイズ毎にリード/ライトを分離して実行したところ、ファイルコピー速度が向上しました(↓)。

  const int copy_buf_siz=512;  // copy buffer size

  int chr;
  int idx;
  int dsiz;
  uint8 dbuf[copy_buf_siz];

  // read sd => buffer
  for (dsiz=0; dsiz<copy_buf_siz; dsiz++)
  {
    chr=fin.read ();
    if (chr<0)
      break;
    dbuf[dsiz]=chr;
  }
  // write buffer => sd
  for (idx=0; idx<dsiz; idx++)
    fout.write (dbuf[idx]);


 コピーバッファサイズcopy_buf_sizに関しては、実験を続けて最適値を探します。

実験継続

 ここで、一旦SOFTWARE_SPIを無効にして、従来通りのSPIユニットを使ってアクセスする実験を行いました。SPIユニットを使ったDUEとSDカードI/Fの接続方法についてはこちらをご確認ください。
 まずは、バイト毎コピー版で確認します(↓)。

  実験サンプル:【 】内はシリアルモニタからの送信データ
  【del,TEMP.TMP】
  OK: del TEMP.TMP

  【copy,MEMO.TXT,TEMP.TMP】
  OK: copy (8351[ms]/951[byte]) MEMO.TXT TEMP.TMP

 先ほどのSOFTWARE_SPIの場合と比較してどうでしょうか?コピーに要した時間はほとんど変わりませんでした(約8.4秒、SOFTWARE_SPIより遅いくらい)。やはり、バイト毎のコピーにとてつもない時間がかかってしまう状況は、SPIユニットでも同じだった様です。
 SPIユニットでも、前節のコピーバッファを使用することで、コピー速度が向上しました。


 次は、SOFTWARE_SPIとSPIユニットのアクセス速度の差を知りたいですね。各条件でコピー速度を計測しましたので、以下表に示します。

【copy】コマンド実行時間[ms] (copy_buf_sizを変化しながら計測)
ファイル名 ファイル
サイズ
[バイト]
SOFTWARE_SPI SPIユニット
バイト毎 256 512 1,024 2,048 バイト毎 512
MEMO.TXT 951 7,632 240 249 199 8,351 244
QVGA.BMP 153,654 7,136 3,316 3,286 3,313 3,564
S8_11K.WAV 2,607,502 110,411 56,196 53,994 53,751 57,419

 以下雑感です。

BMPファイルのロード/セーブ

 「#13-1 TFTシールドにBMPを表示する」にて作成した、BMPファイルをSDカードから読み出してTFTシールドに表示するプログラムを拡張しました。以下にリンクしておきます。

  BMPロード/セーブプログラム:load_bmp.ino

 シリアルモニタから実行可能なコマンドについては、こちらをご参照ください。従来のコマンドに加えて以下コマンドを拡張してあります。

 このプログラム(load_bmp.ino)を、SOFTWARE_SPIを有効にした状態でコンパイル&書き込むと、DUE+TFTシールドにおいて、TFTシールド上のSDカードI/Fに挿入されたSDカードへのアクセスが可能となります。


  実験サンプル:【 】内はシリアルモニタからの送信データ
  【load,QVGA.BMP】
  DBG: 320x240(16b) rot90=1
  OK: load_bmp(0): 2194[ms]

 ↑SDカード上のBMPファイルを読み出して、TFTシールドへ表示しました。
 SDカードへのSOFTWARE_SPIアクセスがTFTシールド表示を通して目視で確認できるので、期待通りの画像が表示されているか?よく確かめてください。もし、SOFTWARE_SPIの挙動が誤っていると、一部間違った画像が表示されたり、あるいはまったく画像が表示されなかったりするはずです。

  【save,TEMP.BMP】
  OK+ start save_bmp, wait a moment...
  OK: save_bmp(0): 3288[ms]

 ↑新たに追加した【save】コマンドで、現在TFTシールドに表示されている画像をBMPファイルとして保存しました。

  【clear】
  OK: clear tft

  【load,TEMP.BMP】
  DBG: 320x240(16b) rot90=1
  OK: load_bmp(0): 2193[ms]

 ↑一旦画面をクリア(【clear】コマンド)して、上でセーブしたBMPファイルを読み出してみました。セーブした画像が正しく表示されたかどうか、よく確認してください。
 以上、BMPファイルのロード/セーブを通して、SDカードへのSOFTWARE_SPIによる正常アクセスを確認しました。テストを完了します。


 SOFTWARE_SPIのパッチによって、UNOと同じピンソケットからSPI端子を引き出してSDカードへアクセスできるようになりました。アクセス速度もそん色ないですし、使い難いICSPピンヘッダから解放され、何より、TFTシールド上のSDカードI/Fを使えるようになったのは嬉しい。


btn_arduino.png

btn_prev.png btn_next.png

home