czwartek, 31 maja 2018

PIC32MM - SPI + DMA zajęcia praktyczne.


Poprzednia randka z SPI i 32-bitowym mikrokontrolerem firmy Microchip PIC32MM została zrealizowana przy wykorzystaniu funkcji wygenerowanych przez wtyczkę do MPLABX-IDE , MCC (MPLAB Code Configuration).  Jakość kodu generowanego przez tę wtyczkę bije na głowę podobne rozwiązania u innych producentów. Wiem, że zaczynam się powtarzać :) ale pracę programistów Microchipa należy docenić, ponieważ dzięki ich trudowi , życie hobbysty jest prostsze :). W artykule pokażę, jak w prosty sposób użyć DMA do transferu danych po SPI oraz inne przydatne rzeczy takie jak np. konfiguracja pinów do współpracy z peryferium sprzętowym.

W PIC32MM mamy do dyspozycji 4 kanały DMA, chciałoby się więcej tym bardziej , że cechy tych kanałów są bardzo ciekawe. Z grubsza DMA w PIC32MM ma następujące cechy :

- możliwy różny rozmiar źródła danych i przeznaczenia (!!!)
- transfer pamięć do pamięci (Flash do RAM lub odwrotnie)
- transfer pamięć do peryferium (i odwrotnie)
- automatyczne włączanie kanałów
- zdarzenia start/stop (mamy nie tylko do dyspozycji przerwania ale bogaty zasób eventów)
- detekcja wzorców (np. chcemy aby wygenerowało nam się przerwanie, jeśli w  ciągu odebranych danych kanałem DMA pojawi się jakiś zdefiniowany wzorzec np. liczba, znak etc., długość wzorca do 2 bajtów)
- kaskadowanie kanałów
- generator CRC do współpracy z DMA, tzw. DMA CRC
- inicjowanie transferu programowo lub na żądanie przerwania

Mnie zaciekawiła w szczególności funkcjonalność w której mamy do czynienia z różnym rozmiarem  źródła danych i przeznaczenia. W manualu DMA dla PIC32 jest pokazany obraz takiego specyficznego transferu w którym , źródło i przeznaczenie mają różne wymiary. 


O co tu chodzi ? Źródło ma rozmiar 4 bajtów a bufor odbiorczy 2 bajty. W przykładzie powyższym przesyłamy za jednym razem dwa bajty w "celach" (komórkach) 2 bajtowych. Transfer się zakończy jak zostaną przesłane 4 bajty ze źródła. W pierwszym transferze 2 bajtowym w buforze odbiorczym pojawią się bajty nr 1 i nr 2 a po drugim transferze i zakończeniu transmisji w buforze odbiorczym będą się znajdować bajty nr 3 i 4. 
Na razie może nie czujemy jak i gdzie taką funkcjonalność możemy wykorzystać w praktyce ale intuicyjnie wydaje się , że jest to arcy ciekawa funkcjonalność.

Nie będę tutaj więcej się rozpisywał na temat teorii , szczegóły na temat DMA w PIC32 możemy sobie doczytać w lekkim manualu link poniżej artykułu.
Ja skupię się na pokazaniu jak tego użyć i skonfigurować w praktyce. Bo samą teorią to nie załapiemy wszystkiego.
Konkretnie rozważymy sobie przypadek w którym chcemy przesłać dane z jednego MCU do drugiego po SPI wykorzystując po stronie Mastera kanały DMA.
Posłużę się dokładnie tym samym zadaniem jak w artykule : PIC32MM SPI - zajęcia praktyczne

Warto tu nadmienić, że koktajl szybkiego SPI w trybie ENHANCED BUFFER MODE plus DMA daje nam bardzo wydajny i szybki sposób na przesyłanie danych. Nie musimy się w tym przypadku martwić o jakieś funkcje do przesyłania danych po SPI bo używając DMA wcale ich nie potrzebujemy :) Wrzucamy tylko dane do bufora nadawczego inicjujemy transfer DMA z tego bufora i dane lecą jak strzała do odbiorcy, jeśli odbiorca coś nam ciekawego prześle dane pojawią się automatycznie w buforze odbiorczym. Piękne proste i niezwykle szybkie.

