hoboNicolaライブラリ UHSライブラリの修正

ISP1807Micro + miniUHS

概要

hoboNicolaライブラリ1.6.1では、AVR(ATmega32U4)のほかに、SAMD21とISP1807(nRF52840)を使ったマイコンボードに対応した (USBアダプター版)。対応にあたって、マイコン自体やマイコンボードごとの相違をライブラリ内で切り分けて吸収し、全体として各ボードを同じように使えるようにした。

中身を忘れないうちに書いておこうということで、前回の投稿で省電流について、今回はmini USB Host Shield (以下miniUHS) = MAX3421Eとのインタフェースについてまとめた。

miniUHSとのインタフェース

接続、配線について

SparkFun Pro Micro との接続については、最初に作ったときの https://okiraku-camera.tokyo/blog/?p=8092  および、現在の2階建て構成の https://okiraku-camera.tokyo/blog/?p=8193 を参照。

ISP1807(nRF52840)を使ったPro Microピン互換のスイッチサイエンス製ISP1807Microボード については、Pro Micro用に作った2階建てアダプターの2階部分(マイコンボード)としてISP1807 MBをそのまま接続する。 https://okiraku-camera.tokyo/blog/?p=13128 を参照。
リンク先の投稿では、オリジナルの USB Host Shield Library 2.0 (以下、UHSライブラリ)でISP1807MBを扱うための修正方法も記載してある(以下の内容と重複する部分も多いが)。

SAMD21 (Seeeduino XIAO-m0とAdafruit QT Py SAMD21) との接続については、Pro Microの代わりにこれらを差せるように配線した基板を作った https://okiraku-camera.tokyo/blog/?p=14346 を参照。

UHSライブラリの改造

hoboNicolaライブラリ 1.6.1 からは、UHSライブラリのクラス名や構造体名などを若干変更した都合もあって、UHSライブラリ自体をhoboNicolaのソースツリーに包含するようにした(そのあたりの理由については、前回の投稿を参照)。以下、hoboNicolaライブラリの src/UHSLib2.0 内 のいくつかのファイルに対して行った修正や検討などについて。

ボードごとの相違の吸収

UHSライブラリでは、SPI通信に関係するポート操作の方法を(UHSライブラリ内で)一般化するため、avrpins.h内でマイコンやボードごとにポートやポート操作方法を定義している。そしてusbhost.hUsbCore.hで、avrpins.hでの定義を用いてSPI転送に特化したピン(SSやINTなど)の操作方法をテンプレートを使って定義している。

avrpins.h

nRF52(ISP1807MB)用の定義

以前の投稿と同様に、nrf_gpio_pin_xxxx() を使ってGPIOポートの操作を行っており、Pro Microと同じ位置のピンを使ってminiUHSとインタフェースしている。

#elif defined(ARDUINO_SSCI_ISP1807_MICRO_BOARD)	// ISP1807 MB
#define MAKE_PIN(className, pin) \
class className { \
public: \
    static void Set() { \
        nrf_gpio_pin_set(pin); \
    } \
    static void Clear() { \
        nrf_gpio_pin_clear(pin); \
    } \
    static void SetDirRead() { \
        nrf_gpio_cfg_input(pin, NRF_GPIO_PIN_NOPULL); \
    } \
    static void SetDirWrite() { \
        nrf_gpio_cfg_output(pin); \
    } \
    static uint8_t IsSet() { \
        return (uint8_t)nrf_gpio_pin_read(pin); \
    } \
};
MAKE_PIN(P15, (6)); // SCK 
MAKE_PIN(P14, (8)); // MISO 
MAKE_PIN(P9, (26));	// INT(D9)
MAKE_PIN(P10, (17)); // SS(D10)
MAKE_PIN(P16, (10)); // MOSI 
#undef MAKE_PIN
#elif ...
SAMD21用

SAMD21用については、QT Py SAMD21と XIAO-m0とでGPIOポートが異なるのでおのおの定義している。ポート操作のために用いる方法は一緒。

