STM32 MCUから高速パフォーマンスを取得する


11

私はSTM32F303VC ディスカバリーキットを使用していますが、そのパフォーマンスに少し困惑しています。システムに慣れるために、このMCUのビットバンギング速度をテストするための非常に単純なプログラムを作成しました。コードは次のように分類できます。

  1. HSIクロック(8 MHz)がオンになっています。
  2. PLLは16のプリスケーラーで開始され、HSI / 2 * 16 = 64 MHzを達成します。
  3. PLLはSYSCLKとして指定されています。
  4. SYSCLKはMCOピン(PA8)で監視され、ピンの1つ(PE10)は無限ループで常にトグルされます。

このプログラムのソースコードを以下に示します。

#include "stm32f3xx.h"

int main(void)
{
      // Initialize the HSI:
      RCC->CR |= RCC_CR_HSION;
      while(!(RCC->CR&RCC_CR_HSIRDY));

      // Initialize the LSI:
      // RCC->CSR |= RCC_CSR_LSION;
      // while(!(RCC->CSR & RCC_CSR_LSIRDY));

      // PLL configuration:
      RCC->CFGR &= ~RCC_CFGR_PLLSRC;     // HSI / 2 selected as the PLL input clock.
      RCC->CFGR |= RCC_CFGR_PLLMUL16;   // HSI / 2 * 16 = 64 MHz
      RCC->CR |= RCC_CR_PLLON;          // Enable PLL
      while(!(RCC->CR&RCC_CR_PLLRDY));  // Wait until PLL is ready

      // Flash configuration:
      FLASH->ACR |= FLASH_ACR_PRFTBE;
      FLASH->ACR |= FLASH_ACR_LATENCY_1;

      // Main clock output (MCO):
      RCC->AHBENR |= RCC_AHBENR_GPIOAEN;
      GPIOA->MODER |= GPIO_MODER_MODER8_1;
      GPIOA->OTYPER &= ~GPIO_OTYPER_OT_8;
      GPIOA->PUPDR &= ~GPIO_PUPDR_PUPDR8;
      GPIOA->OSPEEDR |= GPIO_OSPEEDER_OSPEEDR8;
      GPIOA->AFR[0] &= ~GPIO_AFRL_AFRL0;

      // Output on the MCO pin:
      //RCC->CFGR |= RCC_CFGR_MCO_HSI;
      //RCC->CFGR |= RCC_CFGR_MCO_LSI;
      //RCC->CFGR |= RCC_CFGR_MCO_PLL;
      RCC->CFGR |= RCC_CFGR_MCO_SYSCLK;

      // PLL as the system clock
      RCC->CFGR &= ~RCC_CFGR_SW;    // Clear the SW bits
      RCC->CFGR |= RCC_CFGR_SW_PLL; //Select PLL as the system clock
      while ((RCC->CFGR & RCC_CFGR_SWS_PLL) != RCC_CFGR_SWS_PLL); //Wait until PLL is used

      // Bit-bang monitoring:
      RCC->AHBENR |= RCC_AHBENR_GPIOEEN;
      GPIOE->MODER |= GPIO_MODER_MODER10_0;
      GPIOE->OTYPER &= ~GPIO_OTYPER_OT_10;
      GPIOE->PUPDR &= ~GPIO_PUPDR_PUPDR10;
      GPIOE->OSPEEDR |= GPIO_OSPEEDER_OSPEEDR10;

      while(1)
      {
          GPIOE->BSRRL |= GPIO_BSRR_BS_10;
          GPIOE->BRR |= GPIO_BRR_BR_10;

      }
}

コードは、-O1最適化を使用して、GNU ARM Embedded Toolchainを使用してCoIDE V2でコンパイルされました。オシロスコープで調べたピンPA8(MCO)およびPE10の信号は、次のようになります。 ここに画像の説明を入力してください

MCO(オレンジカーブ)がほぼ64 MHzの発振を示すため、SYSCLKは正しく構成されているように見えます(内部クロックのエラーマージンを考慮)。私にとって奇妙な部分は、PE10(青い曲線)の動作です。無限while(1)ループでは、基本的な3ステップ操作(ビットセット/ビットリセット/リターン)を実行するのに4 + 4 + 5 = 13クロックサイクルかかります。他の最適化レベル(例:-O2、-O3、ar -Os)ではさらに悪化します。信号のLOW部分、つまりPE10の立ち下がりエッジと立ち上がりエッジの間にいくつかの追加のクロックサイクルが追加されます(LSIの有効化この状況を改善するため)。

