こんにちは、ENGかぴです。
VSCodeの拡張機能であるPico SDKを使用するとRaspberry Pi PicoにPIOを実装することができます。PIOはCPUとは独立したステートマシンで動作するためCPU負荷を減らしながらリアルタイム制御ができます。
本記事はPico SDKを使ってC/C++のプロジェクトを作成してPIOを実装します。PIOを使ってGPIOを操作してLEDを点灯/消灯させます。
Raspberry Pi Pico(以下Picoとする)と拡張基板のGrove Shield for Pi Picoを使用しています。
VSCodeのダウンロードとインストールの方法やVSCodeにPicoの開発環境を追加する方法は下記記事を参考にしてください。
Raspberry Pi Picoの開発をVSCodeで行う方法
Picoを使用してArduino IDEやVSCodeで動作確認したことをまとめています。
PIOのAPIを使用する

PIO(Programmable I/O)は独立したステートマシンでGPIO制御が行えるため高速な操作ができますが、アセンブリ言語でソースコードを作成する必要があります。
PicoはPIOブロックを2つ持っており、それぞれに4つのステートマシン(SM)を持つため最大で8つのステートマシンを同時に動作させることができます。
アセンブリ言語で動作させるためクロック単位でGPIOを操作できるため、APIを使用したGPIOの操作よりも高速・高精度な信号の生成ができます。
CPUはBus Fabricを介してPIOステートマシン(PIO State Machine)のレジスタ設定を行います。PIOがスタートするとPIO命令メモリに読み込んだアセンブリ言語で動作開始し、CPUによる命令がなくても独立して動作します。
本記事ではPIO(Programmable I/O)を使って、GPIO制御をステートマシンによって自律的にLEDを点滅させて動作確認を行います。
プロジェクトの生成

