yoshiyuki's blog

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

Visual Studio で Raspberry Pi Pico と通信する Windows アプリを作る (1)

Arduiono や Raspberry Pi Pico を操作して I2C 通信などを実行する Windows アプリを作成したのですが、この記事ではその Windows アプリの基礎となる USB-Serial 通信の制御部分を紹介します。

Visual Studio の入手

Windows アプリは Visual Studio で作成しました。Visual Studio の無償版の Visual Studio Community は以下のサイトの上の方の「Visual Studio 無償版」をクリックした先からダウンロードできます。

visualstudio.microsoft.com

Visual Studio のインストール

Visual Studio のインストールではどのコンポーネントをインストールするかを選ぶ必要があります (インストール後に追加も可能)。今回の Windows アプリの作成のためには「.NET デスクトップ開発」を使うので、これにチェックを入れてインストールします。

新しいプロジェクトの作成

Visual Studio のインストールが完了したら、さっそくアプリの作成に取り掛かります。Visual Studio を開いたら、まずは「新しいプロジェクトのの作成」をクリックします。

次にプロジェクトのテンプレートを選択します。Windows アプリを作成する場合は「Windows フォームアプリ」を選択すれば良いのですが、作成する言語には C# と Vitual Basic の二つがあり、どちらの言語を使うかもここで選択する必要があります。ここでは C# を選択します。

最後にプロジェクト名を設定します。プロジェクト名はなんでもいいのですが、それがそのままソリューション名になり、実行ファイルのファイル名にもなるのでご注意ください。

「次へ」をクリックするとプロジェクトが作成され、作業画面が開きます。作業画面は真ん中に「Form 1」という名前の、のっぺらぼうの Window が置かれただけのものとなります。ここでアプリを作成します。

Hello World!

お試しで簡単なアプリを作ってみます。最初に作るプログラムといえば Hello World なので、ボタンをクリックしたらテキストボックスに「Hello World!」と出力されるアプリを作成します。
まずは空のフォームにボタンとテキストボックスを置きます。これは左のバーからツールボックスを開いて「Button」と「TextBox」をそれぞれドラッグするだけです。お試しなので配置は適当です。

フォーム上では配置した Button や TextBox の形をマウスでドラッグして変更できるのですが、TextBox はデフォルトでは1行となっているため横には広がっても縦には広がりません。そこでこれを複数行に拡張します。
フォームに置いた TextBox をクリックして選択した状態にすると右下に TextBox のプロパティが表示されるので、その中の「Multiline」という項目を探し、Default ではこれが「False」になっているので「True」に変更します。これで TextBox が複数行に対応するようになったので、マウスで縦にも広げられるようになります。

フォームの準備ができたので、ボタンをクリックした際の動作を記述します。動作を記述する場所はさっきフォーム上に置いたボタン (button 1) をクリックすると飛んで、そこにはすでに button 1 をクリックする際に実行される関数の記述が用意されています。ただし、関数の中身は空なので、このままでは button 1 をクリックしても何も起きません。そこで、この中にテキストボックスに Hello World! と表示する記述を追加します。

        private void button1_Click(object sender, EventArgs e)
        {
		textBox1.AppendText("Hello World!\r\n");
        }

button1_Click はフォーム上の button 1 がクリックされた際に呼び出される関数です。これは自動生成されるので、この記述でどうやって button 1 のクリックと関連付けられているのかは私も理解していません。
textBox1.AppendText はテキストボックスにカッコ内の文字を追加出力する関数です。ここで textBox1 はフォームに配置したテキストボックスに割り当てられた名前です。今回は1個しか配置していませんが、フォームに複数のテキストボックスを配置した場合はこの名前を変えることで出力先のテキストボックスを変更できます。
出力する文字は "Hello World!" と改行 (\r\n) です。

記述ができたのでアプリを起動して動作確認を行います。上のメニューの「Debug」「Any CPU」と並んだ先の 「(緑三角) (プロジェクト名: ここでは RPP_CTRL_CS)」をクリックすると Debug mode で作成したアプリが起動します。起動したアプリの方 (作成画面のフォームではなく) で button1 をクリックするたびにテキストボックスに Hello World! が出力されれば動作成功です。

Shokz OpenMove と OpenComm のマイク性能の比較

OpenRun Pro を加えて再比較を行いました。
ysin1128.hatenablog.com

この記事ではオンラインミーティング (Teams会議や Zoom会議) での使用を想定した Shokz の骨伝導イヤホンのマイク性能を比較しています。

比較対象

Shokz OpenMove
Shokz 骨伝導イヤホンのエントリーモデルです。USB Type-C で充電できるのはこのモデルだけです (他のモデルは専用の充電ケーブルが必要)。性能に関しては思った以上に必要十分でした。

Shokz OpenComm (実験に使用したモデル)
Shokz OpenComm 2 (現行モデル)
Shokz 骨伝導ヘッドセット。ヘッドセットなので顔の横に伸びるマイクが付いています。また、16 時間の最大使用時間を持つのが特徴です。

Logicool H340
有線の USB ヘッドセットです。オンラインミーティングで何年も問題無く使用できています。

Panasonic RZ-S30W
フルワイヤレスイヤホンです。外出先での急なオンラインミーティングで使用したことはありますが、マイクには期待できません。持っていたので参考として。

マイク性能比較 / 騒音なし

環境

騒音源の無い室内でマイクの音声を録音しました。

録音結果

Shokz OpenMove

Shokz OpenComm (実験に使用したモデル)
Shokz OpenComm 2 (現行モデル)

Logicool H340

Panasonic RZ-S30W

マイク性能比較 / 騒音あり

騒音源

室内で、以下の動画を最大音量で再生したスマートホン (Xperia 5) を 2m 後方にスマホスタンドでスピーカーを上に向けた状態で置いてマイクの音声を録音しました。


www.youtube.com

録音結果

Shokz OpenMove

Shokz OpenComm (実験に使用したモデル)
Shokz OpenComm 2 (現行モデル)

Logicool H340

Panasonic RZ-S30W

マイク性能比較 / 騒音あり / 意地悪試験

騒音源

室内で、以下の動画を最大音量で再生したスマートホン (Xperia 5) を自分の正面に置きながらマイクの音声を録音しました。


www.youtube.com

録音結果

Shokz OpenMove

Shokz OpenComm (実験に使用したモデル)
Shokz OpenComm 2 (現行モデル)

Logicool H340

Panasonic RZ-S30W

感想

Logicool の有線ヘッドセットが一番良いのは当然としても、Shokz はいずれのモデルでもマイク性能に全く問題が無いことが分かりました。驚いたのが OpenMoe と OpenComm の差の無さです。特に騒音環境となればマイクが顔の横に伸びている OpenComm の方が有利かと思いきや、 Logicool の有線ヘッドセットのようなはっきりとした差が無いどころか、私の耳では差そのものを捉えることすらできませんでした。

雑談

オンラインミーティングには安心確実な Logicool の有線ヘッドセットを使ってきたのですが、持ち運ぶには大きすぎるので出先では使うことが出来なかったり、有線なのでオンラインミーティング中に身動きが制限されすぎていたり、あと、案外、周りの人がオンラインミーティング中でも普通に話しかけてくるなどの不便があったので、小型軽量で無線で耳を塞がない骨伝導イヤホンに興味を持ちました。

オンラインミーティングが目的であれば最初から OpenComm を買うべきだったのですが、骨伝導イヤホンそのものが初めてで自分に合っているかどうか分からないものに約2万円の OpenComm を買うのはためらわれたので、お試しのつもりで買ったのが OpenMove でした。駄目でも有線ヘッドセットを使い続ければ良いし、OpenMove は家用にでもすれば良いかと考えて。

ところが OpenMove は思った以上に快適でした。無線の恩恵もさることながら (有線ヘッドセットのケーブルは本当にうっとうしかった・・・)、スピーカーの音声と一緒に周りの声や音も聞こえるのはオフィスでは案外助かります。当初の心配もどこへやら、有線ヘッドセットは早々に OpenMove に取って代わられて引退となりました。

ここで次の心配事として浮上したのが OpenMove のマイク性能です。何度かオンラインミーティング中に発言した感触としては問題は無さそうだったのですが、オフィスでは周辺で別の人間が別のミーティングで喋っている場合もありますし、出先でオンラインミーティングに参加する場合は周囲の声や騒音なんて当たり前です。そんな環境の中で自分の声だけを相手に伝えるためにはマイクが顔の横に伸びた OpenComm が必要です。

こんなことならどうして最初から OpenComm を買わなかったのかと後悔しながらも、追加で買わずにいられないほど骨伝導イヤホンから離れられなくなっている自分がいました。

この記事はそうして購入した OpenComm のマイク性能が OpenMove と比べていかに勝っているかを示すために作成したのですが、まさかこんな結果になるとは欠片も思っていませんでした。

Logic Data Logger の使用例

この記事では自作アプリの使い方を説明しています。自作アプリに関しては以下の記事をご参照ください。
ysin1128.hatenablog.com

はじめに

ACTRL の Logic Data Logger 機能は Teensy 4.0 または Raspberry Pi Pico との組み合わせで使用可能です。
この機能は Android 版アプリでは使用できません。Windows 版アプリをご使用ください。

接続

Logic 波形を取得したい端子を Teensy の 20, 21, 22, 23 のいずれかに接続してください。Raspberry Pi Pico の場合は GP8, GP9, GP10, GP11 のいずれかに接続してください。また、GND も必ず接続してください。

ここでの動作例では Teensy 4.0 を使って自分自身の I2C 波形を取得してみます。18 (SDA) を 20, 19 (SCL) を 21 に接続します。自分自身との接続なので GND の接続は不要です。

動作例のための準備 (通常は不要です)

動作例において SCL/SDA の Pull-up を外付けするのが面倒なので、SCL/SDA の内蔵Pull-up を有効にする隠しコマンドを使用します。下図のように「Command Input」に「cfg 02 01」と記入して 「Start」 をクリックすると SCL/SDA の内蔵 Pull-up が有効になります。このコマンドは Raspberry Pi Pico でも有効です。

波形取得例

1. 「Logger」タブの「Start Logger」をクリックすると Logger window が開きます。(「Start Logger」ボタンは Teensy 4.0, Raspberry Pi Pico と接続した場合のみ有効になります)

2. Logger window の「Start Logging」をクリックすると波形取得が開始されます (ボタンの表記が「Stop Logging and Show result」に変わります)。以降、Teensy は 20, 21, 22, 23 のいずれかに接続された信号で発生する High/Low 間遷移を記録します。ここではまだ取得した波形は表示されません。

3. 動作例として、20, 21 に接続した SDA/SCL を駆動してその波形を取得させます。SDA/SCL を駆動させるために ACTRL の Main window の「CMD_Multi」タブの「Command Input」に「w 24 00 01」と記入して「Start」をクリックすると、Slave device が接続されていないので NACK が検出されます。この間、Teensy は 20, 21 で SDA/SCL の駆動波形を取得、記録しています。ここでもまだ波形は表示されません。

4. Logger window に戻って「Stop Logging and Show result」をクリックすると取得した SDA/SCL の波形が表示されます。

波形の表示範囲

波形の表示範囲は以下のいずれかの方法で調節できます。

1. 「Start time」「End time」のテキストボックスに表示したい範囲を記入して「Ser Scale」をクリックする。
2. 「Fit Scale」をクリックする。(波形全体が表示されます)
3. 波形に対して右ドラッグで範囲を指定する。(指定した範囲が赤線のガイドで示されます)

