こんにちは、ENGかぴです。
VSCodeの拡張機能であるPico SDKを使用するとRaspberry Pi PicoにPIOを実装することができます。PIOは高速でGPIOを操作する場合に使用すると効果的です。応用例としてRGB LEDを操作する方法をまとめました。
本記事はPico SDKを使ってC/C++のプロジェクトを作成してPIOを実装します。PIOを実装する方法を下記記事にまとめています。
Raspberry Pi PicoのPIOでGPIOを操作する
今回はGrove Mech Keycap(Seeed Studio:秋月電子で購入)に使用されているRGB LEDを操作します。Raspberry Pi Pico(以下Picoとする)と拡張基板のGrove Shield for Pi Picoを使用しています。
VSCodeのダウンロードとインストールの方法やVSCodeにPicoの開発環境を追加する方法は下記記事を参考にしてください。
Raspberry Pi Picoの開発をVSCodeで行う方法
Picoを使用してArduino IDEやVSCodeで動作確認したことをまとめています。
RGB LEDを操作する

Grove Mech Keycapに使用されているRGB LEDはWS2812Bが使用されています。RGB LEDの操作はRGB LEDはユニポーラゼロ調整コードを使用します。一色あたり8ビットの構成になるため256パターンの調整ができます。
RGB LEDは使用する製品によって色の調整がRGBの順に操作するものやGRBで操作するプロトコルがあります。WS2812BはGRBで操作するプロトコルですが、色の順が異なるだけで操作方法は同じなのでRGB LEDで表記を統一します。

WS2812Bは緑・赤・青の順に24ビットの値で色を指定します。Sequence Chartのタイミングに従って0 codeのHigh/Lowや1codeのHigh/Lowの信号を生成して色のパターンを操作します。
データシートによると1 codeとして認識させるためには標準でT1H(1 code, high voltage time)が580ns~1usとT1L(1 code, low voltage time)が580ns~1us必要です。
0 codeとして認識させる場合は標準でT0H(0 code, high voltage time)が220ns~380nsとT0L(1 code, low voltage time)が580ns~1us必要です。
220ns~380nsと高速なGPIOの制御が必要となるためPIOによる制御が効果的です。1と0の長さはnopによるウェイトで範囲に収めるように操作してタイミング波形を生成します。
PR:
わからないを放置せず、あなたにあったスキルを身に着けるコツを教える テックジムPython入門講座の申込
PIOのAPIを使用する
PIOの説明や初期化など使用方法は下記記事にまとめています。
Raspberry Pi PicoのPIOでGPIOを操作する
ここでは、WS2812Bを操作するために必要な処理を中心に説明します。
プロジェクトの生成

