yoshiyuki's blog

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

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

前回、I2C通信で Slave device に書き込みを行うことに成功したので、今回は Slave device のレジスタ値の読み出しに挑戦します。
しつこいですが、I2C通信をやりたいだけなら Wire ライブラリを使った方が簡単、確実です。

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

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

I2C Read Command

作成した Sketch はこちらです。setup の中身と loop の基本的な動作は I2C Write command のものと同じで、今度は Serial通信で受け取った値が示す Register address の値を読み出して Serial通信で返します。例えば、PC から Teensy に Serial通信で 1 を送ると、Teensy は Slave device の Register address = 0x01 の値を読み出して PC に返します。

int cntDAT;
byte inDAT;
byte outDAT;
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 = pageTMP;
    port_i2c->MTDR = 0x00000449;
    port_i2c->MTDR = 0x00000100;

    while((port_i2c->MFSR & 0x00000007) > 3){
      delayMicroseconds(100); 
    }
    
    port_i2c->MTDR = 0x00000200;

    while((port_i2c->MFSR & 0x00070000) == 0){
      delayMicroseconds(100);
    }

    outDAT = port_i2c->MRDR & 0xFF;

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

  delay(1000);
}

MTDR (Master Transmit Data Register)

I2C Read Command も Write の場合と同様に MTDR に値を書き込んで操作します。MTDR の 10-8 bit が Command、7-0 bit が Data の指示となります。

    port_i2c->MTDR = 0x00000448;
    port_i2c->MTDR = pageTMP;
    port_i2c->MTDR = 0x00000449;
    port_i2c->MTDR = 0x00000100;

    while((port_i2c->MFSR & 0x00000007) > 3){
      delayMicroseconds(100); 
    }
    
    port_i2c->MTDR = 0x00000200;

今回の Slave device は Read を行う場合、先に読み出したい Register の address を書き込んでやる必要があるため、最初の2行で Slave device に対してデータの書き込みを行っています。3行目以降が Slave device からデータを読み出すための操作となります。

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

1行目は I2C Write Command と全く同じ記述で、Start Condition を生成するために Command = 0x4 を指定し、Data には Slave address = 0x24 = 010_0100 の最下位bit に Write の 0 を加えた 0100_0100 = 0x48 を指定します。
2行目の pageTMP の値は 0x000000nn となり、nn には Serial通信で PC から受け取った値が代入されます。ここで Command = 0x0 は Data = 0xnn を Slave device に送信するという指示となり、受け取った Slave device は 0xnn を、以降の操作に対する Register address の指定と認識します。

    port_i2c->MTDR = 0x00000449;
    port_i2c->MTDR = 0x00000100;

ここから Read を行うため、Stop Condition を挟まずに再び Start Condition を生成します。
1行目が Start Condition の生成です。Command は先の記述と同じく 0x4 ですが、Data には Slave address = 0x24 = 010_0100 の最下位bit に Read の 1 を追加した 0100_1001 = 0x49 を指定して Write ではなく Read を行うことを Slave device に伝えます。
2行目は Slave device からデータを読み出すための記述です。Command = 001 = 0x1 を書き込むことで Slave device から 1-byte 分のデータ読み出しを行います。この際、Data は不要なのでとりあえず 0x00 を入れておきます。

    port_i2c->MTDR = 0x00000200;

最後に Stop Condition を生成して通信を終了します。

MRDR (Master Receive Data Register)

I2C通信で Slave device から読み出したデータは Teensy 内の受信FIFO に蓄えられ、蓄えられたデータは MRDR の読み出しによって順に取得できるようになっています。

    outDAT = port_i2c->MRDR & 0xFF;

7-0 bit の値が Slave device から読み出した値となるので 0xFF との AND を取って outDAT に代入しています。この後、outDAT を Serial通信で PC に送り返します。

MFSR (Master FIFO Status Register)

MFSR は 18-16 bit が Slave device から読み出した受信データが受信FIFOにいくつ蓄えられているかを示し、3-0 bit が MTDR に書き込んだコマンドが送信FIFO にいくつ蓄えられているかを示しています。

    while((port_i2c->MFSR & 0x00000007) > 3){
      delayMicroseconds(100); 
    }

この記述は MTDR への書き込みによって送信 FIFO に蓄えられたコマンドが 3個以下になるまで待機するためのものです。
MTDR にコマンドの書き込みを行うとその内容はいったん送信FIFO に蓄えられ、順番に処理されます。ここで 送信FIFO はコマンド 4個分のサイズしか無く、それに対して今回の Sketch ではこの記述以前ですでに MTDR に 4回、コマンドの書き込みを行っているので、この記述の後にある MTDR への次のコマンドの書き込みを行う前に送信FIFO に空きができるのを待つのがこの記述の目的です。

    while((port_i2c->MFSR & 0x00070000) == 0){
      delayMicroseconds(100);
    }

この記述は Slave device からのデータの読み出しを待つためのものです。Slave device から読み出したデータが受信FIFOに蓄えられると MFSR の 18-16 bit がその数を示すので、それを待ってから蓄えられたデータの読み出しを行うという流れにしています。

このように送信/受信FIFO の中身を見張りながら処理を行うのは Teesy と I2C の処理速度の大きな差のためです。Teesy は I2C の 100倍、1000倍という速さで処理を行うため、Teesy が自分の速さで MTDR にコマンドを書き込むとあっさりと送信FIFO は溢れ、MRDR から受信データを読み出そうとしても I2C の方はまだ最初の Start Condition すら発行していないということすらあり得ます。そういったトラブルを避けるため、I2C の処理待ちを監視する手段としてこの Sketch では送信/受信FIFO を見張っています。
ちなみに、I2C の処理待ちを監視する手段はいくらでもあり、この方法はあくまで一例で、どちらかというと泥臭い方法に当たると思われます。

実験と結果

Master - Slave 間の接続

前回と同じなので割愛。

読み出し成功

PC - Teensy - Arduino を接続して電源が通ったら PC上で Arduino IDE のシリアルモニタを起動し、シリアルモニタ上で 0 から 9 までの数字を順に送信した結果が以下となります。
f:id:ysin1128:20210301151416p:plain

Teensy から シリアルモニタに Register address = 0x00 から 0x09 までの各値を読み出したものが戻ってきているのを確認できました。I2C Read も無事に成功です。(Slave device のレジスタ値に関しては Slave device 用 Sketch 参照)


Write も Read も成功したので、 Teensy で I2C通信をやるという目的はこれで達成・・・と思ったのですが、この後にもう一苦労がありました。次回はその話です。