Często spotykam się ze stwierdzeniem, że wyświetlacz LCD lepiej jest podłączyć przez ekspander I²C, bo przecież to “zjada” tylko dwa piny mikrokontrolera, a nie minimum 6. To prawda, że oszczędza piny, ale jak wiadomo nie od dziś – nie ma nic za darmo. Sprawdzę dzisiaj dla Ciebie to z czym musisz się liczyć, gdy podłączysz LCD po I²C przez popularne moduły oparte o układy PCF8574.
Poprzednie wpisy dotyczące LCD 16×2 znajdziesz tutaj:
Wyświetlacz LCD 16×2 na STM32 + HAL cz.1
Wyświetlacz LCD 16×2 na STM32 + HAL cz.2
Konwerter LCD na I²C
Konwerter LCD na I2C jest bardzo prosty w budowie. Zawiera wspomniany wyżej ekspander PCF8574, kilka rezystorów, potencjometr do nastawy kontrastu oraz tranzystor. Zerknijmy do noty układu scalonego.
Pierwsze co się rzuca w oczy to to, że porty, które oferuje są dwukierunkowe, czyli możemy zarówno pisać na wyjścia, jak i z nich czytać. To dobrze, bo będzie możliwość pracy z flagą zajętości wyświetlacza.
Zasilanie PCF8574
PCF8574 można zasilić już od 2,5 V co również jest dobrą nowiną, lecz niestety wyświetlacz potrzebuje dostać 5 V na dzielnik do kontrastu oraz na podświetlenie. O ile ciemniejsze podświetlenie można przeżyć, tak z 3,3 V podane na kontrast praktycznie nie ma szans na poprawne działanie wyświetlacza. Jedynym ratunkiem byłby LCD z ustawionym kontrastem na stałe. Są one mniej popularne i słabiej dostępne. Czyli wychodzi na to, że cały układ konwertera trzeba zasilić z 5 V. OK, ale co z pinami komunikacyjnymi? Przecież STM32 chodzi na 3,3 V! Nic strasznego, bo przecież prawie wszystkie piny I/O STMa są 5V-tolerant. No dobra, a w drugą stronę? Czy ekspander będzie wiedział, kiedy na liniach I²C jest stan wysoki? Stan wysoki PCF8574 interpretuje od 0,7 * VDD, co przy zasilaniu napięciem 5 V daje 3,5 V. Ajjj 0,2 V powyżej tego, co jest w stanie dać STM32…
Rozwiązania są dwa(a nawet 3 po głębszym zastanowieniu się). Po pierwsze można zastosować konwerter napięć i mieć pewność, że układ scalony odczyta zawsze to, co było mu wysłane. Dobre rozwiązanie, ale wprowadza kolejne elementy.
Drugim rozwiązaniem jest po prostu spróbowanie z tym, co masz 🙂 Z mojego doświadczenia wynika, że układy interpretują stan wysoki przy trochę niższym napięciu niż podaje producent w nocie. Taki bezpieczny zapas po prostu. Niestety dla każdej partii układu scalonego może być inaczej i na przykład trafi się seria, która trzyma twardo parametry z dokumentacji. Nie zalecam tego, gdy budujesz finalne urządzenie np. do sprzedaży. Lepiej trzymać się wtedy tego, co podaje producent na papierze. Pozwoli to uniknąć niespodzianek z Twojej winy. Co innego do przetestowania na biurku, czy w urządzeniach amatorsko-hobbystycznych 🙂 Raz można przymknąć oko i tak teraz zrobię.
I to jest 100% prawda dla wyjść typu Push-Pull. Teraz przypomnij sobie jak wyglądają wyjścia układów dla interfejsu I²C. Otóż mamy do czynienia z wyjściami typu Open Drain. W skrócie – mamy tylko jeden stan aktywny – zero logiczne. Jedynkę ustala rezystor podciągający na liniach SDA i SCL. Jak możesz zauważyć na schemacie konwertera, rezystory te podłączone są do VDD, czyli 5 V. Stąd stan wysoki na liniach komunikacyjnych będzie na takim właśnie poziomie. bez względu na to na jakich napięciach działają komunikujące się ze sobą układy. Ważne, aby wyjścia tych układów tolerowały tak wysokie napięcie dla jedynki logicznej. Zarówno PCF8574 jak i STM32 tolerują 5 V na swoich wejściach. Będzie działać 🙂
Prędkość komunikacji I²C
Jest jeden parametr, który nie maluje uśmiechu na mej twarzy. Jest to maksymalna częstotliwość zegara I²C. Niestety układ obsługuje tylko tryb standardowy, czyli 100 kHz. Można próbować podkręcać zegar, ale nie ma gwarancji, że będzie to działało poprawnie. Będzie to miało ogromny wpływ na wyniki moich obserwacji.
Schemat konwertera
Znalazłem w Internecie schemat takiego chińskiego konwertera.
Co z niego można ciekawego wyczytać? Otóż jest dostępna kontrola pinu R/W. Można więc czytać flagę zajętości, dzięki której znacznie przyśpiesza się komunikacja w klasycznym sterowaniu.
Inną ciekawostką jest podłączenie podświetlenia przez tranzystor oraz sterowanie tymże portem P3 ekspandera. Można więc sterować podświetleniem jednak nie liczyłbym na PWM. Zwykłe włącz-wyłącz.
Są zworki adresowe, więc na upartego możemy podłączyć 8 wyświetlaczy do jednej magistrali I²C.
Ekspander połączony jest w 4-bitowym trybie obsługi LCD. Nie jest to problemem, bo wykazałem już, że 8-bitowa obsługa jest bez sensu.
Schemat i konfiguracja STM32CubeMX
Dla porównania komunikacji przez I²C z klasycznym sterowaniem wyświetlacza potrzebować będę jak najbardziej zbliżonych warunków. Dlatego użyję Nucleo z STM32F401RE tak samo, jak we wpisach, w których zajmowałem się porównaniem klasycznych metod sterowania kontrolerem HD44780. Zegar HCLK ustaw na 84 MHz. Ekspander podłącz do I2C1 według poniższego schematu.
Jako IDE wykorzystam STM32CubeIDE, w którym od razu mogę konfigurować projekt. Ustawić musisz dwie rzeczy. Po pierwsze I2C1 na pinach PB8 i PB9(domyslnie będą na PB6 i PB7). Speed Mode jako Standard Mode i zegar 100 kHz. Reszta domyślnie.
Drugą rzeczą, którą musisz ustawić jest timer. Całkiem możliwe, że spytasz dlaczego. Jeżeli pamiętasz poprzednie wpisy o LCD to był tam wykorzystywany delay o rozdzielczości 1 µs. Niestety HAL nie dostarcza timera o tak małym ticku, więc trzeba go sobie samemu stworzyć. Wybierz TIM3, jako źródło zegara wybierz Internal Clock. Wpisz w pole Prescaler wartość 83(sprowadzi to zegar to 1 MHz, co da idealnie 1 µs na tick) oraz ustaw maksymalną wartość licznika na 0xFFFF(po prawej stronie pola wartości możesz wybrać formatowanie hexadecymalne).
Projekt wygeneruj z rozdzieleniem plików od peryferiów.
Kod dla konwertera I2C
Większość kodu, który używam do obsługi LCD jest już gotowa. Użyję tego, który pisałem dla klasycznych interfejsów. Biblioteka jest tak napisana, że jedyne co muszę zmienić to funkcje wysyłające dane na wyświetlacz oraz czytające z niego.
Dla ułatwienia powołałem sobie zmienną 8-bitową dla trzymania bajtu, który wysyłam do ekspandera. Dzięki niej mam również pogląd na to, co było wcześniej wysłane. Dlatego też nie będę musiał zastanawiać się, w jakim stanie były wcześniej piny sterujące. Wszelkich zmian będę dokonywał bitowo poprzez maskowanie.
Z tego powodu, że bity sterujące również są podłączone do ekspandera, musiałem dopisać funkcje modyfikujące te piny na konwerterze. Funkcja ustawiania danych jest zbliżona do klasycznej wersji. Jedynie na końcu ustawiane dane trzeba wysłać po I²C.
// // Send/Read data to/from expander function // void LCD_SendDataToExpander(uint8_t *Data) { HAL_I2C_Master_Transmit(hi2c_lcd, LCD_I2C_ADDRESS, Data, 1, LCD_I2C_TIMEOUT); } // // Set data port // static inline void LCD_SetDataPort(uint8_t Data) { ByteToExpander &= ~(0xF0); // Clear Data bits if(Data & (1<<0)) ByteToExpander |= D4_BIT_MASK; if(Data & (1<<1)) ByteToExpander |= D5_BIT_MASK; if(Data & (1<<2)) ByteToExpander |= D6_BIT_MASK; if(Data & (1<<3)) ByteToExpander |= D7_BIT_MASK; LCD_SendDataToExpander(&ByteToExpander); } // // Control signals // static inline void LCD_SetRS(void) { ByteToExpander |= RS_BIT_MASK; LCD_SendDataToExpander(&ByteToExpander); } static inline void LCD_ClearRS(void) { ByteToExpander &= ~(RS_BIT_MASK); LCD_SendDataToExpander(&ByteToExpander); } static inline void LCD_SetEN(void) { ByteToExpander |= EN_BIT_MASK; LCD_SendDataToExpander(&ByteToExpander); } static inline void LCD_ClearEN(void) { ByteToExpander &= ~(EN_BIT_MASK); LCD_SendDataToExpander(&ByteToExpander); } static inline void LCD_SetRW(void) { ByteToExpander |= RW_BIT_MASK; LCD_SendDataToExpander(&ByteToExpander); } static inline void LCD_ClearRW(void) { ByteToExpander &= ~(RW_BIT_MASK); LCD_SendDataToExpander(&ByteToExpander); } void LCD_BacklightOff(void) { ByteToExpander &= ~(BL_BIT_MASK); LCD_SendDataToExpander(&ByteToExpander); } void LCD_BacklightOn(void) { ByteToExpander |= BL_BIT_MASK; LCD_SendDataToExpander(&ByteToExpander); }
Reszta pozostała prawie bez zmian, ale o tym za chwilę.
Jak pamiętasz do testowania czasów komunikacji miałem specjalną funkcję. Czyściła ona LCD i pisała po wszystkich znakach na wyświetlaczu.
while (1) { HAL_GPIO_WritePin(TEST_GPIO_Port, TEST_Pin, 1); // Measurement start LCD_Cls(); LCD_Locate(0,0); LCD_String(" STM32 + HD44780"); LCD_Locate(0,1); LCD_String("www.msalamon.pl "); // Measurement end HAL_GPIO_WritePin(TEST_GPIO_Port, TEST_Pin, 0); HAL_Delay(500); }
Dla przypomnienia wstawiam tabelę z poprzednimi rezultatami dla trybów 4 i 8-bitowych oraz bez i z flagą zajętości.
Tryb | Czas |
---|---|
4-bit HAL_Delay | 67,29 ms |
4-bit bez BF | 7,14 ms |
4-bit z BF | 2,74 ms |
8-bit bez BF | 7,11 ms |
8-bit z BF | 2,74 ms |
Sprawdźmy, czy wynik testu będzie podobny. Na pierwszy ogień idzie obsługa bez czytania flagi zajętości. Opóźnienie realizowane przez timer z tickiem 1 µs. Jak wykazałem wcześniej, nie ma sensu korzystać z delay’a HALowego.
Niestety KATASTROFA czasowa. Aż 55,72 ms. Ta sama czynność dla trybu 4-bitowego bez flagi zajętości zajmowała 7,14 ms, czyli ekspander działa 7,8 razy wolnej. Te 55 ms jest już tak długim czasem, że na ekranie w drugim rzędzie można zaobserwować migotanie podczas odświeżania.
Ok, ale co z flagą zajętości? We wcześniejszych rozważaniach pomogła znacząco. No cóż, napisałem obsługę flagi, ale niestety jest ona bez sensu przy ekspanderze. Dlaczego? Aby wydobyć tę flagę trzeba odczytywać dane z wyświetlacza. Za każdym razem, gdy chcesz odczytać z PCF8574 trzeba go odpowiednio ustawić(nie ma rejestrów). Dla całego bajtu trzeba wykonać dwa takie odczyty(tryb 4-bitowy). To powoduje, że czas jaki potrzebny jest na odczytanie jednego bajtu z LCD wynosi ~1,79 ms.
Natomiast odczekanie na przetworzenie przez wyświetlacz danych wynosi maksymalnie około 100 µs. W takim razie nie ma sensu czytać flagi zajętości, bo zanim odbierzesz ją przez I²C to ona już wieki wcześniej będzie ustawiona. Podobna sytuacja jest przy przetwarzaniu komend przez kontroler HD44780. Maksymalny czas przetworzenia to około 1,5 ms. Nadal mniej niż odczyt jednego bajtu przez ekspander. W takim razie procedura czytania flagi zajętości przez ekspander to zupełna strata czasu…
Dlatego postanowiłem, że całkowicie usunę funkcje odpowiedzialne za czytanie z wyświetlacza po I²C. Nie ma co kusić losu oraz rzucać mniej doświadczonych na potencjalne problemy.
Gdyby układ wspierał wyższe prędkości zegara I²C to wynik byłby zdecydowanie lepszy. Niestety z uwagi na to, że jest to już wiekowy układ, nie ma na co liczyć. Należałoby użyć nowszych i wydajniejszych ekspanderów. Jeżeli będzie zainteresowanie, to przetestuję LCD np. z MCP23017 lub podobnym.
Podsumowanie
Wypada coś powiedzieć o tym, co wyszło z mojego testu.
Tryb | Czas |
---|---|
4-bit HAL_Delay | 67,29 ms |
4-bit bez BF | 7,14 ms |
4-bit z BF | 2,74 ms |
8-bit bez BF | 7,11 ms |
8-bit z BF | 2,74 ms |
ekspander I2C | 55,72 ms |
Ekspander na I2C zmniejsza liczbę potrzebnych pinów MCU i to jest niepodważalna i niezaprzeczalna zaleta. Musisz natomiast pamiętać, że za wolne piny płacisz czasem komunikacji z wyświetlaczem. Prawie 8-krotne spowolnienie może nie być bez znaczenia.
Moja biblioteka nie należy do idealnych i musisz pamiętać, że działa ona blokująco. Dlatego ważne jest, aby te czasy blokowania były jak najkrótsze i to zapewnia klasyczne połączenie LCD.
Już samo to, że jest widoczne wyraźne mrugnięcie wyświetlacza może być sporą wadą. Kiedy więc warto użyć takiego konwertera?
Według mnie w sytuacjach, kiedy budujesz urządzenie z bardzo rzadko odświeżanym wyświetlaczem. Wtedy takie połączenie już ma sens. Dane,h które będą wyświetlane są mało dynamiczne i nie ma potrzeby ciągłego ich monitorowania. Często mogą być to urządzenia low-power, które mają mało dostępnych pinów.
A Ty jaką masz opinię o tego typu ekspanderze? Podziel się w komentarzu.
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ć na ten temat, napisz komentarz. Pamiętaj, że dyskusja ma być kulturalna i zgodna z zasadami języka polskiego.
14 komentarzy
zawodowy_opróżniacz_lodówek · 19/10/2021 o 15:11
Kurde, zamówiłem od Ciebie kilka gratów na Allegro a znalałem się na fantastycznym blogu, analizującym takie drobiazgi jakich próżno szukać w świecie AVRowych Arduino.
Jako stary programista Perla od razu miałem przeczucie że z tymi delayami do “migania ledami” to uczenie złych nawyków, więc poczytałem trochę jak zliczać czas by na tej podstawie “nieblokująco” zmieniać stan leda.
Wiem że nijak się to ma do tematyki artykułu, ale pierwsze wrażenie mnie tak zachwyciło że postanowiłem spłodzić ten komentarz ;p
Muszę przyznać że pomysł z “materiałami marketingowymi w przesyłce” był zdecydowanie trafiony ^_^
Mateusz Salamon · 21/10/2021 o 12:37
Haha dzięki 😀 Po to są te ulotki właśnie, aby można było u mnie zostać na dłużej i dowiedzieć się czegoś wartościowego 🙂
SaS · 03/12/2019 o 10:50
Nie zgodzę się ze stwierdzeniem:
“Ekspander połączony jest w 4-bitowym trybie obsługi LCD. Nie jest to problemem, bo wykazałem już, że 8-bitowa obsługa jest bez sensu.”
“Oczywista oczywistością” jest to, że tryb 8-bit jest szybszy niż 4-bit. Dodatkowe korzyści daje sprzętowy interfejs (FCM/SFCM) ale ten niestety jest tylko w uC >=100pin a szkoda.
Co do LCD jeszcze jedna sztuczka. Wysyłanie danych w przerwaniu. Co ms można cyklicznie wysyłać (w jednym przerwaniu jeden znak) “HOME”, dane do wyświetlenia z bufora. Dla LCD 2×40 znaków takie odświezanie ekranu trwa 81ms (161 dla 4-bit). praktycznie same zalety:
– Nie trzeba czekać na FB czy wymagany okres czasu.
– Komunikacja z LCD “w tle”.
– Zapisy do bufora LCD to nawet w przypadku AVR to us.
– Nic nie miga, bo nie ma CLS dla LCD.
Mateusz Salamon · 03/12/2019 o 10:57
Dzięki za super komentarz! Jeśli czeka się na BusyFlag to 8-bit nie jest warte czterech pinów.
Natomiast pomysł wysyłania w przerwaniu jest dobry i podoba mi się. Skoro olewasz BF, to musisz pewnie posłużyć się jakimś timerem, a więc czekasz “w tle”?
Fakt, że z CLS przesadziłem bo nie jest niezbędny 🙁
SaS · 03/12/2019 o 11:15
Na nic nie czekasz, w przerwaniu “systemowym” (używasz HAL więc je masz, ponadto każdy ARM ma timer, standardowo ustawiany na 1ms) wysyłasz bajt do wyświetlacza. Pierwsze przerwanie wyśle HOME, po zakończeniu przerwania program sobie “hula” przez 1ms (chyba, ze inne przerwania mu przeszkodzą), w kolejnym przerwaniu znak nr0, next przerwanie znak nr 1, itd do ostatniego znaku.
Ten sposób obsługi LCD podpatrzyłem w centralach telefonicznych Slicana i użyłem w kilku projektach, np http://er-mik.prv.pl/ct2x4/
Nawet dla AVR, przerwanie trwające w jego przypadku 3..5us co 1ms to żadne obciążenie (5us do 1ms to 0,1%). Zapis znaku do bufora to nawet nie us. Czyszczenie wyświetlacza (bufora) to us.
nie ma problemu aby w przerwaniu wysyłać dane do LCD podłączonego do ekspandera I2C czy SPI o ile to jedno urządzenie na magistrali, jeśli urządzeń jest więcej sprawa się komplikuje (kolejki i tego typu rozwiązania).
PS1
A tak od siebie dodam, ze nie używam w nowych projektów wyświetlaczy znakowych. Graficzne są tanie a dają nieporównywalnie większe możliwości. Powoli odchodzę też od grafiki mono, na rzecz kolorowej.
PS2
Nawet stare projekty można czasem zmodyfikować przez wymianę wyświetlacza (zgodność pinowa i programowa) oraz programu na OLED alfanumeryczno-graficzny np https://kamami.pl/oled-100×8/178780-oled-weg010008alpp5n00000.html?search_query=oled+100&results=19
Mateusz Salamon · 03/12/2019 o 12:05
Wysłanie jednego znaku w HALowym przerwaniu SysTicka brzmi spoko. Odświeżenie LCD potrwa te około 33 ms, ale często nie trzeba szybko odświeżać na nim danych. Procesor będzie jednak solidnie odetkany.
Odchodzenie od alfanumerycznych powinno nastąpić jakieś 15 lat temu 😀 Aktualnie są o wiele lepsze możliwości
Marek · 01/12/2019 o 07:51
Juz lepiej wstawić jakiś stm32f0 zamiast nowszego ekspandera, wtedy robimy w nim bufor i wysyłamy poprostu znaki po i2c. Cena takiego w tssop20 to poniżej 1zł na ali.
Mateusz Salamon · 02/12/2019 o 20:53
O, dobre to 🙂 To jest rozwiązanie idealne do masowej produkcji bo dla jednej sztuki to trzeba jednak sporo pracy, aby to zadziałało. Pytanie też czy w solidnym hurcie takie ekspandery mogą być tańsze 😉
Kamil · 03/11/2019 o 15:46
Dziwne, bo nie można otworzyć w CUBEMX projektu poprzez open project i wybranie Twojego folderu. Wiesz jak temu zaradzić?
Mateusz Salamon · 03/11/2019 o 15:49
Spróbuj zaimportować cały projekt do swojego Workspace
SaS · 12/08/2019 o 13:21
Jak już na siłę dorabiać I2C do wyświetlacza to warto użyć nowszego ekspandera niż PCF8574. Są wersje 16 bit, i 400kHz, Do wysłania są 164bajy (ADR_I2C, ADR_REJ, dwa półbajty CMD HOME/CLS + 80 półbajtów do CGRAM) więc wysłanie danych do LCD 2×80 znaków to 3,7ms.. Na PCF8574 taka operacja (163 bajty bo nie wysyła się ADR_REJ) zajmuje niecałe 15ms. Inne niepotrzebne manipulacje (start, adr, dana, dana, stop, zamiast start, adr, cmd, dane…… stop) wydłużają ten czas i to kilkakrotnie. Najlepiej użyć LCD z wbudowanym I2C, wtedy wysyła się adres, cmd, dane czyli 82 bajty, co przy 400kHz zajmuje 1,8ms.
Czy tryb 16 bitowy ekspandera byłby szybszy? Nie. Trzeba wysyłać sekwencję ADR_I2C, ADR_REJ, dana, linie_sterE=1, start, ADR_I2C, ADR_REJ, dana, linie_sterE=0). Dla LCD 2×80 wymaga to przesłania 320 bajtów plus CMD dla LCD, razem 324 bajty. Dla 400kHz zajmie to 7,2ms.
Mateusz Salamon · 12/08/2019 o 13:37
To nie jest takie proste, że musisz wysłać tylko 163 bajty. Pamiętaj, że aby emulować interfejs 8080/6800 musisz machać pojedynczymi pinami sterującymi bez zmiany pinów danych. To zdecydowanie wydłuża czas przesyłu. Oczywiście można to optymalizować 🙂
Sas · 03/12/2019 o 10:40
Sterowniki LCD mają zerowy czas trzymania danych przy zapisie. Można równocześnie zdjąć E i dane. Teoria poparta praktyką.
Mateusz Salamon · 03/12/2019 o 10:42
Hmmmmm to można podkręcić lekko działanie 🙂