yoshiyuki's blog

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

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