Seeed XIAO RP2040をArduino-Picoで動かす

2023年8月23日技術開発Arduino,Seeed XIAO RP2040

概要

Raspberry Pi PicoをArduino-Picoで動かすという記事は多く見かけたのですが、Seeed XIAO RP2040を使う記事は少なかったので、調べて色々試した結果を紹介します。
Raspberry Pi PicoとSeeed XIAO RP2040は配線や周辺部品が異なるだけでマイコンは同じものなので、この記事の内容はRaspberry Pi Picoでも応用できると思います。

◆念のためArduino-Picoの説明
Raspberry Pi Picoは公式でArduino IDEに対応しています。ただし、下記のような一部の機能に対応していません。

  • 一部のライブラリ
  • FLASHメモリを疑似EEPROMのように使う
  • 2コアでの並列処理
  • I2C、SPIを複数ポート使ったり、チャンネルを変えて使う

    ※他にもありそうなので随時追加します。

上記の点を改良したArduino Coreを、Earle F. Philhower, III氏が新たに作成して無料で公開されています。
これが「Arduino-Pico」です。私個人としては公式ではなく個人が作成したものだと保守性で問題はないのか…?と感じているのですが、日本語の資料の多くがArduino-Picoを使用した環境のため、様子を見つつ使用しています。
この記事を書いている時点では更新は頻繁にされているようです。

I2CやSPIのポートについては公式のものでも何とかなるケースが多いのですが、2コアでの並列処理が使えるのは大きいですね。

実行環境

MCU: Seeed XIAO RP2040
IDE: Arduino IDE 1.8.19 windows(portable)

参考資料

Arduino-Pico 公式ページ https://arduino-pico.readthedocs.io/en/latest/#
Arduino-Pico Github https://github.com/earlephilhower/arduino-pico

Seeed XIAO RP2040のピンアサイン

Arduino IDEの設定

Raspberry Pi Picoと同じ手順になります。
詳細は参考サイトを参照ください。
※英語が読める方は公式ページ、もしくはGithubを参考にするのが確実です。日本語で読みたい方は↓こちらの「Earle Philhowerコアのインストール」が説明が丁寧です。

Seeed XIAO RP2040をPCに接続する

初回はBOOTSELボタン(下図の赤枠部のボタン)を押したままUSBケーブルでPCに接続します。
この方法で接続すると大容量記憶デバイス(USBメモリ等と同じ扱い)として認識されます。

スイッチサイエンス・購入画面から引用

Arduino IDEを起動し、ツール → ボード → Raspberry Pi RP2040 Boards(X.X.X) → Seeed XIAO RP2040 をクリックしてボードを設定します。
Flash Sizeなどの設定項目が増えますが、とりあえず動かすだけならデフォルトのままでOKです。
後ほど必要に応じて変更します。

Raspberry PiやXIAO RP2040などRP2040搭載ボードは普通のArduinoと違い、PCと接続してもシリアルポートは認識されません。
普通のArduinoはシリアル通信でプログラムを書き込むのでシリアルポートの設定が必要ですが、RP2040は大容量記憶メモリとして認識され、USBメモリのように.uf2ファイルを保存する形でプログラムが書き込めれるためです。
おそらくですが、「RPI-RP2」「RP2-Boot」という名前のデバイスを自動認識して書き込むようです。(詳しくは確認中)

※参考画像ではシリアル通信でデバッグ情報が見れるよう設定変更しています。元に戻すのが面倒で…

ここまで出来たら通常のArduinoと同じく、コードを書いて「マイコンボードに書き込む」ボタンを押せばプログラムが書き込めます。
検証も書き込みもかなり時間がかかるので気長に待ちましょう。
↓このようなメッセージが出てきたら書き込み完了です。(コードやPCの環境によってメッセージが少し変わります)

Resetting COM7
Converting to uf2, output size: 116736, start address: 0x2000
Flashing D: (RPI-RP2)
Wrote 116736 bytes to D:/NEW.UF2

