czwartek, 17 marca 2011

Ustawianie i zmiana częstotliwości taktowania mikrokontrolera w trakcie jego działania


Autor: Olek
Redakcja: Dondu

Artykuł jest częścią cyklu: Kurs AVR (Atmel): Drzaśkowy pamiętnik

W niektórych mikrokontrolerach AVR, zwłaszcza nowszych, możemy zmieniać częstotliwość taktowania w trakcie działania programu. Można dzięki temu zmniejszyć zużycie prądu pobieranego przez układ.

Nie trzeba także zmieniać bitów konfiguracyjnych (fuse-bity), co dla początkujących może być ryzykowne. Opisana niżej funkcjonalność występuje nie tylko w procesorach z rodziny XMega (o czym przeczytasz w tym artykule), ale także ATtiny oraz ATmega (niektóre z nich są wymienione w komentarzu pod niniejszym artykułem).

W artykule przedstawię sposób zmiany częstotliwości taktowania mikrokontrolera na przykładzie migania diodą za pomocą ATtiny13A, ale ta funkcjonalność występuje także w ATtiny13, czy ATtiny13V.


ATtiny13 - Schemat układu używanego w przykładzie.
Schemat układu używanego w przykładzie.


Zobaczmy najpierw sposób dystrybucji sygnału zegarowego w datasheet:


ATtiny13 - Dystrybucja sygnału zegarowego.
ATtiny13 - Dystrybucja sygnału zegarowego.


Tak więc za zmianę taktowania zegara systemowego odpowiedzialny jest blok "AVR Clock Control Unit". Z dalszego opisu dowiadujemy się, że:


ATtiny13 - Preskaler zegara taktującego.


sygnał zegarowy może być podzielony przez preskaler, którego dzielnik ustawiamy w rejestrze CLKPR oraz że podział ma wpływ na częstotliwość wewnętrznych sygnałów zegarowych portów I/O, przetwornika ADC, jednostki centralnej CPU oraz pamięci FLASH.

Zaglądamy więc do rejestru:


ATtiny13 - Rejestr CLKPR


i znajdujemy tam cztery bity preskalera CLKPS[3:0], których znaczenie jest pokazane w tabelce:


ATtiny13 - Dopuszczalne podzielniki preskalera zegara taktującego.


W rejestrze tym występuje jeszcze dodatkowy bit CLKPCE, a z  jego opisu:


ATtiny13 - Bit CLKPCE.


dowiadujemy się, że aby włączyć podział sygnału zegarowego za pomocą bitów CLKPS[3:0], musimy ustawić bit CLKPCE oraz że są specjalne zasady operowania bitami tego rejestru.

W dalszej części opisu rejestru CLKPR uzyskujemy informację, że operacja zmiany taktowania procesora jest czynnością dosyć krytyczną, stąd algorytm zmian składa się z dwóch etapów:




co rozumiemy następująco:
  1. W rejestrze CLKPR ustawiamy bit CLKPCE, jednocześnie ustawiając pozostałe bity na zero,
  2. W ciągu 4 cykli zegara ustawiamy w rejestrze CLKPR odpowiedni stopień prescalera za pomocą bitów CLKPS[3:0], jednocześnie wpisując zero do bitu CLKPCE.

Jeśli procedura nie wykona się w zadanym czasie (np. z powodu zgłoszenia przerwania), zmiany nie zostaną wprowadzone.


Dodatkowo w momencie dokonywania zmian w rejestrze według powyższego algorytmu nic nie może nam przeszkadzać, czyli przerwania muszą być zablokowane:




czyli musimy więc zadbać o atomowość wykonania tej procedury.

Co oznacza atomowość?

Najprościej opisując, to wykonanie jakichś czynności bez możliwości przerwania wykonywania tych czynności przez przerwania oraz zapewnienie, że dane obrabiane nie ulegną zmianie przez inny proces niż proces wykonywany atomowo (patrz Wikipedia).

W naszym przypadku najlepiej zrobić to w ten sposób:

#include <util/atomic.h>

ATOMIC_BLOCK(ATOMIC_RESTORESTATE)
{
  CLKPR = (1<<CLKPCE);
  CLKPR = (0<<CLKPCE) | (0<<CLKPS3)|(0<<CLKPS2)|(0<<CLKPS1)
         |(0<<CLKPS0); //Prescaler division = 1
}

Używanie makr cli() oraz sei() nie jest potrzebne ponieważ odpowiedni kod wykona za nas kompilator. Dzięki zamknięciu kodu w blok ATOMIC_BLOCK mamy pewność, że procedura wykonana się we właściwym reżimie czasowym i przy zablokowanych przerwaniach.

