AVR-mikrokontrollerien alkeet "suoraan" C:llä

[size=140]Alkusanat[/size]
En tiedä minkä verran tällaiselle artikkelille on tarvetta, kun useimmat varmaan aloittavat harrastuksensa jollakin valmiilla kehitysalustalla, jolle on sitten omat valmiit kirjastonsa. Kirjoittelen nyt kuitenkin, toivonmukaan tämä selventää edes joillekin hiukan AVR:ien sielunelämää.

[size=140]Käytettävät ohjelmistot[/size]
Oletan tässä oppaassa, että kääntäjänä ja toolchainina käytetään avr-gcc:tä, avr-libc:tä ja avr-binutils:ia, sekä ohjelmointiin avrdudea. Tämä on se tavallinen yhdistelmä jos koneen käyttöjärjestelmä on Linux, kuten itselläni. Käytän koodiesimerkeissä ATmega324P:lle suunnattuja rekisterejä ja datalehden sivu- ja taulukko- yms. viittauksia. Käyttämäni datalehden päiväys on 08/09.

Oletan esimerkeissä, että käytössä on seuraava kytkentä:

[size=140]Huomautus rekisterinimistä ja keskeytysvektoreiden nimistä, liittyen koodin portattavuuteen.[/size]

Useimmat AVR:t käyttävät rekisteriniminä ja bittiniminä samoja nimiä samalle lisälaitteelle, poislukien mallit, joissa on vain yksi kappale kyseistä oheislaitetta verrattuna malliin, jossa on niitä esimerkiksi kaksi. Näistä esimerkkinä vaikka ATmega8 ja tässä oppaassa käytettävä ATmega324P. Tällöin esimerkiksi UART:in datarekisterin nimi on ensimmäisessä tapauksessa UDR ja jälkimmäisessä käytettävän laitteen (0 tai 1) mukaan UDR0 tai UDR1. Toinen esimerkki ensimmäisessä tapauksessa (ATmega8) UCSRB ja jälkimmäisessä (ATmega324P) UCSR0B tai UCSR1B. Nämä rekisterinimet ovat samat joita käytetään käytössä olevan kontrollerimallin datalehdessä. Samaan sarjaan nimeämisten kanssa kuuluu keskeytysvektoreiden nimeäminen. Nämä riippuvat käsittääkseni käytetystä C-kirjaston eli libc:n toteutuksesta, joten avr-libc:n tapauksessa keskeytysvektoreiden nimet käytetyn AVR-mallin mukaan kannattaa tarkistaa esimerkiksi avr-libc:n manuaalista: http://www.nongnu.org/avr-libc/user-manual/group__avr__interrupts.html.

[size=140]AVR:illä aloitus[/size]

AVR (kuten ilmeisesti useimmat muutkin yksinkertaiset 8-bittiset) on todella helppo siinä mielessä, että se ei tarvitse mitään oheislaitteiden tai kellojen asetuksia yksinkertaisesti käynnistyäkseen. Tämä tarkoittaa, että kontrollerille voidaan kirjoittaa yksinkertainen ohjelma, jossa on vaikka tyhjä pääluuppi, joka ei tee mitään. Kun se ajetaan sisään, niin kontrolleri toimii ja suorittaa ohjelmaa täysin oikein (tekemättä yhtään mitään järkevää…) ilman sen kummempia määrittelyjä.

testi1.c:

[code]#include <avr/io.h>

int main(void)
{

while(1)
{
}

return 0;

}[/code]

Tähän väliin voidaan heittää huomio, että kaikissa sulautetuille tehtävissä ohjelmissa tulee olla yksi ikuinen silmukkarakenne, eli ohjelman suoritus ei koskaan saa päättyä. Tämä johtuu ihan siitä, että esimerkiksi edellisen koodipätkän tapauksessa tuosta syntyvä binääri on ainoa, joka kontrollerilla tulee olemaan. Toisinsanoen sen alla ei pyöri mitään muuta käyttöjärjestelmää, johon ohjelman suoritus voisi siirtyä, kun oma ohjelmamme päättyy. Jos haluamme tehdä jonkin asian vaikka 10 kertaa ja sen jälkeen “lopettaa suorituksen”, niin ohjelman tulee siis kuitenkin jäädä ikuisesti rullaamaan silmukkaan, joka ei vaan enää tee mitään. (tuollaisessa tapauksessa tosin kontrolleri olisi hyvä heittää suoritettujen tehtävien jälkeen sleeppiin, jolloin virrankulutus laskee, siitä lisää myöhemmin)

[size=140]Kääntäminen ja Makefile[/size]

Edellisen ohjelmakoodin kääntäminen ja kontrollerille “prommaus” onnistuu helposti vaikka seuraavan Makefile:n avulla komennoilla make (kääntää ohjelman) ja make load (ajaa ohjelman kontrollerille avrduden avulla). Käännöstiedostojen putsaus onnistuu komennolla make clean.

Makefile:

[code]# Makefile

MCU: esim atmega8, atmega88, attiny2313, …

PARTNO: esim m8, m88, t2313, …

PROMMER: käytettävä ohjelmointilaite (viittaa asetuksiin /etc/avrdude.conf tiedostossa)

PORT: device node, johon käyttämäsi ohjelmointilaite tulee näkyviin

MCU=atmega324p
PARTNO=m324p
PROMMER=avrusb500v2
PORT=/dev/ttyUSB0

CC=avr-gcc
OBJCOPY=avr-objcopy

NAME=testi

optimize for size:

CFLAGS=-g -mmcu=$(MCU) -std=c99 -pedantic -Wall -Wextra -Wstrict-prototypes -Os -mcall-prologues -finline-limit=10 -fno-if-conversion

OBJ=

#-------------------
all: $(NAME).hex
#-------------------
help:
@echo “Usage: make all|load”

#-------------------
$(NAME).hex : $(NAME).out
$(OBJCOPY) -R .eeprom -O ihex $(NAME).out $(NAME).hex

$(NAME).out : $(NAME).o $(OBJ)
$(CC) $(CFLAGS) -o $(NAME).out -Wl,-Map,$(NAME).map $(NAME).o $(OBJ)
avr-size $(NAME).out

$(NAME).o : $(NAME).c $(NAME).h
$(CC) $(CFLAGS) -c $(NAME).c

#jotain.o : jotain.h jotain.c main.h foo.h moar.h

$(CC) $(CFLAGS) -c jotain.c

#------------------
load: $(NAME).hex
avrdude -P $(PORT) -p $(PARTNO) -c $(PROMMER) -e -y -U flash:w:$(NAME).hex

#-------------------
clean:
rm -f *.o *.map *.out *.hex
#-------------------[/code]

Huomautus edellisen Makefile:n rakenteesta:
Alussa määritellään käytetty kontrolleri. Kohta MCU on kääntäjälle, ja PARTNO on avrdude:lle. Ohjelmointilaitteen määrittely PROMMER viittaa /etc/avrdude.conf tiedostosta löytyviin ohjelmointilaitteen määrityksiin. Itse käytän tuxgraphics.org sivustolta aikoinaan ostamaani ohjelmointilaitetta, jolle olen lisännyt oman kohdan tuonne avrdude.conf:iin. Laite on kuitenkin stk500v2-yhteensopiva, joten voisin yhtähyvin käyttää Makefile:ssä vaikka avrispv2 tai avrispmkII prommeria, jotka ovat myös stk500v2-yhteensopivia.

NAME kohtaan tulee pääkooditiedoston nimi ilman .c päätettä. (tämä nyt on vain tehty helpottamaan Makefilen uudelleenkäyttöä, nimi voitaisiin tietysti muuttaa suoraan käännöskomennon yhteyteenkin) Koska kyseinen Makefile haluaa myös $(NAME).h tiedoston löytyvän, niin sellainen pitää luoda (tyhjäkin riittää), tai sitten $(NAME).h kohta poistetaan Makefile:stä.
Jätin esimerkkiin myös kohdan OBJ, johon listattaisiin kaikki objektitiedostot, mikäli koodi olisi jaettu useampiin tiedostoihin. (esimerkiksi tuo jotain.o) Tällöin jokaiselle moduulille (kooditiedostolle) tulee omat rivinsä/käännöskomentonsa, kuten tuo jotain.o esittää. Ensimmäisellä rivillä siis on ensin luotavan objektitiedoston nimi, sitä seuraa välilyönti, sitten kaksoispiste, sitten välilyönti ja sitten lista kaikista koodi- ja otsikkotiedostoista, joista kyseinen koodimoduuli on riippuvainen. Toisinsanoen siinä listataan kaikki tiedostot, joiden muuttuessa kyseinen moduuli halutaan kääntää uudelleen. Käytännössä siis kaikki otsikkotiedostot jotka on sinne includetettu.
Seuraavalla rivillä on ensin yksi tabulaattori, sitten käännöskomento, käännösliput ja lopuksi kerrotaan -c flägillä mikä kooditiedosto halutaan kääntää objektitiedostoksi.
Kääntäminen tapahtuu pääpiirteissään siten, että käännöskomennon mukaan katsotaan minkä niminen tiedosto ollaan luomassa, ja sitten sen kaikki riippuvuudet tarkistetaan ja tarvittaessa luodaan. Jos nuo riippuvuudet puolestaan vaativat joitain muita riippuvuksia, niin ne luodaan. Moduulit käännetään tai linkitetään uudestaan, mikäli jokin sen riippuvuuksista (esim. kooditiedostoista tai otsikkotiedostoista) on muokkausaikaleimaltaan uudempi kuin itse kyseinen moduuli (esim objektitiedosto). Eli toisinsanoen jos koodeihin on tehty muutoksia sen jälkeen kun moduuli on viimeksi käännetty.
Täten, kunhan kaikkien moduulien riippuvuudet on oikein määritelty, voidaan aina kääntää uudelleen vain ne osat ohjelmaa, jotka riippuvat tehdyistä muutoksista. Tuolla ei useimmiten AVR:ien kokoluokan projekteissa ole muutamaan sekuntia enempää merkitystä kääntäessä, mutta noin periaatteellisesti.

