LinkedIn YouTube Facebook
Szukaj

Wstecz
Artykuły

Mikrokontrolery AVR XMEGA w praktyce, część 25. DMA i peryferia

Przesyłanie tablic do peryferiów

Poznaliśmy już elementarne podstawy działania DMA – czas najwyższy przejść do praktycznych zastosowań tego fantastycznego układu. Tym razem DMA będzie pobierać dane z tablicy i przesyłać je do diod LED portu X na płytce eXtrino XL, tworząc prostą animację. Rozwiązanie to będzie zrealizowane całkowicie sprzętowo – wystarczy raz skonfigurować poszczególne peryferia, puścić je w ruch, a potem wszystko będzie działo się automatycznie.

Schemat blokowy połączenia peryferiów pokazano na rysunku 1, natomiast kod programu opisywanego w tym ćwiczeniu zawiera listing 1. Dyrygentem naszej orkiestry będzie timer E0, wyznaczający cykl o okresie około 100ms. Podczas tego cyklu procesor musi wykonać trzy ważne zadania, związanie z tym, że PORTX w płytkach eXtrino XL jest sterowany przez SPI (co zostało omówione w odcinku 17):

  1. Ustawienie pinu CS portu X w stan niski, co uaktywnia układ slave SPI
  2. Rozpoczęcie przesyłania bajtu danych,
  3. Ustawienie pinu CS portu X w stan wysoki, co spowoduje przedstawienie przesłanego bajtu na diodach LED.

Żeby bardziej obrazowo przedstawić sposób, w jaki zachowują się sygnały na magistrali SPI, przedstawiam zdjęcie z oscyloskopu na rysunku 2.

 

Rys. 1. Schemat połączeń między układami peryferyjnymi

 

Rys. 2. Oscylogram przedstawiający przebiegi na liniach MOSI oraz CS

 

Aby cykl pracy wynosił 100ms, musimy ustalić preskaler timera na 1024 oraz ustawić rejestr PER na 200. Timery zostały dokładniej omówione w odcinku 11.

Zacznijmy od omówienia sterowania pinem CS portu X, które najlepiej jest zrobić przy pomocy funkcji Capture/Compare (potocznie zwanej PWM, choć nie każde zastosowanie CC to jest PWM). Fizycznie CS na płytce eXtrino XL poprowadzony jest do pinu E6 procesora, ale timer E0 ma dostęp do pinów E0-E3, a timer E1 może sterować pinami E4-E5. Co tu robić? Na szczęście projektanci XMEGA przewidzieli taki przypadek i umożliwili remapowanie pinów w portach. Możemy wybrane wyjścia timera przenieść z pinów E0-E3 na E4-E7. Wystarczy zatem przenieść tylko E6, który jest powiązany z kanałem C funkcji Captue/Compara (stąd CCC). Remapowanie kanału C timera można włączyć wpisując PORT_TC0C_bm do rejestru PORTE.REMAP.

Ustawienie rejestru TCE0.CCC na wartość 198, kiedy PER jest ustawiony na 200, umożliwi uzyskanie krótkotrwałego stanu niskiego, potrzebnego podczas przesyłania danych przez SPI. Przez większość czasu na pinie E6 będzie stan wysoki, a w chwili kiedy licznik timera zrówna się z wartością 198 (=CCC), pin E6 zostanie ustawiony w stan niski, co będzie trwało aż do osiągnięcia przez timer wartości 200 (=PER).

W programie wykorzystujemy bibliotekę extrino_portx.h (dostępnej w plikach źródłowych na dole strony). Zawiera ona definicję PORTX_AUTOREFRESH, która może przyjmować wartość 1 lub 0, w zależności od tego czy chcemy, bo PORTX odświeżał się automatycznie z wykorzystaniem przerwań czy nie. Ponieważ odświeżaniem zajmuje się timer, musimy do wspomnianej definicji wpisać 0.

