SPI ハードウェア CRC 回路の MMC/SD カードへの応用 (5)

今回は、ChaN さんの「ぷち FatFs」(→こちら) の AVR 用のサンプル・プロジェクト (アーカイブ・ファイル名は "pfsample.zip") に STM32 の SPI ハードウェア CRC 計算機能を組み込んだプログラム例を示しますが、その前に、いくつかの MMC/SD カード・コマンドについて実際のハードウェアで計算した値の例を示します。

コマンド
種別
Byte1 Byte2 Byte3 Byte4 Byte5 Tx CRC
レジスタ
補正後の
CRCバイト
CMD0  40  00  00  00  00  94  95
CMD1  41  00  00  00  00  71  F9
CMD8  48  00  00  01  AA  86  87
CMD9  49  00  00  00  00  AE  AF
CMD10  4A  00  00  00  00  1A  1B
CMD13  4D  00  00  00  00  0C  0D
CMD16  50  00  00  02  00  9D  15
CMD41  69  00  00  00  00  E4  E5
CMD58  7A  00  00  00  00  75  FD
CMD59  7B  00  00  00  01  0B  83

右から二番目の列が SPI の送信 CRC レジスタ (SPIx->TXCRCR) に現れたハードウェア CRC 計算結果で、一番右の列が b0 の固定「1」も含めて補正した 6 バイト目の CRC バイトの値です。
使用したのは「CQ-STARM」基板で、SPI1 が MMC/SD カード用の SPI として使われていますから、プログラムもそれに合わせてあります。
また、STMicro の標準ライブラリを使って記述しています。
まずは、CRC-7 と CRC-16-CCITT の定義です。

//
// CRC-7: g(x) = x**7 + x**3 + 1
//             = {7, 3, 0}
//             = 0x89
//             = 0b10001001

#define GX_CRC7  (0x089)
#define GX_CRC7P (GX_CRC7 ^ (GX_CRC7 << 1))

//
// CRC-16-CCITT generator polynomial
//
// g(x) = x**16 + x**12 + x**5 + 1
//      =  {16, 12, 5, 0}
//      =  0x11021   
//      =  0b10001000000100001
 
#define GX_CRC16 (0x1021)

CRC-7 の定義そのものは「GX_CRC7」ですが、これは計算した CRC の補正のためだけに使われ、SPI モジュールの CRC 多項式レジスタ (SPIx->CRCPR) に設定する値としては「GX_CRC7P」を使います。
「GX_CRC7P」は「パリティ付き CRC-7」といった意味です。 GX_CRC7 と、GX_CRC7 を左に 1 ビットシフトしたものとのエクスクルーシブ OR を取ります。
機能としては「CRC-8」と呼んでも差し支えないものですが、一般的に使われている何種類かの「CRC-8」とは一致しないので、誤解を避けるために名前を変えました。
次は SPI1 の初期化ルーチンで、ぷち FatFs を初期化する前に、他の周辺機能の初期化と合わせて実行します。

void SPI1_configuration( void )
{
// Enable SPI2 clock
  RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE); 
    
// SPI clock = 72 MHz / 4 = 18 MHz    
  SPI_InitStruct.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_4;
  SPI_InitStruct.SPI_CPHA          = SPI_CPHA_1Edge;
  SPI_InitStruct.SPI_CPOL          = SPI_CPOL_Low;
  SPI_InitStruct.SPI_DataSize      = SPI_DataSize_8b;
  SPI_InitStruct.SPI_Direction     = SPI_Direction_2Lines_FullDuplex;
  SPI_InitStruct.SPI_FirstBit      = SPI_FirstBit_MSB;
  SPI_InitStruct.SPI_Mode          = SPI_Mode_Master;
  SPI_InitStruct.SPI_NSS           = SPI_NSS_Soft;
  SPI_InitStruct.SPI_CRCPolynomial = GX_CRC7P; 
    
  SPI_Init(SPI1, &SPI_InitStruct);
  SPI_NSSInternalSoftwareConfig(SPI1, SPI_NSSInternalSoft_Set);
  SPI_CalculateCRC(SPI1, ENABLE); 
 
  SPI_Cmd(SPI1,  ENABLE);   
} // void SPI1_configuration()

