書き込み阻止入門 - GnicoJP/KAMPPHER GitHub Wiki
概要
SPIの不正な書き込みを阻止する方法を説明する.
方針
普通は不正な書き込み命令を送らないようにすることを考えるだろう.
しかし,その方法では,書き込み先アドレスをSDカードに届くよりも先に見るために,信号を5バイト遅延しなければならず,これは(下の試行にあるように)SPIの仕様に合う形で遅延させることができない.
従って,不正な書き込み命令を素通りさせた後に,その命令をキャンセルさせることを考える.
SDカードSPIモードのキホン
SDカードSPIモードは基本,6バイトのコマンド(コマンド番号+引数)がマイコンからSDカードに送られて,それに対するレスポンスがSDカードからマイコンに行われる.
そして,書き込み読み込みはBlock単位で行われる.書き込みには複数ブロックの書き込みを行うMULTI BLOCK WRITEコマンドと,一つのブロックの書き込みを行うSINGLE BLOCK WRITEコマンドがある.引数には書き込み先アドレスを指定する.
さらに,(MultiMediaCardとは異なり,)SET ERASE BLOCK COUNTコマンドというコマンドが存在する.
これは,書き込むブロック数を(引数で)指定するために使用され,MULTI BLOCK WRITEコマンドを送る前に送信する.
Blockとは
Blockとは書き込む領域の単位.ファイルの読み書きをすると普通は数バイト読み書きする.
バイト単位で書き込んでたらコマンド→レスポンスを毎回やらなきゃいけないので,転送速度が低下する.
だから,Blockといって複数バイトをひとまとめに転送する.
SINGLE BLOCK WRITEコマンド
普段のWRITEコマンドのビット列は以下のようになる.

SPIは基本的にコマンド→レスポンスの形になるが,書き込みの場合はレスポンスの後に書き込むデータが続く.
書き込むデータの前にDataTokenと呼ばれるビット列を付加する.
さて,DataTokenには,転送を終了するStop Transmission Tokenと呼ばれるトークンが存在する.
これを送れば転送を終わらせることができる.これを利用して,本機構では次のようにSPIの信号を改変する.

やっていることは単純明快で,DataTokenをStop Transmission Tokenに改変している.
SET ERASE BLOCK COUNTコマンド
このコマンドは複数ブロックを書き込みたい時に,WRITEコマンドを送る前に送ると転送速度が向上する.
何を送っているのかというと,WRITEコマンドで書き込むブロック数を送っている.
コマンドの引数は32bit固定で,書き込み先アドレスを送るだけで埋まってしまう.
だから,WRITEコマンドとは別に,書き込むブロック数を送るSET ERASE BLOCK COUNTコマンド(以下ACMD23と略す.)を用意することで,書き込みブロック数を伝えることができる.
書き込みブロック数が分かれば転送速度が向上する.これはフラッシュメモリの性質による.(WikipediaでNAND型フラッシュメモリの説明を見ると良いかも.)
さて,なぜこれが問題になるかというと,ACMD23を送って,WRITEコマンドを送ると,指定したアドレスのメモリセルの状態が(規格上)不定値になる.
だから,以下のようにしてSDカードのデータが破壊されてしまう.
- 本機構ではコマンドの送信時は何も触らないので,コマンドはSDカードに届く.
- すると,SDカードは書き込み先アドレスから,書き込み先メモリセル全体を初期状態にする.
- この時に初期状態にすると,データが失われるので,(メモリセル全体の)データをあらかじめ読み出しておいて,書き込む部分は普通に書き込む.
- そして,書き込んでいない部分は状態を復元するために,読み出しておいたデータを書き込む.
- しかし,もしACMD23によって書き込むブロック数が分かると,メモリセルでデータを書き換えずに復元しなければならない部分がわかる.
- このときに,書き込むブロック数がメモリセル全体の容量を越していた場合,(全部書き込むことになるから)復元しなければならない部分がなくなる.
- 従って,データをあらかじめ読み出す動作が不要になる.すると,書き込みを高速化することができる.
- この時に初期状態にすると,データが失われるので,(メモリセル全体の)データをあらかじめ読み出しておいて,書き込む部分は普通に書き込む.
- もし,データをあらかじめ読み出していない状態でStop Transmission Tokenを送ると,元のデータが分からないので復元できないので何もしない.
- 消去前に戻すことができず,初期状態になったメモリセルは不定値になる.
だから,ACMD23を送った後に,書き込みを阻止しても書き込み前のデータが破損してしまう恐れがある.
よって,ACMD23を防ぐ必要がある.
- 消去前に戻すことができず,初期状態になったメモリセルは不定値になる.
普段のACMD23のビット列は以下のようになる.

