Język C dla mikrokontrolerów 8051. Moduł 6-cyfrowego wyświetlacza LED.

Język C dla mikrokontrolerów 8051. Moduł 6-cyfrowego wyświetlacza LED.

Ten artykuł jest związany z poprzednio prezentowanym (>>>TUTAJ<<<), jednak zaproponowano rozwiązanie umozliwiające zaoszczędzenie wyprowadzeń mikrokontrolera. Pole odczytowe wyświetlacza ma 6 cyfr LED, po 7 segmentów każda. Doliczając kropkę dziesiętną można powiedzieć, że wyświetlacz wymaga do sterowania 8 bitów, więc same sterowanie segmentami zajmie jeden pełny port. Jeśli użyjemy metody podobnej do tej z poprzedniego przykładu, to dodatkowo sterowanie tranzystorami kluczami załączającymi napięcie na anody cyfr, będzie wymagać następnych sześciu bitów portu. To już razem 14 linii! A jeśli jeszcze konieczne stanie się jakiś układów zewnętrznych takich, jak na przykład klawiatura? Może braknąć wyprowadzeń mikrokontrolera... Prezentowane rozwiązanie to wynik napotkania przeze mnie w przeszłości podobnego dylematu. Chcę pokazać na praktycznym przykładzie jak można zmierzyć się z tak przedstawionym problemem. Pokażę również jak wykorzystać to rozwiązanie do budowy układu prostego licznika impulsów. Użyję w nim przerwania generowanego przez Timer 1 do obsługi wyświetlacza LED oraz przez opadające zbocze sygnału na wejściu INT0 do zliczania impulsów prostokątnych. Użyję również wskaźników i ich arytmetyki – będzie okazja, co nieco się nauczyć.

Schemat połączeń wyświetlacza LED przedstawiono zobaczyć na rysunku 1. Do eksperymentowania z tym przykładem programowania trzeba sobie taki wyświetlacz zbudować chociażby na płytce uniwersalnej. Moim zdaniem przyda on nie tylko do eksperymentów, ale również do wykorzystania w dowolnym układzie.

Rysunek 1. Wyświetlacz 6 cyfr LED z szeregowym wejściem danych (kliknij aby powiększyć).
         
