· 20分で読了

Raspberry Pi Pico PIO 初探

この記事は中国語から自動翻訳されたものです。翻訳によりニュアンスが失われている場合があります。

Raspberry Pi財団は2021年初頭にマイクロコントローラ「Raspberry Pi Pico」をリリースした。当時、Raspberry Piにこのような製品ラインナップはこれまでなかったのでとても斬新に感じ、1枚購入してみたものの、しばらく放置して詳しく調べていなかった。

改めてスペックをよく見てみると、実はかなり優秀であることに気づいた:

Raspberry Pi picoスペックArduino nano
ARM M0+ dual coreMCUATmega328p
133MHzクロック周波数16MHz
32-bitbit8-bit
264kBSRAM2.5kB
2MB (RP2040自体にはflashなし)Flash32kB
UARTx2
USB host 1.1
SPI
Timer
RTC
ペリフェラルUARTx1
SPI
Timer

デュアルコア、32bit、メモリ264kB、2MBのフラッシュメモリを備えており、Arduino Nanoと比べると圧倒的に優れていて、一部のSTM32にすら勝っている。価格も非常に安く、100台湾ドル強で手に入る。C/C++に加えてMicroPythonにも対応しており、ブレークポイントを設定してgdb経由でメモリや変数の状態を観察できるデバッグ機能まで提供されている。

もうひとつ僕を驚かせた機能が PIO(Programmable I/O)だ。しかし詳細に入る前に、まずはGPIOと、PIOが解決しようとしている課題について話そう!

GPIOとは?

GPIOはGeneral Purpose Input/Outputの略で、マイクロコントローラにおいて通常ピンの出力や入力を制御する機能を持つ。プログラムによって特定のピンの出力をHighまたはLowに制御できる。

一番シンプルな例としてはLEDランプが挙げられる。LEDの点滅機能を実装したい場合、LEDの片方の足をGNDに接続し、もう片方をGPIOピンに接続して、プログラムから出力電位のHigh/Lowを制御すれば点滅効果が得られる。LEDの点滅制御だけでなく、GPIOはI2CやUARTなどのデータ通信にも使用される。

Peripherals

マイクロコントローラが外部デバイスと通信できるようにするため、通常マイコン内部には一般的な通信プロトコルがいくつか組み込まれている。例えばArduinoはUARTに対応しているし、Pro Microを使えば、搭載されているAVRチップATmega32U4に組み込みのUSB機能が備わっており直接利用できる。

しかし欠点として、もしマイクロコントローラにこれらの通信プロトコル機能が内蔵されていない場合、開発者は対応するICを別途購入して実装するか、GPIOピンを通じて通信プロトコルを自前で実装する必要がある。この概念は、ハードウェアデコードとソフトウェアデコードの違いに少し似ている。

例えば、ArduinoではSoftwareSerialを使用してソフトウェア層でUARTプロトコルを実現できる。僕が昨年書いたArduinoの二酸化炭素センサー実装1(/devnote/2020-07-24-arduino-esp32-co2-sensor-2/)でも使用した:

// https://github.com/kjj6198/MH-Z14A-arduino/blob/master/co2.ino#L14
...
SoftwareSerial co2Serial(3, 4); // RX, TX
co2Serial.write(commands, 9); // send command
co2Serial.readBytes(response, 9);  

SoftwareSerialの実装の裏側では、GPIOピンを使ってUARTプロトコルを実装している。SoftwareSerialを使うメリットは、Arduino本来のUARTをPCとの通信(デバッグ用)に残したまま、SoftwareSerialを通じて他の外部デバイスと通信させられる点にある。

Arduino UART

データ通信は高精度な時間制御に依存する

ハードウェアのデータ通信ではタイミングへの依存度が極めて高く、エラーを防ぐためにCPUのサイクル数まで正確に計算しなければならないことすらある。SoftwareSerialの実装を見てみると:

void SoftwareSerial::begin(long speed)
{
  // 省略
  // Precalculate the various delays, in number of 4-cycle delays
  uint16_t bit_delay = (F_CPU / speed) / 4;
  // 12 (gcc 4.8.2) or 13 (gcc 4.3.2) cycles from start bit to first bit,
  // 15 (gcc 4.8.2) or 16 (gcc 4.3.2) cycles between bits,
  // 12 (gcc 4.8.2) or 14 (gcc 4.3.2) cycles from last bit to stop bit
  // These are all close enough to just use 15 cycles, since the inter-bit
  // timings are the most critical (deviations stack 8 times)
  _tx_delay = subtract_cap(bit_delay, 15 / 4);
  // Only setup rx when we have a valid PCINT for this pin
  if (digitalPinToPCICR((int8_t)_receivePin)) {
    #if GCC_VERSION > 40800
    // Timings counted from gcc 4.8.2 output. This works up to 115200 on
    // 16Mhz and 57600 on 8Mhz.
    //
    // When the start bit occurs, there are 3 or 4 cycles before the
    // interrupt flag is set, 4 cycles before the PC is set to the right
    // interrupt vector address and the old PC is pushed on the stack,
    // and then 75 cycles of instructions (including the RJMP in the
    // ISR vector table) until the first delay. After the delay, there
    // are 17 more cycles until the pin value is read (excluding the
    // delay in the loop).
    // We want to have a total delay of 1.5 bit time. Inside the loop,
    // we already wait for 1 bit time - 23 cycles, so here we wait for
    // 0.5 bit time - (71 + 18 - 22) cycles.
    _rx_delay_centering = subtract_cap(bit_delay / 2, (4 + 4 + 75 + 17 - 23) / 4);
    // There are 23 cycles in each loop iteration (excluding the delay)
    _rx_delay_intrabit = subtract_cap(bit_delay, 23 / 4);
    // There are 37 cycles from the last bit read to the start of
    // stopbit delay and 11 cycles from the delay until the interrupt
    // mask is enabled again (which _must_ happen during the stopbit).
    // This delay aims at 3/4 of a bit time, meaning the end of the
    // delay will be at 1/4th of the stopbit. This allows some extra
    // time for ISR cleanup, which makes 115200 baud at 16Mhz work more
    // reliably
    _rx_delay_stopbit = subtract_cap(bit_delay * 3 / 4, (37 + 11) / 4);
    #else // Timings counted from gcc 4.3.2 output
    // Note that this code is a _lot_ slower, mostly due to bad register
    // allocation choices of gcc. This works up to 57600 on 16Mhz and
    // 38400 on 8Mhz.
    _rx_delay_centering = subtract_cap(bit_delay / 2, (4 + 4 + 97 + 29 - 11) / 4);
    _rx_delay_intrabit = subtract_cap(bit_delay, 11 / 4);
    _rx_delay_stopbit = subtract_cap(bit_delay * 3 / 4, (44 + 17) / 4);
    #endif
    ...
    tunedDelay(_tx_delay); // if we were low this establishes the end
  }
  ...
}

コード量自体は多くないが、正確なタイミングを計算するために各gccバージョンで消費されるCPUサイクル数まで計算し、それを差し引いた上でディレイをかけている。データ伝送におけるタイミングの重要性がよくわかる。タイマーや割り込み機構を使って実装することもできるが、ハードウェアタイマーの数にも限りがある。

Bit Banging

コードでデータ通信プロトコルを実装できるのは非常に便利だが、欠点としてプロセッサのリソースを大量に消費してしまう。通信周波数が高くなればなるほど、プロセッサはタイミング計算に多くのリソースを割かなければならなくなる。そのため、高精度な時間制御の出力が必要な場合や、プロセッサがプロトコル処理にリソースを食われすぎるのを防ぎたい場合に、PIOが大きな力を発揮する。

PIO(Programmable GPIO)

概要

前述の通り、課題は通信プロトコルが要求するタイミング処理がプロセッサのリソースを消費してしまう点にある。PIOは、プロセッサのリソースを消費することなく、プロセッサと同じ最高周波数(133MHz)で要求を満たすことができる。PIOは「GPIOの中に小さなプロセッサがもう一つ入っている」ようなものだとイメージできる。この小さなプロセッサはメインプロセッサのリソースを占有せず、GPIO専用に設計されており、FIFOやIRQを組み合わせてメインプロセッサと連携できる。

1つのRP2040には2つのPIOブロックがあり、1つのブロックの中に4つのステートマシン(State Machine)がある。各ステートマシンはプログラムを通じて再設定でき、実行時に異なる通信インターフェースを動的に実装できる。

PIOにはシンプルなアセンブリ言語が用意されている。命令は全部で9個、レジスタは2個だけで、最大32命令しか実行できない。非常に簡素に見えるが、大半の通信プロトコルの要件を満たすにはこれで十分だ。

