Dot-matrix VFD
5x7 kłopotliwych kropek

Po lekturze EdW 2/05 można odnieść wrażenie, że wykorzystanie lampy VFD "z odzysku" nie stanowi większego problemu. Ot, garść elementów, mikroprocesor, kawałek programu i sterowanie gotowe. I generalnie jest to prawda. Jeśli trafi nam się lampa o stosunkowo prostej budowie i niewielkiej ilości wyprowadzeń - układ do sterowania jest w miarę prosty w realizacji. Ale możemy też natrafić na "rodzynek" taki jak na fotografii 1, poniżej.


fot. 1

To moduł z matrycowym wyświetlaczem VFD typu 16-SD-01Z firmy Futaba. Lampa ta posiada szesnaście pól odczytowych, każde po 35 segmentów (pixeli) i jak łatwo policzyć do sterowania wymaga 51 linii danych. Próbując taką lampę "oswoić" metodami jak w artykule "Nie bój się VFD...", jesteśmy na najlepszej drodze do zbudowania "tranzystorowego monstrum".

Słów kilka o lampie VFD

Zanim zajmiemy się układem sterującym, poznajmy budowę naszego matrycowego wyświetlacza. Lampę po wyjęciu z modułu (o którym od razu zapominamy) przedstawiają fotografia 2 i fotografia 3.


fot.2

fot.3

Konstrukcja lampy jest wbrew pozorom dość prosta, pod każdą siatką sterującą umieszczone jest 35 segmentów (anod), poukładanych w matrycę 5 kolumn na 7 rzędów. Każdy pixel określony współrzędnymi (k,r) /k = {1..5}, r = {1..7}/, umieszczony pod n-tą siatką jest połączony wewnątrz lampy ze swoimi "sobowtórami" pod pozostałymi siatkami i posiada swoje wyprowadzenie na zewnątrz szkła lampy, co w miarę dobrze pokazuje fotografia 4.


fot. 4

Jak widać, lampa jest dostosowana do sterowania multipleksowanego. Tyle o budowie lampy, teraz zbieramy się za badanie do czego służą poszczególne końcówki. Ponieważ wyprowadznia siatek są dobrze widoczne przez szkło lampy, a w identyfikowaniu segmentów mamy już wprawę, możemy sobie narysować szkic lampy, nazwać (ponumerować) poszczególne segmenty jak na rysunku 1 oraz namalować tabelkę z opisem wyprowadzeń - rysunek 2.


rys. 1

rys. 2

Słów kilka o sprzęcie

Mając rozpoznaną lampę, zastanówmy się jak zbudować układ sterowania. Szesnaście siatek nie stanowi problemu, możemy wykorzystać układ jak w artykule "Nie bój się...", ale szesnaście tranzystorów npn zastąpimy dwoma układami ULN2804. Z segmentami będzie gorzej, bo ich jest aż 35 sztuk i dokładnie tylu linii sterujących będziemy potrzebowali. I żeby nie było "jak w średniowieczu", do sterowania anod wyświetlacza wykorzystamy specjalizowaną kostkę - sterownik VFD typu MAX6921 firmy Maxim. Driver MAX6921 zapewnia sterowanie dwudziestoma końcówkami lampy, ponieważ mamy ich 35 - wykorzystamy dwa takie układziki połączone ze sobą w kaskadę (łańcuch). Cały układ sterowania możemy zobaczyć na schemacie 1.


sch. 1 ( wersja pdf )

Procesor AT90S8515 (U1) w swoim standardowym układzie aplikacyjnym nie stanowi zagadki. Układy U2,U3 - dekodery 3/8 typu 74138 realizują razem dekoder 4/16, wyjścia tego dekodera połączone są bezpośrednio z buforami ULN2804 (układy U4,U5) co wraz z dwoma drabinkami rezystorowymi 47K (RP1,RP2) stanowi całość układu wybierającego siatki. Zasada działania jest znana: ustawienie dowolnej kombinacji stanów logicznych na czterech młodszych bitach portu PA spowoduje że jedna z siatek zostanie spolaryzowana napięciem anodowym (+24V) czyli będzie aktywna, reszta siatek pozostanie na potencjale masy. Sterowanie segmentów jest równie proste - dwa układy MAX6921 (U7,U8) łączymy w łańcuch, czyli wejście danych DIN drugiego układu w kaskadzie (U8) jest podpięte do wyjścia DOUT pierwszego układu. Sygnały: CLK (zegar), LOAD (przepisanie z rejestru szeregowego do buforów wyjściowych) i /BLANK (wygaszanie display-a) są wspólne dla obu układów i podłączone do odpowiednich bitów portu PA procesora. Wyprowadzenia segmentów lampy łączymy bezpośrednio z odpowiednimi wyprowadzeniami kostek MAX6921. "Wysokie napięcie" o wartości +24V jest generowane przez przetwornicę DC/DC typu SIM-0524S (U6), żarnik lampy jest zasilany z +5V (Vcc), przez dwie szeregowo włączone diody prostownicze (D1,D2), co zapewnia napięcie na żarniku o wartości około 3.6 V. Całość, w formie uroczego kłębka kabelków możemy obejrzeć na fotografii 5.