Obsługa wyświetlacza oparta jest o przerwanie generowane przez Timer 1. Z tym zastrzeżeniem można go użyć w dowolnej aplikacji, chociażby jako wyświetlacz panelowy lub w układzie częstościomierza. Może to, co prawda, zaowocować migotaniem przy pomiarze przebiegów o niskiej częstotliwości, ale jakość wyświetlania zależeć będzie od konkretnej aplikacji i od zadanych priorytetów dla przerwań. Jednak tam, gdzie mikrokontroler nie jest zbyt mocno obciążony i gdzie nie ma ścisłych zależności czasowych – śmiało można użyć prezentowanego układu. Z powodzeniem na przykład wykorzystałem go do budowy termometru cyfrowego.
Do konstrukcji wyświetlacza użyłem rejestrów przesuwających 74HCT595. Zrobiłem tak z dwóch powodów. Po pierwsze, cena tych układów jest bardzo niska, a obciążalność wyjść jest wystarczająca do zasilania typowego, średniej wielkości wyświetlacza LED w stanie niskim. Po drugie, układ nie wyprowadza informacji na wyjścia do momentu pojawienia się osobnego impulsu zegarowego, który ją tam przepisze. Nie ma więc efektu migotania cyfr w czasie wpisywania danych do rejestrów. Połączyłem szeregowo dwa układy 74HCT595 tworząc w ten sposób rejestr szesnastobitowy. Jako pierwszy w szeregu znajduje się rejestr segmentów cyfr, jako drugi rejestr załączający poszczególne cyfry. Wejście szeregowe danych taktowane jest sygnałem o częstotliwości około 4,8kHz natomiast cyfry przełączane są z częstotliwością zbliżoną do 300Hz.
Na listingu 1 umieszczono funkcję zapisującą daną o długości 16 bitów w parze rejestrów HCT595. Ważne jest, aby zrozumieć podstawy jej działania do dalszej analizy programu. Dlatego też pozwolę sobie poświęcić jej nieco uwagi. W celach demonstracyjnych funkcja wysyłać będzie wyłącznie liczbę 0x55. Układ 74595 to rejestr z wejściem szeregowym i wyjściem równoległym. Układ posiada dwa wejścia zegarowe (osobne dla zapisu rejestru przesuwnego, osobne dla wyprowadzenia danych na wyjścia równoległe), jedno szeregowe wejście danych oraz 8 wyjść równoległych. W strukturę wbudowano dwa 8 bitowe rejestry. Pierwszy z nich to rejestr przesuwny, drugi zatrzask wyjściowy. Dzięki niemu, stany na wyjściach równoległych nie zmieniają się w czasie wprowadzania danych przez wejście szeregowe. Zmiana stanu na wyjściach wymaga podania osobnego impulsu zegarowego, który przepisze dane na wyjścia układu. Każdy impuls zegarowy docierający do wejścia zegara przesuwu, powoduje próbkowanie stanu wejścia, jego zapis do rejestru przesuwnego oraz przesunięcie o 1 bit w prawo. Na koniec jeszcze jedna uwaga odnośnie sposobu funkcjonowania rejestrów 74595. Układy można łączyć w łańcuchy o praktycznie dowolnej długości (patrz rysunek 1, na którym połączono 2 rejestry).
Do wyżej opisanych właściwości rejestrów dostosowana jest funkcja wysyłająca dane z listingu 1. Wysyłana jest liczba typu unsigned int mająca długość 2 bajtów, to jest 16 bitów. Początek programu zawiera deklaracje:
- wysyłanej zmiennej X,
- linii sterujących układami 74595, to jest: LINIA_DANYCH (szeregowe wejście danych rejestru), LINIA_ZEGARA_PRZES (wejście zegara rejestru przesuwnego), LINIA_ZEGARA_WYJSC (wejście zegara wyjściowych zatrzasków latch),
- zmiennej i, która posłuży do konstrukcji pętli for.
Zgodnie z tym, co napisałem wcześniej, w zmiennej X
zostaje zapamiętana liczba 0x55. Następnie otwierana jest pętla for wykonująca 16 razy operacje zawarte pomiędzy nawiasami klamrowymi. Po pierwsze zmienna X przesuwana jest w lewo o 1 pozycję. W ten sposób najstarszy bit wpływa na stan flagi przeniesienia CY ustawiając ją w przypadku, gdy przeniesiona doń będzie logiczna „1” i zerując, gdy przeniesione będzie logiczne „0”. Następnie stan flagi CY poprzez operację przypisania LINIA_DANYCH = CY; wpływa na stan wyjściowej linii danych mikrokontrolera. Impuls zegarowy wypracowywany przez zmianę stanu LINIA_ZEGARA_PRZES (przypisanie logicznej „1” a następnie logicznego „0”) zapisuje stan linii do rejestru przesuwnego. Te operacje przeprowadzane są 16 razy. Po zakończeniu pracy pętli for, zmiana stanu LINIA_ZEGARA_WYJSC (podobnie, jak dla zegara przesuwu przypisanie logicznej „1” a następnie logicznego „0”) przepisuje dane z wewnętrznego rejestru przesuwnego na wyjścia równoległe.

Listing 1. Zapis danych do rejestrów HCT595

sbit LINIA_DANYCH = P0^1;       //tu podłączone wejście danych układu 74595
sbit LINIA_ZEGARA_PRZES = P0^2; //tu dołączone wejście zegara przesuwu 74595
sbit LINIA_ZEGARA_WYJSC = P0^3; //tu podłączone wejście zegara zatrzasków wyjśc.74595

unsigned int X;                 //ta zmienna zapisywana będzie do pary rejestrów
char i;                         //zmienna, która posłuży do konstrukcji pętli for

void main (void)
{
    X = 0x55;                   //do rejestrów zapisana będzie liczba 0x55
    for (i = 0; i<16; i++)      //pętla powtarzana 16x
    {
        X <<= 1;                //przesunięcie zmiennej X o 1 miejsce w lewo, w ten
                                //sposób najstarszy bit trafia do flagi CY
        LINIA_DANYCH = CY;      //teraz bitowi linii danych przypisywana jest wartość
                                //flagi CY
        LINIA_ZEGARA_PRZES = 1; //wytworzenie impulsu na wejściu zegara przesuwu danych
        LINIA_ZEGARA_PRZES = 0;
     }
     LINIA_ZEGARA_WYJSC = 1;    //przepisanie danych na wyjścia rejestrów 74595
     LINIA_ZEGARA_WYJSC = 0;
}

Wróćmy jednak do przykładu aplikacji wyświetlacza 6-cyfrowego. Podobnie jak w poprzednim przykładzie, użyłem wyświetlaczy LED ze wspólną anodą. Zasilanie anod załączane jest przez tranzystory MOS z kanałem typu P (BS250). Wartości rezystorów podłączonych do poszczególnych segmentów wyświetlacza należy dobrać sobie do posiadanych cyfr. Numery wyprowadzeń wyświetlacza LED należy traktować jako orientacyjne. Istotne są literowe oznaczenia segmentów. Na wejściach rejestrów szeregowych znajdują się rezystory zasilające tak, aby można było wyświetlacz podłączyć do dowolnego z portów mikrokontrolera. Do sterowania wymagane są trzy linie – jedna danych i dwie zegarowe. Wykorzystałem w tym celu wyprowadzenie portów P1.1, P1.2 i P1.3 Oczywiście zmieniając program można użyć dowolnych innych. Również wykonując drobne modyfikacje w programie, można podłączyć do ośmiu wyświetlaczy LED o wspólnej anodzie.
W skrócie funkcjonowanie wyświetlacza wygląda następująco: dane przy pomocy opadającego zbocza sygnału zegarowego podawanego na wyprowadzenie 11 (SRCLK) wpisywane są z wejścia szeregowego na wyprowadzeniu 14 (SER) do wewnętrznego rejestru. Mikrokontroler przesyła pełne słowo 16-to bitowe tak, aby działały oba układy rejestrów. Następnie, po wpisaniu 16 bitów do rejestrów, na wyprowadzenie 12 (RCLK) podawany jest impuls zegarowy, którego opadające zbocze powoduje przepisanie danych z wewnętrznego szeregowo – równoległego rejestru do wyjściowego rejestru typu zatrzask. Tyle na temat zasady działania, zajmijmy się teraz programem.

Mikrokontroler jako licznik

Licznik wykorzystuje dwa przerwania. Pierwsze, zewnętrzne, powodowane przez opadające zbocze napięcia na wejściu INT0, używane jest zwiększania wartości licznika. Drugie – wewnętrzne, generowane cyklicznie - pochodzące od Timer’a 1, przepisuje stan zmiennej o nazwie display do rejestrów wyświetlacza. Program rozpoczyna się od deklaracji. Linia danych wyświetlacza zadeklarowana zostaje jako P1^0, linia zegara szeregowego jako P1^1, linia zegara wyjściowego rejestru latch jako P1^2.

sbit dataline = P1^0;    //port danych wyświetlacza
sbit shiftline = P1^1;   //port sygnału zegarowego dla przesyłania danych
sbit latchline = P1^2;   //port sygnału zegara wyprowadzenia danych

Oprócz tego inicjuję zmienną counter typu unsigned int zawierającą zliczane impulsy oraz stałą interval typu char zawierającej wartość wpisywaną do rejestru TH1 Timer’a 1. Jej wartość wpływa bezpośrednio częstotliwość, z jaką wywoływane jest przerwanie obsługujące wyświetlacz LED. Dalej znajdziemy dobrze znane z poprzednich przykładów konstrukcje: uporządkowana w kolejności rosnącej (od 0 do 9) tablica określająca wygląd wyświetlanego znaku (patterns), tablica z kodami kolejności załączania cyfr (digits) oraz tablica - bufor wyświetlacza w RAM (display). Dwie pierwsze umieściłem w obszarze pamięci ROM mikrokontrolera (słowo kluczowe code) i nadałem im stałe wartości. Trzecia znajduje się w obszarze RAM. Odwzorowuje stanu wyświetlacza i zawiera zawsze kody wszystkich wyświetlane na nim znaków. Każda z tablic ma przypisany wskaźnik, czyli zmienną, która będzie wskazywać na element tablicy. W momencie deklaracji nadaję wskaźnikom wartości. Wyrażenie Wskaźnik = &Tablica powoduje przypisanie zmiennej Wskaźnik adresu, pod którym umieszczona jest Tablica.

Zamiana liczb na znaki graficzne

Funkcja void Translate() zamienia argument x, którym jest dwubajtowa liczba całkowita bez znaku na odpowiadającą tej liczbie zawartość bufora display. Jednym zdaniem, zamienia liczbę na odpowiadający jej wygląd wyświetlacza LED. Metoda jest prosta, chociaż zapis początkowo może się wydać niezrozumiały. Po wywołaniu funkcji wyłączam przerwania Timer’a 1. Robię to celu uniknięcia migotania segmentów wyświetlacza. Później, wskaźnikowi TDisplay, przypisuję wskazanie na ostatni element bufora wyświetlacza. Od niego rozpoczynam translację na kody LED. Przebiega ona według następującego schematu: do wartości wskaźnika TPatterns dodaj resztę z dzielenia argumentu przez 10 a następnie skopiuj wskazywaną w wyniku działania stałą typu char z tablicy patterns pod adres wskazywany przez TDisplay. Podziel liczbę przez 10, przesuń wskazanie na następną pozycję w buforze wyświetlacza i powtórz operację dla następnej cyfry. I tak 6 razy dla każdej z cyfr LED.

Wygaszanie zer nieznaczących

Jako swego rodzaju rozszerzenie funkcjonalności, umieściłem wygaszanie zer nieznaczących na początku cyfry. Pętla – rozpoczynając od początku bufora - sprawdza znak znajdujący się pod wskazanym przez wskaźnik bufora wyświetlacza adresem (TDisplay) i porównuje go ze znakiem na początku tablicy digits to znaczy wzorcem „0”. Jeśli są to te same znaki, kod „0” w buforze wyświetlacza zostaje zamieniony na 0xFF, co odpowiada całkowitemu wygaszeniu cyfry. Tak dzieje się aż do momentu napotkania wzorca różnego od wzorca „0”. Wówczas to instrukcja break (spotkaliśmy ją w konstrukcjach warunku switch) przerywa działanie pętli for. Po zakończeniu pętli, załączane jest wyświetlanie – odpowiada mu zezwolenie na przyjmowanie przerwań Timer’a 1.
Dalej umieszczone są procedury obsługi przerwań. Wyróżnia je słowo kluczowe interrupt umieszczone w nagłówku funkcji. Funkcja void IncrementCounter() obsługuje przerwanie zewnętrzne INT0. W zasadzie nie robi nic za wyjątkiem zwiększenia stanu zmiennej counter i wywołania funkcji Translate. Umieszczono ją poniżej:

//funkcja obsługi przerwania INT0, zwiększanie licznika impulsów
void IncrementCounter(void) interrupt 0
{
    counter++;            //zwiększenie licznika impulsów
    Translate(counter);   //zamiana counter na liczby do wyświetlenia
}

Rola przerwania Timer’a 1

Druga z nich, to procedura obsługi przerwania Timer’a 1 – void DisplaySend() zajmująca się konstrukcją i przesłaniem słowa do wyświetlacza LED. Timer 1 pracuje w trybie 16-to bitowym. Przerwanie zgłaszane jest przez Timer w momencie przepełnienia, to znaczy zmiany stanu z 0xFFFF na 0x0000. Wówczas to wywoływana jest procedura obsługi przerwania. Cykl odliczania rozpoczyna się na nowo od wartości 0 do 0xFFFF. Jeśli nie zdecydujemy inaczej, to od tego momentu do następnego przerwania upłynie czas 65536 cykli maszynowych. W przypadku mojego modelu, byłby to czas około 100 milisekund – z całą pewnością ze względu na swoją długość byłby on powodem migotania cyfr. Można czas do wywołania przerwania skrócić, ustawiając na pożądaną wartość młodszy i starszy bajt Timer’a. Ponieważ nie zależy mi na bardzo dokładnym odmierzaniu czasu, zdecydowałem się na ustawienie (odświeżenie) tylko starszego bajtu.

