ソースに絡まるエスカルゴ

貧弱プログラマの外部記憶装置です。

【Raspberry Pi Pico】Raspberry Pi PicoでIS25LP040Eを使う

 先日安くて容量の大きい不揮発性領域が欲しいと思い、フラッシュメモリー IS25LP040Eを購入しました。

 手元に届いたのでこの電子部品の使い方を調べていたのですが、日本語のページがほとんどヒットしませんでした。

 データシートとにらめっこしたり、ChatGPTに聞いたりしながらなんとかRaspberry Pi Picoで扱えるようになったので今回はその備忘録になります。


 では、始めます。


0:前提
 前提としてこの電子部品は表面実装用のものです。ブレッドボードでテストしたい場合は以下のような「SOP8(1.27mm)を2.54mmピッチに変換する基板」を購入する必要があります。


1:フラッシュメモリー IS25LP040Eについて
 詳しくはデータシートや先に挙げている商品ページを参照してほしいのですが、仕様をざっとまとめると以下になります。

項目 説明
接続方法 SPI(最大クロック104MHz)
サポート SPI Mode 0 または 3
電源電圧 2.3V~3.6V
記憶容量 4Mbit(約500kByte)
書込回数 100,000回以上
形状 SOP8(表面実装)

 ピン配置は以下のようになっています。(データシートより引用して加工)

 上から見て○があるピンが1番ピン(CE#)になります。


2:IS25LP040Eの使い方
 1でも書いていますが、SPI接続なのでSPI設定をする必要があります。

 Arduinoであれば、例えば以下のように設定します。

// 100MHz、MSBFIRST、SPI_MODE3
SPISettings spisettings(100000000, MSBFIRST, SPI_MODE3);

 ちなみに「MSBFIRST」にしか対応してないようなので、第二引数は必ずこの値を設定してください。
 第三引数は「SPI_MODE3」か「SPI_MODE0」を設定してください。
 第一引数は動作させたいクロックを設定します。

 このSPI設定のインスタンスを使用して、基本的には以下のようにしてIS25LP040Eにアクセスします。

SPI.beginTransaction(spisettings); // トランザクション開始
digitalWrite(CE_PIN, LOW);         // CEピンをLOWにして書き込み許可

SPI.transfer(COMMAND);             // コマンドを実行する

/*  データ取得などの処理を書く */

digitalWrite(CE_PIN, HIGH);        // CEピンをHIGHにして書き込み不可
delay(DELAY_TIME);                 // 必要に応じてdelayを入れる
SPI.endTransaction();              // トランザクション終了

 最初にトランザクションを開始し、CEピンを操作して書き込み許可にします。その後SPIで指定のコマンドを送信してIS25LP040Eのモードを切り替えます。そして処理が終わったらCSピンを戻してトランザクションを終了させるというのが一連の流れです。
 コマンドによってはコマンドを送信して一度トランザクションを終了させないと実行されないものもあるので注意してください。

 この説明だけではよくわからないと思うので、実際に回路を組んで動かしてみます。


3:Raspberry Pi PicoでIS25LP040Eをテストする回路
 Raspberry Pi Picoに繋いで確認する場合は、以下のように回路を作ります。

 接続方法がSPIなので、Raspberry Pi Picoの特定のピンしか使えないため注意してください。よくわからない場合は上記の回路そのままにしてください。


4:Arduinoのサンプルスケッチ
 使いやすいように2つのファイルに分けています。

・FlashMemory.h

/**
 * IS25LP040Eのピン接続
 *
 * CE   -> CS(SPIのCSピン)
 * SO   -> SPI RX
 * WP   -> 3.3V(常に書き込み許可するためHIGH)
 * GND  -> GND
 * Vcc  -> 3.3V
 * HOLD -> 3.3V(常にデバイス動作させるためHIGH)
 * SCK  -> SPI SCK
 * SI   -> SPI TX
 */
#include <SPI.h> // SPIライブラリ

// SPI通信の設定(MSBFIRSTかつ、SPI_MODE0かSPI_MODE3のみ使用可能)
// ここでは100MHzでの動作を指定
SPISettings _spisettings(100000000, MSBFIRST, SPI_MODE3);


class FlashMemory {
  public:
    FlashMemory();
    void setCsPin(int cs_pin);
    void setupSpi(int mosi_pin, int miso_pin, int sclk_pin);
    void readChipInfo();
    void writeData(uint32_t address, byte *data, size_t length);
    void writeOnePageData(uint32_t address, size_t start_index, byte *data, size_t length);
    void readData(uint32_t address, byte *data, size_t length);
    void allErase();

  private:
    int _cs_pin;

    void beginSpi();
    void endSpi(int delay_time);
};

// コンストラクタ
FlashMemory::FlashMemory() {
  // 何もしない
}

// CSピンの設定を行う関数
void FlashMemory::setCsPin(int cs_pin) {
  _cs_pin = cs_pin;
  pinMode(_cs_pin, OUTPUT);
  digitalWrite(_cs_pin, HIGH);
}

// SPIの設定
void FlashMemory::setupSpi(int mosi_pin, int miso_pin, int sclk_pin) {
  // SPI0チャンネルならSPI, SPI1チャンネルならSPI1を使う
  SPI.setTX(mosi_pin);
  SPI.setRX(miso_pin);
  SPI.setSCK(sclk_pin);
  SPI.begin(); // SPIを開始する
}

// IS25LP040Eの情報を取得する関数
void FlashMemory::readChipInfo() {
  beginSpi();

  // READ PRODUCT IDENTIFICATION BY JEDEC ID OPERATION(0x9F)
  SPI.transfer(0x9F);

  // IS25LP040Eの場合は以下の値が取得される
  byte manufacturerID = SPI.transfer(0x00); // 9D
  byte memoryType = SPI.transfer(0x00);     // 40
  byte capacity = SPI.transfer(0x00);       // 13

  endSpi(0);

  Serial.print("Manufacturer ID: 0x");
  Serial.println(manufacturerID, HEX);
  Serial.print("Memory Type: 0x");
  Serial.println(memoryType, HEX);
  Serial.print("Capacity: 0x");
  Serial.println(capacity, HEX);
}


// データを書き込む関数
void FlashMemory::writeData(uint32_t address, byte *data, size_t length) {
  uint32_t start_address = address;
  int start_index = address % 256;
  int remainder   = length % 256;
  int page_count  = length / 256;

  // Serial.print("start_index: ");
  // Serial.print(start_index);
  // Serial.print(", remainder: ");
  // Serial.print(remainder);
  // Serial.print(", page_count: ");
  // Serial.println(page_count);

  int data_index = 0;
  size_t diff = 256 - start_index;

  if (diff > length) {
    // 1行で収まる場合はそれだけ実行して終了
    writeOnePageData(start_address, data_index, data, length);
    return;
  }

  // 最初のページ分を書き込み
  writeOnePageData(start_address, data_index, data, diff);
  data_index = data_index + diff;
  start_address = start_address + diff;

  // 256で割り切れる分だけ書き込み
  for (int i = 0; i < page_count-1; i++) {
    writeOnePageData(start_address, data_index, data, 256);
    start_address = start_address + 256;
    data_index = data_index + 256;
  }

  // あまり分を書き込み
  if (remainder > 0 || start_index > 0) {
    writeOnePageData(start_address, data_index, data, remainder + start_index);
  }
}

// 実際に書き込み処理を行う関数
void FlashMemory::writeOnePageData(uint32_t address, size_t start_index, byte *data, size_t length) {
  // 書き込み許可
  beginSpi();
  SPI.transfer(0x06);
  endSpi(0);

  beginSpi();
  // PAGE PROGRAM OPERATION (0x02)
  SPI.transfer(0x02);
  // 書き込むアドレスを送る(3バイト)
  SPI.transfer((address >> 16) & 0xFF);
  SPI.transfer((address >> 8) & 0xFF);
  SPI.transfer(address & 0xFF);

  for (size_t i = 0; i < length; i++) {
    SPI.transfer(data[i+start_index]);
  }

  endSpi(100);
}

// データを読み込む関数
void FlashMemory::readData(uint32_t address, byte *data, size_t length) {
  beginSpi();

  // NORMAL READ OPERATION (0x03)
  SPI.transfer(0x03);
  // 読み込むアドレスを送る(3バイト)
  SPI.transfer((address >> 16) & 0xFF);
  SPI.transfer((address >> 8) & 0xFF);
  SPI.transfer(address & 0xFF);

  // データ読み込み実行
  for (size_t i = 0; i < length; i++) {
    data[i] = SPI.transfer(0x00);
  }

  endSpi(0);
}

// 全てのメモリ領域を消す関数
void FlashMemory::allErase() {
  // 書き込み許可
  beginSpi();
  SPI.transfer(0x06);
  endSpi(0);

  beginSpi();
  // CHIP ERASE (0xC7)
  SPI.transfer(0xC7);
  endSpi(2000);
}

// SPIトランザクションを開始する関数
void FlashMemory::beginSpi() {
  SPI.beginTransaction(_spisettings);
  digitalWrite(_cs_pin, LOW);// CSピンをLOWにする
}

// SPIトランザクションを終了する関数
void FlashMemory::endSpi(int delay_time) {
  digitalWrite(_cs_pin, HIGH);// CSピンをHIGHにする
  delay(delay_time);
  SPI.endTransaction();
}


・pico_IS25LP040E_sample.ino

/**
 * IS25LP040Eのサンプル
 */
#include "FlashMemory.h"

// SPI通信用のピン定義
#define CS_PIN       5  // CSピン(Chip Select)
#define COMMON_MOSI 19  // MOSI(SDA)(SPI TX)
#define COMMON_SCLK 18  // Clock(SCL/SCK)(SPI SCK)
#define COMMON_MISO 16  // MISO(SPI RX)

// 読み込み開始アドレス(アドレス最大値は0x07FFFF)
const uint32_t READ_ADDRESS = 0x000000;
// 書き込み開始アドレス
const uint32_t WRITE_ADDRESS = 0x000010;

// 書き込むデータ
const byte WRITE_DATA[] = {
  0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F
};

// インスタンス作成
FlashMemory fm = FlashMemory();

void setup() {
  Serial.begin(115200);

  // ピン設定とSPI準備
  fm.setCsPin(CS_PIN);
  fm.setupSpi(COMMON_MOSI, COMMON_MISO, COMMON_SCLK);
  delay(4000);

  // チップ情報を取得して表示
  fm.readChipInfo();

  // 全データ削除(2秒かかる)
  fm.allErase();

  // 読み込み処理
  byte readData[sizeof(WRITE_DATA)+WRITE_ADDRESS];
  fm.readData(READ_ADDRESS, readData, sizeof(readData));
  showReadData(readData, sizeof(readData));

  // 書き込む配列をコピー(constから変数にコピー)
  byte dataToWrite[sizeof(WRITE_DATA)];
  memcpy(dataToWrite, WRITE_DATA, sizeof(WRITE_DATA));

  // 書き込み処理
  fm.writeData(WRITE_ADDRESS, dataToWrite, sizeof(dataToWrite));
  // delay(1000); // 呼び出し関数内である程度delayを入れているが、必要な場合は更に適切なdelayを入れる

  // 再度読み込み処理
  fm.readData(READ_ADDRESS, readData, sizeof(readData));
  showReadData(readData, sizeof(readData));
}


void loop() {
  // 無限ループ
  delay(10000);
}


// 配列の中身を16進数で表示させる関数
void showReadData(byte *data, size_t length) {
  Serial.print("read data: ");
  for (int i = 0; i < length; i++) {
    Serial.print(data[i], HEX);
    Serial.print(" ");
  }
  Serial.println("");
}

 上記のサンプルをRaspberry Pi Picoに書き込んで実行すると、シリアルモニタに以下のように表示されます。

17:11:45.334 -> Manufacturer ID: 0x9D
17:11:45.334 -> Memory Type: 0x40
17:11:45.334 -> Capacity: 0x13
17:11:47.324 -> read data: FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF 
17:11:47.408 -> read data: FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF 0 1 2 3 4 5 6 7 8 9 A B C D E F 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F 

 ソースコードの中身を見ればわかるとは思いますが、少し処理について解説します。

 SPIのピン設定をしてSPIを開始した後、「0x9F」のコマンドを送信してIS25LP040Eのチップ情報を取得しています。
 その後「0xC7」コマンドを送信して全てのデータ消去(全てFFにする)を実行しています。この処理には2秒程度かかるので、呼び出し先の関数で2秒delayを入れています。
 全データ消去後「0x03」コマンドと読み込み開始位置のアドレスを送信して、保存されているデータの読み込みを行っています。
 読み込み後「0x02」コマンドと書き込み開始位置のアドレスを送信して、データ書き込みを行っています
 また書き込みや消去を行う場合は指定コマンド実行前に「0x06」の書き込み許可を行うコマンドを送信しています。

 このサンプルでは必要最低限のSPIコマンドを使えるようにしかしてないですが、この他にも色々とSPIコマンドがあるようなので詳しくはデータシートを参照してください。


 以上がRaspberry Pi PicoでIS25LP040Eを使う方法になります。

 あまりネットに情報がなく色々やってなんとか使えるようになっただけなので、バグや間違っているところがあるかと思います。その場合は指摘して頂けると幸いです。


・参考資料