これの防ぎ方は,引数である書き込みブロック数を0にしてしまえばよいだろう.

※何故,「だろう」なのかというと,このコマンドはSDカード独自のコマンドで,SDカードの仕様書は1000ドル払えないと見れないので,本当にどんなカードでも0でコマンドが通るかは分からない.(手元のSDカードは正しく動く.)
MULTI BLOCK WRITEコマンド
今度はMULTI BLOCK WRITEコマンドについて説明する.

まず,マスタが1ブロック目の書き込みは書き込み可能な領域に行って,2ブロック目の書き込みが書き込み禁止な領域に行うことを想定して説明する.
と言ってもこれはSINGLE BLOCK WRITEコマンドと同様にすればよい.1ブロック目の書き込みが終わったら,2ブロック目の書き込みが書き込み禁止な領域であることを確認したら, Stop Transmission Tokenを送信する.
実装するまで気づかなかったのが次のパターンである.

同様に2ブロック目の書き込みが書き込み禁止領域なのだが, マイコンはStop Transmission Tokenを送信して転送を終了している.
これは,禁止領域に書き込む動作ではないので,エラーになるような介入を行えない.
しかしながら,Stop Transmission Tokenを送信するのを見てから行動するわけにもいかない.
従って,先にStop Transmission Tokenを送信してから,マイコンが正しく転送を終了させているのを見て,SDカードがレスポンスを返す振る舞いを代わりにやる必要がある.
これがSpiInjectorが最初は二つでモジュールに分かれてたのに,断念して統合した理由である.
SpiInjectorの状態遷移図みたいなもの(自分用に作ったものなので見づらいかも)
将来的:Multi Block Writeのさらなる高速化
大容量・高速なSDカードはSet Erase Blockによって書き込みを高速化するのは大事なことだと考える.
そこで,SetEraseBlockCount→MultiBlockWriteと来た時に初めて書き込み先アドレスと書き込む長さが確定する.
だからそのタイミングで,いったん転送を終了して,SetEraseBlockCountを送りな押すと良さそうだ.
しかしながらすぐに終了させてしまうとSPIの規格に違反する.レスポンスをNcr以内に返さなきゃならないからだ.
そこで,遅延できるタイミングであるビジーフラグの出力時に転送を中断させる.
ビジーフラグは規格上ずーっと流して良いので,SPIの規格に違反することなくSetEraseBlockCountを送りなおすことができる.

試行たち
試行1:コマンドの長さ分バッファを設けて信号遅延させる
仕様的に使えない

書き込み可能かどうかは,書き込みコマンドか+書き込むアドレスが分かれば良い為,コマンド番号+引数の5オクテット(バイト)分遅延させて,書き込み可能であった場合,SDカードに送るもの.
コマンド-レスポンスはN_CRバイト以内でなければならない.その為,この方法を用いると,レスポンスを返すのに時間がかかりすぎて,エラーが起こってしまう.よって,この方法は仕様的に使えない事がわかる.
試行2:コマンド破壊
実験すると失敗した
コマンドにはCRCとエンドビット1がある.これらをデタラメな値にする事で,コマンドをSDカードに認識させないようにする事ができると考えた.
実験してみると,SDカードは無視する事がわかった.
試行3:即座に打ち消しコマンドを送る.
成功
SINGLE BLOCK WRITE COMMANDには,STOP TRANSMIT COMMANDを,
ERASE BLOCK COUNT SET COMMANDには,打ち消すERASE BLOCK COUNT SET COMMANDを.
ERASE COMMANDには,消去ブロックの範囲指定をおかしくするCOMMANDを.
仕様書を読んでいると,MULTI BLOCK WRITEをした時とSINGLE BLOCK WRITEをした時の状態が同じになっている事に気づいた.ということは,STOP TRANSMIT COMMANDを送れば良いのではないだろうか.ということで送る事ができた.
そして,あまり嬉しくないものがERASEコマンドたちである.ERASE BLOCK COUNT SET COMMANDは次回MULTI BLOCK WRITE時に書き込むブロック数を決めるようなコマンドである(厳密には違う).これを打ち消すには長さが0なERASE BLOCK COUNT SET COMMANDを送る.
ERASEコマンドは,直前に範囲指定のコマンドを送る.したがって,範囲指定のコマンドで不正な範囲を指定する事でわざと失敗させる.
COPYRIGHT (C) Go Suzuki 2019-2020