実験では、常に 18 MHz の SPI クロックを使っていても動作に問題はなかったので、特に 100 kHz 程度の低速クロックからクロック・スピードを上げていくことはせず、最初から 18 MHz SPI クロックを使う設定にしています。
CQ-STARM 基板では MMC/SD カードの CS 信号については GPIO の PC12 を利用していますが、そちらの設定については省略します。
ぷち FatFs の AVR 用サンプル・プロジェクトの「mmc.c」から呼ばれる、下位レイヤーの I/O ルーチンを下に示します。 簡単のため、割り込み等は使用せず、ポーリングを使っています。

void SPI_busy_wait( void )
{
// wait while SPI hardware is busy
  do {} 
    while (SET == SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_BSY)); 
} // void SPI_busy_wait()

void SPI_TxD_empty_wait( void )
{
// wait until xmit data register empty
  do {} 
    while (RESET == SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE)); 
} // void SPI_TxD_empty_wait()

void xmit_spi (BYTE txdata)	// Send a byte 
{
  SPI_TxD_empty_wait(); // wait until Tx Data register empty
  SPI_I2S_SendData (SPI1, txdata); 
} // void xmit_spi()

BYTE rcv_spi (void)	// Send 0xFF and receive a byte 
{
  xmit_spi(0xff); // xmit 0xFF
  SPI_busy_wait();// wait while SPI hardware is busy
  return (SPI_I2S_ReceiveData(SPI1));
} // BYTE rcv_spi()

MMC/SD カード・コマンドの第一バイトを送る前に、CRC 値のリセットを行わなければなりませんが、リセットには少し手間が掛かるので、独立したルーチンとして、SPI の 8 ビット / 16 ビット・モードの切り替えの機能も持たせました。

//
// clear CRC and change 8/16 bit mode
//
void clear_CRC(BYTE mode16b)
{
  SPI_busy_wait();// wait while SPI hardware is busy
  SPI_Cmd(SPI1,  DISABLE);         // disable SPI at first 
  SPI_CalculateCRC(SPI1, DISABLE); // disable CRC calc.
  if (mode16b) { // 16 bit mode
    SPI_DataSizeConfig(SPI1, SPI_DataSize_16b);
    SPI1->CRCPR = GX_CRC16;
  } else { // 8 bit mode
    SPI_DataSizeConfig(SPI1, SPI_DataSize_8b);
    SPI1->CRCPR = GX_CRC7P;
  }
  SPI_CalculateCRC(SPI1, ENABLE);  // enable CRC calc. again
  SPI_Cmd(SPI1,  ENABLE);          // enable SPI again 
} // void clear_CRC()

CRC 値のリセットのためには、標準ライブラリの呼び出しでは、

  SPI_CalculateCRC(SPI1, DISABLE); 
  SPI_CalculateCRC(SPI1, ENABLE); 

というシーケンスが必要で、さらに、CRC 計算のイネブール/ディスエーブルの操作をする際には SPI 動作がディスエーブルされていなければならないので、それらの操作をまとめたのが上のルーチンです。
いったん SPI 動作をディスエーブルするので、同時に 8 ビット / 16 ビット・モードの切り替えと生成多項式の切り替えを行っています。
送信 CRC レジスタの値の読み取りと補正をまとめたのが下の関数です。

// 
// read SPI Tx hardware CRC value (8 bit)
// and adjust to CRC-7 value (7 bit)
//
BYTE read_Tx_CRC7(void)
{
  BYTE crc_value;
  
  SPI_busy_wait();// wait while SPI hardware is busy
  crc_value = SPI_GetCRC(SPI1, SPI_CRC_Tx);
  if (0x01 & crc_value) {
    crc_value ^= (GX_CRC7);
  } // if (0x01 & ...
  return(crc_value | 0x01); // force the last bit to '1'
} // BYTE read_Tx_CRC7()

ぷち FatFs の AVR 用サンプル・プロジェクトの「mmc.c」内の「send_cmd()」関数に施す変更点を下に示します。

/*-------------------------------*/
/* Send a command packet to MMC  */
/*-------------------------------*/

static
BYTE send_cmd (
  BYTE cmd,  /* Command byte */
  DWORD arg  /* Argument */
)
{
  BYTE n, res;

  if (cmd & 0x80) { /* ACMD<n> is the command sequense of CMD55-CMD<n> */
    cmd &= 0x7F;
    res = send_cmd(CMD55, 0);
    if (res > 1) return res;
  }

 /* Select the card */
  DESELECT();
  rcv_spi();
  SELECT();
  rcv_spi();

// ==== 追加 ==== ここから ====
  clear_CRC(0);  // clear CRC in 8-bit mode
// ==== 追加 ==== ここまで ====
  
/* Send a command packet */
  xmit_spi(cmd);                /* Start + Command index */
  xmit_spi((BYTE)(arg >> 24));  /* Argument[31..24] */
  xmit_spi((BYTE)(arg >> 16));  /* Argument[23..16] */
  xmit_spi((BYTE)(arg >> 8));   /* Argument[15..8] */
  xmit_spi((BYTE)arg);          /* Argument[7..0] */

// ==== 追加 ==== ここから ====
  n = read_Tx_CRC7(); // read and adjust Tx CRC-7 
// ==== 追加 ==== ここまで ====
// ==== コメントアウト ==== ここから ====
/*  
  n = 0x01;                   // Dummy CRC + Stop 
  if (cmd == CMD0) n = 0x95;  // Valid CRC for CMD0(0)   
  if (cmd == CMD8) n = 0x87;  // Valid CRC for CMD8(0x1AA) 
*/  
// ==== コメントアウト ==== ここまで ====

  xmit_spi(n);

/* Receive a command response */
  n = 10;  /* Wait for a valid response in timeout of 10 attempts */
  do {
    res = rcv_spi();
  } while ((res & 0x80) && --n);

  return res;  /* Return with the response value */
}

さらに、MMC/SD カードを CMD0 で SPI モードに設定すると、デフォルトで CRC 機能はオフになるので、CRC 機能を有効化するには CMD59 を発行する必要があります。
disk_initialize() 関数に対する変更点を下に示します。

// ==== 追加 ==== ここから ====
#define CMD59	(0x40+59)	// CRC ON/OFF
// ==== 追加 ==== ここまで ====

DSTATUS disk_initialize (void)
{

// ==== 中略 ====

  ty = 0;
  if (send_cmd(CMD0, 0) == 1) {  /* Enter Idle state */

// ==== 追加 ==== ここから ====
    send_cmd(CMD59, 1);  // enable CRC
// ==== 追加 ==== ここまで ====

    if (send_cmd(CMD8, 0x1AA) == 1) {  /* SDv2 */
      for (n = 0; n < 4; n++) ocr[n] = rcv_spi();  /* Get trailing return value of R7 resp */

// ==== 中略 ====
}

CRC 機能を有効化する「意義」としては、データ・ブロック部の伝送誤りを検出して信頼性を上げることの方が大きく、同時にコマンド部の CRC 機能もオンになるので、仕方なくコマンド部の CRC 計算も行うという意味合いが強いです。
SPI モジュールの CRC 計算機能では、16 ビット CRC 計算のためには SPI を 16 ビット・モードで動作させなければなりません。
16 ビット・モードでのデータ・ブロック部のアクセスのためのプログラムについては省略しますが、注意すべき点についてだけ述べておきます。
「ぷち FatFs」では、必要な RAM 容量を減らすために、システム側でセクタ・バッファなどは用意しておらず、たとえばディレクトリ・エントリなどのセクタ内の 16 バイトだけが必要な場合には、必要な部分だけをメモリに書き込み、不必要な部分は読み飛ばす操作を行っています。
そのための読み込み関数が「disk_readp()」で、まず、引数 Offset に与えられた「オフセット」分だけ読み飛ばし、次に引数 Count バイトだけ引数 Buffer が指し示すバッファにデータを読み込み、最後にセクタ内の残りのバイトを読み飛ばすようになっています。
この読み飛ばしバイト数および有効なデータ・バイト数は奇数になる可能性があり、SPI を 16 ビット・モードで動作させる場合には、16 ビット内に読み飛ばしデータと、有効データとが混在する状態も扱わなければならないことに注意する必要があります。
また、下位レイヤーの I/O ルーチンから上位レイヤーに報告するエラーコードとしては、エラーなしを示す「FR_OK」と、回復不能なハードエラーを示す「FR_DISK_ERR」などしかなく、リトライによって回復可能なソフトエラーを示すコードがありません。
したがって、上位レイヤーではリトライを行いませんので、CRC によって信頼性を上げるためには、下位レイヤーのルーチンでリトライを行う必要があります。