2回目以降の書き込みは「BOOTSELボタンを押しながらPCとUSBに接続」する必要はありません。
ただし、たまに書き込みが上手くいかない時があるようで、その時は初回の書き込みと同じ手順が必要です。

Lチカ

ファイル → スケッチ例 → 01.Basics → Blink の順にクリックしてLチカ用のサンプルコードを開きます。

これを一切変更せずに書き込むと、USBコネクタ横の赤LEDが点滅します。

NeoPixel

Seed XIAO RP2040にはNeoPixelが実装されています。
引き出されているピンとは干渉しないので、いつでも使用できます。

以下、1秒ごとに赤 → 緑 → 青 → 白と切り替わるプログラムとなります。
setPixelColorでRGB値で色を指定し、showで反映します。
消灯する際はclear()を使います。

#include <Adafruit_NeoPixel.h>

const int NUMPIXELS = 1;
const int NEO_PWR = 11;
const int NEOPIX = 12;

Adafruit_NeoPixel pixels(NUMPIXELS, NEOPIX, NEO_GRB + NEO_KHZ800);

void setup() {
  pixels.begin();
  pinMode(NEO_PWR, OUTPUT);
  digitalWrite(NEO_PWR, HIGH);
}

void loop() { //core 0
  /* 赤色で点灯 */
  pixels.setPixelColor(0, pixels.Color(255, 0, 0));
  pixels.show();
  
  delay(200);
  
  /* 消灯 */
  pixels.clear();
  pixels.show();
  
  delay(1000);

  /* 緑色で点灯 */
  pixels.setPixelColor(0, pixels.Color(0, 255, 0));
  pixels.show();
  
  delay(200);

  /* 消灯 */
  pixels.clear();
  pixels.show();
  
  delay(1000);

  /* 青色で点灯 */
  pixels.setPixelColor(0, pixels.Color(0, 0, 255));
  pixels.show();
  
  delay(200);
  
  /* 消灯 */
  pixels.clear();
  pixels.show();
  
  delay(1000);

  /* 白色で点灯 */
  pixels.setPixelColor(0, pixels.Color(85, 85, 85));
  pixels.show();
  
  delay(200);
  
  /* 消灯 */
  pixels.clear();
  pixels.show();
  
  delay(1000);
}

GPIO

使い方は普通のArduinoと全く同じです。ただし、ピン番号はD0、D1のように「D」をつける必要があります。

次に、Arduinoで運用すると問題になるdigitalWrite()の処理速度を測定しました。

digitalWrite(D0, HIGH);
digitalWrite(D0, LOW);
ロジックアナライザでの測定結果

2回実行した時の処理時間が708.333[ns]なので、1回当たりの処理時間は約354.2[ns]となります。
安いマイコンと同レベルの結果が出ているので十分実用できますね。

ADC (アナログ入力)

通常のArduinoと同じ標準関数で実行可能で、すべてのピンが対応しています。

またADCの出力レンジを設定する関数が用意されています。RP2040のADCの分解能は12bitなので、4096段階まで引き上げることが出来ます。

以下、D0の電圧をシリアル通信で出力するコードです。

#define VOLT_PIN D0

void setup() {
  /* シリアル通信の設定 */
  Serial.begin(115200);
  /* ADCの出力レンジを設定*/
  analogReadResolution(12);
  /* ピンをインプットに設定 */
  pinMode(VOLT_PIN, INPUT);
}

void loop() {
  /* 電圧を測定 */
  int volt = analogRead(VOLT_PIN);
  /* 測定した値をシリアル通信で出力 */
  Serial.println(volt);
  
  delay(500);
}

PWM (アナログ出力)

通常のArduinoと同じ標準関数で実行可能で、すべてのピンが対応しています。
またPWM周波数やレンジを設定する関数が追加されているので状況に応じて変更が可能です。

以下、D0からDuty25%・周波数25kHzのPWM信号を出力するコードです。

#define PWM_PIN D0

