SSブログ

macOSからPi Picoを使う - その3 [Pi Pico]

前回前々回とでmacOSをホストにしてRaspebrry Pi Picoを簡単に使えることがわかった。Pi Picoとホストの両方でUSBを直接プログラムする必要はなく、ホストからはUSBのCDCデバイスとして見えて、Pi Picoではstdinとstdoutを読み書きすることで、キャラクタをやりとりできる。

いままでWiFi経由で使っているRaspberry Piをもっとシンプルな接続に置き換えることができる。Raspberry Piでは贅沢な使い方になっている場面も多いし、SDカードの信頼性を心配する必要がなくなるし、なんといってもPi Picoのほうが安い。

Pi Picoでできることをざっくりとみて、それらを使って実際につかえるデバイスにするにはどうすればいいかを考える.....

5  GPIO

実はPIOが面白そうでいじってみたいんだけど、直近で使い道がない。5相ステッピングモータのXYステージを円弧補間して動かすためのパルス生成ができるか、と思って考えてみて、かなり効率的な書き方ができないと難しそうだ、というぐらいのことはわかった。ということころぐらいでPIOはのちのちの楽しみとして置いておくことにする。

単一ビット制御のGPIOはこれまでRaspberry Piをいじっていたらわかりやすい。とりあえずGPIOは本家Raspberry Piと基本的なところは同じに見える。本家と違ってGPIOの番号とその場所がちゃんと順番になっている。これであたりまえだけど。

GPIOは初期化すると使えるようになる。
void gpio_init(uint gpio);
GPIO以外の機能を使うときはピンの機能を宣言する。これはhardware_gpioモジュールで宣言されている。
enum gpio_function {
    GPIO_FUNC_XIP = 0,
    GPIO_FUNC_SPI = 1,
    GPIO_FUNC_UART = 2,
    GPIO_FUNC_I2C = 3,
    GPIO_FUNC_PWM = 4,
    GPIO_FUNC_SIO = 5,
    GPIO_FUNC_PIO0 = 6,
    GPIO_FUNC_PIO1 = 7,
    GPIO_FUNC_GPCK = 8,
    GPIO_FUNC_USB = 9,
    GPIO_FUNC_NULL = 0xf,
};

void gpio_set_function(uint gpio, enum gpio_function fn);
enum gpio_function gpio_get_function(uint gpio);
あとは関数の名前を見ればだいたいわかる。

ひとつ便利そうなのは、イベントに対して割り込みを発生させて、コールバック関数を呼んでくれる機能。
enum gpio_irq_level {
    GPIO_IRQ_LEVEL_LOW = 0x1u,
    GPIO_IRQ_LEVEL_HIGH = 0x2u,
    GPIO_IRQ_EDGE_FALL = 0x4u,
    GPIO_IRQ_EDGE_RISE = 0x8u,
};

typedef void (*gpio_irq_callback_t)(uint gpio, uint32_t events);
void gpio_set_irq_enabled_with_callback(uint        gpio,
                                        uint32_t    events,
                                        bool        enabled,
                                        gpio_irq_callback_t callback);
コールバック関数はgpio_set_irq_enabled_with_callback()を読んだコアで実行されるらしい。

ポーリングしなくてもいいけど、割り込みなのでコールバックの中であまり大きな作業をすると痛い目に会う。

GPIOは細かく見なくてもなんとなくわかる。

6  ADC

6.1  ADCのハードウェア

GPIOの次によく使うのはA/Dだろう。RP2040は500kS/sの12ビット逐次変換型(Successive Approximation Register, SAR)ADCをひとつ持っている。0〜4の5チャンネルのアナログマルチプレクサがADCに繋がっていて、ad4は温度計に、ad3はPi Pico基板内部でVSYS/3(電源入力を1/3に分圧)につながっている。

Pi PicoのADCの精度がどの程度なのか一度測定しておく必要がある。12ビットあるけど9.5 ENOBとか書いてある。これがどういうコンディションの場合なのかよくわからないけど、これが最良値だとすると、これまで何度も使った外付けのMC3208なんかの一番条件の悪いとき(Vref < 0.5V、入力周波数 ≈ 10kHzとか)ぐらいのレベルなので、相当悪い。

A/Dの基準電圧Vrefは電源から作られた3.3VにRCのパッシブローパスを通していて、もともと電源電圧の精度がない上にVrefは入力電流を喰うのでパッシブローパスの抵抗ぶん(200Ω)で電圧が変動してしまう、基板上の抵抗R7を外して(外しやすくするため1608とでかい)外部からVrefを与えることができる、デジタルへの電源電圧IOVDDへ保護ダイオードが入っているのでIOVDD以上の電圧を入れるとリークする、と書いてある。

シングルショットとフリーランのモードがあって、フリーランモードは最短で96クロックに1回(=500ksps)の変換をずっと続ける。変換の結果を保持するために4エントリのFIFOレジスタがある。さらにマルチプレクサとの組み合わせでラウンドロビンモードというのがあって、フリーランしながら1サンプルごとに入力を切り替えるというもの。どの入力をラウンドロビンするかは指定することができる。

6.2  hardware_adcモジュール

pico-sdkにADCのためのモジュールがある。ヘッダは
#include "hardware/adc.h"

6.2.1  初期化

まず初期化
void adc_init(void);
static inline void adc_gpio_init(uint gpio);
adc_init()はモジュールの初期化で、adc_gpio_init()は入力ピンをADC用に設定する。引数は26〜29でないといけない。

温度計用には
static inline void adc_set_temp_sensor_enabled(bool enable);
がある。

6.2.2  シングルショット

A/Dを1回してその結果を読むには
static inline void adc_select_input(uint input);
static inline uint16_t adc_read(void);
を使う。入力ピンの選択のinput引数はピン番号ではなく、0〜4のADCの番号を指定する。読込のadc_read()は_blockingがついていないけど、当然変換が終わるまでブロックする。

6.2.3  フリーランとラウンドロビン

モードの切り替え用に
static inline void adc_run(bool run);
static inline void adc_set_round_robin(uint input_mask);
がある。adc_run()はフリーランをスタートする。ラウンドロビンの引数はADC0〜4をビットの位置で指定する。0にするとラウンドロビンしない。

RP2040のデータシートの方には最初に設定されていた入力の値がまず出て、そのあとにラウンドロビンが始まるように書いてある。例えば
    adc_select_input(0);
    adc_set_round_robin(0x06);   // adc1 and adc2
とすると
0	1	2	1	2	1 ...
となるようである。ひとつずれてしまうので、これ現物でちゃんと確認する必要がある。

6.2.4  サンプリング周期の調整

static inline void adc_set_clkdiv(float clkdiv);
でサンプル周期を決められる。clkdiv+1クロックで1回のサンプルになる。最短で96クロック必要なので94以下の値では95と同じになる。面白いのはこの引数がfloatになっていて小数点以下は平均でその値に近くなるようにサンプリングするらしい。たとえばCDのサンプリングレートの44.1kHzにしたければ
    adc_set_clkdiv(1088.44 - 1.0);
とすればいいようである。96kサンプルだと割り切れて499.0となる。

6.3  ADC用のFIFO

ADCを初期化しただけではFIFOは利用できず、結果はRESULTレジスタに書かれる。FIFOをON/OFFするには
static inline void adc_fifo_setup(bool      en,
                                  bool      dreq_en,
                                  uint16_t  dreq_thresh,
                                  bool      err_in_fifo,
                                  bool      byte_shift);
を呼ぶ。引数のenはFIFOのON/OFF、dreq_enはDMAを使うかどうか、dreq_threshはDMAと割り込み用、err_in_fifoはFIFOの中にあるデータにエラーを含めるかどうか、byte_shiftは変換結果の12ビットのうち上位8ビットだけを取り出すかどうかを指定する。byte_shiftをtrueにすると結果の分解能は8ビットになってしまうが、FIFOを4エントリから8エントリに深くできる。ADCそのものは12ビットの変換をするのでレートが速くなるわけではない。

FIFOからデータを取るには
static inline uint16_t adc_fifo_get(void);
static inline uint16_t adc_fifo_get_blocking(void);
を使う。FIFOの中の一番古いデータをポップして返す。

