PR

Raspberry Pi PicoのPIOとUARTでRGB LEDを操作する

Raspberry Pi Pico
本記事はプロモーションが含まれています。

こんにちは、ENGかぴです。

VSCodeの拡張機能であるPico SDKを使用するとRaspberry Pi PicoにPIOを実装することができます。PIOとUARTを使って電文でRGB LEDの色を任意の色で点灯させる方法をまとめました。

本記事は下記記事のPIOとUARTを組み合わせた応用例です。

Raspberry Pi PicoのPIOでRGB LEDを操作する

Raspberry Pi PicoでRTCを実装する

RTCの時刻を設定するために使用したUARTの電文をRGB LED用に流用して使用します。

今回はGrove Mech Keycap(Seeed Studio:秋月電子で購入)に使用されているRGB LEDを操作します。

Raspberry Pi Pico(以下Picoとする)と拡張基板のGrove Shield for Pi Picoを使用しています。PCとPicoの通信はUSBを介して行うため、AE-CH340E-TYPEC(秋月電子)を使用しています。

Picoを使用してArduino IDEやVSCodeで動作確認したことをまとめています。

Raspberry Pi Picoで学べるソフト開発

RGB LEDを操作する

引用:WS2812Bデータシート Data Transfer Time
引用:WS2812Bデータシート Data Transfer Time

Grove Mech Keycapに使用されているRGB LEDはWS2812Bが使用されています。RGB LEDの操作はRGB LEDはユニポーラゼロ調整コードを使用します。一色あたり8ビットの構成になるため256パターンの調整ができます。

RGB LEDは使用する製品によって色の調整がRGBの順に操作するものやGRBで操作するプロトコルがあります。WS2812BはGRBで操作するプロトコルですが、色の順が異なるだけで基本は同じなのでRGB LEDで統一します。

WS2812Bは緑・赤・青の順に24ビットの値を指定します。Sequence Chartのタイミングに従ってHighの長さとLowの長さによって1ビットずつ1か0を指定して点灯パターンを操作します。

引用:WS2812Bデータシート Composition of 24bit Data
引用:WS2812Bデータシート Composition of 24bit Data

RGB LEDはSequence ChartのタイミングによってHighの長さとLowの長さによって1の指定か0の指定かを判定しながら24ビットのデータを送信して赤・緑・青の色を調整します。

データシートによると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によるウェイトで範囲以内に収まるように制御してタイミング波形を生成します。

RGB LEDを操作するためのアセンブリファイルは下記記事と同様のものを使用します。

Raspberry Pi PicoのPIOでRGB LEDを操作する

PR:わからないを放置せず、あなたにあった最低限のスキルを身に着けるコツを教える テックジム 「書けるが先で、理解が後」を体験しよう!

PIOのAPIを使用する

PIOの設定や初期化など使用方法は下記記事にまとめています。

Raspberry Pi PicoのPIOでGPIOを操作する

ここでは、WS2812Bを操作するために必要な処理を中心に説明します。

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ビットデータを指定します。

PR:RUNTEQ(ランテック )- マイベスト4年連続1位を獲得した実績を持つWebエンジニア養成プログラミングスクール

UARTによる受信の確認

while(uart_is_readable(uart0)){
    u0_rcvdata.buf[u0_rcvdata.wp] = uart_getc(uart0);
  uart_putc(uart0,(char)u0_rcvdata.buf[u0_rcvdata.wp] );// PCから受信したデータを表示

  if( ++u0_rcvdata.wp >= RING_SZ ){
    u0_rcvdata.wp = 0;
  }
}

uart_is_readable()関数でUARTの受信状態を確認します。引数に指定したUARTに受信データが存在する場合はtrueが戻り値になります。

uart_getc()関数で引数に指定したUARTの受信データを読み込みます。戻り値が読み込んで受信データになるので変数に格納(3行目)します。

uart_putc()関数は第1引数に指定したUARTに対して、第2引数で指定したchar型のデータを改行変換ありで送信します。文字の\nを送信すると\nを\r\nに変換して送信します。シリアルモニターに改行して表示する場合に使用します。

電文から色のデータを生成する

RGB LEDを操作する電文の構成
RGB LEDを操作する電文の構成

シリアルモニターを使ってRGB LEDの色のデータ送信する電文を生成します。電文はヘッダーに「GRB」の文字列にG(緑)からB(青)までを「-」(ハイフン)で繋げたものです。

/* 受信データからデータを生成 */
void rcvdatechk(void){
  int8_t  rxsz;
  uint8_t allsz;
  uint8_t rp = u0_rcvdata.rp;  
  uint8_t i;
  uint8_t dat[3];

  if( rxsz >= 3 ){
    for( i = 0; i < 3; i++){//データサイズ算出のため仮おき
      dat[i] = u0_rcvdata.buf[ rp ];
      if(++rp >= sizeof(u0_rcvdata.buf) ) rp = 0;
    }

    if( dat[0] == 'G' && dat[1] == 'R' && dat[2] == 'B'){
      allsz = sizeof(str) - 1;
        
      if( rxsz >= allsz){
        if( chkdata() == false ){
          uart_puts(uart0, "GRBSET NG!\n");
        }
      }
    }
  }
}

受信したデータの先頭から3バイトまでを確認して文字列の「GRB」であればRGB LEDの電文を受信したとみなします。(15行目)

受信データが電文のサイズ以上になったとき、電文が到達したと判断して電文から色のデータに変換する処理(19行目)を行います。換算処理がうまくいかなかった場合は、シリアルモニターに「GRBSET NG!」を送信して失敗を通知します。

/* データのチェック */
bool chkdata(void){
  uint8_t green=0;
  uint8_t red=0;
  uint8_t blue=0;
  bool ret;

  //GRB-255-255-255
  green = strtol((const char*)&Rcvdata[4], NULL, 10);
  red = strtol((const char*)&Rcvdata[8], NULL, 10);
  blue = strtol((const char*)&Rcvdata[12], NULL, 10);

  if( green >= 0x100 || red >= 0x100 || blue >= 0x100 ){
    ret = false;
  }
  else{
    color = (green << 24 ) + (red << 16 ) + (blue << 8 );
    pio_sm_put_blocking(pio, 0, color ); //32bitを24BitをMSBとして指定
    btnon= true;
    ret = true;
  }
              
  return ret;
}

電文から緑、赤、青のデータに換算します。9~11行目で電文の文字列からstrtol()関数を使用して色のデータに変換しています。

第1引数に変換する文字列の先頭アドレスを指定します。第2引数は文字変換が終了した位置を格納するポインタアドレスを指定します。使用しない場合はNULLとします。

第3引数は数値の基数を指定します。10進数なら10、16進数なら16を指定します。

電文は緑~青を-(ハイフン)で区切っているためハイフンまでの文字列を変換した結果が整数値に変換されて各色の変数に格納されます。

変換した色のデータが0~255の範囲以内であることを確認(13行目)し、範囲内であれば緑、赤、青のデータをビットシフトして32ビットのデータを生成(17行目)します。

pio_sm_put_blocking()関数で生成した色データをPIOのステートマシンに送信することで任意の色でRGB LEDを点灯させることができます。

スポンサーリンク

動作確認

動作確認用の回路図
動作確認用の回路図

PicoのUART0をシリアルモニターで使用するためUSBモジュール(AE-CH9102F-TYPEC)を使用しています。PicoはDC3.3VのマイコンなのでUSBモジュールの出力がDC3.3Vになるようにしています。

Picoの電源を入れるとPIOステートマシンが動作開始します。RGB LEDの初期値は緑、赤、青をすべて255にしているので白色になります。

Grove Mach keycapのボタンを押すとRGB LEDの点灯/消灯を切り替えることができます。

シリアルモニターを開いて電文のデータを送信すると任意の色でRGB LEDを操作することができます。

シリアルモニターで電文を送信
シリアルモニターで電文を送信

メッセージ欄に「GRB-255-000-000」のように電文を送信します。複数のパターンで電文を送信して動作確認を行いました。

電文で点灯パターンを操作した結果
電文で点灯パターンを操作した結果

点灯結果を確認すると「255-000-000」の場合は緑が255、赤が0、青が0なので緑色に点灯しています。同様に「000-255-000」では赤色、「000-000-255」では青色になっています。