void setup() {
  /* ピンモードを設定 */
  pinMode(PWM_PIN, OUTPUT);
  /* PWMの周波数を設定 */
  analogWriteFreq(25000);
  /* Duty100%の値を設定 */
  analogWriteRange(4096);
  /* Duty50%のPWM信号を出力 */
  analogWrite(PWM_PIN, 1024);
}

void loop() {

}

パルス幅・周波数の測定

通常のArduinoと同じ標準関数で実行可能です。

以下、GPIO28に入力された信号の周波数を0.5秒ごとに出力するコードです。
パルス幅の測定結果はマイクロ秒で出力されること、1周期の半分の値が出力されることを考慮して周波数に換算します。

const int TACHO_PIN = 28;

void setup() {
  /* シリアル通信の設定 */
  Serial.begin(115200);
  /* ピンをインプットに設定 */
  pinMode(TACHO_PIN, INPUT);
}

void loop() {
  /* パルス幅(HIGHの時間)を測定 */
  int pulse_width = pulseIn(TACHO_PIN, HIGH);
  /* パルス幅を周波数に換算する */
  float freq = (1000000.0 / ((float)(pulse_width) * 2.0));
  /* 測定した値をシリアル通信で出力 */
  Serial.println(freq);

  delay(500);
}

動作確認のため、オシロスコープのテストピンの1kHzの信号を入力してみました。

外部割込み

全てのピンで外部割込みが使用可能です。
使用方法も通常のArduinoシリーズと同じです。

以下、D9とGNDが導通したら赤色LEDが点灯するプログラムです。
※個人的に割込みの中で色々処理をするのは好きじゃないので、割込み内ではフラグを立てる処理のみにしています。

bool interrupt_flag = false;

void setup() {
  pinMode(D9,INPUT_PULLUP);
  pinMode(LED_BUILTIN, OUTPUT);

  attachInterrupt(D9, Interrupt, FALLING);
}

void Interrupt(){
  interrupt_flag = true;
}

void loop() {
  if(interrupt_flag){
    interrupt_flag = false;
    digitalWrite(LED_BUILTIN, LOW);
    delay(200);
  }else{
    digitalWrite(LED_BUILTIN, HIGH);
  }
}

タイマー割込み

通常のArduinoで使用するTimerone等は使えませんでした。
その代わりに別の関数が用意されています。
※Timerの何番を使用しているかは確認中です。

以下、1秒(1,000,000[us])ごとにLEDのON/OFFを切り替えるプログラムです。

struct repeating_timer st_timer;
bool timerFlag = false;
int a;

/* タイマー割り込み処理 */
bool Timer(struct repeating_timer *t) {
  timerFlag = true;
  return true;
}

void setup() {
  pinMode(LED_BUILTIN, OUTPUT);

  /* タイマーの初期化(割込み間隔はusで指定) */
  add_repeating_timer_us(1000000, Timer, NULL, &st_timer);
}

void loop() {
  /* timerFlagがtrueになる度にLEDをトグルする */
  if (timerFlag == true) {
    if (a == 0) {
      digitalWrite(LED_BUILTIN, HIGH);
    } else {
      digitalWrite(LED_BUILTIN, LOW);
    }
    a = 1 - a;
    timerFlag = false;
  }
}

シリアル通信(UART)

PCとの通信

シリアル通信は特にボードの設定変更なく使えます。

当方の環境では書き込みか何かが上手くいかなかったのか、最初はシリアルポートが認識されませんでした。
理由はわかりませんが、Debug Portを「Disable」から「Serial」に、Debug Levelを「なし」から「全て」に変更して書き込むとPC側に認識されるようになりました。
以降はデフォルト設定に戻しても普通にシリアルポートが認識されています。

ファイル → 01.Basics → DigitalReadSerial の順にクリックしてスケッチ例を開き、変更せずに書き込みます。

GPIOの電圧をHIGHなのかLOWなのか判断してシリアル出力するサンプルプログラムです。
デフォルトでは、2番ピン(D8)に3.3Vをつなぐと1、GNDをつなぐと0が出力されます。