Wykorzystamy te same piny co we wspomnianym wyżej artykule, a po stronie Slave ten sam kod bez żadnych zmian. Zmiany natomiast będą po stronie Mastera tutaj w kodzie się trochę narobimy, ponieważ mam zamiar konfigurację pinów, SPI i DMA zrobić manualnie bez użycia wtyczki MCC. Jedynie zegar i konfiguracja startowa MCU zostanie  zrobiona przy wykorzystaniu plików wygenerowanych przez MCC
Czyli w sumie pracujemy w trybie pół-automatu, łączymy lenistwo z pracą organiczną, jak zobaczymy tak też się da  :)

Do testów standardowo wykorzystam moje dwie płytki developerskie PIC32MM, bardzo je polubiłem są na prawdę wygodne i urocze :)



Poniżej pokażę w miarę szczegółowo jak dojść do celu czyli przesłać po SPI2 dane z wykorzystaniem kanału DMA po stronie Mastera.

Pierwszą czynnością jaką zrobimy po stronie Mastera to skonfigurujemy piny, przypominam robimy to manualnie wykorzystując czysty szablon wygenerowany przez MCC a który znajdziemy w projekcie Mastera w pliku pin_manager.c
Piny jakie wykorzystamy i podłączenie :

RB3 - SDO  (Master) ----> RA9 - SDI   (Slave)
RA9 - SDI   (Master)  ----> RB3 - SDO (Slave)
RB8 - SCK  (Master) ----> RB8 - SCK  (Slave)


Fizycznie potrzebujemy tylko linii zegara SCK i SDO bo w zadaniu Master nie odbiera danych tylko je wysyła. Nie mniej w programie zrobię pełną konfigurację dla wszystkich powyższych pinów.
Zabieramy się zatem do roboty. Projekt dla Mastera będę przygotowywał za pomocą  GitHub-a, jest on tak wspaniale zaimplementowany w MPLABX-IDE, że żal aby tej funkcjonalności nie wykorzystywać. Projekt w  linku poniżej artykułu jest w każdej chwili do ściągnięcia za pomocą opcji Team-->Git-->Clone, prosto pięknie i przyjemnie. Aktualizacji w GitHub dokonuję na bieżąco razem z postępem pisania artykułu.

Najpierw konfigurujemy kierunki pinów (w pliku pin_manager.c), które użyjemy do SPI po stronie Mastera :

    /*set Input for SPI2*/
    TRISAbits.TRISA9 = 1 ; /*SDI Master*/
    /*set Output for SPI2*/
    TRISBbits.TRISB3 = 0 ; /*SDO Master*/
    TRISBbits.TRISB8 = 0 ; /*SCK Master*/

Ponieważ pin RB3 jest na starcie podpięty do linii analogowej musimy go od tej linii odpiąć robimy to tak :

ANSELBbits.ANSB3 = 0 ; /*RB3 set digital pin*/

A na których pinach takie odpinanie trzeba przeprowadzić w PIC32MM (48 pin) ??? do tego dałem ściągawkę , która się znajduje na końcu pliku pin_manager.c

Kolejnym krokiem będzie przyporządkowanie pinów do konkretnych funkcji SPI. Musimy jednak pamiętać, że w PIC32MM tylko SPI2 ma możliwość dowolnego przyporządkowania pinów, pozostałe SPI mają ustawione na sztywno piny.
W pliku pin_manager.c dodajemy zatem sekwencję kodu :

    /****************************************************************************
     * Set the PPS
     ***************************************************************************/

  
SYSTEM_RegUnlock();             /*unlock PPS*/
    RPCONbits.IOLOCK = 0;

    RPINR11bits.SDI2R = 0x0018; /*RA9->SPI2:SDI2;*/
    RPOR2bits.RP9R = 0x0008;      /*RB3->SPI2:SDO2;*/
    RPOR3bits.RP13R = 0x0009;    /*RB8->SPI2:SCK2OUT;*/

    RPCONbits.IOLOCK = 1;          /*lock   PPS*/
    SYSTEM_RegLock();

