Raspberry Pi Pico(ラズピコ)のIO速度をSDKの違いで比較してみた
これからちょっとラズピコでIOを直接叩く案件が出てきそう.で,そもそもそれがどれぐらい速くてSDKによってどれぐらい犠牲になるのかを試してみた.
試してみたのは次の3種類.
単純なIO出力をウェイトなしでトグル→周波数を測定.結果はこのとおり
追記情報:Viper code emitterを使うと速くなる.当初,このViper code emitter
なるものの使い方を知らずに,「Native code emitterでも同じ」なんて書いていたけどこれは間違いだった.
この記事の最下部に追記した.
SDK | 周波数 |
---|---|
Raspberry Pi Pico SDK | 50MHz(推定値) |
Arduino-Pico (digitalWrite使用) | 600kHz |
MicroPython | 88k~120kHz |
MicroPython(Viper code emitterによるポインタを使ったレジスタアクセス使用) | 31.24MHz(推定値) |
Raspberry Pi Pico C/C++ SDK
まずは純正のSDKから.
SDKのインストールの参考にしたのはこのページとこのドキュメントの9.1節.
使用したバージョンはSDK1.5.1.
コードはpico-examplesの「blink」を改変して,下のようなのを用意した.
#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コールだけでこの速度が出るって,うまく作ってくれてあるのねぇ.
#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);
}
}
Arduino-Pico
ラズピコはArduinoでも使えるらしい.Arduinoで使うためにはSDKが必要だけど,どうやらArduino公式と,公式ぢゃない「Arduino-Pico」があるっぽい.
しかし公式版はどうやらサポートが終了してしまってる雰囲気.Arduino-Picoはいまもアップデートが続いていている.この2年ほど前の記事でも「ソースをざっと眺めたかぎり」としながらもArduino-Picoを推している.
なのでこのArduino-Picoで試してみた.使用したバージョンはリリース3.3.2.
訳あってピンをオープンドレインに設定している.この設定でdigitalWrite()
で出力できるのかどうかの確認も行っている(←問題なくできるっぽい.外付け1kΩでプルアップを追加している).
結果
周波数は600kHzちょい.波形を見るとHIGHよりLOWの期間の方が長い.これはループのオーバーヘッドによる結果だろう.
ArduinoはC/C++とはいえ,そのAPIの下回りで色々やってくれてるんだろうな.
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
}
いちおう念のために,プッシュプル出力の設定に変えて変化が無いかを確認しといた ←変化は無かった.
設定変更は次のコード例のとおり.
void setup() {
// pinMode(sda_pin, INPUT_PULLUP); // ←← コメントアウト
pinMode(sda_pin, OUTPUT); // プッシュプル設定
}
ちなみにArduino環境ではAPIを使わずに直接ハードにアクセスして高速化する方法がある.
それからその後,Raspberry Pi Pico C/C++ SDKのAPIもそのまま使えるという情報を得た.
これらはこちらの記事↓↓にまとめておいた.
MicroPython
マイコンで楽にハードを叩ける便利なMicroPython.この環境はインタープリタなので速度についてはあまり期待できないけど,比較のためにやってみた.
使用したバージョンはv1.20.0 (2023-04-26)
結果0
88kHzぐらいになった.
import machine
import utime
pinout = machine.Pin( 0, machine.Pin.OUT )
while True:
pinout.on()
pinout.off()
結果1
さらにこれも期待できないけど,試しにループ・アンローリング(10倍)してみた→93kHz.
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()
結果2
さらにもうひとつ.アンロールしたコードを関数にまとめて,ネイティブコードエミッターを試してみた.結果は122kHz.
バイパーコードエミッターを使っても変化はなかった. (この記事の最後に訂正の追記を載せた)
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()
あとは..
「MicroPythonの『ハードウェアに直接アクセスする』
も試してみないといけないな」と思うけど,これはまた時間のある時に (´(ェ)`;
って,書いておきながら調べてみたらすぐにできたので追記.
メモリへの直接アクセスがmachine.mem32
(32ビットアクセスの場合)でできるのね.
で,IOがマップされている場所を探すとRP2040のデータシートの
「2.3.1.7. List of Registers」ですぐに見つかった.
これ,たぶん30本のGPIOがそのまま各32ビット・レジスタにマップされてるだけなのかな?と思ってらやっぱり当たってた.
IOのベース・アドレスが0xD0000000,出力のオフセットが0x010になってるのでここに値を書き込んでみる.
結果
110kHzぐらいになった.レジスタの連続アクセスなら速くなるかと思ったら,machine.mem32
のオーバーヘッドは大きいらしくあまり変わらない結果に.これは高速化のための手段として提供されてるのではなくて,ハードへの直接アクセスのためなんだろうな.
でもこの方法であれば複数のIOビットを同時に操作できるので,そのような用途には便利かもしれない.
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()
ちなみに,当初は下のようなコードを書いてみてたんだけど,かなり遅くなってしまった.
足し算とか変数へのアクセスなどがコードの通りに実行されているためだと思う.なのでできるだけその無駄な動きを避けるため,全部即値で置き換えた.
machine.mem32[ SIO_BASE + GPIO_OUT ] = OUTPUT_PATTERN_0
machine.mem32[ SIO_BASE + GPIO_OUT ] = OUTPUT_PATTERN_1
さらにこのレジスタへのアクセスは「バイパーコードエミッター」を使って高速化できる.
上のサンプルコードのようなレジスタへの直接アクセスは,バイパーコードエミッターを使うとポインタを使ってアクセスできるようになる.この例では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 ということになる.
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()