Intel Serial Flash Memory S33 制御モジュール の履歴(No.3)
更新シリアルROMを使った FPGA のコンフィグレーション†
現在、Spartan 3A DSP 1800A Starter Platform を使って計測を行う予定でごりごり開発をしています。
このボードには FPGA のコンフィグレーション用に Intel (現 Numonyx) の S33 というフラッシュROMが載っているため、当然のようにこれを使いたくなるのですが、 (というか、JTAG 以外のコンフィグレーション方法はこれしかない?) ROMへのコンフィグレーションデータの書き込みをするためには、 そのためのIPを自分で書かなければならないようで、、、 そのために苦労した結果をまとめようと思います。
iMPACT が対応していない?†
ISE には iMPACT というソフトが入っていて、通常、開発段階ではこのソフトを使って JTAG 経由で FPGA のコンフィグレーションを行います。
さらに、iMPACT には FPGA のコンフィグレーション用ポートに繋がれたシリアルフラッシュ ROM への書き込み機能があるので、フラッシュROMアクセスのためのIPを 自分で書く必要はないと思っていたのですが・・・
今回、実際に iMPACT で書き込もうという段になって気づいたことには、 2010-05-27 の時点での最新版である ISE 12.1 に付属の iMPACT M.53d は、 Spartan 3A DSP 1800A Starter Platform に載っている Intel (Numonyx) S33 というチップに対応していないようなのです。
iMPACT のマニュアルによれば Spartan 3A DSP 経由での書き込みをサポートしているのは以下のデバイスのみで、 実際、iMPACT の Select Attached SPI/BPI ダイアログでも これ以外のデバイスが一覧に出てきません。
Numonyx | M25P, M25PE, M45PE |
Atmel | AT45DB (Rev B, C, and D) |
Spansion | S25FL032P, S25FL064P, S25FL129P |
Xilinx の forum でも色々書かれていました。
http://tinyurl.com/393se3s
iMPACT を使って書く方法を自力で発見できなかったので、 しかたなく以下の方針で IP を作りました。
書き込めないことはないらしい†
その後の調査の結果、どうやら、JTAG 経由ではなく J10 経由で書き込む方法はあったみたいです。
試してませんが、これでできればその方が楽だったんだと思います。
Intel S33 について†
現在は Numonyx から供給されているそうです。
データシート:
http://www.numonyx.com/Documents/Datasheets/314822_S33_Discrete_DS.pdf
今回使う範囲での特徴としては、
- 動作クロックは 68 MHz 以下
- 容量は 64 Mbit = 8 Mbyte
- フラッシュROMなので、書き込み前に消去が必要
- データ消去は64kバイトのセクター毎に行える
- データ書き込みは256バイトのページ毎に行う
- メモリブロックはそれぞれ独立に 100,000 回の書き換えができる
- バルク消去コマンドでチップ全体を消去できる
- 消去や書き込み後はデバイスがビジーになるので、終わるまで次の操作ができない
- 読み出しにはアドレス境界の制限はなく、指定アドレスから順にいくらでも読み出せる
- 読み出しで最終アドレスを越えると、また先頭アドレスからデータ読み出しが続く
- コマンド間は 100 ns あける
とのことです
アドレス†
アドレスは常に3バイトで指定します。
64 Mbit は 8 Mbyte で、0x000000 - 0x7fffff までのアドレスを取ります。
セクタは64kバイトなので、各セクタの先頭アドレスは 0x??0000 となります。
ページは256バイトなので、各ページの先頭アドレスは 0x????00 となります。
コマンド†
デバイスにコマンドを送るには、S# ラインを下げた後、 クロックの立ち上がりエッジに同期して以下のデータをやりとりします。
- 1バイトのコマンド
- [3バイトのアドレス]
- [ダミーバイト]
- [読み書きされるデータ]
([ ] 内はコマンドによりあったり無かったりします)
各バイトは MSB から LSB の順に、クロックの立ち上がりで読み取られます。
コマンドは S# を下げている間だけ有効で、S# を上げると終了します。
データの書き込み、ベリファイ、読み出しを行うのに最低限必要なのは以下のコマンドです。
- Read SR
- Fast Read Data Bytes
- Write Enable
- (Write Disable)
- Sector Erase
- Page Program
- (Clear SR Fail Flags)
ステータスレジスタの読み出し†
消去や書き込みコマンドを送るとデバイスがビジーになるので、 次の操作を行う前にビジー状態が終わるのを待たなければなりません。
このために必要なのがステータスレジスタの読み出しです。
Read SR コマンド (0x05) を送ると、それから S# を上げるまでの間、 デバイスはステータスレジスタの値を送り続けます。
ステータスレジスタのビット 0 がビジーフラグなので、 通常の使い方では書き込み又は消去のコマンドを送った後、 Read SR 0x05 を送り、ビット 0 が 1 の間 S# を下げっぱなしにしておいて、 1 になったら S# を上げて終了する形になります。
データの読み出し・ベリファイ†
Fast Read Data Bytes (0x0b) に続き、3バイトのアドレスを指定すると、 8ビット分のダミーデータに続き、読み出したデータが連続して送られてきます。
これをシフトレジスタで受取って、ローカルのメモリに書き込む、 あるいはローカルのメモリ内容と比較することで読み出し・ベリファイができます。
データは S# を上げるまでひたすら送られてくるので、 ローカルのメモリサイズ分だけ読んだら S# を上げてコマンドを終了します。
データの書き込み†
データを書き込むためには、その前に消去しなければなりません。
また、書き込みや消去のように、フラッシュの内容を変化させるコマンドを送る前に、 ステータスレジスタの書き込み許可フラグを立てる必要があります。
この許可フラグは、消去や書き込みが成功すると自動的にクリアされるため、 毎回必ず立て直さなければなりません。
方針†
実際の書き込みでは、Spartan 3A DSP の 2kbytes の Block RAM を有効に使うため、
- 64k バイトセクタの消去
- 2k バイトをバッファに貯める
- 256 バイト単位で 2k バイト分のデータを書き込み
- 2k バイトをベリファイ
- ベリファイに失敗したらユーザに問い合わせてそのページへの書き込みを再試行して 4. へ
- ベリファイに成功すれば次の 2k バイトについて 3. へ
- 次の 64k バイトセクタについて 1. へ
という繰り返しになります。 途中にユーザーとの対話などを挟むことになるので、 このレベルの状態遷移はPC側で行いたいところです。
そこで、このモジュールを 32 ビット幅の内部バスに接続し、 バス経由で送られるコマンドを使って内部ステートマシンを起動する形にします。
インターフェース†
- コントローラは 32 ビットバスと接続され
- コマンドによりステートマシンが起動する
- 状態を知るためステータスを読み出す
- バッファメモリと直接やりとりしてデータの書き込み、読み出し、ベリファイを行う
という配置を考えていますが、特定のバスを想定していないので、 バスとの接続は別のコードを書くことになります。
LANG:verilog input rst, // リセット input clk, // クロック( < 136 MHz) input [31:0] command, // コマンド input command_we, // コマンド書き込み output [9:0] status, // モジュールステータス output reg [8:0] mem_a, // バッファメモリへの接続 input [31:0] mem_o, output [31:0] mem_i, output mem_we, output reg spi_seln, // SPI ROM への接続 output reg spi_clk, input spi_miso, output reg spi_mosi
各信号線の意味は以下の通りです。
- rst
- アクティブハイ
- clk
- 立ち上がりエッジを使う
- command
- かなり特殊な形でコマンドを指定する(後述)
- command_we
- コマンドを書き込む(アクティブハイ)
- status
- 下位8ビットはROMデバイスのステータスレジスタ
8ビット目はモジュールのビジーを表す (1 = ビジー)
9ビット目は直前のベリファイでエラーが生じたことを表す (1 = エラー) - mem_???
- バッファメモリとの接続(2kバイトのブロックメモリを接続する)
- spi_???
- S33 ロムとの接続
コード†
以下のコードは内部クロック 100 MHz、SPI クロック 50 MHz で動作確認しています。
Synthesize レポートでは 118 MHz まで動作すると出ていましたが、 それ以上にクロックを上げたければ、count を目的ごとに分けるなどが 必要になります。
入出力にレジスタによる遅延を挿入してあるため、配置には余裕があります。 この遅延のため、コマンド送出からデータ読み出しまでに余分に3クロック待つ必要がありますが、 実用上は問題ないと思います。
LANG:verilog module serial_flash_rom_core #( parameter INTER_COMMAND_WAIT = 5 // クロック周期の2倍と掛けて 100 ns 以上になる値を指定する (>=2) ) ( // 内部クロック 100MHz の時の値が 5 になる input rst, input clk, // spi_clk は 1/2 になる input [31:0] command, // { code[7:0], addr[23:11], 8'b0, mode, code_bits[1:0] } input command_we, output [9:0] status, output reg [8:0] mem_a, input [31:0] mem_o, output [31:0] mem_i, output mem_we, (* IOB="FORCE" *) output reg spi_seln, (* IOB="FORCE" *) output reg spi_clk, (* IOB="FORCE" *) input spi_miso, (* IOB="FORCE" *) output reg spi_mosi ); localparam codeWriteSR = 'h01; // コマンド+status localparam codePageProgram = 'h02; // コマンド+アドレス => データ localparam codeWriteDisable = 'h04; // コマンドのみ localparam codeReadSR = 'h05; // 8 bits を status に読み込み localparam codeWriteEnable = 'h06; // コマンドのみ localparam codeFastReadData = 'h0b; // コマンド+8 bits ダミー の後 データ localparam codeClearSRFailFlags = 'h0c; // コマンドのみ localparam codeSectorErase = 'hd8; // コマンド+アドレス // MAP & PAR を楽にするため、出力にディレイを入れる (メモリクロックで1クロック分) reg spi_seln_int, spi_seln_reg; reg spi_clk_int, spi_clk_reg; wire spi_mosi_int; reg spi_mosi_reg; always @(posedge clk) begin spi_seln_reg <= spi_seln_int; spi_seln <= spi_seln_reg; spi_clk_reg <= spi_clk_int; spi_clk <= spi_clk_reg; spi_mosi_reg <= spi_mosi_int; spi_mosi <= spi_mosi_reg; end // MAP & PAR を楽にするため、入力にディレイを入れる (メモリクロックで1クロック分) reg spi_miso_reg; reg [31:0] idata; always @(posedge clk) if (!spi_clk_int) begin spi_miso_reg <= spi_miso; // データは常にキャプチャしておき、必要なときに読み取る idata <= { idata, spi_miso_reg }; end // メモリからのデータを読むときは合計2クロック分遅れることを考慮する localparam stIdle = 0; localparam stSendCommand0 = 1; localparam stSendCommand1 = 2; localparam stReadStatusCommit = 3; localparam stReadData = 4; localparam stReadDataCommit = 5; localparam stVerifyData = 6; localparam stInterCommand = 7; localparam stInterCommandWait = 8; localparam stPageProgram = 9; reg [3:0] state = stIdle; reg [31:0] odata = 0; reg [7:0] code; reg [5:0] count; reg verify_error = 0; reg [7:0] status_int = 0; reg read_mode; // 0: read / 1: verify // アイドル時はクロックを止める always @(posedge clk) if (rst || state == stIdle) spi_clk_int <= 0; else spi_clk_int <= !spi_clk_int; // odata の MSB が出力される assign spi_mosi_int = odata[31]; // ステータスの構成 assign status = { verify_error, state != stIdle, status_int }; // メモリアクセス assign mem_we = state == stReadDataCommit; assign mem_i = idata; // ステートマシン always @(posedge clk) if (rst) begin state <= stIdle; spi_seln_int <= 1; verify_error <= 0; status_int <= 0; end else begin case (state) stIdle: if (command_we) begin odata <= { command[31:8], 8'h00 }; read_mode <= command[7]; count <= command[6:0]; state <= stSendCommand0; end stSendCommand0: if (spi_clk_int) begin // 立ち下がりエッジ spi_seln_int <= 0; // コマンド開始 code <= odata[31:24]; // 後で使うので取っておく mem_a <= odata[23:0] >> 2; // ワードアクセスのため 2 ビットシフト if (read_mode) verify_error <= 0; state <= stSendCommand1; end stSendCommand1: // odata に入っているコマンドやアドレスを送り、その後データの読み込みも行う if (spi_clk_int) begin // 立ち下がりエッジ odata <= odata << 1; // 次のビットを出力 if (count!=0) begin count <= count - 1; end else begin // 指定クロック経過したらコマンド別の処理に移る if (code == codeReadSR) begin state <= stReadStatusCommit; end else if (code == codeFastReadData) begin state <= stReadData; count <= 32 - 1; end else if (code == codePageProgram) begin state <= stPageProgram; count <= 32 - 1; odata <= mem_o; mem_a <= mem_a + 1; end else begin spi_seln_int <= 1; state <= stInterCommand; end end end stReadStatusCommit: begin status_int <= idata; // レジスタを更新 count <= 7; // 次に備える if (idata[0]) // busy ならもう1バイト読む state <= stSendCommand1; else begin // !busy なら終了 state <= stInterCommand; spi_seln_int <= 1; end end stReadData: if (!spi_clk_int) begin // 立ち上がりエッジ if (count!=0) begin count <= count - 1; end else begin // 1ワード読んだら if (read_mode==0) begin state <= stReadDataCommit; end else begin state <= stVerifyData; end end end stReadDataCommit: begin // 立ち下がりエッジのタイミングで遷移してくる // 2 kbytes 境界を判定 if ( ( mem_a & 'h01ff ) != 'h01ff ) begin state <= stReadData; end else begin state <= stInterCommand; end mem_a <= mem_a + 1; count <= 32 - 1; end stVerifyData: begin // 立ち下がりエッジのタイミングで遷移してくる // エラーなら終了 if (mem_o != idata) begin verify_error <= 1; state <= stInterCommand; end // 2 kbytes 境界を判定 if (( mem_a & 'h01ff ) != 'h01ff ) begin state <= stReadData; end else begin state <= stInterCommand; end mem_a <= mem_a + 1; count <= 32 - 1; end stPageProgram: if (spi_clk_int) begin // 立ち下がりエッジ odata <= odata << 1;// 次のビットを送信 if (count!=0) begin count <= count - 1; end else begin // ワード境界 count <= 32 - 1; odata <= mem_o; mem_a <= mem_a + 1; // 256 bytes 境界を判定 if ( (mem_a & 'h3f) == 'h00) state <= stInterCommand; end end stInterCommand: begin // 100 ns 空ける spi_seln_int <= 1; state <= stInterCommandWait; // stIdle 状態に新しいコマンドが届いてから seln が下がるまで 2 クロックかかるので count <= INTER_COMMAND_WAIT - 2; end stInterCommandWait: if (spi_clk_int) begin count <= count - 1; if (count==0) state <= stIdle; end default: begin spi_seln_int <= 1; // コマンドを終了 state <= stInterCommand; end endcase end endmodule
制約†
ピンに (* IOB="FORCE" *) を付けているので、ほぼそれだけで大丈夫だと思うのですが、 念のため以下のようにスキューを指定しました。
INST "spi_seln" TNM = PADGROUP_SPI_OUT; INST "spi_clk" TNM = PADGROUP_SPI_OUT; INST "spi_miso" TNM = PADGROUP_SPI_OUT; TIMEGRP "PADGROUP_SPI_OUT" OFFSET = OUT 4 ns AFTER "clk_125MHz_in" REFERENCE_PIN "spi_clk";
ピン配置は
NET "spi_seln" LOC = AA7 | IOSTANDARD = LVCMOS33 | SLEW = FAST; NET "spi_clk" LOC = AE24 | IOSTANDARD = LVCMOS33 | SLEW = FAST; NET "spi_mosi" LOC = AB15 | IOSTANDARD = LVCMOS33 | SLEW = FAST; NET "spi_miso" LOC = AF24 | IOSTANDARD = LVCMOS33 | SLEW = FAST;
です。
spi_mosi については PCB 上での配線の取り回しなども含めると遅延量が見積もりにくいのですが、 Write Enable 後の Read SR 結果が 0x02 となるよう調整したところ、 今のところ3クロックの遅延でうまく合っているようでした。
利用方法†
上記モジュールに与える 32 bit のコマンドワードは次の形式になりす。
- 最上位1バイト
- デバイスへ送信する8ビットのコマンドコード
- 中位2バイト
- デバイスへ送信する3バイトのアドレスの上位2バイト(下位1バイトは常にゼロになる) コマンドがアドレスを必要としなければ何でも良い
- 下位1バイト
- 7ビット目:Fast Read Data コマンドに対する読み出しモード (1 = verify / 0 = read)
0〜6ビット:コマンド部分のビット数から1を減じた値
モジュールの動作†
コマンドを受取ったモジュールは、
- コマンドワードの上位3バイトに1バイトの 0x00 を繋げた32ビットのデータを作成する
- 最下位7ビットで指定されたクロックに渡り、1. で作ったデータを MSB から順に送出する
- 指定されたクロック数が32以上の場合、残りは 0 が送出される
という動作をします。
最上位に指定するコマンドが、
- Read SR
- Fast Read Data
- Page Program
以外の場合、上記を行った後にモジュールはアイドル状態に戻ります。
上記3つのコマンドでは、さらに以下の動作を行います。
Read SR : ステータス読み出し†
モジュールはコマンドデータを送出するのと同時に、常にデータをキャプチャしているので、 コマンド送出後にその値をステータスレジスタに保存します。
このときデバイスがビジーであれば(ステータスレジスタの0ビット目が1であれば) レジスタ読み出しを継続し、SPIクロックで8クロック毎にステータスレジスタの値を 更新します。
ステータスレジスタの0ビット目が0になったらアイドルに戻ります。
コマンド指定時には、実際に送るコマンド長の8クロックに、 読み取るデータ長である8クロックおよび、 データ送受信に挿入された遅延量3クロックを加えた数値19から1を減じて、 コマンド長に18を指定します。
command = 0x05000012
Fast Read Data†
コマンド送出後、データを受信してデータバッファに保存、 あるいはデータバッファの値と比較(ベリファイ)します。
コマンドの7ビット目が 0 ならば保存、1 ならばベリファイします。
保存する場合は、2kバイト境界まで連続して読み込みます。
ベリファイでは、2kバイト境界まで連続して処理しますが、 エラーが生じたら status[9] を立てて即座にコマンドを終了します。
コマンド指定時には、実際に送るコマンド長の8クロック+アドレス長24に、 ダミーデータ長である8クロックおよび、 データ送受信に挿入された遅延量3クロックを加えた数値43から1を減じて、 コマンド長に42を指定します。
read command = 0x0b????2a
verify command = 0x0b????aa
Page Program†
コマンド送出後、指定アドレスからデータを送出し、 256バイト境界でストップします。
バッファメモリからのデータ読み出し位置は、コマンドに指定したアドレスの 8〜11ビットの値に応じて適当に選択されます。
コマンド指定時には、実際に送るコマンド長の8クロック+アドレス長24の、 32から1を減じて、コマンド長に31を指定します。
page program command = 0x02????1f
その他のコマンド†
次のコマンドはパラメータを持たないので、コマンド長に8−1=7を指定します。
- Write Disable
- Write Enable
- Clear SR Fail Flags
- Release from DPD only
- Deep Power-Down, Bulk Erase
次のコマンドはパラメータとしてアドレスを指定するので、コマンド長に8+24−1=31を指定します。
- Parameter Block Erase
- Sector Erase
次のコマンドは、アドレスの代わりに1バイトのデータを指定するので、コマンド長に8+8−1=15を指定します。
- Write SPI SR
Read Data Bytes コマンドには対応していません。 代わりに Fast Read Data を使って下さい。
次のコマンドには対応していません。
- OTP Program
- Read OTP Data Bytes
- Read ID
セッション例†
1セクタ書き込み
- 0x05000012 ステータス読み出し (処理の完了を待つ)
- 0x06000007 書き込み許可
- 0xd8????1f セクタ消去
- 2k バッファを埋める
- 0x05000012 ステータス読み出し (処理の完了を待つ)
- 0x06000007 書き込み許可
- 0x02????1f データ書き込み
- 2k 境界に達していなければ 5. へ戻る
- 0x05000012 ステータス読み出し (処理の完了を待つ)
- 0x0b????aa ベリファイ
- 失敗したらユーザーに問い合わせ、必要なら 1. へ戻る
- 64k 境界に達していなければ 4. へ戻る
コマンド FIFO の接続†
ReadStatus コマンドはデバイスがビジーでなくなるまで継続するので、 このモジュールにコマンドを貯めておくための FIFO を接続することで、
- 書き込み許可
- セクタ消去
- ステータス読み出し(終了待ち)
- 書き込み許可
- 書き込み
- ステータス読み出し(終了待ち)
- 書き込み許可
- 書き込み
- ステータス読み出し(終了待ち)
- 書き込み許可
- 書き込み
- ステータス読み出し(終了待ち)
...
のような一連のコマンドを連続して実行することができます。
この場合の構成は次のようになります。
暴走対策†
フラッシュには書き換え階数制限があるため、 ステートマシンやプログラムが暴走して多数回の書き込みが起きると 最悪の場合2度と書き込みが行えなくなってしまいます。
そこで、コマンド受信部分に細工をして暴走対策を行うのがよいかもしれません。
例えば何らかのマジックワードを決めておき、 有効なコマンドを送る前に必ずマジックワードを送らなければならないとしておけば、 バス上のランダムなアドレスに無意味なデータを書き込んでしまうような バグを持つコードを実行してしまったとしても、フラッシュの内容が書き換えられてしまう 確率を低くできると思います。
書き込むデータ†
FPGA コンフィグの回り道
http://www.geocities.jp/altshibabou/win/fpga_config.html
によれば、フラッシュに書き込むデータは、*.bit ファイルの内容そのままで良いそうです?
実はまだうまく行ってません。