Microchip CLC w praktyce: jak używać samodzielnych bloków peryferyjnych w mikrokontrolerach PIC16F [1]

W artykule przedstawimy sposób użycia w realnym projekcie samodzielnego bloku peryferyjnego CLC (mała matryca PLD), w jakie firma Microchip wyposaża produkowane przez siebie wybrane modele mikrokontrolerów PIC16F. Prezentowany projekt pokazuje alternatywną ścieżkę sprzętowego sterowania LED-ami RGB, bazujący na CLC.

Uwaga! Druga część artykułu jest dostępna pod adresem.

Fot. 1. Wygląd LED-RGB WS2812

Od jakiegoś czasu można kupić trójkolorowe diody LED zintegrowanie ze sterownikiem sterowanym cyfrowo. Sterownik występuje w dwu odmianach: WS2811 i WS2812. Oba działają tak samo, a różnice w oznaczeniach oznaczają trochę inne zależności czasowe przy przesyłaniu danych do układów. Z diod LED można budować kompletne ekrany, węże świetlne czy inne elementy prostej grafiki.

Idea działania budowania zestawów wielu diod jest bardzo prosta. Przyjęto, że układy oprócz wyprowadzeń zasilania będą miały jedna linię danych wejściowych DIN i jedną linię danych wyjściowych DOUT. „Magistrala” sterująca jest jedną linią po której są przesyłane szeregowo dane i powstaje z połączeń wyprowadzeń DOUT jednego układu z linią DIN następnego układu. Zostało to pokazane na rysunku 2. Maksymalne uproszczenie sprzętowe zawsze jest połączone z komplikowaniem przesyłania danych. Każdy kto próbował realizować komunikację z układami 1-wire (na przykład z termometrem DS18B20) wie, że jest z tym pewien kłopot. Nie inaczej jest w tym przypadku. Ponieważ nie ma linii taktującej przesyłaniem danych, to muszą być one tak zakodowane, żeby układ sterownika LED mógł odtworzyć sobie zegar sterujący transmisją. Poza tym musi być jakiś sposób na adresowanie kolejnych diod w łańcuchu. Nawet proste animacje graficzne będą wymagały szybkiego szeregowego przesyłania danych do wielu diod w układzie. Należy się spodziewać dużej prędkości transmisji danych kodowanych długością impulsu.

Rys. 2. Łączenie układów WS2811/WS2812 w łańcuchy

Żeby ułatwić sobie sprawę w naszych rozważaniach zajmiemy się sterownikiem WS2812, WS2811 jest sterowany bardzo podobnie. Jak już wspominałem przesyłanie danych jedna linią będzie wymagało kodowania długością impulsów. Przyjęto, że czas przesłania jednego bitu będzie taki sam dla stanu niskiego i wysokiego, a stany będą rozróżniane poprzez różny czas trwania stanu wysokiego.  Sposób kodowania stanów logicznych: wysokiego i niskiego został pokazany na rysunku 3. Żeby ułatwić implementację algorytmów sterujących dopuszczono odchyłki od standardowych czasów stanów TH i TL wynoszące +/- 150 ns i +/- 600 ns dla czasu przesłania jednego bitu.

Rys. 3. Kodowanie danych przesyłanych magistralą

Czas potrzebny na przesłanie 1 bita to nominalnie 1,25 us. Z tego wynika prędkość transmisji równa 800 kb/s. Oprócz dwu stanów: wysokiego i niskiego na magistrali można wystawić stan zerowania. Jest to stan niski na linii danych przez czas większy niż 50 us.

Każdy ze sterowników oczekuje na przesłanie trzech bajtów, czyli 24 bitów. W tych trzech bajtach jest zwarta informacja o intensywności świecenia każdej z trzech diod według zasady pokazanej na rysunku 4.

Rys. 4. Ramka sterująca intensywnością świecenia każdej z diod WS2812