この動作はこのMCUから予想されますか?設定とリセットを2〜4倍速くするだけの簡単なタスクを想像します。物事をスピードアップする方法はありますか?


他のMCUと比較してみましたか?
マルコブルシッチ

3
何を達成しようとしていますか?高速発振出力が必要な場合は、タイマーを使用する必要があります。高速シリアルプロトコルとインターフェイスする場合は、対応するハードウェア周辺機器を使用する必要があります。
ジョナスシェーファー

2
キットからの素晴らしいスタート!!
スコットサイドマン

BSRRまたはBRRレジスタは書き込み専用であるため、これらを使用しないでください。
P__J__

回答:


25

ここでの質問は本当にです:あなたがCプログラムから生成しているマシンコードは何であり、それはあなたが期待するものとどのように違いますか。

元のコードにアクセスできなかった場合、これはリバースエンジニアリング(基本的にはで始まるものradare2 -A arm image.bin; aaa; VV)の演習になりますが、コードが手元にあるため、すべてが簡単になります。

まず、に-gフラグを追加してコンパイルしますCFLAGS(あなたが指定するのと同じ場所-O1)。次に、生成されたアセンブリを確認します。

arm-none-eabi-objdump -S yourprog.elf

もちろん、objdumpバイナリの名前と中間ELFファイルの両方が異なる場合があることに注意してください。

通常、GCCがアセンブラを呼び出す部分をスキップして、アセンブリファイルを確認することもできます。-SGCCコマンドラインに追加するだけですが、通常はビルドが中断されるため、おそらくIDEの外部で実行することになります。

あなたのコードのわずかにパッチを当てたバージョンのアセンブリを行いました:

arm-none-eabi-gcc 
    -O1 ## your optimization level
    -S  ## stop after generating assembly, i.e. don't run `as`
    -I/path/to/CMSIS/ST/STM32F3xx/ -I/path/to/CMSIS/include
     test.c

そして、次のものを得ました(上記のリンクの下にある抜粋、完全なコード):

.L5:
    ldr r2, [r3, #24]
    orr r2, r2, #1024
    str r2, [r3, #24]
    ldr r2, [r3, #40]
    orr r2, r2, #1024
    str r2, [r3, #40]
    b   .L5

これはループです(最後の.L5および先頭の.L5ラベルへの無条件ジャンプに注意してください)。

ここにあるのは

  • 最初にldr(レジスタをロード)、r2メモリ位置の値がr3+ 24バイトで保存されているレジスタをロードします。見上げるのが面倒だ:の可能性が非常に高いBSRR
  • 次にOR、そのr2レジスタ1024 == (1<<10)の10番目のビットの設定に対応する定数を持つレジスタを使用し、結果をr2それ自体に書き込みます。
  • 次にstr、最初のステップで読み込んだメモリの場所に結果を保存します
  • そして、遅延を避けて、別のメモリ位置に対して同じことを繰り返します:最も可能性の高いBRRアドレスです。
  • 最後にb(分岐)最初のステップに戻ります。

したがって、3つの指示ではなく7つの指示があります。b一度だけ発生するため、奇数のサイクルを使用している可能性が非常に高くなります(合計で13であるため、奇数のサイクルカウントが発生する必要があります)。13未満のすべての奇数は1、3、5、7、9、11であり、13-6を超える数値は除外できるため(CPUが1サイクル未満で命令を実行できないと仮定)、ことをb1、3、5、7またはCPUサイクルを要します。

私たちが誰であるかについて、ARMの命令のドキュメントと、M3 にかかるサイクルの量を調べました。

  • ldr 2サイクルかかります(ほとんどの場合)
  • orr 1サイクルかかります
  • str 2サイクルかかります
  • b2〜4サイクルかかります。奇数でなければならないことがわかっているので、ここでは3が必要です。

すべてがあなたの観察と一致している:

13=2cldr+corr+cstr+cb=22+1+2+3=25+3

上記の計算が示すように、ループを高速化する方法はほとんどありません。ARMプロセッサの出力ピンは通常CPUコアレジスタではなくメモリマップされるため、通常のロード-変更-ストアルーチンを実行する必要があります。あなたはそれらで何かをしたいです。

あなたはもちろん読まれていない何ができるか(|=暗黙的に持っているピンの値ごとにループの繰り返しを、ちょうどそのあなただけのトグルごとにループの繰り返し、それにローカル変数の値を読み書きします)。

8ビットマイクロに精通しているようで、8ビット値のみを読み取ってローカルの8ビット変数に保存し、8ビットのチャンクで書き込もうとしていることに気づきます。しないでください。ARMは32ビットアーキテクチャであり、8ビットの32ビットワードを抽出するには追加の命令が必要になる場合があります。可能であれば、32ビットワード全体を読み、必要なものを変更し、全体として書き戻します。もちろん、それが可能かどうかは、書き込み先、つまりメモリマップされたGPIOのレイアウトと機能に依存します。トグルしたいビットを含む32ビットに保存されている情報については、STM32F3データシート/ユーザーズガイドを参照してください。


さて、「低」期間が長くなるという問題を再現しようとしましたが、私は単にできませんでした-ループはコンパイラバージョン-O3とまったく同じに見え-O1ます。あなたはそれを自分でしなければなりません!たぶん、最適化されていないARMサポートを備えたGCCの古いバージョンを使用している可能性があります。


4
あなたが言うように、単に(=ではなく|=)格納するだけでは、OPが探しているまさにスピードアップではないでしょうか?ARMにBRRレジスタとBSRRレジスタが別々にある理由は、read-modify-writeを必要としないためです。この場合、定数はループの外側のレジスタに格納できるため、内側のループは2つのstrと分岐だけなので、ラウンド全体で2 + 2 +3 = 7サイクルですか?
ティモ

ありがとう。それは本当に物事をかなりクリアしました。必要なのは3クロックサイクルだけだと主張するのは少し急いでした。6〜7サイクルは、私が実際に望んでいたものでした。-O3エラーが解決策を清掃し、再構築した後に消えてしまったようです。それにもかかわらず、私のアセンブリコードには追加のUTXH命令が含まれているようです。– .L5: ldrh r3, [r2, #24] uxth r3, r3 orr r3, r3, #1024 strh r3, [r2, #24] @ movhi ldr r3, [r2, #40] orr r3, r3, #1024 str r3, [r2, #40] b .L5
KR

1
uxthGPIO->BSRRLヘッダー内の16ビットレジスタとして(誤って)定義されているためです。STM32CubeF3ライブラリの最新バージョンのヘッダーを使用します。このライブラリには、BSRRLとBSRRHはなく、単一の32ビットBSRRレジスタがあります。@Marcusは明らかに正しいヘッダーを持っているため、彼のコードはハーフワードをロードして拡張するのではなく、完全な32ビットアクセスを行います。
-berendi

なぜシングルバイトをロードするのに余分な命令が必要なのですか?ARMアーキテクチャには、単一の命令でバイトの読み取り/書き込みを実行するものがLDRBありSTRBますか?
-psmears

1
M3コア、周辺のメモリ空間の1 MB領域が32 MB領域にエイリアスされるビットバンディングサポートできます(この特定の実装サポートするかどうかはわかりません)。各ビットには個別のワードアドレスがあります(ビット0のみが使用されます)。おそらく、単にロード/ストアよりも遅いです。
ショーンフーリハネ

8

BSRRそしてBRRレジスタは、個々のポートのビットを設定し、リセットするためです。

GPIOポートビットセット/リセットレジスタ(GPIOx_BSRR)

...

(x = A..H)ビット15:0

BSy:ポートxはビットyを設定(y = 0..15)

これらのビットは書き込み専用です。これらのビットを読み取ると、値0x0000が返されます。

0:対応するODRxビットに対するアクションなし

1:対応するODRxビットを設定します

ご覧のとおり、これらのレジスタを読み取ると常に0が得られるため、コードは

GPIOE->BSRRL |= GPIO_BSRR_BS_10;
GPIOE->BRR |= GPIO_BRR_BR_10;

効果的ではないGPIOE->BRR = 0 | GPIO_BRR_BR_10が、オプティマイザはそれを知らないので、それはのシーケンスを生成しLDRORRSTR代わりに単一のストアの指示。

単純な書き込みにより、高価な読み取り-変更-書き込み操作を回避できます。

GPIOE->BSRRL = GPIO_BSRR_BS_10;
GPIOE->BRR = GPIO_BRR_BR_10;

ループを8で割り切れるアドレスに揃えることで、さらに改善さasm("nop");れる場合がありwhile(1)ます。ループの前に1つまたはモードの命令を入れてみてください。


1

ここで述べたことに加えて:確かにCortex-Mですが、ほとんどすべてのプロセッサ(パイプライン、キャッシュ、分岐予測、またはその他の機能を備えています)では、最も単純なループでさえ取るのは簡単です。

top:
   subs r0,#1
   bne top

必要に応じて何百万回も実行しますが、ループのパフォーマンスを大きく変えることができます。これらの2つの命令だけで、必要に応じて中央にいくつかのnopを追加します。関係ありません。

ループのアライメントを変更すると、パフォーマンスが劇的に変化する可能性があります。特に、1つではなく2つのフェッチラインを使用するような小さなループでは、フラッシュがCPUよりも2倍遅いこのようなマイクロコントローラーで余分なコストを消費しますまたは3で、クロックをアップすると、追加のフェッチを追加するよりも比率が3または4または5悪化します。

おそらくキャッシュを持っていないでしょうが、もし持っていればそれはいくつかのケースでは助けになりますが、他のケースで痛い、そして/または違いを生みません。ここにあるかもしれない(そうでないかもしれない)分岐予測は、パイプで設計された範囲でしか見ることができないため、ループを分岐に変更し、最後に無条件分岐があったとしても(分岐予測子の方が簡単です)使用)すべては、次のフェッチで多くのクロック(通常フェッチする場所から予測子が見ることができる深さまでのパイプのサイズ)を節約することであり、および/または念のためにプリフェッチを行いません。

フェッチおよびキャッシュラインに関するアライメントを変更することにより、分岐予測が役立つかどうかに影響を与えることができます。これは、2つの命令またはいくつかのnopを持つ2つの命令のみをテストしている場合でも、全体的なパフォーマンスで確認できます。

これを行うのは少し簡単です。コンパイルされたコードまたは手書きのアセンブリを使用することを理解すると、これらの要因によりパフォーマンスが大幅に変化することがわかります。 1行のCコード、1つは適切に配置されていないnop。

BSRRレジスタの使用方法を学んだ後、フラッシュの代わりにRAMからコードを実行して(コピーとジャンプ)、他に何もせずに実行速度を2倍から3倍にすることができます。


0

この動作はこのMCUから予想されますか?

これはコードの動作です。

  1. 現在のようにread-modify-writeではなく、BRR / BSRRレジスタに書き込む必要があります。

  2. また、ループのオーバーヘッドが発生します。最大のパフォーマンスを得るには、BRR / BSRR操作を繰り返し複製します→ループ内で複数回コピーアンドペーストして、1回のループオーバーヘッドの前に多くのセット/リセットサイクルを実行します。

編集:IARの下でいくつかの簡単なテスト。

BRR / BSRRへのフリップスルー書き込みは、中程度の最適化では6命令、最高レベルの最適化では3命令かかります。RMW'ngをめくるには10命令/ 6命令かかります。

余分なループオーバーヘッド。


シングルビットセット/リセットフェーズに変更する|==、9クロックサイクルを消費します(リンク)。アセンブリコードは、3つの手順の長さである:.L5 strh r1, [r3, #24] @ movhi str r2, [r3, #40] b .L5
KR

1
ループを手動で展開しないください。それは実際には決して良い考えではありません。この特定のケースでは、それは特に壊滅的です:それは波形を非周期的にします。また、フラッシュで同じコードを何度も使用しても、必ずしも高速になるとは限りません。これはここでは当てはまらないかもしれませんが(そうかもしれません!)、ループの展開は多くの人が役立つと考えているものであり、コンパイラ(gcc -funroll-loops)は非常にうまく機能し、悪用された場合(ここのように)望みの逆の効果があります。
マーカスミュラー

無限ループを効果的に展開して、一貫したタイミング動作を維持することはできません
マーカスミュラー

1
@MarcusMüller:無限ループは、命令が目に見える効果を持たないループのいくつかの繰り返しでポイントがある場合、一貫したタイミングを維持しながら、時々便利に展開できます。たとえばsomePortLatch、下位4ビットが出力用に設定されているポートを制御する場合、while(1) { SomePortLatch ^= (ctr++); }15個の値を出力するコードに展開し、それ以外の場合は同じ値を連続して2回出力するときにループバックして開始できる場合があります。
-supercat

Supercat、本当。また、メモリインターフェイスのタイミングなどの影響により、「部分的に」展開することが賢明になる場合があります。私の文では、あまりにも一般的でしたが、私はダニーのアドバイスは、より一般化さを感じる、とさえ危険なので
マーカス・ミュラー
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.