Należy zwrócić uwagę, że używaliśmy operacji przypisania =, zamiast popularnej sumy bitowej |=. Ta pierwsza generuje krótszy kod assemblerowy, co w tym wypadku jest bardzo istotne zgodnie z punktem nr 2 powyżej!

Stopnie podziału są określone dla danego mikrokontrolera a nasze pokazuje powyższa tabelka. Aby nie trzeba było zaglądać co chwilę do datasheet, można zdefiniować odpowiednie wartości:

#define CLKDF_1  (0<<CLKPS3)|(0<<CLKPS2)|(0<<CLKPS1)|(0<<CLKPS0)
#define CLKDF_2  (0<<CLKPS3)|(0<<CLKPS2)|(0<<CLKPS1)|(1<<CLKPS0)
#define CLKDF_4  (0<<CLKPS3)|(0<<CLKPS2)|(1<<CLKPS1)|(0<<CLKPS0)
#define CLKDF_8  (0<<CLKPS3)|(0<<CLKPS2)|(1<<CLKPS1)|(1<<CLKPS0)
#define CLKDF_16 (0<<CLKPS3)|(1<<CLKPS2)|(0<<CLKPS1)|(0<<CLKPS0)
#define CLKDF_32 (0<<CLKPS3)|(1<<CLKPS2)|(0<<CLKPS1)|(1<<CLKPS0)
#define CLKDF_64 (0<<CLKPS3)|(1<<CLKPS2)|(1<<CLKPS1)|(0<<CLKPS0)
#define CLKDF_128 (0<<CLKPS3)|(1<<CLKPS2)|(1<<CLKPS1)|(1<<CLKPS0)
#define CLKDF_256 (1<<CLKPS3)|(0<<CLKPS2)|(0<<CLKPS1)|(0<<CLKPS0)

Powyższe wartości są odpowiednie dla ATtiny13 oraz ATmega48/88/168/328p(a), dla innych można zdefiniować własne w oparciu o datasheet.

Ponieważ taktowanie procesora zostaje ustalone dynamicznie podczas działania programu, nie możemy już bez zastanowienia polegać na zdefiniowanej wartości F_CPU.

Aby zobaczyć działanie powyższej funkcjonalności, można zmontować układ wg schematu z początku artykułu, i wgrać poniższy program:

/*
 * tiny13_blink.c
 *
 * Created: 2014-01-04 12:38:20
 *  Author: Olek
 */ 


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

#define CLKDF_1  (0<<CLKPS3)|(0<<CLKPS2)|(0<<CLKPS1)|(0<<CLKPS0)
#define CLKDF_2  (0<<CLKPS3)|(0<<CLKPS2)|(0<<CLKPS1)|(1<<CLKPS0)
#define CLKDF_4  (0<<CLKPS3)|(0<<CLKPS2)|(1<<CLKPS1)|(0<<CLKPS0)
#define CLKDF_8  (0<<CLKPS3)|(0<<CLKPS2)|(1<<CLKPS1)|(1<<CLKPS0)
#define CLKDF_16 (0<<CLKPS3)|(1<<CLKPS2)|(0<<CLKPS1)|(0<<CLKPS0)
#define CLKDF_32 (0<<CLKPS3)|(1<<CLKPS2)|(0<<CLKPS1)|(1<<CLKPS0)
#define CLKDF_64 (0<<CLKPS3)|(1<<CLKPS2)|(1<<CLKPS1)|(0<<CLKPS0)
#define CLKDF_128 (0<<CLKPS3)|(1<<CLKPS2)|(1<<CLKPS1)|(1<<CLKPS0)
#define CLKDF_256 (1<<CLKPS3)|(0<<CLKPS2)|(0<<CLKPS1)|(0<<CLKPS0)


void clock_prescaler_select(uint8_t division_factor);

int main(void)
{
 DDRB |= (1<<PB3);
 PORTB = 0x00;

 while(1)
 {
  clock_prescaler_select(CLKDF_1);     //ustaw preskaler F_CPU na 1
                                       //czyli praca z 9,6MHz
  for (uint8_t i=0;i<20;i++)
  {
   _delay_ms(500);                     //okres migania diody LED ok. 1/8s
   PORTB ^= (1<<PB3);
  }
  clock_prescaler_select(CLKDF_8);     //ustaw preskaler F_CPU na 8
                                       //czyli praca z 1,2MHz
  for (uint8_t i=0;i<20;i++)
  {
   _delay_ms(500);                     //Okres migania diody LED ok. 1s
   PORTB ^= (1<<PB3);
  }  
 }
}

