本チュートリアルでは、FlashAirの操作に先立ち、 iSDIOのコマンドを発行するために必要なルーチンを作成していきます。

重要な改訂 (2014-09-29):Sd2CardExt::readExt() 関数に、ダミーデータ送信を追加。

iSDIOコマンドについて

iSDIO規格は、コマンドやりとりの手順を定めたプロトコルであり、 コマンドの発行自体は、SD規格のFunction Extension Commands (class 11)に規定されている、 拡張レジスタの読み書きを行うコマンド CMD48 (READ_EXTR_SINGLE)およびCMD49 (WRITE_EXTR_SINGLE)を利用します(*1)。

注意点: SDIO規格のCMD52、CMD53を利用する方法もありますが、本チュートリアルでは扱いません。

CMD48/49は、次のような3種類のモードがあります。

  • レジスタ: アドレスと長さで指定したメモリ領域に対する読み書き。ステータスなど小さなデータに利用。
  • データポート: 512バイト固定のメモリブロックに対する読み書き。データなど大きなデータに利用。
  • マスク書き込み (CMD49のみ): あるアドレスの1バイトの一部のビットの変更。ステータスリセットなどに利用。

どのモードを使えばよいかは、iSDIO Wireless LAN Simplified Addendumによって、 あるアドレス領域はレジスタモード、あるアドレス領域はデータポートモード、 などと定められており、それに従う必要があります。

ArduinoにCMD48/49を追加する

Arduino IDEには、標準でSDメモリカードのライブラリが含まれています。 このライブラリは、大きく分けて以下のモジュールからなります。

  • SDメモリカードにSPIで通信するクラス(Sd2Card)
  • SDメモリカードのFATファイルシステムにアクセスするクラス(SdFat)
  • 上記をまとめてSDメモリカードのファイルを読み書きするクラス(SD)

本チュートリアルでは、ファイル読み書きは行わないので、 SDメモリカードへのコマンド発行を担当しているSd2Cardクラスのみを利用することにします。

ただし、Sd2CardクラスはCMD48/49には対応していないため、これらの対応を追加していきたいと思います。

ディレクトリ構成

Arduino IDEをインストールすると、ユーザーの「ドキュメント」ディレクトリにArduinoというフォルダが出来ているはずです。 こちらの libraries ディレクトリに、iSDIO というディレクトリを作り、その中にファイルを作っていきます。

作成後のディレクトリ構成は下記のようになります。

<Document>/
+-- Arduino
    +-- libraries
        +-- iSDIO
            +-- iSdio.h *1
            +-- iSdio.cpp *2
            +-- utility
                +-- Sd2Card.h *1
                +-- Sd2Card.cpp *1
                +-- Sd2CardExt.h *2
                +-- Sd2CardExt.cpp *2
                +-- Sd2PinMap.h *1
                +-- SdInfo.h *1

*1のファイルは、Arduino IDE付属のSDメモリカードライブラリProgram Files (x86)\Arduino\libraries\SD\utilityから、 コピーして下さい。なお、Sd2Card.hには、少し変更が必要です。

*2のファイルはこれから、作っていきます。

Sd2CardExtクラスの作成

Sd2Cardクラスを継承してCMD48/49を追加した、Sd2CardExtクラスを作っていきます。 CMD48/49自体はiSDIO規格ではなくSD規格の一部なので、このようなクラス名としました。

CMD48/49の実行は、前述した3つのモードごとに独立した関数としました (10行目~14行目)。 また、非公開関数として、各モードの共通処理を行う関数を作りました (16行目, 17行目)。

Sd2CardExt.h

#ifndef Sd2CardExt_h
#define Sd2CardExt_h

#include "Sd2Card.h"

class Sd2CardExt : public Sd2Card {
 public:
  Sd2CardExt(void) : Sd2Card() {}

  uint8_t readExtDataPort(uint8_t mio, uint8_t func, uint16_t addr, uint8_t* dst);
  uint8_t readExtMemory(uint8_t mio, uint8_t func, uint32_t addr, uint16_t count, uint8_t* dst);
  uint8_t writeExtDataPort(uint8_t mio, uint8_t func, uint16_t addr, const uint8_t* src);
  uint8_t writeExtMemory(uint8_t mio, uint8_t func, uint32_t addr, uint16_t count, const uint8_t* src);
  uint8_t writeExtMask(uint8_t mio, uint8_t func, uint32_t addr, uint8_t mask, const uint8_t* src);
 protected:
  uint8_t readExt(uint32_t arg, uint8_t* src, uint16_t count);
  uint8_t writeExt(uint32_t arg, const uint8_t* src, uint16_t count);
};
#endif  // Sd2CardExt_h

なお、Sd2Cardクラスを継承するために、Sd2Card.hにも少々変更が必要です。

Sd2Card.h

変更前:

 ...
 private:
  uint32_t block_;
  uint8_t chipSelectPin_;
  uint8_t errorCode_;
 ...

変更後:

 ...
 protected:
  uint32_t block_;
  uint8_t chipSelectPin_;
  uint8_t errorCode_;
 ...

まず、CMD48/49に関連した定数を追加します。

Sd2CardExt.cpp (その1)

#include <Arduino.h>
#include "Sd2CardExt.h"

uint8_t const CMD48 = 0x30;
uint8_t const CMD49 = 0x31;
uint8_t const SD_CARD_ERROR_CMD48 = 0x80;
uint8_t const SD_CARD_ERROR_CMD49 = 0x81;

次に、ArduinoのSPIハードウェアを利用してデータの送信・受信を行う関数を用意します。 この関数はSd2Cardモジュールに含まれているものと同じですが、 頻繁に利用するため、インライン関数としてコピーしています。

Sd2CardExt.cpp (その2)

/** Send a byte to the card */
inline void spiSend(uint8_t b) {
  SPDR = b;
  while (!(SPSR & (1 << SPIF)));
}

/** Receive a byte from the card */
inline  uint8_t spiReceive(void) {
  spiSend(0xFF);
  return SPDR;
}

そして、CMD48/49を実際に発行する共通関数として、readExt()関数およびwriteExt()関数を、 Sd2Cardクラスに含まれるCMD17/24用の関数であるreadData()関数およびwriteBlock()関数を流用して作成します。

CMD48/49の信号送受信のタイミングは、一般のメモリアクセス用コマンドCMD17/24と同一ですが、 コマンド番号と引数が違うほか、CMD48/49の読み書きは常に512バイト単位となります。 512バイト未満の読み書きの場合は、ダミーデータを追加して必ず512バイトにします。

なお、readExt()関数で、chipSelectHigh()のあとに、ダミーデータを1バイト分送信しています。 これは、FlashAirにクロックを与えて、コマンドの処理開始を保証するためのものです。

Sd2CardExt.cpp (その3)

/** Perform Extention Read. */
uint8_t Sd2CardExt::readExt(uint32_t arg, uint8_t* dst, uint16_t count) {
  uint16_t i;

  // send command and argument.
  if (cardCommand(CMD48, arg)) {
    error(SD_CARD_ERROR_CMD48);
    goto fail;
  }

  // wait for start block token.
  if (!waitStartBlock()) {
    goto fail;
  }

  // receive data
  for (i = 0; i < count; ++i) {
    dst[i] = spiReceive();
  }

  // skip dummy bytes and 16-bit crc.
  for (; i < 514; ++i) {
    spiReceive();
  }

  chipSelectHigh();
  spiSend(0xFF); // dummy clock to force FlashAir finish the command.
  return true;

 fail:
  chipSelectHigh();
  return false;
}

