CH32VのオレオレArduino環境を作ろう その6 I2Cマスター

概要

前回はI2Cがうまく動いていなかったので割り込み関係をやりました。今回なんとなく動くようになったのでI2Cについてです。

EVTのI2C関連コードを調べる

プロジェクト説明
I2C_7bit_Mode7-bit address mode, master / slave mode, transceiver routine
I2C_10bit_Mode10 bit address mode, master / slave mode transceiver routine
I2C_DMAI2C uses DMA, master / slave mode transceiver routine
I2C_EEPROMI2C interface operation EEPROM peripheral routine
I2C_PECuse PEC error check and master / slave mode transceiver routine
I2C_7bit_Interrupt_ModeUse interrupt to receive and send routine

CH32V103だと上記のサンプルがありました。I2C_10bit_Modeはあまり一般的でないので使いません。DMAとInterruptの割り込みも最初は必要ないと思います。PECもエラーチェックなので最初に確認する必要はないと思います。

I2C_7bit_Mode

一番標準的なのがI2C_7bit_Modeで、設定でマスター側とスレイブ側を切り替えることができます。最初はこれをみたのですが、うまく動きません。

/*
 *@Note
 *7-bit address mode, master / slave mode, transceiver routine:
 *I2C1_SCL(PB8)\I2C1_SDA(PB9).
 *This routine demonstrates that Master sends and Slave receives.
 *Note: The two boards download the Master and Slave programs respectively,
 *and power on at the same time.
 *     Hardware connection:
 *               PB8 -- PB8
 *               PB9 -- PB9
 */

内部の説明文にはSCL同士、SDA同士を接続して同時に電源を入れろとあります。しかになにも起こりません。

M5Stack用拡張ハブユニット [U006]
Groveポートを三つ備えたM5Stack用の拡張ユニットです。任意のユニットを三つまで追加することが可能です。

調査するために、上記のハブを利用しました。これを使うことで複数の回路を並行に接続可能になります。

Grove - デュポン 変換ケーブル 20 cm(各5本セット)
HY2.0-4Pコネクタ(オス)を2.54 mmオス/メス デュポンコネクタに変換できるケーブルです。ケーブル長は20 cmです。各5本の計10本セットです。

そして上記のようなGroveコネクタとQIコネクタのケーブルを利用することでCH32VからGroveケーブルが利用できるようになります。

GROVE - 4ピン - ジャンパオスケーブル(5本セット)
オス4ピンからGrove4ピンへの変換ケーブル(5本セット)です。

実際には手元にあったSeeedさんのケーブルを利用したので黄色と白のケーブル配置が入れ替わっていて、かなり迷いながらの接続となります。

ハブを利用することで4つまで接続可能になりますので1系統をオシロスコープに接続して波形を観察できるようにしました。PicoScopeを使っていたのでリアルタイムに流れているデータをI2C形式でデコード可能でかなり便利でした。

そしてI2C_7bit_Modeのマスターモードのボードと、スレーブモードのボードをハブに接続して動作を確認したところ、、、なんと信号がありません。このI2Cのバスラインをプルアップする人がいないのです。ESP32だとGPIOがプルアップされているのですが、CH32Vだとブルアップされないようですので外部回路でプルアップする必要があります。

てなことで、このサンプルは2台動かす必要があって、しかも外部プルアップが必要なので利用をあきらめました。

I2C_EEPROM

/*
 *@Note
 *I2C interface operation EEPROM peripheral routine:
 *I2C1_SCL(PB10)\I2C1_SDA(PB11).
 *This example uses EEPROM for AT24Cxx series.
 *Steps:
 *READ EEPROM:Start + 0xA0 + 8bit Data Address + Start + 0xA1 + Read Data + Stop.
 *WRITE EERPOM:Start + 0xA0 + 8bit Data Address + Write Data + Stop.
 *
 */

AT24CシリーズのEEPROMにアクセスするサンプルのようでした。これが一番シンプルで通常のI2Cアクセスに近かったので、ベースに利用しています。ただし、1バイト限定の読み書きなので2バイト以上のアクセスができなくてはまりました。

ただ該当のEEPROMをもっていなかったので、動かせてはいません。RTCモジュールとかに搭載されている場合があるのでもしかしたら持っている可能性もありますが面倒なので探していません。

このコードをベースに、手元にあってI2C接続のユニットを接続してプルアップをしつつ、オシロスコープをつかって動作確認をしていきたいと思います。

I2Cスキャン

#include "Wire.h"

void setup() {
  Serial.begin(115200);
  Serial.println("Wire Scan");
  Wire.begin();
  Serial.println("Wire.begin()");
}

void loop() {
  byte error, address;
  int nDevices = 0;

  Serial.println("Scanning for I2C devices ...");
  for (address = 0x01; address < 0x7f; address++) {
    Wire.beginTransmission(address);
    error = Wire.endTransmission();
    if (error == 0) {
      Serial.printf("I2C device found at address 0x%02X\n", address);
      nDevices++;
    } else if (error != 2) {
      Serial.printf("Error %d at address 0x%02X\n", error, address);
    }
    delay(10);
  }
  if (nDevices == 0) {
    Serial.println("No I2C devices found");
  }

  delay(1000);
}

まずはよくある上記のようなI2Cスキャンを作ってみたいと思います。まずはopenwch版Arduinoを使って動きを確認してみます。。。あれ動かない??? どうやらI2Cスキャンは一般的な動きではないので特殊処理を入れないとだめみたいです。まずはESP32で動かしてみて、正しい動きを確認しました。

I2Cスキャンの仕組み

Wire.beginTransmission(address)でアドレスを指定して、その後にWire.endTransmission()を呼び出すことで、そのアドレスから応答があったのかがわかります。自分宛てのアドレスが飛んできたらデバイスは応答をして、それ以外のアドレスは無視します。この無視が結構微妙で、CH32Vのサンプルは応答があるまで無限ループになっています。そのためほぼすべての処理にタイムアウトを設定して、タイムアウトの時に適切なバス開放をしてあげないとそれ以降I2Cが使えない状態になります。

I2C_GenerateSTART(I2C1, ENABLE);

上記の関数でバスの利用を開始して、その後に相手のアドレスを送信します。

I2C_Send7bitAddress(I2C1, 0x02, I2C_Direction_Transmitter);

送信は上記のように7ビットで送信します。上記の場合にはI2C_Direction_Transmitterなので送信のWを指定しています。そしてハマりポイントなのですが、I2Cのアドレス指定は一番下のビットが読み書きを指定しています。

今回テストで利用したI2CデバイスはBME280で0x76がアドレスなのですが、送信時は1ビットシフトしてから書き込みか読み込みかで0xECか0xEDの送信になります。関数名が7ビットアドレスですので、元の7ビット表記である0x76を渡せばいいのかと思いきや、0x76<<1(0xEC)で渡す必要がありました。このへんはオシロスコープとかで実際のデータと突き合わせないとなかなかわからないと思います。

I2C_GenerateSTOP(I2C1, ENABLE);

そして、バスの利用が終わったら上記で開放をする必要があります。とにかくタイムアウトしたら上記で開放しないとそれ以降I2Cが使えなくなるので注意が必要です。そしてこのバス開放をしてもSDAはLOWのままなのでI2Cスキャンでタイムアウト時はオシロスコープの波形が変な形に見えて結構怖いです。

とりあえずI2Cスキャンはタイムアウト処理を大量に入れて、タイムアウト時にはそのアドレスのデバイスがいないことが確認できるようになりました。

GPIOの初期化

    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOB, &GPIO_InitStructure);

    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOB, &GPIO_InitStructure);

上記のようにSCKとSDAのGPIOピンをオープンドレインモードで初期化する必要があります。

  pinMode(PB6, OUTPUT_OPENDRAIN);
  pinMode(PB7, OUTPUT_OPENDRAIN);

pinMode関数を作っていたので、上記で初期化してあげます。しかしまったく動かなくなりました。

/* Configuration Mode enumeration */
typedef enum
{
    GPIO_Mode_AIN = 0x0,
    GPIO_Mode_IN_FLOATING = 0x04,
    GPIO_Mode_IPD = 0x28,
    GPIO_Mode_IPU = 0x48,
    GPIO_Mode_Out_OD = 0x14,
    GPIO_Mode_Out_PP = 0x10,
    GPIO_Mode_AF_OD = 0x1C,
    GPIO_Mode_AF_PP = 0x18
} GPIOMode_TypeDef;