プロジェクトの生成方法はRaspberry Pi Picoの開発をVSCodeで行う方法で説明していますが、FeaturesのPIO interfaceにチェックを入れます。プロジェクトを生成するとデフォルトでPIO関連の初期化と定義が自動で生成されます。
PR:わからないを放置せず、あなたにあった最低限のスキルを身に着けるコツを教える テックジム 「書けるが先で、理解が後」を体験しよう!
PIOの設定
#include "ws2812asm.pio.h" //アセンブリファイル .pioをコンパイルすると生成される
PIO pio = pio0;
uint offset = pio_add_program(pio, &ws2812_program);
// 初期化構造体を取得(アセンブリファイルの定義によって名称が異なるため注意)
pio_sm_config c = ws2812_program_get_default_config(offset);
デフォルトのコードを流用しながらステートマシンの設定を行います。今回はステートマシンの設定は初期化処理内で完結するように実装します。
最初にステートマシン用のアセンブリファイルのヘッダーファイルをインクルードします。アセンブリファイルは後述のアセンブリ言語によるソースコードの作成で説明しているファイルです。
コンパイルするとアセンブリファイルの名称に拡張子の.hをつけたヘッダーファイルが生成されます。例はws2812asm.pioがアセンブリファイルの名称ですが、.hをつけたヘッダーファイルを指定しています。
PIOブロックを選択します。PicoのPIOブロックはpio0とpio1の2つがありますが、ここではpio0を使用します。
pio_add_program()関数はPIOアセンブリプログラムをPIO命令メモリにロードし、その開始位置(offset)を取得します。第1引数にPIOブロックを指定し、第2引数にソースコードを示すアドレスを指定(アセンブリファイル内で定義している)します。
// 出力ピンの設定
sm_config_set_set_pins(&c, RGBLED_PIN, 1);
pio_gpio_init(pio, RGBLED_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, RGBLED_PIN, 1, true);
//クロック分周
sm_config_set_clkdiv_int_frac(&c, 5, 0);
//出力シフトレジスタの設定
sm_config_set_out_shift(&c,false,true,32);
pio_sm_set_consecutive_pindirs()関数はステートマシンのGPIOピンの方向を設定します。第1引数にPIOブロックを指定し、第2引数にステートマシンの番号(0~3)を指定します。第3引数は設定を開始するGPIOのピン番号を指定し、第4引数で設定するピン数を指定します。第5引数はピンの方向を設定します。trueは出力、falseは入力になります。
sm_config_set_clkdiv_int_frac()関数はシステムクロックの分周を指定します。第1引数にステートマシンの初期化構造体を指定します。第2引数にクロック分周の整数部(1~65535)を指定します。第3引数にクロック分周の小数部(0~255)を指定します。
今回は整数部に5、小数部に0を指定したので25MHz(40ns)がステートマシンの動作クロックになります。
sm_config_set_clkdiv()関数を使用しても動作クロックを生成することができますこの場合は第2引数に5を指定して25MHzを生成します。
sm_config_set_out_shift()関数は指定した値を右シフトして出力する左シフトして出力するか選択します。第1引数にステートマシンの初期化構造体を指定します。第2引数にシフトする方向を指定します。trueを指定するとLSBファースト、falseを指定するとMSBファーストになります。
第3引数は第4引数で指定するビット単位でFIFOからデータを読み込むか指定します。trueなら自動的にTX FIFOから次のデータを読み出します。第4引数はTX FIFOから読み出すビット数を指定します。
// ステートマシンに構成を適用して起動
pio_sm_init(pio, 0, offset, &c);
pio_sm_set_enabled(pio, 0, true);
pio_sm_put_blocking(pio, 0, color ); //FIFOにデータを送る
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で停止します。
pio_sm_put_blocking()関数を使用すると指定したデータをステートマシンに送信します。TX FIFOにデータを送るため、sm_config_set_out_shift()関数で自動的に送信するように指定することで送ったデータでステートマシンを操作することができます。
第1引数にPIOブロックを指定し、第2引数にステートマシンの番号(0~3)を指定します。第3引数に送信する32ビットデータを指定します。
CMakeファイルの変更
# Generate PIO header
#pico_generate_pio_header(ws2812 ${CMAKE_CURRENT_LIST_DIR}/blink.pio)
pico_generate_pio_header(ws2812 ${CMAKE_CURRENT_LIST_DIR}/ws2812asm.pio)
デフォルトで作成されたCMakeファイルに自作のアセンブリファイルを認識させるために処理を追加します。デフォルトでは2行目のようにサンプルのファイルが指定されていますが、使用しないので#を入れてコメントアウトし、コピーしてファイル名を変更します。
3行目のように自作のws2812asm.pioを指定してヘッダーファイルを生成できるようにします。
アセンブリ言語によるソースコードの作成

VSCodeのプルジェクトファイル内にステートマシン用のアセンブリファイルを作成します。ファイル(F)から新しいファイルを選択するかエクスプローラー内で右クリックして新しいファイルを選択するとファイルが生成できます。
ファイル名は任意でよいですが拡張子を.pioにします。例ではws2812asm.pioでPIOファイルを作成しています。
Pico専用のアセンブリ言語はデータシートに記載(3.2.1. PIO Program)されています。以下に本記事で使用しているアセンブリ命令をまとめました。
| 命令種別 | 説明 |
|---|---|
| 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の場合はジャンプなし |
| pull | TX FIFOからデータを読み出して、OSR(Output Shift Register)に格納する。 |
| mov x, osr | OSRに格納されたデータを、Xレジスタに格納します。 |
| out x, <value> | OSR(Output Shift Register)から<value>ビット取り出して、xレジスタに移す。 |
アセンブリファイルにのソースコードを作成します。アセンブリ言語のルールに従って処理を追加しますが、1つの命令で1クロック(40ns)になるため注意が必要です。
.program ws2812
set y, 23
pull ; FIFO → OSR 自動で移す場合はなくてもよい
mov x, osr ; OSR → x
loop:
set pins, 1
out x, 1
jmp !x dolow
nop [16] ;Highの場合のH維持
set pins, 0
nop [17] ;Highの場合のL
jmp continue
dolow:
nop [4] ;Lowの場合のH維持
set pins, 0
nop [17] ;Lowの場合のL
continue:
jmp y--, loop
end:
nop[1] ;処理なしエンド
1行目の.program ws2812はPIOアセンブリの名前空間のラベル定義であり、C言語と側から指定する場合はws2812_programという識別子で参照されます。この識別子はPico SDKが生成する関数に影響します。
3行目から5行目はyレジスタに23をセットし、TX FIFOから32ビットのデータを読み出してXレジスタに格納押します。22行目のjmpの条件にyを-1しながら0になるまで繰り返すので24回の繰り返しになります。
8行目はGPIOをHighにします。9行目のOUT命令でOSRのMSBから1ビットデータを取り出してxレジスタに格納します。10行目でビットが0であればdolowにジャンプします。ビットが1であればそのまま次の行に進みます。
11行目のnop[16]は1命令に加えて16回のクロックを待機するため40×17=680nsになります。8行目でGPIOをHighにしてから10行目までの3命令分の時間が経過しているのでnopを抜けるまでは800nsになります。これはT1Hに相当します。
12行目の1命令でGPIOをLowにし13行目のnop[17]で1命令に加えて17回クロックを待機させます。これらによって40×18=720nsになりますが、14行目のjmp命令と22行目のjmp命令文を考慮すると2命令になるので、Lowの区間が800nsになります。これはT1Lに相当します。
10行目でビットが1の場合は17行目にジャンプします。8行目から10行目までの3命令に加えて17行目のnop[4]によってGPIOがHighの区間が40×8=320nsになります。これはT0Hに相当します。
18行目の1命令でGPIOをLowにし19行目のnop[17]で1命令に加えて17回クロックを待機させます。これらによって40×19=760nsになります。22行目のjmp命令を考慮すると1命令になるのでLowの区間が800nsになります。これはT0Lに相当します。
22行目のjmp命令でyレジスタを減算して0になるまでloopラベルに戻って繰り返すことで24ビット分の信号を送信することができます。
PR:スキマ時間で自己啓発!スマホで学べる人気のオンライン資格講座【スタディング】まずは気になる講座を無料で体験しよう!
動作確認

Picoの電源を入れるとPIOステートマシンが動作開始します。動作開始するとGrove Mech KeycapのRGB LEDが緑→赤→青→緑→・・・を2秒ごとに切り替えます。オシロスコープでGPIO17(GP17)の波形を確認しました。

T1Hの波形を確認すると800ns間Highになっており、580ns~1usの範囲に入っています。

T1Lの波形を確認するとLowの期間が800nsであり、580ns~1usの範囲に入っています。

T0Hの波形を確認するとHighの期間が320nsであり、220ns~380nsの範囲に入っています。

T0Lの波形を確認するとLowの期間が800nsであり、580ns~1usの範囲に入っています。
T1H・T1L・T0H・T0Lの波形は範囲内の中央値を基準に生成していますが、波形を確認するとほぼ中央値になっていることが確認できました。次にGrove Mech KeycapのRGB LEDの動作を確認しました。

RGB LEDの色が2秒毎に切り替わっていることが確認できました。PIOの操作によってうまく動作していることが確認できました。
ソースコード全体
ソースコードは記事作成時点において動作確認できていますが、使用しているライブラリの更新により動作が保証できなくなる可能性があります。また、ソースコードを使用したことによって生じた不利益などの一切の責任を負いかねます。参考資料としてお使いください。
#include <stdio.h>
#include "pico/stdlib.h"
#include "hardware/pio.h"
#include "ws2812asm.pio.h"
#define RGBLED_PIN 17
#define PIN_DO1 25
#define TIME_OFF -1 //タイマーを使用しない場合
#define TIME_UP 0 //タイムアップ
#define BASE_CNT 10 //ベースタイマカウント値
#define LED_ONOFF 50
#define COLOR_CNG 200
PIO pio = pio0;
const uint32_t color_G = 0xFF000000; //G
const uint32_t color_R = 0x00FF0000; //R
const uint32_t color_B = 0x0000FF00; //B
int8_t colorcnt;
uint32_t beforetimCnt;
int16_t timled;
int16_t timcolor;
void mainApp(void);
void mainTimer(void);
int main()
{
stdio_init_all();
gpio_init(PIN_DO1); // ピン初期化
gpio_set_dir(PIN_DO1, GPIO_OUT); //DOピン
uint offset = pio_add_program(pio, &ws2812_program);
// 初期化構造体を取得
pio_sm_config c = ws2812_program_get_default_config(offset);
// 出力ピンの設定
sm_config_set_set_pins(&c, RGBLED_PIN, 1);
pio_gpio_init(pio, RGBLED_PIN);
//PIOの入出力設定
pio_sm_set_consecutive_pindirs(pio,0, RGBLED_PIN, 1, true);
//クロック分周
sm_config_set_clkdiv_int_frac(&c, 5, 0);
sm_config_set_out_shift(&c,false,true,32);
// ステートマシンに構成を適用して起動
pio_sm_init(pio, 0, offset, &c);
pio_sm_set_enabled(pio, 0, true);
while (true) {
mainApp();
mainTimer();
}
}
/* メイン処理 */
void mainApp(void){
if( timled == TIME_UP ){
timled = LED_ONOFF;
gpio_put(PIN_DO1, !gpio_get(PIN_DO1));
}
if( timcolor == TIME_UP ){
timcolor = COLOR_CNG;
switch (colorcnt)
{
case 0:
pio_sm_put_blocking(pio, 0, color_G ); //緑
break;
case 1:
pio_sm_put_blocking(pio, 0, color_R ); //赤
break;
case 2:
pio_sm_put_blocking(pio, 0, color_B ); //青
break;
default:
break;
}
if( ++colorcnt >= 3){
colorcnt = 0;
}
}
}
/* タイマ管理 */
void mainTimer(void){
if( to_ms_since_boot(get_absolute_time()) - beforetimCnt > BASE_CNT ){
beforetimCnt = to_ms_since_boot(get_absolute_time());
//10msごとにここに遷移する
if( timled > TIME_UP ){
timled--;
}
if( timcolor > TIME_UP ){
timcolor--;
}
}
}
アセンブリファイルのソースコード:
.program ws2812
.define T0H 8 ;40ns*8=320us
.define T0L 20 ;40ns*20=800ns
.define T1H 20 ;40ns*20=800ns
.define T1L 20 ;40ns*20=800ns
.define H_OFFSET 3 ;3命令文の遅延
.define L_OFFSET 2 ;2命令文の遅延 do制御とjmp命令
set y, 23
pull ; FIFO → OSR
mov x, osr ; OSR → x
loop:
set pins, 1
out x, 1
jmp !x dolow
nop [T1H - H_OFFSET - 1] ;Highの場合のH維持
set pins, 0
nop [T1L - L_OFFSET - 1] ;Highの場合のL
jmp continue
dolow:
nop [T0H- H_OFFSET - 1] ;Lowの場合のH維持
set pins, 0
nop [T0L -L_OFFSET - 1] ;Lowの場合のL
continue:
jmp y--, loop
end:
nop[1] ;処理なしエンド
メインのファイルの内容をコピーして置き換えることで使用できます。
プロジェクトを生成時のCMakeファイルの構成によって動作しない場合があります。CMakeLists.txtの構成で条件が不足している可能性があるため必要に応じて修正してください。
関連リンク
Arduinoのライブラリを使って動作確認を行ったことを下記リンクにまとめています。
Arduinoで学べるマイコンのソフト開発と標準ライブラリの使い方
Seeeduino XIAOで学べるソフト開発と標準ライブラリの使い方
ESP32-WROOM-32Eで学べるソフト開発と標準ライブラリの使い方
PR:RUNTEQ(ランテック )- マイベスト4年連続1位を獲得した実績を持つWebエンジニア養成プログラミングスクール
最後まで、読んでいただきありがとうございました。

点灯する色は各色255段階で切り替えることができるので、任意の色を生成できるようにするなど応用ができます。