Microchip CLC w praktyce: jak używać samodzielnych bloków peryferyjnych w mikrokontrolerach PIC16F [2]
W drugiej części artykułu (pierwsza jest dostępna pod adresem) przedstawiamy sposoby konfiguracji modułów peryferyjnych CLC, na których jest implementowana sprzętowa część interfejsu sterującego LED-RGB. Przedstawiamy także procedury programowe realizujące funkcję interfejsu.
Teraz musimy określić funkcję logiczną, która w czasie trwania stanu logicznego niskiego wygeneruje na wyjściu jeden puls sygnału PWM. Najprościej będzie zanegować sygnał SDO i zrobić iloczyn logiczny z sygnałami SCK i PWM, czyli funkcja generowania stanu niskiego będzie równa (negacja)SDO&SCK&PWM3. Na potrzeby testowania układu zdefiniowałem układ CLC1 wykonujący tę funkcję – rysunek 12. Oscylogram generowania zakodowanego sygnału logicznego niskiego został pokazany na rysunku 13.
Mamy dwa osobne przebiegi, które teraz trzeba złożyć w jeden. Ponieważ oba przebiegi dla przeciwstawnych stanów mają poziom niski, to można je wprost zsumować. Otrzymujemy zatem funkcję (SDO&SCK)||(!SDO&SCK&PWM3).
Moduł CLC1 wykonuje operację !SDO&SDA&PWM3. Skonfigurujemy teraz moduł kolejny moduł CLC3 sumujący wyjście z CLC1 z iloczynem sygnałów SDO i CLC odpowiadającym za kodowanie stanu wysokiego – rysunek 15.
Przebieg na wyjściu CLC3 został pokazany na rysunku 16.
Sygnał z wyjścia modułu CLC3 przekierowujemy na jedną z linii portów i można sterować moduły WS2812.
Żaden z układów CLC nie może wykonać samodzielnie funkcji (SDO&SCK)||(!SDO&SCK&PWM3) i trzeba użyć dwu modułów. Można zastosować metody minimalizacji funkcji logicznych i tak ją przekształcić, żeby była wykonywana tylko przez jedne CLC. Na rysunku 17 pokazano konfigurację CLC równoważną naszej funkcji.
Pierwsze próby działania polegały na przesyłaniu danych za pomocą funkcji uint8_t SPI_Exchange8bit(uint8_t data) wpisującej daną do rejestru SSPBUF modułu SPI i czekającej w pętli na ustawienie bitu informującego o wysłaniu wszystkich 8 bitów przez wyjście SDO – listing 1.
List. 1. Funkcja wysyłająca jeden bajt przez SPI
uint8_t SPI_Exchange8bit(uint8_t data) { // Clear the Write Collision flag, to allow writing SSP1CON1bits.WCOL = 0; SSPBUF = data; while(SSP1STATbits.BF == SPI_RX_IN_PROGRESS) { } return (SSPBUF); }
Oczywiście użycie tej funkcji nie może być docelowe, bo czekanie w pętli na przesłanie bajtu blokuje całkowicie mikrokontroler. Jednak do początkowych testów zupełnie to wystarcza. Początkowo napisałem sobie funkcję wysyłającą 3 kolejne bajty wyliczane na podstawie 24-bitowej liczby określającej kolor – listing 2.
List. 2. Wysłanie 3 bajtów koloru
void SendColor(uint24_t color) { SPI_Exchange8bit(color>>16); SPI_Exchange8bit(color>>8); SPI_Exchange8bit(color); }
Jednak użycie tej funkcji nie dawało spodziewanych rezultatów. Użycie oscyloskopu pokazało, że pierwszy bajt jest wysyłany z opóźnieniem ok. 10 us. Ten czas jest wykorzystywany przez mikrokontroler do wywołania funkcji i wykonania przesunięcia zmiennej color o 16 pozycji w prawo i jest wynikiem użycia stosunkowo wolnego mikrokontrolera i nieoptymalizowanej wersji kompilatora XC8. Widać, że 10 us to czas dłuższy niż czas potrzebny na wysłanie 1 bajtu i najprawdopodobniej sterownik WS81282 po takiej przerwie ignorował ten bajt. Można to było ominąć stosując fragment funkcji napisany w asemblerze, ale to nie było istotne w prowadzonych testach. Zmodyfikowana procedura wysłania 3 bajtów została pokazana na listingu 3.
List. 3. Zmodyfikowana procedura wysłania 3 bajtów koloru
void SenColor(uint8_t G,uint8_t R, uint8_t B)
{ SPI_Exchange8bit(G); SPI_Exchange8bit(R); SPI_Exchange8bit(B); }
Funkcja używa trzech 8-bitowych argumentów, osobnego dla każdego z kolorów składowych. Po użyciu tej funkcji pierwszy bajt był wysyłany z opóźnieniem ok. 5 us i wszystko zaczęło działać prawidłowo. Kolejny listing pokazuje fragment programu zapalający cyklicznie wszystkie diody w moim module składającym się z 24 diod.
List. 4. Nieskończona pętla zapalająca cyklicznie 24 diody
color=0x550000; while(1){ g=color>>16; r=color>>8; b=color; for(i=0;i<8;i++){ SPI_Exchange8bit(g); SPI_Exchange8bit(r); SPI_Exchange8bit(b); SPI_Exchange8bit(b); SPI_Exchange8bit(g); SPI_Exchange8bit(r); SPI_Exchange8bit(r); SPI_Exchange8bit(b); SPI_Exchange8bit(g); SPI_Exchange8bit(0); } __delay_ms(100); color=(color>>8); if(color==0) {color=0x550000; } }
Ta faza testów miała za zadania określenie, czy generowane przebiegi sterujące powtarzane w nieskończonej pętli nie powodują błędnego sterowania układami WS2812. Uruchomienie programu przez dłuższy czas nie powodowało widocznego błędnego działania.
Kolejny etap testów, to stworzenie aplikacji, która nie blokuje czasu procesora. Do tego celu postaramy się wykorzystać przerwania generowane przez moduł SPI. To co ma być wyświetlane zostanie zawarte w buforze pamięci o rozmiarze 24*3=72 bajty. Wszelkie modyfikacje wyświetlanej informacji będą się sprowadzały do modyfikacji zwartości tego bufora i zainicjowania procesu wysłania jego zwartości do sterowników WS2812.
W pierwszym kroku trzeba zdefiniować obsługę przerwania od modułu SPI. Przerwania jest zgłaszane przez ustawienie znacznika SSP1IF po wysłaniu ostatniego, ósmego bitu przez wyjście SDO. Wcześniej trzeba odblokować: globalny system przerwań, system przerwań od modułów peryferyjnych i przerwanie od modułu SPI. Globalny system przerwań jest odblokowywany przez biblioteczną funkcję, a właściwie macro INTERRUPT_GlobalInterruptEnable(), system przerwań przez macro INTERRUPT_PeripheralInterruptEnable(), a przerwania od modułu MSSP skonfigurowanego do pracy SPI Master przez ustawienie bitu SSP1IE w rejestrze PIE1: PIE1bits.SSP1IE=1. Generowanie funkcji obsługi możemy „zlecić” wtyczce MCC. W oknie Interrupt Module zaznaczamy opcję przerwania zgłaszanego po opróżnieniu rejestru SSPBUF – rysunek 18.
Po wygenerowaniu plików przez MCC w pliku interrupt_manager.c zostanie zdefiniowana funkcja interrupt INTERRUPT_InterruptManager (void) pokazana na listingu 5.
List. 5. Funkcja obsługi przerwań
void interrupt INTERRUPT_InterruptManager (void) { // interrupt handler if(INTCONbits.PEIE == 1 && PIE1bits.SSP1IE == 1 && PIR1bits.SSP1IF == 1) { SPI_ISR(); } else { //Unhandled Interrupt } }
Oryginalnie procedura obsługi po ustawieniu SSPI1IF miała nie wiedzieć czemu nazwę I2C_ISR() i zmieniłem ją na odpowiedniejszą SPI_ISR(). MCC nie generuje szkieletu SPI_I2C i po zmianie nazwy SPI_ISR i trzeba ja sobie samemu napisać od początku. Nasza funkcja została pokazana na listingu 6.
List. 6. Procedura obsługi przerwania od SPI
void SPI_ISR(void) { // clear the SPI interrupt flag PIR1bits.SSP1IF=0; WS2812_Send(); }
Po koniecznym programowym wyzerowaniu wskaźnika (flagi) SSP1IF wywoływana jest właściwa funkcja wysłania zawartości bufora Buffer[] o rozmiarze 72 bajtów przez interfejs SPI. Ta funkcja została pokazana na listingu 7.
List. 7. Wysłanie bufora buffer przez SPI
volatile uint8_t point=0; //licznik adresujący elementy w buforze uint8_t buffer[72]={0x55,0,0,0,0x55,0,0,0,0x55,//bufor z predefiniowanymi wartościami 0x55,0,0,0,0x55,0,0,0,0x55, 0x55,0,0,0,0x55,0,0,0,0x55, 0x55,0,0,0,0x55,0,0,0,0x55, 0x55,0,0,0,0x55,0,0,0,0x55, 0x55,0,0,0,0x55,0,0,0,0x55, 0x55,0,0,0,0x55,0,0,0,0x55, 0x55,0,0,0,0x55,0,0,0,0x55,}; void WS2812_Send(void){ if(point>=MAX_BUF_SPI) { PIE1bits.SSP1IE=0; point=0; return;} SSPBUF=buffer[point++]; }
Dodatkowo zdefiniowana zmienna point adresuje kolejne bajty z bufora. Żeby transmisja wielu bajtów z wykorzystaniem przerwań mogła działać najpierw musimy ją zainicjować przez zapisanie bajtu do rejestru SSPBUF. Można wysłać pierwszy bajt z bufora, ale ja wysyłam bajt zerowy, odczekuję 1 ms dla wyzerowania magistrali danych WS1282 i odblokowuję przyjmowanie przerwań. Po wysłaniu bajtu zerowego ustawi się flaga SSPIF i po odblokowaniu przerwań zostanie ono przyjęte. Przyjęcie pierwszego przerwania uruchamia automatyczne wysyłanie kolejnych bajtów z bufora. Po wysłaniu 72 bajtów procedura obsługi przerwania wyzeruje wskaźnik adresowy point i zablokuje przyjmowanie przerwań przez wyzerowanie bitu SSP1IE. Na listingu 8 pokazano fragment programu uruchamiający wysyłanie zawartości bufora.
List. 8. Inicjowanie wysyłania zawartości bufora buffer
SPBUF = 0; __delay_ms(1); PIE1bits.SSP1IE=1;
Tak oto otrzymaliśmy mechanizm pozwalający na sterowanie łańcuchem układów WS1282 wykorzystując unikalne moduły peryferyjne mikrokontrolerów rodziny PIC16F1xxx i nietypowe, sprytne podejście do rozwiązania problemu. Wszystko odbywa się w tle programu głównego z wykorzystaniem przerwań i oczywiście sprzętowych zasobów mikrokontrolera jednocześnie obciążając go w niewielkim stopniu . Można teraz wymyślać i programować sposoby wizualizacji swoich własnych efektów graficznych. Jednym ze sposobów na zbudowanie w pełni funkcjonalnego sterownika jest dołączenie poprzez magistralę I2C pamięci Flash o dużej pojemności, w której zapiszemy swoje wzorce wyświetlanych sekwencji efektów.