続いて 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 受付用のクロックが若干遅れていますが、まあこれはこれで良いでしょう。
〆
ということで、まんまとリファレンスがカスなため、使用にハマってしまいました。やっぱり高レイヤを信頼し切るのは良くないですね...