/** Perform Extention Write. */
uint8_t Sd2CardExt::writeExt(uint32_t arg, const uint8_t* src, uint16_t count) {
  uint16_t i;
  uint8_t status;

  // send command and argument.
  if (cardCommand(CMD49, arg)) {
    error(SD_CARD_ERROR_CMD49);
    goto fail;
  }

  // send start block token.
  spiSend(DATA_START_BLOCK);

  // send data
  for (i = 0; i < count; ++i) {
    spiSend(src[i]);
  }

  // send dummy bytes until 512 bytes.
  for (; i < 512; ++i) {
    spiSend(0xFF);
  }

  // dummy 16-bit crc
  spiSend(0xFF);
  spiSend(0xFF);

  // wait a data response token
  status = spiReceive();
  if ((status & DATA_RES_MASK) != DATA_RES_ACCEPTED) {
    error(SD_CARD_ERROR_WRITE);
    goto fail;
  }

  // wait for flash programming to complete
  if (!waitNotBusy(SD_WRITE_TIMEOUT)) {
    error(SD_CARD_ERROR_WRITE_TIMEOUT);
    goto fail;
  }

  chipSelectHigh();
  return true;

 fail:
  chipSelectHigh();
  return false;
}

最後に、3つの読み書きモードに対応する関数を追加します。 モードに合わせた引数を作成し、読み書きするデータサイズと一緒にreadExt()writeExt()に渡します。

mioは、メモリ領域とI/O領域のどちらを指定するかのフラグです。 iSDIOでは常にI/O領域を指定します。

funcは、機能番号です。 iSDIOでは機能番号0は、一般情報エリアとして予約されているため、 実際の機能は1から始まります。 例えば、FlashAirの無線LAN機能は、1になります。

addrは、アドレスです。 アドレス空間は、512バイトごとに「ページ」という単位に分かれており、 レジスタモードの場合はページの一部のみを読み書きできますが、 データポートモードの場合はあるページ全体を一度に読み書きします。 また、ページをまたがったアクセスはできません。

countは、アクセスするバイト数です。 データポートモードの場合は、0を指定しなければなりません。

詳細は、 SD Specifications Part 1 Physical Layer Simplified Specificationの 5.7.2節を参照してください。

Sd2CardExt.cpp (その4)

uint8_t Sd2CardExt::readExtDataPort(uint8_t mio, uint8_t func, 
        uint16_t addr, uint8_t* dst) {
  uint32_t arg = 
      (((uint32_t)mio & 0x1) << 31) | 
      (mio ? (((uint32_t)func & 0x7) << 28) : (((uint32_t)func & 0xF) << 27)) |
      (((uint32_t)addr & 0x1FE00) << 9);

  return readExt(arg, dst, 512);
}

uint8_t Sd2CardExt::readExtMemory(uint8_t mio, uint8_t func, 
        uint32_t addr, uint16_t count, uint8_t* dst) {
  uint32_t offset = addr & 0x1FF;
  if (offset + count > 512) count = 512 - offset;

  if (count == 0) return true;

  uint32_t arg = 
      (((uint32_t)mio & 0x1) << 31) | 
      (mio ? (((uint32_t)func & 0x7) << 28) : (((uint32_t)func & 0xF) << 27)) |
      ((addr & 0x1FFFF) << 9) |
      ((count - 1) & 0x1FF);

  return readExt(arg, dst, count);
}

uint8_t Sd2CardExt::writeExtDataPort(uint8_t mio, uint8_t func, 
          uint16_t addr, const uint8_t* src) {
  uint32_t arg =
      (((uint32_t)mio & 0x1) << 31) | 
      (mio ? (((uint32_t)func & 0x7) << 28) : (((uint32_t)func & 0xF) << 27)) |
      (((uint32_t)addr & 0x1FE00) << 9);

  return writeExt(arg, src, 512);
}

uint8_t Sd2CardExt::writeExtMemory(uint8_t mio, uint8_t func, 
    uint32_t addr, uint16_t count, const uint8_t* src) {
  uint32_t arg =
      (((uint32_t)mio & 0x1) << 31) | 
      (mio ? (((uint32_t)func & 0x7) << 28) : (((uint32_t)func & 0xF) << 27)) |
      ((addr & 0x1FFFF) << 9) |
      ((count - 1) & 0x1FF);

  return writeExt(arg, src, count);
}

