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フラグがセットされます。
連続転送の基本シーケンス
- TXEがセットされるのを待つ
- TDRに送信データを書き込む
- RXNEがセットされるのを待つ
- 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の両方を提示し、用途に応じて適切な方式を選択できるようにしました。