fbpx

Nadszedł czas, aby wykorzystać MAX7219 w nieco inny sposób niż zapewne został on domyślnie zaprojektowany. Stworzymy wyświetlacze graficzne z diod, które będą kontrolowane przez nadrzędną bibliotekę graficzną. Będzie się to odbywało w dokładnie taki sam sposób jak np. OLEDy. Zobacz jakie to jest proste 🙂

Kompletny cykl wpisów:

Nigdy więcej multipleksowania na GPIO! MAX7219 w akcji cz.1

Nigdy więcej multipleksowania na GPIO! MAX7219 w akcji cz.2

Nigdy więcej multipleksowania na GPIO! MAX7219 w akcji cz.3

MAX7219 – multipleksowanie matryc

To że układy te są przeznaczone z myślą o wyświetlaczach 7-segmentowych nie znaczy, że nie można ich wykorzystać w inny sposób. Dekoder cyfr można wyłączyć, wówczas dane można wpisywać dowolnie. Skoro jedna cyfra to 8 diod, a układ steruje ośmioma cyframi, to rachunek jest prosty – możemy w dowolny sposób wysterować 64 diody. Oczywiście przyjaciele z Chin zadbali o to, aby nie męczyć się z łączeniem tych diod. W sprzedaży dostępne są matryce 8×8 pixeli ze wspólną katodą, które są multipleksowane w taki sam sposób jak zwykłe 7-segmentowce. Po prostu diody są inaczej poukładane.

Możesz kupić moduły pojedyncze, podwójne lub poczwórne. Wszystkie są połączone w kaskadę, więc ułatwia to całe połączenie. Każdy kolejny moduł również dopinasz kaskadowo więc przesyłanie danych wygląda tak samo, jak dla cyfr z tą różnicą, że teraz mamy wyświetlacz graficzny.

Skoro są to pojedyncze pixele, to dlaczego by nie korzystać z tych matryc tak samo, jak np. OLED, o którym pisałem jakiś czas temu. Chodzi o to, aby użyć biblioteki GFX_BW która służy do rysowania prostych grafik oraz bitmap w bardzo łatwy sposób. 

Jak wygląda połączenie z matrycą?

Jak pamiętasz przy OLEDzie działaliśmy na buforze RAM, który odzwierciedlał to, co ma się znajdować na wyświetlaczu. W przypadku matryc MAX7219 zrobiłem to samo. GFX_BW do działania potrzebuje jedynie funkcji ustawiającej pojedynczy pixel. Pixel (0,0) znajduje się w lewym górnym rogu wyświetlacza. Dla przypomnienia – X’y rosą w prawą stronę, a Y’ki w dół.

Jak natomiast przyjmuje dane MAX7219 z myślą o matrycach? Myślę, że najlepiej wyjaśni to rysunek.

R[0÷7], czyli wiersze – są to kolejne cyfry, które wpisywane są do MAX7219. C[0÷7] natomiast są to adekwatne bity znajdujące się w każdej cyfrze(wysyłanym bajcie). Reasumując. Chcąc ustawić pixel (0,0) musisz ustawić siódmy bit zerowej cyfry w MAX7219. Natomiast aby ustawić pixel np. (4,2) musisz zapalić BIT3 drugiej cyfry.

Teraz wystarczy odpowiednio przypisać te dane do bufora, który będzie wypychany na układy MAX7219. W trakcie testów odkryłem, że na rynku są dwa rodzaje modułów 🙁 Nie różnią się one prawie niczym. No właśnie prawie. Jest jeden, mały szczegół – połączenia układu scalonego z matrycą LED są inne. Są dokładnie takie jak na rysunku poniżej.

Różnica polega na lustrzanym połączeniu cyfr oraz ich bitów. Trochę tak, jakby zamienić little-endian na big-endian lub odwrotnie. Sprawia to, że wypełnienie bufora według pierwszego połączenia jest błędne dla tego drugiego i w efekcie napisy i obrazy wyświetlane są błędnie, o czym informowałen na moim fanpage.

Na szczęście ogarnąłem to na poziomie funkcji ustawiającej pixel, więc wyższe warstwy są zupełnie niewzruszone.

W ostateczności funkcja rysująca pixel wygląda tak:

MAX7219_STATUS MAX7219_SetPixel(int x, int y, MAX7219_Color Color)
{
	if ((x < 0) || (x >= MAX7219_X_PIXELS) || (y < 0) || (y >= MAX7219_Y_PIXELS))
		return MAX7219_OUT_OF_RANGE;
	switch(Color)
	{
		#if(MAX7219_MODULE_TYPE == 0)
		case MAX7219_WHITE: Max7219PixelsBuffer[(x/8) + (y*MAX7219_COLUMNS)] |= (0x80 >> (x&7)); break;
		case MAX7219_BLACK: Max7219PixelsBuffer[(x/8) + (y*MAX7219_COLUMNS)] &= ~(0x80 >> (x&7)); break;
		case MAX7219_INVERSE: Max7219PixelsBuffer[(x/8) + (y*MAX7219_COLUMNS)] ^= (0x80 >> (x&7)); break;
		
		#elif(MAX7219_MODULE_TYPE == 1)
		case MAX7219_WHITE: Max7219PixelsBuffer[(x/8) + ((MAX7219_PIXELS_PER_DEVICE_ROW-1) - ((y%8)) + ((y/8)*MAX7219_PIXELS_PER_DEVICE_ROW))*MAX7219_COLUMNS] |= (1 << (x&7)); break;
		case MAX7219_BLACK: Max7219PixelsBuffer[(x/8) + ((MAX7219_PIXELS_PER_DEVICE_ROW-1) - ((y%8)) + ((y/8)*MAX7219_PIXELS_PER_DEVICE_ROW))*MAX7219_COLUMNS] &= ~(1 << (x&7)); break;
		case MAX7219_INVERSE: Max7219PixelsBuffer[(x/8) + ((MAX7219_PIXELS_PER_DEVICE_ROW-1) - ((y%8)) + ((y/8)*MAX7219_PIXELS_PER_DEVICE_ROW))*MAX7219_COLUMNS] ^= (1 << (x&7)); break;
		
		#endif
		
		default: return MAX7219_ERROR;
	}
	return MAX7219_OK;
}

Wyboru tego, z którym rodzajem modułu będziesz walczył dokonujesz przez stałą MAX7219_MODULE_TYPE. Typ ustawiony na 0 to pierwszy, który pokazałem – bardziej ludzki. Jeżeli masz ten drugi moduł, to ustaw tą stałą na 1.

Ustawienia biblioteki

Oprócz wspomnianego wyżej wyboru typu matrycy należy ustawić jeszcze kilka stałych.

Po pierwsze ilość urządzeń, czyli ile masz scalaków MAX7219 w układzie. Dla kaskady z wyświetlaczy 7-segmentowych była to jedna wartość. Tutaj zrobiłem dwie: 

  • Ilość wierszy – MAX7219_ROWS
  • Ilość kolumn – MAX7219_COLUMNS

Możesz bowiem połączyć matryce nie tylko jedna za drugą, ale także jedna pod drugą. Istnieje jeden warunek, który należy spełnić, aby biblioteka działała poprawnie. DOUT ostatniego układu w wierszu MUSISZ połączyć z pierwszym układem w kolejnym wierszu.

Na podstawie rysunku widzisz również jak ustawić ilość urządzeń. Mamy 3 wiersze i 2 kolumny. W pliku nagłówkowym więc będzie wyglądało to tak.

//
// Configuration
//
#define MAX7219_ROWS 3
#define MAX7219_COLUMNS 2

Ostatnim ustawieniem jest decyzja, czy używasz sprzętowej kontroli pinu CS. Opisywałem to również we wpisie z OLEDami

No dobra, ale co z rozdzielczością, a co za tym idzie – rozmiarem bufora. Postanowiłem, że te ustawienia będą się obliczały same na podstawie ilości scalaków w układzie. Wiedząc, że każde urządzenie ma 8×8 pixeli, oraz ile urządzeń jest w każdym kierunku, łatwo jest obliczyć rozdzielczość całego wyświetlacza. Całość rozwiązana jest wygodnymi w użyciu makrami

//
// Resolution
//
#define MAX7219_PIXELS_PER_DEVICE_ROW 8
#define MAX7219_PIXELS_PER_DEVICE_COLUMN 8
#define MAX7219_X_PIXELS (MAX7219_PIXELS_PER_DEVICE_ROW*MAX7219_COLUMNS)
#define MAX7219_Y_PIXELS (MAX7219_PIXELS_PER_DEVICE_COLUMN*MAX7219_ROWS)
#define MAX7219_DEVICES (MAX7219_ROWS * MAX7219_COLUMNS)