Jako drugie wyjście Capture/Compare timera wykorzystamy CCA (tym razem jest to zupełnie dowolne, można wybrać inny kanał). Wyjście to poprowadzimy do DMA przez kanał 0 systemu zdarzeń. Wyzwalaczem DMA mogą być kanały 0, 1 i 2. Po otrzymaniu sygnału wyzwalającego, DMA automatycznie skopiuje kolejną komórkę z tablicy source[] i przeniesie ją do interfejsu SPI, który natychmiast zacznie transmisję do portu X.

Układ DMA może być wyzwalany różnymi sygnałami i co ciekawe, ma on coś w rodzaju własnego systemu zdarzeń. Można więc bezpośrednio połączyć DMA do różnych peryferiów i uzyskać jeszcze większą prędkość kopiowania danych, ale trzeba uważać na pewną pułapkę. Dokładniej rzecz biorąc, transmisję DMA uaktywniać może flaga przerwania wybranego układu peryferyjnego, ale DMA nie zawsze może taką flagę wyzerować! Tak jest w przypadku timerów – gdybyśmy jako wyzwalacz wzięli flagę przerwania CCA, wówczas z wielkim skonsternowaniem byśmy stwierdzili, że po pierwszym wyzwoleniu DMA nie zatrzymuje się, lecz działa w nieskończoność. W takim przypadku powinniśmy odblokować przerwania, ponieważ flaga jest kasowana bezpośrednio po wejściu do procedury przerwania. Jednak jeśli procesor miałby wchodzić do niej tylko po to, by zresetować flagę, to jest całkowicie bez sensu. Dlatego lepiej jest wykorzystać system zdarzeń, który rozwiązuje ten problem.

W kodzie programu na listingu 1 powinniśmy zwrócić uwagę na różnice w ustawieniach względem pierwszego programu. Do rejestru DMA.CH0.REPCNT zostało wpisane zero. Oznacza to, że transakcja składa się z jednego bloku, który kopiowany będzie w nieskończoność. Do rejestru DMA.CH0.TRFCNT wpisujemy z ilu bajtów składa się blok i jest to oczywiście rozmiar tablicy źródłowej, pobrany operatorem sizeof(). Kolejna różnica jest w DMA.CH0.ADDRCTRL, gdzie wpisując DMA_CH_SRCRELOAD_BLOCK_gc ustaliliśmy, że adres tablicy źródłowej zostanie przywrócony do stanu początkowego, po zakończeniu przesyłania bloku. Ostatnie różnice dotyczą rejestru DMA.CH0.CTRLA, gdzie zniknęło polecenie uruchomienia transmisji, a pojawiły się dwa dodatkowe symbole:

  • DMA_CH_SINGLE_bm – ten bit powoduje, że po wystąpieniu sygnału wyzwalającego, DMA wykona tylko jedną transmisję burst, po czym będzie oczekiwać na kolejny wyzwalacz,
  • DMA_CH_REPEAT_bm – włącza powtarzanie transakcji tyle razy, ile jest wpisane do rejestru DMA.CH0.REPCNT. Zero jest wartością specjalną i oznacza kopiowanie w nieskończoną liczbę razy.

Na końcu programu mamy pustą pętle while(1). Kompilujemy program, ładujemy go do procesora i obserwujemy, jak kolejne elementy tablicy source[] pojawiają się na diodowym wyświetlaczu portu X.

List. 1. Kod programu do drugiego ćwiczenia

#include  
#include "extrino_portx.h"

uint8_t source[] = {0b00000001,
                    0b00000011,
                    0b00000111,
                    0b00001111,
                    0b00011111,
                    0b00111111,
                    0b01111111,
                    0b11111111,
                    0b11111110,
                    0b11111100,
                    0b11111000,
                    0b11110000,
                    0b11100000,
                    0b11000000,
                    0b10000000,
                    0b01000000,
                    0b00100000,
                    0b00010000,
                    0b00001000,
                    0b00000100,
                    0b00000010,
                    0b00000001,
                    0b01010101,
                    0b10101010,
                    0b11111111,
                    0b00000000
                    };

