środa, 1 marca 2017

DMA - pokochać nieznane



O DMA czyli Direct Memory Access, każdy z nas pewnie słyszał lub przynajmniej obiło mu się to o uszy. Jakieś tam szybkie operacje na pamięci i na tym nasza przeciętna wiedza się kończy. Ale skąd niby mamy mieć na ten temat wiedzę skoro w AVR-ach (zakładam , że każdy hobbysta staruje z poziomu AVR-a) tej funkcjonalności długo by szukać no dopiero w zapomnianych przez świat AVR XMEGACH (swoją drogą bardzo fajnych mikrokontrolerach).


W mikrokontrolerach PIC24 i PIC32 (do 8bitowców nie zaglądałem) DMA jest na porządku dziennym. W mikrokontrolerze, który obecnie poznaję czyli PIC24HJ128GP502 mamy 8 kanałów DMA potrafiących "gadać" nie tylko z pamięcią ale również z peryferiami takimi jak: UART, SPI, ADC, ECAN, IC, OC, INT0.

Tu trochę bardzo podstawowej teorii o DMA z Wikipedii https://pl.wikipedia.org/wiki/Direct_Memory_Access aby wyrobić sobie ogólny pogląd.

Aby bardziej przybliżyć jak działa DMA rozważmy najpierw jak jest realizowany normalny proces skopiowania jednej komórki pamięci z jednego adresu pod inny adres. W tym przypadku Procesor na magistralę adresową wystawia adres komórki, która ma być odczytana i wysyła żądanie odczytu. Pamięć lub układ peryferyjny udostępnia żądany bajt informacji na magistrali danych.

Procesor kopiuje ten bajt do jednego ze swoich rejestrów roboczych WREG0-WREG15. Następnie procesor na magistralę adresową wypisuje adres komórki, do której mają trafić dane. Na magistralę danych kopiuje wartość rejestru WREG0-WREG15, który przechowuje interesujące nas informacje, po czym wysyła sygnał zapisu. Adresy komórek źródłowych i docelowych również są przechowywane w rejestrach roboczych WREG0-WREG15.

Powyższa operacja, ma dwie wady. Trwa dość długo i całkowicie pochłania rdzeń procesora, przez co nie może on wykonywać żadnych innych operacji. Kopiowanie dużych bloków pamięci może na długi czas zablokować procesor. I tutaj przychodzi nam z pomocą układ DMA.

Zadaniem DMA jest skopiowanie określonej liczby bajtów z jednego miejsca w drugie miejsce bez angażowania procesora. W tym czasie procesor może wykonywać inne operacje całkowicie bez utraty funkcjonalności.

Co się stanie w sytuacji, kiedy procesor będzie chciał uzyskać dostęp do pamięci, w czasie kiedy DMA transferuje dane? Wówczas praca DMA zostanie automatycznie zawieszona, a kiedy procesor zakończy swoje działanie, DMA będzie kontynuować pracę.

Oprócz zastosowań typowych, jak kopiowanie danych z jednej tablicy do drugiej, DMA bardzo dobrze współpracuje z innymi układami peryferyjnymi mikrokontrolera. DMA potrafi przesyłać dane z tablicy do układu interfejsowego UART czy SPI, dzięki czemu całkowicie sprzętowo i bardzo szybko można przesłać duży blok danych pomiędzy pamięcią a urządzeniem peryferyjnym . W podobny sposób istnieje możliwość odbierania danych z układów peryferyjnych i przesyłania ich do pamięci.

DMA równie dobrze współpracuje z przetwornikiem analogowo-cyfrowym i cyfrowo-analogowym. Na przykład, pomiar przetwornikiem może być inicjowany timerem w zadanych odstępach czasowych, a po zakończeniu pomiaru, DMA może kopiować wyniki pomiaru z przetwornika do tablicy w pamięci, którą procesor przetwarza w czasie rzeczywistym.

Poniżej rysunek poglądowy dla obiegu danych DMA w naszym mikrokontrolerze PIC24HJ :


Przyjrzyjmy się rejestrom , które są potrzebne do działania z DMA i poszczególnym bitom, które będziemy wykorzystywać. :

DMAxCON (DMA Control Register) gdzie x oznacza numer kanału od 0 do 7.
W rejestrze tym interesują nas nastepujące bity :

- bit 15 oznaczony jako CHEN (Chanel Enable Bit) uaktywniamy przed transferem lub wyłączamy kanał DMA przy czym po zakończeniu transferu danych wyłączenie (ustawienie 0 następuje automatycznie). Przed wysyłką kolejnego transferu trzeba pamiętać aby bit ustawić na 1.

- bit 14 oznaczony jako SIZE (Data Transfer Size bit) wybieramy rozmiar danych przesyłanych przez DMA, Byte (8 bitów) lub Word (16 bitów). I tu trzeba uważać bo np do przesyłania z/do UARTA musimy ustawić Byte.

- bit 13 DIR (Transfer Direction bit (source/destination bus select) ustawiamy, kierunek przepływu danych np z pamięci do peryferiów lub odwrotnie. Dla przykładu jeśli chcemy nadawać po UART (TX) to ustawiamy tutaj "1".Jeśli odbieramy dane po UART (RX) to ustawiamy bit rejestru na "0".

- bit 5-4 AMODE (Addressing Mode Select bits) adresowanie z inkrementacją lub nie. Standardowe ustawienie to "00" ale wyłączenie inkremetacji też znajduje zastosowanie np jeśli chcemy wynik z ADC tylko w jednej komórce pamięci odświerzać.

- bit 1-0 MODE (Operating Mode Select bits) ważny rejestr w którym ustawiamy tryby pracy DMA, trybów w sumie jest 3 : (OneShot - jeden transfer danych i zamknięcie kanału DMA, Continuous - zapętlony transfer danych, PingPong - sekwencja przesyłu danych z dwóch buforów/tablic) dokłady opis i rysunki działania trybów w materiałach w linku pod artykułem. Na przykład dla transmisji po UART (TX) wybieramy tryb OneShot bez Continuous i bez PinPonga-a , wpisujemy zatem do rejestru "01"

DMAxREQ (IRQ Select Register)
Rejestr ten ma dwa kluczowe zadania : ustawia jak ma być zainicjowany transfer (Manual/Automatic) i drugie zadanie to wskazanie na peryferia (na wektor przerwań od danego peryferia) z jakim będzie DMA wymieniał dane.
W rejestrze tym interesują nas nastepujące bity :

- bit 15 oznaczony jako FORCE, np dla nadawania przez UART (TX), transfer DMA musi być zainicjowany manulanie czyli bit ustawiamy na "1", w przypadku odbioru UART (RX) ustawiamy na automat.

- bit 6-0 IRQSEL tutaj wpisujemy wartość z Table 8-1 str 107 (datasheet link w materiałach pod artykułem) i np dla UART1 (TX) wpisujemy do rejestru wartość 0001100 czyli w hex 0xC.

DMAxSTA (RAM START ADDRESS Register)
tutaj wpisujemy adres początku naszego bufora z danymi do przesłania.
Ważna rzecz, kanał DMA otwieramy (DMAxCONbits.CHEN = 1)po zapisie w tym rejestrze inaczej jak podaje producent mogą być nieprzewidywalne zachowania DMA.

DMAxPAD (PERIPHERAL ADDRESS Register)
tutaj wpisujemy adres rejestru danego peryferia, adresy możemy sobie wyłuskać za pomocą operatora pobrania adresu "&" lub podane mamy je w tabelce Table 8-1 str 107 (datasheet)
Np dla UART1 (TX) rejestrem adekwatnym jest U1TXREG

DMAxSTA i DMAxSTB (RAM START ADDRESS Register)
tutaj wpisujemy adres początku naszego bufora z danymi, mamy dwa rejestry do wykorzystania czyli możemy podać adresy początków dla dwóch oddzielnych buforów.
W trybie PingPong te dwa rejestry możemy pchnąć w jednej sekwencji a w trybie OneShot
możemy alternatywnie albo jeden albo drugi pchnąć w zależności np od jakiegoś warunku.

DMAxCNT (TRANSFER COUNT Register)
tutaj podajemy ile danych chcemy przesłać z bufora, podajemy od 0 do 1023 co odpowiada wartości 1024 bajtów maksymalnie do przesłania. Kontroler DMA wykorzystuje dane z tego rejestru do inkrementowania komórek pamięci podczas transferu danych z bufora.
Przykład zapisu do resjestru może wyglądać tak :
DMA0CNT = strlen(bufor)-1; /*funkcja strlen() oblicza ilość znaków w buforze bez znaków specjalnych, trzeba zainkludować bibliotekę #include <string.h>.*/

I to by było tyle jeśli chodzi o rejestry , które w praktyce będziemy wykorzystywać jeśli chcemy działać na DMA. W sumie jest tego niewiele do ogarnięcia i jak się raz zatrybi o co biega z tym DMA to otwierają nam się drzwi do innego wspaniałego świata pełnego alternatywnych możliwośći :), gdzie mikrokontroler przez większość czasu nam leżakuje i możemy , żeby się za bardzo nie rozleniwił dorzucić mu wiadro innych zadań.

