LogiClover開発ブログ

LogiCloverは、趣味でFPGAを使った電子工作をしているサークルです。主に開発中の出来事や技術メモを投稿します

Microblaze MCSをSpartan6で動かす(ISE)

FPGAを使っていると、ソフトウェアCPUを組み込みたくなることがあります。
CPUを組み込むことで複雑な分岐の処理などを楽に作ることができます。

今回はSpartan-6 FPGAMicroblaze MCSを実装する方法を説明します。
前提条件としてFPGAでLチカができる程度の基礎知識はあるものとします。

まずISEを開き、プロジェクトを作ったら、新規IPをプロジェクトに追加します。
f:id:logiclover:20170823232641p:plain

IPの選択でMicroblaze MCSのコアを選択します。
f:id:logiclover:20170823232650p:plain

IPが作成出来たらIPの設定画面が開きます。
ここではMicroblazeの設定をします。RAM容量や使用するペリフェラルなどを選択することができます。
注意する点としては、インスタンスネームの指定では実際にインスタンシエートするモジュールのパスを入力する点です。
例えば、topの下にcpu0という名前でmicroblazeインスタンス化する場合は、Instance Hierarcical Design Nameの欄にcpu0と入力します。
f:id:logiclover:20170823232654p:plain

次にペリフェラルの設定をします。
今回はLED点灯を試すため、GPOを使用する設定にします。
GPO1に4bitの出力ポートを作っておきます。
f:id:logiclover:20170823232429p:plain

OKを押してIPの設定を終了します。
次にMicroblazeインスタンス化します。
topモジュールを適当に作り、その下にMicroblazeインスタンス化します。
メニューのView Instantiation Templateからテンプレートを取得できます。
f:id:logiclover:20170823232519p:plain

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から設定して表示させます。
f:id:logiclover:20170823232549p:plain

次にtclコンソールで下記のコマンドを実行します。

source ipcore_dir/microblaze_mcs_setup.tcl

なお、このtclスクリプトはデフォルトではipcore_dirディレクトリの中にあります。
注意としては、カレントディレクトリがプロジェクトルートの状態でtclファイルを指定しないとTranslateでコケます。
f:id:logiclover:20170823232558p:plain

ここまでやったら通常と同様にbitファイル作成まで行います。
これでハードウェアの生成は完了です。

SDKでソフトウェアを作成する

次にソフトウェアを作成します。
tclコンソールでxsdkと入力しSDKを起動します。

BSPの作成

起動したらまずはBSPを作成します。
メニューからCreate BSP Projectを選択します。

ipcore_dirの中にハードウェア情報が入ったxmlファイルがあるため、hardware specificationに指定します。
BMMファイルも同じフォルダに入っているため、同様に指定します。
f:id:logiclover:20170823232604p:plain

OSはスタンドアロンを選択します。
f:id:logiclover:20170823232610p:plain

ここまでの手順でボードサポート部までは完成です。

アプリケーションプロジェクトの作成

次にアプリケーションプロジェクトを作成します。
基本デフォルトで良いですが、以前作ったBSPを選択するようにします。
f:id:logiclover:20170823232616p:plain

コーディングする

実際に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;
}

デバッグする

microblazeは通常の組み込みCPUと同様、デバッグを行うことができます。

デバッグをする前に、このままではFPGAがコンフィグされていないため、まずはFPGAをコンフィグします。
iMPACTでコンフィグしてもいいですが、SDKでやる場合はProgram FPGAメニューを使用します。
ただしソフトウェアのバイナリができていないと実行できないため、何かしらのアプリケーションプロジェクトをビルドしておく必要があります。
f:id:logiclover:20170823234340p:plain

書き込みができたらデバッグが可能となります。
最初にデバッグを押すとメニューが出るためGDBデバッグを選択します。
f:id:logiclover:20170823232625p:plain

あとは通常のEclipseデバッグと同様に使用できます。

