yoshiyuki's blog

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

Raspberry Pi Pico / I2C 関数を改造する (1)

Raspberry Pi Pico を自作アプリで操作できる I2C ツールにするファームウェアを作成したのですが、この記事ではそれを作るまでに苦労した話を紹介します。

SDK の I2C 関数を調べる

Raspberry pi pico で I2C 通信をやろうと思ってpico-sdk の I2C 関数を使ってみたところ、基本的な動作には問題は無さそうなのですが、異常時の動作として Slave device を接続していない状態で I2C Write を実行した場合に NACK を検出してくれませんでした。しかし、関数の使い方を間違えているだけかもしれないので実際の関数の中身を見てみることにします。

関数の中には Raspberry pi pico の本体に当たる RP2040 というマイコンレジスタを直接読み書きしている箇所があるので、その意味を RP2040 のデータシートで調べる必要がありました。データシートは公式ページの Documentation の RP2040 Datasheet から入手できます。

関数のヘッダファイルと本体は pico-sdk の中の以下の場所にありました。

pico-sdk
  |-- src
         |-- rp2_common
                |-- hardware_i2c
                       |-- i2c.c
                       |-- include
                              |-- hardware
                                     |-- i2c.h

使った関数は i2c_write_timeout_us でしたが、i2c.h の記述によるとその中身は i2c_write_blocking_until という関数で、i2c.c によるとさらにその中身は i2c_write_blocking_internal という関数になっていました。その中身が以下の記述です。

static int i2c_write_blocking_internal(i2c_inst_t *i2c, uint8_t addr, const uint8_t *src, size_t len, bool nostop,
                                       check_timeout_fn timeout_check, struct timeout_state *ts) {
    invalid_params_if(I2C, addr >= 0x80); // 7-bit addresses
    invalid_params_if(I2C, i2c_reserved_addr(addr));
    // Synopsys hw accepts start/stop flags alongside data items in the same
    // FIFO word, so no 0 byte transfers.
    invalid_params_if(I2C, len == 0);
    invalid_params_if(I2C, ((int)len) < 0);

    i2c->hw->enable = 0;
    i2c->hw->tar = addr;
    i2c->hw->enable = 1;

    bool abort = false;
    bool timeout = false;

    uint32_t abort_reason;
    int byte_ctr;

    int ilen = (int)len;
    for (byte_ctr = 0; byte_ctr < ilen; ++byte_ctr) {
        bool first = byte_ctr == 0;
        bool last = byte_ctr == ilen - 1;

        i2c->hw->data_cmd =
                bool_to_bit(first && i2c->restart_on_next) << I2C_IC_DATA_CMD_RESTART_LSB |
                bool_to_bit(last && !nostop) << I2C_IC_DATA_CMD_STOP_LSB |
                *src++;

        do {
            // Note clearing the abort flag also clears the reason, and this
            // instance of flag is clear-on-read!
            abort_reason = i2c->hw->tx_abrt_source;
            abort = (bool) i2c->hw->clr_tx_abrt;
            if (timeout_check) {
                timeout = timeout_check(ts);
                abort |= timeout;
            }
            tight_loop_contents();
        } while (!abort && !(i2c->hw->status & I2C_IC_STATUS_TFE_BITS));

        // Note the hardware issues a STOP automatically on an abort condition.
        // Note also the hardware clears RX FIFO as well as TX on abort,
        // because we set hwparam IC_AVOID_RX_FIFO_FLUSH_ON_TX_ABRT to 0.
        if (abort)
            break;
    }

    int rval;

    // A lot of things could have just happened due to the ingenious and
    // creative design of I2C. Try to figure things out.
    if (abort) {
        if (timeout)
            rval = PICO_ERROR_TIMEOUT;
        else if (!abort_reason || abort_reason & I2C_IC_TX_ABRT_SOURCE_ABRT_7B_ADDR_NOACK_BITS) {
            // No reported errors - seems to happen if there is nothing connected to the bus.
            // Address byte not acknowledged
            rval = PICO_ERROR_GENERIC;
        } else if (abort_reason & I2C_IC_TX_ABRT_SOURCE_ABRT_TXDATA_NOACK_BITS) {
            // Address acknowledged, some data not acknowledged
            rval = byte_ctr;
        } else {
            //panic("Unknown abort from I2C instance @%08x: %08x\n", (uint32_t) i2c->hw, abort_reason);
            rval = PICO_ERROR_GENERIC;
        }
    } else {
        rval = byte_ctr;
    }

    // nostop means we are now at the end of a *message* but not the end of a *transfer*
    i2c->restart_on_next = nostop;
    return rval;
}

全ての動作を理解するのは面倒なので NACK の判定のところだけを追いかけてみます。NACK 時には関数の戻り値として PICO_ERROR_GENERIC を返してくるはずなので、戻り値を決める部分を探してみます。

    if (abort) {
        if (timeout)
            rval = PICO_ERROR_TIMEOUT;
        else if (!abort_reason || abort_reason & I2C_IC_TX_ABRT_SOURCE_ABRT_7B_ADDR_NOACK_BITS) {
            // No reported errors - seems to happen if there is nothing connected to the bus.
            // Address byte not acknowledged
            rval = PICO_ERROR_GENERIC;
        } else if (abort_reason & I2C_IC_TX_ABRT_SOURCE_ABRT_TXDATA_NOACK_BITS) {
            // Address acknowledged, some data not acknowledged
            rval = byte_ctr;
        } else {
            //panic("Unknown abort from I2C instance @%08x: %08x\n", (uint32_t) i2c->hw, abort_reason);
            rval = PICO_ERROR_GENERIC;
        }
    } else {
        rval = byte_ctr;
    }
    return rval;

戻り値が PICO_ERROR_GENERIC になる分岐が2つありますが、そのうち abort = true かつ abort_reason & I2C_IC_TX_ABRT_SOURCE_ABRT_7B_ADDR_NOACK_BITS = 1 の場合が NACK の判定になっているようです。次に abort と abort_reason を追いかけてみます。

        i2c->hw->data_cmd =
                bool_to_bit(first && i2c->restart_on_next) << I2C_IC_DATA_CMD_RESTART_LSB |
                bool_to_bit(last && !nostop) << I2C_IC_DATA_CMD_STOP_LSB |
                *src++;

        do {
            // Note clearing the abort flag also clears the reason, and this
            // instance of flag is clear-on-read!
            abort_reason = i2c->hw->tx_abrt_source;
            abort = (bool) i2c->hw->clr_tx_abrt;
            if (timeout_check) {
                timeout = timeout_check(ts);
                abort |= timeout;
            }
            tight_loop_contents();
        } while (!abort && !(i2c->hw->status & I2C_IC_STATUS_TFE_BITS));

i2c->hw->data_cmd のような記述は Raspberry pi pico の本体である RP2040 というマイコンレジスタへのポインタです。pico-sdk が環境を整えてくれてるおかげで、このような記述でレジスタにアクセスできるようになっています。各レジスタ、各Bit の意味は RP2040 のデータシートで解説されています。

i2c->hw->data_cmd への値の代入は I2C のコマンドの指示となります。ここで代入したコマンドは I2C コマンドの Tx バッファに格納されて順番に実行されることになるので、先に代入したコマンドの実行を待たずに続けて次のコマンドを代入することも可能なのですが、ここではコマンドを 1 つ入力したらその実行が終わるのを do..while で待っています。
do..while の while の条件のうち !(i2c->hw->status & I2C_IC_STATUS_TFE_BITS) ですが、i2c->hw->status の 2-bit 目は I2C コマンドの Tx バッファの状態を見張っていて、Tx バッファが空になると値が 1 になります。その時、この記述は頭についている反転記号により 0 になるので do..while ループを終了させます。Tx バッファが空になるということはコマンドが終了したということなので、これによってコマンドの終了を見張っているようです。
ちなみに I2C_STATUS_TFE_BITS は pico-sdk が用意した定数で 0x00000004 という値が設定されており、この値は i2c->hw->status というレジスタの TFE と名付けられた Bit (2-bit 目) に対応します。
while のもう一つの条件の !abort ですが、ループの中で abort に代入させている i2c->hw->clr_tx_abrt は値を読むことで abort (中断) フラグを消去するレジスタで、値としては、消去するフラグが無い場合は 0、何かフラグを消去した場合は 1 を返してきます。つまり、何らかの abort が発生していた場合に abort は true となり (代入時に bool に変換しているため)、反転した !abort は false になって do..while ループを終了させます。そして、abort_reason に代入している i2c->hw->tx_abrt_source がフラグの一覧です。

まとめると、この関数はI2C コマンドを 1つ入力するごとに、入力した I2C コマンドの実行が終わるか、または、何らかの異常により中断されるかを待って、中断された場合 (abort = true) はどの異常で中断されたかを abort_reason の値から確認して適切な戻り値を返すようになっているようです。abort_reason の元になる i2c->hw->tx_abort_source の 0-bit 目が NACK が検出されたことを示す bit となっているので、NACK の検出も abort_reason から必ず確認できるはずです。

しかし、実際に NACK は検出されなかったのでこれら記述のどこかに問題があるはずです。

原因探し

NACK が検出されなかった原因を調べます。
I2C 関数の戻り値は abort と abort_reason の値次第なので、戻り値を決める分岐に入るところで abort と abort_reason にどんな値が入っているのかを確認してみたいと思います。しかし、SDK のコードを弄るわけにはいかないので、コピペしたもので以下のコードを書いてみました。

プロジェクトフォルダ: pico-test

pico-test
 |-- pico_sdk.import.cmake
 |-- CMakeLists.txt
  -- try_04
      |-- try_04.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_04)

pico-test\try_04\try_04.c

#include <stdio.h>
#include "pico/stdlib.h"
#include "class/cdc/cdc_device.h"
#include "hardware/i2c.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 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;

    int i;

    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);

            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;
                }

                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));

                if(abort) break;
            }

            serial_print_b(abort_reason);
            serial_print_b(abort);
            serial_print_b(timeout);
            serial_print_b(i2c0->hw->status);
        }
    }
}

pico-test\try_04\CMakeLists.txt

add_executable(try_04
    try_04.c
)

# Add pico_stdlib library which aggregates commonly used features
target_link_libraries(try_04 pico_stdlib hardware_i2c)

# enable usb output, disable uart output
pico_enable_stdio_usb(try_04 1)
pico_enable_stdio_uart(try_04 0)

# create map/bin/hex file etc.
pico_add_extra_outputs(try_04)

Arduino IDE のシリアルモニタに接続することを想定して、シリアルモニタから何かを送ると Raspberry pi pico は I2C 通信を行って、完了時の abort や abort_reason の状態をシリアルモニタに返します。返ってくる値は以下の 4 つです。

  • abort_reason
  • abort
  • timeout
  • i2c0->hw->status

timeout と i2c0->hw->status は NACK 以外で do..while ループを抜ける条件となるので念のために確認しています。

まずは I2C 関数をそのまま試した時と同じように Slave 用の Sketch を書き込んだ ArduinoRaspberry pi pico を接続した状態でシリアルモニタから Raspberry pi pico に1文字ほど送って、このコードで I2C 通信が正常に行われることを確認しました。この時にシリアルモニタに返ってきた値は以下の通りです。

最初の 3 つの abort_reason, abort, timeout は何らかの異常の発生を示すフラグですが、ここでは正常に動作しているのでいずれも 0 になっています。 4つ目の i2c0->hw->status の値は 5, 2, 1, 0 bit が 1 になっていますが、これらは順に「I2C モジュールが Master として動作中」「I2C コマンドの Tx バッファが空」「Tx バッファがいっぱいになっていない」「I2C モジュールが動作中」を示します。このうち do..while ループを抜けたのは 2-bit 目の「Tx バッファが空」になったフラグが立ったためです。

次に Arduino (Slave device) を外した状態でシリアルモニタから Raspberry pi pico に1文字ほど送ってみました。この場合は Slave device が無いので NACK が検出されるはずなのですが、シリアルモニタに返ってきた値は以下でした。

I2C 通信が正常に行われた場合と同じく最初の 3 つの値が全て 0 になっています。NACK が検出されていません。正常動作時と比較して 4 つ目の値のうち 5, 0 bit が 0 になっていますが、これらは I2C モジュールが動作していないことを示しています。(2-bit 目が 1 になっているので do..while ループを抜けることはできます)

以上のように、I2C 関数が NACK を上手く検出できないのは何故かと調べた結果、そもそも NACK を検出すべきところで検出ができていなかったということが分かりました。

しかし、最初に記述を確認した時には NACK を検出する手順に間違いは無いように思いました。よって、どこかの記述が期待とは違った動作をしているのだと考えます。次はそれがどこかを絞り込んでいきます。