SNSへはこちら

ESP8266でベアメタル(10) - I2Cマスター

続いて I2C です。こちらもペリフェラルではありません。なんと、ソフトウェア的に実装しているという。

まずは実装したコード

今回はこちらの秋月 LCD 液晶(16 x 2) を動かしました。なので LCD 用関数も含まれています。コード中の記述通り、SCL が IO14, SDA が IO2 です。いずれもプルアップをお忘れなく。

#include "driver/i2c_master.h" をした上で、以下のコードになります。例によってパーティションテーブル等は省きます。
また、本来だとバスのストールを防ぐために i2c_master_checkAck(); の結果(返り値型が bool)で万が一 NACK を受け取った時の処理もしなければなりませんが、今回は無視しています。取り敢えずの実装だからね。

// LCD I2C Funcs {{{
const static uint8_t lcd_init_cmds[] = {
        0x38, // function set
        0x39, // function set
        0x14, // internal OSC frequency
        0x73, // contrast set
        0x56, // Power/IOCON/contrast control
        0x6c, // follower control
        0x38, // function set
        0x01, // clear display
        0x0c, // display ON/OFF control
};
const static uint8_t address = 0x7c;

void lcd_cmd(uint8_t dat) {
    uint8_t arr[] = {0x00, dat};
    i2c_master_start();
    i2c_master_writeByte(address);
    i2c_master_checkAck();
    i2c_master_writeByte(arr[0]);
    i2c_master_checkAck();
    i2c_master_writeByte(arr[1]);
    i2c_master_checkAck();
    i2c_master_stop();
}
void lcd_data(uint8_t dat) {
    uint8_t arr[] = {0x40, dat};
    i2c_master_start();
    i2c_master_writeByte(address);
    i2c_master_checkAck();
    i2c_master_writeByte(arr[0]);
    i2c_master_checkAck();
    i2c_master_writeByte(arr[1]);
    i2c_master_checkAck();
    i2c_master_stop();
}
void lcd_str(char *str) {
    while( *str != '\0' ) {
        lcd_data(*str);
        str++;
    }
}

void lcd_init(void) {
    for(int i=0; i< 9; i++){
        lcd_cmd(lcd_init_cmds[i]);
        os_printf("LCD Init #%d\r\n", i);
        os_delay_us(200);
        if( i == 5 ) os_delay_us(1000 * 200);
    }
}
// }}}

/*
    I2C_SCL: IO14
    I2C_SDA: IO2
*/
void user_init(void) {
    uart_init(BIT_RATE_115200, BIT_RATE_115200); // For debug output
    os_printf("\r\n");
    /* i2c_master_init(); */
    i2c_master_gpio_init();
    lcd_init();
    lcd_str("Hello!");
}

以下より、ハマりどころとかを述べていきます。

I2C機能のソースコードがあった

NonOS SDK にしては珍しく、I2C のコードがあります。まあ GPIO を使って I2C エミュレートしているので特段隠すことはないんでしょうね。driver_lib/driver/i2c_master.h にあります。

例えば1バイト分のデータを送信する i2c_master_writeByte の実装です。確かに1ビット1クロックずつやってる。すごいなあ(白目)

void ICACHE_FLASH_ATTR
i2c_master_writeByte(uint8_t wrdata)
{
    uint8_t dat;
    sint8 i;

    i2c_master_wait(5);

    i2c_master_setDC(m_nLastSDA, 0);
    i2c_master_wait(5);

    for (i = 7; i >= 0; i--) {
        dat = wrdata >> i;
        i2c_master_setDC(dat, 0);
        i2c_master_wait(5);
        i2c_master_setDC(dat, 1);
        i2c_master_wait(5);

        if (i == 0) {
            i2c_master_wait(3);   ////
        }

        i2c_master_setDC(dat, 0);
        i2c_master_wait(5);
    }
}

今回はこれがあるおかげで助かりました。相変わらず API リファレンスが仕事をしていなかった。こんな感じで。

結構ハマったんですよ。まじで許さん。

仕様

結構ハマったので、その仕様についてお話します。

I2C初期化時に謎の信号が出る

まずはこちらを御覧ください。i2c_master_init の 一部抜粋です。i2c_master_setDC は SDA と SCL の信号レベルを設定する GPIO 関数です。正確には 各ピンを Input(Hi-Z) or Low 出力に変える関数です。

    // set data_cnt to max value
    for (i = 0; i < 28; i++) {
        i2c_master_setDC(1, 0);
        i2c_master_wait(5);    // sda 1, scl 0
        i2c_master_setDC(1, 1);
        i2c_master_wait(5);    // sda 1, scl 1

多分これはスレーブのバッファをクリアするため?よく分かりませんが、なんか奇妙に感じてしまいます。でも動くんですけどね。デバッグの時に不便でした。オシロにこの信号が出てしまいますから。

I2C初期化関数はi2c_master_gpio_initだけでいい

i2c_master_gpio_init という初期化関数があるのですが、まじで実装が API の Reference と一致していません。Reference はこう言っていますよ。

Set GPIO in I2C master mode.

Set の目的語は GPIO ですよね。つまり、GPIO を I2C マスタ用に(おそらくオープンドレインに)すると見受けられますが...違います。実際は

  • SCL, SDA をオープンドレイン構成に
    • オープンドレイン設定は ESP8266 の GPIO に存在しないので、先述のように Input と Low 出力を用いて頑張っている
  • I2C 機能の初期化をする
    • i2c_master_init の中身を実行している...

...と、なんと I2C 機能自体の初期化も兼ねています。ちなみに i2c_master_init という関数もあるのですが、こちらの Reference による説明は

Initialize I2C.

いや、もっとちゃんと describe してくれよ。いずれにしろ、この2つの説明を並べる限り、2つとも関数を呼ぶ必要があるのかな?と思われますが、実際には後者は不要です。どうしてこうなった...

全てを自力で行う必要がある(アドレス、ACK受付 等々...)

これはソースコードを見て解決しました。マニュアルがカスなので。

結論から言うと、この I2C 機能、アドレスの送出、ACK 受付 等々を全て自力でやらなければなりません
アドレスはまあわかりますが、ACK 受付 をやらなければいけないのはちょっと驚きです。ここにハマりまくって、LCD コマンドの送出が上手く行っていないようでした。その ACK 受付をする関数が、コード中の i2c_master_checkAck() です。ではちょっとばかし実験をば。

以下のコードで、i2c_master_checkAck() 無しによって動かしたものが以下です。

void lcd_cmd(uint8_t dat) {
    uint8_t arr[] = {0x00, dat}; // LCD 用コマンド用。気にしないでください
    i2c_master_start(); // Issue Start Condition
    i2c_master_writeByte(address); // Slave Address
    os_delay_us(100); // 分かりやすいようにディレイを入れた(数字は多分違うけど...)
    i2c_master_writeByte(arr[0]); // Data 1
    i2c_master_writeByte(arr[1]); // Data 2
    i2c_master_stop(); // Issue Stop Condition
}

1つ目の i2c_master_writeByte(address); をキャプチャした SCL の中華オシロ波形がこちらです。↓

波形は High から始まっています。そこから Low に落ちて Start Condition 発行。続いて立ち上がりエッジにて SDA のデータを送出するのが正しいマスターの動作です。見ると、8回立ち上がりエッジが見えます
本来だとこの後、スレーブから「OK〜」という反応(これを ACK と言う)をもらうために、SDA を解放したうえで、もう1クロックの SCL 立ち上がりエッジをマスタから送出する必要があります。つまり、1ビットの I2C 通信では、9つの SCL クロックが必要になります。
このコードだと8つしかクロックがないので、これはすなわちスレーブから ACK を貰えず、正しい通信になっていないということを意味します。

実際に i2c_master_checkAck() はもう1クロックを発生させ、その時の SDA の電圧レベルを検知するという実装になっていますので、必要だとわかります。実際にコードを挿入してみましょう。

void lcd_cmd(uint8_t dat) {
    uint8_t arr[] = {0x00, dat}; // LCD 用コマンド用。気にしないでください
    i2c_master_start(); // Issue Start Condition
    i2c_master_writeByte(address); // Slave Address
    i2c_master_checkAck(); // ADD: Send 1 clock & Check whether ACK or not.
    os_delay_us(100); // 分かりやすいようにディレイ
    i2c_master_writeByte(arr[0]); // Data 1
    i2c_master_checkAck(); // ADD: Send 1 clock & Check whether ACK or not.
    i2c_master_writeByte(arr[1]); // Data 2
    i2c_master_checkAck(); // ADD: Send 1 clock & Check whether ACK or not.
    i2c_master_stop(); // Issue Stop Condition
}

ADD: の部分を追加です。ACK は 1byte ごとにスレーブから送出されますので、逐一実行が必要です。何度も言いますが、この結果によってエラーハンドリングをせねばなりません。今回はホビーユースのため無視しています。
この時の波形です↓

確かに、9つのクロックが出ています。ACK 受付用のクロックが若干遅れていますが、まあこれはこれで良いでしょう。

ということで、まんまとリファレンスがカスなため、使用にハマってしまいました。やっぱり高レイヤを信頼し切るのは良くないですね...