PIO アーキテクチャ図

(画像はRP2040のデータシートより引用)

この図からわかるように、4つのステートマシンは同一のコードを共有しており、インストラクションメモリには4つの読み出しポート(read ports)があるため、各ステートマシンはブロッキングを起こすことなく同時にコードへアクセスできる。

ステートマシン(State Machine)の紹介

各PIOブロック内には4つのステートマシンがあり、同じプログラムメモリを共有するが、ステートマシンごとに異なるGPIOピンを設定できる。例えばUARTを実装した場合、4つのステートマシンを使えば最大4系統の完全に独立したUARTを設定可能だ。

ステートマシンは以下のパーツで構成されている:

  • OSR(Output Shift Register):32bit。メインプロセッサからFIFO経由でデータを受け取れる
  • ISR(Input Shift Register):32bit。FIFO経由でメインプロセッサへデータを渡せる
  • X、Yレジスタ:各ステートマシンに2つの汎用レジスタがある
  • PC:プログラムカウンタ
  • clock divider:各ステートマシンは最大でメインプロセッサと同じ周波数まで出せるが、大半の通信プロトコルにとっては速すぎるため、クロック分周器で周波数を調整できる(範囲は1〜65536)
  • プログラムコード

arduino2.drawio

IO mapping

IOマッピングは他のマイクロコントローラよりもやや複雑で、最初は少しややこしく感じるかもしれないが、理解すると非常に理にかなった設計だとわかる。各IOは、input、output、set、sidesetの4つの状態を持つことができる。

  • input:外部センサーや外部デバイスのデータを読み取る(Arduinoの digitalRead に似ている)
  • output:プログラムから電位のHigh/Lowを制御する(Arduinoの digitalWrite に似ている)
  • set:ピンの電位のHigh/Lowを設定する(outputに似ているが、いくつか違いがある)
  • sideset:命令を実行すると同時に、他のピンの電位や方向を変更する

setとsidesetは少し理解しづらいかもしれないが、これについては後で詳しく説明する。同一のGPIOが同時に複数の状態を持つことも可能で、例えばあるGPIOをinputに設定しつつ、同時にoutputに設定することもできる。

各IOマッピングの設定は、base pinとpin countによって行う。例えばGPIO0とGPIO1をSETに設定したい場合、base pinをGPIO0、countを2に設定する。ここからわかるように、各状態のピンは連続している必要がある。つまり、OUTPUTピンがGPIO0、GPIO3、GPIO5のように飛び飛びになる設定はできない。

INPUTとOUTPUTは最大32ピンまでサポートされている(Pico自体には30ピンしかないが)。setとsidesetは最大5ピンまでサポートする。

pico-io-mapping

まとめると、IOマッピングには以下の特徴がある:

  • 同一のピンが同時に複数の状態を持つことができる(例:同時にsetでありoutputでもある)
  • input、outputは最大32ピンまで対応。setとsidesetは最大5ピンまで対応
  • ピンは連続していなければならない(例:GPIO0〜GPIO3)

IRQ(割り込み要求:Interrupt Request)

IRQフラグを通じて割り込み(interrupt)をトリガーしたり、ステートマシン間の状態を同期したりできる。

PIOアセンブリ言語の紹介

PIOには、シンプルながら強力なアセンブリ言語が用意されている。命令は以下の9種類のみだ:

  • SET
  • IN
  • OUT
  • PULL
  • PUSH
  • JMP
  • WAIT
  • MOV
  • IRQ

基本的な書き方は一般的なアセンブリ言語と同じなので文法の細かな説明は省くが、PIOアセンブリ言語には覚えておくべき変数がいくつかある:

  • pins:このPIOが対象としているピンを表す。例えばGPIO0から開始した場合、pin0はGPIO0になる。GPIO2から開始した場合、pin0はGPIO2を指す
  • pindirs:ピンの方向を設定する。0は input、1は output
  • X、Y:レジスタ
  • osr:Output Shift Register
  • isr:Input Shift Register
  • data:即値(イミディエイト)で最大5bit、つまり0〜31を指定可能

レジスタがありjmpもあるので、チューリング完全の基本要件は満たしていると言える。理論上はPIOを使って四則演算を行うことも可能だが、PIOはそもそも計算用には設計されていないため、実験的な遊びとして捉えるのがよい。

待機機能(delay)