Avrduden komennossa -e flägi kertoo, että haluamme tyhjentää kontrollerin. Mukana oleva -y flägi on vapaaehtoinen, se tallentaa EEPROM:n loppuun viimeiseen neljään tavuun prommauskertojen määrän. Täten avrdude osaa joka prommauksen yhteydessä kertoa montako kertaa kyseinen kontrolleri on ohjelmoitu, mikäli kyseinen -y flägi on ollut aina käytössä ohjelmoinnin yhteydessä. Ideana on siis tietää koska kontrollerin ohjelmointikerrat ovat tulossa täyteen. Atmel lupaa AVR:ille yleensä 10 000 ohjelmointikertaa flash-muistille ja 100 000 EEPROM:lle.

[size=140]Kontrollerin kellotaajuus[/size]

Oppaassa käytettävä ATmega324P toimitetaan tehtaalta oletuksena sisäinen RC-oskillaattori valittuna, 8 MHz:n kellotaajuudella ja CKDIV8 fuse päällä eli oskillaattorin lähtötaajuus jaetaan 8:lla, jolloin CPU:n kellotaajuudeksi jää 1 MHz. Eri malleilla saattaa olla eri RC:n taajuudet, mutta yleisesti ottaen kaikki toimivat oletuksena sisäisellä oskillaattorilla ja n. 1 MHz:n CPU:n kellotaajuudella. Tällöin on aina mahdollista valita haluttu kellosignaalin lähde ilman erillisiä järjestelyjä. Jos oletuksena käytettäisiin ulkoista kidettä, ja käyttäjä haluaakin käyttää sisäistä oskillaattoria, olisi sulakkeiden (fuse) asettamisen ajan pakko kytkeä laitteeseen kide ja rinnakkaiskonkat, muutoin ei fuseja saataisi asetettua kun kontrollerilla ei olisi mitään kellosignaalia.

Mikäli kontrollerin kellosignaalin lähdettä halutaan muuttaa, täytyy sulakkeiden arvoja vaihtaa sopivasti halutun kellosignaalin lähteen mukaan. Kellosignaalien lähteistä löytyy tarkemmin tietoa datalehdestä sivulta 30 lähtien. Sulakkeiden asettamista käsitellään seuraavaksi, mutta esimerkeissä ei puututa kellosignaalin lähteeseen.

[size=140]Sulakkeiden (fuse) asettaminen[/size]
Sulakkeilla (fuse) asetetaan AVR:issä tiettyjä kontrollerin asetuksia, kuten kellolähteiden valinta ja tiettyjen toimintojen kuten JTAG aktivointi/pois kytkeminen. Sulakkeita muutettaessa tulee aina olla todella huolellinen ja tarkistaa sekä ohjelmoitava arvo, että ohjelmointikomento muutamaan otteeseen. Yksi komentoon liittyvä virhe on vahingossa ohjelmoida oikea arvo, mutta väärään sulaketavuun (hfuse vs. lfuse). Väärillä asetuksilla voi saada kontrollerinsa niin pahasti jumiin, että sitä ei enää itse saa herätettyä henkiin. Yksi tällainen mahdollisuus on valita kellolähteeksi ulkoinen kide, kun kontrolleri on juotettuna piirilevylleen ja XTAL1 pinniin ei ole enää mahdollista kytkeä ulkoista kellosignaalia, jolloin fuset voitaisiin korjata. Toinen, vielä pahempi moka on vahingossa disabloida SPI-ohjelmointi. Tosin datalehden mukaan tämä ei pitäisi olla mahdollista sarjaohjelmointimoodissa tehtynä.

Itse olen fuset aina asettanut vain suoraan avrdudella. Tähän löytyy muitakin, helpompia tapoja kuten ilmeisesti AVRStudion kautta, mutta en ole niitä koskaan käyttänyt enkä niistä osaa kertoa. Ja tämän oppaan tarkoituksena on opettaa tekemään asioita ns. suoraan.

Olen jokaisen pakasta vedetyn AVR:n aina konffinut vähintään siten, että muutan EESAVE fusen asennosta ‘1’ asentoon ‘0’, jolloin EEPROM-muistia ei tyhjennetä chip erasen yhteydessä eli siis joka kerta kontrolleria ohjelmoitaessa. Tällä säästän hiukan EEPROM:n kirjoituskertoja, ja mikä tärkeämpää, mikäli projektini käyttää EEPROM:iin tallennettuja asetuksia ja/tai laskureita, en joudu niitä asetuksia joka kerta naputtelemaan uudestaan ohjelmani valikoista.

Esimerkiksi ATmega324P:n yhteydessä kannattaa huomata myös, että JTAG on oletuksena aktivoitu, joka tarkoittaa sitä, että pinnit PC2…PC5 eivät toimi normaalilla tavalla. Olen tämän kantapään kautta joutunut toteamaan :wink:

[color=red]Huom! Seuraava esimerkki pätee vain ATmega164P/324P/644P kontrollereille! Älä käytä sitä suoraan muille tai voit saada kontrollerisi jumiin! Suosittelen myös vielä itse tarkistamaan bitit ja komennon jne, en ota mitään vastuuta seuraavan toimivuudesta!

Otetaanpa esimerkki ATmega324P:n fusen asetuksesta. Muutamme nyt fuseja siten, että EEPROM-muistia ei tyhjennetä ohjelmoitaessa/chip erasen yhteydessä, ja poistamme JTAG:n käytöstä. Nämä kummatkin asetukset löytynyvat ATmega324P:n tapauksessa hfuse eli High fuse tavusta.

ATmega324P:ssä hfusen oletusarvo on 0x99. Nyt siis muutamme EESAVE ja JTAGEN bitit, jolloin uusi arvo, jonka haluamme laitteeseen ohjelmoida on 0xD1.

Tämä saadaan asetettua avrdudella seuraavalla komennolla:

avrdude -p m324p -c avrusb500v2 -U hfuse:w:0xD1:m

Fusejen tilan voi tarkistaa avrdudella query komennolla. Tämä kannattaa ehkä tehdä sekä ennen että jälkeen fusejen asettamisen.

avrdude -p m324p -c avrusb500v2 -q -v

Esimerkeissä käytin taas oman ohjelmointilaitteeni asetusnimeä, muuta se oman laitteesi mukaiseksi. Voit mahdollisesti joutua lisäämään mukaan vielä portin määrityksen sen mukaan, miten oma ohjelmointilaitteesi näkyy. Tästä kerrottiin Makefile:n yhteydessä, mutta käytännössä siis tyyliin -P /dev/ttyUSB0 .

Otetaan toisena esimerkkina CKDIV8 fusen poistaminen käytöstä, jolloin kontrollerimme toimii (olettaen että kellosignaalin lähdettä ei ole vaihdettu) sisäisellä RC-oskillaattorilla ja ilman esijakajaa, joten siis suoraan 8 MHz:n kellotaajuudella.

Tämä asetus löytyy ATmega324P:ssä lfuse eli Fuse Low Byte eli alemmasta sulaketavusta. Tämän oletusarvo on 0x62. Muutamme nyt vain CKDIV8 bitin, jolloin saamme uudeksi arvoksi 0xE2.

avrdude -p m324p -c avrusb500v2 -U lfuse:w:0xE2:m

[size=140]AVR:n input/output-pinnien käyttäminen[/size]

Monelle mikrokontrollerien kanssa aloittelevalle käyttäjälle varmaan ensimmäinen kokeilu on vilkutella i/o-pinniin kytkettyä lediä. Tätä varten pitää kontrollerille kertoa, että haluamme kyseisen pinnin toimivan lähtönä.

AVR:ssä pinni voi toimia joko lähtönä (output) tai tulona (input). Lähdöksi määriteltyyn pinniin kontrolleri yrittää ajaa pinnin jännitetasoksi joko maatasoa tai käyttöjännitteen tasoa (käytännössä pieni FET-transistori ajaa pinniä joko maata tai käyttöjännitettä kohti) sen mukaan, mikä on kyseisen pinnin tilaksi määritelty ohjelmakoodissa. AVR:ssä pinnin bitin ollessa ‘0’, ajetaan pinniä maahan ja bitin ollessa ‘1’, ajetaan sitä käyttöjännitteeseen.

Tuloksi määriteltyyn pinniin kontrolleri ei yritä pakottaa mitään jännitetasoa. Mikäli tulopinniin on kytketty ylösvetovastus (pull-up) aktiiviseksi, on pinnissä tällöin n. 50 kilo-ohmin vastus käyttöjännitettä vasten. Tämä estää esimerkiksi muutoin kytkemätöntä tulopinniä kellumasta vapaasti ja ulkoisten radio- yms. signaalien tahdissa. Jos pull-up ei ole aktiivinen, on pinni niinsanotussa high-Z eli korkean tuloimpedanssin tilassa, jolloin kontrolleri ei teoriassa vaikuta pinnin jännitetasoon millään tavalla.