I2C 波形解析

取得した波形が I2C の場合、その内容を解析することができます。

1. 「SDA」「SCL」のプルダウンで、L0/1/2/3 の波形のうちのどれが SDA/SCL かを選択します。この例では L1 が SCL、L0 が SDA です。
2. 「Exec Analysis」ボタンをクリックすると解析結果がテキストボックスに表示されます。

この例の解析結果は 時刻 0 で Start condition が検出され、続いて Slave address = 0x24 に対して Write Command を送ったが (24(W))、それに対して ACK が返らず (N)、Stop Condition で通信が終了したことを示しています。

複数の通信が連続して行われた場合も解析可能です。

Raspberry Pi Pico で使用する場合の注意

Raspberry Pi Pico は SCL = 100 kHz のような低速でも波形を取りこぼすことがあります。

Raspberry Pi Pico / Serial 通信の関数を作る

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

はじめに

自作アプリから Raspberry pi pico を操作するために PC - Raspberry pi pico 間の USB Serial 通信を利用したかったのに、SDK の資料 (Raspberry pi pico の公式ページの Datasheets の pico/raspberry-pi-pico-c-sdk.pdf) に Serial 通信の関数が無い、というところから始まって、pico-sdk の中からそれを見付けた、というのが以前の話でした。今回は、見付かった Serial 関数を使うに際して必要だった処理を関数にまとめたので、それを紹介します。

作成した Serial 通信の関数

作成した関数と、動作確認用のコードは以下になります。作成した関数はヘッダにまとめています。

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

pico-test
 |-- pico_sdk.import.cmake
 |-- CMakeLists.txt
  -- try_08
      |-- try_08.c
       -- CMakeLists.txt
  -- module_serial
      |-- module_serial.h
      |-- module_serial.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_08)
add_subdirectory(module_serial)

pico-test\try_08\try_08.c

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

#include "../module_serial/module_serial.h"

int main() {
    uint8_t cntDAT;
    uint8_t getDAT[1];
    uint8_t inDAT[256];
    uint32_t uintTMP;
    int i;

    stdio_init_all();

    while (true) {
        cntDAT = 0;

        while(serial_available() > 0){
            serial_read(getDAT, 1);
            inDAT[cntDAT] = getDAT[0];
            cntDAT++;
            busy_wait_us_32(10000);
        }

        if(cntDAT > 0){
            // send back
            inDAT[cntDAT] = 0x0A;
            serial_write(inDAT, cntDAT+1);

            // serial_print
            serial_print("Hello world! ");
            serial_print_ln("Hello world!");

            uintTMP = 0xA5CC6633;

            // serial_print_32bit_d
            serial_print_32bit_d(uintTMP);
            serial_print(" ");
            serial_print_32bit_d_ln(uintTMP);

            // serial_print_32bit_b
            serial_print_32bit_b(uintTMP);
            serial_print(" ");
            serial_print_32bit_b_ln(uintTMP);

            // Load test
            for(i=0; i<1000; i++){
                uintTMP = i;
                serial_print_32bit_d(uintTMP);
                serial_print_ln(" Hello world!");
            }
        }
    }
}

pico-test\try_08\CMakeLists.txt

add_executable(try_08
    try_08.c
)

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

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

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

pico-test\module_serial\module_serial.h

#ifndef MODULE_SERIAL_H
#define MODULE_SERIAL_H

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

uint32_t serial_available();

uint32_t serial_read(uint8_t *read_data, int read_data_length);

uint32_t serial_write_1byte(uint8_t write_data);
uint32_t serial_write(uint8_t *write_data, int write_data_length);

uint32_t serial_print(char *write_data);
uint32_t serial_print_ln(char *write_data);

uint32_t serial_print_32bit_d(uint32_t write_data_32bit);
uint32_t serial_print_32bit_d_ln(uint32_t write_data_32bit);

uint32_t serial_print_32bit_b(uint32_t write_data_32bit);
uint32_t serial_print_32bit_b_ln(uint32_t write_data_32bit);

#endif

pico-test\module_serial\module_serial.c

#include "./module_serial.h"

uint32_t serial_available(){
    return tud_cdc_available();
}

uint32_t serial_read(uint8_t *read_data, int read_data_length){
    return tud_cdc_read(read_data, read_data_length);
}

uint32_t serial_write_1byte(uint8_t write_data){
    int cntTO = 0;
    uint8_t write_data_buffer[1] = {write_data};

    while(1 > tud_cdc_write_available()){
        busy_wait_us_32(1000);
        cntTO++;

        if(cntTO == 1000) return 0;
    }

    uint32_t uintTMP = tud_cdc_write(write_data_buffer, 1);
    tud_cdc_write_flush();

    return uintTMP;
}

uint32_t serial_write(uint8_t *write_data, int write_data_length){
    int i;
    uint32_t uintTMP = 0;

    for(i=0;i<write_data_length;i++){
        uintTMP += serial_write_1byte(write_data[i]);
    }

    return uintTMP;
}

uint32_t serial_print(char *write_data){
    return serial_write(write_data, strlen(write_data));
}

uint32_t serial_print_ln(char *write_data){
    int len = strlen(write_data);
    char write_data_buffer[len + 1];
    int i;

    for(i=0; i<len; i++){
        write_data_buffer[i] = write_data[i];
    }

    write_data_buffer[len] = 0x0A;

    return serial_write(write_data_buffer, len + 1);
}

void convert_32bit_d(uint32_t data_32bit, uint8_t *data_out, int *data_digit){
    uint32_t denominator = 10;
    uint32_t data_buffer = data_32bit;
    int i;

    for(i=0; i<9; i++){
        if(data_buffer < denominator){
            *data_digit = i + 1;
            denominator /= 10;
            break;
        }

        if(i == 8){
            *data_digit = i + 2;
            break;
        }
        else{
            denominator *= 10;
        }
    }

    for(i=0; i<*data_digit; i++){
        data_out[i] = '0' + data_buffer / denominator;
        data_buffer %= denominator;
        denominator /= 10;
    }

}