uint8_t Sd2CardExt::writeExtMask(uint8_t mio, uint8_t func, 
    uint32_t addr, uint8_t mask, const uint8_t* src) {
  uint32_t arg =
      (((uint32_t)mio & 0x1) << 31) | 
      (mio ? (((uint32_t)func & 0x7) << 28) : (((uint32_t)func & 0xF) << 27)) |
      (0x1 << 26) |
      ((addr & 0x1FFFF) << 9) |
      mask;

  return writeExt(arg, src, 1);
}

パラメータ作成用ヘルパー関数の作成

iSDIO規格はコマンドやりとりの手順(プロトコル)を定めたものです。 iSDIOのコマンド発行は、 将来登場すると期待される多様なアプリケーションに対応できるよう、 スクリプトのような形で実行内容を渡す方法になっています。 スクリプトと言っても、言語ではなく数値の羅列ですので、プログラムをわかりやすくするために、ヘルパー関数を用意します。

put\_xxx()は、バッファに値を書き込み、値のバイト数分だけポインタを進めて、新しいポインタを返すものです。 put\_xxx_arg()は、スクリプトの引数専用で、値のサイズ、値、4バイト境界に合わせるためのパディング、を書き込みます。 書き込んだデータのバイト数分だけポインタを進めて、新しいポインタを返します。 put\_command\_header()およびput\_command\_info\_header()は、スクリプトのヘッダを作成するために利用します。 書き込んだデータのバイト数分だけポインタを進めて、新しいポインタを返します。 get\_xxx()は、バッファから値を読み取ります。

詳しい使い方は、今後のチュートリアルで解説していきたいと思います。

ヘルパー関数

uint8_t* put_u8(uint8_t* p, uint8_t value);
uint8_t* put_u16(uint8_t* p, uint16_t value);
uint8_t* put_u32(uint8_t* p, uint32_t value);
uint8_t* put_str(uint8_t* p, const char* value);

uint8_t* put_u8_arg(uint8_t* p, uint8_t value);
uint8_t* put_u16_arg(uint8_t* p, uint16_t value);
uint8_t* put_u32_arg(uint8_t* p, uint32_t value);
uint8_t* put_str_arg(uint8_t* p, const char* value);

uint8_t* put_command_header(uint8_t* p, uint8_t num_commands,
        uint32_t command_bytes);
uint8_t* put_command_info_header(uint8_t* p, uint16_t command_id,
        uint32_t sequence_id, uint16_t num_args);

uint8_t get_u8(uint8_t* p);
uint16_t get_u16(uint8_t* p);
uint32_t get_u32(uint8_t* p);

利用方法

Arduino IDEを起動して、メニューから 「スケッチ」 > 「ライブラリを使用」 > 「Add Library ...」を選び、 iSDIO フォルダを指定します。

一度この作業を行うと、次からはメニューに 「スケッチ」 > 「ライブラリを使用」 > 「iSDIO」 が現れます。

実行結果

今回はこのライブラリが正しくコンパイルできるところまで確かめてみましょう。

メニューの「ファイル」 > 「新規ファイル」を選択し、新しいエディタを開きます。 次に、 「スケッチ」 > 「ライブラリを使用」 > 「iSDIO」 を選択します。

カードの初期設定を行うsetup()関数および空のloop()関数を作成します。

arduino_tutorial_02.ino

#include <utility/Sd2CardExt.h>

const int chipSelectPin = 4;
Sd2CardExt card;

void setup() {
  // Initialize UART for message print.
  Serial.begin(9600);
  while (!Serial) {
    ;
  }

  // Initialize SD card.
  Serial.print(F("\nInitializing SD card..."));  
  if (card.init(SPI_HALF_SPEED, chipSelectPin)) {
    Serial.print(F("OK"));
  } else {
    Serial.print(F("NG"));
    abort();
  }
}

void loop() {
}

そして、メニューの「スケッチ」 > 「検証・コンパイル」で、エラーが出ないか確かめてください。

サンプルコード

arduino_tutorial_02.zip (22KB)

本チュートリアルのサンプルコードはGPLv3および二条項BSDライセンスで提供されています。 詳細はダウンロードした各ファイルを参照してください。