AVR:ssä i/o-pinnien käyttämiseen liittyy kolme eri rekisteriä: DDRx, PORTx ja PINx, jossa x on kyseessä olevan portin kirjain (yleensä A, B, C, D, E tai F, yleensä alkaen A:sta ja porttien määrä riippuu kontrollerimallissa olevien i/o-pinnien määrästä, yhteen porttiin mahtuu 8 pinniä).
DDR eli Data Direction Register määrää kyseisen portin pinnien suunnan, eli joko lähtö (output) tai tulo (input). Bitin ollessa ‘1’, on pinni lähtö, ja bitin ollessa ‘0’, on pinni tulo. Saman portin eri pinnit voidaan määritellä vapaasti lähdöiksi tai tuloiksi riippumatta toisistaan.
PORT rekisteri kertoo mitä arvoa portin pinneihin halutaan ajaa. Jos pinni on määritelty lähdöksi, ajetaan kyseistä arvoa ns. ‘väkisin’ eli FET-transistorien avulla. Jos pinni on määritelty tuloksi ja PORT-rekisterissä määritellään pinnin tilaksi ‘1’, on tällöin ylösvetovastus päällä. Pinnin ollessa tulona ja PORT-rekisterissä bitin arvo ‘0’, on pinni tällöin high-Z tilassa eli kontrolleri ei vaikuta sen jännitetasoon.
PIN rekisteri on portin/pinnien tilan lukemiseen tarkoitettu rekisteri. Sen sisältö kertoo, mikä looginen arvo (0 tai 1) eri pinneissä oikeasti on. Datalehti kertoo tarkemmin, miten loogisen 0:n ja 1:n jännitetasot määräytyvät, eli milloin jännite tulkitaan kummaksikin.

Otetaanpa esimerkki, jossa määrittelemme yhden pinnin lähdöksi ja sitten muutamme sen tilaa vuorotellen nollan ja ykkösen välillä, jolloin saamme siihen kytketyn ledin vilkkumaan. Käytämme nyt pinniin PB0 kytkettyä lediä.

[code]#include <avr/io.h>
#include <inttypes.h>

int main(void)
{
uint32_t i;

DDRB = (1 << DDB0);	// Määrittelemme B-portin Data Direction Register rekisterillä haluamamme pinnien suunnat. Nyt asetamme pinnin PB0 lähdöksi ja muut pinnit jätämme tuloiksi. DDB0 on makro tai paremminkin define-määrittely, joka on siis käytännössä lukuarvo. Tässä tapauksessa DDB0:n arvo on 0, joten emme shiftaa ykköstä kertaakaan vaan se jää alimpaan bittiin, jolloin se osuu PB0-pinniin vaikuttavaan bittiin rekisterissä. DDB0 on siis käytännössä sama asia kuin PB0.
// PORTB-rekisterin arvo on oletuksena 0

while(1)
{
	// Otamme loogisen XOR-operaation portin nykyisestä arvosta bittimaskin 0x01 eli 0000 0001 kanssa ja kirjoitamme kyseisen arvon takaisin porttiin,
	// joka käytännössä vaikuttaa siten, että joka kerralla kyseisen bitin tila kääntyy 0 -> 1 -> 0 -> 1 -> 0 jne.
	// Toisinsanoen, vilkutamme lediä \o/
	PORTB ^= (1 << PB0);
	// Käytämme tässä alun esimerkissä vielä huonoa tapaa tuottaa viive, eli delay-looppia.
	// Ilman viivettä tai muuta ajastusta, ehtisi kontrolleri suorittaa vilkuttamista usean sadan kilohertsin taajuudella tässä tapauksessa,
	// kun ohjelmassa ei tehdä mitään muuta. (tässä tapauksessa eli 1 MHz:n kellotaajuudella, jota esimerkeissä oletataan käytettävän)
	for(i = 0; i < 500000; i++)
		;
}


return 0;

}[/code]

Otetaan toinen esimerkki pinnin tilan lukemisesta. Valitettavasti sitä ei ole esimerkkikytkentäkaaviossa kun kirjoitan tätä jälkikäteen, mutta oletetaan, että pinniin PC0 on kytketty painonappi maata vasten. Eli siis kun nappia painetaan, kytkee se pinnin PC0 maahan. Määrittelemme pinnin PC0 tuloksi, ja aktivoimme siihen ylösvetovastuksen. Täten saamme ulkoisen painonapin toimimaan ilman muita komponentteja kuin nappi itse.

[code]#include <avr/io.h>
#include <inttypes.h>

int main(void)
{
uint8_t nappula;

// DDRC &= ~(1 << DDC0);	// DDRC-rekisterin arvo on jo oletuksena nolla, mutta tällä komennolla voisimme nollata PC0-pinniin vaikuttavan bitin.
// Kyseinen bittioperaatio siis tarkoittaa, että ensin otamme luvun jossa ykköstä shiftataan sopiva määrä, jotta se osuu PC0-pinnin kohdalle (nyt 0 kertaa), sitten otamme kyseisestä luvusta negaation, eli siis käännämme kaikki bitit vastakkaiseen arvoon, jolloin PC0-pinniä vastaava bitti on ainoa, joka on '0'. Tämän jälkeen luemme DDRC-rekisterin nykyisen arvon ja otamme bitti-AND:n rekisterin arvon ja äsken luomamme bittimaskin välillä, tuloksena on arvo, jossa PC0-pinniä vastaava bitti on nollautunut ja muut bitit on säilytetty ennallaan. Lopuksi kirjoitamme kyseisen arvon DDRC-rekisteriin.
PORTC = (1 << PC0);	// Aktivoimme ylösvetovastuksen PC0-pinniin.

while(1)
{
	nappula = PINC & (1 << PC0);	// Luemme PINC-rekisteristä eli portin tilaa ilmoittavasta rekisteristä arvon, ja tarkistamme siitä vain PC0-pinniä vastaavan bitin tilan. Täten siis nappula-muuttujan arvoksi jää nolla, mikäli nappia painetaan (koska nappi vetää pinnin maihin). Jos nappia ei paineta, on nappula-muuttujan arvo erisuuri kuin nolla (arvo riippuu siitä mitä bittiä/pinniä käytämme/luemme)

	if(nappula == 0)
	{
		// nappia painettu, tee jotain
	}
}

return 0;

}[/code]

Hieman hyödyllisempi tapa on kytkeä vaikka kuusi painonappia porttiin C ja lukea niiden tilat kerralla, ja sitten voimme tarkistaa mikä tai mitkä napit on pohjassa.

// ...
#define nappimaski	0x3F	// 6 nappia pinneissä PC0..PC5
#define NAPPI_1		0x01	// PC0
#define NAPPI_2		0x02	// PC1
#define NAPPI_3		0x04	// PC2
#define NAPPI_4		0x08	// PC3
#define NAPPI_5		0x10	// PC4
#define NAPPI_6		0x20	// PC5

// ...

	napit = (~PINC) & NAPPIMASKI;	// negatoimme nyt arvon, jolloin painettujen nappien bitti on arvossa '1'

	switch(napit)
	{
		case NAPPI_1:
			// nappi 1 painettuna
			// tee jotain
			break;
		case NAPPI_2:
			// nappi 2 painettuna
			// tee jotain
			break;
		// ...
		case NAPPI_1 | NAPPI_5:
			// napit 1 ja 5 painettuna
			// tee jotain
			break;
		default:
			break;
	}

[size=140]Oheislaitteet: ADC[/size]

Aloitamme oheislaite-esimerkit käsittelemällä lyhyesti ADC:n käyttöä. ADC (Analog to Digital Converter/Conversion) on käytännössä vain jännitteen mittaus. ADC-oheislaitteen sisääntulopinnissä näkyvä jännite suhteessa ADC:n maapinniin muunnetaan digitaaliseksi eli siis lukuarvoksi, jota sitten voidaan käyttää ohjelmassa. 8-bittisten AVR:ien ADC kykenee 10-bittiseen tarkkuuteen, eli saatu lukuarvo sisältää dataa kymmenessä bitissä. Muunnoksen tulos on suhteessa ADC:n referenssi- eli verrokkijännitteeseen. Toisinsanoen jos tulojännite on yhtä suuri kuin verrokkijännite, antaa ADC ulos maksimituloksen joka 10-bittisellä luvulla voidaan esittää, eli 2^10 - 1 = 1023. Jos tulojännite on nolla, on myös tulos 0. Täten siis jänniteväli nollasta verrokkijännitteeseen jakautuu teoriassa tasavälein niin moneen osaan, kuinka monta eri arvoa pystytään esittämään ADC:n bittitarkkuudella, eli nyt 1024:een osaan (koska myös nolla on arvo).

Täten siis saamme ADC:n antamaksi tulokseksi:
arvo = Uin / Uref * 1024

jossa 0 on analogisen maatason jännite, ja 0x03FF on referenssijännite miinus yksi LSB (vähiten merkitsevä bitti), koska suurin esitettävä arvo on 1023.

Ja kääntäen, jos haluamme tietää kuinka suuri jännite ADC-pinnillä on, kun tiedämme muunnostuloksen:
Uin = arvo / 1024 * Uref

Huom: Kummassakin tapauksessa kannattaa/pitää laskea ensin kertolasku, jotta emme menetä tarkkuutta ihan hirveästi. Toinen vaihtoehto olisi skaalata arvoa ensin ylöspäin, mutta se nyt on käytännössä sama asia kuin laskea kertolasku ensin. Eli integereillä laskiessa saisimme aina tulokseksi nollan jos laskemme jakolaskun ensin. Tämä siksi, että välitulos (oikeasti 0:n ja 1:n välillä) katkaistaan integeriksi, joka nyt on aina nolla.

Ja sitten itse koodin pariin. ATmega324P:n tapauksessa ADC:hen liittyvät rekisterit ovat ADMUX, ADCSRA, ADCSRB, ADCL, ADCH sekä DIDR0.
ADMUX rekisteri sisältää A/D-linjojen multipleksaustiedon (eli valittu A/D-linja, bitit MUX4…MUX0), A/D-muuntimen referenssijännitteen valinnan (bitit REFS1 ja REFS0), sekä bitin (ADLAR), jolla tulos voidaan tasata vasemmalle. Vasemmalle tasaus on hyödyllinen, jos halutaan käyttää vain 8-bittistä tarkkuutta, niin tuloksen saamiseksi riittää lukea ylemmän datarekisterin ADCH arvo, eikä tulosta tarvitse itse shiftailla erikseen. (datalehti s. 255)
ADCSRA rekisteri sisältää bitit ADC-laitteen enabloimiseksi (ADEN), A/D-muunnoksen aloittamiseksi (ADSC), automaattisen muunnoksen aloituksen enabloimiseksi (ADATE), A/D muunnoksen valmistumisen keskeytyksen enabloimiseksi (ADIE), ADC:n kellon esijakajan valitsemiseksi (ADPS2…ADPS0) sekä keskeytyslipun arvon (ADIF). A/D-muuntimen kellotaajuuden tulisi olla välillä 50-200 kHz, jotta saavutetaan maksimi resoluutio. ADATE-bitillä voidaan enabloida niinkutsuttu free-running tila, jossa A/D-muunnin tekee jatkuvaa muunnosta, eli aloittaa uuden heti edellisen muunnoksen valmistuttua. Tämä toimintatila on voimassa, mikäli ADATE-bitti on ‘1’ ja ADCSRB-rekisterin arvo on 0x00. (datalehti s. 243, 257, 259)
ADCSRB sisältää ADC:n automaattisen liipaisun lähteen valintaan 3 bittiä (ADTS2…ADTS0) (datalehti s. 258-259)
DIDR0 rekisteri sisältää bitit, joilla voidaan disabloida digitaaliset tulopuskurit pinneiltä, jotka ovat A/D-muuntimen käytössä. Tämä saattaa vähentää virrankulutusta aavistuksen (erityisesti paristokäyttöiset sovellukset). (datalehti s. 259)
ADCL ja ADCH rekisterit sisältävät A/D-muunnoksen tuloksen. Tuloksen sijainti siis riippuu ADLAR-bitin arvosta. Jos ADLAR on jätetty oletusarvoonsa ‘0’, sijaitsee tuloksen ylimmät 2 bittiä ADCH:n alimmissä kahdessa bitissä ja alemmat 8 bittiä on rekisterissä ADCL. Jos taas ADLAR on asetettu arvoon ‘1’, on tuloksen ylimmät 8 bittiä rekisterissä ADCH ja alimmat kaksi bittiä on ADCL:n ylimmissä kahdessa bitissä. (datalehti s. 258)
HUOM! Kun tulosta luetaan, pitää ensin lukea rekisteri ADCL ja sen jälkeen ADCH. Kun ADCL luetaan, ei arvoa päivitetä ennen kuin ADCH on luettu. Täten ja/tai toisaalta jos siis 8-bitin tarkkuus riittää ja ADLAR on asetettu arvoon ‘1’, riittää lukea vain ADCH, ja ADCL voidaan jättää koskematta. Muutoin ne on siis luettava järjestyksessä ensin ADCL, sitten ADCH.

Esimerkki:
Ja esimerkki kertonee enemmän kuin tuhat sanaa liirumlaarumia. Esimerkissä aktivoimme ADC:n, asetamme sille sopivan prescalerin käytetyn CPU:n kellotaajuuden (1 MHz) mukaan, valitsemme A/D-linjan, josta haluamme mitata (nyt ADC1, pinnissä PA1), valitsemme referenssijännitteeksi AVCC-pinnin, enabloimme A/D-muunnoksen valmistumisen keskeytyksen ja tasaamme tuloksen vasemmalle 8-bittisen tuloksen käyttöä helpottamaan. Tässä esimerkissä 8-bittinen A/D-muunnoksen tulos luetaan keskeytyksessä käyttäjän määrittelemään globaaliin muuttujaan adc_tulos, jota voidaan käyttää sitten muussa ohjelmassa. Huomaa volatile-määre, joka kertoo kääntäjälle, että muuttujan arvo voi muuttua normaalin ohjelman suorituksen ulkopuolella (eli nyt keskeytyksessä). Volatile-määrettä tulisi käyttää kaikkien muuttujien yhteydessä, joiden arvoa voidaan muuttaa keskeytyksessä. Tämä estää kääntäjää käyttämästä muuttujan yhteydessä optimointeja, joilla koodi ei toimisi oikein, kun arvoja muutetaan myös keskeytyksestä käsin.

Esimerkissä sytytetään LED pinnissä PB0, mikäli A/D-muunnoksen tulos on suurempi tai yhtäsuuri kuin 128, joka tarkoittaa noin puolet referenssijännitteen tasosta. Jos tulos on pienempi kuin 128, LED sammutetaan. Esimerkissä A/D-muunnos tehdään niin nopeasti kuin AVR siihen kykenee. Toisinsanoen ennen pääluuppia aloitamme ensimmäisen muunnoksen, ja sen jälkeen uusi muunnos aloitetaan ADC-keskeytyksessä, eli aina kun edellinen muunnos on valmistunut.

testi2.c:

[code]#include <avr/io.h>
#include <avr/interrupt.h>
#include <inttypes.h>

#define ADC_START_CONV() ADCSRA |= (1 << ADSC) // Makro A/D-muunnoksen aloitukseen

volatile uint8_t adc_tulos = 0;

ISR(ADC_vect)
{
adc_tulos = ADCH;
ADC_START_CONV();
}

int main(void)
{
DDRA &= ~(1 << DDA1); // Asetetaan A/D-tulopinni (nyt siis PA1) sisääntuloksi (tätä ei tarvitsisi tehdä erikseen, kaikki i/o-pinnit ovat oletuksena sisääntuloja)
DDRB = (1 << DDB0); // Oletetaan, että PB0 pinniin on kytketty LED, asetetaan siis PB0 lähdöksi

ADMUX = (1 << REFS0) | (1 << ADLAR) | (1 << MUX0); // AVCC referenssiksi, tasaa tulos vasemmalle ADCH rekisteriin ja valitse single ended (yksi tulopinni maata vasten mitattuna) kanava ADC1
ADCSRA = (1 << ADEN) | (1 << ADIE);	// Aktivoi ADC, aktivoi ADC keskeytys
ADCSRA |= (1 << ADPS1) | (1 << ADPS0);	// Aseta kellon esijakajaksi 8 (1 MHz / 8 = 125 kHz, joka on välillä 50..200 kHz)
DIDR0 = (1 << ADC1D);	// Poista digitaalinen tulopuskuri A/D-käytössä olevalta pinniltä PA1

sei();			// Salli keskeytykset
ADC_START_CONV();	// Aloita ensimmäinen A/D-muunnos

while(1)	// Pääohjelman ikuinen silmukka
{
	if(adc_tulos >= 128)
		PORTB |= (1 << PB0);	// Sytytä LED
	else
		PORTB &= ~(1 << PB0);	// Sammuta LED
}

return 0;

}[/code]

[size=140]Oheislaitteet: Ajastimet/laskurit (Timer/Counter)[/size]

Kaikissa AVR-malleissa on vähintään yksi Timer/Counter-oheislaite. Timerilla voimme helposti ja kohtuu tarkasti ajastaa joitakin toimintoja ohjelmassamme. Toinen yleinen käyttökohde on kahden tapahtuman välisen ajan mittaaminen.

Varsin usein esimerkkikoodeissa näkee tilanteita, joissa tarvitaan jonkin pituista viivettä kahden toiminnon välillä, ja viive on valitettavan usein tehty niinkutsutulla delay-loopilla. Se on tietysti helpoin ja nopein tehdä koodatessa, mutta se tuo mukanaan monta ongelmaa. Ensinnäkin delay-loopin kesto riippuu kontrollerin kellotaajuudesta, toisaalta kääntäjän optimoinneista ja kolmanneksi itse delay-loopin tarkasta rakenteesta. Lisäksi se blokkaa muun koodin suorituksen niin kauan, että viive on ohi.

Erityisesti jaksollisesti ajastettavat toiminnot on erittäin helppoa ja suositeltavaa tehtä ajastimen avulla. Mikäli kontrollerin kellosignaalin lähde on tarkka, saamme täten aikaan erittäin tarkasti ajastettuja toimintoja. Mikäli siis tarkasti ajatettavat toiminnot ovat tarpeellisia, kannattaa käyttää esimerkiksi ulkoista kidettä kellosignaalin lähteenä. Näiden virhe on usein luokkaa 10…100ppm, kun AVR:n sisäisen RC-oskillaattorin tarkkuus on luokkaa +/- 10%. Mikäli projektina on vaikka kello, on ero varsin huomattava :wink: (kellon heitto n. 1,7s/vuorokausi @ 20ppm vs. 2,4h/vuorokausi @ 10%)

Esittelen seuraavaksi ATmega324P:n Timer/Counter1:n toimintaa. Kyseinen ajastin on 16-bittinen, siinä on kaksi itsenäistä PWM-kanavaa (Pulse Width Modulation) asetettavalla jaksonpituudella (TOP-arvo), yksi input capture yksikkö (keskeytys ja ajastimen laskurirekisterin tallennus ulkoisen signaalin muuttuessa), se kykenee CTC-toimintaan (Clear Timer on Compare match) ja se kykenee tuottamaan keskeytyksen joko laskurin ylivuodosta, vertailuyksikön täsmätessä (huh mikä suomennos, eli compare match joko A ja/tai B kanavasta) ja input capture yksiköstä.