uint32_t serial_print_32bit_d(uint32_t write_data_32bit){
    uint8_t out[11];
    int digit;

    convert_32bit_d(write_data_32bit, out, &digit);

    return serial_write(out, digit);
}

uint32_t serial_print_32bit_d_ln(uint32_t write_data_32bit){
    uint8_t out[11];
    int digit;

    convert_32bit_d(write_data_32bit, out, &digit);
    out[digit] = 0x0A;

    return serial_write(out, digit + 1);
}

void convert_32bit_b(uint32_t data_32bit, uint8_t *data_out){
    int i;

    for(i=0; i<32; i++){
        if((data_32bit & (0x00000001 << (31-i))) > 0){
            data_out[i] = '1';
        }
        else{
            data_out[i] = '0';
        }
    }
}

uint32_t serial_print_32bit_b(uint32_t write_data_32bit){
    uint8_t out[32];

    convert_32bit_b(write_data_32bit, out);

    return serial_write(out, 32);
}

uint32_t serial_print_32bit_b_ln(uint32_t write_data_32bit){
    uint8_t out[33];

    convert_32bit_b(write_data_32bit, out);

    out[32] = 0x0A;

    return serial_write(out, 33);
}

pico-test\module_serial\CMakeLists.txt

add_library(module_serial
        module_serial.c
        )

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

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

作成した関数の解説

uint32_t serial_available(){
    return tud_cdc_available();
}

Serial 通信の受信 Buffer に格納されているデータ数を返します。関数名を合わせることが目的で、中身は pico-sdk の関数そのままになっています。

uint32_t serial_read(uint8_t *read_data, int read_data_length){
    return tud_cdc_read(read_data, read_data_length);
}

Serial 通信の受信 Buffer からデータを読み出します。これも関数名を合わせることが目的で、中身は pico-sdk の関数そのままです。

uint32_t serial_write_1byte(uint8_t write_data){
    int cntTO = 0;
    uint8_t write_data_buffer[1] = {write_data};

    while(1 > tud_cdc_write_available()){
        busy_wait_us_32(1000);
        cntTO++;

        if(cntTO == 1000) return 0;
    }

    uint32_t uintTMP = tud_cdc_write(write_data_buffer, 1);
    tud_cdc_write_flush();

    return uintTMP;
}

uint32_t serial_write(uint8_t *write_data, int write_data_length){
    int i;
    uint32_t uintTMP = 0;

    for(i=0;i<write_data_length;i++){
        uintTMP += serial_write_1byte(write_data[i]);
    }

    return uintTMP;
}

Serial 通信でデータ送信を行います。serial_write の方がメイン関数で、ここでデータを 1-byte 単位に分けて、実際の処理は serial_write_1byte が行います。
pico-sdk の関数ではデータ送信のためはまず tud_cdc_write でデータを送信 Buffer に格納して tud_cdc_write_flush で出力するという2段階の手順が必要なので、この関数でそれを一つにまとめています。また、tud_cdc_write_avairable で送信 Buffer に格納されたデータ数を見張り、一つ前のデータが出力されるのを待ってから次のデータを送信 Buffer に送ることで送信 Buffer が溢れることがないようにしています。

uint32_t serial_print(char *write_data){
    return serial_write(write_data, strlen(write_data));
}

uint32_t serial_print_ln(char *write_data){
    int len = strlen(write_data);
    char write_data_buffer[len + 1];
    int i;

    for(i=0; i<len; i++){
        write_data_buffer[i] = write_data[i];
    }

    write_data_buffer[len] = 0x0A;

    return serial_write(write_data_buffer, len + 1);
}

シリアル通信で文字列を送信するための関数です。_ln は文字列の最後に改行を付加します。先に作った serial_write 関数では文字列の送信があまりに面倒だったので、耐えられなくなってこれを作成しました。

void convert_32bit_d(uint32_t data_32bit, uint8_t *data_out, int *data_digit){
    uint32_t denominator = 10;
    uint32_t data_buffer = data_32bit;
    int i;

    for(i=0; i<9; i++){
        if(data_buffer < denominator){
            *data_digit = i + 1;
            denominator /= 10;
            break;
        }

        if(i == 8){
            *data_digit = i + 2;
            break;
        }
        else{
            denominator *= 10;
        }
    }

    for(i=0; i<*data_digit; i++){
        data_out[i] = '0' + data_buffer / denominator;
        data_buffer %= denominator;
        denominator /= 10;
    }

}

uint32_t serial_print_32bit_d(uint32_t write_data_32bit){
    uint8_t out[11];
    int digit;

    convert_32bit_d(write_data_32bit, out, &digit);

    return serial_write(out, digit);
}

uint32_t serial_print_32bit_d_ln(uint32_t write_data_32bit){
    uint8_t out[11];
    int digit;

    convert_32bit_d(write_data_32bit, out, &digit);
    out[digit] = 0x0A;

    return serial_write(out, digit + 1);
}

Serial 通信で数値を 10進数の文字列に変換したものを送信するための関数です。serial_print_32bit_d がメイン関数で _ln は最後に改行を追加します。入力は uint32_t で、扱える数値は 32-bit = 4,294,967,295 までとなります。
これも、先に作った serial_write ではレジスタ値を読み出してシリアルモニタで確認するのに苦労したことから、シリアルモニタでそのまま読める形に変換するようにしました。

void convert_32bit_b(uint32_t data_32bit, uint8_t *data_out){
    int i;

    for(i=0; i<32; i++){
        if((data_32bit & (0x00000001 << (31-i))) > 0){
            data_out[i] = '1';
        }
        else{
            data_out[i] = '0';
        }
    }
}