fot. 5

Słów kilka o multipleksowaniu

Jak wspomniano, lampa 16-SD-01Z jest dostosowana do sterowania multipleksowanego. Nie wdając się w szczegółowe rozważania, oznacza to że w jednej chwili czasowej aktywna jest tylko jedna siatka (pole odczytowe), układ sterujący podaje na segmenty (anody) odpowiednią kombinację napięć, aby zapewnić odpowiedni kształt znaku. Po wyłączeniu tej siatki, i aktywowaniu następnej, na anody podawana jest inna kombinacja napięć. Cały proces odbywa się na tyle szybko że ludzkie oko ulega złudzeniu i widzi stabilny obraz (napis) a nie "biegające kropki". Ten proces, wraz z wykresem przebiegów napięć na siatkach i anodach lampy jest dokładnie opisany w artykule "Wyświetlacze VFD cz.1" w EdW 2/05.

Słów kilka o programie sterującym

A teraz powolutku szkicujemy nasz program. Najpierw musimy zdefiniować zmienną, która będzie przechowywała dane będące cyfrowym "obrazem" tego co ma być na wyświetlaczu. Jak się łatwo domyśleć - będzie to tablica bajtów (unsigned char) o rozmiarze 16*7 = 112 bajtów. Każdy znak będzie definiowany przez siedem bajtów - jeden bajt to jeden rządek pixeli. Zaletą takiego podejścia jest dość łatwe definiowanie znaków (o tym będzie dalej), wadą jest to, że trzy najstarsze bity każdego bajtu póki co zostaną niewykorzystane. Definiujemy więc bufor na dane dla lampy:
unsigned char uchVfdDataBuffer [ VFD_GRIDS_NUM * VFD_ROWS_NUM ];

a rysunek 3 pokazuje w jaki sposób będzie go widział nasz program.

rys. 3

Może od razu zdefiniujemy też zmienną, w której będziemy przechowywać numer aktualnie obsługiwanej siatki:
unsigned char uchVfdCurrentGrid;
Teraz napiszemy sobie funkcję, która wywoływana z poziomu handlera przerwania zegarowego będzie zmieniała (aktywowała) kolejne siatki lampy i wysyłała do układów MAX6921 dane z "obrazem bitowym" żądanego znaku:
void vfd_update_display ( void ) {
  PORTA = (PORTA & 0xF0) | uchVfdCurrentGrid;
  vfd_load_serial_reg( uchVfdCurrentGrid );
  uchVfdCurrentGrid++;
  if ( uchVfdCurrentGrid ==  VFD_GRIDS_NUM ) {
    uchVfdCurrentGrid = 0;
  }
}
Jak widać, siatki są sterowane bezpośrednio przez zapis czterech młodszych bitów do portu PA. Dane dla MAX6921 są preparowane przez funkcję
vfd_load_serial_reg()
która jako parametr dostaje numer bieżącej siatki. Bierzemy więc w/w funkcję pod lupę... Oto kod:
void vfd_load_serial_reg( unsigned char uchGridNo ) {
  // [a1]
  unsigned char *pGridData = &uchVfdDataBuffer [ uchGridNo * VFD_ROWS_NUM ];
  signed char   nCurrentBit;
  signed char   nRowNo;
  // [a2]
  nCurrentBit = VFD_COLS_NUM;
  VFD_REG_DATA_L;
  while ( nCurrentBit-- ) {
    VFD_REG_CLK_H;
    VFD_REG_CLK_L;
  }
  // [a3]
  nRowNo = VFD_ROWS_NUM - 1;
  while ( nRowNo >= 0 ) {
    // [a4]
    unsigned char nRowData = pGridData [ nRowNo ];
    nCurrentBit = VFD_COLS_NUM - 1;
    // [a5]
    while ( nCurrentBit >= 0 ) {
      VFD_REG_DATA_L;
      if ( nRowData & ((unsigned char)1 << nCurrentBit) ) {
        VFD_REG_DATA_H;
      }
      VFD_REG_CLK_H;
      VFD_REG_CLK_L;
      nCurrentBit--;
    }
    nRowNo--;
  }
  VFD_REG_DATA_L;
  // [a6]
  VFD_REG_LOAD_H;
  VFD_REG_LOAD_L;
}