FIFOの状態を確認するには
static inline bool adc_fifo_is_empty(void);
static inline uint8_t adc_fifo_get_level(void);
get_level()は4エントリのうちいくつ残っているかを返す。

強制的に空にするのは
static inline void adc_fifo_drain(void);
を呼ぶ。これを呼んだときに変換中だと変換が終わるまでブロックする。

変換が終わったときに割り込みをかけるには
static inline void adc_irq_set_enabled(bool enabled);
を呼ぶ。setup()でのdreq_threshで指定されたエントリを超えると割り込みADC0_IRQ_FIFO(IRQ番号=22)が発生する。

割り込みハンドラはhardware_irqモジュールの関数を使う。コアごととコアに関わらず処理するのを割り振ることができる。僕はなるべく割り込みを使わないようにするつもりなので、詳細はsdkを参照して欲しい。

6.3.1  ADCのエラー

A/D変換でエラーが起こることを想定するのはそれほど多くないと思うけど、逐次変換型はコンパレータの閾値付近で値が確定できないことがある。コンパレータに十分ゲインがあると確定するけどノイズに弱くなったりする。RP2040内臓のADCはそういう場合にControl and StatusレジスタのERRビットを立てる。hardware_adcモジュールでこれを読み出す関数は用意されていないけど、このビットはFIFOに反映させることができる。

setup()関数のerr_in_fifo引数をtrueにすると変換エラーのあるデータにはFIFOを読み出したときuint16_tの最上位ビットを立てる。時系列データを取るときに、FIFOを使ってエラーがあったら前後のデータから補間する、というのを想定しているんだろう。小さな親切である。
nice!(0)  コメント(4) 

nice! 0

コメント 4

めかぶ

コメント失礼します。
最近Picoを使い工作を楽しんでいるのですが、PicoのADCピンのサンプリングレートって固定できるのでしょうか?
自分はMicroPythonを使っているのですが、あまりそういった記事が無く...
音声で使いたいのでできれば44.1khz、最低でも22.05kHzで動作させたいのですが、可能なのでしょうか?
初心者のため、言っていることがハチャメチャでしたら申し訳ないです
by めかぶ (2023-05-02 18:40) 

decafish

僕は楽器の入力にPi Pico内蔵のADCを使ってみようと思っていたのですが、一身上の都合でまだできていません。
C/C++インターフェイスのドキュメントを見る限りではサンプリングレートを指定できるのですが、その精度や、MicroPythonで指定が可能なのかどうかは、すみません、わかりません。
しかし逐次変換のADCで1ビットあたり8クロック固定のようなので、モノラル音声のAD変換なら十分なサンプリングレートとレート安定性はありそうです。44.1kHzは十分できると思います。
ただ、リニアリティが悪くて実質8ビット幅ぐらいの精度しかありません。ダイナミックレンジの狭い、例えばマイクで拾った人の声ぐらいでないと無理かもしれません。
また、ファイルに落とすためのAPIはないのでAIFFやWAVにしようとすると自分で書かないといけません。
僕はエレキギター用のエコーチェンバぐらいならなんとかなるんじゃないか、と思っていましたがまだ手をつけられていません。
by decafish (2023-05-02 21:01) 

decafish

他の問題があることを書き忘れていました。
内蔵ADCは片電源なので入力にはVref/2のオフセットを与える必要があります。またVrefを外部から与えることはできますが、あまり低いVrefでは精度がさらに劣化する可能性があるので、音声用ラインレベルではアンプする、あるいはアナログコンプレッサを入れるなどの考慮が必要です。
それらは安価な汎用ADCを音声用に使う場合の配慮ではあるのですが、ADCの前にアナログ回路がそれなりに必要になり、結構めんどくさいです。それはそれでやれば楽しい作業ではあるのですが...
by decafish (2023-05-02 21:15) 

めかぶ

回答ありがとうございます。
結構自分でも調べてみたり色々試してみたりしたのですが、Pythonの経験しかなくCでは理解しきれず難しかったです....
せっかく答えていただいたのにありがとうございます。これを機に少しずつCの勉強もしてみたいと思います!
by めかぶ (2023-05-31 22:19) 

コメントを書く

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

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