uint32_t serial_print_32bit_b(uint32_t write_data_32bit){
    uint8_t out[32];

    convert_32bit_b(write_data_32bit, out);

    return serial_write(out, 32);
}

uint32_t serial_print_32bit_b_ln(uint32_t write_data_32bit){
    uint8_t out[33];

    convert_32bit_b(write_data_32bit, out);

    out[32] = 0x0A;

    return serial_write(out, 33);
}

シリアル通信で数値を2進数の文字列に変換したものを送信するための関数です。serial_print_32bit_b がメイン関数で、_ln は最後に改行を付加します。入力は uint32_t です。
pico-sdk の I2C 関数を調べていた時レジスタの各 Bit の動きを追うのがたいへんだったので、見やすくなるようにこれを作成しました。

動作確認

前出のコードをビルドして生成された try_08.uf2 を Raspberry pi pico に書き込んで関数の動作確認を行いました。動作確認では Arduino IDE のシリアルモニタを使用して Raspberry pi pico との Serial 通信を行いました。
以下が結果です。
f:id:ysin1128:20211210115846p:plain

動作確認用のコードは、シリアルモニタから何かを送ると作成した Serial 通信の関数が色々と送信してくるようになっています。動作確認では「Hello Raspberry Pi Pico!」と送ってみました。
結果の1行目は serial_read と serial_write の動作確認で、正常に動作すればシリアルモニタから送信した文字列の末尾に改行を付加したものが Raspberry pi pico から返ってきます。実際に送信した「Hello Raspberry Pi Pico!」の文字列が返ってきています。
2行目は serial_print と serial_print_ln の動作確認で、どちらも「Hello world!」の文字列を、一方は改行無しで、他方が改行付きでシリアルモニタに送ってきています。
3行目は serial_print_32bit_d と serial_print_32bit_d_ln の動作確認で、0xA5CC6633 を10進数に変換した文字列を改行無し、改行付きの順でシリアルモニタに送ってきています。ちなみに 0xA5CC6633 = 2,781,636,147 です。
4行目は serial_print_32bit_b と serial_print_32bit_b_ln の動作確認で、0xA5CC6633 を2進数 (Binary) に変換した文字列を改行無し、改行付きの順でシリアルモニタに送ってきています。ちなみに 0xA5CC6633 = 1010_0101_1100_1100_0110_0110_0011_0011 です。
5行目以降は高負荷テストとして「(通し番号) Hello world!」の文字列を 1,000 回、繰り返しシリアルモニタに送って、大量にデータを送っても動作に問題が起きないことを確認しています。以下のように 999 まで問題無く送信できています。
f:id:ysin1128:20211210131109p:plain

以上のように想定通りの動作が問題無く確認できました。

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

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

SDK の I2C 関数が NACK を検出してくれなかったので関数の中身を調べたら NACK 検出を示すレジスタが値を変える前にNACKの有無を判定していたためと分かったので、この記事では SDK の関数を元に NACK を検出できる関数を作って、無事に動作確認ができるまでをまとめています。

改造した関数

作った関数と動作確認用のコードは以下になります。I2C 関数はヘッダにまとめています。

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

pico-test
 |-- pico_sdk.import.cmake
 |-- CMakeLists.txt
  -- try_07
      |-- try_07.c
       -- CMakeLists.txt
  -- module_i2c
      |-- module_i2c.h
      |-- module_i2c.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_07)
add_subdirectory(module_i2c)

pico-test\try_07\try_07.c

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

#include "../module_i2c/module_i2c.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[16];

    uint8_t byteADR;
    uint8_t byteWRITE[16];
    uint8_t byteDAT[16];
    uint8_t i2c_result;

    int i;
    int intCNT;

    stdio_init_all();
    i2c_setup(i2c0, 100, 21, 20, true); // HW=i2c0, 100kHz, SCL=GP21, SDA=GP20, Internal pullup

    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(byteADR, byteWRITE, 2, true);

                if(i2c_result == RC_NACK){
                    outDAT[0] = 'F';
                }
                else if(i2c_result == RC_TIMEOUT){
                    outDAT[0] = 'T';
                }
                else{
                    outDAT[0] = 'P';
                }
            }
            else{
                i2c_result = i2c_read(byteADR, byteDAT, 1, true);

                if(i2c_result == RC_NACK){
                    outDAT[0] = 'F';
                }
                else if(i2c_result == RC_TIMEOUT){
                    outDAT[0] = 'T';
                }
                else{
                    outDAT[0] = byteDAT[0] + '0';
                }
            }
            
            outDAT[1] = 0x0a;
            tud_cdc_write(outDAT, 2);
            tud_cdc_write_flush();            

        }
    }
}

pico-test\try_07\CMakeLists.txt

add_executable(try_07
    try_07.c
)

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

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

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

pico-test\module_i2c\module_i2c.h

#ifndef MODULE_I2C_H
#define MODULE_I2C_H

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

#include "hardware/i2c.h"
#include "hardware/timer.h"
#include "pico/timeout_helper.h"

extern i2c_inst_t *reg_i2c;

extern uint16_t i2c_scl_freq_kHz;

extern uint8_t SDA_PIN;
extern uint8_t SCL_PIN;

extern const uint8_t RC_SUCCESS;
extern const uint8_t RC_TIMEOUT;
extern const uint8_t RC_NACK;

void i2c_scl_sda_pullup(bool enable_pullup);

void i2c_setup(i2c_inst_t *i2c_hw_block, uint16_t scl_freq_khz, uint8_t gp_for_scl, uint8_t gp_for_sda, bool scl_sda_pullup);

bool i2c_tx_timeout(uint64_t timeout_microseconds);

bool i2c_rx_timeout(uint64_t timeout_microseconds);

bool i2c_ck_nack();

void i2c_clr_intr();

uint8_t i2c_nack();

uint8_t i2c_abort();