Co do czego służy, po kolei:
[a1] - definiujemy zmienne pomocnicze oraz mając numer siatki ustalamy adres pierwszego bajtu w buforze wyświetlacza, który zawiera dane dla w/w siatki;
[a2] - ponieważ nasz rejestr z dwóch MAX6921 ma 40 bitów, a my wysyłamy tylko 35, należy na początku wysłać do MAX-ów te pięć brakujących (mogą być zera...), potem 35 właściwych z prawdziwymi danymi;
[a3] - ładujemy dane dla jednego znaku. Mamy do wysłania 7 bajtów, wysyłamy je począwszy od ostatniego (siódemgo), dlatego zmienna kontrolna pętli jest dekrementowana. Aktualnie interesujący nas bajt określa wyrażenie [a4];
[a5] - wysyłamy kolejne bity do MAX6921 ale tylko pierwsze pięć, począwszy od najstarszego (piątego) bitu;
[a6] - po wysłaniu pięciu bitów "demo" i 35 bitów danych, generujemy sygnał LOAD, który przepisuje dane z rejestrów szeregowych do buforów wyjściowych układów MAX6921;
Może małe wyjaśnienie: wszystko wysyłamy od końca...?! Rejestry przesuwne mają to do siebie, że to co wpiszemy jako pierwsze, po odpowiedniej ilości taktów zegara (CLK) ląduje na końcu. Więc aby np. pierwszy bit, pierwszego bajtu naszego znaku był prezentowany na wyjściu OUT0 rejestru MAX-a, trzeba go wysłać...na końcu.

Tym sposobem mamy pojęcie jak działa "niskopoziomowa" część programu, czyli jakim to sposobem zawartość tablicy bajtów uchVfdDataBuffer[] pojawia się na wyświetlaczu. No właśnie, zawartość... Skąd się biorą znaki do wyświetlenia?

Słow kilka o typografii

Popatrzmy na rysunek 4.

rys. 4

Wyobraźmy sobie teraz funkcję zdefiniowaną następująco:
void vfd_set_char (unsigned char nWhere, unsigned char uchAny ) {
  // [b1]
  prog_uchar *pPattern = vfd_find_pattern ( uchAny );
  // [b2]
  memcpy_P((unsigned char*)&uchVfdDataBuffer[nWhere*VFD_ROWS_NUM],pPattern,VFD_ROWS_NUM);
}

Funkcja jako parametry dostaje numer siatki (pola odczytowego) oraz kod znaku ASCII (lub umowny identyfikator semigrafiki) do wstawienia do bufora lampy.
[b1] - zmienna pPattern, będąca wskaźnikiem na pamięć programu jest inicjowana wartością zwracaną przez funkcję vfd_find_pattern(), czyli adresem pierwszego bajtu siedmiobajtowego wzorca zapisanego w pamięci stałej (flash);
[b2] - ponieważ bufor wyświetlacza uchVfdDataBuffer[] jest w RAM, a definicja znaczka we flash, używamy "mutacji" funkcji biliotecznej memcpy() o nazwie memcpy_P(), obsługującą pamięć programu. Docelowy adres, pod który zostaną skopiowane bajty wzorca określamy bardzo prosto - jest to adres elementu bufora wyświetlacza o numerze wyliczonym tak: nWhere * VFD_ROWS_NUM; Przykładowe wywołanie to: vfd_set_char ( 0, SEMI_SMILE ); i na pierwszej od lewej pozycji wyświetlacza pojawi się "buźka". Jak widać, taką funkcją możemy już sobie na wyświetlaczu wypisywać różne ciekawe rzeczy, tylko trzeba mieć zdefiniowane wzorce znaków... Definicja jednego znaku (np. kratki) wygląda tak:
prog_uchar char_gen_Hash [ VFD_ROWS_NUM ] = { 0x15, 0x0A, 0x15, 0x0A, 0x15, 0x0A, 0x15 };

I jak się nietrudno domyśleć, takie definiowane znaków w większych ilościach, może być dość żmudne i łatwo sie pomylić...
Ale od czego mamy preprocesor? Zdefiniujmy sobie dwa sympatyczne makra:
#define SET_BIT(value,bit_no)   (value << bit_no)
#define MAKE_ROW(b4,b3,b2,b1,b0)   (SET_BIT(b4,4)|SET_BIT(b3,3)|SET_BIT(b2,2)|SET_BIT(b1,1)|SET_BIT(b0,0))

