SERCOM機能でGPIOが足りない話①

=今ｺｺ= :Arduino IDEでSERCOMを使ったプログラムを書く
\>>[次回](https://qiita.com/conel_fg/items/21badc450e9c66ff25b6) :マイコンのGPIOの名前・SERCOMとGPIOの選び方
\>>[次々回](https://qiita.com/conel_fg/items/c45c156939a41ff6f03c) :ATSAMD2xとATSAMD5xの違いについて

## この記事の対象
* プロトタイピングに慣れてきたひと
* いっぱいセンサを使うデバイスを作りたいひと
* Arduinoを活用したハードウェア開発に興味があるひと

## 概要
* AVR系のマイコンボード(Arduino UNOなど)で余りまくっていたGPIOについて，SAM系マイコンならシリアル通信に再定義して有効活用できるぞっていう話
* ArduinoやAdafruitの公式リファレンス以外の情報が少なかったので記事にしました．
* **今回はArduino IDEでSERCOMを使ったプログラムを書くことについて取り扱います．**

## はじめに 
Arduino系のマイコンボード使ってるとI2C通信でセンサをたくさん繋ぎたくなりますね．
でもI2C通信はノイズに弱すぎて実際はそんなにたくさん繋げない...

SERCOM機能はそういうときに役に立つ機能です．

## SERCOMとは
SERCOMはざっくり言うと，既定のGPIOを再定義して**シリアル通信ポートを擬似的に増やせる**機能です．
SERCOMはSAM系マイコンでのみ使用可能です( Adafruit Metro M0 Express とか)．

現在，各種センサの公開ライブラリがSERCOMに対応していない場合があるので注意(2021年4月時点)．
このような場合はセンサのデータシートを見てコマンド通りに通信するなどで対応可能です．

**今回は基本的に「Adafruit Metro M0 Express」というボードの使用を前提として説明します．**
次回以降を読めばSAM系マイコンを搭載した他のボードでもSERCOMを使用できるはずです．
Adafruitの[チュートリアル](https://learn.adafruit.com/using-atsamd21-sercom-to-add-more-spi-i2c-serial-ports/overview)にある程度プログラミング方法が載っているので，ある程度英語が読めるひとは参照してください．

### UART
以下はUART通信の例です．
Arduino UNOでSoftwareSerialライブラリを使用する場合に似ています．
注意すべき点は，pinPeripheralメソッドはmySerial.beginメソッドの後に実行する必要があることです．
SERCOM_ALTについては，[次回](https://qiita.com/conel_fg/items/21badc450e9c66ff25b6)で説明します．

```C++
#include <Arduino.h>        //必須
#include "wiring_private.h" //必須

#define TX 4
#define RX 3

Uart mySerial(&sercom2, RX, TX, SERCOM_RX_PAD_1, UART_TX_PAD_0); // シリアルポート定義(TX:4, RX:3)
int i = 0;

void setup() {
  mySerial.begin(115200);
  
  pinPeripheral(TX, PIO_SERCOM_ALT); // とりあえず書いておけばOK (D4ピンをSERCOM_ALTとして使用するよという宣言)
  pinPeripheral(RX, PIO_SERCOM_ALT); // とりあえず書いておけばOK (D3ピンをSERCOM_ALTとして使用するよという宣言)
  
}

void loop() {
  /****処理内容****/
}

void SERCOM2_Handler() //とりあえず書いておけばOK (シリアルハンドラ関数)
{
  mySerial.IrqHandler();
}
```

処理内容では，SoftwareSerialライブラリでもおなじみの以下のようなメソッドが使えます．(以下例)

```C++
mySerial.available(); //シリアルポートのバッファにあるデータのバイト数を返す．
mySerial.read();      //バッファのデータを読み出す．
mySerial.write(x);    //シリアルポートからデータx(xはバイト単位orバイト列)を送信する
```

### I2C

以下はI2C通信の例です．
I2Cを用いたプログラミングではセンサを使用する場合が多いので，ここでは[加速度ジャイロセンサ「ICM20948」](https://www.switch-science.com/catalog/5854/)を用いた例を示します．

```C++
#include <Arduino.h>        //必須
#include "wiring_private.h" //必須
#include <Wire.h>           //必須
#include "ICM_20948.h"      //Sparkfun 9DoF IMU Breakout - ICM 20948 - Arduino Library

#define mySDA 11
#define mySCL 13

#define AD0_VAL 1 //I2Cアドレスの末尾のbitを表す．(defaultは1．基板上のジャンパを短絡させると0になる．)

TwoWire myWire(&sercom1, mySDA, mySCL); //11:SDA 13:SCL
ICM_20948_I2C myICM;              //ICM_20948_I2Cクラス

void setup() {
  Serial.begin(115200); //2021/05/16 書き忘れていたので追加しました．
  myWire.begin();       //センサの親機としてI2C通信するので引数無し
  delay(200);

  pinPeripheral(mySDA, PIO_SERCOM); // とりあえず書いておけばOK (D11ピンをSERCOMとして使用するよという宣言)
  pinPeripheral(mySCL, PIO_SERCOM); // とりあえず書いておけばOK (D13ピンをSERCOMとして使用するよという宣言)

  myICM.begin(myWire, AD0_VAL);  //センサ起動&SERCOMのポートを引数に渡す．
  if( myICM.status != ICM_20948_Stat_Ok ){ //センサ起動待ち
    Serial.println( "Trying again..." );
    delay(500);
  }
}

void loop() {
  if( myICM.dataReady() ){ //加速度・ジャイロの値を更新
    myICM.getAGMT();
  }
  //加速度
  float accx = myICM.accX();
  float accy = myICM.accY();
  float accz = myICM.accZ();

  //ジャイロ
  float gyrx = myICM.gyrX();
  float gyry = myICM.gyrY();
  float gyrz = myICM.gyrZ();

  //地磁気
  float magx = myICM.magX();
  float magy = myICM.magY();
  float magz = myICM.magZ();

  delay(1);
}

extern "C" { // とりあえず書いておけばOK
  void SERCOM1_Handler(void);

  void SERCOM1_Handler(void) {
    myWire.onService();
  }
}

```

上記コードにおいて，以下のように，何らかの方法でセンサとやりとりするポートを設定する手段がある場合，使用しているライブラリはSERCOMに対応していると言えます．

```C++
myICM.begin(myWire, AD0_VAL);  //センサ起動&SERCOMのポートを引数に渡す．
```

### SPI
~~SPI通信については筆者がSERCOMを用いたプログラムを書いた経験がないので省略します．今後機会があれば，追記するかもしれません．~~
(2021/05/16追記)たまたま最近SERCOMでSPI機能を使う機会があったので追記しました．以下のコードは**「Adafruit Metro M4 Express」でのみ動作確認しています．**

以下はMarzoghという方が公開されている[SPIMemory](https://github.com/Marzogh/SPIMemory)というライブラリ中のスケッチ例「readwriteString」をSERCOMで使えるように加筆したものです(上記ライブラリは元々SERCOMで動作できるようになっていてとても助かりました)．プログラムの内容としては，NORフラッシュメモリに対して，文字列を書き込み・読み出しを行うものでした．「追加」とコメントしてある行のみ新規に追加したものです．

```C++
#include <SPI.h>
#include "wiring_private.h" //追加
#include <SPIMemory.h> //https://github.com/Marzogh/SPIMemory

uint32_t strAddr;

SPIClass mySPI(&sercom1, 10, 12, 13, SPI_PAD_0_SCK_1, SERCOM_RX_PAD_2); //追加(MISO:10, SCK:12, MOSI:13)

#if defined(ARDUINO_SAMD_ZERO) && defined(SERIAL_PORT_USBVIRTUAL)
// Required for Serial on Zero based boards
#define Serial SERIAL_PORT_USBVIRTUAL
#endif

#define CAPACITY MB(64) //追加
#define ARDUINO_SAMD_ZERO //追加

#if defined (SIMBLEE)
#define BAUD_RATE 250000
#define RANDPIN 1
#else
#define BAUD_RATE 115200
#define RANDPIN A0
#endif

SPIFlash flash(11, &mySPI);       //Use this constructor if using an SPI bus other than the default SPI. Only works with chips with more than one hardware SPI bus
//SPIFlash flash; //この行をコメントアウトし，上の行をコメントアウトから外す

bool readSerialStr(String &inputStr);

void setup() {
  Serial.begin(BAUD_RATE);

  mySPI.begin(); //追加

  pinPeripheral(10, PIO_SERCOM);//追加
  pinPeripheral(12, PIO_SERCOM);//追加
  pinPeripheral(13, PIO_SERCOM);//追加

#if defined (ARDUINO_SAMD_ZERO) || (__AVR_ATmega32U4__)
  while (!Serial) ; // Wait for Serial monitor to open
#endif
  flash.setClock(2000000); //追加
  flash.begin(CAPACITY);   //引数追加

  randomSeed(analogRead(RANDPIN));
  strAddr = random(0, flash.getCapacity());
  String inputString = "This is a test String";
  flash.writeStr(strAddr, inputString);
  Serial.print(F("Written string: "));
  Serial.println(inputString);
  Serial.print(F("To address: "));
  Serial.println(strAddr);
  String outputString = "";
  if (flash.readStr(strAddr, outputString)) {
    Serial.print(F("Read string: "));
    Serial.println(outputString);
    Serial.print(F("From address: "));
    Serial.println(strAddr);
  }
  while (!flash.eraseSector(strAddr));
}

void loop() {

}

//Reads a string from Serial
bool readSerialStr(String &inputStr) {
  if (!Serial)
    Serial.begin(115200);
  while (Serial.available()) {
    inputStr = Serial.readStringUntil('\n');
    Serial.println(inputStr);
    return true;
  }
  return false;
}
```
なお，SPI通信ではシリアルハンドラ関数は必要ありません(理由はよくわかりませんが...)．


## 今回のまとめ
今回はSAM系マイコンを搭載したArduinoボードでSERCOM機能を使用するプログラムを紹介しました．
I2CやSPIのシリアルポートを任意に増やせるのはArduinoの可能性を感じます．

SERCOM機能の具体的な使い道としては以下のようなものが考えられます．

* センサのアドレスが同一である場合
* センサ同士の相性が悪い時
* 多くのセンサをデイジーチェーンしたいとき
* マイコン同士の通信を含め，Arduinoを使った複雑なデバイスを作りたいとき

Arduinoボードの回路図はオープンソースなので，自作のPCBを作りたいひとにはSERCOM機能はとても便利な機能です．

ただし，元々ArduinoはAVRをメインで使用しているため，どうしてもSERCOMに対応していないものが存在します．
なので，センサなどのデータシートを読んでライブラリを自作する能力を身につけると，より自由なデバイス開発ができると思います．