yoshiyuki's blog

Arduino/Teensy/Raspberry pi pico を利用した I2C, SPI 通信アプリを紹介します

Teensy 4.0 / I2C ができるまで (4)

前回までで設定が完了したので実際に I2C 通信を実行してみます。
何度でも言いますが、I2C 通信をやりたいだけであれば素直に Wire ライブラリを使う方が簡単、確実、安全です。

最終的には I2C 関数の自作に至ります。
ysin1128.hatenablog.com

さらに、I2C に加えて SPI と GPIO も制御する Sketch とツールも作成しました。
ysin1128.hatenablog.com

Slave device の作成

I2C通信の実験を行うためには通信相手が必要なので Arduino で代用します。Arduino を Slave device にする Sketch は以前に作成済みで、以下の記事で紹介しています。今回は SDA/SCL の内蔵 Pull-up を無効にした方の Sketch を使います。

ysin1128.hatenablog.com

Arduino の High Level が 5 V なのに対して Teensy の High Level は 3.3 V なので、Arduino 側で 5 V に Pull-up した SDA/SCL を Teensy に繋いでしまうと耐圧超過となってしまいます。そのため、今回は Arduino側の内蔵 Pull-up を無効にし、Teensy側で内蔵 Pull-up を有効にして High Level = 3.3 V で通信を行います。

I2C Write Command

Teensy には以下の Sketch を書みました。

int cntDAT;
byte inDAT;
uint32_t pageTMP;

IMXRT_LPI2C_t *port_i2c;
IMXRT_REGISTER32_t *port_iomuxc;
IMXRT_REGISTER32_t *port_iomuxc_b;

void setup() {
  Serial.begin(9600);

  port_i2c = &IMXRT_LPI2C1; //0x403F0000
  port_iomuxc = &IMXRT_IOMUXC;  //0x401F8000
  port_iomuxc_b = &IMXRT_IOMUXC_b;  //0x401F8400

  // #19 = SCL, GPIO_AD_B1_00
  port_iomuxc->offset0FC = 0x00000013; // SION = 1, MUX_MODE = 011(LPI2C1_SCL)
  port_iomuxc->offset2EC = 0x0000F861; // Pull-up = Enable, PUS = 11 (22kohm), PUE = 1, PKE = 0, ODE = 1, SPEED = 01, DSE = 100, SRE = 1
  port_iomuxc_b->offset0CC = 0x00000001; // DAISY = 1

  // #18 = SDA, GPIO_AD_B1_01
  port_iomuxc->offset100 = 0x00000013; // SION = 1, MUX_MODE = 011(LPI2C1_SDA)
  port_iomuxc->offset2F0 = 0x0000F861; // Pull-up = Enable                                                                                                                                                                                                     
  port_iomuxc_b->offset0D0 = 0x00000001; // DAISY = 1

  // I2C Clock
  CCM_CSCDR2 = (CCM_CSCDR2 & 0xFE03FFFF) | 0x00040000;
  CCM_CCGR2 |= 0x000000C0; // CG3 = 11, enable LPI2C1 clock

  // LPI2C1 Setting
  port_i2c->MCFGR1 = 0x00000001;
  port_i2c->MCFGR2 = 0x05050BB8; // FILTSDA = 5 (0.2 us), FILTSCL = 5 (0.2 us), BUSIDLE = 3000 (250 us)
  port_i2c->MCCR0 = 0x1928373B; // DATAVD = 25 (2.2 us), SETHOLD = 40 (3.4 us), CLKHI = 55 (4.7 us), CLKLO = 59 (5 us)

  port_i2c->MCR = 0x00000001;
}

void loop() {
  cntDAT = 0;

  while(Serial.available()){
    inDAT = Serial.read();
    cntDAT++;
    delay(5);
  }

  if(cntDAT > 0){
    pageTMP = 0x000000FF & (inDAT - 0x30);
    
    port_i2c->MTDR = 0x00000448;
    port_i2c->MTDR = 0x00000000;
    port_i2c->MTDR = pageTMP;
    port_i2c->MTDR = 0x00000200;

    Serial.print(pageTMP);
    Serial.print("-");
  }

  delay(1000);
}

setup には前回前々回で作った設定を放り込んでいますが、SDA/SCL の内蔵Pull-up を有効にするために以下のレジスタに書き込む値は変更しています。

  // #19 = SCL, GPIO_AD_B1_00
  port_iomuxc->offset2EC = 0x0000F861; // Pull-up = Enable, PUS = 11 (22kohm), PUE = 1, PKE = 0, ODE = 1, SPEED = 01, DSE = 100, SRE = 1

  // #18 = SDA, GPIO_AD_B1_01
  port_iomuxc->offset2F0 = 0x0000F861; // Pull-up = Enable                                                                                                                                                                                                     

loop の中は、Teensy が PC から Serial通信で何かを受け取ったら I2C Write コマンドを発信して、ついでに PC にも Serial 通信で何かを返すという内容になっています。

MTDR (Master Transmit Data Register)

I2C Write Command の中身を詳しく見てみます。以下の記述が本体となります。

    port_i2c->MTDR = 0x00000448;
    port_i2c->MTDR = 0x00000000;
    port_i2c->MTDR = pageTMP;
    port_i2c->MTDR = 0x00000200;

MTDR の 10-8 bit に Command、7-0 bit に Data を書き込むと Teensy はそれに従ってI2C通信を行います。

    port_i2c->MTDR = 0x00000448;

Command = 100 = 0x4 は Start condition の生成と、続けて Slave address の発信を行うためのコマンドです。この時、同時に Data として 7-bit Slave address + R/W の値を書き込みます。今回、使用する Slave device の Address は 0x24 = 010_0100 で、これに対して Write Command を実行しようと思っているので 0 を最下位 bit として追加して Data に書き込むべき値は 0100_1000 = 0x48 となります。

    port_i2c->MTDR = 0x00000000;
    port_i2c->MTDR = pageTMP;

先の Slave address の発信に ACK が返ってきたという前提で、この2行ではデータの送信を行います。Command = 000 = 0x0 は、単純に Data に書き込んだ値を I2C で送信します。
1行目によって Slave device には 0x00 = 0000_0000 というデータが送信されます。ちなみに Slave device 側は最初に送られてきたデータを「Register address の指定」と認識するので、このデータにより Register address 0x00 を指定したことになります。
2行目は、1行目で指定した Register address に対して書き込む値となります。例えばここで MTDR に 0x00000001 を書き込むと Slave device には 0x01 = 0000_0001 というデータが送信され、それを受け取った Slave device は先に指定された Register address = 0x00 に 0x01 という値を書き込みます。ここで MTDR に書き込む値を pageTMP という変数にしているのは、ここで送信するデータの値を PC から Serial通信で指定できるようにするためです。pageTMP は以下の記述で生成しています。

    pageTMP = 0x000000FF & (inDAT - 0x30);

この記述により pageTMP には 0x000000nn という値が代入され、nn の部分に Serial通信で受け取った値が代入されます。inDAT が Serial通信で受け取った値ですが、そこから 0x30 が引き算されているのは Serial通信で受け取る値が ascii code となっているためです。Serial通信は Arduino IDE のシリアルモニタで行うのですが、シリアルモニタから送信する "1" は "1" という文字であって、これを数値に直すと 0x31 になります。そこでシリアルモニタから送信される "1" を数値の 1 として受け取るために行っているのがこの引き算となります。

    port_i2c->MTDR = 0x00000200;

最後に Command = 010 = 0x02 で Stop Condition を生成します。この場合は Data は不要なので、値はとりあえず 0x00 としています。

実験と結果

Master - Slave 間の接続

実際に I2C 通信を試してみます。 Teensy と Arduino は以下の図のように接続します。
f:id:ysin1128:20210226142148p:plain
必要なのは GND, SDA, SCL の 3本の接続です。5 V の接続は I2C 通信そのものには必要無いのですが、Slave側の Arduino に電源を与えるために USB を繋ぐのが面倒だったので Teensy の 5 V から Arduino に電源を与えるために接続しています。
また、Teensy の USB を PC と繋いで PC から Serial 通信を行います。

I2C通信成功

PC - Teensy - Arduino を接続して電源が通ったら PC上で Arduino IDE のシリアルモニタを起動します。シリアルモニタから 1 から 9 のいずれかの数値を送信すると、それを受けた Teensy は I2C でその値を Slave device (Arduino) の Register address = 0x00 に書き込みます。ついでに書き込んだのと同じ値をシリアルモニタに返してきます。下図はこの操作を行った際のシリアルモニタのログです。

f:id:ysin1128:20210226144800p:plain

シリアルモニタから 1, 2, 3, 1 と送って、最後に冗談で "a" と送ったら 49 という値として処理されました。
肝心の Teensy - Arduino 間の I2C通信の成否についでですが、Arduino に書き込んだ Slave device 用の Sketch では Register address = 0x00 に値を書き込むと、書き込んだ値の回数だけ Arduino上の LED が点滅するようになっているので、上のシリアルモニタからの送信に対してそれぞれ 1回、2回、3回、1回、49回と点滅することが確認できました。つまり、I2C Write Command の成功です。


これはまとめ記事なので一直線でここまで辿り着いたように書いていますが、実際は各手順で行ったり来たりの繰り返しで、この LED の点滅を見た時は思わず両手を上に突き上げていました。しかし、まだ途中です。次は残る I2C Read に挑みます。