PC以外との通信

※2022/11/20追記

rp2040のUARTは2チャンネルあり、PCと通信しつつ他の回路とシリアル通信をすることが出来ます。
Seeed XIAO RP2040ではUART0がピンアサインされています。

ピン名称Seeed XIAO RP2040
ピンアサイン
TXD6(GPIO0)
RXD7(GPIO1)

実際のプログラムでは、PC通信での「Serial」を「Serial1」に変更するだけです。
※なぜかUART0がSerial1に、UART1がSerial2に割り当てられています。Serial0じゃだめだったのか…?

以下、DigitalReadSerialを書き換えてみました。動作確認にはUSB-シリアル変換ケーブル等、シリアル通信を受信できる回路が必要です。

int pushButton = 2;

void setup() {
  Serial1.begin(9600);
  pinMode(pushButton, INPUT);
}

void loop() {
  int buttonState = digitalRead(pushButton);
  Serial1.println(buttonState);
  delay(1);
}

I2C通信

Seeed XIAO RP2040のI2C通信は、初期化がやや特殊です。
RP2040はI2Cが2チャンネルあり、割り当てピンはいくつかの選択肢から選ぶことが出来ます。しかし、デフォルトの割り当てピンとSeeed XIAO RP2040のI2Cピンが一致していないため、必ずWire.begin()の前にピン設定が必要になります。

ピン名称デフォルト割り当てSeeed XIAO RP2040
ピンアサイン
SDAGPIO4(I2C0)GPIO6(I2C1)
SCLGPIO5(I2C0)GPIO7(I2C1)

↓こちらのブログでSeeed XIAO RP2040を使ったI2Cscannerのコードを紹介されているので、こちらを参考にさせていただきました。

https://aloseed.com/it/xiao_rp2040/#toc18

ピン設定で「SDA」「SCL」という文字列を代入していますが、これはSeeed XIAO RP2040のボード情報の部分でそれぞれ「6」「7」と定義されています。
また当方の環境では「Wire1」を使用したプログラムを書き込むと、PCに認識されなくなってしまいました。(BOOTSELボタンで起動して別のプログラムを書き込めば直ります)
以下、動作確認出来たプログラムです。

#include <Wire.h>

#define XIAO_RP2040_NEW_BOARD (defined(ARDUINO_SEEED_XIAO_RP2040) || (defined(ARDUINO_SEEED_XAIO_RP2040) && defined(__WIRE0_DEVICE)))

#if XIAO_RP2040_NEW_BOARD
#define WIRE Wire
#else
#define WIRE Wire1
#endif

void setup()
{
#if !XIAO_RP2040_NEW_BOARD
  WIRE.setSDA(SDA);
  WIRE.setSCL(SCL);
#endif
  WIRE.begin();
 
  Serial.begin(115200);
  while (!Serial);             // Leonardo: wait for serial monitor
  Serial.println("\nI2C Scanner");
}
 
 
void loop()
{
  byte error, address;
  int nDevices;
 
  Serial.println("Scanning...");
 
  nDevices = 0;
  for(address = 1; address < 127; address++ ) 
  {
    // The i2c_scanner uses the return value of
    // the Write.endTransmisstion to see if
    // a device did acknowledge to the address.
    WIRE.beginTransmission(address);
    error = WIRE.endTransmission();
 
    if (error == 0)
    {
      Serial.print("I2C device found at address 0x");
      if (address<16) 
        Serial.print("0");
      Serial.print(address,HEX);
      Serial.println("  !");
 
      nDevices++;
    }
    else if (error==4) 
    {
      Serial.print("Unknown error at address 0x");
      if (address<16) 
        Serial.print("0");
      Serial.println(address,HEX);
    }    
  }
  if (nDevices == 0)
    Serial.println("No I2C devices found\n");
  else
    Serial.println("done\n");
 
  delay(5000);           // wait 5 seconds for next scan
}