#elif defined(ADAFRUIT_QTPY_M0) || defined(SEEED_XIAO_M0)
#define MAKE_PIN(className, port, pin) \
class className { \
public: \
  static void Set() { \
    PORT->Group[port].OUTSET.reg = (1ul << pin); \
  } \
  static void Clear() { \
    PORT->Group[port].OUTCLR.reg = (1ul << pin); \
  } \
  static void SetDirRead() { \
    PORT->Group[port].PINCFG[pin].reg=(uint8_t)(PORT_PINCFG_INEN) ; \
    PORT->Group[port].DIRCLR.reg = (1ul << pin); \
  } \
  static void SetDirWrite() { \
    PORT->Group[port].PINCFG[pin].reg=(uint8_t)(PORT_PINCFG_INEN) ; \
    PORT->Group[port].DIRSET.reg = (1ul << pin); \
  } \
  static uint8_t IsSet() { \
    return PORT->Group[port].IN.reg & (1ul << pin); \
  } \
};
// Pxx の数字はisp1807に合わせた。
#if defined(ADAFRUIT_QTPY_M0)
 MAKE_PIN(P15, 0, 11); // SCK(SCK / D8 / PA11)
 MAKE_PIN(P14, 0, 9); // MISO(MI / D9 / PA09)
 MAKE_PIN(P16, 0, 10); // MOSI(MO / D10 / PA10)
 MAKE_PIN(P9,  0, 17); // INT (SCL / D5 / PA17)
 MAKE_PIN(P10, 0, 16); // SS (SDA / D4 / PA16)
#elif defined(SEEED_XIAO_M0)
 MAKE_PIN(P15, 0, 7); // SCK(SCK / D8 / PA07)
 MAKE_PIN(P14, 0, 5); // MISO(MI / D9 / PA05)
 MAKE_PIN(P16, 0, 6); // MOSI(MO / D10 / PA06)
 MAKE_PIN(P9,  0, 9); // INT (SCL / D5 / PA09)
 MAKE_PIN(P10, 0, 8); // SS (SDA / D4 / PA08)
#endif
#undef MAKE_PIN
#else ...

(1.6.1版に含まれるソースと異なるが、コメントの誤りなどを修正したこちらが正しい)。

SAMD21を使った別のボードに対応するならば、上記コードにある5行のMAKE_PIN(); をボードに合わせて追加していけばいいだろう。現在の配線では、PORT->Group[port] のportはすべて0 (PA)なので省略可能なのだが、port == 1 (PB)を使う場合を考慮してこのようにした。

別の書き方として、Arduino流のポートピン番号とdigitalWrite()digitalRead() を使うこともできる。この方が読みやすくなるし汎用性も高くなる。

#elif defined(ADAFRUIT_QTPY_M0) || defined(SEEED_XIAO_M0)
#define MAKE_PIN(className, pin) \
class className { \
public: \
  static void Set() {   digitalWrite(pin, 1); } \
  static void Clear() { digitalWrite(pin, 0); } \
  static void SetDirRead()  { pinMode(pin, INPUT); } \
  static void SetDirWrite() { pinMode(pin, OUTPUT); } \
  static uint8_t IsSet() { return digitalRead(pin); } \
};
MAKE_PIN(P15, 8); // SCK(SCK / D8)
MAKE_PIN(P14, 9); // MISO(MI / D9)
MAKE_PIN(P16, 10); // MOSI(MO / D10)
MAKE_PIN(P9,  5); // INT (SCL / D5)
MAKE_PIN(P10, 4); // SS (SDA / D4)
#undef MAKE_PIN
#else

SAMDのI/Oピンコントローラ(PORT) を操作する場合と、Arduinoの標準関数を使う場合の速度の相違については後述している。

usbhost.h

usbhost.hでは以下のようにISP1807MBとSAMD21ボード用の定義を記述した。

