2020年08月08日

I2Cスレーブデバイス実装

STM32マイコンのHALライブラリを用いてI2Cスレーブデバイスを実装する場合について紹介します。最も簡単なI2Cスレーブデバイスの実装としては同期関数であるHAL_I2C_Slave_Transmit、HAL_I2C_Slave_Receiveを用いて通信する方法があります。ただ、この方法では一般的なI2Cスレーブデバイスのような実装ができません。

一般的なI2Cスレーブデバイスの場合、I2Cマスタデバイスから下記のような順でデータを書き出したり、読み出したりします。

@スレーブアドレス
Aスレーブレジスタ書き込み
Bデータ書き込みor書き込み

同期関数を使用した場合はタイムアウトするまでスレーブデバイスとしてI2C受信処理以外ができなくなってしまうため、非同期関数で受信時以外はスレーブデバイスとしてのセンサ情報管理や取得、ADC変換といった別の処理をさせたほうが現実的だと思います。

今回は非同期で上記のような一般的なI2CスレーブデバイスをSTM32マイコンで実装する場合について紹介します。



@CubeMX設定
I2Cデバイス設定で「Parameter Settings」の「Primary slave address」にI2Cスレーブデバイスを7bitアドレスで設定(例では0x32)します。CubeMXで生成後に自動的にコード上ではRead、Writeビットを含めた8bitアドレスしてビットシフトされます。

非同期でI2Cスレーブ受信処理を行うため、NVIC Settingsで「I2C* event interrupt」、必要に応じて「I2C* error interrupt」にチェックを入れて割込みを有効化します。


Aコード実装
グローバル変数として下記を定義します。
volatile uint8_t TransferDirection, TransferRequested;
volatile uint8_t SensorData;

#define I2C_SLAVE_ADDR (0x32<<1)
#define SENSOR_REGISTER 0x1
#define WHO_AM_I_REGISTER 0x0
#define WHO_AM_I_VALUE 0xBC
#define TRANSFER_DIR_WRITE 0x1
#define TRANSFER_DIR_READ 0x0

グローバル関数として割込み関数を定義します。

void HAL_I2C_AddrCallback(I2C_HandleTypeDef *hi2c, uint8_t TransferDirection, uint16_t AddrMatchCode) {
   if(hi2c->Instance == I2C1) {
     if(AddrMatchCode== I2C_SLAVE_ADDR)
     {
       transferRequested = 1;
       transferDirection = TransferDirection;
     }
   }
}

void HAL_I2C_ListenCpltCallback(I2C_HandleTypeDef *hi2c)
{
    if(hi2c->Instance==I2C1){
      HAL_I2C_EnableListen_IT(&hi2c1);
    }
}


int main(void)
{
 〜〜〜〜
 //自動生成初期化コード
 〜〜〜〜
/* USER CODE BEGIN 2 */
 HAL_I2C_EnableListen_IT(&hi2c1);//スレーブアドレス呼び出しの割込み有効化
uint8_t i2cBuf[2];
/* USER CODE END 2 */

/* Infinite loop */
/* USER CODE BEGIN WHILE */
 while (1)
 {

    while(!TransferRequested) {//I2Cスレーブ処理がない場合
       SensorData=XXXX;//センサデータ更新
    }
    TransferRequested = 0;
    if(TransferDirection == TRANSFER_DIR_WRITE) {
      HAL_I2C_Slave_Sequential_Receive_IT(&hi2c1, i2cBuf, 1, I2C_FIRST_FRAME);//レジスタ情報受信
      while (HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_LISTEN);

      switch(i2cBuf[0]) {//レジスタ情報に応じて返す値を変える
        case WHO_AM_I_REGISTER:
            i2cBuf[0] = WHO_AM_I_VALUE;
            break;
        case SENSOR_REGISTER :
            i2cBuf[0] = SensorData;
            break;
        default:
            i2cBuf[0] = 0xFF;
       }
       HAL_I2C_Slave_Sequential_Transmit_IT(&hi2c1, i2cBuf, 1, I2C_LAST_FRAME);
       while (HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_READY);
   }

/* USER CODE END WHILE */
 }

ポイントとしてはHAL_I2C_EnableListen_ITでスレーブアドレスの受信割込みを有効化し、HAL_I2C_Slave_Sequential_Receive_ITとHAL_I2C_Slave_Sequential_Transmit_ITを用いて受信、送信処理をする点です。また、受信、送信処理ではack、nackを返す処理をするためにI2C_FIRST_FRAME、I2C_NEXT_FRAME、I2C_LAST_FRAME等を使い分ける必要があります。

今回の例ではレジスタ値によって返す値の数を1つに固定しましたが、I2Cスレーブデバイスによっては連続するレジスタの値を一度に取得することが可能なものがあります。そのような場合はレジスタの値を配列に予め入れて置き、要求のあったレジスタによってオフセットさせて値を返すようにすることで実現可能です。

STM32マイコンで自作I2Cデバイスを製作してみたいと思います。
posted by Crescent at 00:00| Comment(5) | 組込ソフト | このブログの読者になる | 更新情報をチェックする
この記事へのコメント
こんにちは。
最近stm32を触り始めた初心者です。
私もstm32f4をi2cスレーブとして使おうと考えているところなのですが、この記事の内容で参考にした資料等はございますでしょうか?
Posted by haisen at 2021年05月09日 13:59
CubeMX内のライブラリに様々なサンプルコードが用意されています。そちらを参考に実装しています。I2C通信のサンプルの場合は下記のディレクトリにサンプルがあります。
C:\Users\(ユーザ名)\STM32Cube\Repository\STM32Cube_FW_F4_VXXX\Projects\STM32F411E-Discovery\Examples\I2C
Posted by Crescent at 2021年05月09日 14:32
ご回答ありがとうございます。
Crescent様のコードを基に実装してみたのですが、2byteの受信のみの処理がしたい場合は、

HAL_I2C_Slave_Seq_Receive_IT(&hi2c1, buffer, 2, I2C_FIRST_AND_LAST_FRAME);
while (HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_LISTEN);
--処理--
while (HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_READY);

のようにすれば大丈夫でしょうか。
Posted by haisen at 2021年05月09日 17:28
2byteの場合はサイズが2になりますが、I2C_FIRST_AND_LAST_FRAMEでよいかどうかは検証していないため、分かりません。
Posted by Crescent at 2021年05月09日 20:16
コメントありがとうございます。
FIRST_FRAMEとFIRST_AND_LAST_FRAMEのいずれでも動作確認はできましたが前者のみだと複数スレーブを接続したときなどに問題が出るかもしれませんね。
Posted by haisen at 2021年05月10日 10:41
コメントを書く
お名前:

メールアドレス:

ホームページアドレス:

コメント:

※ブログオーナーが承認したコメントのみ表示されます。