Raspberry Pi Pico を自作アプリで操作できる I2C ツールにするファームウェアを作成したのですが、この記事ではそれを作るまでに苦労した話を紹介します。
SDK の I2C 関数が NACK を検出してくれなかったので関数の中身を調べたら、そもそも NACK のフラグが上がっていなかったのを確認したのが前回でした。今回はフラグが上がらなかった原因を探ります。
エラーフラグの確認
NACK のフラグはコード上で何らかの判定をして生成したものではなく、Raspberry pi pico の本体の RP2040 というマイコンのレジスタの値を読み出したものです。従って、NACK のフラグが上がらないというのは RP2040 が NACK を判定できていないという意味になります。しかし、マイコンにバグが全く無いとは言いませんが、さすがにマイコンの中の NACK の判定のような基本的なものが動作していないとは考え難いです。それよりもありそうなのは RP2040 の該当のレジスタが NACK を示すより先に値を読み出してしまっている可能性です。
というわけで NACK 検出時に RP2040 のエラーフラグがどう変遷するかを以下のコードで確認してみます。
プロジェクトフォルダ: pico-test
pico-test |-- pico_sdk.import.cmake |-- CMakeLists.txt -- try_06 |-- try_06.c -- CMakeLists.txt
pico-test\CMakeLists.txt
cmake_minimum_required(VERSION 3.12) # Pull in SDK (must be before project) include(pico_sdk_import.cmake) project(pico-test) set(CMAKE_C_STANDARD 11) set(CMAKE_CXX_STANDARD 17) # Initialize the SDK pico_sdk_init() # Add subdirectory add_subdirectory(try_06)
pico-test\try_06\try_06.c
#include <stdio.h> #include "pico/stdlib.h" #include "class/cdc/cdc_device.h" #include "hardware/i2c.h" #include "hardware/timer.h" #include "pico/timeout_helper.h" #define SDA_PIN 20 // GP20 = Pin.26 = SDA #define SCL_PIN 21 // GP21 = Pin.27 = SCL int serial_print_b(uint32_t input_data){ uint8_t out[33]; int i; for(i=0; i<32; i++){ if((input_data & (0x00000001 << (31-i))) > 0){ out[i] = '1'; } else{ out[i] = '0'; } } out[32] = 0x0A; tud_cdc_write(out, 33); tud_cdc_write_flush(); } int serial_print_d(uint32_t input_data){ uint32_t denominator = 10; uint32_t uintDAT = input_data; uint8_t out[11]; int digit; int i; for(i=0; i<10; i++){ if((uintDAT < (denominator)) || (i == 9)){ digit = i + 1; break; } denominator *= 10; } denominator = 1; for(i=1; i<digit; i++){ denominator *= 10; } for(i=0; i<digit; i++){ out[i] = '0' + uintDAT / denominator; uintDAT %= denominator; denominator /= 10; } out[digit] = 0x0A; tud_cdc_write(out, digit+1); tud_cdc_write_flush(); } int main() { const uint LED_PIN = PICO_DEFAULT_LED_PIN; uint8_t cntDAT; uint8_t getDAT[1]; uint8_t inDAT[256]; uint8_t outDAT[16]; uint8_t byteADR; uint8_t byteDAT[16]; int i2c_result; bool abort = false; bool timeout = false; uint32_t abort_reason; absolute_time_t t; timeout_state_t ts; uint32_t uintAbort[512]; uint32_t uintState[512]; uint32_t uintTime[512]; int i; int intCNT; stdio_init_all(); gpio_init(LED_PIN); gpio_set_dir(LED_PIN, GPIO_OUT); i2c_init(i2c0, 100000); // use i2c0 module, SCL = 100kHz gpio_set_function(SDA_PIN, GPIO_FUNC_I2C); // set function of SDA_PIN=GP20 I2C gpio_set_function(SCL_PIN, GPIO_FUNC_I2C); // set function of SCL_PIN=GP21 I2C gpio_set_pulls(SDA_PIN, true, false); // enable internal pull-up of SDA_PIN=GP20 gpio_set_pulls(SCL_PIN, true, false); // enable internal pull-up of SCL_PIN=GP21 while (true) { cntDAT = 0; while(tud_cdc_available() > 0){ tud_cdc_read(getDAT, 1); inDAT[cntDAT] = getDAT[0]; cntDAT++; busy_wait_us_32(10000); } if(cntDAT > 0){ i2c0->hw->enable = 0; i2c0->hw->tar = 0x24; i2c0->hw->enable = 1; t = make_timeout_time_us(600); check_timeout_fn timeout_check = init_single_timeout_until(&ts, t); intCNT = 0; for(i=0; i<2; i++){ if(i==0){ i2c0->hw->data_cmd = 0x00000000 | I2C_IC_DATA_CMD_RESTART_BITS; } else{ i2c0->hw->data_cmd = 0x00000001 | I2C_IC_DATA_CMD_STOP_BITS; } uintAbort[intCNT] = i2c0->hw->tx_abrt_source; uintState[intCNT] = i2c0->hw->status; uintTime[intCNT] = time_us_32(); intCNT++; do { // Note clearing the abort flag also clears the reason, and this // instance of flag is clear-on-read! abort_reason = i2c0->hw->tx_abrt_source; abort = (bool) i2c0->hw->clr_tx_abrt; if(timeout_check){ timeout = timeout_check(&ts); abort |= timeout; } tight_loop_contents(); } while (!abort && !(i2c0->hw->status & I2C_IC_STATUS_TFE_BITS)); uintAbort[intCNT] = abort_reason; uintState[intCNT] = i2c0->hw->status; uintTime[intCNT] = time_us_32(); intCNT++; if(abort) break; } do{ uintAbort[intCNT] = i2c0->hw->tx_abrt_source; uintState[intCNT] = i2c0->hw->status; uintTime[intCNT] = time_us_32(); intCNT++; }while(intCNT < 512); for(i=0;i<intCNT;i++){ if(i == 0){ serial_print_d(uintTime[0]); serial_print_b(uintAbort[0]); serial_print_b(uintState[0]); busy_wait_us_32(1000000); } else if((uintAbort[i] != uintAbort[i-1]) || (uintState[i] != uintState[i-1])){ serial_print_d(uintTime[i]); serial_print_b(uintAbort[i]); serial_print_b(uintState[i]); busy_wait_us_32(1000000); } } } } }
pico-test\try_06\CMakeLists.txt
add_executable(try_06 try_06.c ) # Add pico_stdlib library which aggregates commonly used features target_link_libraries(try_06 pico_stdlib hardware_i2c) # enable usb output, disable uart output pico_enable_stdio_usb(try_06 1) pico_enable_stdio_uart(try_06 0) # create map/bin/hex file etc. pico_add_extra_outputs(try_06)
Arduino IDE のシリアルモニタと接続することを想定して、シリアルモニタから何かを送ると I2C コマンドを実行してその合間合間でエラーフラグやステータスを示すレジスタの値を取得してシリアルモニタに返します。取得するのは以下の値です。
- time_us_32()
- abort_reason (= i2c0->hw->tx_abrt_source)
- i2c0->hw->status
i2c0->hw->... は RP2040 のレジスタへのアクセスを示します。レジスタの中身の意味は RP2040 のデータシートをご参照ください。データシートは公式ページの Documentation の RP2040 Datasheet から入手できます。
time_us_32() は 1 us 単位でカウントアップを続けるカウンタの値で、タイムスタンプとして取得しています。
i2c0->hw->tx_abrt_source は I2C コマンドの処理の中断が発生した際にその原因を示してくれるレジスタです。NACK のフラグはここに含まれます。
i2c0->hw->status は I2C モジュールの動作状態を示すレジスタです。
これらの値を I2C コマンド代入、I2C コマンド処理待ち明け、全ての処理が終わった後に取得します。特に全ての処理が終わった後、ここで NACK のフラグが立ち上ったりすると見込んでいるので用意したメモリがいっぱいになるまで繰り返し取得しています。
ちなみに取得した値のシリアルモニタへの返信ですが、全ての取得値を返すにはデータ量が多すぎるので、直前の値と比較して tx_abrt_source か status に変化があった場合のみ返すようにしています。
それでは動作結果です。まずは正常動作として Raspberry pi pico に Slave device を繋いで正常に I2C 通信を行った際にシリアルモニタに戻ってきた値です。
値は 3 行ごとにタイムスタンプ (Dec表示)、tx_abrt_source (Bit 表示), status (Bit 表示) を示しています。正常動作時ということで tx_abrt_source は全ての all 0 (異常無し) になっているので status の変遷のみを追っていきます。
1-3 行目は I2C コマンドを代入した直後です。Status の 5 bit 目と 0 bit 目が 1 になっていますが、これらはそれぞれ I2C が Master として動作中、 I2C モジュールが動作中であることを示しています。1 bit 目の 1 は代入した I2C コマンドを格納する Tx buffer にまだ空きがあることを示しています。今回のコードでは Tx buffer が Full になることは無いので、以降、Status の 1 bit 目の値には言及しません。
4-6 行目は先に代入した I2C コマンドの処理待ちを抜けたところです。直前の状態と比較して Status の 2 bit 目の値が 1 になっていますが、これは I2C コマンドを格納する Tx buffer が空になったことを示しています。元々、このコードは Tx buffer が空になる = I2C コマンドの処理が終わったと判定しているので、ここはコードの通りの動作となります。
7-9, 10-12 行目は 2 個目の I2C コマンドの代入後と処理後で、内容は 1-6 行目と同じなので説明は割愛します。
13-15 行目は全ての処理が終わった後となります。 Status の 5 bit 目と 0 bit 目が 0 になっており、どちらも I2C モジュールが Idle 状態であることを示しています。
これに対して、次は Raspberry pi pico から Slave device を外して I2C 通信を行った場合のシリアルモニタの表示です。
1-12 行目は正常時と同じです。前回の記事の実験では10-12 行目 に相当するところが Slave device の有無に関わらず同じになっているせいで NACK が検出できない、という結果でした。
13-15 行目は全ての処理が終わった後となります。Status の 5 bit 目と 0 bit 目が 0 になる前に、ようやくですが、tx_abrt_source に 1 が現れています。tx_abrt_source の 0 bit 目は NACK が検出されたことを示していて、ここでようやく、求めていた NACK のフラグが確認できました。31 - 23 bit は代入された I2C コマンドのうち、中断によって取り消されたコマンドの数を示すカウンタです。今回は NACK の検出により 2 個目に代入されたコマンドは実行されずに取り消されているのでそれがここにカウントされています。
16-18 行目では Status の 5 bit 目 と 0 bit 目が 0 、つまり、I2C モジュールが Idle 状態になって終わりです。
ここでようやく NACK フラグに出会うことができました。
NACK 検出処理の検証
先の実験で得た tx_abrt_source と status の値から、NACK 検出時の処理とレジスタ値の変遷は以下のようになっているようです。
1. 1 個目の I2C コマンドが代入されて Tx buffer に蓄えられる (1-3 行目)
2. Tx buffer に蓄えられたコマンドが実行中になり Tx buffer が空になる (4-6 行目)
3. 2 個目の I2C コマンドが代入されて Tx buffer に蓄えられる。1 個目のコマンドは実行中 (7-9 行目)
4. 1 個目のコマンドで NACK が検出され、中断処理により Tx buffer に蓄えられたコマンドが取り消されて Tx buffer が空になる (10-12 行目)
5. Tx buffer から取り消されたコマンドの数と NACK が tx_abrt_source に表示される (13-15 行目)
6. I2C モジュールが Idle 状態になる (16-18 行目)
SDK の I2C 関数が NACK を検出できなかった原因ですが、予想通り NACK のフラグが立つ前の tx_abrt_source の値を元に判定を行った結果だったようです。タイムスタンプで確認したところ、NACK の有無の判定に使用する tx_abrt_source の取得 (4) から NACK のフラグが立つ (5) までに 4 us の遅れがありました。
個人的には NACK 検出時の中断処理として Tx buffer を空にするなら (4) それより先に NACK のフラグを立てるくらいして欲しいところですが、マイコンがそういう動作をする以上、コードはそれに合わせて書くしかありません。
他に気付いたこととして、Tx buffer が空になるタイミングについて誤解していました。2 の Tx buffer が空になるタイミングは、今までは Tx buffer に蓄えられたコマンドの処理が完了したタイミングになると考えていましたが、1 と 2 の間のタイムスタンプの比較から Tx buffer にコマンドが蓄えられてからそれが空になるまでに 15 us しか要していないことから、実際には処理を開始したタイミングのようです。(正確には Tx buffer から取り出されたタイミングだと思われます)
ちなみにこのコードでは SCL の周波数を 100 kHz に設定しているので、Slave address の問い合わせ + R/W + ACK、または、8-bit のデータ伝送 + ACK の処理には少なくとも 100 kHz x 9 clock = 90 us を要します。1個目のコマンドの処理時間は正常時は Slave address の問い合わせの後に 8-bit のデータ伝送を行うので合わせてだいたい 200 us、Slave address の問い合わせで NACK を検出したとしてもそこまでの処理時間でだいたい 100 us を要します。
これで SDK の関数に NACK が検出できなかった原因が分かりました。次はこれを改造して NACK を検出できる関数にします。