W ten sposób otrzymujemy świecący „punkt”, którego kolor ma głębię 24-bitową. Po włączeniu zasilania sterowników WS2812 wszystkie diody są wygaszone i każdy ze sterowników oczekuje na odebranie danych. Pierwsze 3 bajty wysłane na magistralę są odbierane przez sterownik WS2812, którego linia DIN jest połączona z linią portu mikrokontrolera i zapala się dioda tego sterownika. Kolejne bajty będą już ignorowane przez ten układ, ale pojawią się bezpośrednio na linii DOUT. Inaczej mówiąc po odebraniu swoich trzech bajtów sterownik staje się przezroczysty dla przychodzących danych. Drugi sterownik WS2812 odbierze kolejne 3 bajty (mikrokontroler wysyła w sumie 6), zapala swoja diodę, a kolejne bajty zignoruje i będzie przesyłał na swoją linię DOUT. W ten sposób kolejne sterowniki są adresowane kolejnością przesyłanych danych. Kolejne bajty danych muszą się pojawiać jeden za drugim. Jeżeli na magistrali pojawi się stan niski dłużej niż 50 us, to wszystkie sterowniki w łańcuchu wyzerują swoje układy wykrywające odebrane dane i kolejne przesyłane dane będą odbierane ponownie od pierwszego sterownika, czyli tego, który ma linię DIN połączoną z linia porty sterownika mikroprocesorowego.

Rys. 5. Cykle zapisu danych do sterowników LED-RGB

Na pierwszy rzut oka sterowanie nie wydaje się skomplikowane. Jednak jeżeli spojrzymy na zależności czasowe, to mogą się pojawić problemy. Sterownik mikroprocesorowy musi wysyłać zakodowane czasowo bajty z dużą prędkością 800 kb/s. Najprostszą implementacją algorytmu sterowania będzie odliczanie programowych opóźnień i modyfikacja stanów logicznych na linii portu zależnie od przysłanej wartości. To rozwiązanie ma jedna zaletę: jest proste do zrealizowania, ale ma też wadę, która je dyskwalifikuje w praktycznych zastosowaniach. Załóżmy, że chcemy sterować diodami za pomocą taniego mikrokontrolera PIC16F taktowanego częstotliwością 16 MHz. Proste oszacowanie czasu potrzebnego na wykonanie zadania pokazuje, że zajmie nam to prawie 100% wydajności mikrokontrolera i to przy dużej optymalizacji kodu. Możemy zapalać diody, ale nie będziemy w stanie wykonać żadnych sekwencji sterujących wyświetlanymi kolorami. Zastosowanie wydajniejszej jednostki bez zmiany podejścia do sposobu sterowania również niewiele pomoże.

Skoro programowo nie można, to trzeba próbować wykorzystać układy sprzętowe. Pierwsza myśl jak mi przyszła do głowy, to wykorzystanie układu PWM i programowa modyfikacja współczynnika wypełnienia na podstawie wartości przesyłanego bitu. Jednak w typowej implementacji modułu PWM nie ma sprzętowej informacji o tym kiedy kończy się okres przebiegu. Wykorzystanie poolingu sprowadza się wprost do metody pierwszej. Można wykorzystać przerwanie od timera taktującego PWM, ale całość robi się skomplikowana i może wprowadzać niedopuszczalne opóźnienia. Kolejny pomysł, to wykorzystanie układu SPI do przesłania 1 bitu. Dla przesłania 0 logicznego wysyłalibyśmy sekwencję 11100000, a dla jedynki logicznej 11111000. Wysłanie zakodowanego 1 bitu wiązałoby się z wysłaniem jednego bajtu przez SPI, czyli dane przesyłane przez SPI musiałby mieć przepływność 8*0,8 Mb/s=6,4 Mb/s, a zegar taktujący częstotliwość 12,8 MHz.

W dużych 32-bitowych mikrokontrolerach taktowanych częstotliwościami 70…100 MHz jest to możliwe i takie implementacje są spotykane, jednak w naszym przypadku to się nie uda. Nie da się uzyskać taktowania SPI o częstotliwości 12,8 MHz przy taktowaniu mikrokontrolera częstotliwością 16MHz. Kolejny pomysł, to wykorzystanie przerwania od timera i sterowanie z wykorzystaniem maszyny stanów. Przerwania musiałby by być zgłaszane z częstotliwością równą 1/0,35 us= 2,85 MHz. Przy taktowaniu częstotliwością 16 MHz rdzeń i peryferie PIC16F są taktowane częstotliwością FOSC/4, czyli 4 MHz. Ten pomysł również nie może być zrealizowany.

Najlepszym praktycznym rozwiązaniem tego typu sterowań jest nieblokująca praca w tle. Zapisujemy jakiś bufor w pamięci RAM o rozmiarze n*3 bajty, (gdzie n – liczba diod na magistrali), a oprogramowanie wysyła dane jak najmniej absorbując rdzeń mikrokontrolera. Użyteczne sterowanie prostymi metodami przy użyciu standardowych układów peryferyjnych jest albo nie możliwe, albo bardzo skomplikowane. Jednak wykorzystując nowe układy peryferyjne Microchipa stosowane w mikrokontrolerach rodziny PIC16F1xxx pracujące niezależnie od rdzenia mikrokontrolera i stosując niestandardowe sprytne podejście do problemu można wykonać taki sterownik mikroprocesorowy obciążający rdzeń w minimalnym stopniu.

Idea takiego rozwiązania został opisana w nocie aplikacyjnej Microchipa AN1606. Oryginalnie nota jest przeznaczona dla mikrokontrolera PIC16G1509. Mimo, że posiadam taki układ zdecydowałem się wykorzystać nowszy PIC16F1619, który ma możliwość bardziej elastycznej konfiguracji wejść modułów peryferyjnych CLC i przez to cały projekt jest prostszy.

Fot. 6. Wygląd płytki Microchip Curiosity

Całość była testowana z wykorzystaniem firmowego modułu firmy Microchip Curiosity (fotografia 6), ale można do tego celu zbudować dowolny układ. Do budowy sterownika diod WS2812 wykorzystamy cztery sprzętowe układy peryferyjne:

  • Układ licznika TMR2 do generowania przebiegu taktującego transmisją,
  • Układ PWM do generowania części przebiegu odpowiedzialnego za czasową sekwencję stanu niskiego,
  • Układ interfejsu SPI pracującego w trybie Master do wysyłania 8 bitów danych,
  • Układ komórki logicznej CLC do wykonania funkcji logicznych niezbędnych do zakodowania czasowego wysyłanych bitów.

Jak już wiemy bity muszą być wysyłane z prędkością 800 kb/s. Intuicyjnie czujemy, że interfejs SPI należy zaprogramować tak, by wysyłał dane z taką prędkością. Z rysunku 3 wynika, że czas jednego bita to 1,25 us +/- 0,6 us. Ta tolerancja pomoże nam w realizacji zadania. Przepływność 800 kb/s oznacza, że zegar taktujący transmisją musi mieć częstotliwość 800 kHz*2=1,6 MHz. Okres tego przebiegu to 625 ns. Licznik T2 w mikrokontrolerze taktowanym z częstotliwością 16 MHz może generować przepełnienia z rozdzielczością 250 ns. Czyli możemy uzyskać okres 500 ns lub 750 ns. Ja zaprogramowałem okres 750 ns. Czas przesyłania jednego bitu przez SPI będzie wynosił 1,5 us, czyli mieści się w tolerancji. Te 750 ns przyda się też do generowania czasu potrzebnego do zakodowania stanu wysokiego, ale o tym za chwilę.

Licznik T2 będzie taktować transmisję SPI, ale też będzie źródłem zegara taktującego dla modułu PWM. Jest to bardzo ważne, bo zapewnia czasową synchronizację generowanych przebiegów. Do konfiguracji układów peryferyjnych wykorzystałem oczywiście środowisko MPLAB X IDE i wtyczkę MCC (MPLAB Code Configurator). Całość został skompilowana bezpłatną pełną wersją kompilatora MPLAB XC8.

Programowanie układów peryferyjnych zaczniemy od licznika T2. Licznik będzie się cyklicznie przepełniał co 750 ns, czyli z częstotliwością 1,3333333 MHz. Jeżeli ten licznik będzie źródłem przebiegu taktującego transmisję, to dane przez interfejs SPI będą przesyłane z prędkością ok 667 kHz.

Rys. 7. Konfiguracja licznika Timer2

Na rysunku 7 została pokazana konfiguracja Timera2 wykonana na pomocą MCC. Po skonfigurowaniu timera możemy wykorzystać sygnał cyklicznego przepełnienia do taktowania układów peryferyjnych. W naszym przypadku będzie to interfejs SPI i układ PWM3.

W konfiguracji SPI wybieramy: tryb pracy Master (moduł SPI jest źródłem sygnału zegarowego SCK) źródło sygnału taktującego moduł Timer2 – rysunek 8.

Rys. 8. Konfiguracja modułu SPI

Tak skonfigurowany moduł będzie wysyłał bity z prędkością 666,67 kb/s. Na rysunku 9 pokazano oscylogram przebiegów sygnału danych SDO (kanał 2) i zegarowego SCK (kanał 1) w czasie wysyłania przez SPI bajtu 55hex.

Rys. 9. Wysyłanie danych przez SPI z prędkością 666,67 kb/s

Przechodzimy teraz do zasadniczej części projektu, czyli kodowania danych przesyłanych do sterowników WS2812. Jak już wiemy potrzebne są dwa czasowo zakodowane stany: wysoki i niski.

Stan wysoki będziemy kodować przez wykonanie funkcji logicznej AND sygnału danej i sygnału zegarowego interfejsu SPI. Kiedy na linii danych jest stan wysoki, to na wyjściu układu realizującego pojawi się okres sygnału zegarowego taktującego transmisję. Jak już wiemy zegar częstotliwość SCK jest równa 666,67 kHz a okres 1,50 us. Czas trwania stanu wysokiego wynosi 750 ns. Według specyfikacji czas trwania stanu T1H przy przesyłaniu zakodowanego stanu wysokiego powinien mieć wartość 700 ns +/-150ns, czyli wszytko jest w porządku. Jak już wiemy okres przesyłania zakodowanego bitu według specyfikacji, to 1,25 us +/-600 ns i nasze 1,5 us również się mieści w tolerancji.

Na rysunku 10 przedstawiono przebiegi sygnału SCK, SDA i na samym dole SCK&SDO. Kiedy na linii danych jest stan wysoki, to na linii reprezentującej SCK&SDA jest jeden impuls zegara, oczywiście zboczami zsynchronizowany z linia danych SCK. Kiedy na linii danych jest stan niski, to na SCK&SDA jest również stan niski. Oscylogram z rysunku 10 został zarejestrowany na wyprowadzeniu z modułu CLC wykonującego funkcję SCK&SDA.

Rys. 10. Kodowanie stanu niskiego za pomocą funkcji SCK&SDO

Jak widać kodowanie stanu wysokiego jest banalnie proste, bo możemy wykorzystać tolerancje czasowe magistrali WS2812. W kolejnym kroku musimy znaleźć sposób na zakodowanie stanu niskiego. Do tego celu wykorzystamy układ peryferyjny PWM3. Źródłem sygnału dla tego układu będzie również Timer2. Jak już wspomniałem gwarantuje to bardzo ważną synchronizację zboczy wszystkich generowanych sygnałów. PWM3 skonfigurujemy tak, by stan wysoki przebiegu był na tyle krótki, żeby mieścił się w tolerancji dla T0H równej 0,35 us +/-150 ns. Konfiguracja TMR3 została pokazana na rysunku 11. Okres PWM wynosi 750 ns, a współczynnik wypełnienia ok 30%, czyli czas trwania stanu wysokiego ok. 210 ns. Jest to na granicy tolerancji, ale w praktyce nie ma z tym problemu. Przy tych częstotliwościach rozdzielczość sygnału PWM spada do 4 bitów i nie ma w praktyce żadnego pola manewru.

Rys. 11. Konfiguracja modułu PWM3

Uwaga! Druga część artykułu jest dostępna pod adresem.

O autorze

Absolwent Wydziału Elektroniki Politechniki Wrocławskiej, współpracownik miesięcznika Elektronika Praktyczna, autor książek o mikrokontrolerach Microchip i wyświetlaczach graficznych, wydanych nakładem Wydawnictwa BTC. Zawodowo zajmuje się projektowaniem zaawansowanych systemów mikroprocesorowych.