プロジェクトの生成方法はRaspberry Pi Picoの開発をVSCodeで行う方法で説明していますが、FeaturesのPIO interfaceにチェックを入れます。プロジェクトを生成するとデフォルトでPIO関連の初期化と定義が自動で生成されます。
PR:わからないを放置せず、あなたにあった最低限のスキルを身に着けるコツを教える テックジム 「書けるが先で、理解が後」を体験しよう!
PIOの設定
#include "blinktest.pio.h" //アセンブリファイル .pioをコンパイルすると生成される
PIO pio = pio0;
uint offset = pio_add_program(pio, &blinktest_program);
// 初期化構造体を取得(アセンブリファイルの定義によって名称が異なるため注意)
pio_sm_config c = blinktest_program_get_default_config(offset);
デフォルトのコードを流用しながらステートマシンの設定を行います。今回はステートマシンの設定は初期化処理内で完結するように実装します。
最初にステートマシン用のアセンブリファイルのヘッダーファイルをインクルードします。アセンブリファイルは後述のアセンブリ言語によるソースコードの作成で説明しているファイルです。
コンパイルするとアセンブリファイルの名称に拡張子の.hをつけたヘッダーファイルが生成されます。例はblinktest.pioがアセンブリファイルの名称ですが、.hをつけたヘッダーファイルを指定しています。
PIOブロックを選択します。PicoのPIOブロックはpio0とpio1の2つがありますが、ここではpio0を使用します。
pio_add_program()関数はPIOアセンブリプログラムをPIO命令メモリにロードし、その開始位置(offset)を取得します。第1引数にPIOブロックを指定し、第2引数にソースコードを示すアドレスを指定(アセンブリファイル内で定義している)します。
// 出力ピンの設定
sm_config_set_set_pins(&c, LED_PIN, 1);
pio_gpio_init(pio, LED_PIN);
sm_config_set_set_pins()関数はPIOステートマシンのSET命令で制御するGPIOピンを指定します。第1引数にステートマシンの設定用構造体を指定します。第2引数にGPIOピンの開始番号を指定します。第3引数に制御するピン数を指定します。
第2引数に0、第3引数に3を指定した場合はGPIO0~GPIO2がset命令の制御対象になります。
pio_gpio_init()関数はGPIOピンをPIO制御ピンに切り替えます。第1引数にPIOブロックを指定し、第2引数に対象のGPIOピン番号を指定します。
//PIOの入出力設定
pio_sm_set_consecutive_pindirs(pio,0, LED_PIN, 1, true);
//クロック分周
sm_config_set_clkdiv(&c, 50000.0f); //125MHz/50000 = 2.5KHz(0.4ms)
pio_sm_set_consecutive_pindirs()関数はステートマシンのGPIOピンの方向を設定します。第1引数にPIOブロックを指定し、第2引数にステートマシンの番号(0~3)を指定します。第3引数は設定を開始するGPIOのピン番号を指定し、第4引数で設定するピン数を指定します。第5引数はピンの方向を設定します。trueは出力、falseは入力になります。
sm_config_set_clkdiv()関数はシステムクロックの分周を指定します。第1引数にステートマシンの初期化構造体を指定します。第2引数に分周率を指定します。Picoの通常のシステムクロックは125MHzですが、例では50000で分周するため125MHz/50000= 2.5kHz(1命令当たり0.4ms)になります。
// ステートマシンに構成を適用して起動
pio_sm_init(pio, 0, offset, &c);
pio_sm_set_enabled(pio, 0, true);
pio_sm_init()関数はステートマシンに対して初期化した設定を適用し実行準備します。第1引数にPIOブロックを指定し、第2引数にステートマシンの番号(0~3)を指定します。
第3引数にpio_add_program()関数で取得した開始位置を指定します。第4引数にステートマシンの初期化構造体を指定します。
pio_sm_set_enabled()関数を使用すると指定したステートマシンで動作開始します。第1引数にPIOブロックを指定し、第2引数にステートマシンの番号(0~3)を指定します。第3引数でステートマシンを起動/停止を指定します。trueで起動し、falseで停止します。
CMakeファイルの変更
# Generate PIO header
#pico_generate_pio_header(blinkled ${CMAKE_CURRENT_LIST_DIR}/blink.pio)
pico_generate_pio_header(blinkled ${CMAKE_CURRENT_LIST_DIR}/blinktest.pio)
デフォルトで作成されたCMakeファイルに自作のアセンブリファイルを認識させるために処理を追加します。デフォルトでは2行目のようにサンプルのファイルが指定されていますが、使用しないので#を入れてコメントアウトし、コピーしてファイル名を変更します。
3行目のように自作のblinktest.pioを指定してヘッダーファイルを生成できるようにします。
アセンブリ言語によるソースコードの作成

VSCodeのプルジェクトファイル内にステートマシン用のアセンブリファイルを作成します。ファイル(F)から新しいファイルを選択するかエクスプローラー内で右クリックして新しいファイルを選択するとファイルが生成できます。ファイル名は任意でよいですが拡張子を.pioにします。例ではblinktest.pioでPIOファイルを作成しています。
.program blinktest
set pins, 1
loop:
set x, 25
set pins, 1
set1:
nop [24] ;10ms
nop [24] ;10ms
jmp x--, set1
set x, 25
set pins, 0
set2:
nop [24] ;10ms
nop [24] ;10ms
jmp x--, set2
jmp loop
アセンブリファイルのソースコードを作成します。Pico専用のアセンブリ言語はデータシートに記載(3.2.1. PIO Program)されています。
1行目の.program blinktestはPIOアセンブリの名前空間のラベル定義であり、C言語と側から指定する場合はblinktest_programという識別子で参照されます。この識別子はPico SDKが生成する関数に影響します。以下に本記事で使用しているアセンブリ命令をまとめました。
命令種別 | 説明 |
---|---|
set pins, <value> | GPIOピン群に対して<value>の値で出力する。 |
XXXX: | xxxxは任意のラベル名 jmp命令などの位置決めに使用しループ処理が構成できる。 |
set x, <value> | ステートマシンのx(y)レジスタに<value>をセットする。 |
nop[<delay>] | nopは1クロック待機。<delay>は0~31を指定する。 |
jmp x–, label | xレジスタを減らしながらラベル位置に条件付きジャンプする。xが0の場合はジャンプなし |
2行目はsm_config_set_set_pins()関数で指定したピンを1にする処理です。本記事の場合はGPIO16ピンをHigh(1)にします。
5行目から10行目はGPIOピンをHigh(1)にした状態でnop[24]を2回処理して20ms待機させます。その後、jmp命令でset1ラベルさせますが25回の条件付きジャンプを繰り返します。20ms×25=500ms間GPIOピンをHighにする処理になります。
12行目から17行目はGPIOピンをLow(0)にした状態で上記と同じように500ms待機させます。set2ラベルの繰り返しが終わるとjmp命令でloopラベルにジャンプします。
loopラベル内の処理を繰り返すため、500ms毎にGPIOピンがHigh/Lowで切り替わるためLEDが点灯/消灯します。
PR:スキマ時間で自己啓発!スマホで学べる人気のオンライン資格講座【スタディング】まずは気になる講座を無料で体験しよう!
動作確認
Picoの電源を入れるとPIOステートマシンが動作開始します。オシロスコープで波形を測定するためGPIO16(GP16)をPIOステートマシン用に設定しました。

波形を確認すると分周比を高くなっているので誤差が少し出ていますが約500ms毎に波形がHighとLowで切り替わっていることがわかります。
波形を測定した後はGPIO25(GP25)を指定するとボード上のLEDが点灯/消灯します。
今回のように低速なLEDの切り替えの場合はPIOを使用せず、メインループでタイマ管理をしてGPIOを切り替える方法で問題がないことが多いです。
RGBLED(WS2812)のようにDOで色を調整する素子を制御する場合にPIOを使用するとCPUの処理を介さずに制御できるため効率の良い制御ができます。
ソースコード全体
ソースコードは記事作成時点において動作確認できていますが、使用しているライブラリの更新により動作が保証できなくなる可能性があります。また、ソースコードを使用したことによって生じた不利益などの一切の責任を負いかねます。参考資料としてお使いください。
#include <stdio.h>
#include "pico/stdlib.h"
#include "hardware/pio.h"
#include "blinktest.pio.h"
//#define LED_PIN 16 //波形データ取得用
#define LED_PIN 25 //ボード上のLEDを使用する場合
int main()
{
stdio_init_all();
// PIO Blinking example
PIO pio = pio0;
uint offset = pio_add_program(pio, &blinktest_program);
// 初期化構造体を取得
pio_sm_config c = blinktest_program_get_default_config(offset);
// 出力ピンの設定
sm_config_set_set_pins(&c, LED_PIN, 1);
pio_gpio_init(pio, LED_PIN);
//PIOの入出力設定
pio_sm_set_consecutive_pindirs(pio,0, LED_PIN, 1, true);
//クロック分周
sm_config_set_clkdiv(&c, 50000.0f); //125MHz/50000 = 2.5KHz(0.4ms)
// ステートマシンに構成を適用して起動
pio_sm_init(pio, 0, offset, &c);
pio_sm_set_enabled(pio, 0, true);
while (true) {
//処理なし
}
}
アセンブリファイルのソースコード:
.program blinktest
set pins, 1
loop:
set x, 25
set pins, 1
set1:
nop [24] ;10ms
nop [24] ;10ms
jmp x--, set1
set x, 25
set pins, 0
set2:
nop [24] ;10ms
nop [24] ;10ms
jmp x--, set2
jmp loop
メインのファイルの内容をコピーして置き換えることで使用できます。
プロジェクトを生成時のCMakeファイルの構成によって動作しない場合があります。CMakeLists.txtの構成で条件が不足している可能性があるため必要に応じて修正してください。
関連リンク
Arduinoのライブラリを使って動作確認を行ったことを下記リンクにまとめています。
Arduinoで学べるマイコンのソフト開発と標準ライブラリの使い方
Seeeduino XIAOで学べるソフト開発と標準ライブラリの使い方
ESP32-WROOM-32Eで学べるソフト開発と標準ライブラリの使い方
PR:企業で求められる即戦力技術を身に付ける テックキャンプエンジニア転職
最後まで、読んでいただきありがとうございました。
今回の例ではLEDの点灯/消灯を目視できるように分周を大きくとっていますが、PIOは高分解能(高速なクロック)でGPIOを制御する用途で使用されることが多いです。