Srkze vstupně výstupní porty komunikuje Atmel se světem. Všechno co dělá nebo čte skrze ně prochází. I přes to že je jejich správa vcelku snadná se uděláme malou prohlídku toho co s nimi jde.
Defakto všichni členové rodiny AVR mají shodné ovládání vstupně výstupních portů a stejné možnosti. Těch možností není mnoho. Každý pin procesoru může být buď vstup nebo výstup. A v kterékoli z těchto dvou rolí je možné u něj zapnout interní pull-up rezistor. Což je prostě rezistor připojený mezi vstupně výstupní pin a napájení čipu. Až na pár jemných nuancí je to vlastně všechno co vstupně výstupní porty umí. Možnost isi předvedeme třeba na čipu Atmega16A.
Vývody Atmelu jsou rozčleněny do tzv. portů. Jeden port má obvykle 8 vývodů (pinů). Porty jsou pojmenovány písmeny, takže existuje Port A, Port B, Port C atd. U menších atmelů nejsou porty plně obsazeny. Jinak řečeno třeba Atmega8 má celý Port D a Port B, ale Portu C jeden vývod chybí. Větší atmely jako třeba Atmega16 mají čtveřici portů, tedy 32 vývodů. Každý vývod má svůj název. Vývody portu C se jmenují PC0,PC1, ... až PC7. Číslování je tedy od nuly. Sdružování vývodů do portů má své výhody. S portem se dá zacházet jako s celkem a program tak může chytře používat celou osmici vývodů (ne však nutně). Každý vývod lze konfigurovat nezvávisle na ostatních. Takže nic neříká, že když je PB1 konfigurován jako vstup, že tak musí být i jeho sousedé.
Nejprve se podívejme jak se chová pin Atmelu jako vstup. Datasheet v sekci "Absolute Maximum Ratings" jasně říká, že na žádném pinu kromě Resetu nesmíme překročit napětí Ucc+0.5V. Napětí Ucc je napájecí napětí čipu. Tedy při napájení 5V nesmíte přivést na vývod větší napětí jak 5.5V. Podobný limit existuje i pro záporné napětí, na žádný z pinů nesmití přivést napětí nižší jak -0.5V. Každý pin má totiž proti GND a proti Ucc ochrannou diodu. Ta ale nejspíš slouží k ochraně před statickým nábojem a nebude schopná odvádět větší proudy, proto na ni neberte zřetel. Obecně platí že hodnoty uvedené v "Absolute Maximum Ratings" by jste překračovat neměli ... nikdy. Reset pin snese až 13V. Tato schopnost ale slouží k HVP programování a z hlediska aplikace je slušné pracovat s Resetem jako s běžným pinem. Dalším limitem je maximální proud výstupu, ten by neměl překročit 40mA. Posledním limitem, který datasheet uvádí je celkový proud všemi napájecími piny. Ten činí 200mA u PDIP pouzder (ty s nožičkami) a 400mA u SMD a MLF pouzder. Tím pádem se nemůžete vymlouvat, že vám čip shořel, když jste každým z jeho 32 vývodů pustili 25mA (celkem tři čtvrtě ampéry). Přirozeně některé překročení těchto limitů Atmel snese. Ze zkušenosti bych mohl referovat o mnohém porušení limitů, které nevedlo ke zničení čipu. Třeba o tině, která vykonávala program a blikala LEDkou i při 12V napájení. Nikdy se ale na něco takového nespoléhejte a vždy si dobře rozmyslete zda používáte vstupy a výstupy v souladu s datasheetem. Murphyho zákony jsou neúprosné a může se vám stát, že při ladění aplikace na nepájivém poli udláte chybu, která se ale neprojeví a vy si jí nevšimnete. Třeba přivedete 5V na vstup čipu s napájením Ucc=3.3V. Necháte vyrobit desky plošných spojů, osadíte je a nebudete se stačit divit proč vám najednou Atmely hoří :)
Vstup Atmelu zaručeně vnímá jako log.1 napětí které je větší než 0.6Ucc a jako log.0 napětí které je nižší jak 0.2Ucc. Může se stát, že vstup bude správně pracovat i mimo tyto limity, ale datasheet vám to nezaručuje. Zajímat vás tyto hodnoty boud hlavně v situacích kdy budete chtít nějáký 3V systém skamarádit s 5V. Vývody XTAL a Reset mají úrovně drobně jinačí, v případě potřeby je dohledáte v datasheetu v sekci "Electrical Characteristics". Do vstupů může téct jistý malý zbytkový proud. Datasheet ho charakterizuje jako menší než 1uA, ale z měření vím, že to typicky bývá o několik řádů menší. Jedinou vyjímkou je situace kdy na vstup přivádíte nějákou analogovou hodnotu, tedy mezi limity pro log.1 a log.0. Pak může do vstupů téct nějáký signifikatní proud. Takovým sitaucím by jste se měli vyhnout. A v případě že na pin potřebujete přivádět analogovou honodu tak využít možnosti vypnout vstupní buffer (jinak řečeno odpojit celou vstupní logickou část pinu). Jak to provést se dozvíte později (nebo zde).
Výstup Atmelu má push-pull topologii. To znamená že v log.1 dokáže dodávat proud a v log.0 dokáže odebírat proud z vnějšího obvodu. Výstupy jsou dost "tvrdé" na to aby mohly přímo ovládat LED (přes rezistor přirozeně !). "Tvrdost" výstupu se pozná podle toho jak moc protákající proud ovlivňuje jeho výstupní napětí. Ideálně tvrdý výstup má nulový výstupní odpor a ať skrze něj protéká jakýkoli proud, je jeho napětí stálé. Reálné porty ale tak moc tvrdé nejsou. Jak se proudové zatížení projeví na jejich napětí shrnují grafy v datasheetu v sekci 28.0.7. Z nich je patrné, že výstupní odpor se pohybuje okolo 25Ohmů při 5V napájení a 50Ohmů při napájení 2.7V. to znamená že pokud zatížíte čip napájený 5V proudem 20mA, bude výstupní napěťová úroveň v log.1 přibližně 4.5V. Podobné paramery platí i pro opačnou situaci, kdy držíte výstup v log.0 a vnějším obvodem vám do výstupu teče proud. Tady je na místě malá poznámka o výkonu. Jestliže protéká výstupem proud 20mA a je na něm úbytek 0.5V (tedy odpor 25 Ohm) je na něm i výkonová ztráta P=U*I = 0.5*0.02 = 10mW. Ta se projevuje uvnitř atmelu a pokud podobným způsobem zatížíte více výstupů, může ztrátový výkon přispívat k ohřevu pouzdra. To vás většinou zajímat nebude, ale pokud budete využívat vnitřní teplotní senzor je potřeba k tomu přihlédnout.
Na každém vývodu ať už je nakonfigurovaný jako vstup nebo jako výstup je možné zapnout vnitřní pull-up rezistor. Ten vám v mnohých aplikacích ušetří pracné připojování pull-up rezistoru z vnějšku. Jeho hodnota je podle datasheetu mezi 20-50kOhm, ale můžete si ji snadno změřit přesně. Stačí pull-up rezistor zapnout (a vývod mít nakonfigurovaný jako vstup) a pak na vývod připojit proti zemi známý rezistor s hodnotou řádově v desítkách kOhm. Ten vytvoří s interním pull-up rezistorem dělič, na vašem rezistoru změříte napětí a s trochou matematiky se dopracujete k hodnotě interního pull-up rezistoru. Druhou možností je měřit přímo ohmmetrem, ale pak dbejte na správnou polaritu a připojte plus měřáku na Ucc. Mínus měřáku pak na příslušný vývod. Já na Attiny2313A naměřil hodnoty v rozptylu od 35KOhm do 36kOhm. Na resetu jsem pak naměřil 69kOhm (datasheet uvádí 30-85kOhm). Reset má pevný pull-up rezistor, takže ho nemůžete vypnout. Reset musíte držet v neutrálním stavu v log.1 (restartujete log.0), takže tuto vlastnost oceníte. Obecně díky internímu pull-up rezistoru se vám značně zjednodušuje návrh obvodů. Tlačítka, klávesnice, enkodéry, to všechno díky tomu můžete připojovat přímo na vývody Atmelu. Typicky také obvody připojené k externímu přerušení budou vyžadovat pull-up rezistor protože mají výstupy s otevřeným kolektorem. Díky tomu je jich pak možné připojovat na jeden vstup Atmelu více.
Ovládání najdete snad v každém návodu na internetu. Takže budu stručný. Každý port má registr DDRx (Data Direction Register), kde si za "x" dosaďte písmenný název portu (A,B,C...). Každý bit v tomto registru ovládá jeden z vývodů portu. Zápisem log.1 do příslušného bitu DDRx je příslušný pin nastaven jako výstup. Příklad: Jestliže chci PB3 nastavit jako výstup musím zapsat do DDRB registru log.1 na třetí místo (při číslování od nuly !). Například kombinace DDRB=0b00010010 nastaví PB1 a PB4 jako výstupy. Po restartu čipu jsou všechny DDRx vynulované, tedy všechny vývody jsou vstupem. Druhý registr nese název PORTx, kde si za x dosaďte písmenné označení portu. Tedy například PORTB, PORTC atd. Každý bit v tomto registru se opět váže k příslušnému vývodu portu (stejně jako u DDRx). Role každého bitu v registru PORTx je dvojí a záleží na tom jestli je daný pin nastaven jako vstup nebo jako výstup. Jestliže je pin nastaven jako vstup tak hodnota příslušného bitu v PORTx registru zapíná nebo vypíná interní pull-up rezistor. Log.1 ho zapíná, log.0 vypíná. Chci-li tedy zapnout pull-up rezistor na pinu PA0, musím do DDRA zapsat na nultou pozici nulu (konfigurace jako vstup) a pak do PORTA na nultou pozici jedničku (zapínám pull-up). Pokud je vývod nastaven jako výstup, pak hodnota v registru PORTx rozhoduje o tom zda na výstupu bude log.1 nebo log.0. Zapsáním log.1 na nultou pozici v PORTA a DDRA nastavíte PA0 jako výstup s log.1 (tedy Ucc). Pro každý pin tedy existují čtyři varianty. Vstup, vstup s pull-up rezistorem, výstup s log.0 a výstup s log.1. Předveďme si to na několika příkladech.
DDRA = 0b00000010; PORTA = 0b100000011;
DDRA = (1<<DDA1); PORTA = (1<<PORTA7) | (1<<PORTA1) | (1<<PORTA0);
DDRA = (1<<1); PORTA = (1<<7) | (1<<1) | (1<<0);
PORTA = PORTA | (1<<PORTA7) | (1<<PORTA5);
PORTA |= (1<<PORTA7) | (1<<PORTA5);
PORTA |= (1<<7) | (1<<5);
PORTA |= 0b10100000;
Opačná situace nastane když chcete vynulovat některý bit v registru. K tomu slouží konstrukce
PORTA = PORTA & ~((1<<PORTA7) | (1<<PORTA5));
PORTA &=~((1<<PORTA7) | (1<<PORTA5));
PORTA &= 0b01011111;
PORTB ^= (1<<PORTB7);
x = PINC;
PINB = (1<<PIN7) | (1<<PIN5);
PINB = 0b10100000;
PORTA |= (1<<PORTA7) | (1<<PORTA5);
if(PINA & (1<<PINA4)){ // udělej něco když je na PA4 log.1 }
if(!(PINA & (1<<PINA4))){ // udělej něco když je na PA4 log.0 }
PORTD &=~(1<<PORTD1); // pokud je PD1 výstup, je na něm držena log.0 DDRD |= (1<<DDD1); // zápis log.0 na "otevřený kolektor" DDRD &=~(1<<DDD1); // zápis log.1 na "otevřený kolektor"
Většina pinů může mít ještě alternativní funkci. Alternativní funkce pinů jsou uvedeny na začátku datasheetu v sekci "Pin Configurations". Tedy na obrázku, kde je rozpis vývodů Atmelu a jsou to ty názvy, které jsou v závorkách. Pokud je alternativních funkcí více, jsou odděleny lomítkem. Pro přehlednost uvedu stručný výčet těchto alternativních funkcí pro Atmega16.
Pokud jste vraždící bestie a každý týden obohatíte křemíkové nebe o pár jednočipů, nejspíš patříte k arduino komunitě a možná by jste měli uvažovat o nějáké odolnější variantě.
Potřebuji spínat vyšší napětí než je napájecí napětí čipu ? Můžete využít například jednu ze těchto čtyř variant (schema č.1). Varianta A) a B) je nejjednodušší. Externí pullup rezistor zařídí, že je na výstupu obvodu vysoké napětí. Sepnutím tranzistoru se pak výstup uzemní. Rozepnutím tranzistoru se skrze pull-up rezistor (R1 nebo R3) přivádí na výstup napětí. Výstup ale nesmí odebírat přiliš velký proud, protože zdroj napětí je "měkký" a jeho výstupní napětí s odebíraným proudem klesá. Měkkost zdroje určuje pullup rezistor (R1 respektive R3). Čím je jeho hodnota menší tím je výstup tvrdší a tím větší proud lze dodávat aniž by došlo k singifikatnímu poklesu napětí na výstupu. Problém je v tom, že se zmenšováním odporu roste odběr ve stavu kdy je tranzistor sepnut (tedy kdy je výstup v log.0). Je tedy potřeba najít kompromis. Používat ho tedy typicky můžete jen tam kde z výstupu není potřeba odebírat proud. Varianta s bipolárním tranzistorem vyžaduje ještě omezení proudu do báze rezistorem R2. Varianta B) s unipolárním tranzistorem je v tomto jenodušší a navíc její výstupní napětí dosáhne nižších hodnot jak předchozí varianta. Bohužel ale varianta B) s FET nemusí být funkční při nízkém pracovním napětí (Atmega16A může pracovat už od 2.7V a jiné varianty už od 1.8V). Tak nízké napětí pak nemusí stačit na sepnutí tranzistoru a v takovém případě se musíte vrátit k variantě A). Pokud potřebujete aby byl napěťově posílený výstup schopen dodávat signifikantní proud budete si muset postavit celou push-pull konfiguraci jako je na schématech C) a D). Tyto varianty typicky využijete při ovládání vyšších napětí jako je 12V, 24V a podobně. Pokud potřebujete transformovat výstup z 3V na 5V určitě se poohlédněte po nějákém integrovaném obvodu, zjednoduší vám to práci (já s oblibou používám 74LVC4245). Přirozeně vám ale nikdo nebrání některou z těchto variant použít. Počítejte ale s citelným omezením na maximální komunikační frekvenci. Při volbě R1 (respektive R3) okolo 1k Ohm budou dlouho trvající vzestupné hrany signálu omezovat frekvenci na řádově stovky kHz. Při volbě okolo 50kOhm se komunikační rychlost redukuje do oblasti desítek kHz.
Pokud je stačí spínat v dolní větvi je situace jednodušší (viz schéma č.2) a můžete použít variantu A) nebo B). U varianty A) si ale pohlídejte aby proud do báze dostačoval požadovanému proudu kolektorem a příslušně tomu zmenšete rezistor R2. Pokud mají být proudy opravdu veliké, můžete ještě použít "darlington" tranzistor (E). Potom ale pozor na výkonové zatížení a s ním spojený ohřev. Pokud takových výstupů potřebujete víc, můžete využít tranzistorových polí ULN2003 (nebo podobných). Tuto variantu uvítáte třeba při buzení multiplexovaného sedmisegmentového displeje. Varianta B) žádnými úbytky sice netrpí, ale zase je potřeba dávat pozor na provoz při malém napětí Atmelu, kdy by nemuselo dojít ke kompletnímu otevření tranzistoru. Ke spínání v horní větvi slouží varianty C) a D). Opět je na místě pouvažovat zda nepoužít integrovaný obvod - tzv. High side switch, který typicky obsahuje proudový limit, ochranu proti přehřátí a odpojení vstupu. Žádná varianta na schématu č.2 neobsahuje proudový limit, takže zkrat na výstupu spolu s dostatečně tvrdým zdrojem může bez problémů způsobit zničení výstupního tranzistoru.
Skoro jistě budete chtít vybavit svou aplikaci různými spínači nebo tlačítky, které budou typicky sloužit pro ovládání lidskou obsluhou. Nejjednodušší způsob jak je můžete připojit k mikrokontroléru, spočívá v jejich připojení takzvaně "proti zemi". Jeden konec tlačítka připojíte na zem a druhý konec na libovolný vývod Atmelu. Poté uvnitř atmelu zapnete interní pull-up rezistor a vše je připraveno. Jestliže tlačítko není stisknuto, dostane se na vstup skrze interní pull-up rezistor napětí Ucc a na vstupu je log.1. Jestliže obsluha tlačítko stiskne, spojí se vybraný vývod Atmelu se zemí a bude na něm log.0. Přijít na to, že k této události došlo je už úkol pro program. Vzhledem k tomu, že stisk tlačítka lidskou rukou je vždy relativně pomalý, má program spoustu času zjistit, že k němu došlo. Člověk je typicky schopný stisknout tlačítko nanejvýš 5 krát za sekundu, program má tedy řádově desítky až stovky milisekund času. Díky tomu bývá program jednoduchý a stačí stisk kontrolovat například jen 10x za sekundu. Když si ale zkusíte takto jednoduše tlačítko k Atmelu připojit, budete nemile překvapeni výsledkem. Mechanický kontakt bude vytvářet zákmity. Jakmile prst začne působit na hmatník tlačítka, začnou se kontakty uvnitř tlačítka přibližovat. Během přibližování se ale jemně chvějí, a jakmile se přiblíží na velmi malou vzdálenost tak se vlivem chvění mohou několikrát po sobě spojit a zase rozpojit. Případně se nespojit dokonale, tak že vznikne mezi kontakty tlačítka přechodový odpor. Tyto jevy vyústí ve velmi nepěkný průběh napětí, který můžete vidět na obrázku x1 a kterému se česky říká zákmity (anglicky bouncing).
Jestliže by program počítal stisky tlačítka dostatečně rychle (a programy umí počítat velmi rychle), dokázal by mezi stisky tlačítek započítat i zákmity. To je přirozeně nežádoucí, protože program by pak jeden stisk tlačítky vyhodnotil jako několik stisků. Tyto problémy je možné ošetřit mnoha cestami. Softwarové ošetření zákmitů spočívá v tom že necháte program záměrně sledovat tlačítka jen jednou za nějáký relativně dlouhý čas, dejme tomu 20ms. Softwarové ošetření se vám ale nemusí zamlouvat (třeba z důvodů lenosti :D ) a tak si můžete pomoci hardwarovým řešením. Stačí tlačítko přemostit kondenzátorem o vhodné hodnotě. Během krátkých zákmitů se přes pull-up rezistor nestihne filtrační kondenzátor nabít a zákmity se tak odfiltrují. Protože interní pull-up rezistor má hodnotu přibližně 35kOhm, zvolil jsem pro ukázku kondenzátor o kapacitě 100nF. Doba nabíjení kondenzátoru by v takovém případě měla být řádově 4ms. Dá se tedy odhadovat, že během zákmitů dlouhých řádově desítky us se kondenzátor stihne nabít na napětí řádově desítek mV. Taková napětí bude mikrokontrolér spolehlivě považovat za log.0 a zákmity tak budou ošetřeny. Tomuto procesu se anglicky říká debouncing. Na obrázku x2 je pak vidět výsledek takového ošetření. Záměrně jsem stiskl tlačítko dvakrát po sobě co nejrychleji aby bylo vidět, že se během nich dokáže kondenzátor spolehlivě nabít a žádný ze stisků tím neutrpí.
Aby byl princip činnosti obvodu co nejzřetelnější, záměrně jsem mačkal tlačítko "špatně". Výsledek těchto pokusů vidíte na obrázku x3. Velké množství velmi nepěkných zákmitů nepřerostlo hodnotu přibližně 650mV. Z datasheetu Atmelu vyčtete že maximální úroveň napětí, při které čip vstup zaručeně vyhodnotí jako log.0 je 0.2Vcc, při napájení 3.3V je to tedy 660mV. Naše zákmity jsou v tomto případě nebezpěčně blízko. Bylo by tedy vhodnější zvolit kapacitu filtračního kondenzátoru raději vyšší, například 1uF.
Protože kapitola byla hodně teoretický jste určitě hladoví po nějákém příkladu. Tak alespoň jeden jsem pro vás připravil. Jedná se o jednoduchý program, detekujicí stisk tlačítka. Na výstup PA0 vyvedeme LED diodu, která se bude přepínat s každým stiskem. Rutinu detekujicí pouze jeden stisk tlačítka budete jistě potřebovat, takže bude žádoucí trochu si ji okomentovat. Tlačítko připojíme na pin PA2, který nakonfigurujeme jako vstup s interním pull-up rezistorem. Pak necháme program skenovat stav 3 bitu v registru PINA, který obsahuje stav na vstupu PA2. Pomocí proměnné stav_tlacitka pak budeme detekovat sestupnou hranu na PA2. Jakmile k ní dojde, přepneme LED a vynulováním proměnné stav_tlacitka zamčeme další vstup do těla podmínky. Teprve až dojde k uvolnění tlačítka tak zápisem jedničky do stav_tlacitka podmínku opět odemčeme. Všimněte si použití maker. Při ovládání různých logických vstupů a výstupu je velmi vhodné využívat makra (A dobře je pojmenovávat). Jednak kvůli tomu, že je pak kód čitelnější a srozumitelnější. A hlavně proto, že je pak snazší měnit pozice pinů. Pokud bych chtěl LED přesunout na vývod PA5, stačilo by mi změnit makro LED_PREPNI a přirozeně i inicializaci portu A. Pokud bych makro nepoužil, musel bych ručně projít celý zdrojový kód a všechny příkazy ovládání LED ručně přepsat. Je skoro jisté, že byste během této úpravy některé řádky přehlédli. Ještě mnohem lepší by bylo pomocí maker vyřešit i inicializaci portů, ale nebudeme příklad komplikovat. Zkuste si na příkladu mimo jiné i co se stane když ošetření zákmitů neprovedet.
// A) Vstup tlačítka (dektece stisku) #include <avr/io.h> #define TLACITKO (PINA & (1<<PINA2)) #define LED_PREPNI PORTA ^= (1<<PORTA1) char stav_tlacitka=1; int main(void){ DDRA = (1<<DDA1); // PA0 výstup, PA2-PA7 vstupy PORTA = (1<<PORTA2); // na PA2 zapínáme pull-up while(1){ if((!TLACITKO) && stav_tlacitka){ LED_PREPNI; // prepni stav LED1 (na PA1) stav_tlacitka = 0; // "zamkni" tlacitko az do uvolneni (dektece sestupne hrany) } if(TLACITKO){ stav_tlacitka = 1; // "odemci" tlacitko (evidentne bylo uvolneno) } } }
Kdysi jsem narazil na vtipná videa poukazující na klasické chyby při zacházení se vstupy / výstupy mikrokontroléru. Jestli si s jejich ovládáním moc nevěříte, mrkněte na ně zde.