STM32 HALを使ってSPIで通信する

  • 2020.06.27
  • HAL
STM32 HALを使ってSPIで通信する

以前、I2C通信に関する記事を書きましたが今回は同じようなインターフェースのSPI通信について書いてみます。

投稿時の開発環境を記しておきます。

PC:Windows10 OS
IDE: STM32CubeIDE Version1.3.0
Configurator: STM32CubeMX Version5.5.0
Board: STM32Nucleo-F401RE

SPI通信の概要

SPIはシリアル・ペリフェラル・インターフェースの略です。
I2Cと同じようにマイコンと周辺デバイスを(一般的には)プリント基板上で通信するための手段です。

I2Cではデータバスが1本で送信と受信が兼用になっていますが、SPIは別々です。
別ということは全2重通信が可能なので、その分通信速度は速いことになります。

・モトローラが開発した通信方式
・SCK, MISO, MOSI, SSの4本の通信ラインを使って双方向で通信が可能
・通信速度はI2Cに比べて高速
・マスタとスレーブが存在する

SPIについては こちら を見て頂ければと思います。

スレーブとなるターゲット

STM32(をマスタ)で制御し、スレーブのデバイスは以下のものを使ってみました。

気圧センサーモジュール

価格は600円でネットを使って購入しました。

メーカーはSTマイクロです。デバイスのデータシートは こちら です。

接続

下表のとおりに接続します。

このモジュールは I2C か SPI のどちらで接続するのか選択することができます。
今回はSPIで接続するので5番ピンをチップセレクトの信号とつなぎます。
割り込みは使わないのでINT端子は接続していません。

なおSPIで通信する場合、裏面のJ1, J2の半田ジャンパーはオープンにする必要があります。

プロジェクトを作成する

IDEを起動し、File- New – STM32 Project を選択し、Target Selection ウィンドウが出たら Board Selector タブを選択し Boards List から NUCLEO-F401RE を選択し Next ボタンを押します。

Project 名に F401SPiHAL と入力し、Finishボタンを押します。
Initialize all peripherals with their default Mode ? と聞いてくるので Yesを押します。
This kind of project is associated with the STM32CubeMx perspective. Do you want to open this perspective now ? と聞いてくるので Yesを押します。

コードジェネレーターでSPIインターフェースを追加する

今回使うSPIの端子はPA4, 5, 6, 7です。(画像の左上付近の赤枠で囲った部分)

PA4を右クリックして選択し、GPIO_Output を選択します。
PA5を右クリックして選択し、SPI1_SCK を選択します。
PA6を右クリックして選択し、SPI1_MISO を選択します。
PA7を右クリックして選択し、SPI1_MOSI を選択します。

そして Pinout & Configuration – Categories – Connectivity – SPI1 を選択し、
SPI1 Mode and Configuration – Mode のリストから Full Duplex Master を選択します。
Hardware NSS Signal は Disable のままにしておきます。

Project Explorer で F401SPiHAL を選択し、Project – Build Project でビルドします。

main.cに MX_SPI1_Init()が生成されてエラーがないことを確認します。

コードを追加する

MX_SPI1_Init()関数内で完結するコードを書いてみました。

static void MX_SPI1_Init(void)
{

  /* USER CODE BEGIN SPI1_Init 0 */
#define WHO_AM_I 0x0f
#define CTRL_REG1 0x20
#define WAKE_UP 0x90
#define P_ADRS 0x28

  HAL_StatusTypeDef s;
  uint8_t err;
  uint8_t sbuf[16], rbuf[16];
  uint32_t press;

  /* USER CODE END SPI1_Init 0 */

  /* USER CODE BEGIN SPI1_Init 1 */

  /* USER CODE END SPI1_Init 1 */
  /* SPI1 parameter configuration*/
  hspi1.Instance = SPI1;
  hspi1.Init.Mode = SPI_MODE_MASTER;
  hspi1.Init.Direction = SPI_DIRECTION_2LINES;
  hspi1.Init.DataSize = SPI_DATASIZE_8BIT;
  hspi1.Init.CLKPolarity = SPI_POLARITY_LOW;
  hspi1.Init.CLKPhase = SPI_PHASE_1EDGE;
  hspi1.Init.NSS = SPI_NSS_SOFT;
  hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_64;
  hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;
  hspi1.Init.TIMode = SPI_TIMODE_DISABLE;
  hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
  hspi1.Init.CRCPolynomial = 10;
  if (HAL_SPI_Init(&hspi1) != HAL_OK)
  {
    Error_Handler();
  }
  /* USER CODE BEGIN SPI1_Init 2 */

  sbuf[0] = WHO_AM_I | 0x80;	// Who am I ?
  sbuf[1] = 0x00;

  rbuf[0] = rbuf[1] = 0x00;

  HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET);
  s = HAL_SPI_TransmitReceive(&hspi1, sbuf, rbuf, 2, 1000);
  HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET);

  if (HAL_OK == s)
  {
	  if (0xbd == rbuf[1])
	  {
		  err = 0;
	  }
	  else
	  {
		  err = 1;
	  }

  }
  else
  {
	  err = 1;
  }

  sbuf[0] = CTRL_REG1;	// Control Register1
  sbuf[1] = WAKE_UP;

  HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET);
  s = HAL_SPI_TransmitReceive(&hspi1, sbuf, rbuf, 2, 1000);
  HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET);

  HAL_Delay(1000);

  sbuf[0] = P_ADRS | 0xc0;

  HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET);
  s = HAL_SPI_TransmitReceive(&hspi1, sbuf, rbuf, 4, 1000);
  HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET);

  press = rbuf[3] << 16 | rbuf[2] << 8 | rbuf[1];
  press /= 4096;

  /* USER CODE END SPI1_Init 2 */

}

コードの解説

クロックの極性を以下のコードで決めているのですが、気圧センサーのデータシートを読む限り、ここは HIGH (SPI_POLARITY_HIGH)にするべきです。
ですが実際には SPI_POLARITY_LOW にしないとデバイスを認識できませんでした。

hspi1.Init.CLKPolarity = SPI_POLARITY_LOW;

おそらくデータシートの誤りではないかと思います。
SPIのモードでいうとモード0で動作することが確認できています。
(私の解釈ではデータシートにはモード3の仕様が書かれています)

2021.1.11 追記

=== ここから

上に「データシートの誤りではないか」と記述しましたが、そうではありませんでした。
お詫びいたします、たいへん失礼しました。

気圧センサーの仕様書に合わせて、以下の設定を行い動作確認したところ、問題なく WHO_AM_I が読めました。
( CPOL=1, CPHA=1 なので mode=3 となり気圧センサーの仕様書通りです )

hspi1.Init.CLKPolarity = SPI_POLARITY_HIGH; // CPOL=1
hspi1.Init.CLKPhase = SPI_PHASE_2EDGE; // CPHA=1

以下のコードに変更したところ WHO_AM_I でデバイス認識コードが正しく返ってきました。
最初に1度、カラ読みする必要があるようです。

sbuf[0] = WHO_AM_I | 0x80;	// Who am I ?
sbuf[1] = 0x00;

rbuf[0] = rbuf[1] = 0x00;

HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET);

s = HAL_SPI_TransmitReceive(&hspi1, sbuf, rbuf, 2, 1000); // 1度カラ読みする

HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET);

HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET);

s = HAL_SPI_TransmitReceive(&hspi1, sbuf, rbuf, 2, 1000);

HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET);

ここまで ===

2.21.1.14 さらに追記

=== ここから

1度カラ読みしなければならない理由がわかりましたので追記します。
以下のコードに変更することで、カラ読み不要になります。


// これもやっておくべき
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); // CS=Hにしておく

__HAL_SPI_ENABLE(&hspi1); // clockが動かないように、あらかじめEnableにしておく

HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET);
s = HAL_SPI_TransmitReceive(&hspi1, sbuf, rbuf, 2, 1000);
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET);

if (HAL_OK == s)
{
  if (0xbd == rbuf[1])

以下 省略

HAL_SPI_TransmitReceive()関数の中に、以下の処理があります。
最初だけ、このif()の中を通るようです。
このSPIをイネーブルにする処理で、クロックが動いてしまいます。
対策として、CS=Lにする前にSPIをイネーブルにする処理を入れました。

if ((hspi->Instance->CR1 & SPI_CR1_SPE) != SPI_CR1_SPE)
{
__HAL_SPI_ENABLE(hspi);
}

ここまで ===

それからハードウェアでチップセレクト信号を制御する方法もあるようですが融通が利かないようなのでGPIO(PA4)を使って制御することにしました。

通信処理の部分を HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET) と
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET) ではさみます。

最初に WHO_AM_I というレジスタの値を読んで、 0xbdであればデバイスを認識できているとのことです。
次にコントロールレジスタ1に WAKE_UP(0x90) を書いてパワーダウンモードを解除します。

気圧の計測値は 0x28 (P_ADRS), 0x29, 0x2a のレジスタから読み込みます。

データシートよりレジスタの値を読む場合にはレジスタの値に 0x80 を or したものをまず送ります。
レジスタの値を送るのに8クロック、その後にそれを読むのに8クロック必要なので、HAL_SPI_TransmitReceive()
4番目の引数には2をセットします。

その後にコントロールレジスタに WAKE_UP(0x90)を書き込みます。
書き込みの際には 0x80 を or してはいけません。
この場合もレジスタの値を送るのに8クロック、その後にそこに書き込む値を送るのに8クロック必要です。
ですからHAL_SPI_TransmitReceive()の4番目の引数には同じく2をセットします。

これで気圧センサーが動作を始めたので、1秒待ってから気圧の値を読み込みます。

レジスタ P_ADRS に 0xc0 を or することでレジスタのアドレスをオートインクリメントしながら次々に読むことができます。
(リード時には 0x80 を or し レジスタをオートインクリメントするには更に 0x40 を or するということ)

レジスタの値を送るのに8クロック、その後にそれら3バイトを読むのに24クロック必要なので、HAL_SPI_TransmitReceive()
4番目の引数には4をセットします。

読んだ値は rbuf[1]から入ってきます。
読んだ値を 4096 で割ることで単位 [hPa]の気圧を得ることができます。

気圧は 1014 [hPa] でした。

関連記事を こちら に掲載しましたので、ぜひご覧ください。

HALカテゴリの最新記事