高精度なタイミング制御をインストラクションメモリを無駄にすることなく実現するため、PIOには非常に実用的な機能が備わっている。命令の実行時に後ろに [] を付けることで、ディレイさせるサイクル数を指定できる。例えば:

loop:
  set pins, 1 [1] ; setに1サイクル必要、さらに追加で1サイクル待つ
  set pins, 0
  jmp loop

これは以下と同等の効果を持つ:

loop:
  set pins, 1
  nop
  set pins, 0
  jmp  loop

こうすることで、pinsがHighの期間を2サイクル、Lowの期間を2サイクルに制御でき、nop を詰めて命令空間を消費しながらサイクル数を調整する必要がなくなる。[] 内の数値の範囲も同様に1〜31(5bit)だ。

Side-Set

命令を実行するのと同時に、side-setピンの電位を設定できる。プログラム内で使用するピン数を明示的に宣言する必要がある:

.side_set 1

これは1つのピンをside-setとして使用することを表す。最大5ピンまで設定でき、他のマッピング(inputやoutputなど)と重複してもよい。

これは状態遷移を行う際に非常に便利だ。例えばUARTはアイドル状態ではHighを維持し、スタートビットはLowになる。データを引き出す(pull)と同時に電位を反転させることができる(以下の例は pico-examples/uart_tx.pio より引用):

.program uart_tx
.side_set 1
  pull side 1 [7]
  set x, 7 side 0 [7]
loop:
  out pins, 1
  jmp x-- bitloop [6]

この例では、pull を呼び出すたびに同時にside-setピンを1に設定し、ストップビットとして機能させている。xレジスタを設定する際も、side-setを使って直接0に設定してスタートビットにできる。こうすれば設定のために別のサイクルと命令を消費する必要がなく、非常にスマートだ。

ここでは1ビットあたりに必要なサイクル数として8サイクルを使用しているため、1ビットの間に最大8個の命令を実行できる。使わなかったサイクル数があっても、delay機能を使えば正確なタイミングを簡単に実現できる。

注意点として、side-set を使用するとdelayに使えるサイクル数が減る。例えば:

.side_set 3 ; 3つのピンをside_setに設定
set x, 1 side 0 [3] ; 3bitがside_setに使われるため、delayは最大2bit(5-3)、すなわち1〜3までとなる

SET

SET

dataをdestinationに書き込む。destinationがpinsの場合は、SETとして設定されているピンに書き込まれる。

set X, 30 ; Xを30に設定
set pins, 1 ; SETピンを1に設定
set pins, 5 ; SETピンを5に設定(2進数では0b101)

IN

IN

sourceからbit count分のビットを右シフトまたは左シフトして取り込む。例えば:

in osr, 1

これはOSRの内容を1ビットシフトしてISRに格納することを意味する。あるいは:

in pins, 4

これは input pins(同様に、base pinから数える)から4ビットのデータを読み取り、ISRに格納することを意味する。

OUT

OUT

OSRのデータをシフトしてdestinationに格納する。例えば:

out pins, 2

これはOSRから2ビット取り出してoutputピンに出力することを意味する。

PULL

PULL

Tx FIFOから32ビットを読み出してOSRに格納する。pullの後ろにパラメータがない場合のデフォルトは noblock ではなく、実際はブロック動作となり、Txにデータが入ってくるまでプログラムは待機し続け、データが来てから次の実行に移る。例えば:

loop:
  pull ; Txにデータが来るまで待機し、データが来たら実行を継続する。なければここで停止し続ける
  out pins ; OSRのデータをoutput pinsに出力
  jmp loop

その他に以下のパラメータを設定できる:

  • ifempty:OSRが空になった時だけ実行し、そうでなければ何もしない
  • block:Txにデータがない間、待機し続ける(stall)
  • noblock:レジスタXのデータをOSRに入れる(MOV OSR, X と同等)

UARTのようなプロトコルでは、データ通信がない間はスタートビットを待ち続け、データを受信してから後続の処理を開始するため、block を使えばこうした動作を簡単に実現できる。

PUSH

PUSH

ISRのデータをRx FIFOに格納し、ISRを0にクリアする。

  • iffull:ISRがいっぱいになった時だけ実行し、そうでなければ何もしない
  • block:Rxがいっぱいの間、待機し続ける

JMP

JMP

