Nucleo F303K8でSPI接続のI/OエキスパンダーMCP23S17を使ってみた

電子工作

今回の工作では、STマイクロのNucleo F303K8ボードを使い、Microchip製のI/Oエキスパンダー MCP23S17 を試してみました。MCP23S17はSPIインターフェースで最大16本のGPIOを増設できる便利なICで、小型マイコンのピン不足を補うのに最適です。今回はその基本的な使い方として、LEDを点灯させる出力制御と、スイッチ入力を読み取る実験を行いました。


ハードウェア構成

MCP23S17は28ピンのDIPやSSOPパッケージで販売されています。今回使用したのはDIPタイプで、秋月電子で購入しました。1個240円でした。
https://akizukidenshi.com/catalog/g/g110644/

これを制御するマイコンはNucleo F303K8を使います。これはQFP32というパッケージの32ピンのマイコンが載ったボードでピン数が多くないのでまさに今回のIOエキスパンダーを繋ぐ意義が大きいと思います。

実験はブレッドボードを使っていきます。まずは両方をブレッドボード上に配置します。
接続は以下の通りです。電源のVDD, VSSを接続し、SPI通信用にSCK, MOSI, MISOの3線とCSとして使うGPIOをつなぎます。また、アドレス指定に使うためのピンA0, A1, A2は初期設定では参照されない設定になっていますが念のためGNDに接続しておきます。さらにRESET信号用にGPIOを1つつないでおきます。

  • Nucleo F303K8 ⇔ MCP23S17
    • SPIクロック (SCK, D13, PB_3) → SCKピン
    • MOSI (D11, PB_5) → SIピン
    • MISO (D12, PB_4) → SOピン
    • GPIO (A3) → CSピン
    • 3.3V → VDD
    • GND → VSS
    • GPIO (A2) → RESET
    • GND → A2
    • GND → A1
    • GND → A0

注意点としてマイコンボードのNucleo F303K8は初期設定のジャンパだとPA_5, PA_6のMISOとSCKが使えません。なので別のピンに機能を割り当てています。私はこのことに気づかず少しハマりました。

GPIOの片側(ポートA)をLED制御に、もう片側(ポートB)をスイッチ入力に割り当てています。LEDには電流制限抵抗を介して接続し、スイッチはプルアップ抵抗を入れてGNDに落とす形にしました。設定でIC内部のプルアップ抵抗を有効にすることもできますがこの設定は後で試すこととしまずは設定せずにやってみます。


ソフトウェア概要

ソフトウェアの開発はSTM32CubeIDEの環境を使用します。
まずはピン設定から行います。配線したピンが適切に動くようにピンに機能を割り当てていきます。設定するピンはSPI用の3つとGPIOの出力に2つの合計5つのピンです。

ピンの割り当てをしたらSPIの機能を有効にします。ConnectivityのカテゴリにあるSPI1をFull-Duplex Masterモードで有効化します。ほとんどは初期値のままでいいのですがData Sizeは初期値が4bitになっているので8bitに変更します。また、NSSPという設定もDisabledに変更します。必要な設定を記載しておくと次のようになっていればOKです。

  • Frame Format: Motorola
  • Data Size: 8bit
  • First Bit: MSB First
  • Baud Rate: 4.0 MBits/s
  • Clock Polarity: Low
  • Clock Phase: 1 Edge
  • CRC Calculation: Disabled
  • NSSP Mode: Disabled
  • NSS Signal Type: Software

設定が完了したらGenerate Codeをしてプログラムのひな形を生成します。
生成されたコードのmain関数の中にMCP23S17操作用のプログラムを書いていきます。

初めに作るプログラムはLED1つの単純なLチカでやってみます。MCP23S17のGPA7(ピン28)に電流制限用抵抗と合わせてLEDを接続します。

ソフトウェアとしてはまずはIOCONレジスタを設定していきます。このレジスタのアドレスは0x0Aです。初期値は全てのビットが0になっていて、SEQOPというビットのみ1にし、他はそのまま0とします。SEQOPビットは6番目のビット(ビット5)なので設定値は0x20となります。

MCP23S17のレジスタに値を書き込む手順はSPIのCS(Chip Select)ピンをLowにしてからチップのアドレス、レジスタのアドレス、レジスタに書き込む値の3Byteを送信し最後にCSをHighに戻します。
MCP23S17のデフォルトのチップのアドレスは0x40なので上記のIOCONの設定は0x40, 0x0A, 0x20の順に3Byteを送ればいいことになります。

IOCONレジスタの次はIODIRレジスタを設定します。このレジスタはIOピンの入出力方向を設定するレジスタでポートA, B用にそれぞれ1つずつ存在します。デフォルトでは全てのビットが1で入力用のピンに設定されています。今回は出力ピンとして使いたいのでビットを0に設定します。レジスタのアドレスはA, Bポートごとに0x00と0x01です。今回はポートAのビット7(GPA7)を出力設定にして使ってみます。この場合送信するデータは0x40, 0x00, 0x80となります。

出力値の変更はOLATレジスタを操作することで行います。これはOutput Latchレジスタで出力値を書き込むためのレジスタです。初期値は全てのビットが0でLowを出力するようになっています。今回LEDを繋いでいるピンはGPA7なのでアドレス0x14の8番目のビット(ビット7)の値を変えることで点滅を実現できます。具体的には「0x40, 0x14, 0x80」と「0x40, 0x14, 0x00」を交互に送ることで点滅させることができます。


実際に動かしてみた

先ほどのレジスタ操作を実装していきます。
MCP23S17を操作する前にリセット用のピンを操作して明示的にリセットしたあと、IOCONレジスタとIODIRAを設定しOLATレジスタにデータを書き込んでいきます。