Wspomnę jeszcze dla porządku o rejestrach DMACS0,DMACS1 i DSADR ale to już wyższa półka działania z DMA, w 95 % nie ma potrzeby korzystania z tych rejestrów.


Wszelkie operacje DMA muszą być dokonywane w obszarze wydzielonej pamięci DMA RAM , pamięć ta ma w PIC24HJ128GP502 wymiar 2 kB. W jednej transmisji DMA możemy przesłać maksymalnie 1024 bajtów , przy czym np.w trybie PingPong mamy możliwość wysłania danych z dwóch buforów po 1024 bajty każdy i pchnięcie tego w jednej sekwencji - jeden bufor po drugim. Przykładowe zdefiniowanie bufora z danymi w obszarze pamięci DMA RAM wygląda tak :

char bufor[] __attribute__((space(dma)))="Transfer DMA\r\n";


I teraz jak chcemy np.ten bufor pchnąć po UART-cie to wszystkim zajmuje się kontroler DMA bez angażowania cykli procesora.

W przypadku mikrokontrolerów dsPIC33 w jednym strzale można przez DMA puścić aż 16 kB danych.

Poniżej kod z przykładem transferu za pomocą DMA, danych z bufora do UART1 (TX) 
Definiujemy sobie w DMA RAM, bufor znaków  i wypychamy w świat po UART, cała operacja zostanie wykonana w tle bez angażowania zasobów (cykli) procesora . 


Pozdrawiam
picmajster.blog@gmail.com


Linki :
http://ww1.microchip.com/downloads/en/DeviceDoc/70309B.pdf

http://forbot.pl/blog/artykuly/programowanie/kurs-stm32-8-dma-czyli-bezposredni-dostep-do-pamieci-id8465

http://ww1.microchip.com/downloads/en/DeviceDoc/70293G.pdf

/****************************************************************************** 
 File:   ustaw_zegar.h
 ******************************************************************************/
#ifndef USTAW_ZEGAR_H
#define USTAW_ZEGAR_H

#define FCY 40000000 /* podajemy wartosc ustawionego zegara (40 MHz), wazne 
aby przed includowaniem <libpic30.h>, potrzebne to jest do wyliczania delay-i*/

/*deklaracja funkcji*/
void ustaw_zegar(void) ;

#endif  /* USTAW_ZEGAR_H */



/******************************************************************************* 
 File:   uart.h
 *******************************************************************************/
#ifndef UART_H
#define UART_H

#define BAUDRATE 9600 /*definujemy szybkosc UART-a*/
#define BRGVAL ((FCY/BAUDRATE)/16)-1
/*deklaracja funkcji*/
void Ustaw_UART1(void);
#endif  /* UART_H */



/******************************************************************************* 
 File:   ustaw_zegar.c
 ******************************************************************************/
/*Ustawiamy zegar wewnetrzny na ok 40 MHz (dokladnie na 39.61375 MHz*/
#include "xc.h" /* wykrywa rodzaj procka i includuje odpowiedni plik 
naglówkowy "p24HJ128GP502.h"*/
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h> /*dyrektywy do uint8_t itp*/
#include "ustaw_zegar.h" /*z uwagi na FCY musi byc przed #include <libpic30.h>*/

/*definicja funkcji*/
void ustaw_zegar(void) {
 /*
  * to co mozemy ustawic za pomoca '#pragma' jest dostepne w pliku 
  * xc16/docs/config_index.html
  */
 #pragma config JTAGEN = OFF
 // Watchdog timer enable/disable by user software
 #pragma config FWDTEN = OFF 
  
 //********************Start Ustawien Zegara************************
 /* 
  * Fcy - zegar instrukcji , Fosc - zegar rdzenia (jest zawsze dwa razy wiekszy 
  * od zegara instrukcji)) Ustawiamy zegar instrukcji na 40 Mhz z wewnetrznego 
  * oscylatora Fin=7.37 MHz w/g wzoru Fcy=Fosc/2 gdzie Fosc=Fin x (M/(N1+N2))
  * gdzie M=43, N2=2, N1=2 ustawiane w rejestrach PLLFBD/PLLPOST/PLLPRE
  */
 //Select Internal FRC (Fast RC Oscillator)
 #pragma config FNOSC = FRC // FOSCSEL-->FNOSC=0b000 (Fast RC Oscillator (FRC))
 //Enable Clock Switching and Configure
 #pragma config FCKSM = CSECMD //FOSC-->FCKSM=0b01 - wlacz zegar
 #pragma config OSCIOFNC = OFF //FOSC-->OSCIOFNC=1 - Fcy b?dzie na pinie OSCO
     /*Config PLL prescaler, PLL postscaler, PLL divisor*/
      PLLFBD = 41 ; //M=43 (0 bit to 2 st?d 41 = 43 patrz w rejestrze), tutaj 3.685 x 43 = 158.455MHz
      CLKDIVbits.PLLPRE=0 ;  //N1=2, tutaj 7.37 MHz / 2 = 3.685 MHz
      CLKDIVbits.PLLPOST=0 ; //N2=2, tutaj 158.455 MHz / 2 = 79.2275 MHz (Fosc)
   /*   
   * UWAGA przerwania musza byc wylaczone podczas wywolywania ponizszych 
   * dwóch funkcji __builtin_write_...brak definicji w pliku naglówkowym
   * to wewnetrzne funkcje kompilatora patrz help M-LAB IDE
   * i datasheet str 140(11.6.3.1 Control Register Lock)
   */
  /*Initiate Clock Switch to Internal FRC with PLL (OSCCON-->NOSC = 0b001)*/
      __builtin_write_OSCCONH(0x01); //tutaj argumentem jest wartosc z NOSC
  /*Start clock switching*/
      __builtin_write_OSCCONL(0x01);
      
  /*Wait for Clock switch to occur*/
      while(OSCCONbits.COSC !=0b001);
      
      /*Wait for PLL to lock*/
      while(OSCCONbits.LOCK !=1) {};
 }







/*******************************************************************************
 File:   uart.c
 ******************************************************************************/
#include "xc.h"
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h> /*dyrektywy uint8_t itp*/
#define FCY 40000000 /* podajemy wartosc ustawionego zegara (40 MHz), wazne 
aby przed includowaniem <libpic30.h>, potrzebne to jest do wyliczania delay-i*/
#include <libpic30.h> // biblioteka dajaca dostep do delay-i.
#include "uart.h"

/*definicje funkcji*/
 void Ustaw_UART1(void) {
/*remapping pinow pod UART1*/
/*TX ustawiamy na pinie 14 (RP5)--> U1TX wartosc 00011, wpisac na bitach od 8 do 12 do rejestru RPOR2*/
RPOR2bits.RP5R = 3;
/*RX ustawiamy na pinie 11 (RP4)--> U1RX wartosc 00100, wpisac na bitach od 0 do 5 do rejestru RPINR18*/
RPINR18bits.U1RXR = 4 ;

/*konfiguracja UARTA1 do wspolpracy z DMA*/
U1MODEbits.STSEL = 0;           /* 1-Stop bit*/
U1MODEbits.PDSEL = 0;           /* No Parity, 8-Data bits*/
U1MODEbits.ABAUD = 0;           /* Auto-Baud disabled */
U1MODEbits.BRGH = 0;            /* Standard-Speed mode */
U1BRG = BRGVAL;                 /* Baud Rate setting */
U1STAbits.UTXISEL0 = 0;     /*Interrupt after one TX character is transmitted*/
U1STAbits.UTXISEL1 = 0;
U1MODEbits.UARTEN = 1;      /*Enable UART*/
U1STAbits.UTXEN = 1;        /*Enable UART TX*/
/* Wait at least 105 microseconds (1/9600) before sending first char */
__delay_us(105);   
/*Uff..koniec konfiguracji UART1*/
 } 



/*******************************************************************************
 File:   main.c
 ******************************************************************************/
#include "xc.h"
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h> /*dyrektywy uint8_t itp*/
#include <string.h>
#include "ustaw_zegar.h" /*z uwagi na FCY musi byc przed #include <libpic30.h>*/
#include <libpic30.h> // biblioteka dajaca dostep do delay-i.
#include "uart.h"

/*deklaracje funkcji*/
void transfer_DMA_UART1(void) ; 

/*lokujemy bufor do pamieci DMA RAM, uzywamy wewnetrznej funkcji kompilatora __attribute__((space(dma)))*/
char bufor[] __attribute__((space(dma)))="Transfer DMA\r\n";
/*dma
 - PIC24E/H MCUs, dsPIC33E/F DSCs only
Allocate the variable in DMA memory. Variables in DMA memory can be accessed using ordinary C statements and by the DMA peripheral. 
__builtin_dmaoffset() and __builtin_dmapage() can be used to find the correct offset for configuring the DMA peripheral. See ?Built-in Functions? for details.
    #include <p24Hxxxx.h>
    unsigned int BufferA[8] 
__attribute__((space(dma)));
    unsigned int BufferB[8] 
__attribute__((space(dma)));
    
    int main()
    {
      DMA1STA = 
__builtin_dmaoffset(BufferA);
      DMA1STB = 
__builtin_dmaoffset(BufferB);
     }
*/

int main(void) {
/*konfiguracja zegara i inne potrzebne do startu mikrokontrolera*/
ustaw_zegar();
Ustaw_UART1();

__delay_ms(3000) ;

transfer_DMA_UART1(); /*wyslij do UARTA nasz bufor z danymi*/
while(DMA0CONbits.CHEN != 0) {} /*czekaj az kanal po ostatniej transmisji na DMA0 zostanie zamkniety/zwolniony*/
transfer_DMA_UART1(); /*ponownie wyslij do UARTA nasz bufor z danymi*/

    while(1)
    {
        /*petla glowna programu*/         
    }
    return 0;
}

/*definicje funkcji*/
void transfer_DMA_UART1(void){
    
    /*Konfiguracja DMA kanal 0 do transmisji UART w trybie One_Shot (bez powtówrzenia)*/
/* Rejestr DMAxCON
 * bit15    -0 chen/chanel --> disable
 * bit14    -1 size --> Byte    
 * bit13    -1 dir --> Read from DMA RAM address write to peripheral address
 * bit12    -0 half --> Initiate block transfer complete interrupt when all of the data has been moved
 * bit11    -0 nullw --> Normal operation
 * bit10-6 Unimplemented raed as 0
 * bit5-4   -00 amode  --> Register Indirect with Post_Incerement mode
 * bit3-2 Unimplemented read as 0
 * bit1-0   -01 mode --> OneShot, Ping-Pong modes disabled*/
DMA0CON = 0x6001 ;/*0x6001 -m.in kanal DMA disable, wartosc wynika z ustawienia bitow jak wyzej*/

DMA0CNT = strlen(bufor)-1;/*wyliczamy ile znaków do przeslania mamy z bufora, max 1024 bajty*/
/*IRQ Select Register,wskazujemy UART1 TX*/
DMA0REQ = 0x000C ;/*UART1 TX*/
/*Peripheral Adress Register*/
DMA0PAD =  (volatile unsigned int)&U1TXREG ; /*rzutowanie typu i pobranie adresu rejestru U1TXREG*/
/*wewnetrzna konstrukcja/funkcja kompilatora - "__builtin_dmaoffset()"*/
DMA0STA = __builtin_dmaoffset(bufor) ;/*taka jest konstrukcja wskazania na bufor z danymi*/

IFS0bits.DMA0IF = 0 ; /*clear DMA Interrupt Flag */
IEC0bits.DMA0IE = 1 ; /*enable DMA Interrupt */

/*Wazne kanal DMA moze byc otwarty dopiero po wpisaniu danych do rejestru DMASTA i DMAxCNT*/
DMA0CONbits.CHEN  = 1/*Canal DMA0 enable*/
/*Force a single DMA transfer (Manual mode), inicjuj transfer*/
DMA0REQbits.FORCE = 1

  /*po zakonczonym transferze automatycznie kanal DMA zostaje wylaczony, zmienia
   *sie wartosc rejestru DMAxCON na 16-tym bicie z "1" na "0". Aby ponownie odpalic transfer
   *nalezy wlaczyc ten bit DMAxCON.bits.CHEN=1 i odpalic transfer DMA0REQbits.FORCE = 1
   */
}

/*przerwanie DMA kiedy blok danych zosta? wyslany z DMA RAM do UART-a*/
/*Ale uwaga to nie oznacza , ze transmisja UART zostala zakonczona calkowicie*/
void __attribute__((interrupt, no_auto_psv))_DMA0Interrupt(void)
{
    /*kod obslugi przerwania jesli potrzeba*/
    IFS0bits.DMA0IF = 0;  /*Clear DMA Interrupt flag*/ 
}


1 komentarz:

  1. Your style is very unique inn comparison to other people I have read stuff from.
    Thanks for posting when you have the opportunity, Guess I will just bookmark this web site.

    OdpowiedzUsuń