void clock_prescaler_select(uint8_t division_factor)
{
 ATOMIC_BLOCK(ATOMIC_RESTORESTATE)
 {
  CLKPR = (1<<CLKPCE);
  CLKPR = division_factor;
 }
}


A oto efekt końcowy:



Mikrokontrolery Xmega oferują znacznie bardziej zaawansowane możliwości dystrybucji sygnału zegarowego poprzez rejestry, o czym napisał Dominik Leon Bieczyński w artykule Kurs XMega (07): Sygnały zegarowe.





Ustawienie F_CPU podczas kompilacji vs zmiana częstotliwości taktowania

Podczas przygotowania projektu informujesz kompilator dla jakiej częstotliwości taktowania mikrokontrolera ma zostać przygotowany program. Ustawienia te wykonujesz w opcjach projektu - patrz artykuł: F_CPU – gdzie definiować?

Kompilator musi znać częstotliwość taktowania mikrokontrolera po to, by odpowiednio przygotować kod wynikowy tak, by np. opóźnienia używane w programach były takie jak sobie tego życzysz. W przeciwnym wypadku może posypać się wiele rzeczy np. komunikacja z LCD, itp.

Powstaje więc pytanie:

Jak ma się zmiana częstotliwości taktowania do częstotliwości ustawionej podczas kompilacji programu?

Odpowiedź jest prosta. Kompilator nie wie:
  • kiedy będziesz miał zamiar przełączyć programowo mikrokontroler na inną częstotliwość taktowania,
  • który fragment programu z jaką częstotliwością będzie wykonywany,
  • czy ten sam fragment programu może być realizowany z różnymi prędkościami,
  • itd.
dlatego też, cała odpowiedzialność za zmianę częstotliwości mikrokontrolera spada na Ciebie i to Ty musisz odpowiednio napisać program. Oznacza to ni mniej, ni więcej, że program musi uwzględniać różne prędkości działania mikrokontrolera.

Takie programy nie są już tak łatwe do pisania jak program dla stałej częstotliwości pracy mikrokontrolera, ale rewanżują się na przykład zmniejszonym zużyciem energii, przez co dłuższą pracą na baterii, czy akumulatorze. Więcej na ten temat znajdziesz w artykułach z cyklu Bateria zasila mikrokontroler




Fuse bit CKDIV8

Ze zmianą preskalera zegara jest związany także bit konfiguracyjny (fuse bit) CKDIV8. Informację na jego temat znajdujemy w tabeli:


ATtiny13 - Fusebity - bit CKDIV8


gdzie dowiadujemy się, że odpowiada on za podział sygnału zegarowego przez osiem, i że fabrycznie jest on zaprogramowany. Dokładniejszą informację znajdziemy jednak w części dot. zegara:




gdzie mamy potwierdzenie, że domyślnie (fabrycznie) mikrokontroler dostarczany jest z ustawieniem źródła zegara na wewnętrzny generator RC o częstotliwości 9,6MHz i włączonym domyślnie preskalerze ustawionym na podział przez osiem.

W związku z tym nowy mikrokontroler jest ustawiony fabrycznie na zegar o częstotliwości:



Jak więc ma się bit konfiguracyjny, do możliwości programowej zmiany częstotliwości F_CPU?

Ustawienie fuse bitu CKDIV8 powoduje po prostu po włączeniu zasilania ustawienie bitów CLKPS[3:0] w rejestrze CLKPR na wartość odpowiadającą preskalerowi 8.

Oznacza to ni mniej ni więcej, że pomimo iż ustawimy fusebit CKDIV8, możemy w trakcie działania programu dowolnie zmieniać wartość preskalera za pomocą bitów CLKPS[3:0].





Uwaga na napięcie zasilania!

Zastanówmy się na przypadkiem, w którym mamy na przykład fabrycznie ustawiony wewnętrzny generator RC na 9,6MHz oraz ustawiony fuse bit CKDIV8, czyli jak policzyłem wyżej faktycznie nasz mikrokontroler pracuje z częstotliwością 1,2MHz.

Załóżmy, że nasz ATtiny13A zasilany jest anpięciem 1,8V.

Co się stanie, gdy zapragniemy zmienić programowo preskaler zegara systemowego z 8 na 1 co oznacza, że mikrokontroler zostanie zmuszony do pracy z częstotliwością 9,6MHz?

Niestety w takiej sytuacji mikrokontroler może przestać działać prawidłowo ponieważ:


ATtiny13 - Wykres maksymalnych częstotliwości zegara taktującego w zależności od napięcia zasilania.


przy napięciu 1,8V ATtiny13A nie może pracować z częstotliwością większą niż 4MHz.

Dlatego też między innymi wspomina o tym dokumentacja w części dot. systemu zegara:




16 komentarzy:

  1. Zawsze myślałem, że CKDIV8 blokuje możliwość zmiany częstotliwości mikrokontrolera na stałe - w końcu to fusebit. A tu niespodzianka.

    OdpowiedzUsuń
  2. Bardzo fajny artykłu, nie wiedziałem otakich możliwościach.

    OdpowiedzUsuń
  3. Czy jest gdzieś jakiś wykaz AVRów, które mają takie możliwości?

    OdpowiedzUsuń
    Odpowiedzi
    1. Wszystkie nowsze mają taką możliwość (możesz założyć, że te, które mają 3-cyfrowe oznaczenie mają preskaler zegara). Wiele starszych też takie możliwości posiada. Jednak, jeśli z jakiegoś powodu interesuje cię programowa zmiana częstotliwości taktowania, to o wiele lepszym wyborem z AVR jest XMEGA. Preskalery są bardziej elastyczne, jest też wiele źródeł zegara do wyboru. W zależności od potrzeb można wybierać stabilniejsze, lub cechujące się niższym poborem energii.

      Usuń
  4. I znowu coś nowego się dowiedziałem - thx!

    OdpowiedzUsuń
  5. Czy w procesorach takich jak Atmega 8, 16, 32 też można to zrobić?

    OdpowiedzUsuń
    Odpowiedzi
    1. Szukaj w datasheet bitu CLKPCE. Mają go:
      - ATmega48/88/168
      - ATmega48A/PA/88A/PA/168A/PA/328/P
      - ATmega16/32 ale wersja U4
      - ATmega164P/324P/644P
      - ATmega164P/324P/644P
      - ATmega640/1280/1281/2560/2561
      - ATtiny13
      - ATtiny24/4484
      - ATtiny25/45/85
      - ATtiny2313
      - itd.

      Usuń
  6. Witam
    Czy w funkcji "clock_prescaler_select" z zamieszczonego przykładu nie powinien być
    od razu zerowany bit "CLKPCE" w rejestrze "CLKPR"

    OdpowiedzUsuń
    Odpowiedzi
    1. Ponieważ do rejestru CLKPR wpisujemy nową wartość za pomocą operatora = a nie |=, poprzednia wartość jest nadpisywana. Więc i bit CLKPCE zostanie wyzerowany, gdyż nie jest on ustawiony w zmiennej division_factor.

      Usuń
  7. Witam.
    Chciałem sprawdzić działanie w/w programu, skopiowałem i po kompilacji mam następujący komunikat:
    Zmiana_prescalera.c:(.text.startup.main+0x8): undefined reference to `clock_prescaler_select'
    Zmiana_prescalera.c:(.text.startup.main+0x2a): undefined reference to `clock_prescaler_select'
    Zmiana_prescalera.c:(.text.startup.main+0x48): undefined reference to `clock_prescaler_select'

    Dlaczego.
    Dopisałem trzeci trzeci prescaler.
    Eclipse Mars.

    OdpowiedzUsuń
    Odpowiedzi
    1. Czy dał Pan przed wywołaniem deklarację funkcji? :
      void clock_prescaler_select(uint8_t division_factor);

      Usuń
    2. Ja nic nie zmieniałem w programie, który jest pokazany na początku.

      void clock_prescaler_select(uint8_t division_factor) - jest

      Usuń
    3. Niestety nie korzystam z Eclipse, więc nie mam jak powtórzyć przypadku, natomiast sprawdziłem ponownie kompilację w Avr-studio 6.2 (Avr toolchain w wersji 3.4.1061) i wszystko się ładnie kompiluje.
      Komunikat błędu sugeruje niemożność znalezienia funkcji w programie. Czy na pewno skopiował Pan cały listing? Czy nie pozmieniały się znaki w kodzie? Czy kompilator jest poprawnie skonfigurowany?

      Podobny błąd udało mi się wygenerować usuwając definicję funkcji (ostatnie linie listingu) - czy nie zapomniał ich Pan skopiować?

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

    OdpowiedzUsuń
  9. Listing skopiowany jest poprawnie.
    Kompilator jest chyba OK, do tej pory wszystko kompilował poprawnie.
    Powalczę jeszcze.
    Dzięki za podpowiedzi.

    OdpowiedzUsuń
  10. Pobrałem nowego Eclipsa i nowy listing wszystko ruszyło.

    OdpowiedzUsuń