STM32マイコンを用いたSPI通信とW25Q64フラッシュメモリ制御の実装解説

SPI通信の基礎

SPIはMotorolaが開発した同期式シリアル通信規格です。次の4本の信号線で構成されます:

  • SCK(シリアルクロック)
  • MOSI(マスター出力、スレーブ入力)
  • MISO(マスター入力、スレーブ出力)
  • SS(スレーブ選択:アクティブロー)

全二重通信が可能で、1台のマスターに複数のスレーブを接続できます。

ハードウェア接続の要点

  • SCK、MOSI、MISOの各ラインは全てのSPIデバイス間で共通接続
  • マスターから各スレーブへは個別のSS制御信号を配線
  • 出力ピンはプッシュプル、入力ピンはプルアップまたはフローティングに設定
  • 未選択スレーブのMISOはハイインピーダンス状態にする必要あり

シフトレジスタ動作の仕組み

各デバイスは8ビットシフトレジスタを持ちます。マスターが供給するSCKに同期して、両デバイスのレジスタが連動してシフトします。MSBファーストが原則で、1クロックごとに1ビットずつデータが交換されます。SCKの立ち上がりエッジでレジスタがシフトし、立ち下がりエッジでデータがサンプリングされます。

SPIタイミングの基本動作

通信開始/終了条件

  • 開始:SSをハイからローに遷移
  • 終了:SSをローからハイに戻す

代表的な動作モード(モード0)

  • CPOL=0:アイドル時にSCKはローレベル
  • CPHA=0:SCKの最初のエッジでデータ取り込み、2番目のエッジでデータ出力

このモードでは、MOSI/MISO信号はSCKエッジより半周期先行して変化します。

STM32内蔵SPIペリフェラル

STM32はSPI通信用のハードウェア回路を内蔵しており、CPU負荷を軽減しながら高速通信が可能です。主な特徴:

  • 8/16ビットデータフレーム構成、MSB/LSBファースト切り替え可能
  • クロック分周:PCLKを2,4,8,16,32,64,128,256のいずれかで分割
  • マルチマスター、マスター/スレーブ動作対応
  • 半二重/単信通信への簡略化可能
  • DMA転送対応
  • I2Sプロトコルとの互換性
  • STM32F103C8T6ではSPI1、SPI2の2系統搭載

SPIブロックの動作概要

送信データはまず送信データレジスタ(TDR)に書き込まれ、シフトレジスタが空くと自動転送されます。転送完了時にTXEフラグがセットされ、次のデータをTDRに準備できます。受信側はシフトレジスタから受信バッファ(RDR)へデータが移されるとRXNEフラグがセットされます。

連続転送の基本シーケンス

  1. TXEがセットされるのを待つ
  2. TDRに送信データを書き込む
  3. RXNEがセットされるのを待つ
  4. RDRから受信データを読み出す

W25Q64フラッシュメモリモジュール

Winbond社のW25QxxシリーズはSPI接続のNorフラッシュメモリで、データ保存やファームウェア格納に適しています。

容量バリエーション(一部)

  • W25Q64:64Mbit(8MByte)
  • W25Q128:128Mbit(16MByte)
  • W25Q256:256Mbit(32MByte)

フラッシュ操作の重要な制約

  • 書き込み前には必ず書き込みイネーブルコマンドを発行
  • ビットは1→0へのみ変更可能、0→1は不可(書き込み前の消去必須)
  • 最小消去単位はセクター(4KB)。製品全体の消去も可能
  • 連続書き込みは1ページ(256バイト)単位。ページ境界を超えると先頭に戻る
  • 書き込み/消去後はビジー状態となり、完了まで次のコマンドを受け付けない
  • 読み出しにはイネーブル不要でページ制限もないが、ビジー中は不可

ソフトウェア実装例(ソフトウェアSPI)

SPIピン制御の基本関数

// SS制御
void SW_SPI_SetSS(uint8_t state) {
    GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)state);
}

// SCK制御
void SW_SPI_SetSCK(uint8_t state) {
    GPIO_WriteBit(GPIOA, GPIO_Pin_5, (BitAction)state);
}

// MOSI制御
void SW_SPI_SetMOSI(uint8_t state) {
    GPIO_WriteBit(GPIOA, GPIO_Pin_7, (BitAction)state);
}

// MISO読み取り
uint8_t SW_SPI_GetMISO(void) {
    return GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6);
}

SPI初期化(モード0)

void SW_SPI_Init(void) {
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    
    GPIO_InitTypeDef gpio_cfg;
    gpio_cfg.GPIO_Mode = GPIO_Mode_Out_PP;
    gpio_cfg.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_7;
    gpio_cfg.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &gpio_cfg);
    
    gpio_cfg.GPIO_Mode = GPIO_Mode_IPU;
    gpio_cfg.GPIO_Pin = GPIO_Pin_6;
    GPIO_Init(GPIOA, &gpio_cfg);
    
    SW_SPI_SetSS(1);
    SW_SPI_SetSCK(0);
}