上記がCH32V103のGPIOに設定できるモードなのですがOUTPUT_OPENDRAINではGPIO_Mode_Out_ODを設定していました。元のコードをみてみるとGPIO_Mode_AF_ODを指定しています。似た名前ですが、通常のGPIOでオープンドレインを利用する場合と、プリフェラルで利用する場合でモードが違うようです。

UARTもGPIO_Mode_Out_PPで初期化してしまっていますが、本当はGPIO_Mode_AF_PPを指定する必要があったみたいです。今度pinMode周りは少し修正したいと思います。

書き込み

  Wire.beginTransmission(address);
  Wire.write(reg);
  Wire.endTransmission();

Arduinoだと上記のコードになります。

// Wire.beginTransmission(address);
I2C_GenerateSTART(I2C1, ENABLE);
I2C_Send7bitAddress(I2C1, address<<1, I2C_Direction_Transmitter);

// Wire.write(reg);
I2C_SendData(I2C1, reg);

// Wire.endTransmission();
I2C_GenerateSTOP(I2C1, ENABLE);

状態待ちを取り除くと上記の流れで送信可能です。SendDataは複数回送信することもおそらく可能です。通常はレジスタアドレスとその設定値の2バイト送信することが多いはずです。

読み出し

  Wire.beginTransmission(address);
  Wire.write(reg);
  Wire.endTransmission();
  Wire.requestFrom(address, 1);
  uint8_t read = Wire.read();

Arduinoだと上記のコードになります。

// Wire.beginTransmission(address);
I2C_GenerateSTART(I2C1, ENABLE);
I2C_Send7bitAddress(I2C1, address<<1, I2C_Direction_Transmitter);

// Wire.write(reg);
I2C_SendData(_i2c, reg);

// Wire.endTransmission();
I2C_GenerateSTOP(I2C1, ENABLE);

// Wire.requestFrom(address, 1);
I2C_GenerateSTART(I2C1, ENABLE);
I2C_Send7bitAddress(I2C1, address<<1, I2C_Direction_Receiver);

// uint8_t read = Wire.read();
uint8_t read = I2C_ReceiveData(I2C1);

上記のような流れになるのですが、1バイトは読み出せますが、複数バイトの読み出しがうまくいきません。

    I2C_AcknowledgeConfig(_i2c, ENABLE);
    if (_len == 1)
    {
        I2C_AcknowledgeConfig(_i2c, DISABLE);
    }

最終的にはopenwchのコードをみたところI2C_AcknowledgeConfigの設定が必要なことがわかりましたので上記の条件を追加しました。requestFromで何バイト読み込むか指定していますのでカウントを行い、最後のバイト以外ではI2C_AcknowledgeConfigをENABLEにして継続読み込み可能であることをI2Cデバイスに伝える必要がありました。

I2Cの仕様を勉強する

技術コラム(第18回)I2Cについて | 組込開発.com
はじめに   私たちは色々なセンサデバイスを用いて組み込み装置を開発しています。以下で紹介するI2Cはこれらセンシング技術と切っても切れない関係であり、熟知していなければ開発を行えません。今回の組込開発.comの技術コラムではI2Cの技術に...

ここまで動かすことができたので、I2Cの仕様を調べてみます。本当は最初に調べた方が効率的ですが、動かしてからじゃないと理解できないことってありますよね。。。

仕様によると読み出し時にはリピートスタートコンディションを指定しないとストップコンディションで通信を終了してしまうみたいです。

まとめ

なんとなくBME280から温度が取得できたので、一旦クラスにまとめてリリースをしてみました。詳しい動作検証はしていないのでデバイスによっては動かないかもしれません。

マスターモードのみでスレーブモードも対応していないので、もう少し落ち着いたらまた手を入れたいと思いますがI2Cばかり触っていると全然ライブラリが完成しないので次の機能に取り掛かります。

次回USB PDを触りたかったのですが、ちょっとタイマーまわりが必要そうなのでタイマークラスを整備したいと思います。

コメント