Pomiar wykorzystanych cykli mikrokontrolera AVR

Potrzebowałem na szybko oszacować złożoność czasową kilku algorytmów na mikrokontrolerze AVR, aby poznać ilość cykli jakie skonsumuje procesor do wykonania tych interesujących mnie fragmentów kodu. Ponoć symulator posiada taką funkcjonalność, ja niestety nie mam takich zabawek. Można byłoby na piechotę policzyć ilość taktów na podstawie dokumentacji i wygenerowanego kodu, ale to raczej byłoby karkołomnym zadaniem.

Lepiej wykorzystać do tego sam mikrokontroler i liczyć na żywo ilość przeoranych taktów procesora. Do tego celu najlepiej użyć licznika napędzanego systemowym zegarem. A idea jest bardzo prosta. Uruchamiamy licznik na czas wykonania mierzonego kodu, a po zakończeniu odejmując narzuty na obsługę timerów, otrzymujemy dokładną ilość cykli, jakie wymagane były do przetworzenia tego fragmentu kodu.

Napisałem sobie prosty kod w asemblerze wykonujący tą cała brudna robotę, w postaci 3 funkcji:

void StartTickCounter();
void StopTickCounter();
uint16_t GetTicks();

Idealnie w tym zadaniu spełni się 16-bitowy licznik, który zliczy maksymalnie 65535 taktów, co wydaje się wystarczające prawie we wszystkich przypadkach. Oczywiście, jeśli to za mało to nic nie stoi na przeszkodzie, aby podpiąć się pod overflow timera i liczyć aż do 32-bitów.

Funkcja StartTickCounter inicjalizuje i konfiguruje licznik, który uruchamia tuż przed samym wyjściem z funkcji, aby zminimalizować narzut dodatkowych instrukcji:

StartTickCounter:
	push r16
	clr r16
 
	; clear counter
	sts TCNT1H, r16
	sts TCNT1L, r16
 
	; start timer
	ldi r16, (1<<CS10)	; clk source
	sts TCCR1B, r16
 
	pop r16				; 2 cycles
	ret					; 4

Ten dodatkowy narzut w postaci 6 cykli, potrzebny na porządki na stosie i powrót z funkcji, oczywiście jest brany pod uwagę przy kalkulowaniu ostatecznego wyniku.

Podobnie wygląda to przy zatrzymywaniu licznika w funkcji StopTickCounter:

StopTickCounter:
	push r16			; 2 cycles
	clr r16				; 1
 
	; stop timer
	sts TCCR1B, r16		; 2
 
	pop r16
	ret

Tutaj narzutem jest wywołanie tejże funkcji (call - 4 takty) i kilka instrukcji zanim licznik się zatrzyma, czyli dokładnie 9 cykli procesora.

Wszystkie te dodatkowe takty odejmowane są od wyniku zwracanego przez funkcję GetTicks:

GetTicks:
	lds r24, TCNT1L
	lds r25, TCNT1H
 
	; subtract additional tics
	sbiw r24, StartCountTicksAdds + StopCountTicksAdds
 
	ret

Co ostatecznie czyni pomiar bardzo dokładnym. Można to sprawdzić za pomocą wbudowanej funkcji intrinsic do opóźnień w avr-gcc: __builtin_avr_delay_cycles.

[...]
#include "avr-tick-counter.h"
 
int main() {
 
	LcdInit();
	LcdGoto(0, 0);
 
	StartTickCounter();
 
	////////////////////
 
	__builtin_avr_delay_cycles(1000);
 
	//////////////////////	
 
	StopTickCounter();
 
	char buf[16];
	sprintf(buf, "cycles: %d", GetTicks());
	LcdWriteText(buf);
 
	while (true) {
		;
	}
 
	return 0;
}

Czysty interfejs w C jest dosyć prosty, w końcu to tylko 3 funkcje. Dla C++ możemy trochę bardziej się postarać i prócz tego standardowego z stylu C, dorzucić coś bardziej odpowiedniego w C++, upakowanego w jakieś sensownej przestrzeni nazw.

namespace Tick {
 
void StartCounter() {
	::StartTickCounter();
}
 
void StopCounter() {
	::StopTickCounter();
}
 
uint16_t Get() {
	return ::GetTicks();
}
 
} // namespace Tick

A do tego aż się prosi, aby dodać proste RAII na automatyczne zarządzanie licznikiem:

struct AutoCounter {
 
	AutoCounter() {
		StartCounter();
	}
 
	~AutoCounter() {
		StopCounter();
	}
 
};

aby w kodzie C++ nie musieć się bawić w ręczne odpalanie i zatrzymywanie pomiarów:

{
	Tick::AutoCounter ticks;
	//////////////////////
 
	__builtin_avr_delay_cycles(1234);
 
	////////////////////////
}

Całość kodu wrzuciłem na mojego githuba do gist-ów, dostępny jest tutaj do dedykowanego repo. Jeśli wymagana jest jakaś licencja, a warto wszystko jakaś opatrzać, to standardowo jest to MIT.

Może komuś się okaże się pomocny ten jakże bardzo prosty kod, szczególnie przy różnego rodzaju zabawach i eksperymentach. Mi się przydał głownie na potrzeby badania złożoności i czasu wykonania dzielenia pod AVR-kami, które niestety nie maja sprzętowej obsługi takich operacji.

Dodano 24-03-17 @ 00:10

Drobna aktualizacja. Kod się trochę w międzyczasie rozwinął, więc ostatecznie przeniosłem go z gista do pełnoprawnego github-owego repozytorium. Pozostała mi jeszcze głównie do zbadania i załatwienia pewna sprawa z dziwnymi dodatkowymi lub brakującymi cyklami wynikające z implementacji przerwania przepełnienia licznika…

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *