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 を検出する手順に間違いは無いように思いました。よって、どこかの記述が期待とは違った動作をしているのだと考えます。次はそれがどこかを絞り込んでいきます。

Raspberry Pi Pico / SDK の I2C 関数を使ってみる

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

pico-sdk の I2C 関数

I2C ツールを作るには当然、I2C 関数が必要です。I2C 関数のような基本的なものは、これも当然、 Raspberry pi pico の開発環境である pico-sdk に含まれているのでそれを使ってみます。

pico-sdk に含まれる関数は公式ページの Documentation のところにある Raspberry Pi Pico C/C++ SDK という pdf にまとめられていて、I2C 関数に関しては hardware_i2c の節に解説があります。Write/Read の関数はいくつかあるのですが、ここでは以下の関数を使ってみます。

static int i2c_write_timeout_us (i2c_inst_t *i2c, uint8_t addr, const uint8_t *src, size_t len, bool nostop, uint timeout_us)
static int i2c_read_timeout_us (i2c_inst_t *i2c, uint8_t addr, uint8_t *dst, size_t len, bool nostop, uint timeout_us)

これらは Timeout を持っているのが特徴です。以前に Arduino で I2C ツールを作ろうとした際に Timeout が無くて苦労したので私にとっては Timeout は必須です。

pico-sdk の I2C関数を使ってみる

pico-sdk の関数を使ってコードを書いてみました。

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

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

pico-test\try_03\try_03.c

#include <stdio.h>
#include "pico/stdlib.h"
#include "class/cdc/cdc_device.h"
#include "hardware/i2c.h"

#define SDA_PIN 20  // GP20 = Pin.26 = SDA
#define SCL_PIN 21  // GP21 = Pin.27 = SCL

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 byteWRITE[16];
    uint8_t byteREAD[16];

    int i2c_result;

    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){
            byteADR = 0x24;
            byteWRITE[0] = 0x00;
            byteWRITE[1] = inDAT[0] - '0';

            if((byteWRITE[1] > 0) && (byteWRITE[1] < 10)){
                i2c_result = i2c_write_timeout_us(i2c0, byteADR, byteWRITE, 2, false, 600);

                if(i2c_result == PICO_ERROR_GENERIC){
                    outDAT[0] = 'F';
                }
                else if(i2c_result == PICO_ERROR_TIMEOUT){
                    outDAT[0] = 'T';
                }
                else{
                    outDAT[0] = i2c_result + '0';
                }


            }
            else{
                i2c_result = i2c_write_timeout_us(i2c0, byteADR, byteWRITE, 1, false, 400);

                busy_wait_us_32(1000);

                if((i2c_result != PICO_ERROR_GENERIC) && (i2c_result != PICO_ERROR_TIMEOUT)){
                    i2c_result = i2c_read_timeout_us(i2c0, byteADR, byteREAD, 1, false, 400);
                }

                if(i2c_result == PICO_ERROR_GENERIC){
                    outDAT[0] = 'F';
                }
                else if(i2c_result == PICO_ERROR_TIMEOUT){
                    outDAT[0] = 'T';
                }
                else{
                    outDAT[0] = byteREAD[0] + '0';
                }
            }
            
            outDAT[1] = 0x0a;
            tud_cdc_write(outDAT, 2);
            tud_cdc_write_flush();            
        }
    }
}

pico-test\try_03\CMakeLists.txt

add_executable(try_03
    try_03.c
)

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

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

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

コードの流れですが、Raspberry pi pico を Arduiono IDE のシリアルモニタと接続することを想定して、シリアルモニタから 1 から 9 の数字を送ると Raspberry pi pico は受け取った数字を I2C で Slave device に書き込み、それ以外の文字を送ると Raspberry pi pico は Slave device のレジスタ値を読み出します。また、書き込み/読み出しを行った結果をシリアルモニタに返します。

USB serial 通信関係の関数については別の記事で解説しています。

I2C 関数について順に説明します。
まず、I2C 関数は i2c.h に含まれるので 、ヘッダファイルの include と CMakeLists.txt の target_link_libraries への追加が必要です。

    i2c_init(i2c0, 100000); // use i2c0 module, SCL = 100kHz

I2C HW block を有効にします。Raspberry pi pico は i2c0 と i2c1 の2個の I2C HW block を持っていて、ここでは i2c0 の方を有効にしています。ちなみに i2c0 と i2c1 とでは SCL/SDA として使用できる端子が異なる以外は特に違いはありません。
同時に i2c0 の伝送周波数を 100000 Hz = 100 kHz に設定しています。

#define SDA_PIN 20  // GP20 = Pin.26 = SDA
#define SCL_PIN 21  // GP21 = Pin.27 = SCL
    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

これらの記述では SCL/SDA 端子を設定しています。
i2c_init で HW block として i2c0 を選択したので、SCL/SDA として使用できる端子は下の Raspberry pi pico の端子図のうち I2C0 SCL, I2C0 SDA がタグ付けされている端子となり、ここではそのうち Pin.27 = GP21 を SCL, Pin.26 = GP20 を SDA として使おうとしています。
gpio_set_function では指定した端子にどの機能を割り当てるかを設定しています。Raspberry pi pico の各端子は、1つの端子が複数の機能を持っていて、そのうちのどの機能を使うかを選べるようになっています。上の記述では GP20, GP21 の機能として I2C を選択しています。前述の通り GP20, GP21 の I2C としての機能は i2c0 の SDA, SCL で固定されているため、この関数では I2C と選択するだけで良く、i2c0, I2c1, SDA, SCL を個別に選択する必要はありません (選ぶこともできません)。
gpio_set_pulls では端子の内蔵 Pull-up/Pull-down を設定しています。対象端子に対して 2番目の引数で Pull-up, 3番目の引数で Pull-down の有効/無効を設定します。I2C の SCL/SDA には Pull-up が必要で、この後の実験環境には外部に Pull-up が無いため、この記述では内蔵 Pull-up を有効にしていますが、外部の Pull-up を使用する場合は 2番目の引数を false にすることで内蔵 Pull-up を無効にできます。

f:id:ysin1128:20210902143536p:plain

            byteADR = 0x24;
            byteWRITE[0] = 0x00;
            byteWRITE[1] = inDAT[0] - '0';
                i2c_result = i2c_write_timeout_us(i2c0, byteADR, byteWRITE, 2, false, 600);

これらの記述で I2C Write を実行しています。

主役は i2c_write_timeout_us です。
i2c0 は I2C HW の指定です。
byteADR は書き込みを行う相手の Slave address (7 bit) の指定で、ここでは後の実験の通信相手である 0x24 を指定しています。
byteWRITE は書き込むデータを格納した配列のポインタの指定です。ここでは書き込むデータの格納用に byteWRITE[16] を用意しているので、そのポインタである byteWRITE を引数に置いています。
2 は、byteWRITE のうち実際に I2C 通信で書き込むデータの長さを指定しています。byteWRITE[16] は 16-byte の配列として用意されていますが、実際に書き込むのは最初の 2-byte、つまり byteWRITE[0] と byteWRITE[1] だけとなります。ここでは後の実験用に byteWRITE[0] に 0、byteWRITE[1] に Serial通信から受け取った数字 (Ascii) を数値に変換したものを代入しています。
false は、この関数による I2C 通信が終わった後に Stop condition を生成して SCL/SDA を離すよう指示しています。ここを true にすると通信が終わった後も SCL/SDA を保持して、他のデバイスが通信を始められないようにします。他のデバイスに割り込ませずに連続して別の通信を行いたい場合は true に設定します。
600 は Timeout の時間の指定で、600 us を意味します。ここでは I2C の伝送周波数を 100 kHz に設定していて、1-byte の伝送時間をだいたい 100 kHz x 8 bit + alpha = 100 us として、Slave address + 2-byte のデータを伝送することから 100 us x 3 = 300 us が必要な通信時間として、その倍の 600 us を Timeout の時間としています。
戻り値は伝送したデータ数、または、Error コードとなります。書き込みに成功した場合、書き込むデータ量は 2-byte なので 2 という値が返ってきます。NACK の場合は PICO_ERROR_GENERIC、 Timeout になった場合は PICO_ERROR_TIMEOUT が返ってきます。

                i2c_result = i2c_write_timeout_us(i2c0, byteADR, byteWRITE, 1, false, 400);

                busy_wait_us_32(1000);

                if((i2c_result != PICO_ERROR_GENERIC) && (i2c_result != PICO_ERROR_TIMEOUT)){
                    i2c_result = i2c_read_timeout_us(i2c0, byteADR, byteREAD, 1, false, 400);
                }

これらの記述では I2C Read を実行しています。

最初の i2c_write_timeout_us では、続く Read で読み出すレジスタのアドレスを指定しています。各引数の意味は先の解説と同じなので省略します。

i2c_write_timeout_us の戻り値が Error コードでなかった場合は i2c_read_timeout_us を実行します。
i2c_read_timeout_us の引数のうち、i2c0, byteARD, false, 400 の意味は i2c_write_timeout_us と同じです。
byteREAD は読み出したデータを格納する配列のポインタの指定です。ここでは読み出したデータの格納用に byteREAD[16] を用意しているので、そのポインタである byteREAD を引数に置いています。
1 は読み出すデータ量の指定です。ここでは I2C 通信で 1-byte だけ読み出して、その値を byteREAD[0] に格納します。例としてここで 2 を指定すると読み出すデータ量は 2-byte になり、読み出したデータは byteREAD[0] と byteREAD[1] に格納されます。
戻り値は読み出したデータ数、または、Error コードとなります。Error コードの意味は i2c_write_timeout_us と同じです。

i2c_write_timeout_us と i2c_read_timeout_us の間の 1000 us の Wait はおまじないです。私の実験では、この Wait が 0 だと i2c_read_timeout_us が必ず PICO_ERROR_GENERIC を返してきました。100 us でも同じく駄目で、1000 us でやっと i2c_read_timeout_us が Error コード以外を返すようになったのでそれをそのまま残しています。実際にどれだけの Wait があれば十分なのか、または、他に何か要因があるのかは分かりません。
i2c_write_timeout_us と i2c_read_timeout_us の間には i2c_write_timeout_us で指示した I2C 通信が完了するのを待つための Wait が必要です。i2c_write_timeout_us 関数は I2C HW block に I2C 通信を指示した後、それが終わるのを待たずに関数を終了させます。そのため、関数が終わった時点では I2C 通信はまさにまだ実行中で、そこに Wait 無しで i2c_read_timeout_us 関数を開始してしまうと、この関数は最初に I2C HW block をリセットしてしまうので、それで通信がおかしくなってエラーになっていたようです。 Wait の適正時間は厳密には 1-2 byte 分の通信時間 + 処理時間ですが、余裕をもって 4-byte 分くらいにしておけば大丈夫だと思われます。(コードは原因が分かる前に書いたので 1000 us = 約10 byte分になっていますが)

動作確認

Build して出来た try_03.uf2 を Raspberry pi pico に書き込んで動作確認です。通信相手の Slave device には Arduino に以下の記事の Sketch を書き込んだものを使用しました。なお、Arduino の内蔵 Pull-up 電圧 は 5 V で Raspberry pi pico の動作電圧 3.3 V より大きいので内蔵 Pull-up 無効の方を使用します。

ysin1128.hatenablog.com

Raspberry pi pico と Arduino は下図のように接続します。また、Raspberry pi pico、Arduino ともに PC に接続します。 (Arduino を PC に接続するのは電源のためなので、接続先は PC で無くても構いません)

f:id:ysin1128:20210902175145p:plain

PC に接続後、Arduino IDERaspberry pi pico の COM番号を指定してシリアルモニタを立ち上げ、"1", "a", "2", "a", "5", "a" の順で1文字ずつ、文字を送った結果が以下になります。

f:id:ysin1128:20210902175340p:plain

"1", "2", "5" を送ると Raspberry pi pico からは "2" が返ってきてシリアルモニタに表示されました。"2" は I2C Write で書き込んだデータの数が 2-byte であることを示していて、これは記述で指定した通りの数です。また、Slave device となった Arduino は Register address = 0x00 に値が書き込まれると書き込まれた値の数だけ LED を点滅させるようになっているので、この LED の点滅によってシリアルモニタから送った数字が狙い通り Slave device の Register address = 0x00 に書き込まれていることを確認できました。

また、"a" に対して返ってきたのは I2C Read で読み出した Slave device の Register address = 0x00 の値なので、"1" を書き込んだ直後の "a" に対しては "1", "2" を書き込んだ直後の "a" に対しては "2"、というように、狙った Register の値が、その変更も含めて読み出せていることが確認できました。

ここまでで正常時の動作は確認できたので、次は異常時の動作を確認します。Slave device を外して Raspberry pi pico のみを PC と接続した状態で シリアルモニタから "1", "a" を1文字ずつ送った結果が以下になります。

f:id:ysin1128:20210902180643p:plain

Slave device が無いので NACK と判定され、I2C Write/Read の関数の戻り値は PICO_ERROR_GENERIC になり、その場合、シリアルモニタに戻ってくるのは "F" (Fail) の文字となるはずで、"a" に対しては狙い通り "F" が返ってきているのですが、 "1" に対しては何故か正常時と同じ "2" が返ってきています。I2C Read の関数はちゃんと NACK を判定してくれていますが、 I2C Write の関数は NACK でも構わずにデータを送信してその結果を返してきているようです。pico-sdk の関数をそのまま使っているだけなので関数のバグの可能性が考えられます。

もう一つ、異常時の動作の確認として、また Raspberry pi pico だけを PC と接続した状態で SCL/SDA をそれぞれ GND に短絡した状態でシリアルモニタから "1", "a" を1文字ずつ送った結果が以下になります。

f:id:ysin1128:20210902182020p:plain

SCL/SDA が GND に短絡していると Raspberry pi pico は他の誰かが I2C 通信中と判断して通信が終わるのを待つのですが、強制短絡しているだけなので待っても終わるはずは無く、その間に Timeout が来るので I2C Write/Read 関数は PICO_ERROR_TIMEOUT を返して通信を諦めます。その場合、シリアルモニタに戻ってくるのは "T" (Timeout) の文字となります。"1" に対しても "a" に対しても狙い通り "T" が返ってきているので、Timeout 処理が正常になされていることが確認できました。

以上のように、NACK の判定に少し疑問が残りましたが、 pico-sdk の I2C 関数の動作確認は概ね成功です。

NACK の判定がうまくいかなかった理由は次の記事で詳しく調べています。
ysin1128.hatenablog.com

Raspberry Pi Pico / Serial 通信ができるまで

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

はじめに

自作アプリは元々、Arduino を I2C ツールとして操作するためのものでしたが、アプリから Arduino へのコマンドの送信、Arduino からアプリへのコマンドの結果の返信は USB 接続を経由した Serial 通信で行っていました。なので、実はツール側は アプリと Serial 通信ができて、アプリからのコマンドを実行できるものであれば Arduino でなくてもよく、実際に Teensy 4.0 でも同じことができています。今回はそれを Raspberry Pi Pico でやろうとしたのですが、最初の Serial 通信ができるまでに苦労がありました。

Serial 通信の関数が見付からない

いきなり大きな壁にぶつかりました。
Arduino や Teensy のコードは Arduino IDE で作成したので Serial 通信には Serial.read や Serial.write を使えば済みました。一方で Raspberry Pi Pico の場合、pico-sdk に含まれる関数は公式ページの Documentation のところにある Raspberry Pi Pico C/C++ SDK という pdf にまとめられているのですが、その中に Serial.read や Serial.write に相当するものがありません。いや、実際にはあったのかもしれませんが、私には見付けることができませんでした。
というわけで、Serial 通信の関数を何とかすることが最初のステップとなります。

Hello World

まず見付けたヒントは pico-examples の中の Hello world のサンプルコードの hello_usb.c です。名前からして USB に Hello world を出力してくれそうなので、試しにこれを Raspberry pi pico に書き込んでみることにします。pico-examples の中の全てのコードは開発環境構築の際に Build 済みなので pico-examples\build\hello_world\usb フォルダから hello_usb.uf2 を BOOTSEL mode で接続した Raspberry pi pico に書き込み、シリアルモニタで通信状況を確認すると、Raspberry pi pico から 1秒ごとに "Hello, world!" の文字が送られてきているのが確認できました。ちなみにシリアルモニタは Arduino IDE のものを使用しています。
f:id:ysin1128:20210823203551p:plain

これで少なくとも Serial 通信ができることは分かったので、hello_usb.c を参考にどんなコードで実現できるのかを見てみます。

hello_usb.c

/**
 * Copyright (c) 2020 Raspberry Pi (Trading) Ltd.
 *
 * SPDX-License-Identifier: BSD-3-Clause
 */

#include <stdio.h>
#include "pico/stdlib.h"

int main() {
    stdio_init_all();
    while (true) {
        printf("Hello, world!\n");
        sleep_ms(1000);
    }
    return 0;
}

CMakeLists.txt

if (TARGET tinyusb_device)
    add_executable(hello_usb
            hello_usb.c
            )

    # Pull in our pico_stdlib which aggregates commonly used features
    target_link_libraries(hello_usb pico_stdlib)

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

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

    # add url via pico_set_program_url
    example_auto_set_url(hello_usb)
elseif(PICO_ON_DEVICE)
    message(WARNING "not building hello_usb because TinyUSB submodule is not initialized in the SDK")
endif()

この中でキーとなるのは CMakeLists.txt の以下の記述です。

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

Raspberry pi pico は stdio として USB と UART を利用でき、この記述で USB を有効、UART を無効としています。USB の方は USB を経由した Serial 通信のことを意味しています。この設定のおかげで hello_usb.c の中では printf が stdio = USB経由の Serial通信への出力、つまり Serial.write に相当するものとなっています。逆に Serial.read の代わりには scanf が使えそうです。

ちなみに、これはだいぶ後になって気付いたのですが、これら内容は 公式ページの Documentation のところにある Getting Started with Raspberry Pi Pico の 'Chapter 4. Saying "Hello World" in C' でちゃんと解説されていました。

Serial 通信実験

Hello world の記述を利用して任意の Serial 通信ができるのかを実験してみます。以下がフォルダ構成とコードです。pico_sdk.import.cmake は pico-examples 直下にあるもののコピペです。

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

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

pico-test\try_01\try_01.c

#include "pico/stdlib.h"
#include <stdio.h>

int main() {
    const uint LED_PIN = PICO_DEFAULT_LED_PIN;
    char DIN;

    stdio_init_all();

    gpio_init(LED_PIN);
    gpio_set_dir(LED_PIN, GPIO_OUT);
    while (true) {
        scanf("%c", &DIN);

        gpio_put(LED_PIN, 1);
        sleep_ms(250);
        gpio_put(LED_PIN, 0);
        sleep_ms(250);

        sleep_ms(1000);
    }
}

pico-test\try_01\CMakeLists.txt

add_executable(try_01
        try_01.c
        )

# Pull in our pico_stdlib which pulls in commonly used features
target_link_libraries(try_01 pico_stdlib)

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


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

pico-examles から hello_world と blink を参考に作った、Serial通信で何かを受け取ったら LED が点滅するコードです。これを Build して出来上がった uf2 ファイルを Raspberry pi pico に書き込んで、シリアルモニタから適当な文字を送ると文字の数 (+ 改行の数) だけ LED が点滅するのを確認しました。実験は成功です。

関数発見

さて、ここまでで printf, scanf で Serial 通信ができることが分かったのですが、残念ながらこれらでは私の目的である自作アプリとの組み合わせで使うことができません。特に scanf が問題です。scanf は stdio からデータを取り込んでくれるのですが、データが無い場合はデータが来るまで延々と待ちます。
一方で、自作アプリが送るデータ量はコマンドによって変わります。 Arduino の Sketch ではデータの待ち時間に Timeout を設定し、一定時間、次のデータが来なければデータ伝送完了と判定させることでコマンドごとに異なるデータ量に対応させていましたが、scanf の待ち時間には Timeout が無いので同じことができません。scanf で 4-byte のデータを取り込む、というような記述はできるのですが、その場合、3-byte しかデータを送ってこないコマンドに対して scanf は 4-byte目が来るまで待ち続けてしまうのです。

というわけで別の関数を探すこととなりました。

結果としては目的の関数を見付けるに至ったのですが、色々とやり過ぎてどういう経緯で見付けたかを覚えていません。pico-sdk の中から関係がありそうなコードを片っ端から探し回ったり GitHub を探し回ったりぐぐったりした結果です。
見付けた関数で先の Serial通信のコードを改造したものが以下となります。

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

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

pico-test\try_02\try_02.c

#include <stdio.h>
#include "pico/stdlib.h"
#include "class/cdc/cdc_device.h"

int main() {
    const uint LED_PIN = PICO_DEFAULT_LED_PIN;

    uint8_t cntDAT;
    uint8_t getDAT[1];
    uint8_t inDAT[256];
    uint8_t outDAT[2];

    uint8_t i;

    stdio_init_all();

    gpio_init(LED_PIN);
    gpio_set_dir(LED_PIN, GPIO_OUT);

    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){
            for(i=0;i<cntDAT;i++){
                gpio_put(LED_PIN, 1);
                sleep_ms(250);
                gpio_put(LED_PIN, 0);
                sleep_ms(250);
            }

            outDAT[0] = cntDAT + '0';
            outDAT[1] = inDAT[0];

            tud_cdc_write(outDAT, 2);
            tud_cdc_write_flush();
        }

        busy_wait_us_32(100000);
    }
}

pico-test\try_02\CMakeLists.txt

add_executable(try_02
    try_02.c
)

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

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

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

Serial通信を受信すると受け取ったデータの数だけ LED が点滅し、データ数と、受け取ったデータの最初の 1-byte の値を Serial通信で返します。お目当ての関数はヘッダファイル class/cdc/cdc_device.h に含まれる tud_cdc_available, tud_cdc_read, tud_cdc_write, tud_cdc_write_flush でした。なお、cdc_device.h の include は CMakeLists.txt の pico_enable_stdio_usb を有効にしないとエラーになります。

uint32_t tud_cdc_available (void);

入力バッファに格納されたデータの数 (Byte) を返します。入力バッファには Serial通信で受信したデータが格納されます。格納されたデータは読み出すと入力バッファから削除されます。よって、この関数が返す値は Serial通信で受信して、まだ読み出していないデータの数となります。
Arduino IDE の Serial.available に相当します。

uint32_t tud_cdc_read (void* buffer, uint32_t bufsize);

入力バッファからデータを読み出します。引数の buffer には読み出したデータを格納する変数のポインタを指定します。bufsize は読み出すデータ数 (Byte) を指定します。
Arduino IDE の Serial.read に相当します。

uint32_t tud_cdc_write (void* buffer, uint32_t bufsize);

出力バッファにデータを書き込みます。引数の buffer には書き込むデータを格納する変数のポインタを、bufsize には書き込むデータ数 (Byte) を指定します。ただし、この関数は Serial通信の出力バッファにデータを書き込むだけなので、この関数だけでは Serial 通信によるデータ送信は行われません。

uint32_t tud_cdc_write_flush (void);

出力バッファに蓄えられたデータを Serial 通信で送信します。この関数を実行する前に tud_cdc_write で出力バッファに送信したいデータを書き込んでおく必要があります。
Arduino IDE の Serial.write は tud_cdc_write と tud_cdc_write_flush を合わせたものとなります。

上記のコードでこれら関数が期待通りに働いてくれることが確認できたので Raspberry pi pico と自作アプリを組み合わせて使う目途が立ちました。

余談 / Windowsアプリと Raspberry Pi Pico の Serial 通信

余談です。
この後、自作アプリ向けのコードを書き進めていったのですが、デバッグでどうしても上手く動いてくれません、というか、動いてくれません。どうもアプリと Raspberry pi pico 間の Serial 通信そのものが上手くいっていないようです。

原因の絞り込みのために Raspberry pi pico にこの記事で作った try_02.uf2 を書き込んでアプリから送ったデータに対してどういう応答をするかを確かめたところ、Raspberry pi pico は受け取ったデータの数だけ LED を点滅させるのですが、受け取ったデータ数と最初の 1-byte をアプリ側に返してきません。アプリから Raspberry pi pico への Serial 通信には問題は無く、逆方向の通信に何か問題があるようです。

しかし、この記事の通り Arduino IDE のシリアルモニタと Raspberry pi pico 間であれば順方向/逆方向ともに Serial 通信ができています。また、Raspberry pi pico に代わって Arduino に同様の Sketch を書き込んでアプリから同じデータを送ってみたところ、Arduino はちゃんと LED を点滅させ、アプリに Serial 通信を返してきました。というわけで Raspberry pi pico にもアプリにも致命的な問題があるわけではないようです。

であれば、Serial 通信の設定に何か問題があるのではないかと思うのですが、特に Raspberry pi pico の Serial 通信は手探りで見付けた関数を使っているくらいなのでどんな設定になっているのかすら分かりません。そこで Windowsコマンドプロンプトから Mode コマンドを使ってみることにしました。Mode は各デバイスの設定を表示するコマンドで、「mode COMx」と打てば COMx の設定を確認することができます。これで アプリと Raspberry pi pico が通信する際の COM の設定とシリアルモニタと Raspberry pi pico が通信する際の COM の設定を比べてみます。ちなみに、対象の COM が使用中だと Mode コマンドで設定を確認することができないため、通信させた後でアプリなりシリアルモニタなりを閉じた後で Mode コマンドを実行しています。

問題がある場合 / アプリと Raspberry pi pico の通信後
f:id:ysin1128:20210827160358p:plain

問題が無い場合 / シリアルモニタと Raspberry pi pico の通信後
f:id:ysin1128:20210827160445p:plain

DTR サーキットと RTS サーキットの設定に違いあることが分かりました。そして、これが原因でした。

アプリは Visual Studio 2019 の VB.net で作成しているのですが、そこでは SerialPort の DTR と RTS が、デフォルトでは共に無効になっています。Arduino や Teensy 4.0 とはこれらが無効のままでも通信ができていたのですが、Raspberry pi pico の場合は無効では通信してくれませんでした。アプリの方の DTR, RTS を有効にした結果、アプリと Raspberry pi pico 間で順方向/逆方向ともに Serial 通信ができるようになりました。

関数を作ってみた

シリアルモニタでそのまま値を読むことができる Serial.print のような関数が欲しくなったので作ってみました。詳しくは以下の記事で。
ysin1128.hatenablog.com

Arduino Nano / Teensy 4.0 / Raspberry Pi Pico / MAX32625PICO 比較

自作の USB-I2C変換ツールとして使う場合の各マイコンボードの機能と性能の比較です。

比較表

Arduino Nano Teensy 4.0 Raspberry Pi Pico MAX32625PICO Note
信号レベル 5.0 V 3.3 V 3.3 V 3.3V
I2C Yes Yes Yes Yes Sketch/Firm で SCL = 100 kHz に固定
SPI Yes (最大SCK = 8 MHz, Word幅 = 8 bit) Yes (最大SCK = 50 MHz, Word幅 = 32 bit) Yes (最大SCK = 10 MHz, Word幅 = 8 bit) Yes
UART No Yes Yes Yes Sketch/Firm で Baudrate = 115.2 kbps に固定
GPIO パターン幅: 1ms to 65s パターン幅: 1us to 65ms パターン幅: 1us to 65ms パターン幅: 1us to 65ms 最小パターン幅では処理が間に合わないため、実際の最小パターン幅は設定値より大きくなる
Clock 最大8MHz 最大50MHz 最大62.5MHz 最大xxMHz
Data Logger No Yes, Sample rate = 4 MHz Yes, Sample rate = 1 MHz No 4096 events

感想

  • 5V の信号を扱うのであれば Arduino Nano の一択です。
  • Teensy 4.0 と Raspberry Pi Pico を比べると処理速度は Teensy 4.0 の方が上です。GPIO で時間的により細かいパターンを生成できるのは Teensy 4.0 の方です。
  • Teensy 4.0 の SPI の Word幅は 32 bit です。8/16/24 bit だけ送ろうとしても必ず 32 bit 単位 (データが無い bit の値は all 0 として) で伝送されます。

Raspberry Pi Pico で I2C

元々、Arduino を使った USB-I2C 変換ツールとアプリを作って公開していましたが、今度は Raspberry Pi Pico を使って同じことができるファームを作成しました。Windows/Android アプリは Arduino 用と同じものが使用できます。

入手方法

Amazon やスイッチサイエンスなどで購入できます。以下の記事をご参照ください。

ysin1128.hatenablog.com

Arduino版との違い

  • USBコネクタが Micro-B (Arduino版 は Mini-B)

スマホでお馴染みの Micro-B で接続できます。ものすごく便利です。

  • I2C, SPI 他の信号の High Level が 3.3 V (Arduino版 は 5 V)

レベルシフタで 3.3 V に変換する手間が無くなります。ただし、逆に 5 V では使用できません。外部から 5 V の信号を印加すると耐圧超過で壊れます。

  • GPIO コマンドの信号幅の単位がマイクロ秒 (Arduino版はミリ秒)

GPIO コマンドにおいて信号幅に 00 01 を入力した場合、Arduino版では 1 ms を意味していましたが、Raspberry Pi Pico では 1 us を意味します。ただし、 1 us では短すぎて処理が間に合わないようで、実用できそうなのは 50 us 程度からとなります。

  • Clock 出力コマンドの最大周波数が 62.5 MHz (Arduino版は 8 MHz)
  • UART 対応
  • Logic Data Logger 対応

ファームウェア

下記の記事をご参照ください。

ysin1128.hatenablog.com

セットアップ

1. PC に Raspberry Pi Pico を BOOTSEL mode で接続する
Raspberry Pi Pico 上の BOOTSELボタンを押しながら PC に接続すると Raspberry Pi Pico が USB メモリのようにストレージとして認識されます。下図は認識例で RPI-RP2 (D:) として認識されています。


2. Raspberry Pi Pico にファームウェアを書き込む
USB ストレージとして認識された Raspberry Pi Pico にファームウェア (pico_ctrl_Rxx.uf2) をドラッグ&ドロップします。通常はドロップした直後に Raspberry Pi Pico がストレージとして認識されなくなり、消えます。これでセットアップは完了です。

各ピンの役割


アプリと使い方

対応するアプリのバージョンに注意してください。

  • Windows アプリ ver. 0.53 以降
  • Android アプリ ver. 3.5 以降

Arduino版と同じように使用できますが前述の Arduino版との違いには注意してください。
Windows アプリでは Startボタンをクリックする前に Config タブの変更が必要です。Keyword にはデバイスマネージャにおける Raspberry Pi Pico の認識名、または、その一部を書き込んでください。私の場合は「USB Serial Device (COM14)」という名前で認識されていたので「USB Serial」を Keyword にしました。「USB Serial Device」や「COM14」を Keyword にすることも可能です。Arduino System Clock Frequency には 100 を記入してください。

ysin1128.hatenablog.com
ysin1128.hatenablog.com
ysin1128.hatenablog.com

Teensy 4.0 / GPIO の制御

Teensy 4.0 の GPIO を制御して High/Low 信号やパターン信号を任意に作れるようにしようとして苦労した話です。

設定

苦労しなくても GPIO なんて digitalWrite と digitalRead で制御できると思うのですが、ここでも I2C の時と同じくレジスタの読み書きで制御を行います。趣味です。

端子の選択

GPIO として使用する端子を選びます。電源端子等の例外を除くと基本的にはどの端子も GPIO として使用できるので、ここでは単純に番号の小さい方から 2番と3番と出力、4番と5番と入力として使うことにします。

f:id:ysin1128:20210208114524p:plain

上で選んだ基板上の端子に対応するマイコンの端子名を回路図から確認します。

f:id:ysin1128:20210208114640p:plain

#2 EMC_04
#3 EMC_05
#4 EMC_06
#5 EMC_08

これらの端子に対して設定を行っていきます。

Multiplexer

Teensy の本体であるマイコン IMXRT1060 の各端子には色々な機能を任意に割り当てられるようになっているので、まずは上で選んだ端子に対して GPIO を割り当てるように設定する必要があります。I2C の時と同様に Multiplexer (MUX) の設定で GPIO モジュールと各端子とを繋げてやります。以下が設定の記述となります。

IMXRT_REGISTER32_t *port_iomuxc;

void setup() {
  port_iomuxc = &IMXRT_IOMUXC;  //0x401F8000

  // #2 = D2, GPIO_EMC_04
  port_iomuxc->offset024 = 0x00000015; // SIN = 1, MUX_MODE = 101 (GPIO4_IO04)

  // #3 = D3, GPIO_EMC_05
  port_iomuxc->offset028 = 0x00000015; // SIN = 1, MUX_MODE = 101 (GPIO4_IO05)

  // #4 = D4, GPIO_EMC_06
  port_iomuxc->offset02C = 0x00000015; // SIN = 1, MUX_MODE = 101 (GPIO4_IO06)

  // #5 = D5, GPIO_EMC_08
  port_iomuxc->offset034 = 0x00000015; // SIN = 1, MUX_MODE = 101 (GPIO4_IO08)
}

port_iomuxc->offsetxxx で特定のレジスタを指定できる仕組みは I2C の時にも説明していますが、簡単に言うと Arduino IDE がこの記述で対象のレジスタを指定できるように下準備してくれているからです。
レジスタの詳細、つまり、どのレジスタがどの端子のどんな設定に対応しているかは IMXRT1060 の Reference Manual 参照です。
この記述で指定した各レジスタはそれぞれの端子の MUX の設定に対応し、2-0 bit に 101 = 0x5 を書き込むことで接続先に GPIO モジュールを選択できます。また 4 bit に 1 を書き込むことで端子の MUX の選択が固定されます。
ちなみに、4端子とも接続先は GPIO4 モジュールとなっていました。

アナログ設定

続いて端子のアナログ設定を行います。これは端子そのものの動作速度や Pull-up/Pull-down 抵抗の設定となります。以下は、上の MUX の記述にアナログ設定の記述を追加したものです。

IMXRT_REGISTER32_t *port_iomuxc;

void setup() {
  port_iomuxc = &IMXRT_IOMUXC;  //0x401F8000

  // #2 = D2, GPIO_EMC_04
  port_iomuxc->offset024 = 0x00000015; // SIN = 1, MUX_MODE = 101 (GPIO4_IO04)
  port_iomuxc->offset214 = 0x000000B0; // PKE = 0 (disable pull-up/Keeper), Speed = 10 (Fast), DSE = 110, SRE = 0 (Slow)

  // #3 = D3, GPIO_EMC_05
  port_iomuxc->offset028 = 0x00000015; // SIN = 1, MUX_MODE = 101 (GPIO4_IO05)
  port_iomuxc->offset218 = 0x000000B0; // PKE = 0 (disable pull-up/Keeper), Speed = 10 (Fast), DSE = 110, SRE = 0 (Slow)

  // #4 = D4, GPIO_EMC_06
  port_iomuxc->offset02C = 0x00000015; // SIN = 1, MUX_MODE = 101 (GPIO4_IO06)
  port_iomuxc->offset21C = 0x000000B0; // PKE = 0 (disable pull-up/Keeper), Speed = 10 (Fast), DSE = 110, SRE = 0 (Slow)

  // #5 = D5, GPIO_EMC_08
  port_iomuxc->offset034 = 0x00000015; // SIN = 1, MUX_MODE = 101 (GPIO4_IO08)
  port_iomuxc->offset224 = 0x000000B0; // PKE = 0 (disable pull-up/Keeper), Speed = 10 (Fast), DSE = 110, SRE = 0 (Slow)
}

12 bit が Pull-up/Pull-down/Keeper の有効化ですが、今回はこれら機能を無効にするので 0 を書き込んでいます。ちなみに、有効にする場合は 15-13 bit でさらに設定が必要です。
11 bit が Open Drain の有効化で I2C 端子として使用する場合は必要ですが、今回はロジック信号の制御なので 0 を書き込んで無効化しています。
7-6 bit は端子の動作速度の設定です。値を 10 とすると 150 MHz に対応します。
5-3 bit は High/Low を出力するドライバのオン抵抗を設定します。値が大きいほどオン抵抗は小さくなります。
0 bit は Slew Rate の設定です。 0 は Slow、1 は Fast となります。
今のところ用途があるわけではなく、 7-0 bit にどんな設定が必要か分からないので、これらは Default の値に設定しています。

以上で端子の設定は完了です。

Clock の設定

I2C で学びましたが Teensy のマイコン IMXRT1060 は省電力のために使わないモジュールへの Clock の供給を停止する機能があり、Arduino IDE が気を利かせてその機能で Clock を停止している場合があるので、必要な Clock が供給されるようしっかりと設定しておく必要があります。
今回、使用する端子に繋がっているのが GPIO4 モジュールであることは上で確認済みなので、GPIO4 モジュール への Clock の供給を制御する Clock Gating を以下の記述で設定します。

  CCM_CCGR3 |= 0x00003000; // CG6 = 11, enable GPIO4 clock

CCM_CCGR3 の 13-12 bit の値に 11 を書き込むと GPIO4 モジュールに Clock が供給されます。供給される Clock の周波数は 108 MHz のはずですが、この Clock 周波数が決まるまでの設定に関しては面倒だったのであまりよく確認しておらず、間違っているかもしれません。

GPIO モジュールの設定

残るは GPIO モジュールの中身の設定です。とは言え実は、GPIO モジュールの中では IO の選択くらいしか設定することがありません。

IMXRT_REGISTER32_t *port_gpio;

void setup() {
  port_gpio = &IMXRT_GPIO4; //0x401C4000

  port_gpio->offset004 = 0x00000030; // GPIO4_IO04/05 = Output, GPIO4_IO06/08 = Input
}

今回、使用するモジュールは GPIO4 となっており、port_gpio->offset004 で GPIO4 モジュールの各 IO を入力、または、出力に設定します。port_gpio->offset004 は 32 bit のレジスタで、各 bit が GPIO4 モジュールの 32個の IO の設定に対応し、値を 1 にすると出力、 0 にすると入力に設定されます。
ここでは基板上の 2番、3番の端子を出力に設定します。2番、3番の端子は GPIO4 モジュールの 04 番 と 05 番の IO に繋がるので port_gpio->offset004 の 5-4 bit の値に 1 を書き込んで出力に設定しています。

GPIO の制御

最後に GPIO の制御方法の確認です。ここでは出力端子の出力の High/Low を制御する方法と、各端子の High/Low 状態を読み出す方法を確認します。

uint32_t uintTMP;

IMXRT_REGISTER32_t *port_gpio;

void setup() {
  port_gpio = &IMXRT_GPIO4; //0x401C4000

  port_gpio->offset004 = 0x00000030; // GPIO4_IO04/05 = Output, GPIO4_IO06/08 = Input
}

void loop() {
  port_gpio->offset000 = 0x00000030; // GPIO4_IO04/05 = High
  uintTMP = port_gpio->offset008;

  delay(1000);
}

出力の High/Low は port_gpio->offset000 への値の書き込みで制御します。port_gpio->offset000 は 32 bit のレジスタで、各 Bit が GPIO4 モジュールの各 IO の High/Low 制御に相当します。
今回の制御対象は port_gpio->offset004 で出力に設定した 04, 05 番の IO なので、上の記述では対応する Bit に 1 を書き込むことで 04, 05 番の出力を High に設定しています。反対にこれらの Bit に 0 を書き込むと 04, 05 番の出力は Low になります。
端子の High/Low 状態は port_gpio->offset008 のレジスタ値で確認できます。port_gpio->offset008 は 32 bit のレジスタで、各 Bit が GPIO4 モジュールの各 IO の High/Low 状態を示しています。port_gpio->offset004 で入力に設定しても出力に設定しても関係無く High/Low 状態を読み取ることができます。
今回は基板上の 4番, 5番端子を入力端子として使用し、それに対応するのは GPIO4 モジュールの 06 番, 08 番 の IO となるので、port_gpio->offset008 の 8 bit と 6 bit が、レジスタを読み出した瞬間に各端子に入力されている信号の High/Low 状態を示します。また、この時に 5-4 bit で基板上の 2番, 3番端子の High/Low の状態を読み出すことができます。これらは出力に設定しているので基本的には port_gpio->offset000 で設定した High/Low 状態と同じものが読み取れます。

実験と失敗

ここまでで作った設定を使って GPIO の動作確認をしてみました。結果を先に言うと失敗でした。

実験用 Sketch

GPIO の動作実験を以下の記述で行いました。
setup には上で作った設定を放り込んでいます。
loop は PC から Arduino IDE のシリアルモニタと通信を行うことを想定して、シリアルモニタから 1 から 3 の数字を受け取ったら基板上の 2番端子、3番端子の High/Low を操作して、基板上の 2番から5番までの端子の High/Low 状態をシリアルモニタに返すようにしています。また、念のためにシリアルモニタからどの数字を受け取ったかをシリアルモニタに返すようにしています。1 から 3 の数字以外のものを受け取った場合は 2番端子、3番端子の出力をどちらも Low にします。

byte inDAT;
int cntDAT;

IMXRT_REGISTER32_t *port_iomuxc;
IMXRT_REGISTER32_t *port_gpio;

void setup() {
  Serial.begin(9600);

  port_iomuxc = &IMXRT_IOMUXC;  //0x401F8000
  port_gpio = &IMXRT_GPIO4; //0x401C4000

  CCM_CCGR3 |= 0x00003000; // CG6 = 11, enable GPIO4 clock

  // #2 = D2, GPIO_EMC_04
  port_iomuxc->offset024 = 0x00000015; // SIN = 1, MUX_MODE = 101 (GPIO4_IO04)
  port_iomuxc->offset214 = 0x000000B0; // PKE = 0 (disable pull-up/Keeper), Speed = 10 (Fast), DSE = 110, SRE = 0 (Slow)

  // #3 = D3, GPIO_EMC_05
  port_iomuxc->offset028 = 0x00000015; // SIN = 1, MUX_MODE = 101 (GPIO4_IO05)
  port_iomuxc->offset218 = 0x000000B0; // PKE = 0 (disable pull-up/Keeper), Speed = 10 (Fast), DSE = 110, SRE = 0 (Slow)

  // #4 = D4, GPIO_EMC_06
  port_iomuxc->offset02C = 0x00000015; // SIN = 1, MUX_MODE = 101 (GPIO4_IO06)
  port_iomuxc->offset21C = 0x000000B0; // PKE = 0 (disable pull-up/Keeper), Speed = 10 (Fast), DSE = 110, SRE = 0 (Slow)

  // #5 = D5, GPIO_EMC_08
  port_iomuxc->offset034 = 0x00000015; // SIN = 1, MUX_MODE = 101 (GPIO4_IO08)
  port_iomuxc->offset224 = 0x000000B0; // PKE = 0 (disable pull-up/Keeper), Speed = 10 (Fast), DSE = 110, SRE = 0 (Slow)

  // GPIO module
  port_gpio->offset004 = 0x00000030; // GPIO4_IO04/05 = Output, GPIO4_IO06/08 = Input
}

void loop() {
  cntDAT = 0;

  while(Serial.available()){
    inDAT = Serial.read();
    cntDAT++;
    delay(5);
  }

  if(cntDAT > 0){
    inDAT = inDAT - 0x30;
    
    if(inDAT == 1){
      port_gpio->offset000 = 0x00000010;
      Serial.print("1-");
    }
    else if(inDAT == 2){
      port_gpio->offset000 = 0x00000020;      
      Serial.print("2-");
    }
    else if(inDAT == 3){
      port_gpio->offset000 = 0x00000030;            
      Serial.print("3-");
    }
    else{
      port_gpio->offset000 = 0x00000000;                  
      Serial.print("0-");
    }

    delay(1);

    Serial.print(port_gpio->offset008 & 0x00000170);
    Serial.print("\n");    
  }

  delay(1000);
}

配線

実験として、2番端子 (出力) を 4番端子 (入力) に、3番端子 (出力) を 5番端子 (入力) に接続します。これで High/Low状態の読み出しにおいて 2番と4番、3番と5番それぞれの値が一致するはずです。

f:id:ysin1128:20210315150118p:plain

実験結果

f:id:ysin1128:20210315150311p:plain

シリアルモニタから 0, 1, 2, 3 の数字を1文字ずつ順番に送った結果です。
各行ともハイフン (-) 以前の数字が変わっているので期待通りの動作はしてくれているようです。なので、おかしいのは GPIO の High/Low 状態を読み出した値であるハイフン以後の数字が各行とも 0 であることです。シリアルモニタから送る数字によって 2番、3番端子の High/Low 状態は変わるはずなのですが、シリアルモニタから送った数字に関わらず (さらにその数字が正しく認識されているにも関わらず) 2番から5番の High/Low 状態を読み出した値が 0、つまり全て Low になっています。
実際の出力はどうかと 2番, 3番端子の電圧をテスターで測ってみましたが、シリアルモニタから 3 の数字を送って両方とも High を出力するように設定しても両端子の出力は Low のままでした。
逆に 4 番端子に外部から 3.3 V = High を印加した状態でシリアルモニタから 3 の数字を送ると、シリアルモニタに返ってきた数字は 0-64 となり、64 = 0x00000040 から 4番端子 = 06番の IO が High になっていることが確認できました。

というわけで失敗です。
問題は GPIO の出力の制御の方のようですが、制御といっても端子を出力に設定して High/Low を指示するだけだったので、うまく動かなかった理由が分かりません。ここから長い試行錯誤の繰り返しでした。

修正と実験成功

以下が実験に成功した Sketch と実験結果です。

byte inDAT;
int cntDAT;

IMXRT_REGISTER32_t *port_iomuxc;
IMXRT_REGISTER32_t *port_gpio;

void setup() {
  Serial.begin(9600);

  port_iomuxc = &IMXRT_IOMUXC;  //0x401F8000
  port_gpio = &IMXRT_GPIO9; //0x4200C000

  // CCM_CCGR3 |= 0x00003000; // CG6 = 11, enable GPIO4 clock

  // #2 = D2, GPIO_EMC_04
  port_iomuxc->offset024 = 0x00000015; // SIN = 1, MUX_MODE = 101 (GPIO4/9_IO04)
  port_iomuxc->offset214 = 0x000000B0; // PKE = 0 (disable pull-up/Keeper), Speed = 10 (Fast), DSE = 110, SRE = 0 (Slow)

  // #3 = D3, GPIO_EMC_05
  port_iomuxc->offset028 = 0x00000015; // SIN = 1, MUX_MODE = 101 (GPIO4/9_IO05)
  port_iomuxc->offset218 = 0x000000B0; // PKE = 0 (disable pull-up/Keeper), Speed = 10 (Fast), DSE = 110, SRE = 0 (Slow)

  // #4 = D4, GPIO_EMC_06
  port_iomuxc->offset02C = 0x00000015; // SIN = 1, MUX_MODE = 101 (GPIO4/9_IO06)
  port_iomuxc->offset21C = 0x000000B0; // PKE = 0 (disable pull-up/Keeper), Speed = 10 (Fast), DSE = 110, SRE = 0 (Slow)

  // #5 = D5, GPIO_EMC_08
  port_iomuxc->offset034 = 0x00000015; // SIN = 1, MUX_MODE = 101 (GPIO4/9_IO08)
  port_iomuxc->offset224 = 0x000000B0; // PKE = 0 (disable pull-up/Keeper), Speed = 10 (Fast), DSE = 110, SRE = 0 (Slow)

  // GPIO module
  port_gpio->offset004 = 0x00000030; // GPIO9_IO04/05 = Output, GPIO9_IO06/08 = Input
}

void loop() {
  cntDAT = 0;

  while(Serial.available()){
    inDAT = Serial.read();
    cntDAT++;
    delay(5);
  }

  if(cntDAT > 0){
    inDAT = inDAT - 0x30;
    
    if(inDAT == 1){
      port_gpio->offset000 = 0x00000010;
      Serial.print("1-");
    }
    else if(inDAT == 2){
      port_gpio->offset000 = 0x00000020;      
      Serial.print("2-");
    }
    else if(inDAT == 3){
      port_gpio->offset000 = 0x00000030;            
      Serial.print("3-");
    }
    else{
      port_gpio->offset000 = 0x00000000;                  
      Serial.print("0-");
    }

    delay(1);

    Serial.print(port_gpio->offset008 & 0x00000170);
    Serial.print("\n");    
  }

  delay(1000);
}

f:id:ysin1128:20210315165359p:plain

シリアルモニタから 1 を送ると 80 = 0x00000050 が返ってきています。つまり、6 bit と 4 bit の値が 1 になっているということで、これは2番端子から High が出力されてそれを 2番端子自身が検出し、それに繋がっている 4番端子も High を検出していることを意味します。シリアルモニタから 2, 3 を送って返ってきた値も、該当する Bit の値が 1 になっています。GPIO の制御に成功しました。

修正点

修正したのは以下の記述です。

  port_gpio = &IMXRT_GPIO9; //0x4200C000

  // CCM_CCGR3 |= 0x00003000; // CG6 = 11, enable GPIO4 clock

port_gpio に代入する GPIO モジュールのレジスタ位置のポインタを GPIO4 モジュールのものから GPIO9 モジュールのものに変更しました。
IMXRT1060 は Standard-speed GPIO (GPIO1-5 モジュール) の他に High-speed GPIO (GPIO6-9 モジュール) を備えていて、GPIO1-4 モジュールと GPIO6-9 モジュールは排他使用となっています。IMXRT1060 のデフォルトでは GPIO1-4 が使用されるようになっているのですが Arduino IDE がそれを勝手に GPIO6-9 を使用するように変更していたようです。
LPI2C の Clock Gating でも同じようなことでやられましたが、レジスタの状態は IMXRT1060 の Reference Manual よりも実際のレジスタ値で確認するべきでした・・・。
ともあれ、GPIO4 が上手く動かなかったのは GPIO4 の代わりに GPIO9 が有効になっていたからだったので、レジスタ位置のポインタを GPIO9 モジュールに変更するだけで解決しました。

また、GPIO9 モジュールの Clock は Gating が無い (常に供給されている) ので、不要となった CCM_CCGR3 の行はコメントアウトしています。

GPIO9 モジュールに気付いたのは、残念ながら自力ではなくカンニングです。 Tennsy4.0用の digitalWrite コマンドの記述を見に行ったところ、GPIO6-9 しか使われていなかったことで気付きました。ちなみに下記のファイルです。
/hardware/teensy/avr/cores/teensy4/
digital.c
core_pins.h

それなら最初から digitalWrite を使えという話ですが、全くその通りです。私は趣味で苦労しただけなので、ただ GPIO の制御をしたいだけであれば digitalWrite/digitalRead を使うことをお勧めします。



おまけ。GPIO の他、I2C や SPI も制御できる Sketch とツールを作成しました。
ysin1128.hatenablog.com