poniedziałek, 4 kwietnia 2011

Funkcje opóźnienia _delay_xx()

Autor: tmf
Redakcja: Dondu

Funkcje opóźnień w kompilatorze GCC dla mikrokontrolerów AVR sprawiają problemy początkującym.
Powtarzającym się problemem na różnych forach są kłopoty związane z funkcjami z nagłówka <util/delay.h>. Często pojawiają się problemy związane z nieprawidłowym odmierzaniem czasu przez te funkcje. 

Niestety równie często pojawiają się zupełnie błędne podpowiedzi, że problemem jest przekroczenie maksymalnej wartości argumentu funkcji _delay_xx().


Co powinieneś wiedzieć?

Błędne odmierzanie czasu przez funkcje z nagłówka delay.h jest spowodowane głównie trzema czynnikami:
  1. Brakiem lub błędnie zdefiniowanym symbolem F_CPU zawierającym prędkość taktowania mikrokontrolera w Hertz'ach.
  2. Błędnym ustawieniem fusebitów, w efekcie procesor jest taktowany inną częstotliwością niż nam się wydaje.
  3. Brakiem włączonej optymalizacji.


Problem F_CPU został szerzej omówiony w artykule: F_CPU – gdzie definiować? Warto go przeczytać, zanim przejdziesz przez resztę tego artykułu.

Z kolei problem optymalizacji (a raczej jej braku) dotyczy głównie użytkowników AVR Studio 5.x i jego następcy Atmel Studio – przejrzyj więc cykl artykułów o tym środowisku: Atmel Studio - zintegrowane IDE

Po lekturze wskazanych artykułów możemy kontynuować.

sweter_007
... używając funkcji _delay_ms(argument) oraz _delay_us(argument) należy zwracać uwagę na maksymalną wartość argumentu dla danego zegara i nie należy jej przekraczać:
_delay_ms(argument) - The maximal possible delay is 262.14 ms / F_CPU in MHz.
_delay_us(argument) - The maximal possible delay is 768 us / F_CPU in MHz.

Czy aby na pewno sweter_007 miał rację?

Rozwiejmy więc bardzo popularny mit na temat funkcji _delay_xx() mówiący, iż po przekroczeniu pewnej wartości argumentu odmierzany czas jest błędny. Nic bardziej mylnego.

Mit ten najprawdopodobniej wziął się z niedokładnego czytania dokumentacji AVR-LIBC, w której znajdujemy to co zacytował sweter_007:

The maximal possible delay is 262.14 ms / F_CPU in MHz.

czyli, że maksymalny czas opóźnienia (dla funkcji _delay_ms) wynosi 262.14 ms / F_CPU w MHz.
Problem w tym, że osoby powołujące się na ten zapis najpewniej nie przeczytały ciągu dalszego:

When the user request delay which exceed the maximum possible one, _delay_ms() provides a decreased resolution functionality. In this mode _delay_ms() will work with a resolution of 1/10 ms, providing delays up to 6.5535 seconds (independent from CPU frequency).


czyli, że kiedy użytkownik przekroczy podany zakres, funkcja _delay_ms() będzie działać ze zmniejszoną rozdzielczością. W tym trybie _delay_ms() będzie działać z rozdzielczością 1/10 ms, umożliwiając opóźnienia do 6,5535 sekundy (niezależnie od częstotliwości taktowania mikrokontrolera). Sześć i pół sekundy to całkiem sporo.

Wynika z tego jasno, że argumentem funkcji może być praktycznie dowolny czas, jednak po przekroczeniu wartości 262.14 ms / F_CPU rozdzielczość odmierzanego czasu jest ograniczona. Nie ma to jednak praktycznie żadnego znaczenia – przy tak długich oczekiwaniach, czymże jest 100 μs?


AVR-GCC i AVR-libc (wersja powyżej 1.7)

Jednak w nowym kompilatorze AVR-GCC i AVR-libc (wersja powyżej 1.7) czai się jeszcze jeden problem, którego nie było we wcześniejszych wersjach biblioteki. Funkcje _delay_xx() realizowane są przy pomocy wbudowanej w kompilator GCC funkcji:

__builtin_avr_delay_cycles(unsigned long)

Jak widzimy jej argumentem jest zmienna o typie unsigned long, która dla AVR ma długość 32 bitów, stąd też maksymalna wartość opóźnienia wynosi 4294967.295 ms/ F_CPU [w MHz]. Po przekroczeniu tej wartości, wyliczone opóźnienie osiąga wartość maksymalną, tzn. mniej więcej 2^32 taktów zegara - mniej więcej, bo dochodzi parę taktów na organizację pętli, ale to bez znaczenia przy takiej liczbie :-)

Jednak są to opóźnienia sięgające prawie 5 minut – trudno sobie wyobrazić jakikolwiek sensowny i w miarę poprawnie napisany program, w którym występują takie opóźnienia!

No to jeden problem mamy rozwiązany – wiemy, że funkcje _delay_ms() można stosować z prawie dowolnymi opóźnieniami.


Ułamkowe opóźnienia

Od powyższej zasady są wyjątki - popatrzmy na kod funkcji _dalay_us():
void
_delay_us(double __us)
{
 uint8_t __ticks;
 double __tmp = ((F_CPU) / 3e6) * __us;
 if (__tmp < 1.0)
  __ticks = 1;  //ustaw na sztywno, gdy żądano zbyt krótkiego opóźnienia
 else if (__tmp > 255)
 {
  _delay_ms(__us / 1000.0);
  return;
 }
 else
  __ticks = (uint8_t)__tmp;
 _delay_loop_1(__ticks);
}

Gdy przeanalizujesz kod okazuje się, że w momencie, gdy z obliczeń zmiennej pomocniczej __tmp wyjdzie, że jest mniejsza od jeden, to przyjmowane jest minimalne opóźnienie, czyli jeden.

Jest to oczywiste, bo funkcja pomocnicza _delay_loop_1(__ticks) nie może zostać wykonana w ułamkowej części tylko w częściach całkowitych.

Podobnie jest w definicji funkcji _delay_ms(), której kod załączm poniżej.


Zmienne opóźnienia

Drugim częstym problemem jest pytanie o to, jak uzyskać zmienne opóźnienie? Początkujący często wykorzystują nasuwające się rozwiązanie, polegające na zastosowaniu zmiennej jako argumentu funkcji opóźniającej:

#include <avr/io.h>
#include <util/delay.h>

int a;

int main(void)
{
  if(PORTA) a=200; else a=400;
  _delay_ms(a);

  while(1)
  {
    //TODO:: Please write your application code
  }
}

W powyższym przykładzie opóźnienie ma zależeć od stanu portu A mikrokontrolera. Powyższy przykład jakkolwiek prosty jest bardzo pouczający.

Po próbie jego skompilowania, możemy uzyskać dwojakie rezultaty. Kompilując go przy pomocy toolchaina z pakietu WinAVR uzyskamy kod o sporej długości – około 3852 bajty (wartość może się nieco różnić w zależności od wybranego mikrokontrolera i innych opcji).

Jeśli tak prosty program jest kompilowany do kodu o długości prawie 4kB to można się załamać, prawda? Ale zamiast zwalać winę na kompilator zastanówmy się co się stało. W tym celu warto zajrzeć do definicji funkcji _delay_ms():


void _delay_ms(double __ms)
{
  uint16_t __ticks;
  double __tmp = ((F_CPU) / 4e3) * __ms;
  if (__tmp < 1.0)__ticks = 1; //ustaw, gdy żądano zbyt krótkiego opóźnienia
  else if (__tmp > 65535)
  {
    //__ticks = requested delay in 1/10 ms
    __ticks = (uint16_t) (__ms * 10.0);

    while(__ticks)
    {
      // wait 1/10 ms
      _delay_loop_2(((F_CPU) / 4e3) / 10);
      __ticks --;
    }
    return;
  }
  else __ticks = (uint16_t)__tmp;

  _delay_loop_2(__ticks);
}

Dla nieco bardziej doświadczonych osób istota problemu już jest oczywista – obliczenia na zmiennych typu double – a więc wykorzystanie arytmetyki zmiennopozycyjnej.

Rzut okiem do pliku .map i .lss potwierdza nasze przypuszczenia – do programu zostały dołączone funkcje realizujące pewne operacje na zmiennych typu double (float).

Dlaczego? Ano dlatego, że dla zmiennej nie jest możliwe wyliczenie wartości na etapie kompilacji. Jeśli jako argument funkcji opóźniającej podamy stałą to wszystkie obliczenia zostaną wykonane na etapie kompilacji i nie będzie potrzeby dołączania funkcji obsługi liczb zmiennopozycyjnych.

Jeśli zmienną w wywołaniu funkcji _delay_ms() zamienimy na stałą, to cały program skróci się do 222 bajtów!

No dobrze, jak więc otrzymać zmienne opóźnienia?
Dosyć prosto – poprzez wielokrotne wywołanie funkcji opóźniającej ze stałym argumentem:

#include <avr/io.h>
#include <util/delay.h>

_delay_ms_var(uint16_t a)
{
  while(a--)
  {
    _delay_ms(1);
  }
}

int a;

int main(void)
{
  if(PORTA) a=200; else a=400;
  _delay_ms_var(a);
  
  while(1)
  {
    //TODO:: Please write your application code
  }
}

Tym razem program po kompilacji ma tylko 286 bajtów. Niezły zysk, prawda?

Zamiast funkcji można zadeklarować definicję pętli:

czerstwy22
dondu, jesteś wielki, teraz nie używa ramu i spokojnie wejdzie w Attiny13 (238 bajtów). Za diabła bym na to nie wpadł. Dzięki teraz definicja wygląda tak:
#define czekaj for(i=0;i<czas;i++) _delay_ms(1);
i przed główną pętlą zadeklarowałem i.


Z reguły lepszym rozwiązaniem jest definiowanie funkcji i jej wywoływanie w odpowiednich miejscach kodu, ale jeżeli już chcemy używać definicji, to sugerowałbym użycie takiej konstrukcji:
#define czekaj(czas) for(int i=0;i<(czas);i++) _delay_ms(1);



Atmel Studio vs _delay_ms(a)

Ale to nie wszystko. Rezultaty takie jak pokazane powyżej uzyskamy kompilując program przy pomocy pakietu narzędzi z WinAVR. Jednak użytkownicy Atmel Studio w ogóle nie skompilują powyższych programów, jeśli argumentem funkcji opóźniającej będzie zmienna. Próba kompilacji:

_delay_ms(a);

Zakończy się błędem:

Kompilator:
Error 1 __builtin_avr_delay_cycles expects an integer constant.


Dlaczego? Ano dlatego, że nowsze wersje AVR-libc wykorzystują do relizacji opóźnień wbudowaną w kompilator wspomnianą wcześniej funkcję:

__builtin_avr_delay_cycles(unsigned long)

Funkcja ta wymaga, aby wartość argumentu była znana na etapie kompilacji programu, nie można więc w argumencie funkcji _delay_xx() stosować zmiennych.

Przy okazji uchroni nas to przed niezamierzonym znacznym wzrostem objętości kodu wynikowego.


Liczby zmiennopozycyjne w _delay_ms()

Jest jeszcze jeden problem - czy argumentem funkcji _delay_ms() może być liczba zmiennopozycyjna i czy nie wydłuży to wygenerowanego kodu?

W świetle pokazanych wcześniej przykładów, odpowiedź jest prosta – tak, może być i nie, nie wydłuży to kodu. Dlaczego? Ponieważ podana stała zostanie wyliczona na etapie kompilacji programu, w efekcie w finalnym kodzie żadne obliczenia zmiennopozycyjne nie będą wykonywane.


Warning'i

Warto jeszcze wspomnieć o najczęściej sygnalizowanym problemie dot. niewłaściwego użycia biblioteki delay.h. W czasie kompilacji możesz otrzymać pojedynczo lub oba warning'i (ostrzeżenia), które ostrzegają o problemach uniemożliwiających prawidłowe działanie twojego programu. Są nimi:

1. Brak definicji F_CPU


Kompilator:
c:/../avr/include/util/delay.h:85:3: warning: #warning "F_CPU not defined for <util/delay.h>"

Ten warning oznacza, że nie zdefiniowałeś prawidłowo (w kompilatorze) częstotliwości zegara taktującego Twój mikrokontroler, przez co kompilator nie wie jak ma policzyć opóźnienia.

Co więcej, jeżeli nie zdefiniujesz F_CPU, a jednocześnie użyjesz bibliotekę delay.h, to standardowo F_CPU ustawiany jest na 1MHz:

#ifndef F_CPU
/* prevent compiler error by supplying a default */
# warning "F_CPU not defined for "
# define F_CPU 1000000UL
#endif

Ostry23
O kurczę, rzeczywiście. Nie wiedziałem, że coś takiego tam siedzi.

Co zrobić? Należy zdefiniować F_CPU. Więcej na ten temat znajdziesz tutaj: F_CPU – gdzie definiować?


2. Nie włączona optymalizacja


Kompilator:
c:/../avr/include/util/delay.h:90:3: warning: #warning "Compiler optimizations disabled; functions from <util/delay.h> won't work as designed"

Ten warning ostrzega Ciebie o tym, że nie masz włączonej optymalizacji. Skutek tego jest taki, że kompilator nie potrafi zbudować prawidłowo kodu wynikowego zawierającego funkcje z biblioteki delay.h

Co zrobić? Należy włączyć optymalizację kodu wynikowego. 
Więcej na ten temat tutaj:

Oczywiście to nie wszystkie blaski i cienie funkcji delay. 
Inne znajdziesz w książce „Mikrokontrolery AVR – od podstaw do zaawansowanych aplikacji”.

10 komentarzy:

  1. "6,5535 sekundy"
    A w przypadku przekroczenia tej długości?

    OdpowiedzUsuń
  2. W artykule jest odpowiedź na to pytanie.
    Z drugiej strony jeśli ktoś przekracza 5 minut (to limit dla nowego AVR-libc), czy nawet 6 sekund to znaczy, że coś jest mocno nie tak z programem i należy się zastanowić nad jego zmianą.

    OdpowiedzUsuń
  3. Jednego nie mogę zrozumieć. Dlaczego F_CPU dzielimy przez 4e3 tam gdzie mamy ms zamiast przez 1e4?

    OdpowiedzUsuń
    Odpowiedzi
    1. Wynika to z czasów opóźnienia wprowadzanych przez wewnętrzne funkcje biblioteki. Ne ma co wnikać - _delay_ms i _delay_us są realizowane przez AVR-libc, dla programisty ważny jest tylko ich prototyp.

      Usuń
    2. Witam wszystkich.Mam pytanko.Jestem początkujący i dopiero zaczynam więc jak coś głupiego napisze to proszę się nie złościć.A gdybym chciał uzyskać opóźnienie w granicach jednej godziny? chce podłączyć buzerek żeby co godzine dawał znać.Pytam z czystej ciekawości.

      Usuń
    3. W takim przypadku WYBIJ SOBIE Z GŁOWY użycie funkcji delay bo to droga do nikąd. Wystukaj w google hasło "timery programowe".

      Usuń
    4. Nie martw się o poziom pytań, to blog kierowany do początkujących :-)

      Napisz dokładnie co chcesz osiągnąć, czyli opisz dokładniej swój projekt. Podaj także, jakiej dokładności w odliczaniu tej godziny oczekujesz.

      Usuń
    5. Jak rozumiem w czasie tej godziny procesor najlepiej gdyby był uśpiony? Jak już koledy powyżej napisali w takiej sytuacji delay jest trochę bez sensu, ale zawsze możesz np. 60 razy zapętlić delay po 1 minutę i masz 60 minut. Bez sensu, ale proste. Lepiej wziąć procka z RTC, zaprogramować odpowiednio timer i uśpić wszystko oprócz RTC. Po godzinie ci procka wybudzi i problem z głowy.

      Usuń
    6. Ten komentarz został usunięty przez autora.

      Usuń
    7. Zapytałem tylko z czystej ciekawości ponieważ dopiero zacząłem nauke jezyka C i sie zastanawiałem czy jest jakas inna funkcja.Wszędzie dużo pojęc niezrozumiałych dla początkującego i nauka idzie mi to bardzo opornie ale nie poddaję się bo jest to bardzo interesujące.

      Usuń