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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
//
// 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.

1
2
3
4
5
6
7
8
9
10
11
12
13
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.

TrybCzas
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. 

TrybCzas
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 I2C55,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.


Raz na jakiś czas wyślę Ci też e-mail z ciekawymi rzeczami które znalazłem


2 Komentarze

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ć 🙂

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *

Serwis wykorzystuje pliki cookies. Korzystając ze strony wyrażasz zgodę na wykorzystywanie plików cookies. Więcej informacji znajdziesz na stronie Polityka Prywatności

The cookie settings on this website are set to "allow cookies" to give you the best browsing experience possible. If you continue to use this website without changing your cookie settings or you click "Accept" below then you are consenting to this.

Close