void MCP23S17_Reset(void) {
  HAL_GPIO_WritePin(GPIOA, GPIO_PIN_3, GPIO_PIN_RESET); /* RESET -> Low */
  HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); /* SPI_CS -> High */
  HAL_Delay(1);
  HAL_GPIO_WritePin(GPIOA, GPIO_PIN_3, GPIO_PIN_SET); /* RESET -> High */
  HAL_Delay(1);
}

void MCP23S17_SendThreeByteData(uint8_t d1, uint8_t d2, uint8_t d3) {
  uint8_t tx_data[3] = { d1, d2, d3 };
  uint8_t rx_data[3];
  HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET); /* SPI_CS -> Low */
  HAL_Delay(1);
  HAL_SPI_TransmitReceive(&hspi1, tx_data, rx_data, 3, 1000);
  HAL_Delay(1);
  HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); /* SPI_CS -> High */
  HAL_Delay(1);
}

int main(void) {
  /* Auto Generated Codes */
  /* : */
  /* : */

  /* USER CODE BEGIN 2 */
  MCP23S17_Reset();
  MCP23S17_SendThreeByteData(0x40, 0x0A, 0x20);
  MCP23S17_SendThreeByteData(0x40, 0x00, 0x00);
  for (;;) {
    MCP23S17_SendThreeByteData(0x40, 0x14, 0x80);
    HAL_Delay(500);
    MCP23S17_SendThreeByteData(0x40, 0x14, 0x00);
    HAL_Delay(500);
  }
  /* USER CODE END 2 */

  /* Auto Generated Codes */
  /* : */
  /* : */
}

このプログラムを書き込むとGPA7につないだLEDが1秒周期で点滅する動作を確認できます。

次は入力ピンの実験としてスイッチをつないでみます。ポートBのビット0(GPB0)にスイッチをつなぎスイッチの反対側はGNDと接続します。プルアップ抵抗はチップの内部で有効にする設定を行います。

入力ピンに割り当てるためにはアドレスが0x01のIODIRBレジスタのビット0を1にします。デフォルトでビットは1なので省略しても動きますが明示的に設定することとし、またポートBの全てのピンを入力にしておきたいので0xFFを書き込みます。

プルアップを有効にするためにはアドレスが0x0DのGPPUBレジスタに1をセットします。今回はピン0が対象なので0x01をセットすれば良いです。

スイッチの状態(ピンの状態)を取得するにはアドレス0x13のGPIOBレジスタを読み出します。レジスタの値を読み出す時はチップのアドレス0x40に1を加えた0x41を指定します。次に読み出したいレジスタのアドレスを指定すると次のバイトで値が送られてきます。具体的には0x41, 0x0Dを送信すると3Byte目にレジスタの値が送られてきます。

void MCP23S17_Reset(void) {
  HAL_GPIO_WritePin(GPIOA, GPIO_PIN_3, GPIO_PIN_RESET); /* RESET -> Low */
  HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); /* SPI_CS -> High */
  HAL_Delay(1);
  HAL_GPIO_WritePin(GPIOA, GPIO_PIN_3, GPIO_PIN_SET); /* RESET -> High */
  HAL_Delay(1);
}

void MCP23S17_SendThreeByteData(uint8_t d1, uint8_t d2, uint8_t d3) {
  uint8_t tx_data[3] = { d1, d2, d3 };
  uint8_t rx_data[3];
  HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET); /* SPI_CS -> Low */
  HAL_Delay(1);
  HAL_SPI_TransmitReceive(&hspi1, tx_data, rx_data, 3, 1000);
  HAL_Delay(1);
  HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); /* SPI_CS -> High */
  HAL_Delay(1);
}

uint8_t MCP23S17_ReadByteData(uint8_t d1, uint8_t d2) {
  uint8_t tx_data[3] = { d1, d2, 0xFF };
  uint8_t rx_data[3];
  HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET); /* SPI_CS -> Low */
  HAL_Delay(1);
  HAL_SPI_TransmitReceive(&hspi1, tx_data, rx_data, 3, 1000);
  HAL_Delay(1);
  HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); /* SPI_CS -> High */
  HAL_Delay(1);
  return rx_data[2];
}

int main(void) {
  /* Auto Generated Codes */
  /* : */
  /* : */

  /* USER CODE BEGIN 2 */
  MCP23S17_Reset();
  MCP23S17_SendThreeByteData(0x40, 0x0A, 0x20);
  MCP23S17_SendThreeByteData(0x40, 0x00, 0x00);
  MCP23S17_SendThreeByteData(0x40, 0x01, 0xFF);
  MCP23S17_SendThreeByteData(0x40, 0x0D, 0x01);
  for (;;) {
    const uint8_t state = MCP23S17_ReadByteData(0x41, 0x13);
    const uint8_t output_pin_state = (state & 0x01) ? 0x80 : 0x00;
    MCP23S17_SendThreeByteData(0x40, 0x14, output_pin_state);
    HAL_Delay(100);
  }
  /* USER CODE END 2 */

  /* Auto Generated Codes */
  /* : */
  /* : */
}

これを実装したのが上のプログラムです。注意点としてスイッチを押した状態とLEDの点灯で論理が反転しているのでスイッチが押されていない時に点灯しスイッチが押された時に消灯します。


まとめ

MCP23S17を使うことで、Nucleo F303K8の限られたI/Oを簡単に拡張できることを確認できました。設定はレジスタ操作さえ理解してしまえば難しくなく、LED制御やスイッチ入力などの基本的な用途であればすぐに活用できます。複数のセンサや表示器を同時に扱いたい場合にも有効で、SPI接続なので複数デバイスを共有できるのも魅力です。今後は割り込み機能も試してみたいと思います。

コメント