Szybka wydajność dzięki MCU STM32


11

Pracuję z zestawem do wykrywania STM32F303VC i jestem nieco zaskoczony jego wydajnością. Aby zapoznać się z systemem, napisałem bardzo prosty program, aby po prostu przetestować szybkość bitowania tego MCU. Kod można podzielić w następujący sposób:

  1. Zegar HSI (8 MHz) jest włączony;
  2. PLL jest inicjowany za pomocą prekalera 16, aby osiągnąć HSI / 2 * 16 = 64 MHz;
  3. PLL jest oznaczony jako SYSCLK;
  4. SYSCLK jest monitorowany na pinie MCO (PA8), a jeden z pinów (PE10) jest stale przełączany w nieskończonej pętli.

Kod źródłowy tego programu przedstawiono poniżej:

#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 został skompilowany z CoIDE V2 z wbudowanym narzędziem GNU ARM przy użyciu optymalizacji -O1. Sygnały na pinach PA8 (MCO) i PE10, badane za pomocą oscyloskopu, wyglądają następująco: wprowadź opis zdjęcia tutaj

SYSCLK wydaje się być poprawnie skonfigurowany, ponieważ MCO (krzywa pomarańczowa) wykazuje oscylację prawie 64 MHz (uwzględniając margines błędu zegara wewnętrznego). Dziwne dla mnie jest zachowanie na PE10 (niebieska krzywa). W nieskończonej pętli while (1) potrzeba 4 + 4 + 5 = 13 cykli zegara, aby wykonać podstawową 3-etapową operację (tj. Zestaw bitów / reset bitów / powrót). Jest jeszcze gorzej na innych poziomach optymalizacji (np. -O2, -O3, ar -Os): kilka dodatkowych cykli zegara jest dodawanych do NISKIEJ części sygnału, tj. Między opadającymi i rosnącymi krawędziami PE10 (włączenie LSI w jakiś sposób wydaje się zaradzić tej sytuacji).

Czy tego zachowania oczekuje się od tego MCU? Wyobrażam sobie, że zadanie tak proste jak ustawienie i resetowanie powinno być 2-4 razy szybsze. Czy istnieje sposób na przyspieszenie?


Czy próbowałeś z innym MCU do porównania?
Marko Buršič

3
Co próbujesz osiągnąć? Jeśli potrzebujesz szybkiego oscylującego wyjścia, powinieneś używać timerów. Jeśli chcesz połączyć się z szybkimi protokołami szeregowymi, powinieneś używać odpowiedniego sprzętu peryferyjnego.
Jonas Schäfer

2
Świetny początek z zestawem !!
Scott Seidman

Nie wolno | | rejestrów BSRR lub BRR, ponieważ są one tylko do zapisu.
P__J__

Odpowiedzi:


25

Pytanie tutaj naprawdę brzmi: jaki jest kod maszynowy generowany przez program C i czym różni się od tego, czego można oczekiwać.

Jeśli nie miałeś dostępu do oryginalnego kodu, byłoby to ćwiczenie z inżynierii wstecznej (w zasadzie coś zaczynającego się od:) radare2 -A arm image.bin; aaa; VV, ale masz kod, więc to wszystko ułatwia.

Najpierw skompiluj go z -gflagą dodaną do CFLAGS(to samo miejsce, w którym również określasz -O1). Następnie spójrz na wygenerowany zespół:

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

Zauważ, że zarówno nazwa objdumppliku binarnego, jak i pośredni plik ELF mogą być różne.

Zwykle można również pominąć część, w której GCC wywołuje asembler, i po prostu spojrzeć na plik zespołu. Po prostu dodaj -Sdo wiersza poleceń GCC - ale to normalnie zepsuje twoją kompilację, więc najprawdopodobniej zrobiłbyś to poza twoim IDE.

Zrobiłem montaż nieco poprawionej wersji twojego kodu :

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

i otrzymałem następujące (fragment, pełny kod pod linkiem powyżej):

.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

Która jest pętlą (zwróć uwagę na bezwarunkowy skok do .L5 na końcu i etykietę .L5 na początku).