Timer/Counter1-yksikön rekisterit:
TCCR1A sisältää compare match yksikköön kytkettyjen pinnien toiminnan määrittelyn sekä kaksi bittiä ajastimen toimintatilan määrittävistä biteistä. (datalehti s. 132)
TCCR1B sisältää input capture yksikön kohinan eston aktivoinnin, input capturen signaalin reunan (nouseva/laskeva) valinnan, loput kaksi bittiä ajastimen toimintatilan valintaan sekä kolme bittiä ajastimen esijakajan valintaan. (datalehti s. 134)
TCCR1C sisältää compare match yksiköiden compare matchin pakotuksen.
TCNT1 on itse laskurirekisteri (koostuu osista TCNT1H ja TCNT1L, mutta voidaan käyttää suoraan TCNT1:nä eli 16-bittisenä)
OCR1A compare match A yksikön vertailuarvo, tai tietyisä PWM-moodeissa laskurin TOP-arvo. Myös 16-bittinen kuten TCNT1.
OCR1B compare match B yksikön vertailuarvo. Myös 16-bittinen kuten TCNT1 ja OCR1A.
ICR1 input capture yksikön lähtörekisteri, mikäli input capture toiminto käytössä. Sisältää tuolloin TCNT1-rekisterin arvon ulkoisen signaalin muutoshetkellä. ICR1 voi olla myös tieyissä PWM- tai CTC-moodeissa TOP-arvon sisältävä rekisteri.
TIMSK1 sisältää haluttujen keskeytysten valinnat.
TIFR1 sisältää keskeytysliput.

Esimerkki 1: ajastinkeskeytys CTC-moodissa
Otamme ensimmäiseksi esimerkiksi yksinkertaisesti ajastinkeskeytyksen tuottamisen tasaisin välein. Haluamme nyt tuottaa ajastinkeskeytyksen 10ms:n välein eli 100 kertaa sekunnissa. Käytämme tähän CTC-moodia, jossa ajastimen laskurirekisteri nollataan automaattisesti aina kun sen arvo täsmää asetettuun vertailuarvoon. Aktivoimme myös CTC-keskeytyksen, joten hyppäämme keskeytykseen jokaisen 10ms:n kohdalla. Keskeytysvektorissa asetamme käyttäjän määrittelemän muuttujan/rekisterin yhden bitin arvoksi ‘1’, jota seuraamme/tarkistamme pääohjelmassa. Laskemalla pääohjelmassa näitä 10ms:n tickejä, voimme tuottaa minkä pituisia viiveitä tahansa 10ms:n tarkkuudella. Huomaa, että jos pääohjelma tekisi jotain toimintoja jotka kestävät yli 10ms suorittaa, voimme olla huomaamatta joitakin keskeytyksessä asetettuja lippuja (eli asetamme sen kahdesti tai useammin ennen kuin ehdimme tarkistaa sen tilaa). Tällöin voisi olla parempi laskea kulunutta aikaa jo keskeytyksessä kasvattamalla jonkin laskurin arvoa. Toisaalta jos suoritus kestää yli 10ms, niin emme kuitenkaan voi ajastaa toimintoja 10ms:n välein, koska emme enää ehdi suorittaa niitä yhden aikaikkunan sisällä.

Ja koodiesimerkin pariin:
testi3.c:

[code]#include <avr/io.h>
#include <avr/interrupt.h>
#include <inttypes.h>

#define FLAG_10MS 0x01
#define LIPUT GPIOR0 // General Purpose i/o register 0, tätä ei ole kaikissa AVR-malleissa. Näiden käyttäminen on nopeampaa kuin SRAM muistin, joten ne ovat suositeltavia erityisesti usein keskeytyksissä käpisteltäviin käyttötarkoituksiin

ISR(TIMER1_COMPA_vect)
{
LIPUT |= FLAG_10MS; // Aseta bitti
}

int main(void)
{
uint8_t ticks_10ms = 0;

DDRB = (1 << DDB1) | (1 << DDB0);	// PB1 ja PB0 lähdöiksi ledejä varten

OCR1A = 9999;		// 10ms @ 1 MHz = 10 000 kellojaksoa. Yksi kellojakso tarvitaan CTC-logiikan resetointiin, siksi miinustetaan siitä yksi.
TIMSK1 = (1 << OCIE1A);	// Aktivoi Output Compare Match A keskeytys
TCCR1B = (1 << WGM12);	// CTC-moodi, jossa TOP-arvona toimii OCR1A
TCCR1B |= (1 << CS10);	// Käynnistä ajastin, esijakajan arvolla 1

sei();	// Aktivoi keskeytykset globaalisti

while(1)
{
	if(LIPUT & FLAG_10MS)	// Aina 10ms:n välein
	{
		if(++ticks_10ms >= 50)	// Aina 500ms = 0,5 sekunnin välein
		{
			PORTB ^= (1 << PB0);	// Muuta LED:n tilaa pinnissä PB0 puolen sekunnin välein
			ticks_10ms = 0;
		}
		PORTB ^= (1 << PB1);	// Muuta LED:n tilaa pinnissä PB1 10ms:n välein (vilkkuu siis 50Hz:n taajuudella, ei silmällä erota enää)
		LIPUT &= ~FLAG_10MS;	// Nollaa bitti
	}
}

return 0;

}[/code]

Vastaavalla tavalle voisimme lisätä useampia if-lauseita erilaisilla aikarajoilla saavuttaaksemme useita eripituisia jaksoja. Riippuen rakenteesta, jokainen näistä voisi tarvita oman muuttujansa/laskurinsa, elleivät ne ole sopivasti toistensa monikertoja.

Toinen tapa tuottaa ajastinkeskeytyksiä olisi käyttää ylivuotomoodia, ja asettaa aina keskeytyksessä laskurirekisterin arvo sopivasti siten, että ylivuotoon kuluva aika on haluttu. Mielestäni CTC on kuitenkin “helpompi” ja samalla säästää yhden rekisterisijoituksen.

Esimerkki 2: PWM
Otetaan toinen ajastinesimerkki PWM:n käytöstä. PWM eli Pulse Width Modulation on signaalin modulointitapa, jossa signaalilla on kiinteä päätaajuus, mutta signaalissa ON ja OFF jaksojen suhdetta muutetaan. Jos taajuus on riittävän korkea, ja signaalille tehdään hieman alipäästösuodatusta, saadaan täten PWM:llä tuotettua ulos erilaisia jännitteitä nollan ja syöttöjännitteen väliltä.

Esimerkkikuva wikipediassa: http://en.wikipedia.org/wiki/File:Pwm.png.

Esimerkissä ajamme lediä PWM:llä ilman mitään suodatuksia. Silmälle tämä kuitenkin näkyy erilaisina kirkkauksina, koska ledin päälläoloaika kokonaisajasta muuttuu. Oletamme, että LED on kytketty pinniin PD4, joka on aseteltavissa Timer/Counter1:n B-kanavan ulostulopinniksi.

testi4.c:

[code]#include <avr/io.h>

int main(void)
{
DDRD = (1 << DDD4); // PD4 ulostuloksi

TCCR1A = (1 << COM1B1);		// Fast PWM moodi: nollaa OC1B compare matchilla, aseta BOTTOM:ssa (eli ei-invertoitu PWM toiminta)
TCCR1A |= (1 << WGM10);		// Valitaan 8-bittinen Fast PWM moodi
TCCR1B = (1 << WGM12);		// Valitaan 8-bittinen Fast PWM moodi
TCCR1B |= (1 << CS10);		// Käynnistetaan ajastin, esijakaja 1, joten PWM:n taajuudeksi saadaan 1 MHz / 256 = ~3.9 kHz
OCR1B = 100;			// Tämä on B-kanavan vertailuarvo. Koska käytämme 8-bittistä moodia, niin se voi olla välillä 0..255. Mitä suurempi arvo, sitä kirkkaammin LED palaa.

while(1)
{
}

return 0;

}[/code]

Edellinen esimerkki on hivenen tylsä, se vain vilkuttaa lediä tietyllä pulssisuhteella, joka siis silmälle näkyy korkean taajuuden (3.9 kHz) ansiosta tiettynä tasaisena kirkkautena. Voisimme liittää mukaan ajastinkeskeytyksen avulla (edellinen esimerkki) tehdyn ajastetun toiminnan, jossa muuttaisimme vertailuarvoa jonkin ehtojen mukaan.

Otetaanpa toinen esimerkki, jossa kasvatamme ledin kirkkautta nollasta maksimiin ja sitten palaamme takaisin nollaan ja toistamme tätä loputtomiin.

testi5.c:

[code]#include <avr/io.h>
#include <avr/interrupt.h>
#include <inttypes.h>

#define FLAG_TIMER 0x01
#define LIPUT GPIOR0 // General Purpose i/o register 0, tätä ei ole kaikissa AVR-malleissa. Näiden käyttäminen on nopeampaa kuin SRAM muistin, joten ne ovat suositeltavia erityisesti usein keskeytyksissä käpisteltäviin käyttötarkoituksiin

ISR(TIMER1_OVF_vect)
{
LIPUT |= FLAG_TIMER; // Asetetaan bitti
}