SPI Flashの使い方 (2) - Arduinoから読み書き

組み込みでデータを保持したいときに使用するSPI Flashを実際にArduinoから制御してみます。

偶然手元にあった構成で、Arduino MicroとCypress製のSPI FlashであるS25FL032*1を使用します。

この記事は以下の記事の続きとなるので、部品の機能やコマンドについての詳細はこちらをご参照願います。

logiclover.hatenablog.jp

結線方法

基本的には前記事を参照していただければピン機能はわかると思います。

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()

動画

どうやら全データ書き換えられているようです。

終わりに

コード全体は以下に示します。自己責任でお願い致します。

間違い等ございましたらご指摘いただけると幸いです。

#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を実現することが出来ます。

jp.silabs.com

余談ですが、もっと高速な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に使われているエフェクトの検証ツールで使用している例です。

f:id:logiclover:20170612013548p:plain

終わりに

これでc#から直接SPIデバイスを制御できるようになりました!最高ですね!

SPI FlashやSPI DAC/ADCなどを繋いでも良いですし、FPGAの自作ペリフェラル検証に使用してもいいと思います。

KiCadでピン数の多い回路シンボルを作成する

KiCadで回路を引くときに厄介な作業として回路シンボルの作成があります。

近頃はライブラリのOSS化が進んでいますが、自分が使うデバイスがない場合自作する必要があります。しかしピン数が100や200を超えてくると自力でやるには限度があります。

シンボルの中身はテキストなので自前で生成してもいいですが、今回はQuick KICAD Library Component Builder(以下コンポーネントビルダー)を使って作成します。

kicad.rohrbacher.net

例として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

f:id:logiclover:20170501133405p:plain

Intel(Altera)の場合、各デバイスごとにまとめてあります

www.altera.co.jp

自分が使いたいデバイスのメーカサイトを探して、テキスト形式のピン配置ファイルを入手します。

ピンリストの加工

あとで説明しますが、コンポーネントビルダーはスペース区切りでピン番号、ピン名が並んだファイルをインポートできるので加工します。

スペースが複数あったり、後ろに余計な文字がくっついているとうまくピン名がインポートできませんでした。

編集前 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

f:id:logiclover:20170501134841p:plain

バイス名とシンボルの形状を選びます。今回はBGAなので一番右下にグリッドの大きさを入力しています。

準備が出来たらAssign Pinsをクリックしてピン一覧編集に移ります。

f:id:logiclover:20170501135046p:plain

ピン数が少なければ手入力でもいいですがなかなか骨が折れる作業ですので、ページ一番下にあるImport pin listをクリックします。

ここで先ほど作成したピンリストを読み込ませると

f:id:logiclover:20170501135217p:plain

f:id:logiclover:20170501135230p:plain

すべてのピン名とピン名から推論した属性まで設定してくれます。最高です。

あとはページ一番下のBuild Library Componentをクリックすると完成した.libがダウンロードできるのでKiCadで読み込みます。

f:id:logiclover:20170501135441p:plain

成功です。これでピンがたくさんあっても怖くありません。

*1:私は過去にMAX10のシンボルを作ったときはExcel、今回はVSCodeでやっています。

マルチエフェクター製作

以前FPGA基板を使って作ったマルチエフェクターを、1枚の基板にしました。
名前はComet Audio Effectorです。
f:id:logiclover:20170429154115j:plain
画面はコーラスの調整画面です。
エフェクトはFPGAでデジタル音声処理をして作っています。
AD変換した音声をFPGAで音声処理し、DA変換して出力します。
イコライザー、ディレイ、コーラス、ディストーションの4種類を実装しています。

内部構成

中身は5枚の基板で構成しています。
基板はすべてKiCADを使って自分たちで設計しています。

ベースボード、LCD制御部、UI部に分かれています。
液晶はパラレルI/F、ボタンやLCDはI2Cで接続しています。
画面の表示や操作子の制御にはAtmelのARMマイコンを使っています。
f:id:logiclover:20170429154226j:plain

画面

各エフェクトの画面はこんな感じ。
f:id:logiclover:20170429160824j:plain

動作

音楽をかけて簡単に動作テストしてみました。


まだまだFPGAの処理能力に余裕があるので、いろんなエフェクトを増やしていこうと思います。

SPI Flashの使い方

組み込み工作でデータを格納したいときはFlashメモリを使うと便利です。

Flashメモリと言っても様々な種類があり、ざっくり以下のような分類ができます。

各社*1開発を行っており、微妙な違いで様々な製品があります。 例外もありますが型番の多くがメーカー名+数字で構成されておりカッコ内の示してあります。

  • NAND Flash
    • Parallel (_29)
      • Address/Data組み合わせのコマンド方式
  • NOR Flash
    • Parallel (_29)
      • Address/Data組み合わせのコマンド方式
    • Serial
      • SPI/QSPI/DSPI (_25)
      • I2C (_24)

大容量のNAND Flashはお馴染みのUSBメモリなど、本当に大容量なデータの格納に使われています。 中・小規模なその他のFlashはプログラムの格納(外付けの場合)やFPGAのコンフィグデータの格納、ログデータやリソースの格納などに使われています。

LogiCloverでは

  • FPGA(Xilinx社 Spartan6)のコンフィグデータの格納
  • マイコン(NXP社 LPC4330)を使ったボードのプログラム格納先
  • SPDIF出力する音声データの格納先

などで使用しました。

f:id:logiclover:20170402023957j:plain

FPGAのコンフィグROMをPCから書き換える実験。

今回はSPIで制御するSerial NOR Flash(以下SPI Flash)の最低限の使い方について、FPGAのコンフィグで使用しているMicron社M25P40を例に説明します。 用意されているコマンドの種類やニーモニックが異なるだけで、基本的な使い方は各社同じです。

パッケージ

殆どのSPI FlashはSOIC8かSOIC16のようです。(1.27mmピッチ)

ピンアサイン

SOIC8のピンアサインは以下のとおりです (M25P40は対応していませんが、QSPIも含めた解説もしておきます。)

f:id:logiclover:20170402000153p:plain

ピン名 方向 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指定可能なもの、ダミーサイクル数が指定可能なものなどもあります。

f:id:logiclover:20170402010527p:plain

命令のみの場合

Write Enable, Write Disable, Bulk Erase, Deep Power-down など

命令1byte

例えば書き込み有効化のWrite Enableは、0x06を1byte送れば実行できます。

f:id:logiclover:20170402011235p:plain

ステータス読み出しの場合

Read Identification, Read Status Register, Release from Deep Power-downなど

命令1byte + 読み出すデータ(IO1に出力される)

Masterから送るデータは最初の1byteに命令、あとは適当なダミーデータを送り、IO1のデータを読み取れば大丈夫です。

f:id:logiclover:20170402023207p:plain

データ書き込みの場合

Page Programなど

命令1byte + 読み出しアドレス3byte + 書き込むデータ

指定したアドレスから順にデータを書き込んでいきます。

注意すべき点としては読み出しが一度に全部読み出せるのに対し、書き込みはページ単位(256byte)ごとに処理する必要があります。

f:id:logiclover:20170402012114p:plain

データ読み出しの場合

Read Data Bytes, Read Data Bytes at Higher Speed など

命令1byte + 読み出しアドレス3byte + 読み出されたデータ(IO1に出力される。)

読み出したいデータ分だけ転送バイト数を増やすだけで大丈夫です。

通信速度を上げたい場合、代わりにat Higher Speed命令を送り、アドレスの後1byteは適当なデータを送っておけば大丈夫です。

f:id:logiclover:20170402011553p:plain


制御方法