uint8_t i2c_write(uint8_t slave_adr, uint8_t *write_data, uint8_t write_data_length, bool gen_stop);

uint8_t i2c_read(uint8_t slave_adr, uint8_t *read_data, uint8_t read_data_length, bool start);

uint8_t i2c_read_full(uint8_t slave_adr, uint8_t *reg_adr, uint8_t reg_adr_length, uint8_t *read_data, uint8_t read_data_length);

#endif

pico-test\module_i2c\module_i2c.c

#include "./module_i2c.h"

i2c_inst_t *reg_i2c = i2c0;

uint16_t i2c_scl_freq_kHz = 100;

uint8_t SDA_PIN = 20;    // GP20 = Pin.26 = SDA
uint8_t SCL_PIN = 21;    // GP21 = Pin.27 = SCL

uint8_t const RC_SUCCESS = 0xFF;
uint8_t const RC_TIMEOUT = 0x80;
uint8_t const RC_NACK = 0x00;

void i2c_scl_sda_pullup(bool enable_pullup){
    gpio_set_pulls(SDA_PIN, enable_pullup, false);
    gpio_set_pulls(SCL_PIN, enable_pullup, false);
}

void i2c_setup(i2c_inst_t *i2c_hw_block, uint16_t scl_freq_khz, uint8_t gp_for_scl, uint8_t gp_for_sda, bool scl_sda_internal_pullup){
    reg_i2c = i2c_hw_block;
    i2c_scl_freq_kHz = scl_freq_khz;
    SCL_PIN = gp_for_scl;
    SDA_PIN = gp_for_sda;

    i2c_init(reg_i2c, i2c_scl_freq_kHz * 1000);

    gpio_set_function(SDA_PIN, GPIO_FUNC_I2C);
    gpio_set_function(SCL_PIN, GPIO_FUNC_I2C);

    i2c_scl_sda_pullup(scl_sda_internal_pullup);
}

bool i2c_tx_timeout(uint64_t timeout_microseconds){
    absolute_time_t timeTO = make_timeout_time_us(timeout_microseconds);

    while(reg_i2c->hw->txflr > 0){
        if(time_reached(timeTO)){
            return true;
        }
    }

    return false;
}

bool i2c_rx_timeout(uint64_t timeout_microseconds){
    absolute_time_t timeTO = make_timeout_time_us(timeout_microseconds);

    while(reg_i2c->hw->rxflr == 0){
        if((reg_i2c->hw->status & 0x00000001) == 1){
            timeTO = make_timeout_time_us(timeout_microseconds);
        }

        if(time_reached(timeTO)){
            return true;
        }
    }
    return false;
}

bool i2c_ck_nack(){
    busy_wait_us_32((1000/i2c_scl_freq_kHz) * 10);

    if((reg_i2c->hw->tx_abrt_source & 0x00000009) > 0){
        return true;
    }
    return false;
}

void i2c_clr_intr(){
    uint32_t uintTMP = reg_i2c->hw->clr_intr;
}

uint8_t i2c_nack(){
    i2c_clr_intr();
    return RC_NACK;
}

uint8_t i2c_abort(){
    reg_i2c->hw->enable = 0x00000003;

    while((reg_i2c->hw->enable & 0x00000002) > 0){
    }

    reg_i2c->hw->enable = 0;
    reg_i2c->hw->enable = 1;
    i2c_clr_intr();

    return RC_TIMEOUT;
}

uint8_t i2c_write(uint8_t slave_adr, uint8_t *write_data, uint8_t write_data_length, bool gen_stop){
    uint8_t cur;
    uint32_t uintCMD;
 
    reg_i2c->hw->enable = 0;
    reg_i2c->hw->tar = 0x00000000 | slave_adr;
    reg_i2c->hw->enable = 1;

    for(cur = 0; cur < write_data_length; cur++){
        uintCMD = write_data[cur];
        if(cur == 0) uintCMD |= 0x00000400;
        if((cur == (write_data_length -1)) && gen_stop) uintCMD |= 0x00000200;

        reg_i2c->hw->data_cmd = uintCMD;

        if(i2c_tx_timeout((1000/i2c_scl_freq_kHz)*100)){
            return i2c_abort();
        }

        if(i2c_ck_nack()){
            return i2c_nack();
        }
    }

    return RC_SUCCESS;
}

uint8_t i2c_read(uint8_t slave_adr, uint8_t *read_data, uint8_t read_data_length, bool start){
    int RC;
    uint8_t cur = 0;
    uint32_t uintCMD;

    if(start){
        reg_i2c->hw->enable = 0;
        reg_i2c->hw->tar = 0x00000000 | slave_adr;
        reg_i2c->hw->enable = 1;
    }

    for(cur = 0; cur < read_data_length; cur++){
        uintCMD = 0x00000100;
        if(cur == 0) uintCMD |= 0x00000400;
        if(cur == (read_data_length-1)) uintCMD |= 0x00000200;

        reg_i2c->hw->data_cmd = uintCMD;

        if(i2c_rx_timeout((1000/i2c_scl_freq_kHz)*100)){
            if(i2c_ck_nack()){
                return i2c_nack();
            }
            else{
                return i2c_abort();
            }
        }

        read_data[cur] = reg_i2c->hw->data_cmd & 0xFF;
    }

    return RC_SUCCESS;
}

uint8_t i2c_read_full(uint8_t slave_adr, uint8_t *reg_adr, uint8_t reg_adr_length, uint8_t *read_data, uint8_t read_data_length){
    uint8_t RC;

    RC = i2c_write(slave_adr, reg_adr, reg_adr_length, false);

    if(RC == RC_SUCCESS){
        busy_wait_us_32((1000/i2c_scl_freq_kHz)*20);
        return i2c_read(slave_adr, read_data, read_data_length, false);
    }
    else{
        return RC;
    }
}

