25
19

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2023-08-13

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);
    }
}

pico-sdk-x10.png

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
} 

Arduino-SDK.png

いちおう念のために,プッシュプル出力の設定に変えて変化が無いかを確認しといた ←変化は無かった.
設定変更は次のコード例のとおり.

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()

mp-basic.png

結果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()

mp-10.png

結果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()

mp-nce.png

あとは..

「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()

スクリーンショット 2023-08-14 9.43.35.png

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

	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 ] = 0x2reg_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()

スクリーンショット 2023-09-03 15.26.29.png
スクリーンショット 2023-09-03 15.26.41.png

25
19
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
25
19

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?