以前の内容
I2C(NordicではTWI)についてはSDK 1.x時代にも触れています。その時はNRFXドライバーを直接アクセスする方式での記述でした。
ちなみにこのプロジェクトはSDK 2.xでは(なぜか)コンパイルできません
サンプルを漁ってみる
VSCode + SDK 2.xになって色々とやりやすくなったので、I2Cを使ったサンプルプロジェクトもそろそろあるんじゃないかと思って漁ってみたところ・・・ありました。
富士通のFRAMにアクセスするサンプルプロジェクトのようです。
とりあえず動かす
さっそくこれを動かしてみましょう。FRAMなんか用意できないとかなんとか、あれやこれや考えてみても進まないのでとりあえず何も考えず(笑)にデバッガーを走らせてみます。なお、ターゲットとなるI2Cデバイスも特に何も繋いでいません。いざ!
・・・???
うんともすんとも言いません。write_bytes関数実行後は内蔵プルアップが有効になった(?)ように見えますが、波形そのものは何も出てきません。
data[0] = 0xAE;
ret = write_bytes(i2c_dev, 0x00, &data[0], 1);
サンプルプロジェクトそのままだとこの後のエラー処理に引っかかって終わってしまいますが、いずれにしてもまともには動いていなさそうなのは間違いなさそうです。
ターゲットデバイスを接続していないから?
ターゲットデバイスを何も繋いでいないからダメなのかと思って、いつも使っているInvensenseのIMUセンサー評価ボードを繋いでみました。すると・・・お、波形が出たじゃないですか。
デバイスアドレスが合っていないのでNACKで終わってしまうのは仕方がないですね。波形を見てみるとどうやら100kHzで動いているようですが、特に設定したわけでもないのでそりゃそうかって感じです。
初期化ってどこにあるの?
このサンプルプロジェクトを見てまず最初に思うのは、nRF SDK時代でもあったtwi[m]_initのような初期化関数がどこにも見当たらないのはなぜなのかということです。ですが、いきなりwrite_bytes関数を呼び出しても動いているということはKConfig(prj.conf)で使う設定にすると自動で初期化されるような仕組みになっているのでしょう。
実際、ここでは説明しませんがそうなっています
サンプル魔改造(笑)
なんとなく動きそうな目途が立ってきたのでいよいよちゃんとしたものに魔改造(笑)していくフェーズです。まずは手元のIMUセンサーICが動くようにしてみます。
/*
* Copyright (c) 2012-2014 Wind River Systems, Inc.
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <zephyr/zephyr.h>
#include <zephyr/device.h>
#include <zephyr/drivers/i2c.h>
#define ICM2064x_ADDR0 (0x68)
static int write_bytes(const struct device *i2c_dev, uint8_t addr, uint8_t *data, uint32_t num_bytes)
{
struct i2c_msg msgs[2];
/* Send the address to write to */
msgs[0].buf = &addr;
msgs[0].len = 1U;
msgs[0].flags = I2C_MSG_WRITE;
/* Data to be written, and STOP after this. */
msgs[1].buf = data;
msgs[1].len = num_bytes;
msgs[1].flags = I2C_MSG_WRITE | I2C_MSG_STOP;
return i2c_transfer(i2c_dev, &msgs[0], 2, ICM2064x_ADDR0);
}
static int read_bytes(const struct device *i2c_dev, uint8_t addr, uint8_t *data, uint32_t num_bytes)
{
struct i2c_msg msgs[2] = {0};
/* Send the address to read from */
msgs[0].buf = &addr;
msgs[0].len = 1U;
msgs[0].flags = I2C_MSG_WRITE;
/* Read from device. STOP after this. */
msgs[1].buf = data;
msgs[1].len = num_bytes;
msgs[1].flags = I2C_MSG_READ | I2C_MSG_STOP;
return i2c_transfer(i2c_dev, &msgs[0], 2, ICM2064x_ADDR0);
}
void main(void)
{
const struct device *i2c_dev = DEVICE_DT_GET(DT_NODELABEL(i2c0));
uint8_t data[12];
int ret;
if (!device_is_ready(i2c_dev)) {
printk("I2C: Device is not ready.\n");
return;
}
// Write to
data[0] = 0x80; // PWR_MGMT_1_DEVICE_RESET
ret = write_bytes(i2c_dev, 0x06, &data[0], 1); // UB0_PWR_MGMT_1
if (ret) {
printk("Error writing to device! error code (%d)\n", ret);
} else {
printk("Write OK.\n");
}
// Read from
ret = read_bytes(i2c_dev, 0x2d, data, sizeof(data)); // UB0_ACCEL_XOUT_H
if (ret) {
printk("Error reading from device! error code (%d)\n", ret);
} else {
printk("Read OK.\n", sizeof(data));
}
}
かなり変わってしまいましたが、要は書き込みと読み込みを1回ずつ動作させてみるというものです。ということで早速動かしてみると・・・。
お、デバイスに対してちゃんと書き込みしていそうです。元のソースコードが書き込みデータのブロックを2つに分けているので2バイト目と3バイト目の間が空いているのが若干気になりますが、今後の利便性(元のソースコードではアドレスとして2バイト取っていた)を考慮するとこの形のほうがいいのかなと思ってそのまま使っています。
ちなみに初期化コマンド不足なのでこれではIMUセンサーは動きません
続いてリードです。ここまで来たらそりゃ動くよね・・・ふぁっ?!
NACKじゃなくてちゃんとACKで終わっているのに、なんで最初のアドレス送信だけで終わっているの・・・?
色々と試してみましたが、どうがんばっても2ブロック目のリード部分が出力されません。
きっと不具合です
TWIではなくTWIMしか動かない
上記にも書いたようにきっと不具合だと思われるのですが、TWIはリードがちゃんと動かないようです。さんざん悩んだ結果、TWIMを使うと動作するということが判明しました。
プリセットのNordic評価ボードを使っているのでTWIをTWIMに変更するにはoverlayを使います。app.overlayファイルをプロジェクトフォルダに追加することで自動的に上書きをしてくれます。中身は以下のようにi2c0だけを上書きします。
// Copyright (c) 2022 Nordic Semiconductor ASA
// SPDX-License-Identifier: Apache-2.0
&i2c0 {
compatible = "nordic,nrf-twim";
status = "okay";
pinctrl-0 = <&i2c0_default>;
pinctrl-1 = <&i2c0_sleep>;
pinctrl-names = "default", "sleep";
};
上書きなのでcompatibleの部分だけでもよいかも知れません
追加後にbuild configurationを再び走らせるとTWIからTWIMに変わります。
変更されていない場合はVSCodeを再起動します
再度動かしてみる
TWIをTWIMに変更して再度動かしてみると・・・ちゃんと動きました。
それにしてもきったねぇ波形だよなぁ・・・と思ってSegger Embedded Studioを使ってnRF SDKでも動かしてみたのですが一緒でした。いや、ハードウェア(評価ボード)が一緒なんだから当たり前だっつーの(笑)
リード部分が全部0なのは最初のほうにも書いたように初期化コマンドが足りていなくてICが動作していないからです。これでちゃんと動いているのか分かるのかって?ま、動いているということで勘弁してください・・・。
400kHzで動かしてみる
さてさて。
最初のほうにチラッと触れましたが、KConfigで使う設定さえすれば初期化が不要なので特に何もせずに使えます。でも!でも!400kHzで動かしたいじゃないですか。どこでどう設定すればいいのでしょうか。KConfigを探してみてもそれらしい設定は見当たらず・・・。
ということでメッチャ(SDK内を)検索して探しまくったところ、ついに見つけました。
uint32_t i2c_cfg = (I2C_MODE_CONTROLLER | I2C_SPEED_SET(I2C_SPEED_FAST));
i2c_configure(i2c_dev, i2c_cfg);
え?たったこれだけ・・・、っていうか400kHzで動かすための設定ですしおすし。
厳密には上記ドンピシャの記述はどこにもありません
100kHzと比較しやすいようにタイムレンジを同じにしています
お、早くなった!!!
ん?でもなんか臭いな・・・。400kHzなので4倍ですよ・・・?
もう少し拡大して見てみるか・・・。
ん~・・・。
なんか変だぞ・・・。
さらに拡大してみよう。
え?
200kHz・・・?
しかもデューティ比がおかしくないですか?
400kHz(FAST MODE)はバグっている
どうも400kHzの動作はバグっているようです。このSDKもまだまだですね・・・。
とりあえずFAST MODEでの使用は止めたほうがいいと思います。
この段階でSDK使うことってあまりないですからね。
やっぱりVer. 10くらいにならないとまともに使えませんねwww
非同期処理のフィードバック
ちなみに非同期処理のフィードバックですが、内部的には非同期処理のフィードバック関数が存在していますが、それをユーザーがオーバーライドできるような仕組みには今のところなっていないようです。
オーバーライドと表現しましたがそもそもC++ではありません
なので、非同期処理で特殊なことをする場合には残念ながら全く使い物にならないというのが現状です。
例えばTWI0とTWI1を同時に処理して両方が終わるまで待つ、みたいなの