STM32 MCU'dan hızlı performans elde etme


11

STM32F303VC keşif kiti ile çalışıyorum ve performansından biraz şaşkınım . Sistemle tanışmak için, bu MCU'nun bit hızını test etmek için çok basit bir program yazdım. Kod aşağıdaki gibi parçalanabilir:

  1. HSI saati (8 MHz) açık;
  2. PLL, HSI / 2 * 16 = 64 MHz'e ulaşmak için 16 ön ölçekleyici ile başlatılır;
  3. PLL, SYSCLK olarak belirlenmiştir;
  4. SYSCLK MCO pimi (PA8) üzerinde izlenir ve pimlerden biri (PE10) sonsuz döngüde sürekli olarak değiştirilir.

Bu programın kaynak kodu aşağıda sunulmuştur:

#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;

      }
}

Kod, -O1 optimizasyonu kullanılarak GNU ARM Gömülü Alet Zinciri ile CoIDE V2 ile derlenmiştir. Bir osiloskopla incelenen PA8 (MCO) ve PE10 pimleri üzerindeki sinyaller şöyle görünür: resim açıklamasını buraya girin

MCO (turuncu eğri) yaklaşık 64 MHz'lik bir salınım gösterdiğinden SYSCLK doğru yapılandırılmış gibi görünmektedir (dahili saatin hata payı dikkate alınarak). Benim için garip olan kısım PE10 (mavi eğri) üzerindeki davranış. Sonsuz while (1) döngüsünde, temel bir 3 adımlı işlem (yani bit-set / bit-reset / return) gerçekleştirmek için 4 + 4 + 5 = 13 saat çevrimi gerekir. Diğer optimizasyon seviyelerinde daha da kötüleşir (örn. -O2, -O3, ar -Os): sinyalin DÜŞÜK kısmına, yani PE10'un düşen ve yükselen kenarlarına (LSI'nin bir şekilde görünmesini sağlamak için) birkaç ek saat döngüsü eklenir bu durumu düzeltmek için).

Bu davranış, bu MCU'dan bekleniyor mu? Biraz ayar ve sıfırlama kadar basit bir görevi 2-4 kat daha hızlı olmalı. İşleri hızlandırmanın bir yolu var mı?


Karşılaştırmak için başka bir MCU ile denediniz mi?
Marko Buršič

3
Neyi başarmaya çalışıyorsunuz? Hızlı salınımlı bir çıkış istiyorsanız, zamanlayıcıları kullanmalısınız. Hızlı seri protokollerle arabirim oluşturmak istiyorsanız, ilgili donanım çevre birimini kullanmanız gerekir.
Jonas Schäfer

2
Kit ile harika bir başlangıç!
Scott Seidman

| = BSRR veya BRR kayıtlarını yalnızca yazıldıkları için yapmamalısınız.
P__J__

Yanıtlar:


25

Buradaki soru gerçekten: C programından ürettiğiniz makine kodu nedir ve beklediğinizden nasıl farklıdır.

Orijinal koda erişiminiz olmasaydı, bu tersine mühendislikte bir alıştırma olurdu radare2 -A arm image.bin; aaa; VV.

İlk olarak, -gişareti CFLAGS(belirttiğiniz aynı yere) eklenen bayrakla derleyin -O1. Ardından, oluşturulan montaja bakın:

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

Elbette hem objdumpikili dosya adının hem de ara ELF dosyanızın farklı olabileceğine dikkat edin.

Genellikle, GCC'nin montajcıyı çağırdığı kısmı atlayabilir ve sadece montaj dosyasına bakabilirsiniz. Sadece -SGCC komut satırına ekleyin - ancak normal olarak yapınızı bozar, bu yüzden büyük olasılıkla IDE'nizin dışında yaparsınız .

Kodunuzun biraz yamalı bir versiyonunun montajını yaptım :

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

ve aşağıdakileri aldı (alıntı, yukarıdaki bağlantı altında tam kod):

.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

Bu bir döngüdür (sonunda koşulsuz atlamayı .L5 ve başında .L5 etiketine dikkat edin).

Burada gördüğümüz şey,

  • önce ldr(yük kaydı) + 24 Bayt'ta r2saklanan hafıza konumundaki değeri içeren kaydı r3. Bunu aramak için çok tembel olmak: büyük olasılıkla yeri BSRR.
  • Daha sonra OR, bu r2kayıttaki 1024 == (1<<10)10. bitin ayarlanmasına karşılık gelen sabiti olan kayıt ve sonucu r2kendisine yazın.
  • Ardından str, sonucu ilk adımda okuduğumuz bellek konumuna (depolayın)
  • ve sonra tembellikten farklı bir bellek konumu için aynı işlemi tekrarlayın: büyük olasılıkla BRRadresi.
  • Sonunda b(dal) ilk adıma geri döner.

Başlamak için üç değil 7 talimatımız var. Sadece bir bkez olur ve bu nedenle tek sayıda döngü alan şey çok olasıdır (toplamda 13 var, bu yüzden bir yerde tek bir döngü sayısı gelmelidir). 13'ün altındaki tüm tek sayılar 1, 3, 5, 7, 9, 11 olduğundan ve 13-6'dan daha büyük sayıları ekartebileceğimiz için (CPU'nun bir döngüden daha az bir sürede komut veremediği varsayılarak ), biliyoruz bu b, 1, 3, 5 veya 7 CPU devir alır.

Kim olduğumuza göre, ARM'nin talimatlar belgelerine ve M3 için ne kadar döngü aldıklarına baktım :

  • ldr 2 döngü alır (çoğu durumda)
  • orr 1 döngü alır
  • str 2 döngü alır
  • b2 ila 4 döngü alır. Biz bu yüzden, bir tek sayı olması gerekir biliyorum gerekir burada, 3 alır.

Tüm bunlar sizin gözleminize uygun:

13=2(cldr+corr+cstr)+cb=2(2+1+2)+3=25+3

Yukarıdaki hesaplamanın gösterdiği gibi, döngünüzü daha hızlı yapmanın bir yolu olmayacaktır - ARM işlemcilerindeki çıkış pinleri genellikle CPU eşlemeli kayıtlar değil, bellek eşlemelidir , bu nedenle normal yük - değiştir - sakla rutini onlarla bir şey yapmak istiyorsun.

Elbette yapabileceğiniz şey, her döngü yinelemesinde pinin değerini okumak ( |=dolaylı olarak okumak zorundadır ) değil, sadece her döngü yinelemesini değiştirdiğiniz yerel bir değişkenin değerini yazmaktır.

8 bitlik mikronlara aşina olabileceğinizi ve sadece 8 bitlik değerleri okumaya, bunları yerel 8 bitlik değişkenlerde saklamaya ve 8 bitlik parçalar halinde yazmaya çalıştığınıza dikkat edin. Yapma. ARM 32 bit bir mimaridir ve 8 bit 32 bitlik bir kelimenin ayıklanması ek talimatlar alabilir. Yapabiliyorsanız, 32 bitlik kelimenin tamamını okuyun, ihtiyacınız olanı değiştirin ve bir bütün olarak tekrar yazın. Bunun mümkün olup olmadığı elbette ne yazdığınıza, yani bellek eşlemeli GPIO'nuzun düzenine ve işlevselliğine bağlıdır. Değiştirmek istediğiniz biti içeren 32 bit'te nelerin depolandığı hakkında bilgi için STM32F3 veri sayfası / kullanıcı kılavuzuna başvurun.


Şimdi, "düşük" dönemi süreleri uzuyor ile sorunu yeniden çalıştı ama ben sadece yapamadım - döngü görünüyor ile tam olarak aynı -O3olduğu gibi -O1benim derleyici sürümü ile. Bunu kendiniz yapmanız gerekecek! Belki de yetersiz ARM desteği ile eski GCC versiyonunu kullanıyorsunuz.


4
Söylediğiniz gibi sadece depolamak ( =yerine |=) tam olarak OP'nin aradığı hızlanma olmaz mı? ARM'lerin BRR ve BSRR kayıtlarına ayrı ayrı sahip olmalarının nedeni, okuma-değiştirme-yazma gerektirmemesidir. Bu durumda, sabitler döngü dışındaki kayıtlarda saklanabilir, böylece iç döngü sadece 2 str ve bir dal olur, bu nedenle tüm tur için 2 + 2 +3 = 7 döngü?
Timo

Teşekkürler. Bu gerçekten işleri biraz temizledi. Sadece 3 saat döngüsüne ihtiyaç duyulacağında ısrar etmek biraz aceleyle düşünülmüştü - 6 ila 7 döngü aslında umduğum bir şeydi. Çözüm -O3temizlendikten ve yeniden oluşturulduktan sonra hata ortadan kalkmış gibi görünüyor. Bununla birlikte, montaj kodumun içinde ek bir UTXH talimatı var gibi görünüyor: .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
uxthçünkü GPIO->BSRRL(yanlış) başlıklarınızda 16 bitlik kayıt olarak tanımlanmıştır. BSMRL ve BSRRH'nin olmadığı, ancak 32 bitlik tek bir kaydın bulunduğu STM32CubeF3 kütüphanelerinden başlıkların son bir sürümünü kullanın BSRR. @Marcus görünüşe göre doğru başlıklara sahiptir, bu yüzden kodu yarım kelime yüklemek ve genişletmek yerine tam 32 bit erişim yapar.
berendi -

Neden tek bir baytın yüklenmesi ek talimatlar alacak? ARM mimarisi vardır LDRBve STRBbayt hayır, tek talimatında / yazma okur gerçekleştiren?
17'de psmears

1
M3 çekirdeği , 1 MB'lık bir çevresel bellek alanının 32 MB'lik bir bölgeye takıldığı bit bantlamasını (bu özel uygulamanın yapıp yapmadığından emin değil) destekleyebilir. Her bitin ayrı bir kelime adresi vardır (sadece bit 0 kullanılır). Muhtemelen hala bir yükten / mağazadan daha yavaştır.
Sean Houlihane

8

BSRRVe BRRkayıtları tek tek liman bitlerini ayarlamak ve sıfırlamak için şunlardır:

GPIO bağlantı noktası biti ayarlama / sıfırlama kaydı (GPIOx_BSRR)

...

(x = A..H) Bit 15: 0

BSy: Bağlantı noktası x set bit y (y = 0..15)

Bu bitler salt yazılır. Bu bitlere yapılan bir okuma 0x0000 değerini döndürür.

0: Karşılık gelen ODRx bitinde işlem yok

1: İlgili ODRx bitini ayarlar

Gördüğünüz gibi, bu kayıtları okumak her zaman 0 verir, bu nedenle kodunuz

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

etkin bir şekilde yok olduğunu GPIOE->BRR = 0 | GPIO_BRR_BR_10, ancak bir dizi oluşturur, böylece en iyi duruma, bilmiyor LDR, ORR, STRbunun yerine tek bir mağaza talimatlar.

Yazarak pahalı okuma-değiştirme-yazma işleminden kaçınabilirsiniz

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

Döngüyü 8 ile eşit olarak bölünebilen bir adrese hizalayarak daha fazla gelişme elde edebilirsiniz. Döngüden asm("nop");önce bir veya mod talimatı koymayı deneyin while(1).


1

Burada söylenenlere eklemek için: Kesinlikle Cortex-M, ancak hemen hemen tüm işlemcilerle (bir boru hattı, önbellek, dal tahmini veya diğer özelliklerle), en basit döngüyü bile almak önemsizdir:

top:
   subs r0,#1
   bne top

İstediğiniz kadar milyonlarca kez çalıştırın, ancak bu döngünün performansının büyük ölçüde değişebilmesini sağlayın, sadece bu iki talimat, isterseniz ortada bazı düğümler ekleyin; önemli değil.

Döngünün hizalamasının değiştirilmesi, performansı önemli ölçüde değiştirebilir, özellikle de bir yerine iki getirme çizgisi alırsa, flaşın CPU'dan 2 daha yavaş olduğu böyle bir mikrodenetleyicide böyle bir mikrodenetleyicide böyle bir mikro kontrolörde yemek yiyebilirsiniz. ya da daha sonra saati yükselterek oran, ilave getirme işleminden 3 ya da 4 ya da 5 daha da kötüleşir.

Muhtemelen bir önbelleğe sahip değilsiniz, ancak bazı durumlarda yardımcı olduysa, ancak başkalarında acıyor ve / veya fark yaratmıyor. Burada olabilir veya olmayabilir (muhtemelen değil) şube tahmini sadece boruda tasarlanan kadarıyla görebilir, bu nedenle döngüyü dallanacak şekilde değiştirmiş ve sonunda koşulsuz bir dalınız olsa bile (bir dal tahmincisi için daha kolay) kullanım) tüm yapmanız gereken sizi bir sonraki getiride birçok saatin (normalde tahmin edicinin ne kadar derine gelebileceği boru boyutu) ve / veya her durumda bir ön getirme yapmamasıdır.

Getirme ve önbellek satırlarına göre hizalamayı değiştirerek, şube öngörücüsünün size yardım edip etmeyeceğini etkileyebilir ve yalnızca iki talimatı veya bazı düğümleri olanları test ediyor olsanız bile genel performansta görülebilir. .

Bunu yapmak biraz önemsizdir ve derledikten sonra derlenmiş kod veya hatta elle yazılmış derleme alarak, performansının bu faktörler nedeniyle büyük ölçüde değişebileceğini, birkaç ila birkaç yüz yüz ekleyerek veya kaydederek, bir satır C kodu, biri kötü yerleştirilmiş nop.

BSRR kaydını kullanmayı öğrendikten sonra, kodunuzu flaş yerine RAM'den (kopyala ve atla) çalıştırmayı deneyin, bu da size başka bir şey yapmadan yürütmede anında 2-3 kat artış sağlar.


0

Bu davranış, bu MCU'dan bekleniyor mu?

Bu, kodunuzun bir davranışıdır.

  1. BRR / BSRR kayıtlarına yazmalısınız, şimdi yaptığınız gibi read-change-write yazmamalısınız.

  2. Ayrıca döngü yükü de meydana gelir. Maksimum performans için, BRR / BSRR işlemlerini tekrar tekrar çoğaltın → bunları bir döngüde tekrar tekrar yapıştırın ve yapıştırın, böylece bir döngü yükünden önce birçok ayar / sıfırlama döngüsünden geçersiniz.

edit: IAR altında bazı hızlı testler.

BRR / BSRR'ye yazı yazma, orta düzeyde optimizasyon altında 6 ve en yüksek düzeyde optimizasyon altında 3 talimat alır; RMW'ng'de bir çevirme 10 talimat / 6 talimat alır.

döngü havai ekstra.


Değiştirerek |=için =tek bir bit kümesi / sıfırlama faz 9 saat döngüsü (tüketir bağlantı ). Montaj kodu 3 talimat uzunluğundadır:.L5 strh r1, [r3, #24] @ movhi str r2, [r3, #40] b .L5
KR

1
Yapma elle göz önüne sermek döngüler. Bu neredeyse hiç iyi bir fikir değil. Bu özel durumda, özellikle yıkıcıdır: dalga formunu periyodik olmayan hale getirir. Ayrıca, aynı kodun flaşta birçok kez bulunması mutlaka daha hızlı değildir. Bu burada geçerli olmayabilir (olabilir!), Ancak döngü açma, birçok insanın yardımcı olduğunu düşündüğü, derleyicilerin ( gcc -funroll-loops) çok iyi yapabileceği ve kötüye kullanıldığında (burada olduğu gibi) istediğiniz şeyin ters etkisine sahip olduğu bir şeydir.
Marcus Müller

Sonsuz bir döngü , tutarlı bir zamanlama davranışını sürdürmek için hiçbir zaman etkili bir şekilde açılamaz .
Marcus Müller

1
@ MarcusMüller: Bir komutun görünür bir etkisinin olmayacağı döngünün bazı tekrarlarında herhangi bir nokta varsa, sürekli döngüleri korurken sonsuz döngüler bazen kullanışlı bir şekilde açılabilir. Örneğin somePortLatch, daha düşük 4 biti çıkış için ayarlanmış bir bağlantı noktasını kontrol ederse , while(1) { SomePortLatch ^= (ctr++); }15 değer çıkaran ve daha sonra aynı değeri arka arkaya iki kez çıkardığı zamandan başlamak için geri dönebilen bir kodun açılması mümkündür .
supercat

Supercat, doğru. Ayrıca, bellek arayüzünün zamanlaması gibi efektler "kısmen" açılmayı mantıklı kılabilir. Danny'nin tavsiyesi daha genelleştirici olduğunu ve hatta tehlikeli böylece My ifadesi çok genel, ama hissediyorum
Marcus Müller
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.