Powyższy kod wymaga komentarza. 
Krytyczne rejestry w PIC32MM mają ochronę  przed np. przypadkowym zapisem czy niezamierzoną zmianą. Spis rejestrów , które podlegają takiej ochronie pokazuje tabelka poniżej :


Z tabelki wynika, że system , który odpowiada za przyporządkowanie pinów do różnych funkcji sprzętowych - PPS (Periphelar Pin Select) podlega ochronie.
Zatem aby zmienić konfigurację pinów w systemie PPS musimy najpierw  użyć funkcji SYSTEM_RegUnlock();, która zdejmuje ogólną systemową blokadę rejestrów a następnie ustawienie RPCONbits.IOLOCK = 0;, które zdejmuje blokadę dedykowaną dla portów I/O. Teraz dopiero po zdjętych blokadach można dokonać zmian na rejestrach systemu PPS, po zakończeniu zmian przywracamy blokady. 

Tera przyjrzymy się jak wygląda mechanizm przyporządkowanie pinów do funkcji SPI. Weźmy jako pierwszy rozważmy pin RA9, chcemy aby na nim była linia SDI Mastera a konkretnie SDI2 w SPI2. W tym celu szukamy na stronie 116 Datasheet MCU PIC32MM tabelki zatytułowanej Input Pin Selection :



W powyższej tabelce odszukujemy wiersz SPI2 Data Input, patrząc na kolejne elementy tego wiersza widzimy, że za to ustawienie odpowiada rejestr RPINR11 i bity w tym rejestrze SDI2R<4:0>. Tylko pytanie co wpisać na tych 5-ciu bitach rejestru RPINR11 ???? to proste tam musimy wpisać numer naszego pinu RA9, ale nadal nie wiemy jak to zrobić. Wszystko się rozjaśnia jak na stronie 117 datasheet znajdziemy poniższą tabelkę :


Z tabelki tej odczytujemy wartość 0x18 (dec 24) jaka reprezentuje pin RA9 i tę wartość wpisujemy w rejestr jak poniżej :
RPINR11bits.SDI2R = 0x0018; /*RA9->SPI2:SDI2;*/
  
Od tego momentu na pinie RA9 mamy linię  SDI2 Mastera na SPI2. Zauważmy tym samym jak przyjemnie się robi w świecie Microchipa zapisy do rejestru.
Teraz prześledzimy jak skonfigurować pin RB3, chcemy na nim mieć linię SDO2 w SPI2. Zerkamy na tabelkę Output Pin Selection , którą znajdziemy na stronie 119 datasheet PIC32MM :



W powyższej tabelce szukamy wiersza z SPI2 Data Output i odczytujemy wartość w kolumnie Output Function Number czyli 8 . Tą wartość zapamiętujemy i udajemy się do znanej już nam wyżej tabelki :


W  powyższej tabelce szukamy wiersza RB3 pins i zapamiętujemy wartość z kolumny RPn Pins czyli RP9. Mając w pamięci wartość 8 i RP9 szukamy rejestru RPORx w którym znajdziemy bity opisane RP9R (będzie to rejestr RPOR2) i na tych bitach wpisujemy zapamiętaną wartość 8
Zapis do rejestru będzie wyglądał jak poniżej :


RPOR2bits.RP9R = 0x0008;      /*RB3->SPI2:SDO2;*/

Od tego momentu na pinie RB3 mamy linię SDO2 Mastera na SPI2.

Takie przyporządkowanie pinów do peryferium jest charakterystyczne dla ekosystemu PIC. Jak raz zatrybimy jak to robić to ta wiedza przenosi się na dowolny MCU PIC-a

Pinu RB8 , który jest skonfigurowany pod linię zegara SPI2 nie będę omawiał bo jego konfiguracja przebiega tak jak dla RB3 i SDO2

Mamy ustawione i skonfigurowane piny do współpracy z SPI2, czas na konfigurację SPI2 po stronie Mastera. Przypominam , że kod dla Slave jest taki sam jak w artykule PIC32MM SPI - zajęcia praktyczne
W Masterze nie będziemy używać przerwań od SPI , nie ma takiej potrzeby ponieważ wszystko przejmuje DMA , użyjemy ewentualnie przerwania DMA po opróżnieniu bufora nadawczego.
W Masterze włączymy sobie tryb ENHANCED BUFFER MODE, jak szaleć to na maksa. Niech FIFO + DMA dadzą ognia z rury :)
Cała sekwencja inicjalizacji sprzętowego SPI2 wygląda jak poniżej i znajduje się w pliku spi2.c :

void SPI2_Initialize (void)
{
      
    SPI2BRG = 0x9;                     /*Baud Rate Generator Manual SPI page 29*/
    SPI2CONbits.MSTEN = 1 ;  /*Master mode ON*/
    SPI2CONbits.SMP = 1;        /*Input Sample Phase bit*/
    SPI2CONbits.ON = 1;           /*SPI Peripheral ON*/
    SPI2CONbits.ENHBUF = 1; /*Enhanced Buffer mode ON*/
       
}

Zbliżamy się powoli do mety. Ostatnim aktem, będzie konfiguracja kanału DMA i połączenie z SPI2.

Konfiguracja DMA0 do wysyłania danych po SPI2 wygląda jak poniżej :

void DMA0_Initialize(void){

IEC3bits.DMA0IE = 0 ; /*disable DMA0 interrupt, datasheet page 66*/
IFS3bits.DMA0IF = 0 ; /*clear any existing DMA channel 0 interrupt flag, datasheet page 66*/

DMACONbits.ON = 1; /*enable the DMA controller*/
DCH0CONbits.CHEN = 0; /* channel off*/
DCH0CONbits.CHPRI = 3; /*channel priority 3*/

DCH0ECON=0; /*no start or stop IRQs, no pattern match*/
DCH0SSA = ((uint32_t)myWriteBuffer & 0x1FFFFFFF); // Convert virtual address to physical address
DCH0DSA = ((uint32_t)&SPI2BUF & 0x1FFFFFFF); // Convert virtual address to physical address

DCH0SSIZ = sizeof(myWriteBuffer); /*source size*/
DCH0DSIZ = 1; /*desination size, one byte per SPI transfer */
DCH0CSIZ = sizeof(myWriteBuffer); /*cell size*/

DCH0INTCLR = 0xFF;      /*clear existing all Interrupt flag*/
DCH0INTbits.CHCCIE = 1; /*Channel Cell Transfer Complete Interrupt Enable bit*/

IPC24bits.DMA0IP = 1 ; /*set interrupt priority*/
IPC24bits.DMA0IS = 0 ; /*set interrupt subpriority*/

DCH0CONbits.CHEN = 1; /*channel on*/
IEC3bits.DMA0IE = 1; /*enable DMA0 interrupt, datasheet page 66*/
IFS3bits.DMA0IF = 0; /*clear any existing DMA channel 0 interrupt flag*/
}

W powyższej funkcji inicjalizującej kanał DMA0 do współpracy z SPI2, kluczowym aspektem jest podanie we właściwej formie adresu źródłowego i przeznaczenia, które podajemy w rejestrach DCH0SSA i DCH0DSA. Powiem szczerze , że zawiesiłem się na tym aspekcie. W manualu DMA w przykładach przewijała się jakaś tajemnicza funkcja VirtToPchys() ale nie znalazłem rozwinięcia tego tematu. Dwa dni poszły do piachu zanim doszedłem o co kaman a chodziło o nałożenie maski 0x1FFFFFFF na zrzutowany adres bufora źródłowego z danymi myWriteBuffer i bufora SPI2. Przyznam , że w PIC-ach 16-bitowych o wiele klarowniej było DMA potraktowane, w PIC32 mamy więcej opcji i podopcji i łatwiej się w tym pogubić. 
Warto zauważyć , że w rejestrze DCH0DSIZ podaliśmy rozmiar źródła 1 bajt . Bo taki mamy domyślny rozmiar bufora sprzętowego SPI, gdybyśmy chcieli po SPI przesłać 16 lub 32 bity w jednym rzucie, to trzeba to ustawić w inicjalizacji SPI i odpowiednią ilość bajtów wpisać do rejestru DCH0DSIZ.
Rejestr  DCH0CSIZ należy traktować jako licznik przesyłanych danych i tu podajemy ile danych chcemy przesłać za pomocą DMA.