Pierwsze makro to zwykłe przesunięcie bitowe w lewo o zadaną ilość bitów. Drugie, dostając na wejście kombinację zer i jedynek, zwraca wynik w postaci bajtu z odpowiednio poustawianymi bitami. Po co ta komplikacja? A po to, aby można było sobie napisać definicję wzorca np. tak:
prog_uchar char_gen_Smile[ VFD_ROWS_NUM ] = {
  MAKE_ROW ( 1, 1, 0, 1, 1 ),
  MAKE_ROW ( 1, 1, 0, 1, 1 ),
  MAKE_ROW ( 0, 0, 1, 0, 0 ),
  MAKE_ROW ( 0, 0, 1, 0, 0 ),
  MAKE_ROW ( 1, 0, 0, 0, 1 ),
  MAKE_ROW ( 1, 0, 0, 0, 1 ),
  MAKE_ROW ( 0, 1, 1, 1, 0 )
};
W ten sposób, widzimy (wprawdzie zero-jedynkowy, ale zawsze) obraz definicji znaku, a nie tylko same liczby zapisane w hex... A tak na marginesie, całkiem ładne fonty można sobie "przepisać" z dowolnego pdf-a od jakiegokolwiek modułu LCD, kilka ostatnich stron porządnej dokumentacji to są z reguły definicje liter, cyfr i znaków specjalnych dla modułu. No i wracamy do programowania... Skoro mamy już zdefiniowane znaki (wzorce), potrzeba jeszcze funkcji-wyszukiwarki, coś jak vfd_find_pattern(), która znajdzie nam potrzebny wzorzec dostając na wejście jego kod (identyfikator). Piszemy zatem:
prog_uchar* vfd_find_pattern ( unsigned char chAny ) {
  switch ( chAny ) {
    case 'E':       return ((prog_uchar*)char_gen_E);
    case 'd':       return ((prog_uchar*)char_gen_d);
    case 'W':       return ((prog_uchar*)char_gen_W);
    case ' ':       return ((prog_uchar*)char_gen_SPACE);
    case '?':       return ((prog_uchar*)char_gen_QUESTION);
    case SEMI_SMILE:return ((prog_uchar*)char_gen_Smile);
    case SEMI_BOX:  return ((prog_uchar*)char_gen_Box);
    case SEMI_HEART:return ((prog_uchar*)char_gen_Heart);
    case SEMI_HASH: return ((prog_uchar*)char_gen_Hash);
    case SEMI_TREE: return ((prog_uchar*)char_gen_Tree);
  }
  return ((prog_uchar*)char_gen_Hash);
}

Jak można było się spodziewać, ciało funkcji to jeden wielki switch()... Nie jest to rozwiązanie eleganckie, ponieważ kod tej funkcji będzie tracił na uroku w miarę dodawania nowych definicji znaków. Można ten problem rozwiązać lepiej, zapisując wskaźniki na definicje znaków w dodatkowej tabeli i taką tabelę przeszukiwać w pętli. A wracając do naszej funkcji, jeżeli instrukcja wyboru nie znajdzie odpowiedniego wzorca, zwracany jest jeden z istniejących, w tym przypadku: kratka. Tu mała uwaga: lepiej zdefiniować tylko te znaki, które są rzeczywiście potrzebne, a nie cały alfabet jak leci, bo szkoda pamięci programu i naszego czasu. I dlatego chyba dobrym pomysłem byłoby zastąpienie domyślnego wyniku funkcji vfd_find_pattern() czymś "spektakularnym", wtedy braki w definicjach generatora znaków będzie od razu widać na display-u.

Słów kilka podsumowania

Zaprezentowane sterowanie matrycowym VFD, pomimo że na pierwszy rzut oka nieco skomplikowane, pozwala uzyskać pełną kontrolę nad lampą i umożliwia tworzenie wielu ciekawych efektów. Ponieważ procesor ma dostęp do wejścia /BLANK sterownika VFD, możemy na przykład zaprogramować sobie sterowanie jasnością poszczególnych znaków stosując modulację szerokości impulsu (PWM). Możemy też w miarę łatwo tworzyć animowane znaki, całą sztuka to odpowiednie operacje na bitach bufora wyświetlacza, fantazjować można długo...

A na koniec opowiadania fotografia 6 - zdjęcie wyświetlacza (wyposażonego w sprzętowy poprawiacz kontrastu, czyli zielony filtr),
pokazujące że na tak "oswojonej" lampie możemy sobie wyświetlać cokolwiek, począwszy od uśmieszków, serduszek i choinek a skończywszy na napisie "Portal EdW"...


fot. 6

Mała uwaga
Zamieszczone fragmenty kodu służą jedynie do ilustracji pewnych zagadnień, pełne i nawet działające oprogramowanie znajduje się w spakowanym archiwum vfd_5x7_src.zip.

Natasza Biecek 2004-2017/~, e-mail