#elif defined(ARDUINO_SSCI_ISP1807_MICRO_BOARD) 
typedef SPi< P15, P16, P14, P10 > spi;
#elif defined(ADAFRUIT_QTPY_M0) || defined(SEEED_XIAO_M0)
typedef SPi< P15, P16, P14, P10 > spi;
#else
...

avrpins,hで、Pxx クラスを定義するとき、SAMD21用の各ピンがISP1807MBと同じ名前になるようにしているので、上記のように同じ内容で問題ない。テンプレート SPiは以下のように定義されている。

template< typename SPI_CLK, typename SPI_MOSI, typename SPI_MISO, typename SPI_SS > class SPi { ... };

hoboNicolaライブラリはマイコンのハードウェアSPI を使ったSPIライブラリに依存している (SPI_HAS_TRANSACTION がdefineされている) ので、結局のところSPI_SS (Slave Select操作用クラス)しか利用されない。

UsbCore.h

UsbCore.hでは、テンプレート MAX3421eのパラメータとしてSSピンとINTピンの操作クラス名を与えたボードごとの MAX3421E を定義している。MAX3421eテンプレートは usbhost.hで宣言されていて、MAX3421EはUsbCore.h で定義されているのがなんとも分かりづらいのだが、もともとそうなっているのでしょうがない。

hoboNicola用に追加したボードについては、以下の標準Arduinoボード用の定義と同じになるように、avrpins.hでP10とP9を定義している。

...
#else
typedef MAX3421e<P10, P9> MAX3421E; // Official Arduinos (UNO, Duemilanove, Mega, 2560, Leonardo, Due etc.), Intel Edison, Intel Galileo 2 or Teensy 2.0 and 3.x
#endif

したがって特に追加を修正の必要はなかった。

SPIクロックについて(usbhost.h)

UHSライブラリによるSPIクロック設定に問題があったので、修正を入れた。

オリジナルライブラリの動作

UHSライブラリでは、MAX3421EとのSPI通信の速度(クロック)として、MAX3421Eのスペック上の最大値(26MHz)を使うようになっていて、以下のようなコードでSPIトランザクションの初期化を行っている。

USB_SPI.beginTransaction(SPISettings(26000000, MSBFIRST, SPI_MODE0)); // The MAX3421E can handle up to 26MHz, use MSB First and SPI mode 0

各マイコン用のSPIライブラリ内に定義されている SPISettings クラスでは、マイコンの種類や動作クロックなどに応じて、26MHz以下の、なるべく高速で動作可能なクロック値を採用するようになっている前提なのだが、hoboNicolaで対応した各マイコンボードでは以下の値になる。

ボード コアクロック SPIクロック
修正前
SPIクロック
修正後
Pro Micro (8MHz) 8MHz 4MHz 4MHz
ATmega32U4(16MHz) 16MHz 8MHz 8MHz
QT Py SAMD21
XIAO-m0
48MHz 24MHz (注1) 12MHz
ISP1807 MB 64MHz 32MHz (注2) 16MHz
  1. Adafruit のSAMD Boards BSP (1.3.9)を使うと24MHz、Arduino SAMD21 BSP(1.8.13)では12MHzとなる。24MHzはSAMD21のSPIクロックのスペックを超えているが一応動作した。
  2. MAX3421Eのスペックを超える値になるが一応動作した。32MHzになってしまうのは、Adafruit nRF52のSPIライブラリ(SPI.cpp)の、void SPIClass::setClockDivider(uint32_t div) の問題というより、UHSライブラリ側の配慮のなさによるものだろう。
SPIクロックについての修正

SAMD21とISP1807(nRF52)のSPIクロックがスペックを外れているのは困るので、UHSライブラリ側で対応した。src/UHSLib2.0/usbhost.h 内に以下のような定義を追加した。

#if defined (__SAMD21G18A__) || defined(__SAMD21E18A__)
	#define SPI_CLOCK 12000000
#elif defined(ARDUINO_NRF52_ADAFRUIT)
	#define SPI_CLOCK 16000000
#else
	#define SPI_CLOCK 26000000
#endif

そして、トランザクション部分もSPI_CLOCK  を使うように変更。このようにすることで、ここで定義したクロックで通信が行われることを確認した。

#if defined(SPI_HAS_TRANSACTION)
  USB_SPI.beginTransaction(SPISettings(SPI_CLOCK, MSBFIRST, SPI_MODE0)); // The MAX3421E can handle up to 26MHz, use MSB First and SPI mode 0
#endif

今回利用した AVR, SAMD21, nRF52の各SPIライブラリ (SPI.H)は、#define SPI_HAS_TRANSACTION 1   を含んでいる。

SAMD21 用SPIのクロックのスペックと扱いについて

hoboNicolaのSAMD21対応では、Adafruit SAMD Boards BSPに含まれているSPIライブラリを利用することになる。Adafruitのライブラリは Arduino Zero用の古めのSPIライブラリを流用している。オリジナルの ArduinoCore-samdは、以下のように「保守的に」上限12MHzに修正されている(SPI/SPI.h)。

#if defined(ARDUINO_ARCH_SAMD)
  // The datasheet specifies a typical SPI SCK period (tSCK) of 42 ns,
  // see "Table 36-48. SPI Timing Characteristics and Requirements",
  // which translates into a maximum SPI clock of 23.8 MHz.
  // Conservatively, the divider is set for a 12 MHz maximum SPI clock.
  #define SPI_MIN_CLOCK_DIVIDER (uint8_t)(1 + ((F_CPU - 1) / 12000000))
#endif

それに対して AdafruitのSPI.Hでは、「楽観的に」24MHzを上限としている。

  // The datasheet specifies a typical SPI SCK period (tSCK) of 42 ns,
  // see "Table 36-48. SPI Timing Characteristics and Requirements",
  // which translates into a maximum SPI clock of 23.8 MHz.
  // We'll permit use of 24 MHz SPI even though this is slightly out
  // of spec. Given how clock dividers work, the next "sensible"
  // threshold would be a substantial drop down to 12 MHz.
  #if !defined(MAX_SPI)
    #define MAX_SPI 24000000
  #endif
  #define SPI_MIN_CLOCK_DIVIDER (uint8_t)(1 + ((F_CPU - 1) / MAX_SPI))

hoboNicola1.6.1では、UHSライブラリ側から12MHzを与えることにしてAdafruitのBSPに含まれるSPIライブラリをそのまま使って潜在的な問題が生じないようにした。このあたりの話は、ブレッドボードで試しているとき、たまに動かないことがあったので確認してわかったこと。

nRF52の場合、ライブラリ内でコアクロック=64Mと定義されており、INT(64÷26) = 2 となってSPIクロックは32MHzが採用されてしまう。16Mを与えるとコアクロックの1/4なので16MHzになる。

SPIトランザクションについて

UHSライブラリ内からのMAX3421Eのレジスタ操作(書込み、読出し)は、おもにusbhost.hに定義されているテンプレートMAX3421eの staticメソッド regWr()regRd() を使って行われており、これらは以下のようなコードになっている (hoboNicolaで使われない部分は省略)。

template< typename SPI_SS, typename INTR >
void MAX3421e< SPI_SS, INTR >::regWr(uint8_t reg, uint8_t data) {
        USB_SPI.beginTransaction(SPISettings(SPI_CLOCK, MSBFIRST, SPI_MODE0)); // 転送設定
        SPI_SS::Clear();          // スレーブセレクト
        uint8_t c[2];
        c[0] = reg | 0x02;        // 書込みなのでbit1をセット。
        c[1] = data;              // レジスタに書くデータ
        USB_SPI.transfer(c, 2);   // 転送実行
        SPI_SS::Set();            // スレーブセレクト終了
        USB_SPI.endTransaction(); // 転送終了
        return;
};