pico-test\module_i2c\CMakeLists.txt

add_library(module_i2c
        module_i2c.c
        )

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

改造した関数の解説

関数の中身を解説します。

void i2c_setup(i2c_inst_t *i2c_hw_block, uint16_t scl_freq_khz, uint8_t gp_for_scl, uint8_t gp_for_sda, bool scl_sda_internal_pullup){
    reg_i2c = i2c_hw_block;
    i2c_scl_freq_kHz = scl_freq_khz;
    SCL_PIN = gp_for_scl;
    SDA_PIN = gp_for_sda;

    i2c_init(reg_i2c, i2c_scl_freq_kHz * 1000);

    gpio_set_function(SDA_PIN, GPIO_FUNC_I2C);
    gpio_set_function(SCL_PIN, GPIO_FUNC_I2C);

    i2c_scl_sda_pullup(scl_sda_internal_pullup);
}

I2C を使うための初期設定を一つにまとめたものです。
最初の reg_i2c と i2c_scl_freq_kHz の二つの変数はこの関数の中だけではなく改造した Read/Write 関数でも参照するため、ここで適切な値を代入しておく必要があります。
i2c_init と gpio_set_function は SDK の関数をそのまま使用しているだけで、それぞれ I2C HW block の初期化と、指定した GPIO 端子の機能を設定する関数となっています。
i2c_scl_sda_pullup は GPIO 端子の内蔵 Pull-up の有効/無効を設定する関数で、中身は SDK の関数となっています。

uint8_t i2c_write(uint8_t slave_adr, uint8_t *write_data, uint8_t write_data_length, bool gen_stop){
    uint8_t cur;
    uint32_t uintCMD;
 
    reg_i2c->hw->enable = 0;
    reg_i2c->hw->tar = 0x00000000 | slave_adr;
    reg_i2c->hw->enable = 1;

    for(cur = 0; cur < write_data_length; cur++){
        uintCMD = write_data[cur];
        if(cur == 0) uintCMD |= 0x00000400;
        if((cur == (write_data_length -1)) && gen_stop) uintCMD |= 0x00000200;

        reg_i2c->hw->data_cmd = uintCMD;

        if(i2c_tx_timeout((1000/i2c_scl_freq_kHz)*100)){
            return i2c_abort();
        }

        if(i2c_ck_nack()){
            return i2c_nack();
        }
    }

    return RC_SUCCESS;
}

Write 関数です。中身を順番に追っていきます。

    reg_i2c->hw->enable = 0;
    reg_i2c->hw->tar = 0x00000000 | slave_adr;
    reg_i2c->hw->enable = 1;

reg_i2c は変数で、RP2040 が持つ二つの I2C HW block (i2c0, i2c1) のどちらかを代入します。reg_i2c への値の代入は先の i2c_setup 関数で行っています。
reg_i2c->hw->xx は Raspberry pi pico の本体のマイコンである RP2040 のレジスタにアクセスするための記述です。(この記述でアクセスできるように pico-sdk が用意してくれています。) 例えば reg_i2c->hw->enable で I2C HW block の IC_ENABLE (I2C HW block が i2c0 の場合は 0x40044000 + offset 0x6C) のレジスタへのポインタとなり、この記述によって該当するレジスタの値を読み出したり、逆に値を書き込んだりできるようになっています。

最初に reg_i2c->hw->enable に 0 を代入して I2C HW block を Disable にします。RP2040 のデータシートによると、このレジスタの値 (厳密には 0 番目の bit の値) を 0 にすると I2C HW block が Disable になり、コマンドを蓄える TX Buffer や Slave から読み出したデータを蓄える RX Buffer、Status bit がクリアされます。
次に reg_i2c->hw->tar に Write を行う相手の Slave address を代入します。ここで代入する Slave address は R/W bit を除いた 7-bit です。
最後に reg_i2c->hw->enable に 1 を代入して Enable にします。

    for(cur = 0; cur < write_data_length; cur++){
        uintCMD = write_data[cur];
        if(cur == 0) uintCMD |= 0x00000400;
        if((cur == (write_data_length -1)) && gen_stop) uintCMD |= 0x00000200;

        reg_i2c->hw->data_cmd = uintCMD;

        if(i2c_tx_timeout((1000/i2c_scl_freq_kHz)*100)){
            return i2c_abort();
        }

        if(i2c_ck_nack()){
            return i2c_nack();
        }
    }

これら記述では Slave に書き込むデータを 1-byte ずつ処理しています。

I2C のコマンドの指示は reg_i2c->hw->data_cmd への値の書き込みによって行います。
8-bit 目が Master Write/Read の指示で、ここに 0 を書き込むと Master Write, 1 を書き込むと Master Read の指示となります。
Master Write を指示する場合、7-0 bit に Slave に書き込むデータを代入します。
10-bit 目は、このコマンド指示を実行する前に Start condition を生成するかどうかの指示で、1 を書き込むと SDA/SCL が Start condition を生成します。
9-bit 目は、このコマンド指示を実行した後に Stop condition を生成するかどうかの指示で、1 を書き込むと SDA/SCL が Stop condition を生成します。
記述では reg_i2c->hw->data_cmd に書き込む値として unitCMD という変数を用意して 7-0 bit に Slave に書き込むデータを代入し、必要に応じて 10-bit 目 と 9-bit 目の値に 1 を加えています。8-bit 目は触っていないので 0 のままです。

reg-i2c->hw->data_cmd に unitCMD を代入すると i2c_tx_timeout で data_cmd に書き込んだコマンド指示が実行開始されるのを待ち、一定時間 (ここでは SCL x 100 clock 分に設定)、実行開始されない場合は Timeout として i2c_abort 関数を呼び出します。実行開始された場合、今度は i2c_ck_nack で NACK の検出がなかったかを確認し、NACK が検出されていれば i2c_nack を呼び出します。i2c_aboart と i2c_nack はそれぞれ中断処理を行った後に Timeout または NACK のエラーコードを返します。Timeout にも NACK にもひっかからなかった場合は for loop の最初に戻って次のデータの処理を行います。