int main(void)
{
uint8_t counter = 0;

DDRD = (1 << DDD4);	// PD4 ulostuloksi

TCCR1A = (1 << COM1B1);		// Fast PWM moodi: nollaa OC1B compare matchilla, aseta BOTTOM:ssa (eli ei-invertoitu PWM toiminta)
TCCR1A |= (1 << WGM10);		// Valitaan 8-bittinen Fast PWM moodi
TCCR1B = (1 << WGM12);		// Valitaan 8-bittinen Fast PWM moodi
TCCR1B |= (1 << CS11);		// Käynnistetaan ajastin, esijakaja 8, joten PWM:n taajuudeksi saadaan 1 MHz / 8 / 256 = ~488 Hz
OCR1B = 0;			// Tämä on B-kanavan vertailuarvo. Koska käytämme 8-bittistä moodia, niin se voi olla välillä 0..255. Mitä suurempi arvo, sitä kirkkaammin LED palaa.
TIMSK1 = (1 << TOIE1);		// Aktivoimme ajastimen ylivuotokeskeytyksen

sei();				// Sallitaan keskeytykset globaalisti

while(1)
{
	if(LIPUT & FLAG_TIMER)	// n. 2ms:n välein
	{
		if(++counter >= 6)	// n. 12ms:n välein
		{
			OCR1B = (OCR1B + 1) & 0xFF;	// Kasvatetaan arvoa yhdellä ja maskataan se kahdeksaan alimpaan bittiin, ts. pyörähdetään 255 -> 0
			counter = 0;
		}
		LIPUT &= ~FLAG_TIMER;	// Nollataan bitti
	}
}

return 0;

}[/code]

Ajastimien avulla voimme siis todella helposti tuottaa pulssimaisia lähtösignaaleja jopa suoraan raudalla. Ohjelmallisesti tehdyt vertailut ja arvojen kasvatukset ja porttien ohjaukset ovat suurilla taajuuksilla todella raskaita suorittaa ja vievät paljon CPU-aikaa muilta toiminnoilta, ja niillä ei edes tästä syystä päästä kovin korkeille taajuuksille. Esimerkiksi CTC-moodissa ajamalla pinniä suoraan raudalla voimme tuottaa ulos kanttiaaltoa jonka taajuus on maksimissaan puolet CPU:n kellotaajuudesta, ja se ei vaadi yhtään CPU-aikaa.

[size=140]Oheislaitteet: UART[/size]

