SSブログ

macOSからPi Picoを使う [Pi Pico]

Raspberry Pi Picoが日本ではショップによって手に入るところが出てきた。でも一瞬で無くなったりする。僕はついこないだやっと2個だけ確保できた。

Raspberry Pi PicoをmacOSから使いたいんだけど、vendor specificなクラスのUSBデバイスを扱う必要がある。

それもめんどくさいなあ、とりあえずlibusbを使って、あとからIOUSBHostで書き換えるか、Pi Pico側はTinyUSBなんだけど、これはドキュメントが整備されてなくて厳しいなあ、cmakeって慣れてなくて難しいなあ、などと思っていた。

pico_sdkを落としていろいろいじっているうちにいろんなことがわかってきた...

2  macOSへのsdkの整備

まず、pico_sdkはcmakeで統一的にビルドできるようになってるんだけど、依存関係が複雑で、僕がcmakeに慣れてないせいもあって(いかにXcodeにスポイルされているかよくわかる)、Getting_startedからちょっと違うことをしようとするとうまくいかない。TinyUSBにいたってはexamplesのビルドさえうまくいかない。

それでもGetting_started(pdf)のChapter9.1 Building on Apple macOSに従って設定するとexampleはmacOSでビルドできるようになった。まずhomebrewで必要なライブラリやコマンドをインストールする。それからsdkをcloneするんだけど、サブモジュールもインストールしておかないとtinyUSBがロードされない。
$ git clone -b master https://github.com/raspberrypi/pico-sdk.git
$ cd pico-sdk
$ git submodule update --init --recursive
$ cd ..
$ git clone -b master https://github.com/raspberrypi/pico-examples.git
あとはPICO_SDK_PATH変数をpico-sdkディレクトリに設定する。さて、これでexampleはビルドできるようになったんだけど、自分のプロジェクトにするとクロスコンパイラでなくネイティブのclangが呼ばれてしまう。CMakeLists.txtのどこかに設定があるんだろうけど見つからない。色々試すとなぜかpico-sdkと同じレベルのディレクトリにすると設定される。全然わからん。

誰か教えて。

まあ、でもそうすることで、cmakeが動いてビルドできるようになる。

3  macOSとの通信

macOSとはUSBを経由して通信したいんだけど、vendor specificなUSBデバイスと通信するには、macOS側はI/O Kitを使うか、USBDriverKitか、IOUSBHostか、はたまたlibusbか、と色々選択肢があって(というか、どれだと近未来のobsolete地雷を踏まずに済むか)、どれを使えばいいのかよくわからない。

また、Pi Pico側はTinyUSBを使うんだけど、なぜかいまだにexampleさえビルドすることができない。pico_sdkは簡単にできるように工夫されてるようなんだけど、その構造は複雑で難しい。勉強が必要である。

どうしようか悩んでるうちにあることに気がついた。

3.1  pico_stdio

pico_examplesにあるhello_worldはGetting_startedのblinkの次に紹介される基本的なサンプルで、これはCランタイムのstdioのサブセットであるpico_stdioというモジュールを経由して文字列を出力する。出力先はUART(シリアルポート)とUSBのふたつが使える。USBのためにはTinyUSBがインストールされてないといけない。さらにCMakeLists.txtの中に
pico_enable_stdio_uart(hello_world 0)
pico_enable_stdio_usb(hello_world 1)
とする。これはstdin、stdoutをUARTではなくUSBにする、という宣言らしくて、こうするとTinyUSBが自動的にリンク(というかソースからコンパイルするmakefileが生成される。OS前提に慣れた身には抵抗を感じるけど、組み込みでは「ライブラリをリンク」と「.oファイルを一緒にビルド」の区別はないわな)される。

pico_stdioはCランタイムとformat引数記述の上ではコンパチのprintfが実装されている。これを使えば文字列の出力はかなり自由にできる。ただし、入力はあまり用意されていない。ただひとつ
int getchar_timeout_us(uint32_t timeout_us);
はstdinからunsigned charをひとつだけ読み込むことができる。引数はμsec単位のtimeoutで、これを超えるとPICO_ERROR_TIMEOUTを返すことになっている。0を指定するとunixのfcntl()関数にO_NONBLOCKを指定したときの同じような動作をする。