条件が成立した時に指定されたアドレスへジャンプする。PIOのjmpには様々な条件(condition)を設定できる:

  • No condition:条件が未指定の場合は always

  • !X:X=0 の時にジャンプ

  • X--:実行後に自動で X - 1 され、デクリメント前の X!=0 の時にジャンプ(0でない時にジャンプ)

  • !Y:Y=0 の時にジャンプ

  • Y--:実行後に自動で Y - 1 され、デクリメント前の Y!=0 の時にジャンプ(0でない時にジャンプ)

  • X!=Y:X!=Y の時にジャンプ

  • PIN:input pinの電位の高低によってジャンプするか決定する(sm_config_set_jmp_pin 関数で設定が必要)

    • Highの時にジャンプ
    • Lowの時はジャンプしない
  • !OSRE:OSRが空の時にジャンプ

loop:
  set x, 30 ; xを設定
  jmp x-- loop ; xが0でない間loopへジャンプし、同時にxを-1する

WAIT

pico.008

条件が真になるまで待機し続ける。いくつか特有のパラメータがある:

  • GPIO:インデックスから対応するGPIOピンを選択する。これはステートマシンのIOマッピングとは無関係(絶対指定)である点に注意。
  • PIN:インデックスから対応するinputピンを選択する(ステートマシンのIOマッピングに基づく相対指定)
  • IRQ:指定されたインデックスのIRQフラグが指定の極性(polarity)になるまで待ってから次の行を実行する
wait 1 pin 0 ; input pin0が1になるのを待ってから次のコードを実行
wait 0 gpio 1 ; GPIO1が0になるのを待ってから次のコードを実行

MOV

MOV

データをsourceからdestinationへコピーする。操作を助ける2つの特殊構文がある:

  • ! または ~:コピー時にビット反転(ビット単位のNOT)を行う
  • :::ビットの並びを反転(ビットリバース)させてコピーする

この設計も命令空間を節約するためのものだ。

mov X, Y ; YをXにコピー
mov pins, X ; Xをpinsにコピー
mov pins, ::X ; Xのビットを反転させてpinsにコピー

IRQ

IRQ

IRQフラグのセットまたはクリアを行う。IRQフラグがセットされた後、メインプログラム側の設定に基づいて割り込みを発生させるかどうかを決定できる。

irq wait 1 rel

使用できるパラメータは以下の通り:

  • wait:フラグがクリアされるのを待ってから次の処理を継続する
  • nowait:フラグのクリアを待たずに次の処理を継続する
  • clear:IRQフラグをクリアする

パラメータがない場合のデフォルトは nowait で、フラグのクリアを待たずに後続を実行する。rel を付けると、「指定した番号 + 現在のステートマシンのインデックス(mod 4)」を対象にすることを意味する。これにより、メインプログラムがステートマシンごとに個別のアクションを取りやすくなる。

プログラムの実行位置の指定

ステートマシンを停止しない限り、コードの最後まで実行されると先頭に戻って繰り返し実行されるが、特定のラベルを追加してPIOにどこから開始し、どこで終了するかを指示することもできる:

set x, 8
.wrap_target
  set pins, 1
  set pins, 0
.wrap

.wrap_target と .wrap で囲むことでステートマシンが繰り返し実行するセクションを指定でき、jmp命令を1つ節約できる。

メインプログラムとの統合(C/C++)

pico-examples内には参考にできるPIOのサンプルがたくさん用意されているが、ここではシンプルなblink(Lチカ)機能を例として取り上げる。2

まず、拡張子 .pio でPIOコードを作成する:

.program blink
.wrap_target
  set pins, 1
	nop [19]
	nop [19]
	set pins, 0
	nop [19]
	nop [19]
.wrap

このプログラムは非常にシンプルで、setを使ってピンから1を出力し、40サイクル待ってから0に設定する。これによってLEDの点滅効果が得られる。次に初期化コードを書く必要があるが、公式ドキュメントではpioファイル内に直接記述することが推奨されている:

.program blink
.wrap_target
  set pins, 1
	nop [19]
	nop [19]
	set pins, 0
	nop [19]
	nop [19]
.wrap

% c-sdk {
void blink_program_init(PIO pio, uint sm, uint offset, uint pin, float div) {
   pio_sm_config c = blink_program_get_default_config(offset);
   pio_gpio_init(pio, pin);
   pio_sm_set_consecutive_pindirs(pio, sm, pin, 1, true); // この行は無くても構わない。今回の例ではset pinしか使っていないため
   sm_config_set_set_pins(&c, pin, 1);
	 sm_config_set_clkdiv(&c, div);
   pio_sm_init(pio, sm, offset, &c);
}
%}