int main(void) {
    
    // inicjalizacja PORTX (uwaga - przerwania wyłączone w pliku extrino_portx.h)
    PortxInit();
    
    // konfiguracja timera by zgłaszał zdarzenie co 1 sek i sterował pinem CS portu X
    TCE0.CTRLB          =    TC_WGMODE_SINGLESLOPE_gc|       // tryb normalny
                             TC0_CCCEN_bm;                   // włączenie wyjścia kanału output compare C
    TCE0.CTRLA          =    TC_CLKSEL_DIV1024_gc;           // ustawienie preskalera i uruchomienie 
    TCE0.CCC            =    198;                            // wartość wyzwalająca kanał C
    TCE0.CCA            =    199;                            // wartość wyzwalająca kanał A
    TCE0.PER            =    200;                            // okres timera
    
    // konfiguracja CS PORTX
    PORTE.REMAP         =    PORT_TC0C_bm;                   // przeniesienie wyjścia kanału C TC0 z E2 na E6
    PORTE.DIRSET        =    PIN6_bm;                        // pin E6 jako wyjście
    
    // konfiguracja systemu zdarzeń
    EVSYS.CH0MUX        =    EVSYS_CHMUX_TCE0_CCA_gc;        // zdarzenie na CH0 wywołuje kanał A TE0
    
    // konfiguracja kontrolera DMA
    DMA.CTRL            =    DMA_ENABLE_bm|                  // włączenie kontrolera
                             DMA_DBUFMODE_DISABLED_gc|       // bez podwójnego buforowania
                             DMA_PRIMODE_RR0123_gc;          // wszystkie kanały równy priorytet
    
    // konfiguracja kanału DMA
    DMA.CH0.SRCADDR0    =    (uint16_t)source & 0xFF;       // adres źródła
    DMA.CH0.SRCADDR1    =    (uint16_t)source >> 8;
    DMA.CH0.SRCADDR2    =    0;
    
    DMA.CH0.DESTADDR0   =    (uint16_t)&SPIC.DATA & 0xFF;  // adres celu
    DMA.CH0.DESTADDR1   =    (uint16_t)&SPIC.DATA >> 8;
    DMA.CH0.DESTADDR2   =    0;
    
    DMA.CH0.TRFCNT      =    sizeof(source);                 // rozmiar bloku = rozmiar tablicy source
    DMA.CH0.REPCNT      =    0;                              // ile bloków, 0 oznacza wysyłanie w nieskończoność
    DMA.CH0.TRIGSRC     =    DMA_CH_TRIGSRC_EVSYS_CH0_gc;    // kanał CH0 powoduje transfer
    DMA.CH0.ADDRCTRL    =    DMA_CH_SRCRELOAD_BLOCK_gc|      // przeładowanie adresu źródła po zakończeniu bloku
                             DMA_CH_SRCDIR_INC_gc|           // zwiększanie adresu źródła po każdym bajcie
                             DMA_CH_DESTRELOAD_NONE_gc|      // przeładowanie adresu celu nigdy
                             DMA_CH_DESTDIR_FIXED_gc;        // stały adres docelowy
    DMA.CH0.CTRLA       =    DMA_CH_ENABLE_bm|               // włączenie kanału
                             DMA_CH_BURSTLEN_1BYTE_gc|       // burst = 1 bajt
                             DMA_CH_SINGLE_bm|               // pojedynczy burst po każdym zdarzeniu
                             DMA_CH_REPEAT_bm;               // powtarzanie
                            
    // pusta pętla główna    
    while(1) {}
}

Cały program to jedynie konfiguracja kilkunastu rejestrów, a potem wszystko dzieje się całkowicie sprzętowo. Czy XMEGA to wciąż zwykły mikrokontroler czy może specyficzne FPGA z różnymi peryferiami, które możemy sobie łączyć jak chcemy?

Dystrybutorem zestawu X3-DIL64 jest KAMAMI.pl.

 

Dominik Leon Bieczyński

http://leon-instruments.blogspot.com