bool i2c_tx_timeout(uint64_t timeout_microseconds){
    absolute_time_t timeTO = make_timeout_time_us(timeout_microseconds);

    while(reg_i2c->hw->txflr > 0){
        if(time_reached(timeTO)){
            return true;
        }
    }

    return false;
}

Timeout の判定です。
make_timeout_time_us は SDK の関数で、関数呼び出し時点の RP2040 の内部カウンタ (時計) の値に引数分の時間 (us) に相当する値を足した値を返します。記述ではその値を timeTO という変数に代入し、while loop の中で time_reached という関数がその値を参照しています。
time_reached も SDK の関数で、RP2040 の内部カウンタの値が引数の値を越えると true, それ以前は false を返します。
この記述では while loop で i2c_reg->hw->txflr の値を見張っています。このレジスタは I2C のコマンドバッファに蓄えられたコマンドの数を示しています。I2C のコマンドバッファには reg_i2c->hw->data_cmd に代入したコマンドが蓄えられるので、ここでは事前にコマンドバッファに送ったコマンドの処理が開始されて txflr が空になる (値が 0 になる) のを待っています。txflr が空になる前に time_reached が true を返すと Timeout と判定します。
ちなみに SDK の I2C 関数では i2c_reg->hw->status の 3-bit 目の値 (コマンドバッファが空になると 0, コマンドを一つ以上蓄えていると 1) で判定していましたが、見ているものは同じです。

bool i2c_ck_nack(){
    busy_wait_us_32((1000/i2c_scl_freq_kHz) * 10);

    if((reg_i2c->hw->tx_abrt_source & 0x00000009) > 0){
        return true;
    }
    return false;
}

因縁の NACK の判定です。
SDK の I2C 関数が NACK を判定できなかったのは NACK 検出の bit の値が立つまでのライムラグが原因だったので、ここでは最初に待ち時間を設定しています。
busy_wait_us_32 は SDK の関数で、引数分の時間 (us) だけ処理待ちをします。ここでは待ち時間を SCL x 10 clock 分、大雑把に言うと 1-byte 分の通信時間としています。
待ち時間が経過したら reg_i2c->hw->tx_abrt_source の値を確認し、NACK に相当する bit (4-bit 目と 0-bit 目) が 1 になっていたら NACK と判定します。

Read 関数も改造していますが、同じような内容なので説明は省略します。

動作確認

それでは動作確認です。 SDK の関数の動作確認と同じように Slave device 用 Sketch を書き込んだ Arduino を Slave として接続して、改造した関数の動作を確認します。Raspberry pi pico は PC に接続して、Arduino IDE のシリアルモニタで通信します。Arduiono nano を PC に接続しているのは電源のためなので、電源供給してくれるものであれば PC でなくても構いません。
f:id:ysin1128:20210902175145p:plain

シリアルモニタから Raspberry pi pico に "1", "a", "2", "a", "5", "a" の順で文字を送った結果が以下になります。
f:id:ysin1128:20211201162435p:plain

この動作確認用の記述は、 0 から 9 までの数字を送ると Raspberry pi pico が I2C で Slave address の Register address = 0x00 に受け取った値を書き込み、それ以外の文字を送ると Raspberry pi pico は I2C で同 Register の値を読み出して、読み出した値をシリアルモニタに返します。
"1", "2", "5" に対しては "P" が返ってきていますが、これは Raspberry pi pico が I2C で送った数字を書き込もうとした結果を示していて、"P" は Pass、つまり成功を意味しています。失敗した場合は "F"ail でした。ちなみに、SDK の関数の場合は Raspberry pi pico が Slave device に送ったデータの数 (Byte) が返ってきていましたが、改造したこの関数ではその部分を変更しています。
"a" に対しては Raspberry pi pico が I2C で読み出した値が返ってきています。対象 Register の値は直前に書き込んだばかりなので、その書き込んだ値が正しく読み出せていることが分かります。
というわけで、読み書きに関して無事に動作していることが確認できました。

次は異常時ということで、Slave device の接続を外した状態で "1", "a" を 1文字ずつ送った結果が以下になります。
f:id:ysin1128:20211201163828p:plain

どちらの文字に対しても "F" つまり Fail が返ってきました。"1" に対しては i2c_write 関数, "a" に対しては i2c_read 関数が動作しますが、どちらも問題なく NACK を検出できたようです。SDK の関数の場合はここで write が NACK を検出できなくて、それがきっかけで関数の改造に手を付けることとなったのですが、これにて無事に目的達成です。

最後にもう一つ、SDA/SCL を GND に短絡した状態で "1", "a" を 1文字ずつ送った結果です。
f:id:ysin1128:20211201171551p:plain

SDA/SCL が GND に短絡していると Raspberry pi pico は他の誰かが I2C 通信のために SDA/SCL を使っていると考えて待機するのですが、そのまま一定時間が過ぎると Timeout と判断して諦めます。
"1", "a" のどちらに対しても "T" が返ってきています。つまり、i2c_write 関数も i2c_read 関数も Timeout の判定ができたということで、動作確認OK となります。


というわけで改造は完了です。
肝心の NACK の判定待ちが決め打ちの待機時間の挿入なので、待ち時間がまだ足りなくて NACK を検出できないという可能性はあるかもしれないのですが、今はこれでよしとします。そのうちもっと良いアイデアを思い付いたらまた改造します。

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

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 通信を行った際にシリアルモニタに戻ってきた値です。
f:id:ysin1128:20210928115529p:plain

値は 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 通信を行った場合のシリアルモニタの表示です。
f:id:ysin1128:20210928133910p:plain

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 を検出できる関数にします。