Váratlan újraszámlálás
ATtiny programozás Arduino IDE nélkül
Az előző bejegyzésben egy fél mondatban megemlékeztem arról, hogy nem sikerült Raspberry Pi segítségével kódot juttatni az ATtiny-re. Azon a ponton a projekt befejezése volt fókuszban, úgyhogy az első működő megoldással mentem, de a probléma azóta sem hagyott nyugodni, úgyhogy következzék a második felvonás.
A terv az, hogy működésre bírjuk a Raspberry Pi és az ATtiny közötti kapcsolatot egy új hardver segítségével, majd egy kicsit közelebbről is megismerkedünk az ATtiny belső világával.
Szintlépés
Valahol olvastam arról, hogy okozhat az problémát, hogy a Pi 3.3V-os GPIO pin-jei kevesek az ATtiny-nek, aki jobban szereti az 5V-ot. Ennek megoldására rendeltem egy pár CD4504 fantázianevű IC-t:
┌───┬──┬───┐
VCC ┤ • └──┘ ├ VDD
A OUT ┤ ├ F OUT
A IN ┤ ├ F IN
B OUT ┤ ├ SELECT
B IN ┤ CD4504 ├ E OUT
C OUT ┤ ├ E IN
C IN ┤ ├ D OUT
VSS ┤ ├ D IN
└──────────┘
Lehet, hogy nem a tökéletes választás erre a célra, de később még jó lehet más projekthez is. A chip lényege, hogy a VCC-n kapott feszültséget cseréli le a VDD-n kapottra. Például VCC-n jön a 3.3V, VDD-n az 5V, akkor az A IN-en érkező 3.3V-os jelet az A OUT-on 5V-osként fogjuk megkapni.
A SELECT-tel lehetne TTL és CMOS jelek között váltani, de amennyire értek hozzá nekünk mind a két eszközünk CMOS, úgyhogy a SELECT-et alacsonyra állítjuk.
A mai játszadozásunkhoz most egy ATtiny84-et fogunk használni, az ATtiny24 még mindig a számlálós próbapanelen van. Pin-ek tekintetében ugyanolyan a kettő, csak a 84-esben egy kicsivel több memória van:
┌───┬──┬───┐
VDD ┤ • └──┘ ├ VSS
PB0 ┤ ├ PA0
PB1 ┤ ├ PA1
PB3 (RESET') ┤ ATtiny84 ├ PA2
PB2 ┤ ├ PA3
PA7 ┤ ├ PA4 (SCLK)
PA6 (MOSI) ┤ ├ PA5 (MISO)
└──────────┘
A két irányú kommunikációnk a következőképpen néz ki:
- a Pi-től az ATtiny felé (
RESET,SCLK,MOSI) - az ATtiny felől a Pi felé (
MISO)
Emiatt két darab CD4504-et használunk, az egyik a 3.3V->5V, a másik az 5V->3.3V irányba konvertál.
Hogy legyen valami kis átláthatóság a vezetékdzsungelben, a csíkos vezetékek 5V-osak, az egyszínűek pedig 3.3V-osak. A próbapanel felső része a 3.3V, az alsó része pedig az 5V. A Pi felől érkező jelek (RESET, SCLK, MOSI) a 3.3V->5V irányú CD4504-en mennek át, a Pi felé menő jel (MISO) az 5V->3.3V irányú CD4504-en megy át.
Feltöltés
Vegyünk egy egyszerű programot, mondjuk a klasszikus LED villogtatás. A PA7 pin-re rádugott LED-et egy másodpercig bekapcsolt, majd egy másodpercig kikapcsolt állapotban tartjuk.
blink.c
#define F_CPU 1000000UL
#include <avr/io.h>
#include <util/delay.h>
int main(void) {
DDRA |= _BV(PA7);
while (1) {
PORTA |= _BV(PA7);
_delay_ms(1000);
PORTA &= ~_BV(PA7);
_delay_ms(1000);
}
return 0;
}
Mint az látszik, itt kicsit másképp mennek a dolgok. Az Arduino IDE rengeteg dolgot rejtett el előlünk, a későbbiekben majd kitérünk ezekre egy kicsit részletesebben is.
Az ATtiny-re küldés sem csak egy gombnyomás lesz. Először is telepítenünk kell néhány csomagot. Pi 5-ön futó Raspberry Pi OS esetén nálam ez volt a nyerő parancs:
# apt install avrdude avr-libc gcc binutils
Aztán lefordíthatjuk a kis programunkat:
$ avr-gcc -mmcu=attiny84 -Wall -Os -o blink.elf blink.c
Ezen a ponton van lehetőségünk ellenőrizni, hogy rá fog-e férni a program az ATtiny-re az avr-size parancs segítségével:
$ avr-size --format=avr --mcu=attiny84 blink.elf
AVR Memory Usage
----------------
Device: attiny84
Program: 100 bytes (1.2% Full)
(.text + .data + .bootloader)
Data: 0 bytes (0.0% Full)
(.data + .bss + .noinit)
Ha minden jónak tűnik, akkor legyárthatjuk belőle a szükséges hex fájlt:
$ avr-objcopy -j .text -j .data -O ihex blink.elf blink.hex
Amit végre-valahára fel is másolhatunk az ATTiny-re:
$ avrdude -p t84 -c linuxspi -P /dev/spidev0.0:/dev/gpiochip0 -x disable_no_cs -B 10kHz -U flash:w:blink.hex
avrdude: AVR device initialized and ready to accept instructions
avrdude: device signature = 0x1e930c (probably t84)
avrdude: Note: flash memory has been specified, an erase cycle will be performed.
To disable this feature, specify the -D option.
avrdude: erasing chip
avrdude: reading input file blink.hex for flash
with 100 bytes in 1 section within [0, 0x63]
using 2 pages and 28 pad bytes
avrdude: writing 100 bytes flash ...
Writing | ################################################## | 100% 0.36 s
avrdude: 100 bytes of flash written
avrdude: verifying flash memory against blink.hex
Reading | ################################################## | 100% 0.34 s
avrdude: 100 bytes of flash verified
avrdude done. Thank you.
És ezzel meg is volnánk.... de ha már itt járunk, ismerkedjünk meg közelebbről egy kicsit az ATtiny-vel és írjuk meg újra a korábbi számláló programunkat.
Egy kis ismerkedés
Nem tervezek végigmenni a chip teljes (több, mint 200 oldalas) dokumentációján. Csak annyit fogunk megnézni, ami a számlálónkhoz feltétlenül szükséges. Az egésznek az alapja a regiszterek, amivel elérhetjük a különböző hardver funkciókat. Engem az MCP23S17-re emlékeztetett a megoldás (amit a 286-os projektben is használtunk). Érdekes módon ugyanaz a gyártó is.
Van például a DDRB regiszter, amivel a 'B' porton található pin-ek
(PB0-PB3) irányát tudjuk állítani. Az indexelés a bitek helyi értékének felel meg, a PB0 a legalacsonyabb helyi értékű bit. A PORTB regiszterrel lehet állítani a kimenő lábak állapotát, a PINB regiszterbe pedig a bejövő lábak értékei kerülnek. Hasonló módon léteznek ezek a regiszterek az 'A' port pin-jeire, de vannak regiszterek időzítők, megszakítások és sok egyéb más beállításaira és adataira is.
Ami nagyon jól látszik, hogy rengeteg bitműveletet fogunk használni. Kapunk is egy _BV nevezetű makrót a bit shift-elés rövidítésére (az 1 << (bit) műveletnek felel meg), amit gyakran fogunk használni a kódunkban.
Újraszámlálás
Ennyi bevezető után neki is kezdhetünk a számláló programunknak. A hardver ugyanaz lesz, mint korábban. Emlékeztetőül, ezek vannak a mikrokontrollerünk lábaira kötve:
PA0 -> Kijelző pin 12 (Dig1)
PA1 -> Kijelző pin 11 (a)
PA2 -> Kijelző pin 10 (f)
PA3 -> Kijelző pin 9 (Dig2)
PA4 -> Kijelző pin 8 (Dig3)
PA5 -> Kijelző pin 7 (b)
PA6 -> Kijelző pin 5 (g)
PA7 -> Kijelző pin 4 (c)
PB0 -> Kijelző pin 1 (e)
PB1 -> Kijelző pin 2 (d)
PB2 -> Léptető gomb
PB3 -> Reset gomb
Szerencsétlenségünkre a kijelző pin-jei mind a két portot érintik. Az egész projektet nem akartam átkábelezni, úgyhogy ezzel a helyzettel együtt kell élnünk majd a kódban. Az ideális valószínűleg az lenne, ha a szegmens pin-ek és a léptető gomb kerülne a PA oldalra, a számjegy választó pin-ek és a reset gomb pedig a PB oldalra.
Ha megnézzük a fenti összerendelést, a szegmensek eloszlása a két porton így néz ki:
PA | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
| c | g | b | _ | _ | f | a | _ |
PB | 3 | 2 | 1 | 0 |
---|---|---|---|---|
| _ | _ | d | e |
A kijelzőnk közös anódos, úgyhogy a szegmens pin-en nullát, a számjegy választó pin-en pedig egyet kell küldenünk, hogy az adott számjegy adott szegmense világítson. A nullás szám esetében az a-f szegmenseknek nullának, a g szegmensnek pedig egyesnek kell lennie, így a két portra a következő értékeket kell írnunk:
PA | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
| 0 | 1 | 0 | _ | _ | 0 | 0 | _ |
PB | 3 | 2 | 1 | 0 |
---|---|---|---|---|
| _ | _ | 0 | 0 |
Az aláhúzás jellel jelölt bitekkel nem itt kell majd foglalkoznunk, úgyhogy azoknak a helyére írhatunk nullát.
PA | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
| 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 |
PB | 3 | 2 | 1 | 0 |
---|---|---|---|---|
| 0 | 0 | 0 | 0 |
Ugyanezt megcsinálva a többi számjegyre is, a következő tömböket fogjuk kapni:
#define F_CPU 1000000UL
#include <avr/interrupt.h>
#include <avr/io.h>
const uint8_t segments_a[16] = {
0b01000000, 0b01000110, 0b10000100, 0b00000100,
0b00000010, 0b00100000, 0b00100000, 0b01000100,
0b00000000, 0b00000000, 0b00000000, 0b00100010,
0b11100000, 0b00000110, 0b10100000, 0b10100000,
};
const uint8_t segments_b[16] = {
0b0000, 0b0011, 0b0000, 0b0001,
0b0011, 0b0001, 0b0000, 0b0011,
0b0000, 0b0001, 0b0010, 0b0000,
0b0000, 0b0000, 0b0000, 0b0010,
};
Hogy egy kicsit izgalmasabb legyen a dolog, hexadecimális számot fogunk megjeleníteni, így 0-tól egészen 4095-ig el tudunk majd számolni.
const uint8_t pin[3] = { PA0, PA3, PA4 };
uint8_t dig[3] = { 0, 0, 0 };
uint8_t i = 0;
uint8_t stable = 0;
uint8_t samples = 0;
Lesz még néhány változónk az érték tárolásához, a számjegy kiválasztásához és a pergés kiszűréséhez.
Ezzel el is érkeztünk a main függvényhez.
int main(void) {
DDRA = 0b11111111;
DDRB = 0b0011;
TCCR1A = 0;
TCCR1B |= _BV(CS11) | _BV(WGM12);
OCR1A = 24;
TIMSK1 |= _BV(OCIE1A);
sei();
while (1) {}
return 0;
}
A függvény meglepően rövid, de annál értelmezhetetlenebb. Legalábbis a szép hosszú dokumentáció nélkül.
- A
DDRAésDDRBsegítségével az 'A' és 'B' portok irányát állítjuk be (minden kimenő, kivéve a két gombot). - A
TCCR1AésTCCR1Bsegítségével egy időzítőt kezdünk el beállítani.- A
CS11ésCS12(TCCR1B) bitek segítségével az időzítő órajele állítható be. Mivel az alapérték nulla, ezért nekünk csak az egyiket kell beállítanunk, hogy az ATtiny órajel nyolcadát kapjuk meg. - A
WGM10,WGM11(TCCR1A),WGM12ésWGM13(TCCR1B) bitekkel a számláló módját lehet beállítani. Az általunk beállított módban (csak aWGM12bit 1-es, a többi 0-ás) azOCR1Aregiszterben található értékig fog számolni, aztán lenullázza magát.
- A
- Az
OCR1Aregisztert beállítjuk 24-re, aminek hatására 0-tól 24-ig fogunk számolni. - A
TIMSK1az időzítő megszakítás beállításait tartalmazza. AzOCIE1Abit beállításával kapunk egy megszakítást, ha elértük azOCR1A-ban lévő értékét.
Végül a sei() függvény meghívása engedélyezi a megszakításokat és elérünk egy végtelen ciklushoz, ami nem csinál semmit.
Az ATtiny 1MHz-en[1] fut, az időzítő órajele ennek a nyolcada lesz, tehát 125kHz. A számlálót 25 alkalommal léptetjük egy megszakításhoz, így ezekkel a beállításokkal 1/5000 másodpercenként (0,2 ezredmásodpercenként) fogunk kapni egy megszakítást.
ISR(TIM1_COMPA_vect) {
PORTA = segments_a[dig[i]];
PORTB = segments_b[dig[i]];
PORTA |= _BV(pin[i]);
if (++i > 2) i = 0;
uint8_t now = (PINB & _BV(PB2)) ? 1 : 0;
if (now != stable) {
if (++samples > 50) {
stable = now;
samples = 0;
if (stable == 1) {
if (++dig[2] > 0xF) {
dig[2] = 0;
if (++dig[1] > 0xF) {
dig[1] = 0;
if (++dig[0] > 0xF) {
dig[0] = 0;
}
}
}
}
}
} else {
samples = 0;
}
}
Ő lenne a függvényünk, ami a megszakítás során meghívódik másodpercenként ötezer alkalommal.[2]
A PORTA/PORTB beállításával megjelenítjük az aktuális számjegyet és léptetjük i-t a következő számjegyre. Lehet itt egy olyan megérzésünk, hogy a két PORTA módosítást egybe is el lehetne végezni, de ez elég érdekes működésekhez tud vezetni:
PORTA = segments_a[dig[i]] | _BV(pin[i]);
PORTB = segments_b[dig[i]];
Ebben az esetben előbb váltunk számjegyet, minthogy a B porton lévő d és e szegmenset a megfelelő értékre állítanánk, így bizonyos esetekben a d és e szegmens halványan világítani fog akkor is, amikor nem kellene.
PORTB = segments_b[dig[i]];
PORTA = segments_a[dig[i]] | _BV(pin[i]);
Ebben az esetben pedig a 'B' porton lévő d és e szegmens előbb kap új értéket, minthogy számjegyet váltanánk, ami hasonló problémához vezet.
Az eredeti felállásban az 'A' port előbb kap értéket, de azzal együtt le is kapcsoljuk az összes számjegyet, a 'B' port is megkapja az új értéket majd bekapcsoljuk az új számjegyet. Így nincs olyan időpillanat, amikor rossz helyen világítana egy szegmens.
A kijelző frissítése után megnézzük a gomb értékét. Ha nem egyezik a jelenlegi stabil értékkel, akkor növeljük a mintavételek számát. Ha van ötven[3] jó mintánk, akkor az új érték lesz a stabil érték. Ha a gomb meg lett nyomva, akkor a szokott módon megnöveljük a számjegyek értékét is, de most már nem csak 9-ig megyünk, hanem 15-ig.
A letárolt stabil állapottal és a mintavételezéssel azt érjük el, hogy a gomb állapotváltozását csak akkor tekintjük érvényesnek, ha stabilizálódik az új jel legalább 10 ezredmásodpercnyi időre, kiszűrve ezzel a gomb lenyomása és felengedése pillanatában keletkező pergést.
A már fentebb tárgyalt módon a kódot feltölthetjük a számláláshoz használt korábbi ATtiny24-re és egy hexadecimális számláló boldog tulajdonosaivá válhatunk.
Számvetés
Ezen is túl vagyunk. Még egy számláló... ki gondolta volna az előző bejegyzés után? Az új kóddal sikerült valamivel jobbá tennünk az előző verziót:
- nem kellett
PROGMEM-et használnunk - 999 helyett 4095-ig tudunk elszámolni
- a pergés elleni védelem megbízhatóbb lett
- a kijelző számjegyeit sokkal gyorsabban frissítjük
Lehetne még mit javítani, ha unatkoznánk (például a kezdő nullák elrejtése), de engem csak eddig hajtott a kíváncsiság. Az Arduino IDE-ből kiszabadulva sokkal közelebb kerülhettünk a hardverhez. Talán közelebb is, mint szerettük volna.
Jegyzetek
1. ↑ Legalábbis az alapbeállítások szerint, ami módosítható biztosítékokkal, de az nem tenne jót a számításainknak.
2. ↑ Ezt leírva elkezdtem aggódni, hogy esetleg nem-e csinál túl sok mindent ez a függvény, úgyhogy utánaszámoltam és ha minden igaz, akkor a legrosszabb esetben is végez 60 órajel alatt és 200 órajelnyi időnk van két megszakítás között.
3. ↑ A minták számával lehet kísérletezgetni, hogy mi az a pont, amikor már nem kapunk fals gombnyomásokat és az ismétlődő gombnyomások sebessége is megfelelőnek érződik.