UART eli asynkroninen sarjaväylä on erittäin kätevä tapa siirtää tietoa esimerkiksi AVR:n ja tietokoneen välillä, tai vaihtoehtoisesti vaikka kahden AVR:n välillä. Näin voidaan toteuttaa esimerkiksi lämpömittari, joka siirtää lämpötilatietoa tai jotain muuta mielenkiintoista PC-tietokoneelle, joka puolestaan piirtää niistä käppyrää webbisivuille (esimerkiksi näin: http://masa.dy.fi/pulsar_stats/). Mikäli PC:lle juttelu kiinnostaa, olen itse käyttänyt FTDI:n FT232RL-piirejä, jotka ovat UART to USB piirejä. Toinen hyvä vaihtoehto on ruuvipenkissäkin jokin aika sitten uutisoitu CP2102. Täten saamme mukavasti AVR:n kiinni tietokoneen USB-väylään. Esimerkiksi FT232RL-piirille on olemassa virtuaalisarjaporttiajuri, jolloin se näkyy tietokoneelle sarjaporttina. Sitä voidaan siten käyttää esimerkiksi terminaaliohjelmilla.

ATmega324P:ssä on UART-moduuleja 2 kpl. Täten rekisterien nimissä on 0 tai 1 sen mukaan, kumpaa niistä ollaan käyttämässä. Käytämme tässä esimerkissä ensimmäistä eli UART 0:aa. UART:n hallintaan liittyvät rekisterit ovat UDR0, UCSR0A, UCSR0B, UCSR0C ja UBRR0H sekä UBRR0L.

UDR0 on datarekisteri. Samaa i/o osoitetta eli tätä rekisteriä käytetään sekä lähetettävän että vastaanotettavan tiedon yhteydessä.
UCSR0A rekisteri sisältää tietoa UART-moduulin tilasta sekä datanopeuden tuplausbitin ja multiprosessorimoodin valinnan.
UCSR0B rekisteri sisältää UART:iin liittyvien keskeytysten aktivoinnin, lähetyksen ja vastaanoton sallimisen, merkkikoon valinnan sekä 9-bittisen merkkikoon tapauksessa lähetettävän ja vastaanotettavan tiedon ylimmän bitin.
UCSR0C rekisteri sisältää UART:n moodien valintaa, parity moodin valinnan, stop bittien määrän valinnan ja loput bitit merkkikoon valintaan.
UBRR0H rekisteri sisältää BAUD-nopeuden jakajan neljä ylintä bittiä.
UBRR0L rekisteri sisältää BAUD-nopeuden jakajan alimmat 8 bittiä.

UART:n yhteydessä tulee huomioida AVR:n kellosignaalin tarkkuus ja halutun BAUD-nopeuden virhe käytettävällä kellotaajuudella. Mitä suurempi virhe, sitä helpommin tiedonsiirtoon eksyy virheitä tai tiedonsiirto ei onnistu ollenkaan. Tätä varten on olemassa niinkutsuttuja “magic number” kiteitä, joiden kellotaajuus on valittu niin, että kaikki standardit BAUD-nopeudet voidaan tuottaa teoriassa ilman virhettä. Tällaisia kellotaajuuksia ovat esimerkiksi 1.8432 MHz ja sen monikerrat kuten 7.3728 MHz ja 18.432 MHz.

Esimerkki
Oletamme esimerkissä, että käytössä on edelleen 1 MHz:n CPU:n kellotaajuus. Käytämme esimerkissä BAUD-nopeutena 2400 bps, jolla virhe on teoreettisesti 0,2%. Suurempi tekijä onkin tässä sisäisen RC-oskillaattorin tarkkuus. Itse olen kuitenkin pystynyt projekteissa käyttämään näitä arvoja ongelmitta.

Ensimmäisessä esimerkissä aktivoimme UART:n ja lähetämme sen kautta sekunnin välein kasvavan lukuarvon. Käytämme ajastukseen timeria.

Ja koodin pariin.
testi6.c:

[code]#include <avr/io.h>
#include <avr/interrupt.h>
#include <inttypes.h>

#define FLAG_10MS 0x01
#define LIPUT GPIOR0 // General Purpose i/o register 0, tätä ei ole kaikissa AVR-malleissa. Näiden käyttäminen on nopeampaa kuin SRAM muistin, joten ne ovat suositeltavia erityisesti usein keskeytyksissä käpisteltäviin käyttötarkoituksiin

ISR(TIMER1_COMPA_vect)
{
LIPUT |= FLAG_10MS; // Aseta bitti
}

int main(void)
{
uint8_t ticks_10ms = 0;
uint8_t luku = 0;

OCR1A = 9999;		// 10ms @ 1 MHz = 10 000 kellojaksoa. Yksi kellojakso tarvitaan CTC-logiikan resetointiin, siksi miinustetaan siitä yksi.
TIMSK1 = (1 << OCIE1A);	// Aktivoi Output Compare Match A keskeytys
TCCR1B = (1 << WGM12);	// CTC-moodi, jossa TOP-arvona toimii OCR1A
TCCR1B |= (1 << CS10);	// Käynnistä ajastin, esijakajan arvolla 1

UCSR0B = (1 << TXEN0);	// Aktivoi UART:n lähetin
UCSR0C = (1 << UCSZ01) | (1 << UCSZ00);	// Valitaan merkin pituudeksi 8 bittiä
UBRR0L = 25;		// Valitaan 2400 bps BAUD-nopeus 1 MHz:n CPU:n kellotaajuudella

sei();	// Aktivoi keskeytykset globaalisti

while(1)
{
	if(LIPUT & FLAG_10MS)	// Aina 10ms:n välein
	{
		if(++ticks_10ms >= 100)	// Aina sekunnin välein
		{
			UDR0 = luku++;	// Asetamme UDR0-rekisteriin arvon (1 tavu), jonka haluamme lähettää, ja sitten kasvatamme lukuarvoa
		}
		LIPUT &= ~FLAG_10MS;	// Nollaa bitti
	}
}

return 0;

}[/code]

Jos AVR:n pinni PD1, joka on UART 0 moduulin Tx eli lähetyspinni, on nyt kytketty jotain kautta PC:lle Rx eli vastaanottopinniin, vaikkapa FT232RL-piirin RXD-pinniin, ja avaamme terminaaliohjelmalla sarjaporttilaitteen joka kuuluu FT232RL:lle, pitäisi terminaaliohjelmaan nyt ilmestyä sekunnin välein uusi lukuarvo, joka kasvaa joka kerta yhdellä. Huomaa, että lukuarvo on binääridataa, joten terminaaliohjelma kannattaa asettaa esim hexamoodiin.

Esimerkki 2:
Varsinaiseen hyödylliseen tiedonsiirtoon laitteiden välille tarvitsemme kuitenkin hiukan monipuolisemman ja monimutkaisemman rakenteen. Haluamme lähettää tietoa kehyksinä, joissa on otsikkotavu, komentotavu, jokunen tietoa sisältävä tavu ja tarkistussumma. Koska kehys koostuu useasta tavusta jotka halutaan ohjelmasta lähettää nopeasti peräjälkeen, on käytettävä tiedon puskurointia. Lisäksi kun käytämme hyödyksi UART:n keskeytyksiä, saamme tämän homman toimimaan kivasti ilman jatkuvaa puskurin seuraamista pääohjelmasta. Käytämme puskureina ns. rengas- tai ympyräpuskureita, eli indeksin kasvaessa puskurin lopun ohi se palautuu alkuun, jolloin puskuri näyttää tavallaan ympyrältä. Käytämme omia erillisiä puskureita lähetykselle ja vastaanotolle.

Periaatteena tässä on, että aina kun lisäämme lähetyspuskuriin dataa, aktivoimme samalla UDRIE eli “UDR-rekisteri tyhjä”-keskeytyksen. Täten aina kun edellinen tavu on oikeasti lähetetty eteenpäin, saamme keskeytyksen ja voimme lukea keskeytyksessä puskurista seuraavan lähetettävän tavun, mikäli niitä vielä on jäljellä. Jos puskuri on tyhjä, poistamme keskeytyksen käytöstä.

Vastaanotettaessa käytämme RXCIE eli “Rx Complete”-keskeytystä. Täten aina kun UART-vastaanottaa yhden tavun esim PC:ltä, saamme RXC-keskeytyksen, jossa lisäämme kyseisen tavun vastaanottopuskuriin. Sitten pääohjelmassa jonkinlaisin väliajoin käymme tarkistamassa, onko vastaanottopuskurissa tietoa ja jos on, alamme käsitellä sitä.

testi7.c:

[code]#include <avr/io.h>
#include <avr/interrupt.h>
#include <inttypes.h>

#define FLAG_10MS 0x01
#define LIPUT GPIOR0 // General Purpose i/o register 0, tätä ei ole kaikissa AVR-malleissa. Näiden käyttäminen on nopeampaa kuin SRAM muistin, joten ne ovat suositeltavia erityisesti usein keskeytyksissä käpisteltäviin käyttötarkoituksiin

/*
±-------±----±----±----±----±----±----+
| header | cmd | d1 | d2 | d3 | d4 | CRC |
±-------±----±----±----±----±----±----+
*/

#define UART_HEADER 0xAA
#define UART_RX_BUFFER_LENGTH 32
#define UART_TX_BUFFER_LENGTH 32
#define UART_TX_CMD_FOO 0x05 // keksitty komentotavu
#define UART_TX_CMD_BAR 0x34 // keksitty komentotavu

#define UART_RX_CMD_BAR 0x34 // keksitty komentotavu

volatile uint8_t rx_wr = 0;
volatile uint8_t rx_rd = 0;
volatile uint8_t tx_wr = 0;
volatile uint8_t tx_rd = 0;
uint8_t uart_tx_buffer[UART_TX_BUFFER_LENGTH];
uint8_t uart_rx_buffer[UART_RX_BUFFER_LENGTH];

uint8_t CRC8(uint8_t input, uint8_t seed)
{
uint8_t i, feedback;

for (i = 0; i < 8; i++)
{
	feedback = ((seed ^ input) & 0x01);

	if (!feedback) seed >>= 1;
	else
	{
		seed ^= 0x18;
		seed >>= 1;
		seed |= 0x80;
	}
	input >>= 1;
}

return seed;

}

uint8_t CRC8xN(uint8_t *input, uint8_t n)
{
uint8_t i, check;
check = 0;

for (i = 0; i < n; i++)
{
	check = CRC8(*input, check);
	input++;
}

return check;

}

void tx_byte(uint8_t byte)
{
uart_tx_buffer[tx_wr++] = byte;

if(tx_wr >= UART_TX_BUFFER_LENGTH)
	tx_wr = 0;

UCSR0B |= (1 << UDRIE0);	// Sallitaan "UDR-rekisteri tyhjä"-keskeytykset

}

uint8_t rx_byte(void)
{
uint8_t byte;

byte = uart_rx_buffer[rx_rd++];

if(rx_rd >= UART_RX_BUFFER_LENGTH)
	rx_rd = 0;

return byte;

}

void send_frame(uint8_t cmd, uint32_t data)
{
uint8_t buffer[7], i;
buffer[0] = UART_HEADER;
buffer[1] = cmd;
buffer[2] = data >> 24;
buffer[3] = (data >> 16) & 0xff;
buffer[4] = (data >> 8) & 0xff;
buffer[5] = data & 0xff;
buffer[6] = CRC8xN(&buffer[1], 5); // Lasketaan tarkistussumma lähtien cmd-tavusta ja päättyen tavuun ennen tätä CRC-tavua => nyt 5 tavua

for(i = 0; i < 7; i++)
	tx_byte(buffer[i]);

}

void uart_rx_handler(void)
{
uint8_t byte;
static uint8_t bufferi[6]; // Emme tallenna header-tavua => 6 tavua riittää
static int8_t indeksi = -1;

while(rx_rd != rx_wr)	// niin kauan kuin dataa riittää puskurissa
{
	byte = rx_byte();

	if(indeksi < 0)
	{
		if(byte == UART_HEADER)		// löydettiin otsikkotavu vastaanotettavasta tiedosta, kehys voi alkaa tästä
		{
			indeksi = 0;
		}
	}
	else
	{
		bufferi[indeksi++] = byte;
	}

	if(indeksi >= 6)	// yksi kehys kokonaan vastaanotettu
	{
		if(CRC8xN(bufferi, 5) == bufferi[6])	// CRC täsmää, kehys ehjä
		{
			// Tässä käsitellään vastaanotettu data
			switch(bufferi[0])	// cmd-tavu
			{
				case UART_RX_CMD_BAR:
					// kirjoita vaikka LCD:lle vastaanotettu arvo tai jotain...
					// lcd_kirjoita_luku(*(uint32_t *)&bufferi[1]);
					break;
				default:
					// tuntematon komentotavu
					break;
			}
		}
		else	// CRC ei täsmää, kehys rikki
		{
			// TODO: errori iski, tee jotain
		}

		indeksi = -1;	// Siirrytään odottaamaan seuraavaa otsikkotavua
	}
}

}

ISR(TIMER1_COMPA_vect)
{
LIPUT |= FLAG_10MS; // Aseta bitti
}

ISR(USART0_RX_vect)
{
uart_rx_buffer[rx_wr++] = UDR0;

if(rx_wr >= UART_RX_BUFFER_LENGTH)	// palataan alkuun, jos mennään puskurin lopun ohi
	rx_wr = 0;

}

ISR(USART0_UDRE_vect)
{
if(tx_rd != tx_wr) // kun indeksit ovat erisuuret, on puskurissa tietoa
{
UDR0 = uart_tx_buffer[tx_rd++]; // Lähetetään yksi tavu

	if(tx_rd >= UART_TX_BUFFER_LENGTH)
		tx_rd = 0;
}
else
	UCSR0B &= ~(1 << UDRIE0);	// Puskuri tyhjä, kielletään keskeytys

}

int main(void)
{
uint8_t ticks_10ms = 0;
uint32_t luku = 1;

OCR1A = 9999;		// 10ms @ 1 MHz = 10 000 kellojaksoa. Yksi kellojakso tarvitaan CTC-logiikan resetointiin, siksi miinustetaan siitä yksi.
TIMSK1 = (1 << OCIE1A);	// Aktivoi Output Compare Match A keskeytys
TCCR1B = (1 << WGM12);	// CTC-moodi, jossa TOP-arvona toimii OCR1A
TCCR1B |= (1 << CS10);	// Käynnistä ajastin, esijakajan arvolla 1

UCSR0B = (1 << TXEN0) | (1 << RXEN0) | (1 << RXCIE0);	// Aktivoi UART:n lähetin, vastaanotin ja Rx Complete keskeytys
UCSR0C = (1 << UCSZ01) | (1 << UCSZ00);	// Valitaan merkin pituudeksi 8 bittiä
UBRR0L = 25;		// Valitaan 2400 bps BAUD-nopeus 1 MHz:n CPU:n kellotaajuudella

sei();	// Aktivoi keskeytykset globaalisti

while(1)
{
	uart_rx_handler();	// Käsittele vastaanottopuskurin data, mikäli siellä on jotain.

	if(LIPUT & FLAG_10MS)	// Aina 10ms:n välein
	{
		if(++ticks_10ms >= 100)	// Aina sekunnin välein
		{
			send_frame(UART_TX_CMD_FOO, luku);	// Lähetetään yksi kehys. Kehys on tyyppiä "UART_TX_CMD_FOO".
			luku *= 2;	// kerrotaan luku vaikka kahdella
		}
		LIPUT &= ~FLAG_10MS;	// Nollaa bitti
	}
}

return 0;

}[/code]

Esimerkki on vain esimerkki miten homman voi toteuttaa. Erityisesti funktiokutsuja kannattaa muokata ja viedä niille pointteri puskurin sisältävään structiin, ja kaikki uart-funktiot kannattaisi siirtää omaan kooditiedostoonsa.

[size=140]Sleep-toiminto eli kontrollerin laittaminen uneen[/size]
Sleep-toiminto on varsin hyödyllinen, mikäli systeemin virrankulutusta tulisi minimoida. Toisaalta samalla myös periaatteessa saadaan kontrolleri käymään viileämpänä, mutta AVR nyt ei lämpene merkittävästi muutenkaan. Sleep voidaan aktivoida siis silloin, kun kontrollerin ei tarvitse hetkeen suorittaa mitään toimintoja tai laskentaa. AVR:ssä on muutamia erilaisia sleep moodeja, riippuen kontrollerin mallista. Näissä erona on käyntiin jäävät oheislaitteet, sekä herätykseen kelpaavat tapahtumat. Niinsanotusti vähäisin unimoodi on IDLE, jossa vain itse kontrollerin core eli koodia suorittava osa ja flashmuistin luku pysäytetään. Alhaisin unitila taas on STANDBY. Siinä on lisäksi i/o-kello, ADC-kello ja asymmetristen toimintojen kello pysäytetty. Eri unitiloista ja herätyslähteistä löytyy hyvä taulukko datalehdestä sivulta 42.

Otetaanpa esimerkki, jossa teemme yksinkertaisen kellon. Tätä varten aktivoimme keskeytyksen 1 sekunnin välein. Keskeytyksessä asetamme yhden bitin tilaan ‘1’ ja pääohjelmassa lisäämme kellonaikaan sekunnin aina kun bitti on ylhäällä. Laitamme kontrollerin uneen aina siksi aikaa kun emme tee mitään.
Tämä esimerkki vaatii toimiakseen, että kontrolleriin on kytketty 32,768 kHz:n kellokide TOSC1 ja TOSC2 pinneihin.

[code]#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/sleep.h>
#include <inttypes.h>

#define LIPUT GPIOR0
#define FLAG_RTC 0x01

typedef struct
{
uint8_t year;
uint8_t month;
uint8_t day;
uint8_t hour;
uint8_t min;
uint8_t sec;
uint8_t wday; // Viikonpäivä, välillä 0…6
uint8_t isdst; // onko kesäaika? 1 = kesäaika, 0 = talvika/normaaliaika
} rtc_t;

static uint8_t rtc_not_leap(uint16_t year)
{
// Vuosi on karkausvuosi, mikäli:
// 1: Vuosi on jaollinen neljällä, tai
// 2: Vuosi on jaollinen sadalla JA 400:lla

if(! (year % 100) )		// Jaollinen sadalla
	return (year % 400);	// ... mutta ei jaollinen 400:lla -> ei karkausvuosi
else
	return (year % 4);	// Ei jaollinen neljällä -> ei karkausvuosi

}

void update_rtc(rtc_t *rtc, uint8_t increment)
{
rtc->sec += increment; // Kasvata sekunteja

if(rtc->sec >= 60)	// Minuutti vaihtuu
{
	rtc->sec -= 60;		// Minuutti vaihtui

	if(++rtc->min >= 60)	// Tunti vaihtuu
	{
		rtc->min -= 60;

		if(++rtc->hour >= 24)	// Vuorokausi vaihtuu
		{
			rtc->hour -= 24;

			if(				// Kuukausi vaihtuu, mikäi seuraava on tosi
				rtc->day == 31 ||
				(rtc->day == 30 && (rtc->month == 4 || rtc->month == 6 || rtc->month == 9 || rtc->month == 11)) ||
				(rtc->day == 29 && rtc->month == 2) ||
				(rtc->day == 28 && rtc->month == 2 && rtc_not_leap(2000 + rtc->year))
			)
			{
				rtc->day = 1;

				if(++rtc->month >= 13)
				{
					rtc->month = 1;
					rtc->year++;
				}
			}
			else
				rtc->day++;

			if(++rt->wday >= 7)
				rtc->wday = 0;

		}
		// Vaihda kesäaikaan
		else if(rtc->month == 3 && rtc->wday == 6 && rtc->day >= 25 && rtc->hour == 3)
		{
			rtc->hour++;
			rtc->isdst = 1;
		}
		// Vaihda normaaliaikaan
		else if(rtc->month == 10 && rtc->wday == 6 && rtc->day >= 25 && rtc->hour == 4 && rtc->isdst == 1)
		{
			rtc->hour--;
			rtc->isdst = 0;
		}
	}
}

}

ISR(TIMER2_COMPA_vect)
{
LIPUT |= FLAG_RTC; // Aseta bitti/lippu
}

int main(void)
{
rtc_t rtc;
rtc.year = 11;
rtc.month = 2;
rtc.day = 27;
rtc.hour = 14;
rtc.min = 1;
rtc.sec = 0;
rtc.wday = 6;
rtc.isdst = 0;

// Käytämme Timer/Counter2:sta asynkronisessa moodissa keskeytyksiin, jolloin se toimii herätyslähteenäkin
// Safe procedure BEGIN for switching to asynchronous operation:
TIMSK2 = 0x00;						// Disable interrupts
ASSR = (1 << AS2);					// Select asynchronous clock source
TCNT2 = 0;						// Clear the counter
OCR2A = 0x1F;						// dec: 31; 1 second at 32768 Hz and prescaler 1024 = 32 cycles = 0..31
TCCR2A = (1 << WGM21);					// Select CTC mode
TCCR2B = (1 << CS22) | (1 << CS21) | (1 << CS20);	// Start Timer2 with prescaler 1024

// Wait until the values get latched into the registers
while( ASSR & 0x1F ) ;					// Wait until the values get latched into the registers
// Safe procedure END

TIFR2 = 0x07;		// Clear the interrupt flags
TIMSK2 = (1 << OCIE2A);	// Timer/Counter2 Output Compare Match A Interrupt Enable

// Seuraavassa kytkemme pois kaiken tarpeettoman minimoidaksemme virrankulutuksen
ACSR = (1 << ACD);		// Poista käytöstä analoginen vertailija (Analog Comparator)
ADCSRA &= ~(1 << ADEN);		// Poista ADC käytöstä
DIDR0 = 0xFF;			// Poista digitaaliset tulopuskurit ADC-pinneiltä PA0..7
PRR = 0xBF;			// Poista käytöstä kaikki oheislaitteet paitsi Timer/Counter2 (Power Reduction Register)

set_sleep_mode(SLEEP_MODE_PWR_SAVE);	// Valitaan sleep moodiksi POWER_SAVE

sei();	// Salli keskeytykset

while(1)
{
	if(LIPUT & FLAG_RTC)	// Sekunti kulunut
	{
		update_rtc(&rtc, 1);	// Päivitä kellonaika
		LIPUT &= ~FLAG_RTC;	// Nollaa bitti/lippu

		sleep_enable();		// Aseta SE (Sleep Enable) bitti eli salli unitila
		sleep_cpu();		// Siirry unitilaan (SLEEP komento)
		sleep_disable();	// Kiellä unitila. Tämä suositellaan tehtävän, kun kontrolleri ei saa mennä uneen, eli heräämisen jälkeen.
	}
}

return 0;

}[/code]

Pari vuotta sitten tekemäni testin mukaan ATmega324P:n virrankulutus PWR_SAVE unitilassa on luokkaa 5 µA, mikäli kaikki muut oheislaitteet on sammutettu Timer/Counter2:sta lukuunottamatta ja kaikki i/o pinnit on asetettu sisääntuloiksi ja ylösvetovastukset poistettu käytöstä.

[size=140]Loppusanat[/size]
Huhhuh, tulipa tekstiä. Toivottavasti nuo esimerkit nyt edes jokseenkin toimii, en vielä ehtinyt/jaksanut niitä itse testata :wink:
Perustuvat osittain vanhoihin projekteihin (lähinnä tuo UART-koodi), joten periaattellisesti jokseenkin noin, toivottavasti on käytännössäkin edes lähes oikein. Ilmoitelkaa virheistä niin korjailen, ellen itse innostu joku päivä testaamaan noita esimerkkejä…

[size=140]Muokkaushistoria[/size]
2011-02-27 14:20: Lisätty sulakkeiden (fuse) asettaminen, hiukan lisämainintaa kellosignaalin lähteestä ja lisätty osio sleep:stä.
2011-03-01 16:57: Korjattu jälkimmäiseen UART:n koodiesimerkkiin puuttuvat static määreet uart_rx_handler()-funktioon.
2011-03-08 00:38: Lisätty alkuperäisestä artikkelista kokonaan unohtunut osio i/o-pinnien käyttämisestä. Pahoittelut sen puuttumisesta tähän asti.

Millä virittelet fuseja? Tiedän, että ne voi hihkaista avr-dudelle. Tämä on kuitenkin hankalaa ja virheherkkää. Itse kuitenkin teen niin. Aina tarvitsee miettiä mitä jokainen bitti tarkoittaa.

Ja jos sontakasa menee tuulimyllyyn, ja laittaa ulkoisen kiteen päälle vaikkei olekaan sellaista, tilanteesta useimmin selviää nykäisemällä kontrollerin kannastaan irti ja laittamalla sen koekytkentälevylle ja ajamalla esimerkiksi ATMega88:n kellon ulosotosta XTAL1 pinniä. Silloin sen voi ohjelmoida.

Toisaalta sopivasta signaaligeniksestä voi kytkentäänkin ajaa sopivaa 0.5-5MHz: kanttia. Usein vain muu kytkentä häiritsee tuota pelastusoperaatiota. Siksi DIP ja DIP-kanta on hyvä ajatus.

UARTin käsittelyyn on olemassa ihan mukava kirjasto. DS1820 lämpötilaesimerkki (google löytää) on ainakin käytetty. Sitten on liblcd, joka tekee tekstinäytön käpistelystä mukavaa.

Niin ja ADC voitiin tehdä Aref, AVcc tai sisäisen referenssin välillä useinmiten.

Tuli myös mieleen, että nämä voisi jossain muistuttaa.

/*globaaleina muuttujina*/
const unsigned char vakio = 0x42;
volatile unsigned char irq_sta_muutettava_muuttuja = 0x42; 
void foo(){
  static unsigned char tila = 0x42;
  switch(tila){
    case 0x42:
      bar();
      tila = 0x24:
      break;
    case 0x24:
      joku_juttu();
      break;
  }
}

Tuossahan foon ensimmäisellä kerralla ajetaan bar() ja sen jälkeen joku_juttu() ja tilan arvo muistetaan kutsujen välillä. kiva ominaisuus, kun sitä tarvitsee.

Fusejen laskuun engbedded.com/fusecalc
ite tykkään.