1バイト転送(モード0による実装)

uint8_t SW_SPI_TransferByte(uint8_t tx_byte) {
    uint8_t rx_byte = 0;
    
    for(uint8_t i = 0; i < 8; i++) {
        SW_SPI_SetMOSI(tx_byte & (0x80 >> i));
        SW_SPI_SetSCK(1);
        if(SW_SPI_GetMISO()) {
            rx_byte |= (0x80 >> i);
        }
        SW_SPI_SetSCK(0);
    }
    return rx_byte;
}

W25Q64制御関数の実装

コマンド定義(一部抜粋)

#define CMD_WRITE_ENABLE        0x06
#define CMD_WRITE_DISABLE       0x04
#define CMD_READ_STATUS1        0x05
#define CMD_PAGE_PROGRAM        0x02
#define CMD_SECTOR_ERASE_4KB    0x20
#define CMD_CHIP_ERASE          0xC7
#define CMD_READ_DATA           0x03
#define CMD_JEDEC_ID            0x9F
#define DUMMY_BYTE              0xFF

デバイスID読み出し

void W25Q64_ReadID(uint8_t *manuf_id, uint16_t *device_id) {
    SW_SPI_Start();
    SW_SPI_TransferByte(CMD_JEDEC_ID);
    *manuf_id = SW_SPI_TransferByte(DUMMY_BYTE);
    *device_id = (uint16_t)SW_SPI_TransferByte(DUMMY_BYTE) << 8;
    *device_id |= SW_SPI_TransferByte(DUMMY_BYTE);
    SW_SPI_Stop();
}

ページ書き込み(事前消去必須)

void W25Q64_PageWrite(uint32_t addr, uint8_t *data, uint16_t len) {
    W25Q64_WriteEnable();
    SW_SPI_Start();
    SW_SPI_TransferByte(CMD_PAGE_PROGRAM);
    SW_SPI_TransferByte(addr >> 16);
    SW_SPI_TransferByte(addr >> 8);
    SW_SPI_TransferByte(addr);
    for(uint16_t i = 0; i < len; i++) {
        SW_SPI_TransferByte(data[i]);
    }
    SW_SPI_Stop();
    W25Q64_WaitBusy();
}

ハードウェアSPIによる実装

STM32内蔵SPIを使う場合は、以下のようにペリフェラルを設定します。

void HW_SPI_Init(void) {
    // GPIO: SCK(PA5), MOSI(PA7)をAF_PP、MISO(PA6)をIPUに設定
    // SS(PA4)はGPIOでソフト制御
    
    SPI_InitTypeDef spi_cfg;
    spi_cfg.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_128;
    spi_cfg.SPI_CPHA = SPI_CPHA_1Edge;
    spi_cfg.SPI_CPOL = SPI_CPOL_Low;
    spi_cfg.SPI_DataSize = SPI_DataSize_8b;
    spi_cfg.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
    spi_cfg.SPI_FirstBit = SPI_FirstBit_MSB;
    spi_cfg.SPI_Mode = SPI_Mode_Master;
    spi_cfg.SPI_NSS = SPI_NSS_Soft;
    SPI_Init(SPI1, &spi_cfg);
    SPI_Cmd(SPI1, ENABLE);
}

uint8_t HW_SPI_TransferByte(uint8_t tx_byte) {
    while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == 0);
    SPI_I2S_SendData(SPI1, tx_byte);
    while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == 0);
    return SPI_I2S_ReceiveData(SPI1);
}

メイン処理フロー例

int main(void) {
    OLED_Init();
    W25Q64_Init();
    
    uint8_t mid;
    uint16_t did;
    uint8_t wdata[] = {0x11, 0x22, 0x33, 0x44};
    uint8_t rdata[4];
    
    W25Q64_ReadID(&mid, &did);
    OLED_ShowHexNum(1, 1, mid, 2);
    OLED_ShowHexNum(1, 10, did, 4);
    
    // 対象アドレスのセクターを消去後、データ書き込み
    W25Q64_SectorErase(0x000000);
    W25Q64_PageWrite(0x000000, wdata, 4);
    
    W25Q64_ReadData(0x000000, rdata, 4);
    
    // 読み出しデータを表示
    for(int i=0; i<4; i++) {
        OLED_ShowHexNum(2, 1+i*3, wdata[i], 2);
        OLED_ShowHexNum(3, 1+i*3, rdata[i], 2);
    }
    
    while(1) {}
}

本稿ではSPIの基本からSTM32での実装、W25Q64フラッシュメモリの制御方法までを解説しました。ソフトウェアSPIとハードウェアSPIの両方を提示し、用途に応じて適切な方式を選択できるようにしました。

タグ: STM32 SPI W25Q64 フラッシュメモリ 組込みシステム

6月9日 16:56 投稿