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:
- Zegar HSI (8 MHz) jest włączony;
- PLL jest inicjowany za pomocą prekalera 16, aby osiągnąć HSI / 2 * 16 = 64 MHz;
- PLL jest oznaczony jako SYSCLK;
- 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:
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?
Odpowiedzi:
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
-g
flagą dodaną doCFLAGS
(to samo miejsce, w którym również określasz-O1
). Następnie spójrz na wygenerowany zespół:Zauważ, że zarówno nazwa
objdump
pliku 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
-S
do 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 :
i otrzymałem następujące (fragment, pełny kod pod linkiem powyżej):
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
ldr
(załaduj rejestr) rejestrr2
o wartości w lokalizacji pamięci zapisanej wr3
+ 24 bajtach. Będąc zbyt leniwym, aby to sprawdzić: bardzo prawdopodobne jest położenieBSRR
.OR
r2
1024 == (1<<10)
r2
str
(zapisz) wynik w lokalizacji pamięci, z której czytaliśmy w pierwszym krokuBRR
adres.b
(rozgałęzienie) powrót do pierwszego kroku.Na początek mamy 7 instrukcji, a nie 3. To się
b
zdarza 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 żeb
zajmuje 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 cyklstr
zajmuje 2 cykleb
zajmuje 2 do 4 cykli. Wiemy, że musi to być liczba nieparzysta, więc tutaj musi zająć 3.Wszystko zgadza się z twoją obserwacją:
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
-O3
jak-O1
w mojej wersji kompilatora. Musisz to zrobić sam! Być może używasz starożytnej wersji GCC z nieoptymalną obsługą ARM.źródło
=
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?-O3
Błą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
uxth
jest 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 pojedynczyBSRR
rejestr 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ć.LDRB
iSTRB
wykonuje bajty do odczytu / zapisu w jednej instrukcji, nie?Te
BSRR
iBRR
rejestrów do ustawiania i zerowania poszczególnych bitów portów:Jak widać, czytanie tych rejestrów zawsze daje 0, a więc kod
robi to skutecznie
GPIOE->BRR = 0 | GPIO_BRR_BR_10
, ale optymalizator nie wie, że tak to generuje sekwencjęLDR
,ORR
,STR
instrukcje zamiast jednego sklepu.Możesz uniknąć kosztownej operacji odczytu-modyfikacji-zapisu, po prostu pisząc
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 przedwhile(1)
pętlą.źródło
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:
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.
źródło
Jest to zachowanie twojego kodu.
Powinieneś pisać do rejestrów BRR / BSRR, a nie czytać-modyfikować-pisać tak jak teraz.
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.
źródło
|=
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
gcc -funroll-loops
) mogą sobie radzić bardzo dobrze, a gdy są nadużywane (jak tutaj), mają odwrotny efekt, co chcesz.somePortLatch
kontroluje 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.