A végső visszaszámlálás
LCD vezérlés ATtiny segítségével
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.
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.
COM1pin-t3V-ra állítjuk.COM2-4pin-eket1.5V-ra állítjuk.- A
2-es pin-t0V-ra állítjuk. - A többi szegmens pin-t (
1,3,4,5,6)3V-ra állítjuk. - Várunk egy kicsit.
COM1pin-t0V-ra állítjuk.COM2-4pin-ek maradnak1.5V-on.- A
2-es pin-t3V-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.
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.
Ö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:
0b000101000b011110100b01110110
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.