「000-255-255」は赤と青を光の3原色で合成した紫色になっています。「255-000-255」は緑と青の合成の水色になっています。「255-255-000」は緑と赤の合成の黄色になっています。

電文でRGB LEDを操作できていることが確認できました。

PR:スキマ時間で自己啓発!スマホで学べる人気のオンライン資格講座【スタディング】まずは気になる講座を無料で体験しよう!

ソースコード全体

ソースコードは記事作成時点において動作確認できていますが、使用しているライブラリの更新により動作が保証できなくなる可能性があります。また、ソースコードを使用したことによって生じた不利益などの一切の責任を負いかねます。参考資料としてお使いください。

#include <stdio.h>
#include "pico/stdlib.h"
#include "hardware/pio.h"
#include "ws2812asm.pio.h"
#include <stdlib.h> //strtol()を使用するため

#define RGBLED_PIN 17
#define PIN_DI1 16

#define PIN_DO1 25
#define TIME_OFF -1 //タイマーを使用しない場合
#define TIME_UP 0 //タイムアップ
#define BASE_CNT 10 //ベースタイマカウント値
#define LED_ONOFF 50
#define RING_SZ 128
#define TIM_RX_WAIT 5   //50ms
#define FILT_MIN 1
#define DI_FILT_MAX 4

struct RING_MNG{
  uint8_t wp;
  uint8_t rp;
  uint8_t buf[RING_SZ];
};

struct DIFILT_TYP{
  uint8_t wp;
  uint8_t buf[DI_FILT_MAX];
  uint8_t di1;
};

PIO pio = pio0;
uint32_t color = 0xFFFFFF00; //GRB
RING_MNG u0_rcvdata;
uint32_t beforetimCnt;
int16_t timled = LED_ONOFF;
int16_t timrcv = TIME_OFF;
int16_t timdifilt = TIME_OFF;
char str[] = {"GRB-255-255-255"};
uint8_t Rcvdata[sizeof(str)];
DIFILT_TYP difilt;
bool btnflg1;
bool btnon = false;

void mainApp(void);
void mainTimer(void);
void rcvdatechk(void);
void ReadPointerAdd(void);
bool chkdata(void);
void DiFilter(void);

int main()
{
  stdio_init_all();
  gpio_init(PIN_DO1);     // ピン初期化
  gpio_set_dir(PIN_DO1, GPIO_OUT);    //DOピン
  gpio_init(PIN_DI1);     // ピン初期化
  gpio_set_dir(PIN_DI1, GPIO_IN);    //DOピン
  gpio_pull_up(PIN_DI1);

  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);
  pio_sm_put_blocking(pio, 0, color ); //TX FIFOにデータを送る

  timdifilt = FILT_MIN;
  for( uint8_t i=0; i < 20; i++ ){
    mainTimer();
    DiFilter();
    sleep_ms(100);
  }

  uart_puts(uart0, "Hello, PIO!\n");

  while (true) {
    mainApp();
    mainTimer();
    DiFilter();
  }
}
/* メイン処理 */
void mainApp(void){

  while(uart_is_readable(uart0)){
    u0_rcvdata.buf[u0_rcvdata.wp] = uart_getc(uart0);
    uart_putc(uart0,(char)u0_rcvdata.buf[u0_rcvdata.wp] );// PCから受信したデータを表示

    if( ++u0_rcvdata.wp >= RING_SZ ){
      u0_rcvdata.wp = 0;
    }
  }

  rcvdatechk();

  if(difilt.di1 == 1){
    if(btnflg1){
      btnflg1 = false;
      
      if( btnon ){
        btnon = false;
        pio_sm_put_blocking(pio, 0, 0 ); //32bitを24BitをMSBとして指定
      }
      else{
        pio_sm_put_blocking(pio, 0, color ); //32bitを24BitをMSBとして指定
        btnon = true;
      }
    }
  }else{
    btnflg1 = true;
  }

  if( timled == TIME_UP ){
    timled = LED_ONOFF;
    gpio_put(PIN_DO1, !gpio_get(PIN_DO1));
  }
}

/* タイマ管理 */
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( timrcv > TIME_UP ){
      timrcv--;
    }

    if( timdifilt > TIME_UP ){
      timdifilt--;
    }
  }
}

/* 受信データからデータを生成 */
void rcvdatechk(void){
  int8_t  rxsz;
  uint8_t allsz;
  uint8_t rp = u0_rcvdata.rp;  
  uint8_t i;
  uint8_t dat[3];

  if( timrcv == TIME_UP){
    timrcv = TIME_OFF;
    ReadPointerAdd();    
  }

  rxsz = u0_rcvdata.wp - u0_rcvdata.rp; //受信データ数の算出
    
  if( rxsz < 0 ){
    rxsz = rxsz + sizeof(u0_rcvdata.buf);
  }
            
  if( rxsz == 0 ){
    timrcv = TIME_OFF;
  }
  else{
    if( timrcv == TIME_OFF ){
      timrcv = TIM_RX_WAIT;
    }

    if( rxsz >= 3 ){
      for( i = 0; i < 3; i++){//データサイズ算出のため仮おき
        dat[i] = u0_rcvdata.buf[ rp ];
        if(++rp >= sizeof(u0_rcvdata.buf) ) rp = 0;
      }

      if( dat[0] == 'G' && dat[1] == 'R' && dat[2] == 'B'){
        allsz = sizeof(str) - 1;
        
        if( rxsz >= allsz){
          timrcv = TIME_OFF;
                          
          for(i=0; i < sizeof(Rcvdata); i++){
            Rcvdata[i] = 0;
          }
                          
          for( i=0; i < allsz; i++ ){
            Rcvdata[i] = u0_rcvdata.buf[u0_rcvdata.rp];
            ReadPointerAdd();
          }

          if( chkdata() == false ){
            uart_puts(uart0, "GRBSET NG!\n");
          }
        }
      }
    }
  }
}

/* 読み込み位置の更新 */
void ReadPointerAdd(void){
    
  if(++u0_rcvdata.rp >= sizeof(u0_rcvdata.buf) ){
    u0_rcvdata.rp = 0;
  }
}

/* データのチェック */
bool chkdata(void){
  uint8_t green=0;
  uint8_t red=0;
  uint8_t blue=0;
  bool ret;

  //GRB-255-255-255
  green = strtol((const char*)&Rcvdata[4], NULL, 10);
  red = strtol((const char*)&Rcvdata[8], NULL, 10);
  blue = strtol((const char*)&Rcvdata[12], NULL, 10);

  if( green >= 0x100 || red >= 0x100 || blue >= 0x100 ){
    ret = false;
  }
  else{
    color = (green << 24 ) + (red << 16 ) + (blue << 8 );
    pio_sm_put_blocking(pio, 0, color ); //32bitを24BitをMSBとして指定
    btnon= true;
    ret = true;
  }
              
  return ret;
}

/* DiFilter function add */
void DiFilter(void){

  if( timdifilt == TIME_UP ){
    difilt.buf[difilt.wp] = gpio_get(PIN_DI1);
    
    if( difilt.buf[0] == difilt.buf[1] &&
        difilt.buf[1] == difilt.buf[2] &&
        difilt.buf[2] == difilt.buf[3] ){ //4回一致を確認
          difilt.di1 = difilt.buf[0];
        }
    
    if( ++difilt.wp >= DI_FILT_MAX ){
      difilt.wp = 0;
    }

    timdifilt = FILT_MIN;
  }
}

アセンブリファイルのソースコードは下記記事のものを使用してください。

Raspberry Pi PicoのPIOでRGB LEDを操作する

メインのファイルの内容をコピーして置き換えることで使用できます。

プロジェクトを生成時のCMakeファイルの構成によって動作しない場合があります。CMakeLists.txtの構成で条件が不足している可能性があるため必要に応じて修正してください。

関連リンク

Arduinoのライブラリを使って動作確認を行ったことを下記リンクにまとめています。

Arduinoで学べるマイコンのソフト開発と標準ライブラリの使い方

Seeeduino XIAOで学べるソフト開発と標準ライブラリの使い方

ESP32-WROOM-32Eで学べるソフト開発と標準ライブラリの使い方

Raspberry Pi Picoで学べるソフト開発

PR:企業で求められる即戦力技術を身に付ける テックキャンプエンジニア転職

最後まで、読んでいただきありがとうございました。

タイトルとURLをコピーしました