Opóźnienia i ich dokładność – delay.h
Jeśli jesteś tutaj nowy, możesz rozpocząć subskrypcję mojego kanału RSS. Dzięki temu nie przegapisz nowych wpisów. Dziękuję za wizytę!
Opóźnienia, to jedne z najczęściej wykorzystywanych funkcji w mikrokontrolerach.
Przez długi okres czasu tworzyłem takie funkcje dla każdej aplikacji praktycznie od zera, ponieważ chciałem aby były możliwie dokładne dla konkretnej częstotliwości pracy układu. Wiedziałem, że istnieją gotowe rozwiązania w bibliotece avr-libc, ale nie znałem ich dokładności i zamiast się zastanawiać czy zadziałają tak jak sobie tego życzę, tworzyłem na szybko procedurę dedykowaną.
Na szczęście znalazłem w sobie chęci i sprawdziłem ile warte są funkcje z avr-libc.
1. Funkcje
Funkcje o których mowa znajdują się w pliku util/delay.h z biblioteki avr-libc i są to:
void _delay_us(double _us)
Funkcja powoduje opóźnienie o_usmikrosekund. Maksymalne generowane opóźnienie wynosi 768us / F_CPU(w MHz). Jeśli podana wartość przekracza dopuszczalne maksimum, automatycznie wywoływana jest funkcja_delay_ms().void _delay_ms(double _ms)
Funkcja powoduje opóźnienie o_msmilisekund. Maksymalne generowane opóźnienie wynosi 262.14ms / F_CPU(w MHz). Jeśli podana wartość przekracza dopuszczalne maksimum, funkcja działa ze zmniejszoną rozdzielczością (0.1ms) generując opóźnienia do 6.5535s niezależnie od częstotliwości pracy mikrokontrolera.
Jak to działa?
Patrząc na powyższe deklaracje funkcji opóźniających, możemy zauważyć, że parametr w którym przekazujemy opóźnienie jest typu double. Mikrokontrolery AVR są układami 8-bitowymi i operują na 8-bitowych liczbach stałoprzecinkowych. Implementacja obliczeń na liczbach zmiennoprzecinkowych typu double wymaga zastosowania rozbudowanych procedur, które zajmują pamięć mikrokontrolera i czas potrzebny na ich wykonanie.
Na szczęście funkcje skonstruowane są w ten sposób, że wszelkie obliczenia wykonywane są przez kompilator i dlatego operacje na liczbach zmiennoprzecinkowych nie muszą być implementowane w AVRce.
Uwagi
- ważne jest, aby włączona była optymalizacja kodu. W innym przypadku powyższe funkcje opóźniające nie będą działały zgodnie z zamierzeniem,
- parametrami funkcji nie mogą być zmienne programu, a tylko i wyłącznie wartości stałe – znane w czasie kompilacji,
- kolejną ważną rzeczą jest konieczność prawidłowego zdefiniowania wartości F_CPU. Zapewnia to generowanie odpowiednich opóźnień dla konkretnej częstotliwości pracy układu. Ja definiuję tą wartość globalnie w pliku
makefilei uważam, że jest to najlepsze rozwiązanie – myślę, że nie trzeba tłumaczyć dlaczego.
2. Dokładność
Dokładność opóźnień generowanych przez te funkcje zależy od wartości opóźnienia i częstotliwości pracy mikrokontrolera.
_delay_us()
Dla _delay_us(), który korzysta z funkcji _delay_loop_1() z util/delay_basic.h realne generowane opóźnienie można obliczyć w następujący sposób:
- obliczamy wartość F_CPU / 3000000 * _us,
- zaokrąglamy wynik w dół (jęsli otrzymamy wartość 0, zmieniamy ją na 1),
- powyższy wynik mnożymy przez 3 / F_CPU.
Warto zauważyć, że najmniejsze możliwe opóźnienie wynosi 3 / F_CPU, co przy niewielkiej częstotliwości pracy układu przekłada się na brak możliwości uzyskania niewielkich opóźnień. Np. dla F_CPU = 1MHz najmniejsze opóźnienie wynosi 3us – trzeba o tym pamiętać.
Przykład:
Dla częstotliwości pracy mikrokontrolera F_CPU = 14,7456MHz i _us = 1, mamy:
- 14745600 / 3000000 * 1 = 4,9152
- zaokrąglamy w dół, czyli mamy wartość 4
- 4 * 3 / 14745600 = 0,8138us
Widzimy tu dość spory błąd wynikający z zaokrąglenia w dół liczby 4,9152. Lepszy wynik uzyskalibyśmy, gdyby zaokrąglenie przebiegało zgodnie z normami. Przy zaokrągleniu do 5, uzyskujemy: 1,0173us.
Jeśli zależy nam na dokładności, mamy dwie możliwości:
- dobieramy odpowiednio parametr
_us, by skorygować błąd zaokrąglania (w powyższym przypadku _us = 5 / 4,9152 = 1,0173), - modyfikujemy funkcję
_delay_us()zmieniając linię:double __tmp = ((F_CPU) / 3e6) * __us;
na:
double __tmp = ((F_CPU) / 3e6) * __us + 0.5;
Kompilator generuje następujący kod dla tej funkcji (r24 zawiera wyliczoną wartość po zaokrągleniu):
ldi r24, 0x04; loop: dec r24; brne loop;
_delay_ms()
Dla _delay_ms(), który korzysta z funkcji _delay_loop_2() z util/delay_basic.h realne opóźnienie oblicza się podobnie jak dla _delay_us():
- obliczamy wartość F_CPU / 4000 * _ms,
- zaokrąglamy wynik w dół,
- mnożymy razy 4 i dodajemy 1,
- powyższy wynik mnożymy przez 1 / F_CPU.
W tej funkcji także występuje problem z zaokrąglaniem, choć jego wpływ jest znikomy. Dla pewności można jednak zastosować rozwiązanie z modyfikacją kodu analogiczne jak dla _delay_us().
Kompilator generuje następujący kod dla tej funkcji (r24 oraz r25 zawierają wyliczoną wartość po zaokrągleniu):
ldi r24, 0xD4; ldi r25, 0x30; loop: sbiw r24, 0x01; brne loop;
_delay_ms() – zmniejszona rozdzielczość
Jeśli wartość parametru _ms przekracza 262.14ms / F_CPU(w MHz) (np. dla F_CPU = 14,7456MHz będzie to 17,7775ms) funkcja _delay_ms() działa w trybie zmniejszonej rozdzielczości. Zmniejsza się także jej dokładność. Realną wartość opóźnienia obliczamy następująco:
- obliczamy wartość F_CPU / 40000,
- zaokrąglamy wynik w dół,
- mnożymy razy 4,
- dodajemy 4,
- obliczamy _ms * 10 i zaokrąglamy w dół,
- powyższe wyniki mnożymy przez siebie,
- dodajemy 3,
- wynik mnożymy przez 1 / F_CPU.
Żeby to lepiej zrozumieć przeanalizujmy przykład dla częstotliwości pracy mikrokontrolera F_CPU = 10MHz i _ms = 100:
- 10000000 / 40000 = 250,
- zaokrąglamy wynik w dół – czyli nadal 250,
- mnożymy – mamy 1000,
- dodajemy 4 czyli mamy 1004,
- obliczamy 100 * 10 z zaokrągleniem w dół, co daje 1000,
- powyższe wyniki mnożymy przez siebie – 1004000,
- dodajemy 3 – 1004003,
- wynik mnożymy przez 0,1us, czyli opóźnienie wynosi 100,4003ms.
Jeśli zależy nam na większej dokładności, musimy odpowiednio skorygować wartość parametru _ms.
Dla powyższego przypadku, rozpatrując obliczenia od końca:
- 100ms * 10MHz = 100000,
- 100000 – 3 = 99997,
- 99997 / 1004 = 99,5986.
Wartość tą należy zaokrąglić ponieważ funkcja działa z rozdzielczością 0,1ms. Dla _ms = 99,6 realne opóźnienie wynosi 99,9984ms, a dla _ms = 99,7 będzie to 100,0988ms.
Błąd dla tej funkcji rośnie przy zwiększaniu opóźnienia i zmniejszaniu częstotliwości pracy układu i wynosi około _ms * 4% / F_CPU(w MHz).
Zmieniając nieznacznie kod, możemy bardzo poprawić dokładność. Ponieważ wewnętrzna pętla jest wykonywana n + 4 cykli, wystarczy zrównoważyć liczbę n odejmując od niej 4. W praktyce wygląda to tak – linia:
_delay_loop_2(((F_CPU) / 4e3) / 10);
zamieniona jest na:
_delay_loop_2(((F_CPU) / 4e3) / 10 - 1 + 0.5);
Taki zapis gwarantuje także prawidłowe zaokrąglanie.
Kompilator generuje następujący kod dla tej funkcji:
ldi r24, 0x7C; ldi r25, 0x15; ldi r18, 0x19; ldi r19, 0x00; loop2: movw r30, r18; loop: sbiw r30, 0x01; brne loop; sbiw r24, 0x01; brne loop2;
3. Podsumowanie
Obliczając realne opóźnienia generowane przez powyższe funkcje należy pamiętać, aby w pierwszej kolejności zastanowić się, która z funkcji zostanie wywołana dla danej wartości parametru wejściowego – a następnie dokonać stosownych wyliczeń.
Jeśli zależy nam na maksymalnej precyzji, korzystamy z liczników. W wielu zastosowaniach zdecydowanie wystarczy dokładność omawianych funkcji, zwłaszcza że jest ona naprawdę niezła, a z proponowanymi zmianami bliska ideału. Wersja ze zmianami dostępna poniżej.
| Plik: | delay.h |
|---|---|
| Wersja: | 1.0 |
| Z dnia: | 11/11/2008 |
| Rozmiar: | 5.7 KB |
Dobrze jest wiedzieć jak działa kod, który stosujemy i wykorzystywać go w sposób świadomy. Mam nadzieje, że przedstawiony tu tekst pomoże w oswojeniu się z funkcjami opóźniającymi dostępnymi w bibliotece avr-libc.

1 komentarz do “Opóźnienia i ich dokładność – delay.h”
Bardzo ciekawy artykuł! Warto by powiększyć ten dział i dodać więcej podobnych, bo na pewno przydadzą się innym użytkownikom. pozdrawiam