STM32のFreeRTOSを使ってみる

I2CとSPI


STコアにはI2CライブラリとSPIライブラリが含まれていますが、沢山のボードをサポートしているので、I2CとSPIのGPIOもボード毎 に違います。
I2C、SPIのデフォルトでアサインされているGPIOは、こ ちらの各ボードのvariant_generic.hを見ないと分かりません。
また、ARDUINO_BLUEPILL_F103CB/ARDUINO_BLUEPILL_F103C8のValiantを使うと、こ ちらの定義が使われます。

例えば、ボード名がDIYMORE_F407VGTの場合、variant.hは以下の様になっています。
// On-board LED pin number
#define LED_BUILTIN             PE0

// On-board user button
#define BTN_K_UP                PD15

// SPI Definitions
#define PIN_SPI_MOSI            PB5
#define PIN_SPI_MISO            PB4
#define PIN_SPI_SCK             PB3
#define PIN_SPI_SS              PB6

// I2C Definitions
#define PIN_WIRE_SDA            PB9
#define PIN_WIRE_SCL            PB8

これで、このボードのデフォルトの設定が分かります。
オンボードLEDはPE0
オンボードボタンはPD15
SPIはPB5/PB4/PB3/PB6
I2CはPB9/PB8

ボードによってはこのようにピン番号が直で書かれていることが有ります。
// On-board user button
#define USER_BTN                2

// SPI Definitions
#define PIN_SPI_SS2             14
#define PIN_SPI_MOSI            PA7
#define PIN_SPI_MISO            5
#define PIN_SPI_SCK             PA5

各ピンはピン名(Pxn)と、ピン番号を持っていて、その対応も同じvariant.hに定義されています。
#define PA0  2
#define PA6  5
#define PB12 14



I2C

以下のピンがデフォルトのI2Cとして定義されています。
F4ボードはコンパイル時に選ぶValiantにより、微妙に定義が変わります。

SDA SCL
Nucleo PB9 PB8
blackpill_f103c8 PB7 PB6
bluepill_f103c8 PB7 PB6
genericSTM32F103xx PB7 PB6
genericSTM32F303xx PB7 PB6
blackpill_f401cc PB7 PB6
genericSTM32F401CC PB3 PB10
blackpill_f411ce PB7 PB6
genericSTM32F411CE PB3 PB10
genericSTM32F405RG PB7 PB6
black_f407vg PB9 PB8
diymore_f407vgt PB7 PB6
genericSTM32F407VGT6 PB9 PB8
disco_f407vg PB7 PB8


I2CのRemap

例えばSTM32F103のI2Cの定義は以下の様になっています。
//*** I2C ***

#ifdef HAL_I2C_MODULE_ENABLED
WEAK const PinMap PinMap_I2C_SDA[] = {
  {PB_7,  I2C1, STM_PIN_DATA(STM_MODE_AF_OD, GPIO_NOPULL, AFIO_NONE)},
  {PB_9,  I2C1, STM_PIN_DATA(STM_MODE_AF_OD, GPIO_NOPULL, AFIO_I2C1_ENABLE)},
#ifndef ARDUINO_BLUEPILL_F103C6
  {PB_11, I2C2, STM_PIN_DATA(STM_MODE_AF_OD, GPIO_NOPULL, AFIO_NONE)},
#endif
  {NC,    NP,    0}
};
#endif

#ifdef HAL_I2C_MODULE_ENABLED
WEAK const PinMap PinMap_I2C_SCL[] = {
  {PB_6,  I2C1, STM_PIN_DATA(STM_MODE_AF_OD, GPIO_NOPULL, AFIO_NONE)},
  {PB_8,  I2C1, STM_PIN_DATA(STM_MODE_AF_OD, GPIO_NOPULL, AFIO_I2C1_ENABLE)},
#ifndef ARDUINO_BLUEPILL_F103C6
  {PB_10, I2C2, STM_PIN_DATA(STM_MODE_AF_OD, GPIO_NOPULL, AFIO_NONE)},
#endif
  {NC,    NP,    0}
};
#endif

これを見ると、以下のピンをI2Cとして使えることが分かります。

I2C1 I2C1 I2C2
SDA PB7 PB9 PB11
SCL PB6 PB8 PB10

Wire.hをインクルードするとI2Cのインスタンスが自動的に生成され、
PB6/PB7のセットがI2Cピンとして使えるようになりますが、
以下のコードで簡単にI2Cインスタンスのピンを再定義(Remap)することができます。
#include <Wire.h>
void setup() {
  delay(1000);            
  Serial.begin(115200);

  /* Wire remapped to I2C1 Alternative */
  Wire.setSCL(PB8);
  Wire.setSDA(PB9);
  Wire.begin();

以下省略


I2Cの追加

こちらに I2Cインスタンスの追加手順が公開されています。
STM32F103C6でなければ、以下の手順でWire2(名前は何でもいい)を追加することができます。
WireとWire2は物理的に違うGPIOを使うので、同時に使うことができます。
#include <Wire.h>
//            SDA   SCL
TwoWire Wire2(PB11, PB10);

void setup() {
  Wire2.begin();
}

以下省略


ちょっと脱線

I2Cが使用するGPIOの指定方法には方言が有ります。
I2Cを使用するライブラリには、ライブラリ内部でこれらの違いを吸収している場合が有りますが、
ライブラリの外で指定する方がスマートです。
但し、esp8266だけは、ライブラリの外で指定することができません。
ESP32やSTM32のコアライブラリはAPIを追加することで対応していますが、
ESP8266のコアライブラリは、互換性という点でダメダメです。
- arduino core for esp32:
Wire.setPins(sdaPin, sclPin);
Wire.begin();

- arduino core for esp8266:
Wire.begin(SDA,SCL);

- atmega:
#define SDA_PORT PORTD
#define SDA_PIN sdaPin
#define SCL_PORT PORTD
#define SCL_PIN sclPin
#include <SoftI2CMaster.h>
#include <SoftWire.h>
SoftWire Wire = SoftWire();
Wire.begin();

- Arduino core support for STM32 based boards:
Wire.setSCL(sclPin);
Wire.setSDA(sdaPin);
Wire.begin();

- Arduino Core for Nordic Semiconductor nRF5 based boards:
Wire.setPins(sdaPin, sclPin);
Wire.begin();


SPI

以下のピンがデフォルトのSPIとして定義されています。
F407ボードはコンパイル時に選ぶボードにより、微妙に定義が変わります。

SS MOSI MISO SCK
Nucleo PB6 PA7 PA6 PA5
blackpill_f103c8 PA4 PA7 PA6 PA5
bluepill_f103c8 PA4 PA7 PA6 PA5
genericSTM32F103xx PA4 PA7 PA6 PA5
genericSTM32F303xx PA4 PA7 PA6 PA5
blackpill_f401cc PA4 PA7 PA6 PA5
genericSTM32F401CC PA4 PA7 PA6 PA5
blackpill_f411ce PA4 PA7 PA6 PA5
genericSTM32F411CE PB12 PA1 PA11 PB13
genericSTM32F405RG PA4 PA7 PA6 PA5
black_f407vg PB7(*) PB5 PB4 PB3
diymore_f407vgt PB6 PB5 PB4 PB3
genericSTM32F407VGT6 PA4 PA7 PA6 PA5
disco_f407vg PB12 PA7 PA6 PA5

(*)オンボードのNRF24 connectorで使用しているので使えません。


SPIのRemap

例えばSTM32F103のSPIの定義は以下の様になっています。
#ifdef HAL_SPI_MODULE_ENABLED
WEAK const PinMap PinMap_SPI_MOSI[] = {
  {PA_7,  SPI1, STM_PIN_DATA(STM_MODE_AF_PP, GPIO_PULLUP, AFIO_NONE)},
  {PB_5,  SPI1, STM_PIN_DATA(STM_MODE_AF_PP, GPIO_PULLUP, AFIO_SPI1_ENABLE)},
#ifndef ARDUINO_BLUEPILL_F103C6
  {PB_15, SPI2, STM_PIN_DATA(STM_MODE_AF_PP, GPIO_PULLUP, AFIO_NONE)},
#endif
  {NC,    NP,    0}
};
#endif

#ifdef HAL_SPI_MODULE_ENABLED
WEAK const PinMap PinMap_SPI_MISO[] = {
  {PA_6,  SPI1, STM_PIN_DATA(STM_MODE_AF_PP, GPIO_PULLUP, AFIO_NONE)},
  {PB_4,  SPI1, STM_PIN_DATA(STM_MODE_AF_PP, GPIO_PULLUP, AFIO_SPI1_ENABLE)},
#ifndef ARDUINO_BLUEPILL_F103C6
  {PB_14, SPI2, STM_PIN_DATA(STM_MODE_AF_PP, GPIO_PULLUP, AFIO_NONE)},
#endif
  {NC,    NP,    0}
};
#endif

#ifdef HAL_SPI_MODULE_ENABLED
WEAK const PinMap PinMap_SPI_SCLK[] = {
  {PA_5,  SPI1, STM_PIN_DATA(STM_MODE_AF_PP, GPIO_PULLUP, AFIO_NONE)},
  {PB_3,  SPI1, STM_PIN_DATA(STM_MODE_AF_PP, GPIO_PULLUP, AFIO_SPI1_ENABLE)},
#ifndef ARDUINO_BLUEPILL_F103C6
  {PB_13, SPI2, STM_PIN_DATA(STM_MODE_AF_PP, GPIO_PULLUP, AFIO_NONE)},
#endif
  {NC,    NP,    0}
};
#endif

#ifdef HAL_SPI_MODULE_ENABLED
WEAK const PinMap PinMap_SPI_SSEL[] = {
  {PA_4,  SPI1, STM_PIN_DATA(STM_MODE_AF_PP, GPIO_PULLUP, AFIO_NONE)},
  {PA_15, SPI1, STM_PIN_DATA(STM_MODE_AF_PP, GPIO_PULLUP, AFIO_SPI1_ENABLE)},
#ifndef ARDUINO_BLUEPILL_F103C6
  {PB_12, SPI2, STM_PIN_DATA(STM_MODE_AF_PP, GPIO_PULLUP, AFIO_NONE)},
#endif
  {NC,    NP,    0}
};
#endif

これを見ると、F103C8ならば以下のピンをSPIとして使えることが分かります。

SPI1 SPI1 SPI2
MOSI PA7 PB5 PB15(*)
MISO PA6 PB4 PB14(*)
SCLK PA5 PB3 PB13(*)
SSEL PA4 PA15 PB12(*)

(*) 5V tolerant

SPI.hをインクルードするとSPIのインスタンスが自動的に生成され、
PA7/PA6/PA5/PA4のセットがSPIピンとして使えるようになりますが、
以下のコードで簡単にSPIインスタンスのピンを再定義(Remap)することができます。
一部のボードでは PA15 PB3 PB4 はデフォルトでJTAGのデバッグポートにアサインされています。
これらのピンを別の用途で使うときは、JTAGデバッグの機能を無効にする必要が有ります。
void setup() {
  delay(1000);               
  Serial.begin(115200);

  /* SPI remapped to SPI1 Alternative */
  pinF1_DisconnectDebug(PB_3); // disable Serial wire JTAG configuration
  pinF1_DisconnectDebug(PB_4); // disable Serial wire JTAG configuration
  pinF1_DisconnectDebug(PA_15); // disable Serial wire JTAG configuration
  SPI.setMOSI(PB5);
  SPI.setMISO(PB4);
  SPI.setSCLK(PB3);
  SPI.setSSEL(PA15);
  SPI.begin();

以下省略


SPIの追加

こちらに SPIインスタンスの追加手順が公開されています。
STM32F103C6でなければ、以下の手順でSPI(名前は何でもいい)を追加することができます。
SPIとSPIは物理的に違うGPIOを使うので、同時に使うことができます。
#include <SPI.h>
//             MOSI  MISO  SCLK  SSEL
SPIClass SPI_3(PB15, PB14, PB13, PB12);

void setup() {
  SPI_3.begin(); // Enables the SPI_3 instance
  SPI_3.beginTransaction(1, settings);
  SPI_3.transfer(2, 0x52);
  SPI_3.transfer(1, 0xA4);
  SPI_3.end(); // Disable SPI_3 instance
}

CSピンは実行時に指定することもできます。
#include <SPI.h>
//             MOSI  MISO  SCLK
SPIClass SPI_3(PB15, PB14, PB13);

void setup() {
  SPI_3.begin(PB12); // Enables the SPI_3 instance
  SPI_3.beginTransaction(1, settings);
  SPI_3.transfer(2, 0x52);
  SPI_3.transfer(1, 0xA4);
  SPI_3.end(); // Disable SPI_3 instance
}


SPIの互換性

ArduinoでのSPIライブラリの使い方には以下の2つの書き方が有ります。
こちらがLegacyなコードです。
SPI.begin()
SPI.setClockDivider(divider)
SPI.setDataMode(mode)
SPI.setBitOrder(order)



SPI.transfer(data);

Arduinoのリファレンスを見ると上記の手順は非推奨になっていて、新しいプロジェクトでは以下のコードを使えと書いてあります。
こちらがTransaction付きのコードです。
SPI.begin()
spiSettings = SPISettings(SPI_SPEED, MSBFIRST, SPI_MODE0);



SPI.beginTransaction(spiSettings)
SPI.transfer(data);
SPI.endTransaction()

UNOやMEGAではLegacyコードも問題なく動きますが、Arduino_Core_STM32ではTransaction付きのコードし か動き ません。
SPI通信を行うライブラリの中にArduino_Core_STM32では動かないものが有りますが、これが原因です。

毎回、SPIの設定を行うので効率は悪いですが、STM32の場合、CPUクロックの種類が多いので、
SPI_CLOCK_DIVでSPIスピードを指定すると、モデルごとにばらばらのスピードになってしまいます。
SPISettingを使えばCPUクロックに関係なく、SPIスピードを決めることができます。

トランザクション付きのSPIをサポートするかどうかはSPIライブラリに依存します。
トランザクション付きのSPIをサポートするライブラリでは、SPI.begin()するとSPI_HAS_TRANSACTIONが定義される ので、
この定義の有無を判断して以下の様にコードを振り分けます。
  SPI.begin();
#if defined(SPI_HAS_TRANSACTION)
  spiSettings = SPISettings(8000000, MSBFIRST, SPI_MODE0);
#else
  SPI.setClockDivider(SPI_CLOCK_DIV8);
  SPI.setBitOrder(MSBFIRST);
  SPI.setDataMode(SPI_MODE0);
#endif


#if defined(SPI_HAS_TRANSACTION)
  SPI.beginTransaction(spiSettings);
#endif
  uint8_t value = SPI.transfer(data);
#if defined(SPI_HAS_TRANSACTION)
  SPI.endTransaction();
#endif

その後の調査で、以下の様にsetClockDivider()を使わなければ、Legacyなコードでも動くことが分かりました。
setClockDivider()を使わない場合のSPIスピードですが、どこにも資料が見当たりません。
SPI.begin()
//SPI.setClockDivider(divider)
SPI.setDataMode(mode)
SPI.setBitOrder(order)



SPI.transfer(data);

SPI通信を行うライブラリの中にArduino_Core_STM32では動かないものが有りますが、
ほとんど以下のどちらかが原因です。
・チップセレクトが10固定。
・setClockDivider()を使ってSPIスピードを指定


ベンチマーク

こちらで 公開されているベンチマークのスケッチを手元のボードに書き込んでみました。

float[ms] int[ms] 動作Clock[Mhz]
F072RB 1826 131 48
F103RC 652 17 72
F303CC 553 20 72
F401CC 315 12 84
F411CE 276 11 96
F412RE 276 11 96
F405RG 163 6 168
F407VG 163 6 168
F446RE 152 5 180
F767ZI 77 4 216
G030C8 1564 96 64
G431CB 160 6 170
H750VB 34 2 480
L452RE 331 12 80
参考
UNO 3514 1553 16
ESP8266 431 55 160
ESP32 95 3
240

Maple CoreはほとんどSTM32F103専用のコアですが、ST Coreなら色々なボードを試すことができます。
F103でもUNOに比べたら爆速ですが、F407やF446になると超爆速になります。
F405/407とESP8266はほとんど同じ動作Clockですが、CPUアーキテクチャーの違いがベンチマークに出ています。
但し、Core Libraryがボードごとに分かれていて、ボードを変えるとCore Libraryをソースからフルコンパイルするので、
コンパイルには時間がかかります。

続く....