3.2  pico_stdioを使う

pico_stdioを使うためにはまず
#include <stdio.h>
でヘッダを読んでmain()関数の初めのほうに
    stdio_init_all();
で初期化をする。これでstdinとstdoutが使えるようになる。

3.3  macOS側

さてこれで、Pi Pico側はUSB経由でキャラクタを入出力できるようになる。

macOS側だけど、これも簡単な方法がみつかった。

実はPi Picoでpico_stdioをUSBに設定すると、Pi PicoのUSBはdeviceモードになり、CDCクラスとしてhostから見えるようにdescriptorが設定される。

これはそうやったhello_worldを走らしたPi PicoをMacに接続してUSBProberで見たところ。
0510usbprober.png
vendorID、productIDは正しいようである。シリアルNo.は0になっている。デバイスクラスは239(=0xEF)はMiscellaneousでサブクラスが2、プロトコルが1になっている。これはIAD(微妙にシンプルなpdf仕様書)という複数の機能を持ってるデバイス用の特別な値の組み合わせらしい。

残りを見てみると、interfaceが3つあって
....
Interface Association   Communications-Control
    First Interface   0
    Interface Count   2
    Function Class   2   (Communications-Control)
    Function Subclass   2
    Interface Protocol   0
    Function String   0 (none)
Interface #0 - Communications-Control ............................   "Board CDC"
    Alternate Setting   0
    Number of Endpoints   1
    Interface Class:   2   (Communications-Control)
    Interface Subclass;   2
    Interface Protocol:   0
    Comm Class Header Functional Descriptor   
....
Address:   0x81  (IN)
Attributes:   0x03  (Interrupt)
Max Packet Size:   8
Polling Interval:   16 ms
Interface #1 - Communications-Data/Unknown Comm Class Model   
    Alternate Setting   0
    Number of Endpoints   2
    Interface Class:   10   (Communications-Data)
    Interface Subclass;   0   (Unknown Comm Class Model)
    Interface Protocol:   0
    Endpoint 0x02 - Bulk Output   
Address:   0x02  (OUT)
Attributes:   0x02  (Bulk)
Max Packet Size:   64
Polling Interval:   0 ms
    Endpoint 0x82 - Bulk Input   
Address:   0x82  (IN)
Attributes:   0x02  (Bulk)
Max Packet Size:   64
Polling Interval:   0 ms
Interface #2 - Vendor-specific .......................................   "Reset"
    Alternate Setting   0
    Number of Endpoints   0
    Interface Class:   255   (Vendor-specific)
    Interface Subclass;   0   (Vendor-specific)
    Interface Protocol:   1
となっている。詳しくはわからないんだけど、function classがCDC(Communication Data Class)になっている。interface#0がなんらかの制御用らしくてIN方向(デバイス→ホスト)のendpointだけ持っていて、interface#1がbulk transactionの入出力になっている。これが実際にデータをやり取りするpipeを持っているらしい。残ったinterface#2はResetの名前になっているけどVendor specificでendpointもないので、このままでは機能はわからない。

複雑な書き方だけど、これでmacOSは認識するので一般的なデスクリプタ記述のようである(macOSでは、もともとはWindowsに固有だけど今ではデファクトになってるような規格を無視することがあったので、警戒心が養われてしまった)。

CDCクラスはRS232CやISDNなんかをUSB経由で動かすことを想定したもので、USB-シリアルコンバータなんかがこのクラスになっている。実際には別にRS232Cが繋がっていなくて、デバイスの中で閉じていてもホストからはわからない。vendor specificなクラスとして実装するとキャラクタ以外のデータも通信できるけど、しょせんPi PicoはUSB1.1だし、Pi Picoを使って動画を送ろうと言う人はなかなかないだろうし(僕はそんなことはやらない)、ということでこれで十分である。

これは便利で、macOS側は汎用のキャラクタデバイスとして見えるはずである。

/devの中をさぐるとPi Picoの抜き差しで
crw-rw-rw-  1 root  wheel    9,   8  5  7 10:06 /dev/tty.usbmodem0000000000001
というデバイスファイルができたり無くなったりする。かならずこの名前になるのかどうかわからないし、さらにはCDCデバイスが未来永劫macOSでサポートされるかどうかわからないけど、もしこれがあればmacOSはPOSIX互換だということになってるので、このデバイスファイルを通してキャラクタをやりとりすることができる。

このおかげで一気に簡単になった。

ためしにこんなのを書いた。test.cに
#include <stdio.h>
#include "pico/stdlib.h"
#include "hardware/adc.h"

int main() {
    stdio_init_all();
    adc_init();
    adc_select_input(4);
    
    while (true) {
        const float conversion_factor = 3.3f / (1 << 12);
        int c = getchar_timeout_us(1000000);
        if (c != PICO_ERROR_TIMEOUT)
            printf("received character [%c]\n", c);
        else {
            uint16_t result = adc_read();
            float   adcVoltage = result * conversion_factor;
            float   celsius = 225.126 - 322.81 * adcVoltage;
            printf("timeout.\n");
            printf("%s 0x%03x, %s %5.3f V, %s %5.2f degree\n",
                   "Raw value:", result,
                   "voltage:", adcVoltage,
                   "temperature:", celsius);
        }
    }
    return 0;
}
これはstdinに入力があるとそのキャラクタをstdoutに返して、1秒以上入力がないとチップ温度を表示する、というだけのもの。

CMakeLists.txtに
cmake_minimum_required(VERSION 3.13)

include(pico_sdk_import.cmake)
project(test_project C CXX ASM)
set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)
pico_sdk_init()
add_executable(test
    test.c
)

pico_enable_stdio_usb(test 1)
pico_enable_stdio_uart(test 0)

pico_add_extra_outputs(test)

target_link_libraries(test pico_stdlib hardware_adc)
としてビルドする。つまりGetting_startedにあるように
$ export PICO_SDK_PATH=../pico-sdk
$ mkdir build
$ cd build
$ cmake ..
$ make
でうまくいけばbuildディレクトリは
$ ls
CMakeCache.txt              pico-sdk
CMakeDoxyfile.in            test.bin
CMakeDoxygenDefaults.cmake  test.dis
CMakeFiles                  test.elf
Makefile                    test.elf.map
cmake_install.cmake         test.hex
elf2uf2                     test.uf2
generated
$ 
みたいになるはずで、Pi PicoのBOOTSELボタンを押しながらUSBを挿して、マウントされたボリュームにこのtest.uf2をコピーすればいい。
$ cp test.uf2 /Volumes/RPI-RP2/
こうするとPi Pico側からかってにアンマウントするので、必ず「ディスクの不正な取り出し」という通知がOSから出る。もちろんPi Pico側のファイルシステムは壊れたりはしていないので通知は無視していい。とはいうものの、毎回なので煩わしい。

これでUSBを引っこ抜いて挿し直すとこのプログラムが動き出すはずである。

とりあえずmacOS側でさっきのキャラクタデバイスとやりとりできるように
$ screen /dev/tty.usbmodem0000000000001 9600
として(最後の引数はボーレートだけど今回は関係ない。ちなみになんでscreenという名前なのかと言うとVT100のエミュレータだからだそうである。懐かしい)適当にキーを打つと
received character [a]
received character [s]
received character [d]
received character [f]
timeout.
Raw value: 0x2f7, voltage: 0.611 V, temperature 27.73 degree
timeout.
Raw value: 0x2f6, voltage: 0.611 V, temperature 27.99 degree
やめると1秒ごとにタイムアウトして温度を表示する。

確認できた。

これでとりあえずのPi PicoとmacOSとの通信手段が手に入った。あとはZeroWだとOSにおんぶにだっこできた部分をどこまでpico-sdkで肩代わりできるか、というのをみていく。
nice!(0)  コメント(0) 

nice! 0

コメント 0

コメントを書く

お名前:
URL:
コメント:
画像認証:
下の画像に表示されている文字を入力してください。

KEXT、DriverKit、USBDr..オリンピック ブログトップ

この広告は前回の更新から一定期間経過したブログに表示されています。更新すると自動で解除されます。