% c-sdk{ %} で囲んで記述する。

  • blink_program_get_default_config で設定を取得(この関数は自動生成される)
  • pio_gpio_init(pio, pin) でPIOが使用するGPIOピンを設定
  • pio_sm_set_consecutive_pindirs(pio, sm, pin, 1, true) でピンの方向を設定(falseはinput、trueはoutput)
  • sm_config_set_set_pins(&c, pin, 1) でsetピンを設定
  • sm_config_set_clkdiv(&c, div) で分周比を設定
  • pio_sm_init(pio, sm, offset, &c) でステートマシンを初期化

set_set_pins の他にも set_sideset_pins などがある。

PIO機能を使用するには、target_link_libraries に hardware_pio を追加する必要がある:

cmake_minimum_required(VERSION 3.12)
include($ENV{PICO_SDK_PATH}/external/pico_sdk_import.cmake)
include($ENV{PICO_SDK_PATH}/tools/CMakeLists.txt)

project(pio C CXX ASM)
set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)

pico_sdk_init()

add_executable(${PROJECT_NAME}
  main.c
)

pico_add_extra_outputs(${PROJECT_NAME})
pico_generate_pio_header(${PROJECT_NAME}
  ${CMAKE_CURRENT_LIST_DIR}/blink.pio
)

+ target_link_libraries(${PROJECT_NAME}
+	pico_stdlib
+	hardware_pio
+)

pico_enable_stdio_usb(pio 1)
pico_enable_stdio_uart(pio 1)

続いて main.c の記述:

#include <stdio.h>
#include "pico/stdlib.h"
#include "hardware/pio.h"
#include "hardware/clocks.h"
#include "blink.pio.h" // pioのコンパイル後に自動生成されるヘッダーファイル

void blink(PIO pio, uint sm, uint offset, uint pin, uint freq);

int main()
{
	stdio_init_all();
	PIO pio = pio0;
	
	uint offset = pio_add_program(pio, &blink_program);
	blink(pio, 0, offset, 2, 2000);
	while (true)
	{
		printf("test");
		sleep_ms(200);
	}
}

void blink(PIO pio, uint sm, uint offset, uint pin, uint freq)
{
	float div = clock_get_hz(clk_sys) / freq;
	blink_program_init(pio, sm, offset, pin, div); // PIOファイル内で宣言した関数
	pio_sm_set_enabled(pio, sm, true);
}

クロック情報の取得やPIO関連の操作を行うために、hardware/pio.h と hardware/clocks.h をインクルードする必要がある。コンパイルしてコードを書き込むと、LEDが点滅していると同時に、シリアルには継続して test という文字列が出力されていることが確認できる。2つの機能が完全に独立しており、互いに影響を与えていない証拠だ!

まとめ

この記事では主にPIOの基本的な理解と構文の紹介を行った。次回はPIOを使ってUARTなどの一般的な通信プロトコルを実装してみたり、PIOを使ってDHT11温度湿度センサーと通信したりして、PIOへの理解をさらに深めていく予定だ。PIOは僕にとって非常に斬新な概念であり、僕が知る限り、このような設計を持つマイクロコントローラを見たのは初めてだ。

この設計によって、通信プロトコルによってプロセッサのリソースが大量に消費されるのを防げるだけでなく、開発者はハードウェアのサポート有無に縛られず、PIOを通じて直接やりたい機能を実現できるようになる。そのため、PIOがもたらす応用の可能性にとてもワクワクしている。また、公式からはRP2040単体でも販売されており、公式ドキュメントを参考にして独自のボードを作ることも可能だ3。

公式ドキュメント4は非常に詳細に書かれており、なぜこのような設計にしたのかがはっきりと伝わってくるので、ぜひ一読することをおすすめしたい。SDKの関数について疑問があればこちらで確認できる。

Footnotes

  1. /devnote/2020-07-24-arduino-esp32-co2-sensor-2/ ↩

  2. 開発環境のセットアップは https://www.raspberrypi.com/documentation/microcontrollers/raspberry-pi-pico.html を参照 ↩

  3. https://datasheets.raspberrypi.com/rp2040/hardware-design-with-rp2040.pdf ↩

  4. https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf ↩

関連記事

他のトピックを探索