Widzimy tutaj to, że my

  • najpierw ldr(załaduj rejestr) rejestr r2o wartości w lokalizacji pamięci zapisanej w r3+ 24 bajtach. Będąc zbyt leniwym, aby to sprawdzić: bardzo prawdopodobne jest położenie BSRR.
  • Następnie rejestr ze stałą , która odpowiada ustawieniu bitu 10. w tym rejestrze i zapisać wynik do siebie.ORr21024 == (1<<10)r2
  • Następnie str(zapisz) wynik w lokalizacji pamięci, z której czytaliśmy w pierwszym kroku
  • a następnie powtórz to samo dla innej lokalizacji pamięci, z lenistwa: najprawdopodobniej BRRadres.
  • Wreszcie b(rozgałęzienie) powrót do pierwszego kroku.

Na początek mamy 7 instrukcji, a nie 3. To się bzdarza tylko raz, a zatem jest bardzo prawdopodobne, że trwa nieparzysta liczba cykli (w sumie mamy 13, więc gdzieś musi pochodzić nieparzysta liczba cykli). Ponieważ wszystkie nieparzyste liczby poniżej 13 to 1, 3, 5, 7, 9, 11 i możemy wykluczyć dowolne liczby większe niż 13-6 (zakładając, że procesor nie może wykonać instrukcji w mniej niż jednym cyklu), wiemy że bzajmuje 1, 3, 5 lub 7 cykli procesora.

Będąc tym, kim jesteśmy, spojrzałem na dokumentację instrukcji ARM i ile cykli zajmują M3:

  • ldr zajmuje 2 cykle (w większości przypadków)
  • orr zajmuje 1 cykl
  • str zajmuje 2 cykle
  • bzajmuje 2 do 4 cykli. Wiemy, że musi to być liczba nieparzysta, więc tutaj musi zająć 3.

Wszystko zgadza się z twoją obserwacją:

13=2)(dolrer+doorr+dostr)+dob=2)(2)+1+2))+3)=2)5+3)

Jak pokazuje powyższe obliczenie, nie będzie sposobu na przyspieszenie pętli - styki wyjściowe w procesorach ARM są zwykle mapowane w pamięci , a nie w rejestrach rdzenia procesora, więc musisz przejść przez zwykłe ładowanie - modyfikować - przechowywać procedurę, jeśli chcesz coś z tym zrobić.

Co można zrobić, to oczywiście nie czytać ( |=domyślnie ma czytać) wartość kołek w każdej iteracji pętli, ale wystarczy napisać wartość zmiennej lokalnej do niego, który po prostu przełączać każdej iteracji pętli.

Zauważ, że wydaje mi się, że znasz mikrosfery 8-bitowe i próbowałbyś odczytać tylko 8-bitowe wartości, przechowywać je w lokalnych 8-bitowych zmiennych i zapisywać je w 8-bitowych porcjach. Nie rób ARM to architektura 32-bitowa, a wyodrębnienie 8-bitowego słowa 32-bitowego może wymagać dodatkowych instrukcji. Jeśli możesz, po prostu przeczytaj całe 32-bitowe słowo, zmodyfikuj to, czego potrzebujesz, i zapisz je jako całość. To, czy jest to możliwe, zależy oczywiście od tego, do czego piszesz, tj. Układu i funkcjonalności GPIO odwzorowanego w pamięci. Zapoznaj się z arkuszem danych / instrukcją użytkownika STM32F3, aby uzyskać informacje na temat tego, co jest przechowywane w 32-bitowym bicie zawierającym bit, który chcesz przełączyć.


Teraz próbowałem odtworzyć twój problem z „niskim” okresem wydłużania się, ale po prostu nie mogłem - pętla wygląda dokładnie tak samo -O3jak -O1w mojej wersji kompilatora. Musisz to zrobić sam! Być może używasz starożytnej wersji GCC z nieoptymalną obsługą ARM.


4
Czy nie byłoby tak , jak powiedziałeś, przechowywanie ( =zamiast |=) dokładnie takiego przyspieszenia, jakiego szuka OP? Powodem, dla którego ARM mają osobno rejestry BRR i BSRR, jest niewymaganie odczytu-modyfikacji-zapisu. W takim przypadku stałe mogą być przechowywane w rejestrach poza pętlą, więc wewnętrzna pętla byłaby tylko 2 str i gałęzią, więc 2 + 2 +3 = 7 cykli dla całej rundy?
Timo