あとはコマンドをどう使うかだけ確認します。ここまでくれば後は送るだけです。

データ読み出し

Read Data Bytesで読み取りたい長さ分のSPI転送を行えば大丈夫です。

アドレス指定が出来るので任意アドレスのデータを読み出せます。

データ消去

消去命令は全領域消去を行うBulk Eraseと、特定セクター*3の消去を行うSector Eraseがあります。

Erase/Program系の命令他の命令と異なり別のコマンドと組み合わせが必要です。

  • 実行前にWrite Enableを行う必要があります
  • コマンド発行から完了まで時間がかかるためRead Status Registerでステータスを1byte読み出し、最下位ビットのWrite in Progress(WIP)が0になるまで待つ必要があります。

以下にフローを示します。

f:id:logiclover:20170402021200p:plain

データ書き込み

PROGRAM命令を使って書き込み先を指定して書き込みます。

読み出しと異なりデータ長はページごと(M25P40の場合は256byteごと)に行います。

データ消去同様、Write Enableを事前に実行し書き込み後にWIPを待つ必要があります。

これは補足ですが一般的に書き込む前に事前にデータ消去を行います。

既にデータが書かれている場所にもう一度書き込んだ場合に正しく書き込めるかは保証されません。


終わりに

今回はSPI Flashの基本的な使い方についてまとめました。

PCからFlashFPGAのコンフィグを行う実験のまとめになります。

後々需要があればツールは公開予定ですが、ご要望があれば優先度は考えます。

Arduinoでの制御例

2017/08/15 書きました、とりあえず動かしたい方はどうぞ。

logiclover.hatenablog.jp

※ Micron社 M25P40データシートより引用 https://www.micron.com/~/media/documents/products/data-sheet/nor-flash/serial-nor/m25p/m25p40.pdf

*1:2017年4月時点だとCypress/Micron/Macronix/Winbond/ESMT/Microchipなど

*2:モードはM25P40には実装されていない

*3:M25P40の場合 0x10000ごとセクターが別れている

ISE Webpackでchipscopeが使えるようになっていたので試してみた

FPGA開発ツールには、実機でデバッグできるロジアナがついていることがありますが、Xilinxのオンチップロジアナ「Chipscope」はライセンスを購入しないと使用することができませんでした。
Xilinxの新しいツールのVivadoでは、ILAといわれるロジアナがフリーで使えるようになっています。
先日、ISEのライセンスを取り直したところ、WebpackのライセンスでChipscopeが使えるようにシレっと変更されていたため、試しに使ってみました。

Chipscopeの詳しい使い方はほかにも詳しいサイトがあるため、適宜割愛します。

1.プロジェクトにchipscopeを組み込むファイルを追加する。
ファイルマネージャーで右クリックし、New fileを選択します。
f:id:logiclover:20170331220523p:plain

通常のIPを追加するときと同様にファイルを作っていきます。
特に必要な設定はありません。
f:id:logiclover:20170331220525p:plain

次に、作成したchipscopeのファイルをダブルクリックすると、Core Inserterが起動します。
この画面で見たい信号を選択します。
f:id:logiclover:20170331220528p:plain

この画面で見る信号を選択します。
詳細は割愛します。
f:id:logiclover:20170331220532p:plain

設定をしたあと、ビットストリームファイルを生成します。
FPGAにbitファイルを書き込めば、FPGA内にロジアナが組み込まれます。
書き込んだ後、iMPACTを閉じないとJTAGが解放されないようなので注意しましょう。

書き込みが完了したら、Processesの一番下にある「Analyze Design Using Chipscope」を起動するとchipscopeが起動します。
f:id:logiclover:20170331221349p:plain
このように、FPGA内の信号を見ることができます。
これはオーディオの波形を観測しています。

というわけでISE webpackでもchipscopeが使えるようになりました。
VivadoやAlteraのツールではすでに無料で使えるため、やっとという感じです。