W programie po stronie Mastera, wykorzystałem przerwanie , które sygnalizuje wykonanie kompletnego transferu DMA, ten zapis włącza takie przerwanie : DCH0INTbits.CHCCIE = 1; /*Channel Cell Transfer Complete Interrupt Enable bit*/. W przerwaniu zapalamy diodę LED. Tu warto przypomnieć, że jeśli chcemy używać jakichkolwiek przerwań w PIC32 musimy koniecznie nadać priorytet dla przerwania inaczej przerwanie nie zostanie wykonane.
Definicja funkcji przerwania DMA wygląda jak poniżej :

void __ISR(_DMA0_VECTOR) __DMA0Interrupt(void)
{
PORTBbits.RB6 = 1 ;     /*set LED ON*/
DCH0INTCLR = 0xFF;      /*clear existing all DMA0 Interrupt flag*/
IFS3bits.DMA0IF = 0 ;   /*clear existing DMA0 channel 0 interrupt flag, datasheet page 66*/
}

W przerwaniu użyłem zapisu atomowego do rejestru  DCH0INT aby pokazać jak łatwo to się robi w PIC32, DCH0INTCLR = 0xFF;  tam gdzie ustawimy bity tam bity będą wyzerowane atomowo.

Transfer DMA w kanale 0 uruchamiamy za pomocą ustawienia bitu CFORCE, w rejestrze DCH0ECON. I to tyle.

Co robi program ?


Master (bez wyświetlacza LCD) po 2 sekundach zwłoki od włączenia zasilania prześle po SPI2 jeden raz paczkę danych w postaci napisu Witaj DMA , po wysłaniu danych zapali diodę LED na pinie RB6

Slave (z wyświetlaczem LCD) wyświetla napis kontrolny TEST SPI .. , odbiera paczkę danych (Witaj DMA) od Mastera i wyświetla ją na wyświetlaczu LCD w dolnym wierszu.




W artykule pokazałem jak możemy wykorzystać  DMA do współpracy z peryferiami a konkretnie z SPI . Daje nam to ciekawe możliwości i wygodę , nie trzeba m.in martwić się o funkcje nadające i odbierające po SPI , dane wrzucamy w bufor nadawczy naciskamy guzik z napisem DMA i po chwili mamy wysłane dane w tle bez angażowania rdzenia i jednocześnie odebrane dane mamy w buforze odbiorczym. O ile rozgryzienie wszystkich niuansów związanych z DMA w PIC32MM zajęło trochę czasu o tyle korzystanie z tego to już czysta przyjemność.

Pozdrawiam
picmajster.blog@gmail.com


Linki :

PIC32 Family Reference Manual - DMA Controller
PIC32 SPI Manual
Projekt PIC32MM_SPI_Master_DMA
Projekt PIC32MM_SPI_Slave 

4 komentarze:

  1. Ciekawe jak pójdzie SPI z DMA z Wiznet w5500 , jak ogarnę się z czasem udostępnię na github pic32mm z IOlibrary dla W5x00, potem sprawdzę czy DMA przyśpiesza transfer

    OdpowiedzUsuń
  2. To fajny temat ogarniasz , przewinął się mi kiedyś ten temat przez głowę ale coś słabo widziałem wykorzystanie a nie jestem zwolennikiem robienia z MCU serwera czy modułu webowego. Tę domenę zostawiam modułom embedded z MPU. Nie zmienia to faktu, że Wiznet w5500 to fajny moduł , który zwalnia MCU z posiadania stosu do obsługi Ethernetu.

    OdpowiedzUsuń
  3. znalazłem ciekawy poradnik o pic32 http://www.aidanmocke.com/blog/2019/01/08/DMA-Intro/

    OdpowiedzUsuń
  4. Dziękuję Witek za informację.

    OdpowiedzUsuń