STM32のFreeRTOSを使ってみる

GPIOの高速IO


なんだか、Arduino_Core_STM32の紹介ばかりしていますが、気にせずに進めます。
ATmegaではPort Registerを使って、高速にIOを行うことができます。
ATmegaのPort Registerの使い方はこ ちらなどに公開されていますが、例えば以下のコードでD2からD9の8ポートを順番にON/OFFすることができます。
DEBUGを1にするとゆっくり動くので、LEDを付けると動きが良く分かります。
#define DEBUG 0

#define BMASK         0x03              //more intuitive style for mixed Ports
#define DMASK         0xFC              //does exactly the same as previous

void write8(uint8_t bytes) {
  PORTB = (PORTB & ~BMASK) | ((bytes) & 0xC0) >> 6;
  PORTD = (PORTD & ~DMASK) | ((bytes) & 0x3F) << 2;
}

void setup() {
  Serial.begin(115200);
  pinMode(2, OUTPUT);
  pinMode(3, OUTPUT);
  pinMode(4, OUTPUT);
  pinMode(5, OUTPUT);
  pinMode(6, OUTPUT);
  pinMode(7, OUTPUT);
  pinMode(8, OUTPUT);
  pinMode(9, OUTPUT);
  write8(0x0);
}

void loop(){
  uint8_t bytes = 0;
  uint32_t smill = millis();
  for(uint32_t i=0;i<100000;i++) {
    write8(bytes);
    bytes++;
    if (DEBUG) delay(100);
  }
  uint32_t emill = millis();
  Serial.print("Elasped=");
  Serial.println(emill - smill);
}



Arduino_Core_STM32でも、LL GPIO Generic Driver(通称LLドライバー)を使えば、同様に複数のポートを一括して操作することができます。
LLドライバーはGPIOだけでなく、I2CやSPIも操作することができます。
HALドライバーとLLドライバーのドキュメントがこ ちらで公開されていますが、GPIOのLLドライバーだけならばこ ちらに非公式のAPIが公開されています。

そこで、digitalWriteとLL GPIOドライバーの速度を比較してみました。
こちらがdigitalWriteを使ったコードです。
PA0からPA7の8ポートを順番に100,000回、ON/OFFしています。
#define DEBUG 0

uint32_t Pins[] = {PA0, PA1, PA2, PA3, PA4, PA5, PA6, PA7};

void write8(uint8_t bytes) {
  for(uint8_t bits=0;bits<8;bits++) {
    uint8_t mask = 1<<bits;
#if 0
    Serial.print("mask=");
    Serial.print(mask,HEX);
    Serial.print(" ");
    Serial.println((bytes & mask), HEX);
#endif
    if ( (bytes & mask) != 0 ) {
      digitalWrite(Pins[bits], HIGH);
    } else {
      digitalWrite(Pins[bits], LOW);
    }
  }
}

void setup() {
  Serial.begin(115200);
  pinMode(PA0, OUTPUT);
  pinMode(PA1, OUTPUT);
  pinMode(PA2, OUTPUT);
  pinMode(PA3, OUTPUT);
  pinMode(PA4, OUTPUT);
  pinMode(PA5, OUTPUT);
  pinMode(PA6, OUTPUT);
  pinMode(PA7, OUTPUT);
  write8(0x0);
}

void loop(){
  uint8_t bytes = 0;
  uint32_t smill = millis();
  for(long i=0;i<100000;i++) {
    if (DEBUG) {
      Serial.print("bytes=");
      Serial.println(bytes, HEX);
      delay(100);
    }
    write8(bytes);
    bytes++;
  }
  uint32_t emill = millis();
  Serial.print("Elasped=");
  Serial.println(emill - smill);
}



次に、こちらがLL GPIOドライバーを使ったコードです。
同様に、PA0からPA7の8ポートを順番に100,000回、ON/OFFしています。
#define DEBUG 0

uint32_t Pins[] = {LL_GPIO_PIN_0, LL_GPIO_PIN_1, LL_GPIO_PIN_2, LL_GPIO_PIN_3, LL_GPIO_PIN_4, LL_GPIO_PIN_5, LL_GPIO_PIN_6, LL_GPIO_PIN_7};

void write8(uint8_t bytes) {
  uint32_t PinMask = 0;
  for(uint8_t bits=0;bits<8;bits++) {
    uint8_t mask = 1<<bits;
#if 0
    Serial.print("mask=");
    Serial.print(mask,HEX);
    Serial.print(" ");
    Serial.println((bytes & mask), HEX);
#endif
    if ( (bytes & mask) != 0 ) PinMask = PinMask + Pins[bits];
  }
#if 0
  Serial.print("bytes=");
  Serial.print(bytes,HEX);
  Serial.print(" PinMask=");
  Serial.println(PinMask,HEX);
#endif
  LL_GPIO_WriteOutputPort(GPIOA, PinMask);
}

void setup() {
  Serial.begin(115200);
  pinMode(PA0, OUTPUT);
  pinMode(PA1, OUTPUT);
  pinMode(PA2, OUTPUT);
  pinMode(PA3, OUTPUT);
  pinMode(PA4, OUTPUT);
  pinMode(PA5, OUTPUT);
  pinMode(PA6, OUTPUT);
  pinMode(PA7, OUTPUT);
  write8(0x0);
}

void loop(){
  uint8_t bytes = 0;
  uint32_t smill = millis();
  for(uint32_t i=0;i<100000;i++) {
    if (DEBUG) {
      Serial.print("bytes=");
      Serial.println(bytes, HEX);
      delay(100);
    }
    write8(bytes);
    bytes++;
  }
  uint32_t emill = millis();
  Serial.print("Elasped=");
  Serial.println(emill - smill);
}

各ボードの結果はこのようになりました。

digitalWrite() LL_GPIO_WriteOutputPort 高速化率
STM32F103 1253 253 x4.9
STM32F303 1294
218 x5.9
STM32F401 578 129 x4.4
STM32F411 509 113 x4.5
STM32F407 313 67 x4.6
参考

digitalWrite() PORT操作 高速化率
ATmega328 5204 264 x19.7

ATmegaのPORT操作よりも早くIOできることが分かりました。



LL GPIOドライバーを使うとdigitalWrite()も高速化することができます。
以下のコードはいずれもPB4をON/OFFするコードですが、LLドライバーを使うと高速にON/OFFすることができます。
DEBUGを1にするとゆっくり動くので、LEDを付けると動きが良く分かります。
#define LL_HIGH(LL_GPIO_PIN) LL_GPIO_WriteOutputPort(GPIOB, (LL_GPIO_ReadOutputPort(GPIOB) | LL_GPIO_PIN))
#define LL_LOW(LL_GPIO_PIN)  LL_GPIO_WriteOutputPort(GPIOB, (LL_GPIO_ReadOutputPort(GPIOB) & ~(LL_GPIO_PIN)))

#define DEBUG 0

void LL_HIGH_FUNC(uint32_t LL_GPIO_PIN) {
#if 0
  uint32_t port = LL_GPIO_ReadOutputPort(GPIOB);
  port = port | (LL_GPIO_PIN);
#endif
  LL_GPIO_WriteOutputPort(GPIOB, (LL_GPIO_ReadOutputPort(GPIOB) | LL_GPIO_PIN));
}

void LL_LOW_FUNC(uint32_t LL_GPIO_PIN) {
#if 0
  uint32_t port = LL_GPIO_ReadOutputPort(GPIOB);
  port = port & ~(LL_GPIO_PIN);
#endif
  LL_GPIO_WriteOutputPort(GPIOB, (LL_GPIO_ReadOutputPort(GPIOB) & ~(LL_GPIO_PIN)));
}

void setup() {
  Serial.begin(115200);
  pinMode(PB3, OUTPUT);
  pinMode(PB4, OUTPUT);
  pinMode(PB5, OUTPUT);
  digitalWrite(PB3, HIGH);
  digitalWrite(PB5, HIGH);
}

void loop(){
  uint32_t counter = 1000000;
  if (DEBUG) counter = 5;
  Serial.print("counter=");
  Serial.println(counter);

  uint32_t smill = millis();
  for (uint32_t i=0;i<counter;i++) {
      digitalWrite(PB4, HIGH);
      if (DEBUG) delay(1000);
      digitalWrite(PB4, LOW);
      if (DEBUG) delay(1000);
  }
  uint32_t emill = millis();
  Serial.print("Elasped(digitalWrite)=");
  Serial.println(emill - smill);

  uint32_t port;
  smill = millis();
  for (uint32_t i=0;i<counter;i++) {
      port = LL_GPIO_ReadOutputPort(GPIOB);
      port = port | LL_GPIO_PIN_4;
      LL_GPIO_WriteOutputPort(GPIOB, port);
      if (DEBUG) delay(1000);
      port = LL_GPIO_ReadOutputPort(GPIOB);
      port = port & ~(LL_GPIO_PIN_4);
      LL_GPIO_WriteOutputPort(GPIOB, port);
      if (DEBUG) delay(1000);
  }
  emill = millis();
  Serial.print("Elasped(LL_GPIO)=");
  Serial.println(emill - smill);

  smill = millis();
  for (uint32_t i=0;i<counter;i++) {
      LL_HIGH_FUNC(LL_GPIO_PIN_4);
      if (DEBUG) delay(1000);
      LL_LOW_FUNC(LL_GPIO_PIN_4);
      if (DEBUG) delay(1000);
  }
  emill = millis();
  Serial.print("Elasped(LL_GPIO_FUNC)=");
  Serial.println(emill - smill);

  smill = millis();
  for (uint32_t i=0;i<counter;i++) {
      LL_HIGH(LL_GPIO_PIN_4);
      if (DEBUG) delay(1000);
      LL_LOW(LL_GPIO_PIN_4);
      if (DEBUG) delay(1000);
  }
  emill = millis();
  Serial.print("Elasped(LL_GPIO_MACRO)=");
  Serial.println(emill - smill);

}


各ボードの結果はこのようになりました。

digitalWrite() LL_GPIO LL_GPIO_FUNC LL_GPIO_MACRO
STM32F103 1779 292 695 292
STM32F303 1891 209 695 181
STM32F401 762 132 298 132
STM32F407 393 78 160 78

LL_HIGH_FUNC()とLL_HIGHマクロは同じ内容ですが、関数呼び出しにするよりもマクロで埋め込んだ方が高速になります。



Arduino core for STM32にはCMSISレジスターが定義されています。
CMSISレジスターはこ ちらに定義されていますが、ボードごとに微妙に構造体が違います。
こちらはSTM32F103のGPIOレジスターの構造体です。
typedef struct
{
  __IO uint32_t CRL;
  __IO uint32_t CRH;
  __IO uint32_t IDR;
  __IO uint32_t ODR;
  __IO uint32_t BSRR;
  __IO uint32_t BRR;
  __IO uint32_t LCKR;
} GPIO_TypeDef;

こちらはSTM32F303のGPIOレジスターの構造体です。
typedef struct
{
  __IO uint32_t MODER;        /*!< GPIO port mode register,               Address offset: 0x00      */
  __IO uint32_t OTYPER;       /*!< GPIO port output type register,        Address offset: 0x04      */
  __IO uint32_t OSPEEDR;      /*!< GPIO port output speed register,       Address offset: 0x08      */
  __IO uint32_t PUPDR;        /*!< GPIO port pull-up/pull-down register,  Address offset: 0x0C      */
  __IO uint32_t IDR;          /*!< GPIO port input data register,         Address offset: 0x10      */
  __IO uint32_t ODR;          /*!< GPIO port output data register,        Address offset: 0x14      */
  __IO uint32_t BSRR;         /*!< GPIO port bit set/reset register,      Address offset: 0x1A */
  __IO uint32_t LCKR;         /*!< GPIO port configuration lock register, Address offset: 0x1C      */
  __IO uint32_t AFR[2];       /*!< GPIO alternate function registers,     Address offset: 0x20-0x24 */
  __IO uint32_t BRR;          /*!< GPIO bit reset register,               Address offset: 0x28 */
}GPIO_TypeDef;

モデルごとにレジスター構造が違うので、このレジスターを使って、GPIOの入出力方向やPullUp/PullDownの設定を行うと、
モデル依存のコードになりますが、ODRレジスターは(おそらく)どのモデルにもあるので、このレジスターに書き込むことで、
ポートを一括して操作することができます。
ODRレジスターの詳細はこちらに公開 されていますが、各ポート毎(GPIO_AからGPIO_G)に存在し、32ビットのうち下位16ビットだけを使います。
GPIO_A(PA0からPA15)のレジスターはGPIOA->ODR、GPIO_B(PB0からPB15)のレジスターは GPIOB->ODRとなります。
GPIOA->ODR=0x0001とするとPA0だけがONとなり、GPIOA->ODR=0x0002とするとPA1だけがON になり、
それ以外のGPIOはOFFになります。

こちらがCMSISレジスターを使ったコードです。
PA0からPA15の16個のGPIOを100,000回、ON/OFFしています。
ポート内の全てのGPIOの値を書き換えるので、特定の1つだけを書き換える用途には向いていません。
#define DEBUG 0

void write8(uint8_t bytes) {
  GPIOA->ODR = bytes;
}

void setup() {
  Serial.begin(115200);
  pinMode(PA0, OUTPUT);
  pinMode(PA1, OUTPUT);
  pinMode(PA2, OUTPUT);
  pinMode(PA3, OUTPUT);
  pinMode(PA4, OUTPUT);
  pinMode(PA5, OUTPUT);
  pinMode(PA6, OUTPUT);
  pinMode(PA7, OUTPUT);
  write8(0x0);
}

void loop(){
  uint8_t bytes = 0;
  uint32_t smill = millis();
  for(uint32_t i=0;i<100000;i++) {
    if (DEBUG) {
      Serial.print("bytes=");
      Serial.println(bytes, HEX);
      delay(100);
    }
    write8(bytes);
    bytes++;
  }
  uint32_t emill = millis();
  Serial.print("Elasped=");
  Serial.println(emill - smill);
}

各ボードの結果はこのようになりました。
無茶苦茶速くなります。

digitalWrite() CMSIS ODRレジスター 高速化率
STM32F103 1253 12
x104.4
STM32F303 1294 14 x92.4
STM32F401 579 7 x82.7
STM32F411 509 6 x84.8
STM32F407 313 4 x78.2
参考

digitalWrite() PORT操作 高速化率
ATmega328 5204 264 x19.7



このようなArduino-UNO用のTFTシールドが安価に売られています。
安いものだと$6程度で入手することができます。


I/Fは8ビットパラレルで、1ドットを表示するためにGPIOを15回操作する必要が有ります。
240x320ドットの再描画を行う場合、15*240*320の1,152,000回のGPIO操作が発生し、GPIOの処理スピードがそのま ま表示スピード になります。
そこで、LL GPIO Generic DriverとCMSIS ODRレジスターを使ったライブラリをこちらで 公開しています。
Arduino_Core_STM32がサポートするボードならばなんでも動くはずです。
手元のSTM32F446でこ ちらのサンプルを動かしてみましたが、目が回るぐらい高速に立方体が回転します。

続く
...