Raspberry Pi Pico(ラズピコ)：各種SDKによるIO速度の比較

# [Raspberry Pi Pico(ラズピコ)](https://www.raspberrypi.com/products/raspberry-pi-pico/)のIO速度をSDKの違いで比較してみた
これからちょっとラズピコでIOを直接叩く案件が出てきそう．で，そもそもそれがどれぐらい速くてSDKによってどれぐらい犠牲になるのかを試してみた． 

試してみたのは次の3種類．　
単純なIO出力をウェイトなしでトグル→周波数を測定．結果はこのとおり 

:::note info
追記情報：[Viper code emitter](https://micropython-docs-ja.readthedocs.io/ja/latest/reference/speed_python.html#the-viper-code-emitter)を使うと速くなる．当初，この`Viper code emitter`なるものの使い方を知らずに，「[Native code emitter](https://micropython-docs-ja.readthedocs.io/ja/latest/reference/speed_python.html#the-native-code-emitter)でも同じ」なんて書いていたけどこれは間違いだった． 
この記事の最下部に追記した．
:::

|SDK|周波数|
|----|----|
|[Raspberry Pi Pico SDK](https://github.com/raspberrypi/pico-sdk)|50MHz(推定値)|
|[Arduino-Pico](https://github.com/earlephilhower/arduino-pico) (digitalWrite使用)|600kHz|
|[MicroPython](https://micropython.org)|88k~120kHz|
|[MicroPython](https://micropython.org)(Viper code emitterによるポインタを使ったレジスタアクセス使用)|31.24MHz(推定値)|

## Raspberry Pi Pico C/C++ SDK
まずは純正のSDKから．
SDKのインストールの参考にしたのは[このページ](https://vanhunteradams.com/Pico/Setup/PicoSetupMac.html)と[このドキュメント](https://datasheets.raspberrypi.com/pico/getting-started-with-pico.pdf)の9.1節．
使用したバージョンは[SDK1.5.1](https://github.com/raspberrypi/pico-sdk/releases/tag/1.5.1)．


コードは[pico-examples](https://github.com/raspberrypi/pico-examples)の「blink」を改変して，下のようなのを用意した．

```c 
#include "pico/stdlib.h"

int main() {
//    const uint LED_PIN = PICO_DEFAULT_LED_PIN;
    const uint LED_PIN = 0;
    gpio_init(LED_PIN);
    gpio_set_dir(LED_PIN, GPIO_OUT);
    while (true) {
		gpio_put(LED_PIN, 1);
//        sleep_ms(100);
		gpio_put(LED_PIN, 0);
//        sleep_ms(100);
    }
}
```


### 結果
IOはかなり速い．LPC1768のFastGPIO(CPUクロックサイクルでIOを叩ける)と同じぐらいかもしれない． 
残念ながら家にあるオシロスコープでは直接その周波数を測ることはできなかった． 
なのでしょうがなくコードを下のように変更，10倍に引き伸ばして周波数を見てみたところ，5MHzとなった．素のIOトグルなら50MHz出てたのかも？

単純なAPIコールだけでこの速度が出るって，うまく作ってくれてあるのねぇ．

```c 
#include "pico/stdlib.h"

int main() {
//    const uint LED_PIN = PICO_DEFAULT_LED_PIN;
    const uint LED_PIN = 0;
    gpio_init(LED_PIN);
    gpio_set_dir(LED_PIN, GPIO_OUT);
    while (true) {
		gpio_put(LED_PIN, 1);
		gpio_put(LED_PIN, 1);
		gpio_put(LED_PIN, 1);
		gpio_put(LED_PIN, 1);
		gpio_put(LED_PIN, 1);
		gpio_put(LED_PIN, 1);
		gpio_put(LED_PIN, 1);
		gpio_put(LED_PIN, 1);
		gpio_put(LED_PIN, 1);
		gpio_put(LED_PIN, 1);
//        sleep_ms(100);
		gpio_put(LED_PIN, 0);
		gpio_put(LED_PIN, 0);
		gpio_put(LED_PIN, 0);
		gpio_put(LED_PIN, 0);
		gpio_put(LED_PIN, 0);
		gpio_put(LED_PIN, 0);
		gpio_put(LED_PIN, 0);
		gpio_put(LED_PIN, 0);
		gpio_put(LED_PIN, 0);
		gpio_put(LED_PIN, 0);
//        sleep_ms(100);
    }
}
```

![pico-sdk-x10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3288603/ba059c2f-bf31-418c-cc33-4dca176df444.png)

## Arduino-Pico
ラズピコはArduinoでも使えるらしい．Arduinoで使うためにはSDKが必要だけど，どうやらArduino公式と，公式ぢゃない「[Arduino-Pico](https://github.com/earlephilhower/arduino-pico)」があるっぽい．　
しかし公式版はどうやらサポートが終了してしまってる雰囲気．Arduino-Picoはいまもアップデートが続いていている．この[2年ほど前の記事](https://lipoyang.hatenablog.com/entry/2021/11/27/202142)でも「ソースをざっと眺めたかぎり」としながらもArduino-Picoを推している． 
なのでこのArduino-Picoで試してみた．使用したバージョンは[リリース3.3.2](https://github.com/earlephilhower/arduino-pico/releases/tag/3.3.2)．

訳あってピンを[オープンドレイン](https://knowledge.ni.com/KnowledgeArticleDetails?id=kA03q000000YHwFCAW&l=ja-JP)に設定している．この設定で`digitalWrite()`で出力できるのかどうかの確認も行っている（←問題なくできるっぽい．外付け1kΩでプルアップを追加している）．

### 結果 
周波数は600kHzちょい．波形を見るとHIGHよりLOWの期間の方が長い．これはループのオーバーヘッドによる結果だろう．
ArduinoはC/C++とはいえ，そのAPIの下回りで色々やってくれてるんだろうな．

```c
constexpr int sda_pin  = 0;

void setup() {
  pinMode(sda_pin, INPUT_PULLUP);
}

void loop() {
  digitalWrite(sda_pin, HIGH);  // turn the LED on (HIGH is the voltage level)
  digitalWrite(sda_pin, LOW);   // turn the LED off by making the voltage LOW
} 
```
![Arduino-SDK.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3288603/db79e8f8-4865-02a7-c9e7-790e8b1e421a.png)

いちおう念のために，プッシュプル出力の設定に変えて変化が無いかを確認しといた ←変化は無かった．
設定変更は次のコード例のとおり．
```c
void setup() {
  //  pinMode(sda_pin, INPUT_PULLUP);  // ←← コメントアウト
  pinMode(sda_pin, OUTPUT);  //  プッシュプル設定
}
```
ちなみにArduino環境ではAPIを使わずに直接ハードにアクセスして高速化する方法がある． 
それからその後，Raspberry Pi Pico C/C++ SDKのAPIもそのまま使えるという情報を得た． 
これらはこちらの記事↓↓にまとめておいた．

https://qiita.com/teddokano/items/b77b782292b80bfcd374


## MicroPython
マイコンで楽にハードを叩ける便利なMicroPython．この環境はインタープリタなので速度についてはあまり期待できないけど，比較のためにやってみた．
使用したバージョンは[v1.20.0 (2023-04-26)](https://micropython.org/download/rp2-pico/) 

### 結果0
88kHzぐらいになった．
```python  
import	machine
import	utime
pinout	= machine.Pin( 0, machine.Pin.OUT )

while True:
	pinout.on()
	pinout.off()
```
![mp-basic.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3288603/936315bd-61f3-677c-afe0-2d38c0988347.png)

### 結果1
さらにこれも期待できないけど，試しにループ・アンローリング(10倍)してみた→93kHz．　

```python 
import	machine
import	utime
pinout	= machine.Pin( 0, machine.Pin.OUT )

while True:
	pinout.on()
	pinout.off()
	pinout.on()
	pinout.off()
	pinout.on()
	pinout.off()
	pinout.on()
	pinout.off()
	pinout.on()
	pinout.off()
	pinout.on()
	pinout.off()
	pinout.on()
	pinout.off()
	pinout.on()
	pinout.off()
	pinout.on()
	pinout.off()
	pinout.on()
	pinout.off()
```
![mp-10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3288603/311d54f9-cba5-e246-ed96-e5632faac337.png)

### 結果2
さらにもうひとつ．アンロールしたコードを関数にまとめて，[ネイティブコードエミッター](https://micropython-docs-ja.readthedocs.io/ja/latest/reference/speed_python.html#the-native-code-emitter)を試してみた．結果は122kHz．
~~[バイパーコードエミッター](https://micropython-docs-ja.readthedocs.io/ja/latest/reference/speed_python.html#the-viper-code-emitter)を使っても変化はなかった．~~ (この記事の最後に訂正の追記を載せた)

```python 
import	machine
import	utime
pinout	= machine.Pin( 0, machine.Pin.OUT )

@micropython.native
def toggle():
	pinout.on()
	pinout.off()
	pinout.on()
	pinout.off()
	pinout.on()
	pinout.off()
	pinout.on()
	pinout.off()
	pinout.on()
	pinout.off()
	pinout.on()
	pinout.off()
	pinout.on()
	pinout.off()
	pinout.on()
	pinout.off()
	pinout.on()
	pinout.off()
	pinout.on()
	pinout.off()

while True:
	toggle()
```
![mp-nce.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3288603/80bda740-5aaa-f433-db01-597812028266.png)

## あとは..

「MicroPythonの『[ハードウェアに直接アクセスする](https://micropython-docs-ja.readthedocs.io/ja/latest/reference/speed_python.html#accessing-hardware-directly)』
も試してみないといけないな」と思うけど，これはまた時間のある時に （´(ｪ)｀;

って，書いておきながら調べてみたらすぐにできたので追記． 

メモリへの直接アクセスが`machine.mem32`(32ビットアクセスの場合)でできるのね． 
で，IOがマップされている場所を探すと[RP2040のデータシート](https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf)の
「2.3.1.7. List of Registers」ですぐに見つかった．　
これ，たぶん30本のGPIOがそのまま各32ビット・レジスタにマップされてるだけなのかな？と思ってらやっぱり当たってた．

IOのベース・アドレスが0xD0000000，出力のオフセットが0x010になってるのでここに値を書き込んでみる． 


### 結果
110kHzぐらいになった．レジスタの連続アクセスなら速くなるかと思ったら，`machine.mem32`のオーバーヘッドは大きいらしくあまり変わらない結果に．これは高速化のための手段として提供されてるのではなくて，ハードへの直接アクセスのためなんだろうな．

でもこの方法であれば複数のIOビットを同時に操作できるので，そのような用途には便利かもしれない． 

```python 
import	machine
import	utime
pinout0	= machine.Pin( 0, machine.Pin.OUT )
pinout1	= machine.Pin( 1, machine.Pin.OUT )

SIO_BASE	= 0xD0000010
GPIO_OUT	= 0x010

OUTPUT	= SIO_BASE + GPIO_OUT

OUTPUT_PATTERN_0	= 0x2
OUTPUT_PATTERN_1	= 0x1

@micropython.native
def toggle():
	machine.mem32[ 0xD0000010 ] = 0x2
	machine.mem32[ 0xD0000010 ] = 0x1
	machine.mem32[ 0xD0000010 ] = 0x2
	machine.mem32[ 0xD0000010 ] = 0x1
	machine.mem32[ 0xD0000010 ] = 0x2
	machine.mem32[ 0xD0000010 ] = 0x1
	machine.mem32[ 0xD0000010 ] = 0x2
	machine.mem32[ 0xD0000010 ] = 0x1
	machine.mem32[ 0xD0000010 ] = 0x2
	machine.mem32[ 0xD0000010 ] = 0x1
	machine.mem32[ 0xD0000010 ] = 0x2
	machine.mem32[ 0xD0000010 ] = 0x1
	machine.mem32[ 0xD0000010 ] = 0x2
	machine.mem32[ 0xD0000010 ] = 0x1
	machine.mem32[ 0xD0000010 ] = 0x2
	machine.mem32[ 0xD0000010 ] = 0x1
	machine.mem32[ 0xD0000010 ] = 0x2
	machine.mem32[ 0xD0000010 ] = 0x1
	machine.mem32[ 0xD0000010 ] = 0x2
	machine.mem32[ 0xD0000010 ] = 0x1

while True:
	toggle()
```

![スクリーンショット 2023-08-14 9.43.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3288603/39a77d55-073e-c1df-1de2-de9f3b6d68c3.png)

ちなみに，当初は下のようなコードを書いてみてたんだけど，かなり遅くなってしまった． 
足し算とか変数へのアクセスなどがコードの通りに実行されているためだと思う．なのでできるだけその無駄な動きを避けるため，全部即値で置き換えた． 

```python 
	machine.mem32[ SIO_BASE + GPIO_OUT ] = OUTPUT_PATTERN_0
	machine.mem32[ SIO_BASE + GPIO_OUT ] = OUTPUT_PATTERN_1
```

### さらにこのレジスタへのアクセスは「[バイパーコードエミッター](https://micropython-docs-ja.readthedocs.io/ja/latest/reference/speed_python.html#the-viper-code-emitter)」を使って高速化できる．　

上のサンプルコードのようなレジスタへの直接アクセスは，バイパーコードエミッターを使うとポインタを使ってアクセスできるようになる．この例では`toggle()`関数の前に`@micropython.viper`デコレータを宣言しておく． 
この関数内では`reg_out_p = ptr32( _REG_GPIO_OUT )`によって32ビットのポインタを定義して，その最初の要素に値を書き込むことでIOレジスタを更新している．　

ちなみに以前の記述では定数などを変数として宣言しておくと，いちいちそれを参照して値を得るため遅くなるからできるだけ即値で書いたりしてたけど，どうやら`_`で始まる名前で`const()`宣言を使うと，Cの`#define`的なことができるらしい．上記の`_REG_GPIO_OUT`もこの方法を使っている．

`reg_out_p[ 0 ] = 0x2`と`reg_out_p[ 0 ] = 0x1`を交互にアンロールした状態では，出力のトグルはかなり早くて周波数を直接測ることができなかったため，同じピン出力を10回ずつ並べて書くことによって周波数を下げた． 
この結果得られた周波数は3.124MHz． ピン出力の切替を交互に行えば10倍早くなるので，実際に出せる速度は**31.24MHz** ということになる．

```python
import	machine
from micropython import const

pinout0	= machine.Pin( 0, machine.Pin.OUT )
pinout1	= machine.Pin( 1, machine.Pin.OUT )

_REG_GPIO_OUT	= const( 0xD0000010 )

@micropython.viper
def toggle():
	reg_out_p	= ptr32( _REG_GPIO_OUT )
	
	# (phase 0)
	reg_out_p[ 0 ] = 0x2
	reg_out_p[ 0 ] = 0x2
	reg_out_p[ 0 ] = 0x2
	reg_out_p[ 0 ] = 0x2
	reg_out_p[ 0 ] = 0x2
	reg_out_p[ 0 ] = 0x2
	reg_out_p[ 0 ] = 0x2
	reg_out_p[ 0 ] = 0x2
	reg_out_p[ 0 ] = 0x2
	reg_out_p[ 0 ] = 0x2

	# (phase 1)
	reg_out_p[ 0 ] = 0x1
	reg_out_p[ 0 ] = 0x1
	reg_out_p[ 0 ] = 0x1
	reg_out_p[ 0 ] = 0x1
	reg_out_p[ 0 ] = 0x1
	reg_out_p[ 0 ] = 0x1
	reg_out_p[ 0 ] = 0x1
	reg_out_p[ 0 ] = 0x1
	reg_out_p[ 0 ] = 0x1
	reg_out_p[ 0 ] = 0x1

    ... # 以下phase0, phase1を合計10回繰り返す
    ..

while True:
	toggle()
```

![スクリーンショット 2023-09-03 15.26.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3288603/2b4d82c9-1c75-e9fe-52d7-6495ce822815.png)
![スクリーンショット 2023-09-03 15.26.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3288603/631c64e8-851c-739d-b5e1-4e4888c11c95.png)