Microblaze MCSをSpartan6で動かす(ISE)
FPGAを使っていると、ソフトウェアCPUを組み込みたくなることがあります。
CPUを組み込むことで複雑な分岐の処理などを楽に作ることができます。
今回はSpartan-6 FPGAにMicroblaze MCSを実装する方法を説明します。
前提条件としてFPGAでLチカができる程度の基礎知識はあるものとします。
まずISEを開き、プロジェクトを作ったら、新規IPをプロジェクトに追加します。
IPの選択でMicroblaze MCSのコアを選択します。
IPが作成出来たらIPの設定画面が開きます。
ここではMicroblazeの設定をします。RAM容量や使用するペリフェラルなどを選択することができます。
注意する点としては、インスタンスネームの指定では実際にインスタンシエートするモジュールのパスを入力する点です。
例えば、topの下にcpu0という名前でmicroblazeをインスタンス化する場合は、Instance Hierarcical Design Nameの欄にcpu0と入力します。
次にペリフェラルの設定をします。
今回はLED点灯を試すため、GPOを使用する設定にします。
GPO1に4bitの出力ポートを作っておきます。
OKを押してIPの設定を終了します。
次にMicroblazeをインスタンス化します。
topモジュールを適当に作り、その下にMicroblazeをインスタンス化します。
メニューのView Instantiation Templateからテンプレートを取得できます。
topに図のようなコードを追加しました。
module top( input Clk, input Reset, output [3:0] GPO1 ); mb_mcs cpu0 ( .Clk(Clk), // input Clk .Reset(Reset), // input Reset .GPO1(GPO1) // output [3 : 0] GPO1 ); endmodule
ここまで来たら一度シンセサイズを行い問題なく通ることを確認します。
次に、Implement Designを正しく行うため、tclスクリプトを実行します。
TCLコンソールを使いますが、コンソールウインドウが初期設定では表示されていないので、図のようにView-Panelsから設定して表示させます。
次にtclコンソールで下記のコマンドを実行します。
source ipcore_dir/microblaze_mcs_setup.tcl
なお、このtclスクリプトはデフォルトではipcore_dirディレクトリの中にあります。
注意としては、カレントディレクトリがプロジェクトルートの状態でtclファイルを指定しないとTranslateでコケます。
ここまでやったら通常と同様にbitファイル作成まで行います。
これでハードウェアの生成は完了です。
SDKでソフトウェアを作成する
次にソフトウェアを作成します。
tclコンソールでxsdkと入力しSDKを起動します。
BSPの作成
起動したらまずはBSPを作成します。
メニューからCreate BSP Projectを選択します。
ipcore_dirの中にハードウェア情報が入ったxmlファイルがあるため、hardware specificationに指定します。
BMMファイルも同じフォルダに入っているため、同様に指定します。
OSはスタンドアロンを選択します。
ここまでの手順でボードサポート部までは完成です。
アプリケーションプロジェクトの作成
次にアプリケーションプロジェクトを作成します。
基本デフォルトで良いですが、以前作ったBSPを選択するようにします。
コーディングする
実際にLチカをさせるプログラムを記載していきます。
microblaze上のペリフェラルはすべてXIOModuleというモジュール経由で操作するようになっています。
下記のようにXIOModuleのドライバ経由でGPOを制御しました。
waitのカウンタは動作周波数に応じて適宜調整してください。
#include "platform.h" #include "xparameters.h" #include "xiomodule.h" void wait(void) { volatile int cnt = 100000; while(cnt--); } int main() { XIOModule xio; init_platform(); XIOModule_Initialize(&xio, XPAR_IOMODULE_0_DEVICE_ID); while(1){ XIOModule_DiscreteWrite(&xio, 1, 0xF); wait(); XIOModule_DiscreteWrite(&xio, 1, 0); wait(); } return 0; }
SPI Flashの使い方 (2) - Arduinoから読み書き
組み込みでデータを保持したいときに使用するSPI Flashを実際にArduinoから制御してみます。
偶然手元にあった構成で、Arduino MicroとCypress製のSPI FlashであるS25FL032*1を使用します。
この記事は以下の記事の続きとなるので、部品の機能やコマンドについての詳細はこちらをご参照願います。
結線方法
基本的には前記事を参照していただければピン機能はわかると思います。
ArduinoなどでI/O電圧が5Vである場合、レベル変換が必要になります。
Arduino | SPI Flash | 備考 |
---|---|---|
D10 | CS# | digitalWrite可能なピンであればどこでも良い |
SCK | SCK | クロック |
MOSI | IO0/SI | データ入力 |
MISO | IO1/SO | データ出力 |
- | IO2/W# | QSPIは使わず、W#も使わないためH固定 |
- | IO3/HOLD# | QSPIは使わず、HOLD#も使わないためH固定 |
まずはSPI転送とCS制御を書く
コマンドを幾つか実装する上で、まずはGPIOの制御とSPI自体の制御が必要になります。
Arduino以外のプラットフォームでも動作できるよう、ある程度汎用性をもたせるように設計します。
SPIはSPI.h
を利用して、GPIOはpinMode
, digitalWrite
で実装します。
Serialでの表示はデバッグ用に残しているもので、不要であれば削除します。
#include <SPI.h> #define FLASH_CS 10 void spi_init(){ SPI.begin(); pinMode(FLASH_CS, OUTPUT); spi_set_cs(0x1); } void spi_set_cs(uint8_t is_high) { Serial.print("#spi_set_cs is_high= "); Serial.print(is_high, DEC); digitalWrite(FLASH_CS, is_high & 0x1); Serial.print(" ... Done\n"); } void spi_transfer(const uint8_t* tx_buf, uint8_t* rx_buf, uint16_t length){ Serial.print("#spi_transfer length= "); Serial.print(length, DEC); for(uint16_t i = 0 ; i < length ; ++i){ Serial.print("."); uint8_t data = SPI.transfer(tx_buf != NULL ? tx_buf[i] : 0x0); if(rx_buf != NULL){ rx_buf[i] = data; } } Serial.print(" Done\n"); }
void spi_transfer(const uint8_t* tx_buf, uint8_t* rx_buf, uint16_t length)
はlengthで指定した分のSPI転送を行います。
その際にtx_bufが指定されていなければ0x0のダミーデータを、rx_bufが指定されていたときのみ受信データを格納するように工夫します。
Flash制御の共通的な制御を書く
どのコマンドにも共通して、コマンドを送る、アドレスを送る、ダミーバイトを送る、データを送る、データを読み取る動作が存在します。
毎回spi_transfer
を呼び出してもいいですが、面倒なのと可読性を考慮して一段ラップします。呼び出しコストが気になるようであればコンパイル時にインライン展開されるような修飾を施します。
void flash_send_cmd(uint8_t cmd){ spi_transfer(&cmd, NULL, 1); } void flash_send_addr(uint32_t addr){ const uint8_t data[] = { (addr >> 16) & 0xff, (addr >> 8) & 0xff, (addr >> 0) & 0xff, }; spi_transfer(data, NULL, 3); } void flash_send_dummy(uint16_t length){ spi_transfer(NULL, NULL, length); } void flash_read_data(uint8_t* rx_buf, uint16_t length){ spi_transfer(NULL, rx_buf, length); } void flash_write_data(const uint8_t* tx_buf, uint16_t length){ spi_transfer(tx_buf, NULL, length); }
基本的なコマンドを実装する
いよいよコマンドの実装です。データシートに記載された全種類を実装してもいいですが、冗長なので基本的な命令を一通り実装します。
メーカー間で差があるといえど全く異なるわけではないので、コマンドのニーモニックやダミーバイト数などを調節すれば他のデバイスも制御できます。
新しいコマンドを追加したい場合も既存の記述を参照すれば簡単に追加可能です。もっと高機能にしたくなりますが、高級な実装は後回しです。
//Flashのコマンドを発行(S25FL032) #define CMD_RDID (0x9f) #define CMD_READ (0x03) #define CMD_WREN (0x06) #define CMD_WRDI (0x04) #define CMD_P4E (0x20) #define CMD_P8E (0x40) #define CMD_BE (0x60) #define CMD_PP (0x02) #define CMD_RDSR (0x05) void flash_rdid(uint8_t* rx_buf, uint16_t length){ spi_set_cs(0x0); flash_send_cmd(CMD_RDID); flash_read_data(rx_buf, length); spi_set_cs(0x1); } void flash_read(uint32_t addr, uint8_t* rx_buf, uint16_t length){ spi_set_cs(0x0); flash_send_cmd(CMD_READ); flash_send_addr(addr); flash_read_data(rx_buf, length); spi_set_cs(0x1); } void flash_wren(){ spi_set_cs(0x0); flash_send_cmd(CMD_WREN); spi_set_cs(0x1); } void flash_wrdi(){ spi_set_cs(0x0); flash_send_cmd(CMD_WRDI); spi_set_cs(0x1); } void flash_p4e(uint32_t addr){ spi_set_cs(0x0); flash_send_cmd(CMD_P4E); flash_send_addr(addr); spi_set_cs(0x1); } void flash_be(){ spi_set_cs(0x0); flash_send_cmd(CMD_BE); spi_set_cs(0x1); } void flash_pp(uint32_t addr, const uint8_t* wr_data, uint16_t length){ spi_set_cs(0x0); flash_send_cmd(CMD_PP); flash_send_addr(addr); flash_write_data(wr_data, length); spi_set_cs(0x1); } void flash_rdsr(uint8_t* rx_buf, uint16_t length){ spi_set_cs(0x0); flash_send_cmd(CMD_RDSR); flash_read_data(rx_buf, length); spi_set_cs(0x1); }
その他便利関数
EraseやProgramには完了を確認するためにステータスレジスタのWIP(Write In Progress)を監視する必要があるので、確認関数を追加します。
また、データのデバッグを楽にするためのデバッグプリントも実装しておきます。
//(WIP)Write In Progressかどうか確認 uint8_t flash_is_wip(){ uint8_t data; flash_rdsr(&data, 0x1); return (data & 0x1);//WIP } //配列を表示 void data_debug_print(const uint8_t* data, uint16_t bytes){ Serial.print("+0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +A +B +C +D +E +F\n"); Serial.print("-----------------------------------------------\n"); uint16_t depth = ((bytes - 1) >> 4) + 1; for(uint16_t j = 0 ; j < depth ; ++j){ for(uint16_t i = 0 ; i < 16 ; ++i){ uint8_t d = data[i + (j << 4)]; Serial.print((d >> 4) & 0x0f, HEX); Serial.print((d >> 0) & 0x0f, HEX); Serial.print(" "); } Serial.print("\n"); } }
まずは動作確認
まずは確実に定値が読み出せるコマンドで、デバイスと正しく疎通できているか確認します。うまくいかない場合はコマンドか結線を疑います。オシロがあると楽です。
今回の場合、Read Identification(RDID)
を使えば{Manufacturer Id, Device Id, Extended Bytes}
の固定値が読み出せそうなのでこれを使います。
期待値は{0x01, 0x02, 0x15, 0x4d}
です。
検証コード
void setup() { uint8_t rx_buf[256]; spi_init(); Serial.begin(9600); while (!Serial) {} Serial.print("RDID\n"); flash_rdid(rx_buf, 4); data_debug_print(rx_buf, 4); }
結果
RDID #spi_set_cs is_high= 0 ... Done #spi_transfer length= 1. Done #spi_transfer length= 4.... Done #spi_set_cs is_high= 1 ... Done +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +A +B +C +D +E +F ----------------------------------------------- 01 02 15 4D 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
CSをLowにした後1byte(コマンド) -> 4byte(RDIDのデータ)の順にspi_transferが実行されています。
読み出した結果も先頭に注目すると期待通りの結果が読み出せています。(残りはゴミです)
直接関係ないのですが、USBを内蔵したATMELマイコンをベースにしたArduino特有の現象でSerial.begin()
の後に初期化が終わるまで待たなければならないようです。
これはSerialの読み書きを、外部ではなく内部のUSB CDCを利用していることに起因しているようです。*2
特定の領域でRead, Erase, Program, Verifyをしてみる
デバイスとの疎通は出来ているようなので、以下のシーケンスで任意のデータを書き込んでみます。
- 現在のデータを読み出し
- セクタイレースを実行 (全体削除のBulk Eraseは検証には時間がかかりすぎるため)
- 適当な値を書き込み(シーケンシャルなデータを書き込む)
- データを読み出して書き込んだ値と相違ないか確認
注意すべき点として、保持されているデータを変化させる命令にはWriteEnableを事前実行する必要があることです。
ちなみにステータスレジスタあたりから確認できます。またWriteDisable命令もあります。
検証コード
void setup() { uint8_t tx_buf[256]; uint8_t rx_buf[256]; uint32_t addr = 0x0; uint16_t length = 0x100; spi_init(); Serial.begin(9600); while (!Serial) {} Serial.print("RDID\n"); flash_rdid(rx_buf, 4); data_debug_print(rx_buf, 4); Serial.print("Read\n"); flash_read(addr, rx_buf, length); data_debug_print(rx_buf, length); Serial.print("Read after P4E\n"); flash_wren();//P4Eの前に必要 flash_p4e(addr); while(flash_is_wip()){//消去待ち Serial.print("."); } Serial.print("\n"); flash_read(addr, rx_buf, length); data_debug_print(rx_buf, length); Serial.print("PP Sequential Data\n"); for(uint16_t i = 0 ; i < length ; ++i){ tx_buf[i] = i & 0xff; } flash_wren(); flash_pp(addr, tx_buf, length); while(flash_is_wip()){//書き込み完了待ち Serial.print("."); } Serial.print("\n"); Serial.print("Read after PP\n"); flash_read(addr, rx_buf, length); data_debug_print(rx_buf, length); }
結果
一部見やすいように加工しています
RDID #spi_set_cs is_high= 0 ... Done #spi_transfer length= 1. Done #spi_transfer length= 4.... Done #spi_set_cs is_high= 1 ... Done +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +A +B +C +D +E +F ----------------------------------------------- 01 02 15 4D 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F (RDIDの結果、正しく通信できている) Read #spi_set_cs is_high= 0 ... Done #spi_transfer length= 1. Done #spi_transfer length= 3... Done #spi_transfer length= 256..(省略).. Done #spi_set_cs is_high= 1 ... Done +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +A +B +C +D +E +F ----------------------------------------------- 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F 20 21 22 23 24 25 26 27 28 29 2A 2B 2C 2D 2E 2F 30 31 32 33 34 35 36 37 38 39 3A 3B 3C 3D 3E 3F 40 41 42 43 44 45 46 47 48 49 4A 4B 4C 4D 4E 4F 50 51 52 53 54 55 56 57 58 59 5A 5B 5C 5D 5E 5F 60 61 62 63 64 65 66 67 68 69 6A 6B 6C 6D 6E 6F 70 71 72 73 74 75 76 77 78 79 7A 7B 7C 7D 7E 7F 80 81 82 83 84 85 86 87 88 89 8A 8B 8C 8D 8E 8F 90 91 92 93 94 95 96 97 98 99 9A 9B 9C 9D 9E 9F A0 A1 A2 A3 A4 A5 A6 A7 A8 A9 AA AB AC AD AE AF B0 B1 B2 B3 B4 B5 B6 B7 B8 B9 BA BB BC BD BE BF C0 C1 C2 C3 C4 C5 C6 C7 C8 C9 CA CB CC CD CE CF D0 D1 D2 D3 D4 D5 D6 D7 D8 D9 DA DB DC DD DE DF E0 E1 E2 E3 E4 E5 E6 E7 E8 E9 EA EB EC ED EE EF F0 F1 F2 F3 F4 F5 F6 F7 F8 F9 FA FB FC FD FE FF (同じコードを何度か実行しているので前のデータがそのまま見えている) Read after P4E #spi_set_cs is_high= 0 ... Done #spi_transfer length= 1. Done #spi_set_cs is_high= 1 ... Done (ここまでがWrite Enableコマンド、データを改変するコマンドには事前にWRENする必要がある) #spi_set_cs is_high= 0 ... Done #spi_transfer length= 1. Done #spi_transfer length= 3... Done #spi_set_cs is_high= 1 ... Done (ここがP4Eコマンド、指定したセクタのデータが全部1になる、コマンドの完了には時間がかかるためWIPを見て待つ) (ここからWIP待ち) #spi_set_cs is_high= 0 ... Done #spi_transfer length= 1. Done #spi_transfer length= 1. Done #spi_set_cs is_high= 1 ... Done (長いので省略...) #spi_set_cs is_high= 0 ... Done #spi_transfer length= 1. Done #spi_transfer length= 3... Done #spi_transfer length= 256..(省略).. Done #spi_set_cs is_high= 1 ... Done +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +A +B +C +D +E +F ----------------------------------------------- FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF (削除後のデータ、全セルの値が1なのでffで埋め尽くされている) PP Sequential Data (WREN) #spi_set_cs is_high= 0 ... Done #spi_transfer length= 1. Done #spi_set_cs is_high= 1 ... Done (PP) #spi_set_cs is_high= 0 ... Done #spi_transfer length= 1. Done #spi_transfer length= 3... Done #spi_transfer length= 256...(省略).. Done #spi_set_cs is_high= 1 ... Done (WIP待ち) #spi_set_cs is_high= 0 ... Done #spi_transfer length= 1. Done #spi_transfer length= 1. Done #spi_set_cs is_high= 1 ... Done Read after PP #spi_set_cs is_high= 0 ... Done #spi_transfer length= 1. Done #spi_transfer length= 3... Done #spi_transfer length= 256..(省略).. Done #spi_set_cs is_high= 1 ... Done +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +A +B +C +D +E +F ----------------------------------------------- 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F 20 21 22 23 24 25 26 27 28 29 2A 2B 2C 2D 2E 2F 30 31 32 33 34 35 36 37 38 39 3A 3B 3C 3D 3E 3F 40 41 42 43 44 45 46 47 48 49 4A 4B 4C 4D 4E 4F 50 51 52 53 54 55 56 57 58 59 5A 5B 5C 5D 5E 5F 60 61 62 63 64 65 66 67 68 69 6A 6B 6C 6D 6E 6F 70 71 72 73 74 75 76 77 78 79 7A 7B 7C 7D 7E 7F 80 81 82 83 84 85 86 87 88 89 8A 8B 8C 8D 8E 8F 90 91 92 93 94 95 96 97 98 99 9A 9B 9C 9D 9E 9F A0 A1 A2 A3 A4 A5 A6 A7 A8 A9 AA AB AC AD AE AF B0 B1 B2 B3 B4 B5 B6 B7 B8 B9 BA BB BC BD BE BF C0 C1 C2 C3 C4 C5 C6 C7 C8 C9 CA CB CC CD CE CF D0 D1 D2 D3 D4 D5 D6 D7 D8 D9 DA DB DC DD DE DF E0 E1 E2 E3 E4 E5 E6 E7 E8 E9 EA EB EC ED EE EF F0 F1 F2 F3 F4 F5 F6 F7 F8 F9 FA FB FC FD FE FF (書き込み後のデータ、正しくインクリメンタルな値になった)
ログ中に注釈したとおり、Erase後にffになったデータがPPで書き込めていることが確認できます。
全面書き換えを行う
動作確認が出来たので、全面書き換えをしてみます。
Page Programはページごとに処理して、WIP待ちをしなければならないので0x100刻みに適当なデータを作って処理します。 ページをまたがなければ分ける必要はないので、今回は考慮していませんが0x3から100byteだけ書き換えたりなどは大丈夫です。
検証コード
uint8_t tx_buf[256]; uint8_t rx_buf[256]; uint32_t length = 0x400000; /* 全面書き換え */ flash_wren(); Serial.print("Bulk Erase ..."); flash_be(); while(flash_is_wip()){ //Serial.print("."); } Serial.print(" Done\n"); //1Pageごと(256byteごと)処理をする for(uint32_t addr = 0x0 ; addr < length ; addr += 0x100){ Serial.print("PP @"); Serial.print(addr, HEX); //適当なデータを作る for(uint16_t i = 0 ; i < 0x100 ; ++i){ tx_buf[i] = (i + (addr >> 8)) & 0xff; } flash_wren(); flash_pp(addr, tx_buf, 0x100); while(flash_is_wip()){//書き込み完了待ち Serial.print("."); } Serial.print(" Done\n"); } //読み出して照合する bool is_all_match = 0x1; for(uint32_t addr = 0x0 ; addr < length ; addr += 0x100){ Serial.print("Verify @"); Serial.print(addr, HEX); flash_read(addr, rx_buf, 0x100); //結果を比較 bool is_match = 0x1; for(uint16_t i = 0 ; i < 0x100 ; ++i){ tx_buf[i] = (i + (addr >> 8)) & 0xff; if(tx_buf[i] != rx_buf[i]) { is_match = 0x0; is_all_match = 0x0; Serial.print(" Fail\n"); break; } } if(is_match){ Serial.print(" Pass\n"); } } //先頭だけでも見ておく Serial.print("\n\n\n@0x000000\n"); flash_read(0x0, rx_buf, 0x100); data_debug_print(rx_buf, 0x100); if(is_all_match){ Serial.print("Pass: spi_flash_test2()\n"); } else { Serial.print("Fail: spi_flash_test2()\n"); }
結果
(めちゃくちゃ長いため省略) Verify @3FF800 Pass Verify @3FF900 Pass Verify @3FFA00 Pass Verify @3FFB00 Pass Verify @3FFC00 Pass Verify @3FFD00 Pass Verify @3FFE00 Pass Verify @3FFF00 Pass @0x000000 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +A +B +C +D +E +F ----------------------------------------------- 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F 20 21 22 23 24 25 26 27 28 29 2A 2B 2C 2D 2E 2F 30 31 32 33 34 35 36 37 38 39 3A 3B 3C 3D 3E 3F 40 41 42 43 44 45 46 47 48 49 4A 4B 4C 4D 4E 4F 50 51 52 53 54 55 56 57 58 59 5A 5B 5C 5D 5E 5F 60 61 62 63 64 65 66 67 68 69 6A 6B 6C 6D 6E 6F 70 71 72 73 74 75 76 77 78 79 7A 7B 7C 7D 7E 7F 80 81 82 83 84 85 86 87 88 89 8A 8B 8C 8D 8E 8F 90 91 92 93 94 95 96 97 98 99 9A 9B 9C 9D 9E 9F A0 A1 A2 A3 A4 A5 A6 A7 A8 A9 AA AB AC AD AE AF B0 B1 B2 B3 B4 B5 B6 B7 B8 B9 BA BB BC BD BE BF C0 C1 C2 C3 C4 C5 C6 C7 C8 C9 CA CB CC CD CE CF D0 D1 D2 D3 D4 D5 D6 D7 D8 D9 DA DB DC DD DE DF E0 E1 E2 E3 E4 E5 E6 E7 E8 E9 EA EB EC ED EE EF F0 F1 F2 F3 F4 F5 F6 F7 F8 F9 FA FB FC FD FE FF Pass: spi_flash_test2()
動画
ArduinoからSPI Flashメモリを書き換えています。 pic.twitter.com/0UvuXtIa62
— LogiClover (@logiclover_jp) 2017年8月14日
どうやら全データ書き換えられているようです。
終わりに
コード全体は以下に示します。自己責任でお願い致します。
間違い等ございましたらご指摘いただけると幸いです。
#include <SPI.h> #define FLASH_CS 10 ///////////////////////////////////////////////////////////////////////////// //デバイスによって書き換える void spi_init(){ SPI.begin(); pinMode(FLASH_CS, OUTPUT); spi_set_cs(0x1); } void spi_set_cs(uint8_t is_high) { //Serial.print("#spi_set_cs is_high= "); //Serial.print(is_high, DEC); digitalWrite(FLASH_CS, is_high & 0x1); //Serial.print(" ... Done\n"); } void spi_transfer(const uint8_t* tx_buf, uint8_t* rx_buf, uint16_t length){ //Serial.print("#spi_transfer length= "); //Serial.print(length, DEC); for(uint16_t i = 0 ; i < length ; ++i){ //Serial.print("."); uint8_t data = SPI.transfer(tx_buf != NULL ? tx_buf[i] : 0x0); if(rx_buf != NULL){ rx_buf[i] = data; } } //Serial.print(" Done\n"); } ///////////////////////////////////////////////////////////////////////////// //Flashの制御 void flash_send_cmd(uint8_t cmd){ spi_transfer(&cmd, NULL, 1); } void flash_send_addr(uint32_t addr){ const uint8_t data[] = { (addr >> 16) & 0xff, (addr >> 8) & 0xff, (addr >> 0) & 0xff, }; spi_transfer(data, NULL, 3); } void flash_send_dummy(uint16_t length){ spi_transfer(NULL, NULL, length); } void flash_read_data(uint8_t* rx_buf, uint16_t length){ spi_transfer(NULL, rx_buf, length); } void flash_write_data(const uint8_t* tx_buf, uint16_t length){ spi_transfer(tx_buf, NULL, length); } ///////////////////////////////////////////////////////////////////////////// //Flashのコマンドを発行(S25FL032) #define CMD_RDID (0x9f) #define CMD_READ (0x03) #define CMD_WREN (0x06) #define CMD_WRDI (0x04) #define CMD_P4E (0x20) #define CMD_P8E (0x40) #define CMD_BE (0x60) #define CMD_PP (0x02) #define CMD_RDSR (0x05) void flash_rdid(uint8_t* rx_buf, uint16_t length){ spi_set_cs(0x0); flash_send_cmd(CMD_RDID); flash_read_data(rx_buf, length); spi_set_cs(0x1); } void flash_read(uint32_t addr, uint8_t* rx_buf, uint16_t length){ spi_set_cs(0x0); flash_send_cmd(CMD_READ); flash_send_addr(addr); flash_read_data(rx_buf, length); spi_set_cs(0x1); } void flash_wren(){ spi_set_cs(0x0); flash_send_cmd(CMD_WREN); spi_set_cs(0x1); } void flash_wrdi(){ spi_set_cs(0x0); flash_send_cmd(CMD_WRDI); spi_set_cs(0x1); } void flash_p4e(uint32_t addr){ spi_set_cs(0x0); flash_send_cmd(CMD_P4E); flash_send_addr(addr); spi_set_cs(0x1); } void flash_be(){ spi_set_cs(0x0); flash_send_cmd(CMD_BE); spi_set_cs(0x1); } void flash_pp(uint32_t addr, const uint8_t* wr_data, uint16_t length){ spi_set_cs(0x0); flash_send_cmd(CMD_PP); flash_send_addr(addr); flash_write_data(wr_data, length); spi_set_cs(0x1); } void flash_rdsr(uint8_t* rx_buf, uint16_t length){ spi_set_cs(0x0); flash_send_cmd(CMD_RDSR); flash_read_data(rx_buf, length); spi_set_cs(0x1); } ///////////////////////////////////////////////////////////////////////////// //その他 //(WIP)Write In Progressかどうか確認 uint8_t flash_is_wip(){ uint8_t data; flash_rdsr(&data, 0x1); return (data & 0x1);//WIP } //配列を表示 void data_debug_print(const uint8_t* data, uint16_t bytes){ Serial.print("+0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +A +B +C +D +E +F\n"); Serial.print("-----------------------------------------------\n"); uint16_t depth = ((bytes - 1) >> 4) + 1; for(uint16_t j = 0 ; j < depth ; ++j){ for(uint16_t i = 0 ; i < 16 ; ++i){ uint8_t d = data[i + (j << 4)]; Serial.print((d >> 4) & 0x0f, HEX); Serial.print((d >> 0) & 0x0f, HEX); Serial.print(" "); } Serial.print("\n"); } } ///////////////////////////////////////////////////////////////////////////// //Arduinoの適当な制御コード void spi_flash_test(){ uint8_t tx_buf[256]; uint8_t rx_buf[256]; uint32_t addr = 0x0; uint16_t length = 0x100; Serial.print("RDID\n"); flash_rdid(rx_buf, 4); data_debug_print(rx_buf, 4); Serial.print("Read\n"); flash_read(addr, rx_buf, length); data_debug_print(rx_buf, length); Serial.print("Read after P4E\n"); flash_wren();//P4Eの前に必要 flash_p4e(addr); while(flash_is_wip()){//消去待ち Serial.print("."); } Serial.print("\n"); flash_read(addr, rx_buf, length); data_debug_print(rx_buf, length); Serial.print("PP Sequential Data\n"); for(uint16_t i = 0 ; i < length ; ++i){ tx_buf[i] = i & 0xff; } flash_wren(); flash_pp(addr, tx_buf, length); while(flash_is_wip()){//書き込み完了待ち Serial.print("."); } Serial.print("\n"); Serial.print("Read after PP\n"); flash_read(addr, rx_buf, length); data_debug_print(rx_buf, length); } void spi_flash_test2(){ uint8_t tx_buf[256]; uint8_t rx_buf[256]; uint32_t length = 0x400000; /* 全面書き換え */ flash_wren(); Serial.print("Bulk Erase ..."); flash_be(); while(flash_is_wip()){ //Serial.print("."); } Serial.print(" Done\n"); //1Pageごと(256byteごと)処理をする for(uint32_t addr = 0x0 ; addr < length ; addr += 0x100){ Serial.print("PP @"); Serial.print(addr, HEX); //適当なデータを作る for(uint16_t i = 0 ; i < 0x100 ; ++i){ tx_buf[i] = (i + (addr >> 8)) & 0xff; } flash_wren(); flash_pp(addr, tx_buf, 0x100); while(flash_is_wip()){//書き込み完了待ち Serial.print("."); } Serial.print(" Done\n"); } //読み出して照合する bool is_all_match = 0x1; for(uint32_t addr = 0x0 ; addr < length ; addr += 0x100){ Serial.print("Verify @"); Serial.print(addr, HEX); flash_read(addr, rx_buf, 0x100); //結果を比較 bool is_match = 0x1; for(uint16_t i = 0 ; i < 0x100 ; ++i){ tx_buf[i] = (i + (addr >> 8)) & 0xff; if(tx_buf[i] != rx_buf[i]) { is_match = 0x0; is_all_match = 0x0; Serial.print(" Fail\n"); break; } } if(is_match){ Serial.print(" Pass\n"); } } //先頭だけでも見ておく Serial.print("\n\n\n@0x000000\n"); flash_read(0x0, rx_buf, 0x100); data_debug_print(rx_buf, 0x100); if(is_all_match){ Serial.print("Pass: spi_flash_test2()\n"); } else { Serial.print("Fail: spi_flash_test2()\n"); } } void setup() { spi_init(); Serial.begin(9600); while (!Serial) {} spi_flash_test2(); } void loop() { }
*1:2017年8月現在 新規設計非推奨となっていてS25FL-Lシリーズが推奨されています
*2:https://www.arduino.cc/en/Guide/ArduinoLeonardoMicro#toc6
CP2130でC#からUSB-SPIを実現する
FPGAやマイコンでアプリケーションを作成しているときに悩ましい項目としてPCとの通信方法があります。
低速なものであればFT232に代表されるようなUSB-UARTでいいのですが、UARTはRTLと相性がよくありません。
項目 | 利点 | 欠点 |
---|---|---|
USB-UART | 手軽 | 遅い |
USB-I2C | I2C I/Fのデバイスを直接制御できる、マルチスレーブ対応 | PC側でアービトレーションを実装する必要があり |
USB-SPI | SPI I/Fのデバイスを直接制御できる、ある程度速く出来る、実装が簡単 |
FPGAで実装するにはSPIを使うとかなり簡単にアクセスできそうです。今回はSilicon Laboratory社が販売しているUSB-SPI Bridge IC CP2130を使ってみます。このICは最大12MHzでUSB-SPIを実現することが出来ます。
余談ですが、もっと高速なUSB通信を行う場合Cypress社のEZ-USB FXに代表されるような製品が使われます。
http://japan.cypress.com/products/ez-usb-fx3-superspeed-usb-30-peripheral-controller
評価ツールをダウンロード
評価ボードのダウンロード先がどういうわけかCP210xの別製品のものになっているので*1、サポートページからダウンロードします。
CP2130 Classic USB Bridge | Silicon Labs
上記サイト下側にある「CP2130 Software package for Windows」をダウンロードしてインストールします。Linuxの場合はfor Linuxを使用します。
サンプルアプリケーションを眺める
以後Windowsで解説します。インストールが完了すると “C:\SiliconLabs\MCU\CP2130_SDK\Software"にライブラリとサンプルアプリケーションが追加されています。
CP2130 Demo ApplicationがC#+WPFで記述されたソースコード付きのサンプルになっています。
細かな制御の方法や作例はこちらを眺めると良いでしょう。
動作させてみる
サンプルアプリケーションを読んでみると、SLAB_USB_SPI.csがDllImport経由でSLAB_USB_SPI.dllの関数を呼び出していることがわかります。
このままでは扱いづらいので転送だけ出来るWrapperを作成します。
CSをGPIO経由で手動で上げ下げすることも考慮してIsCSEnableとGPIO制御もつけておきます。
using SLAB_USB_SPI_DLL; using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Text; using System.Threading.Tasks; namespace LogiClover.Lib { class CP2130SPI : IDisposable { private IntPtr deviceHandle = IntPtr.Zero; public bool IsAvailable => deviceHandle != IntPtr.Zero; #region Static Method /// <summary> /// 現在接続されているデバイス数を返します /// </summary> /// <returns></returns> public static int GetNumDevices() { uint n = 0; SLAB_USB_SPI.CP213x_GetNumDevices(ref n); return (int)n; } /// <summary> /// デバイス名のリストを返します /// </summary> /// <returns></returns> public static IEnumerable<string> GetDeviceNames() { var n = GetNumDevices(); for (uint i = 0; i < n; ++i) { var sb = new StringBuilder(SLAB_USB_SPI.MAX_PATH); SLAB_USB_SPI.CP213x_GetDevicePath(i, sb); yield return sb.ToString(); } } #endregion #region Device Connection /// <summary> /// デバイスと接続します /// </summary> /// <param name="index"></param> /// <returns>正常に接続できた場合true</returns> public bool Open(int index) { if (index < 0) return false; this.Close(); var result = SLAB_USB_SPI.CP213x_OpenByIndex((uint)index, ref deviceHandle); if (result == SLAB_USB_SPI.USB_SPI_ERRCODE_SUCCESS) { return true; } else { Debug.WriteLine($"#Exception Errorcode:{result}"); Close(); return false; } } /// <summary> /// デバイスを切断します /// </summary> public void Close() { if (IsAvailable) { SLAB_USB_SPI.CP213x_Close(deviceHandle); deviceHandle = IntPtr.Zero; } } #endregion #region Parameters public enum ClockRate { F12M = SLAB_USB_SPI.SPICTL_CLKRATE_12M, F6M = SLAB_USB_SPI.SPICTL_CLKRATE_6M, F3M = SLAB_USB_SPI.SPICTL_CLKRATE_3M, F1M5 = SLAB_USB_SPI.SPICTL_CLKRATE_1M5, F750K = SLAB_USB_SPI.SPICTL_CLKRATE_750K, F375K = SLAB_USB_SPI.SPICTL_CLKRATE_375K, F187K5 = SLAB_USB_SPI.SPICTL_CLKRATE_187K5, F93K75 = SLAB_USB_SPI.SPICTL_CLKRATE_93K75, }; /// <summary> /// 転送周波数 /// </summary> public ClockRate SCKRate { get; set; } = ClockRate.F12M; /// <summary> /// CS番号 /// </summary> public int CSIndex { get; set; } = 0; /// <summary> /// CSを駆動するか /// </summary> public bool IsCSEnable { get; set; } = true; /// <summary> /// タイムアウト時間[ms] /// </summary> public uint TimeOutMS { get; set; } = 1000; #endregion public bool Configuration() { if (!IsAvailable) return false; if (IsCSEnable) { SLAB_USB_SPI.CP213x_SetChipSelect(deviceHandle, (byte)CSIndex, SLAB_USB_SPI.CSMODE_ACTIVE_OTHERS_IDLE); } else { //CS無効化 SLAB_USB_SPI.CP213x_SetChipSelect(deviceHandle, (byte)CSIndex, SLAB_USB_SPI.CSMODE_IDLE); } return true; } /// <summary> /// SPI通信を行います /// </summary> /// <param name="srcData">転送したいデータ、受信だけ行う場合はダミーデータを渡す</param> /// <returns>受信したデータ、データ長はsrcDataと同じ</returns> public byte[] Transfer(params byte[] srcData) { if (!IsAvailable) return new byte[] { }; uint bytesTransferred = 0; var dstData = new byte[srcData.Length]; SLAB_USB_SPI.CP213x_TransferWriteRead( deviceHandle, srcData, dstData, (uint)srcData.Length, true, TimeOutMS, ref bytesTransferred); return dstData; } /// <summary> /// GPIOに値を書き込みます /// </summary> /// <param name="index">制御するGPIO番号</param> /// <param name="value"></param> public void WriteIO(int index, bool value) { if (IsAvailable) { SLAB_USB_SPI.CP213x_SetGpioModeAndLevel(deviceHandle, (byte)index, SLAB_USB_SPI.GPIO_MODE_OUTPUT_PP, (byte)(value ? 0x1 : 0x0)); } } /// <summary> /// GPIOの値を読み込みます /// </summary> /// <param name="index"></param> /// <returns></returns> public bool? ReadIO(int index) { if (!IsAvailable) return null; byte mode = 0; byte level = 0; SLAB_USB_SPI.CP213x_GetGpioModeAndLevel(deviceHandle, (byte)index, ref mode, ref level); return level != 0x0; } public void Dispose() { this.Close(); } } }
あとはこのクラスの利用例ですが
ユーザーにデバイスを選ばせる
コンボボックスなどで表示しておくとわかりやすいかもしれません。
var numofDevice = CP2130SPI.GetNumDevices();//デバイス数を取得 var deviceNames = CP2130SPI.GetDeviceNames();//デバイス名一覧を取得
デバイスと通信を行う
開いて保持したままにしてもいいのですが、常時SPIを送り続けるアプリケーションではないので今回はお行儀良く送ったらCloseする制御にします。
//送る int index = 0;//通信に使うデバイスインデックス using (var spiBridge = new CP2130SPI()) { try { if (!spiBridge.Open(index)) { //デバイスオープンに失敗 return; } } catch (Exception ex) { //ほかの例外 return; } //SPIの設定 spiBridge.CSIndex = 0x0;//CSとして駆動させるピン番号 spiBridge.IsCSEnable = true;//CSは転送時に自動駆動させる spiBridge.SCKRate = CP2130SPI.ClockRate.F375K;//転送周波数 spiBridge.TimeOutMS = 1000;//タイムアウト時間 spiBridge.Configuration(); //適当なデータを作る var sendData = Enumerable.Range(0, 0xff).ToArray(); //データ転送 var recvData = spiBridge.Transfer(sendData);//byte[]が返ってくる }
動作させる
SLAB_USB_SPI.dllが実行ディレクトリに無いとDllImportに失敗するのでプロジェクトに入れて、プロパティを出力ディレクトリに新しい場合はコピーするようにします。
スクリーンショットはComet*2に使われているエフェクトの検証ツールで使用している例です。
終わりに
これでc#から直接SPIデバイスを制御できるようになりました!最高ですね!
SPI FlashやSPI DAC/ADCなどを繋いでも良いですし、FPGAの自作ペリフェラル検証に使用してもいいと思います。
*1:2017年6月12日現在
KiCadでピン数の多い回路シンボルを作成する
KiCadで回路を引くときに厄介な作業として回路シンボルの作成があります。
近頃はライブラリのOSS化が進んでいますが、自分が使うデバイスがない場合自作する必要があります。しかしピン数が100や200を超えてくると自力でやるには限度があります。
シンボルの中身はテキストなので自前で生成してもいいですが、今回はQuick KICAD Library Component Builder(以下コンポーネントビルダー)を使って作成します。
例としてXilinx社のXC7Z015-1CLG485という22×22のBGAのシンボルを作成してみます。
ピンリストの入手
多くのメーカはバウンダリスキャン用のBSDLファイルやピン配置のcsvやテキストファイルを公開しています。
Xilinx社Zynqの場合、UG865に記載があります。
https://japan.xilinx.com/support/documentation/user_guides/j_ug865-Zynq-7000-Pkg-Pinout.pdf
Intel(Altera)の場合、各デバイスごとにまとめてあります
自分が使いたいデバイスのメーカサイトを探して、テキスト形式のピン配置ファイルを入手します。
ピンリストの加工
あとで説明しますが、コンポーネントビルダーはスペース区切りでピン番号、ピン名が並んだファイルをインポートできるので加工します。
スペースが複数あったり、後ろに余計な文字がくっついているとうまくピン名がインポートできませんでした。
編集前 xc7z015clg485pkg.txt
Device/Package xc7z015clg485 10/16/2013 16:15:27 Pin Pin Name Memory Byte Group Bank VCCAUX Group Super Logic Region I/O Type No-Connect T10 DONE_0 NA 0 NA NA CONFIG NA N12 DXP_0 NA 0 NA NA CONFIG NA K11 GNDADC_0 NA 0 NA NA CONFIG NA ...
編集後
T10 DONE_0 N12 DXP_0 K11 GNDADC_0 ...
やる作業としては
余計な行を削除する(#がコメントアウトとして扱われるような記載があるような気がしますが試していません)
行の後ろ側の余計な情報を削除する
ピン番号とピン名をスペース1つ区切りにする(余計なスペースの削除、タブやカンマ区切りの場合は置換)
マルチカーソルや矩形選択のような機能を有するテキストエディタか、Excelで編集したものをCSVで出して置換を書けるといいと思います。*1
コンポーネントビルダーを使う
まずはサイトにアクセスします。
http://kicad.rohrbacher.net/quicklib.php
デバイス名とシンボルの形状を選びます。今回はBGAなので一番右下にグリッドの大きさを入力しています。
準備が出来たらAssign Pinsをクリックしてピン一覧編集に移ります。
ピン数が少なければ手入力でもいいですがなかなか骨が折れる作業ですので、ページ一番下にあるImport pin listをクリックします。
ここで先ほど作成したピンリストを読み込ませると
すべてのピン名とピン名から推論した属性まで設定してくれます。最高です。
あとはページ一番下のBuild Library Componentをクリックすると完成した.libがダウンロードできるのでKiCadで読み込みます。
成功です。これでピンがたくさんあっても怖くありません。
マルチエフェクター製作
以前FPGA基板を使って作ったマルチエフェクターを、1枚の基板にしました。
名前はComet Audio Effectorです。
画面はコーラスの調整画面です。
エフェクトはFPGAでデジタル音声処理をして作っています。
AD変換した音声をFPGAで音声処理し、DA変換して出力します。
イコライザー、ディレイ、コーラス、ディストーションの4種類を実装しています。
内部構成
中身は5枚の基板で構成しています。
基板はすべてKiCADを使って自分たちで設計しています。
ベースボード、LCD制御部、UI部に分かれています。
液晶はパラレルI/F、ボタンやLCDはI2Cで接続しています。
画面の表示や操作子の制御にはAtmelのARMマイコンを使っています。
画面
各エフェクトの画面はこんな感じ。
動作
音楽をかけて簡単に動作テストしてみました。
Comet Audio Effector 4種類のエフェクト動作確認です。 pic.twitter.com/49Qv53yS5l
— LogiClover (@logiclover_jp) 2017年4月29日
まだまだFPGAの処理能力に余裕があるので、いろんなエフェクトを増やしていこうと思います。
SPI Flashの使い方
組み込み工作でデータを格納したいときはFlashメモリを使うと便利です。
Flashメモリと言っても様々な種類があり、ざっくり以下のような分類ができます。
各社*1開発を行っており、微妙な違いで様々な製品があります。 例外もありますが型番の多くがメーカー名+数字で構成されておりカッコ内の示してあります。
- NAND Flash
- Parallel (_29)
- Address/Data組み合わせのコマンド方式
- Parallel (_29)
- NOR Flash
- Parallel (_29)
- Address/Data組み合わせのコマンド方式
- Serial
- SPI/QSPI/DSPI (_25)
- I2C (_24)
- Parallel (_29)
大容量のNAND Flashはお馴染みのUSBメモリなど、本当に大容量なデータの格納に使われています。 中・小規模なその他のFlashはプログラムの格納(外付けの場合)やFPGAのコンフィグデータの格納、ログデータやリソースの格納などに使われています。
LogiCloverでは
などで使用しました。
FPGAのコンフィグROMをPCから書き換える実験。
今回はSPIで制御するSerial NOR Flash(以下SPI Flash)の最低限の使い方について、FPGAのコンフィグで使用しているMicron社M25P40を例に説明します。 用意されているコマンドの種類やニーモニックが異なるだけで、基本的な使い方は各社同じです。
パッケージ
殆どのSPI FlashはSOIC8かSOIC16のようです。(1.27mmピッチ)
ピンアサイン
SOIC8のピンアサインは以下のとおりです (M25P40は対応していませんが、QSPIも含めた解説もしておきます。)
ピン名 | 方向 | SPI時 | QSPI時 |
---|---|---|---|
S# | 入力 | チップセレクト(CS) | チップセレクト(CS) |
C | 入力 | クロック入力(SCK) | クロック入力(SCK) |
DQ0 | 双方向 | データ入力(MOSI) | データ線0 |
DQ1 | 双方向 | データ出力(MISO) | データ線1 |
W#/DQ2 | 双方向 | 書き込み保護(使わなければH固定) | データ線2 |
HOLD#/DQ3 | 双方向 | 通信一時停止(使わなければH固定) | データ線3 |
SPIで使うときはW#/HOLD#はHigh固定で問題ないです。 QSPI時は帯域を増やすためにこの2つの信号線も通信に使用します。
インターフェース
電圧 : 2.3V ~ 3.6V
デバイスによっては1.8Vなど低い電圧までサポートしているものがありますが、5Vはほぼサポートしていないと見ていいでしょう。
もしArduinoの5V I/Oなどから使用する場合は必ずレベル変換してください。
通信周波数 : コマンドと電源電圧による
これがSPI Flashの厄介なところで、殆どのデバイスがコマンドと電源電圧によって最大周波数が異なります。 多くの場合Read命令とRead Fast命令が別れています。
Read Fastはダミーサイクルを挿入する代わりに(コマンドについては後述)最大周波数を上げています。 最近のデバイスでは100MHz超えも当たり前になっています。
M25P40を3.3Vで使った場合、下の表のとおりになります。
条件 | 周波数[MHz] |
---|---|
Read | 33 |
Read Fast | 75 |
それ以外 | 75 |
通信フォーマット
Master側のペリフェラル設定については割愛します。特殊なことは必要ありません。
クロック極性
立ち上がりエッジでキャプチャできるようにしてあれば大丈夫です。
CPHA,CPOLが(0, 0)か(1, 1)
ビットオーダー
MSBファーストで転送します。([D7, D6, D5, D4 … D0]の順に送る。)
送り方
命令によってデータの付け方が異なります。殆どの場合、データシートにまとめた表がだいたいあります。
送る順としては命令 + アドレス + モード + ダミー + データになっています。*2
アドレス、モード、ダミーなどは命令によって省略されるのでデータシートを確認してください。また最近のデバイスではアドレスに32bit指定可能なもの、ダミーサイクル数が指定可能なものなどもあります。
命令のみの場合
Write Enable, Write Disable, Bulk Erase, Deep Power-down など
命令1byte
例えば書き込み有効化のWrite Enableは、0x06を1byte送れば実行できます。
ステータス読み出しの場合
Read Identification, Read Status Register, Release from Deep Power-downなど
命令1byte + 読み出すデータ(IO1に出力される)
Masterから送るデータは最初の1byteに命令、あとは適当なダミーデータを送り、IO1のデータを読み取れば大丈夫です。
データ書き込みの場合
Page Programなど
命令1byte + 読み出しアドレス3byte + 書き込むデータ
指定したアドレスから順にデータを書き込んでいきます。
注意すべき点としては読み出しが一度に全部読み出せるのに対し、書き込みはページ単位(256byte)ごとに処理する必要があります。
データ読み出しの場合
Read Data Bytes, Read Data Bytes at Higher Speed など
命令1byte + 読み出しアドレス3byte + 読み出されたデータ(IO1に出力される。)
読み出したいデータ分だけ転送バイト数を増やすだけで大丈夫です。
通信速度を上げたい場合、代わりにat Higher Speed命令を送り、アドレスの後1byteは適当なデータを送っておけば大丈夫です。
制御方法
あとはコマンドをどう使うかだけ確認します。ここまでくれば後は送るだけです。
データ読み出し
Read Data Bytesで読み取りたい長さ分のSPI転送を行えば大丈夫です。
アドレス指定が出来るので任意アドレスのデータを読み出せます。
データ消去
消去命令は全領域消去を行うBulk Eraseと、特定セクター*3の消去を行うSector Eraseがあります。
Erase/Program系の命令他の命令と異なり別のコマンドと組み合わせが必要です。
- 実行前にWrite Enableを行う必要があります
- コマンド発行から完了まで時間がかかるためRead Status Registerでステータスを1byte読み出し、最下位ビットのWrite in Progress(WIP)が0になるまで待つ必要があります。
以下にフローを示します。
データ書き込み
PROGRAM命令を使って書き込み先を指定して書き込みます。
読み出しと異なりデータ長はページごと(M25P40の場合は256byteごと)に行います。
データ消去同様、Write Enableを事前に実行し書き込み後にWIPを待つ必要があります。
これは補足ですが一般的に書き込む前に事前にデータ消去を行います。
既にデータが書かれている場所にもう一度書き込んだ場合に正しく書き込めるかは保証されません。
終わりに
今回はSPI Flashの基本的な使い方についてまとめました。
PCからFlashやFPGAのコンフィグを行う実験のまとめになります。
後々需要があればツールは公開予定ですが、ご要望があれば優先度は考えます。
Arduinoでの制御例
2017/08/15 書きました、とりあえず動かしたい方はどうぞ。
※ Micron社 M25P40データシートより引用 https://www.micron.com/~/media/documents/products/data-sheet/nor-flash/serial-nor/m25p/m25p40.pdf
ISE Webpackでchipscopeが使えるようになっていたので試してみた
FPGA開発ツールには、実機でデバッグできるロジアナがついていることがありますが、Xilinxのオンチップロジアナ「Chipscope」はライセンスを購入しないと使用することができませんでした。
Xilinxの新しいツールのVivadoでは、ILAといわれるロジアナがフリーで使えるようになっています。
先日、ISEのライセンスを取り直したところ、WebpackのライセンスでChipscopeが使えるようにシレっと変更されていたため、試しに使ってみました。
Chipscopeの詳しい使い方はほかにも詳しいサイトがあるため、適宜割愛します。
1.プロジェクトにchipscopeを組み込むファイルを追加する。
ファイルマネージャーで右クリックし、New fileを選択します。
通常のIPを追加するときと同様にファイルを作っていきます。
特に必要な設定はありません。
次に、作成したchipscopeのファイルをダブルクリックすると、Core Inserterが起動します。
この画面で見たい信号を選択します。
この画面で見る信号を選択します。
詳細は割愛します。
設定をしたあと、ビットストリームファイルを生成します。
FPGAにbitファイルを書き込めば、FPGA内にロジアナが組み込まれます。
書き込んだ後、iMPACTを閉じないとJTAGが解放されないようなので注意しましょう。
書き込みが完了したら、Processesの一番下にある「Analyze Design Using Chipscope」を起動するとchipscopeが起動します。
このように、FPGA内の信号を見ることができます。
これはオーディオの波形を観測しています。
というわけでISE webpackでもchipscopeが使えるようになりました。
VivadoやAlteraのツールではすでに無料で使えるため、やっとという感じです。