Funkcje biblioteki

Oprócz standardowej inicjalizacji, masz do dyspozycji 3 funkcje, z których będziesz intensywnie korzystał.

MAX7219_STATUS MAX7219_SetPixel(int x, int y, MAX7219_Color Color);
MAX7219_STATUS MAX7219_Clear(MAX7219_Color Color);
MAX7219_STATUS MAX7219_Display(void);

MAX7219_SetPixel do ustawiania pixela, którą przekazujesz do nadrzędnej biblioteki GFX_BW.

MAX7219_Clear do czyszczenia bufora RAM na ustalony kolor.

MAX7219_Display do przetransferowania bufora RAM na wyświetlacz.

Więcej nie jest już nam potrzebne. Całą resztę załatwia GFX_BW.

Działanie wyświetlacza

Wziąłem na biurko dwa moduły po 4 układy MAX7219. Połączone jeden za drugim budują wyświetlacz o rozdzielczości 64 x 8 pixeli. Do konfiguracji jest aż 8 urządzeń oraz zawsze każda “cyfra” w układach MAX7219 jest ustawiana, nawet jeżeli żaden pixel się nie zmienia. Dlatego też odświeżenie całego wyświetlacza będzie trochę trwało. Zobaczmy ile.

SCK 1,25 MHz

SCK 2,5 MHz

SCK 5 MHz

SCK 10 MHz

  • Zegar 1,25 MHz – 8,45 ms
  • Zegar 2,5 MHz – 4,38 ms
  • Zegar 5 MHz – 2,34 ms
  • Zegar 10 MHz – 1,32 ms

Różnice pomiędzy wynikami są takie, jakbym się spodziewał. No dobrze, ale nic Ci to pewnie nie mówi. Ustawię teraz tyle urządzeń, aby rozdzielczość wyszła 128 x 64 jak w przypadku OLEDów. Będzie to 16 x 8 urządzeń, czyli łącznie 128 układów MAX7219. Nie mam ich aż tyle, ale na pinie SDIN i tak przecież pojawi się sygnał dla nich 🙂

I co?! Aż 291 ms! Masakra… Na co mi taki wyświetlacz, który jestem w stanie odświeżyć tylko 3 razy na sekundę… Czy da się to przyśpieszyć? Co jakbym chciał zbudować kilkukrotnie większą matrycę?!

Speed up!

Tak wolne działanie jest niedopuszczalne. Problem polega na tym, że wykonujemy tylko jedno przypisanie do jednego scalaka na raz, podczas gdy do pozostałych wysyłane są No-op’y. W ten sposób wysyłając informacje do 128 układów, na 8192 pixeli ustawiamy tylko 8. Jak łatwo policzyć, trzeba 1024 razy przesłać informację o ośmiu pixelach do wszystkich 128 MAX’ów. 

Jak można to przyśpieszyć? Można wyeliminować te nieszczęsne No-op’y. Zamiast wysyłać zera do prawie wszystkich układów, wysyłajmy zawsze do każdego z nich przydatne informacje. Można przecież wysłać do każdego dane dla zerowych cyfr za jednym zamachem. Później dla pierwszych cyfr, drugich itd. W ten sposób przetransferujemy dane dla 128 urządzeń tylko 8 razy!

W tym celu należy przerobić funkcję MAX7219_Display  w taki sposób, aby nie wysyłała danych do pojedynczych układów, a pakowała w swój bufor dane dla każdego MAX’a i wtedy pchała go na SPI.

Oczywiście udało mi się zmodyfikować w ten sposób kod. Dla testowych ośmiu układów przesłanie całej ramki trwa kilkukrotnie szybciej.

SCK 1,25 MHz

SCK 2,5 MHz

SCK 5 MHz

SCK 10 MHz

  • Zegar 1,25 MHz – 1,06 ms
  • Zegar 2,5 MHz – 0,55 ms
  • Zegar 5 MHz – 0,29 ms
  • Zegar 10 MHz – 0,175 ms

Piękna redukcja czasu odświeżania. Jeszcze tylko zerknę jak to wygląda dla rozdzielczości 128×64 i 10 MHz na zegarze:

BUM! Tylko 2,34 ms. To ponad stukrotny zysk!

Zestawię jeszcze wszystkie zebrane przeze mnie dane, abyś miał wygodne porównanie.