Dzięki. To naprawdę trochę wyjaśniło. Twierdzenie, że potrzebne będą tylko 3 cykle zegara, było nieco pochopnym pomysłem - od 6 do 7 cykli było czymś, na co tak naprawdę liczyłem. -O3Błąd wydaje się, że zniknął po oczyszczeniu i odbudowy rozwiązanie. Niemniej jednak mój kod asemblera wydaje się zawierać w sobie dodatkową instrukcję 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
uxthjest tam, ponieważ GPIO->BSRRL(niepoprawnie) jest zdefiniowany jako 16-bitowy rejestr w twoich nagłówkach. Użyj najnowszej wersji nagłówków z bibliotek STM32CubeF3 , w których nie ma BSRRL i BSRRH, ale pojedynczy BSRRrejestr 32-bitowy . @Markus najwyraźniej ma poprawne nagłówki, więc jego kod ma pełny 32-bitowy dostęp zamiast ładować półsłówka i go rozszerzać.
berendi - protestujący

Dlaczego ładowanie jednego bajtu wymaga dodatkowych instrukcji? Architektura ARM ma LDRBi STRBwykonuje bajty do odczytu / zapisu w jednej instrukcji, nie?
psmears

1
Rdzeń M3 może obsługiwać pasmowanie bitów (nie jestem pewien, czy ta konkretna implementacja tak robi), w którym obszar 1 MB przestrzeni pamięci peryferyjnej jest aliasowany do regionu 32 MB. Każdy bit ma dyskretny adres słowa (bit 0 jest używany tylko). Przypuszczalnie nadal wolniejszy niż ładunek / sklep.
Sean Houlihane

8

Te BSRRi BRRrejestrów do ustawiania i zerowania poszczególnych bitów portów:

Rejestr bitów portu / reset rejestru GPIOx_BSRR

...

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

BSy: Port x ustawić bit y (y = 0..15)

Te bity są tylko do zapisu. Odczyt tych bitów zwraca wartość 0x0000.

0: Brak działania na odpowiednim bicie ODRx

1: Ustawia odpowiedni bit ODRx

Jak widać, czytanie tych rejestrów zawsze daje 0, a więc kod

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

robi to skutecznie GPIOE->BRR = 0 | GPIO_BRR_BR_10, ale optymalizator nie wie, że tak to generuje sekwencję LDR, ORR, STRinstrukcje zamiast jednego sklepu.

Możesz uniknąć kosztownej operacji odczytu-modyfikacji-zapisu, po prostu pisząc

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

Możesz uzyskać dalszą poprawę, dopasowując pętlę do adresu równomiernie podzielnego przez 8. Spróbuj umieścić asm("nop");instrukcję lub instrukcję trybu przed while(1)pętlą.


1

Aby dodać do tego, co zostało tutaj powiedziane: Z pewnością z Cortex-M, ale prawie z każdym procesorem (z potokiem, pamięcią podręczną, prognozowaniem gałęzi lub innymi funkcjami), banalne jest wykonanie nawet najprostszej pętli:

top:
   subs r0,#1
   bne top

Uruchom go tyle milionów razy, ile chcesz, ale wydajność tej pętli może się znacznie różnić, tylko te dwie instrukcje, jeśli chcesz, dodaj trochę środkowych; to nie ma znaczenia.

Zmiana wyrównania pętli może radykalnie zmienić wydajność, szczególnie w przypadku małej pętli, jeśli zajmuje dwie linie pobierania zamiast jednej, zjadasz ten dodatkowy koszt, na takim mikrokontrolerze, w którym flash jest wolniejszy niż procesor o 2 lub 3, a następnie przez zwiększenie zegara stosunek staje się jeszcze gorszy 3 lub 4 lub 5 niż dodanie dodatkowego pobierania.

Prawdopodobnie nie masz pamięci podręcznej, ale jeśli tak, to pomaga w niektórych przypadkach, ale boli w innych i / lub nie robi różnicy. Przewidywanie rozgałęzień, które możesz tutaj mieć (a prawdopodobnie nie ma), może zobaczyć tylko tyle, ile jest zaprojektowane w potoku, więc nawet jeśli zmieniłeś pętlę na rozgałęzienie i miałeś bezwarunkową gałąź na końcu (łatwiej dla predyktora gałęzi do use) wszystko, co robi, to oszczędza ci tyle zegarów (rozmiar potoku, z którego normalnie pobierałby do tego, jak głęboko widzi predyktor) przy następnym pobieraniu i / lub nie robi pobierania wstępnego na wszelki wypadek.

Zmieniając wyrównanie w odniesieniu do linii pobierania i pamięci podręcznej, możesz wpływać na to, czy predyktor gałęzi pomaga, czy nie, i można to zobaczyć w ogólnej wydajności, nawet jeśli testujesz tylko dwie instrukcje lub te dwie z niektórymi zerami .

Jest to nieco trywialne, a kiedy to zrozumiesz, biorąc pod uwagę skompilowany kod, a nawet ręcznie napisany zestaw, możesz zauważyć, że jego wydajność może się znacznie różnić z powodu tych czynników, dodając lub oszczędzając kilka do kilkuset procent, jedna linia kodu C, jedna źle ustawiona nop.

Po nauczeniu się korzystania z rejestru BSRR, spróbuj uruchomić swój kod z pamięci RAM (kopiowanie i przeskakiwanie) zamiast flasha, co powinno zapewnić natychmiastowy wzrost wydajności 2-3 razy bez wykonywania żadnych innych czynności.


0

Czy tego zachowania oczekuje się od tego MCU?

Jest to zachowanie twojego kodu.

  1. Powinieneś pisać do rejestrów BRR / BSRR, a nie czytać-modyfikować-pisać tak jak teraz.

  2. Ty również ponosisz narzut pętli. Aby uzyskać maksymalną wydajność, powtarzaj operacje BRR / BSRR w kółko → kopiuj i wklej je w pętli wiele razy, aby przejść przez wiele cykli ustawiania / resetowania przed narzutem jednej pętli.

edycja: kilka szybkich testów w ramach IAR.

przewrócenie zapisu do BRR / BSRR wymaga 6 instrukcji przy umiarkowanej optymalizacji i 3 instrukcji przy najwyższym poziomie optymalizacji; przerzucenie RMW'ng zajmuje 10 instrukcji / 6 instrukcji.

dodatkowa pętla.


Po przejściu |=na =jeden bit faza ustawiania / zerowania zużywa 9 cykli zegara ( łącze ). Kod zestawu zawiera 3 instrukcje:.L5 strh r1, [r3, #24] @ movhi str r2, [r3, #40] b .L5
KR

1
Nie rozwijaj ręcznie pętli. To praktycznie nigdy nie jest dobry pomysł. W tym konkretnym przypadku jest to szczególnie katastrofalne: powoduje, że przebieg nie jest okresowy. Również posiadanie tego samego kodu wiele razy we flashu niekoniecznie jest szybsze. Może to nie dotyczyć tutaj (może!), Ale rozwijanie pętli jest czymś, co wielu ludziom myśli, że pomaga, że ​​kompilatory ( gcc -funroll-loops) mogą sobie radzić bardzo dobrze, a gdy są nadużywane (jak tutaj), mają odwrotny efekt, co chcesz.
Marcus Müller

Nieskończonej pętli nigdy nie można skutecznie rozwinąć, aby zachować spójne zachowanie czasowe.
Marcus Müller

1
@ MarcusMüller: Pętle nieskończone mogą czasem być użytecznie rozwijane przy zachowaniu spójnego timingu, jeśli w niektórych powtórzeniach pętli są jakieś punkty, w których instrukcja nie miałaby widocznego efektu. Na przykład, jeśli somePortLatchkontroluje port, którego dolne 4 bity są ustawione na wyjście, może być możliwe rozwinięcie się while(1) { SomePortLatch ^= (ctr++); }w kod, który wyprowadza 15 wartości, a następnie zapętlenie z powrotem, aby rozpocząć w momencie, gdy w przeciwnym razie wyprowadziłby tę samą wartość dwa razy z rzędu.
supercat

Supercat, prawda. Ponadto efekty takie jak synchronizacja interfejsu pamięci itp. Mogą sprawić, że sensowne będzie „częściowe” rozwinięcie. Moje oświadczenie było zbyt ogólne, ale czuję, że rada Danny'ego jest jeszcze bardziej uogólniona, a nawet niebezpiecznie
Marcus Müller
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.