template< typename SPI_SS, typename INTR >
uint8_t MAX3421e< SPI_SS, INTR >::regRd(uint8_t reg) {
        USB_SPI.beginTransaction(SPISettings(SPI_CLOCK, MSBFIRST, SPI_MODE0)); // The MAX3421E can handle up to 26MHz, use MSB First and SPI mode 0
        SPI_SS::Clear();                  // スレーブセレクト
        USB_SPI.transfer(reg);            // 対象レジスタを書込み
        uint8_t rv = USB_SPI.transfer(0); // 読出し時は 0 を転送。
        SPI_SS::Set();                    // スレーブセレクト終了
        USB_SPI.endTransaction();
        return (rv);
}

SAMD21版 (QT Py SAMD21) での regRd() の処理時間がどれくらいかをロジアナで観測してみると以下のようになった。これらはレジスタ#25(HIRQ)からの読出し時のもので、MAX3421Eに接続されたUSBバスやデバイス(キーボード)の状態をポーリングしているところ。

SAMD21 SPI Read1
SAMD21 SPI Read2

最初が前に書いた SAMD21専用のポート操作で、2番めがArduinoの digitalWrite() などを使った場合。

SS (Slave Select) == LOW の期間が 3.87usecと5.61usecで、約1.8usecの違いがある。この程度の違いならば、Arduino風の書き方の方が読みやすくて良さそうである。なお、8ビットの転送に要する時間が約660nsecなので、1ビットあたり82.5nsec、約12MHzクロックで動作していることが分かる。

nRF52の方がもっと遅い

SPIクロックはSAMD21が12MHzでnRF52が16MHzに設定した。しかしながら、SPIトランザクションにかかる時間(SS=LOW期間)はnRF52の方が長い。

nRF52 SPI Read

SAMD21と同じ状況で得たSPI波形だが、こちらはSS=LOW期間が 11usecもあって、SAMD21でArduino流に書いた場合より2倍時間がかかっている。16MHzなのでクロック出力期間は3/4程度の時間で収まっているのだが、SSをLOWにしてからクロック出力までと、レジスタアドレス(0xC8)を書いて読出し用クロック出力開始までのいずれもが長い。

以下は、同じレジスタに対するregWr()の実行時だが、こちらはSS=LOW期間が6usec程度で収まっているが、なかなかクロックが出力されていないことが分かる。

nRF52 SPI Write

regWr()では USB_SPI.transfer() の呼び出しが1回だけなのに対し、regRd()では2回に分かれている。どうやらnRF52用のSPIライブラリでは、USB_SPI.transfer() の呼出しから実際に転送クロックが出力されるまでにちょっと時間がかかるようだ。このあたりは要検討項目としているが、人間の扱うキーボードが相手なので遅いとかいうレベルでもないだろう。

きょうのまとめ

hoboNicolaライブラリに包含したUHSライブラリ (src/UHSLib2.0) は、USB Host Shield Library 2.0 の1.6.1版からHIDキーボード用コントローラを実現するために必要なファイルを抜き出したものになっている。スケッチ(.ino)から src内の uhslib2.h をincludeすることで、Arduino IDEのライブラリとして追加された UHSライブラリ内のファイルではなく、hoboNicolaライブラリ内のUHSLib2.0以下のファイルが選択される(はず)。

hoboNicolaアダプターでは、今のところMAX3421E (あるいはMAX3421Eが載った基板、シールド)が必須なのだが、2022年4月現在、MAX3421E自体が品薄でminiUHSも品薄かつ流通在庫の値段も上がっている。2018年には500円くらいで買えたのに今は2000円くらいのようだ。

世の中には、USB Host Library SAMD という UHSライブラリをSAMD(Cortex-m0コア)に移植したライブラリもあるようなので、このあたりも試してみたいところ。あるいは、中国製のCH559を使ったUSBホストコントローラもあるようで、こちらの方が安い。いずれにしろ、プログラムが安定して動くころには MAX3421Eを使ったボード/シールドの値段が安くなってるかもしれないが。