8 IC (64 x 8 px)8 IC (64 x 8 px) optimized128 IC (128 x 64 px)128 IC (128 x 64 px) optimized
1,25 MHz 8,45 ms1,06 ms2,13 s16,70 ms
2,5 MHz4,38 ms0,55 ms1,08 s8,50 ms
5 MHz2,34 ms0,30 ms554 ms4,39 ms
10 MHz1,32 ms0,175 ms291 ms2,34 ms

A tak prezentuje się działanie.

Podsumowanie

Oczywiście można ten kod w razie potrzeby jeszcze udoskonalać, chociażby za pomocą DMA. Ja natomiast na tym poprzestałbym tę serię wpisów o MAX7219. Szczerze mówiąc nie myślałem, że wyjdą aż trzy wpisy na ten temat. Układy te wydawały mi się banalne, ale zagłębiając się w nie miałem niezły fun z udoskonalania kodu. Mam nadzieję, że czytając bawiłeś się chociaż w połowie tak dobrze jak ja, huehue 🙂

MAX7219 jest świetnym układem scalonym. Jeżeli masz możliwość wrzucenia go do projektu, to polecam zastępując klasyczne multipleksowanie na GPIO. Nie ma co się mordować z optymalizacją kodu. Daj znać w komentarzu co myślisz o tych układach.

Pełny projekt wraz z biblioteką znajdziesz jak zwykle na moim GitHubie: link

Jeśli zauważyłeś jakiś błąd, nie zgadzasz się z czymś, chciałbyś coś dodać istotnego lub po prostu uważasz, że chciałbyś podyskutować ne ten temat, napisz komentarz. Pamiętaj, że dyskusja ma być kulturalna i zgodna z zasadami języka polskiego.

5/5 - (3 votes)

Podobne artykuły

.

5 komentarzy

ft9000 · 12/01/2024 o 23:40

Ciekawy materiał 🙂 brawo, natomiast mam:
[1] (spostrzeżenie) – nie wiem czy to wina MAX7219 czy kodu wysyłającego dane ale w Twojej prezentacji (film) przy przewijaniu “obrazu” na matrycach , pojawiają się “duchy” czyli w kroku przesunięcia nie znika informacja na wcześniejszej kolumnie danych a wyświetlane są przez chwilę te same dane i przesunięte. / przed przesunięciem obrazu, aby było prawidłowo zobrazowane przesunięcie, musiałby układ wygasić najpierw zawartość na tej pozycji przed krokiem przesunięcia a następnie wyświetlić w nowym miejscu. Możliwe, że poprawa przez wysyłanie wygaszenia bardzo pogorszy wydajność…

[2] jakie jest “zużycie prądu” przez (wyświetlacze i MAX’y) przy demonstracji na Twoim filmie?

Mahmood · 18/02/2022 o 17:28

Cześć, drogi przyjacielu
Dziękujemy za opublikowanie kodu max7219.

Z opublikowanym przez Ciebie kodem bez problemu udało mi się ustawić matrycę punktową o numerze części 1088AS. Ale Dot Matrix ma problem z numerem części 1088BS + i słowa są zaśmiecone.
Czy istnieje rozwiązanie problemu?

Wdzięczny

    Mateusz Salamon · 20/02/2022 o 15:34

    Musisz zobaczyć czym się różnią, bo nie mam pojęcia 🙁

dambo · 09/06/2019 o 08:23

Ja mam jedną uwagę/sugestię (a właśnie przy pisaniu tego pojawiła się druga – nie znika ją opisy pól w tym formularzu – jak zaczniesz pisać to zobaczysz) – w kodzie biblioteki masz kilka wywołań:
“`
#ifndef SPI_CS_HARDWARE_CONTROL
HAL_GPIO_WritePin(MAX7219_CS_GPIO_Port, MAX7219_CS_Pin, GPIO_PIN_RESET);
#endif
“`
można takie rzeczy ładnie przerzucać do oddzielnych funkcji i wtedy w głównych funkcjach masz o wiele większy porządek, a makra są “ukryte”

    Mateusz Salamon · 09/06/2019 o 22:38

    Rozumiem, że chodzi o formularz komentarzy, tak? Co do makr, to masz rację. Można to pochować i będzie ładniej. Przy takim małym makrze nie będzie tego aż tak widać, ale mam w przygotowaniu taki kod, że na pewno zrobi go czytelniejszym. Dzięki 🙂

Dodaj komentarz

Avatar placeholder

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *