A végső visszaszámlálás

LCD vezérlés ATtiny segítségével

Ellenállások és egy LCD kijelző, ami a 02A számot mutatja

Számos számlálós szösszenetünk után már csak egy elvarratlan szál maradt: mi történt a cuki kis LCD panelekkel?

Úgy másfél hónap bolyongás után végül ideértek. A lábaikat egyesével kellett kiegyenesítenem, mert az út igencsak megviselte őket, de azt leszámítva egészségesnek tűntek. Itt az ideje kezdeni velük valamit.

IC-kre hasonlító LCD kijelzők

Hogyan működik az LCD?

Fogalmam sincs, szerintem varázslat. De a sok viszontagság után azt hiszem azt legalább el tudom mesélni, hogy hogyan lehet őket működtetni. Legalábbis ezt a konkrét panelt, nem tudom mennyire hasznosítható ez a tudás más LCD-kre.

A korábban használt LED-es kijelzőnek 3 COM és 7 szegmens lába volt (nyolc, ha a pontot is számoljuk, de azzal nem foglalkoztunk). Ennek az LCD-nek 4 COM lába és 6 szegmens lába van, amiből lehet sejteni, hogy a COM lábak nincsenek egy az egyben megfeleltetve a számjegyekkel. Szerencsére van egy táblázat, ami megmondja nekünk a kettő közötti kapcsolatot.

COM / Pin 1 2 3 4 5 6 7 8 9 10
COM1 3D 2D 1D COM1
COM2 3C 3E 2C 2E 1C 1E COM2
COM3 3B 3G 2B 2G 1B 1G COM3
COM4 3A 3F 2A 2F 1A 1F COM4

Pont lett volna még hely a pontnak is. Az is látszik, hogy egy számjegyhez két pin tartozik. A számozás balról jobbra értelmezendő, az 1D szegmens a bal szélső számhoz tartozik, a 3D szegmens a jobb szélsőhöz. Biztos kellemes lesz majd ezt lekódolni, de mielőtt még oda jutnánk, más huncutságokat is tartogat nekünk ez a kijelző.

A LED kijelző esetén közös anódos és katódos fajta volt, ami eldöntötte, hogy a szegmens pin-t egyre a COM pin-t pedig nullára kell állítanunk vagy fordítva. Az LCD-nél ilyen nincsen, váltogatnunk kell ezt az irányt minden egyes alkalommal. És ez még nem minden. Az inaktív COM pin-eknek nem az aktív ellentétének kell lennie, hanem a feszültség felén kell tartani őket.

Nézzünk egy példát. Mondjuk a 3D szegmenst szeretnénk bekapcsolni, ami a COM1 és a 2-es pin-ek kereszteződésében van. A kijelzőnek 3V-ra van szüksége, úgyhogy a feszültség fele 1.5V lesz és van a földünk, ami 0V.

  • COM1 pin-t 3V-ra állítjuk.
  • COM2-4 pin-eket 1.5V-ra állítjuk.
  • A 2-es pin-t 0V-ra állítjuk.
  • A többi szegmens pin-t (1, 3, 4, 5, 6) 3V-ra állítjuk.
  • Várunk egy kicsit.
  • COM1 pin-t 0V-ra állítjuk.
  • COM2-4 pin-ek maradnak 1.5V-on.
  • A 2-es pin-t 3V-ra állítjuk.
  • A többi szegmens pin-t 0V-ra állítjuk.
  • Várunk egy kicsit és kezdődik az egész elölről.

Tetszőleges szám megjelenítéséhez ezt a huzavonát kell általánosítanunk:

Lépés COM1 COM2 COM3 COM4 1 2 3 4 5 6
1. 3V 1.5V 1.5V 1.5V - 0V - 0V - 3V
2. 1.5V 3V 1.5V 1.5V 0V 3V 3V 0V 0V 3V
3. 1.5V 1.5V 3V 1.5V 0V 0V 0V 0V 0V 3V
4. 1.5V 1.5V 1.5V 3V 0V 3V 0V 3V 3V 3V
5. 0V 1.5V 1.5V 1.5V - 3V - 3V - 0V
6. 1.5V 0V 1.5V 1.5V 3V 0V 0V 3V 3V 0V
7. 1.5V 1.5V 0V 1.5V 3V 3V 3V 3V 3V 0V
8. 1.5V 1.5V 1.5V 0V 3V 0V 3V 0V 0V 0V

A COM pin-ek tánca fixen ismétlődik, a szegmens pineket pedig az aktuálisan megjelenítendő szám határozza meg. Ha nem rontottam el semmit, akkor ezt a nyolc lépést ismételgetve a kijelzőn az 123-as szám fog megjelenni. Szépen látszik, hogy az első négy lépés a második négy lépés inverze.

Már csak azt kell kitalálnunk, hogy honnan fogunk 1.5V-ot szerezni.

Az ellenállás nem is hasztalan

Két "trükköt" fogunk alkalmazni. Az egyik az az, hogy ha a 0V és 3V közé sorba kötünk két ellenállást, akkor a két ellenállás között pont 1.5V-ot kapunk. Hogy pontosan mekkora ellenállásokat? Biztos ki lehetne számítani, én két darab 47 kΩ-ossal próbálkoztam és bevált.

A másik "trükk", hogy hogyan fogunk tudni a három feszültségi szint között váltogatni. Most is egy ATtiny84-et fogunk használni, aminek három állapotú lábai vannak: alacsony, magas, magas impedancia (bemeneti mód). Ha a mikrokontroller lábát az LCD lábával a két feszültségosztó ellenálláson keresztül kötjük össze, akkor alacsony állapotban 0V-ot fog kapni az LCD, magas állapotban 3V-ot, bemeneti állapotban pedig 1.5V-ot.

Kapcsolási rajz: ATtiny84 és LCD kijelző

Az LCD-t egy tíz lábú IC helyettesíti

A vezetékezésnél az elsődleges cél az volt, hogy egyszerűbb legyen majd kódolni. Ezért is van szegény ATtiny fejjel lefelé, hogy az összes szegmens pin az A portra kerülhessen. A COM pin-ek pedig nagyrészt a B portra (a RESET miatt nincs ott elég hely), ami majd meg is látszik a kódon. Gombokkal most nem foglalkozunk, a program automatikusan fogja csökkenteni a számlálót bizonyos időközönként.

A fizikai összeszereléshez egy még kisebb próbapanelt használtam, amit aztán többször meg is bántam, nem volt rajta túl sok hely, de kinézetben jól passzol az LCD kijelzőhöz.

A korábbi mikrokontrolleres számláló és az LCD számláló egymás alatt

Összehasonlításként látható a képen a korábbi legkisebb LED-es változatunk is.

Sajnos a lábak távolsága végül nem bizonyult teljesen próbapanel-kompatibilisnek, de egy kis rábeszéléssel azért meg lehetett győzni, hogy belemenjen.

Rá is térhetünk a kódolásra. Előre szólok, vérlázító mennyiségű bitművelet következik, csak szülői felügyelet mellett ajánlott.

Bitkolbászolás

A felprogramozás maradt a régi, csak a szegmenseket kell valamilyen más módon működésre bírnunk. Először is egy kis összefoglaló, hogy hogyan is vannak a dolgaink összekötve:

LCD pin ATtiny pin Számjegy COM1 COM2 COM3 COM4
6 PA0 1 D E G F
5 PA1 1 - C B A
4 PA2 2 D E G F
3 PA3 2 - C B A
2 PA4 3 D E G F
1 PA5 3 - C B A

A számjegyek ugyanazt a mintát követik, de egy számjegy szegmensei több COM-ra vannak szétdarabolva. Egy COM-on két bitnyi szegmens adat van egy számjegyhez, az négy COM-mal pont nyolc bit, úgyhogy tárolhatjuk a szegmens adatokat mondjuk így:

    | COM4  | COM3  | COM2  | COM1  |
    |-------|-------|-------|-------|
    | F | A | G | B | E | C | D | _ |
|---|---|---|---|---|---|---|---|---|
| 7 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 0 |
|---|---|---|---|---|---|---|---|---|
| 8 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 |
|---|---|---|---|---|---|---|---|---|

Az _ helyére bármit írhatnánk, én nullával mentem. Az összes számunkra így néznek ki ezek az értékek:

#define F_CPU 1000000UL

#include <avr/interrupt.h>
#include <avr/io.h>
#include <util/delay.h>

const uint8_t segments[16] = {
    0b11011110,
    0b00010100,
    0b01111010,
    0b01110110,
    0b10110100,
    0b11100110,
    0b11101110,
    0b01010100,
    0b11111110,
    0b11110110,
    0b11111100,
    0b10101110,
    0b11001010,
    0b00111110,
    0b11101010,
    0b11101000,
};

Ezen kívül kelleni fog még néhány segédváltozó:

const uint8_t com_pins[4] = { PA7, PB2, PB1, PB0 };
const uint8_t seg_pins[6] = { PA5, PA4, PA3, PA2, PA1, PA0 };

uint8_t com_idx = 0;
uint8_t direction = 0;

int16_t count = 0xFFF;

A com_idx lesz az, hogy éppen hányadik COM az aktív, a direction dönti el, hogy nulla vagy egy lesz éppen az értéke, a count a szám, ahol éppen tartunk. Jöhet is a main függvény:

int main(void) {
  DDRA = 0b11111111;
  DDRB = 0b0111;

  PORTA = 0;
  PORTB = 0;

  TCCR1A = 0;
  TCCR1B |= _BV(CS12) | _BV(WGM12);
  OCR1A = 15;
  TIMSK1 |= _BV(OCIE1A);

  sei();

  while (1) {
    _delay_ms(1000);
    cli();
    if (--count < 0) {
      count = 0xFFF;
    }
    sei();
  }
  return 0;
}

Nagyon hasonló az előzőhöz, az egyetlen különbség, hogy lassabbra vettük az időzítőt és másodpercenként csökkentjük a megjelenítendő számunkat. Ha csak a CS12 aktív, akkor a rendszer órajel 256-od részét kapja csak meg az időzítő, amivel 16-ig számolunk, tehát a megszakítás függvényünk kb. 240-szer hívódik meg másodpercenként. Ez a négy COM miatt még négy részre oszlik, tehát körülbelül a 60 Hz-es frissítést fogjuk hozni.

Nézzük is meg a megszakítás függvényünket:

ISR(TIM1_COMPA_vect) {
  if (direction) {
    PORTA |= _BV(seg_pins[0]) | _BV(seg_pins[1]) | _BV(seg_pins[2]) | _BV(seg_pins[3]) | _BV(seg_pins[4]) | _BV(seg_pins[5]);
  } else {
    PORTA &= ~(_BV(seg_pins[0]) | _BV(seg_pins[1]) | _BV(seg_pins[2]) | _BV(seg_pins[3]) | _BV(seg_pins[4]) | _BV(seg_pins[5]));
  }

  // ...
}

Először is a szegmens pin-jeinket az iránynak megfelelő kikapcsolt állapotba helyezzük.

PORTA &= ~_BV(com_pins[0]);
DDRA &= ~_BV(com_pins[0]);
PORTB &= ~(_BV(com_pins[1]) | _BV(com_pins[2]) | _BV(com_pins[3]));
DDRB &= ~(_BV(com_pins[1]) | _BV(com_pins[2]) | _BV(com_pins[3]));

Felhúzó ellenállás nélküli bemenetre állítjuk az összes COM pin-t (magas impedancia állapot).

if (com_idx == 0) {
  DDRA |= _BV(com_pins[com_idx]);
  if (direction) {
    PORTA |= _BV(com_pins[com_idx]);
  } else {
    PORTA &= ~_BV(com_pins[com_idx]);
  }
} else {
  DDRB |= _BV(com_pins[com_idx]);
  if (direction) {
    PORTB |= _BV(com_pins[com_idx]);
  } else {
    PORTB &= ~_BV(com_pins[com_idx]);
  }
}

A com_idx által jelölt COM pin-t átkapcsoljuk kimeneti üzemmódba és az iránynak megfelelő jelet küldjük ki rajta. Itt van egy kis huncutság, hogy az egyik pin-ünk az az A porton van, így azt külön kell kezelnünk a többitől.

uint8_t seg_value1 = segments[(count >> 8) & 0b1111];
uint8_t seg_value2 = segments[(count >> 4) & 0b1111];
uint8_t seg_value3 = segments[count & 0b1111];

uint8_t dig1 = (seg_value1 >> (com_idx * 2)) & 0b11;
uint8_t dig2 = (seg_value2 >> (com_idx * 2)) & 0b11;
uint8_t dig3 = (seg_value3 >> (com_idx * 2)) & 0b11;

uint8_t data = (dig1 << 4) | (dig2 << 2) | dig3;

Kiszámoljuk a szegmens pin-ek állapotát, ami első ránézésre egy kicsit vadnak tűnhet, de nézzünk meg egy példát, hogy mit is csinál pontosan. Tegyük fel, hogy a count értéke 0x123, ami binárisan 0b000100100011.

A count >> 8 az 0b0001, amit ha bináris és-elünk 0b1111-gyel, akkor továbbra is 0b0001 marad, ami az egyes szám.

A count >> 4 az 0b00010010, bináris és-elve 0b1111-gyel 0b0010, ami a kettes szám.

A count (0b000100100011) bináris és-elve 0b1111-gyel pedig 0b0011, ami a hármas szám.

Tehát a fenti segments tömbből kiszedjük az első, második és harmadik elemet:

  • 0b00010100
  • 0b01111010
  • 0b01110110

A számokat ezután eltoljuk jobbra 0-val, 2-vel, 4-gyel vagy 6-tal, attól függően, hogy a com_idx éppen melyik COM-nál jár és bináris és-eljük őket 0b11-gyel. Az eredmény COM1 esetén a zöld számok, COM2 esetén a kék számok, COM3 esetén a narancssárga számok és COM4 esetén a lila számok lesznek.

Ezek után még egy balra eltolás és bináris vagy-olás és végül COM1 esetén megkapjuk a 0b001010 értéket, COM2 esetén a 0b011001 értéket, COM3 esetén a 0b011111 értéket és COM4 esetén a 0b000101 értéket. Pont azokat a biteket, amiket az adott COM esetén a szegmenseknek be kell állítanunk.

for (uint8_t i = 0; i < 6; ++i) {
  if (direction ^ ((data >> i) & 1)) {
    PORTA |= _BV(seg_pins[i]);
  } else {
    PORTA &= ~_BV(seg_pins[i]);
  }
}

Merő véletlenségből a seg_pins tömbben pont olyan sorrendben vannak a pin-ek, ahogy az adatban, amit összeépítettünk, úgyhogy kivesszük a megfelelő bitet az adatból és beállítjuk az értéket az iránytól függően.

if (++com_idx > 3) {
  com_idx = 0;
  direction = !direction;
}

Végül átvándorolunk a következő COM-ra és ha a COM-ok végére értünk, akkor kezdjük elölről az egészet a másik irányból.

0x000

Itt van hát a vég, kifogytunk a számokból.

Kihoztunk mindent, amit a számlálókból csak lehetett, sőt, még többet is. Nem maradt több elvarratlan szál... csak az az egy bizonyos projekt, aminek a halogatása elindította ezt az egészet.

Egyszer talán majd oda is eljutunk.

This post is also available in English: The final countdown

Hozzáfűznél valamit?

Dobj egy emailt a blog kukac deadlime pont hu címre.

Feliratkoznál?

Az RSS feed-et ajánljuk, ha kedveled a régi jó dolgokat.