TH1 = interaval;   //odświeżenie zawartości rejestru Timer’a 1 przy pomocystałej interval

Dalej zwiększam są wartości wskaźników TDisplay (wskaźnik do cyfry w buforze wyświetlacza) oraz TDigits (wskaźnik do tablicy z kodami załączenia cyfr). Oba te wskaźniki zmieniane są synchronicznie! To znaczy przesunięcie się na następną cyfrę powoduje również zmianę kodu załączenia wyświetlacza a tym samym odpowiadającemu mu tranzystora klucza.
Warunek if (TDisplay == 0) służy do zbadania czy napotkano znak końca bufora w RAM i jeżeli równość jest spełniona, wskaźniki ustawiane są ponownie na początek wskazywanych tablic. Kod załączenia musi być wysłany jako pierwszy, więc na początek zmiennej x przypisywana jest wartość kodu załączenia cyfry. Później przesuwana jest ona w lewo o 8 pozycji, a następnie sumowany jest z nią wzorzec znaku do wyświetlenia z bufora display. Te wszystkie operacje muszą być wykonane w celu poprawnej budowy słowa do sterowania wyświetlaczem. Poświęćmy teraz nieco uwagi pętli for, której zadaniem jest wysłanie wszystkich 16 bitów słowa do wyświetlacza.
Znajdująca się wewnątrz pętli operacja przesuwania w lewo zmiennej x, ma na celu przeniesienie pojedynczego bitu słowa do flagi CY mikrokontrolera, która to w następnym poleceniu (dataline = CY) wpływa na stan bitu portu – wyjścia danych. Impuls zegarowy, zmiana stanu shiftline z wysokiego na niski, kończy proces wysłania bitu. Pętla for powtarza operację 16 razy, dla wszystkich bitów zmiennej. Transmisję kończy przepisanie danych z wewnętrznego rejestru do rejestru wyjściowego poprzez zmianę stanu linii latchline. Na tym kończę obsługę przerwania Timer’a 1.
Program główny main() zawiera tylko proste ustawienia mikrokontrolera oraz, na samym początku, zapisanie znaku końca bufora wyświetlacza. Ustawiane są:
- rejestr TMOD na wartość 0x11, to znaczy oba Timer’y jako 16-to bitowe (TMOD = 0x11),
- starszy bajt licznika Timer’a 1 (TH1 = interval),
- globalnie włączane są przerwania (EA = 1).
Program główny kończy pętla while(1), w której mikrokontroler oczekuje na impulsy przychodzące na INT0 oraz zajmuje się obsługą wyświetlania. Pętla jest konieczna, aby mikrokontroler pozostał w programie głównym i nie wykonywał instrukcji znajdujących się bezpośrednio poza nim.

Listing 2. Program licznika jako przykład wykorzystania wyświetlacza „6 cyfr LED z wejściem szeregowym”.

/*********************************************
  Obsługa wyświetlacza 6 cyfr LED z wejściem
  szeregowym.
  rezonator kwarcowy 7,3728MHz
 *********************************************/
//wybór modelu pamięci
#pragma SMALL
//dołączenie definicji rejestrów mikrokontrolera
#include <reg51.h>
//definicje linii sterujących wyświetlaniem
sbit dataline  = P1^0;
sbit shiftline = P1^1;
sbit latchline = P1^2;
//licznik impulsów
unsigned int counter = 0;
//wartość rejestru TH1, TL1 nie jest modyfikowane liczy zawsze od 0
const char interval = 0xF8;

/* tutaj wzorce cyfr
       d5
       ======
   d1 |      |d6
      | d0   |
       ======
   d7 |      |d4
      |      |
       ======
       d2        */

char code patterns[10] = { 0x09,0xAF,0x1A,0x8A,0xAC,0xC8,0x48,0x8F,0x08,0x88 };
//przecinek = d3, wyłączenie cyfry = 0xFF
char *TPatterns = &patterns;

//tutaj kolejność załączania
char code digits[6] = { 0xFE,0xFD,0xFB,0xF7,0xEF,0xDF };
char *TDigits = &digits;

//bufor wyświetlacza w RAM
char data display[7];
char data *TDisplay = &display;

//zamiana zmiennej x na zawartość bufora do wyświetlenia
void Translate(unsigned int x)
{
    char temp;
    ET1 = 0;              //wyłączenie wyświetlania na czas translacji (przez migotanie LED)
    TDisplay = &display + 5; //zaczynamy translację od
                         //najmłodszej cyfry
    for (temp=0; temp<6; temp++)
    {
        *TDisplay = *(TPatterns + x % 10); //adres wzorca
                         //+ (reszta z
             TDisplay--; //dzielenia przez 10)
        x /= 10;
    }
    TDisplay++;          //po poprzedniej pętli TDisplay jest o 1 za małe
                         //wygaszenie zer nieznaczących od pozycji 1..5
    for (temp=0; temp<5; temp++)
         {
        if (*TDisplay == *TPatterns) *TDisplay = 0xFF;
         else break;     //jeśli napotkamy znak różny od 0, to koniec
        TDisplay++;
    }
    ET1 = 1;             //załączenie przerwań (wyświetlania)
}

//procedura obsługi przerwania INT0
//zwiększanie licznika impulsów
void IncrementCounter(void) interrupt 0
{
    counter++;           //zwiększenie licznika impulsów
    Translate(counter);  //zamiana counter na liczby do
                         //wyświetlenia
}


//procedura obsługi przerwania od timer'a 1
//wysłanie zmiennej 2-bajtowej do wyświetlacza - 1 znak z bufora display
void DisplaySend(void) interrupt 3
{
    char temp;
    unsigned int x;
    
    TH1 = interval;      //odświeżenie zawartości timera 1
    TDisplay++;          //następna pozycja do wyświetlenia
    TDigits++;
    if (*TDisplay == 0)  //jeśli osiągnięto koniec bufora,
                         //wróć do początku
    {
        TDisplay = &display;
        TDigits = &digits;
    }
    x = *TDigits;        //x przyjmuje wartość liczby do wysłania
    x <<= 8;             //składa się ona z bajtu wzorca cyfry
    x |= *TDisplay;      //i bajtu kolejności załączenia
    
    for (temp = 0; temp<16; temp++)    //wysłanie cyfry poprzez
                         // przypisanie flagi C do wyjścia
    {
        x <<= 1;
        dataline = CY;
        shiftline = 1;   //impuls na wyjściu zegara przesuw.
        shiftline = 0;
    }
    latchline = 1;       //przepisanie danych do wyjść rej.
    latchline = 0;
}

//program główny
void main(void)
{
    *(TDisplay + 6) = 0x00;  //tutaj kod końca danych
    Translate(counter);      //wyświetlenia 0.
    TMOD = 0x11;         //oba timery jako 16 bitowe
    TH1 = interval;      //przerwanie wywoływane z częst.50Hz
    ET1 = 1;             //zezwol.na przerwanie od Timer’a 1
    TR1 = 1;             //uruchomienie Timer’a 1
    IT0 = 1;             //opadające zbocze na INT0 wyzwala przerwanie
    EX0 = 1;             //załączenie przerwania INT0
    EA = 1;              //zezwol.na przyjmowanie przerwań
    while (1);           //oczekiwanie na przerwania,
                        //koniec programu
głównego, dalej wyłączenie
}                       //oczekiwanie na przerwania od Timer’a 1 oraz INT0

 

Jacek Bogusz
j.bogusz@easy-soft.net.pl

 

http://www.tomaszbogusz.blox.pl/

Dodaj nowy komentarz

Zawartość pola nie będzie udostępniana publicznie.