Ovládání portů u AVR

Dodat oscilogram spínání posílených výstupů. pořešit komunikaci otevřenými kolektory

Abstrakt

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.

úvod

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.

pojmenování

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é.

Limitní elektrické vlastnosti

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ří :)

elektrické parametry vstupů

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).

elektrické parametry výstupů

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.

Pull-up rezistor

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.

Jak porty ovládat

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;

Tento zápis nastaví PA1 jako výstup a nastaví na něj log.1, piny PA0 a PA7 nastaví jako vstupy se zaptnutým pull-up rezistorem. A to je prostě vše :) Přirozeně ale v céčku existuje mnoho cest jak následující příklad zapsat. Přímý zápis v binární podobě potkáte zřídkakdy, je totiž mizerně čitelný. Musíte odpočítávat jedničky a nuly aby jste zjistili jaká je konfigurace pinů. Můžete použít čitelnější formu:
DDRA = (1<<DDA1);
PORTA = (1<<PORTA7) | (1<<PORTA1) | (1<<PORTA0);

která je z hlediska jazyka úplně stejná jako zápis:
DDRA = (1<<1);
PORTA = (1<<7) | (1<<1) | (1<<0);
Všechny tři varianty projdou překladem se stejným výsledkem. Je jen na vás jakou formu zvolíte. Doporučuji ale tu nejčitelnější (tedy tu s názvy bitů). Pomůže vám to vyznat se ve vlastním kódu. Krom toho když jakoukoli jinou variantu pošlete někam na forum aby vám poradili co máte špatně, tak vás odpálkují. Nikdo totiž nemá náladu listovat datasheetem a odpočítávat jedničky a nuly ;) Všechny tyto tři varianty skýtají jisté úskalí. Zapisují do registru přímo danou hodnotu (takže prostě přepíšou VŠECHNY jeho bity). Velice často ale budete potřebovat v registru změnit jen jeden nebo dva bity a všechny ostatní nechat v původním stavu. K tomu vám v C poslouží následující konstrukce:
PORTA = PORTA | (1<<PORTA7) | (1<<PORTA5);
což je ekvivalenní jako
PORTA |= (1<<PORTA7) | (1<<PORTA5);
což je ekvivalentní jako
PORTA |= (1<<7) | (1<<5);
což je ekvivalentní jako
PORTA |= 0b10100000;
udělá prostou věc, nastaví 7. a 5. bit registru PORTA do log.1 a ostatní bity ponechá nezměněné. Druhý zápis je nejčastější a asi i nejčitelnější - používejte ho.

Opačná situace nastane když chcete vynulovat některý bit v registru. K tomu slouží konstrukce

PORTA = PORTA & ~((1<<PORTA7) | (1<<PORTA5));
a nebo zkrácená forma:
PORTA &=~((1<<PORTA7) | (1<<PORTA5));
a nebo
PORTA &= 0b01011111;
Všechny tři udělají to samé, vynulují 5. a 7. bit v registru PORTA. Druhá varianta je také nejčastější, zvykněte si na ni. Pokud v tomto okamžiku nerozumíte zápisům, zagooglete a zrekapitulujte si bitové operátory v C. Za makry PORTA7 se skrývá jen pořadové číslo bitu, v tomto případě číslo 7. Může se vám zdát že v případě portů je košaté rozepisování zbytečně pracné, že přece stačí namísto (1<<PORTA7) zapsat prostě (1<<7) a každý to pochopí. A budete mít asi pravdu, ale jiné registry nemají tak jednoduché pojmenování bitů jako PORTy a tam by zápis (1<<7) byl nečitelný. A proto vám doporučuji dodržovat štábní kulturu a psáto takhle. Konec konců ošoupaná klávesnice je dobrou známkou pogramátora :D

Občas se vám může stát že budete potřebovat přepnout hodnotu bitu v registru. Typicky právě u portů. K tomu můžete využít tento příkaz:
PORTB ^= (1<<PORTB7); 
který přepne stav pinu PB7. Novější atmely umožňují ještě jeden způsob jak to provést. K němu ale potřebujete znát poslední registr sloužící k ovládání portů. A tím je PINx (PINA, PINB...). Ten slouží ke čtení vstupního stavu. Ať už máte nastaven vývod jako vstup nebo výstup, můžete si přečíst jaká je na něm logická hodnota. Číst budete vždy celý registr PINx (i když vás třeba celý nezajímá). Každý jeho bit reflektuje stav na příslušném pinu. Jedinou vyjímkou kdy si z PIN registru nemůžete nic smysluplného přečíst je pokud máte vypnutý vstupní buffer. Příkazem:
x = PINC; 
si prostě přečtete stav celé osmice vývodů na portu C. Jak s informací naložíte je pak na vás. My si to za chvíli ukážeme. U novějších členů rodiny AVR dostal regisr PINx ještě jednu roli. slouží k přepínání výstupů. Zápisem log.1 do některého z bitů registru PINx přepnete stav příslušných bitů registru PORTx. Jinak řečeno jestliže chci přepnout hodnoty 5. a 7. bitu v registru PORTB (a tím typicky přepnout i stav vývodů) stačí mi zapsat:
PINB = (1<<PIN7) | (1<<PIN5); 
a nebo též
PINB = 0b10100000;
(všimněte si že do nej pouze zapisuji, ne jako v případě "nastavení bitu" o pár řádků výše). Bity 5 a 7 v registru PORTB pak změní svoji hodnotu.

Pull-up rezistory se dají globálně vypnout nastavením bitu PUD v registru SFIOR. Což asi najde primární uplatnění při přechodech do režimu spánku. Na co uspávat čip aby odebíral jednotky mikroampér, když skrze pullup rezistory tečou desítky že ;)

U všech výše zmíněných operací je potřeba brát zřetel na to, že jde o sekvence čtení - modifikace - zápis. Například příkaz
PORTA |= (1<<PORTA7) | (1<<PORTA5);
se ve skutečnosti realizuje instrukcí čtení (přečte se stav PORTA). Jeho hodnota se pak upraví (logickým součtem) a výsledek se opět zapíše do registru PORTA. A to je obecně rizikové. Ve většině případů vám to nemusí vadit, ale existují situace kdy může vzniknout průšvih. Představte si že program provádí výše zmíněnou operaci. Nejprve přečte stav registru PORTA (aby ho nadále mohl upravoat) a v tom okamžiku přijde přerušení. V rutině přerušení se změní hodnota registru PORTA (přerušení třeba manipuluje s nějakým jiným pinem). Když přerušení skončí, program pokračuje ve rozdělané činnosti. Upravuje hodnotu kterou načetl z PORTA. Jenže ta je špatě ! Stav regisru PORTA se mezi tím změnil. Program tedy dokončí úpravu a upravený starý stav registru PORTA zase zpět zapíše. A změna kterou v něm udělala rutina přerušení je zrušená... Průšvih ! Pokud vám něco takového hrozí, budete muset při podobné operaci blokovat přerušení. K tomu slouží funkce cli() a sei(). Takové operaci, která se skládá z několika kroků se říká že není atomická - tedy že ji jde přerušit. A je potřeba s tím počítat. To jen aby jste se nedivili. A teď k praktickým ukázkám.
Chcete-li zjistit stav bitu v registru můžete pužít konstrukci
if(PINA & (1<<PINA4)){ 
// udělej něco když je na PA4 log.1 
} 
nebo opačnou:
if(!(PINA & (1<<PINA4))){ 
// udělej něco když je na PA4 log.0         
}  
Komunikaci s otevřeným kolektorem (za předpokladu že je připojen vnější pull-up rezistor) třeba na pinu PD1 můžete realizovat následovně:
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éto konfiguraci můžete spojovat výstupy čipů (což jinak nesmíte !). Jen si dávejte pozor na parazitní kapacity. Této konfigurace navíc využívá třeba i I2C periferie.

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.

Použití alternativní funkce většinou zapínáte v příslušné periferii. Jestliže tedy třeba pin PD0 má sloužit jako RX jednotky USART (a vy jí ovládání tohoto pinu povolíte), nebude záležet jak jste jej nakonfigurovali v registrech DDRD a PORTD. Některé piny ale budou vyžadovat jisté nastavení, například OC0A na PB3, i když čítači předáte řízení pinu PB3, musíte jej mít nastavený jako výstup. Některé alternativní funkce se dokonce na vašem nastavení vůbec neprojeví například INT0 na PD2 může fungovat a vy nadále můžete používat příslušný pin. Někdy dokonce vaše nastavení pinu ovlivní funkci jiné periferie. Například nastavením pinu XCK (PB0) jako výstup může přepnout jednotku USART do synchronního režimu. Dále je třeba dát pozor na to že některé piny mají alternativní funkci nastavenou pomocí Fuses pevně. Třeba u ATmega16 (32 a dalších) piny PC2,PC3,PC4 a PC5 sloužící k JTAG programován a debugování čipu. Takže bez změny fuses je vůbec nemůžete používat - mají pevně zapnutou alternativní funkci (pokud JTAG nepoužíváte, doporučuji ho ve fuses vypnout a piny si zpřístupnit). Podobně je na tom PD6 u Atmega8 který je pomocí fuses implicitně překonfigurován jako reset (takže ho také nemůžete ovládat). Ale ten prosím nezkoušejte změnit ! Vypnuli by jste si SPI rozhraní :D Podrobnější informace o tom kdy a za jakých okolností jsou jednotlivé piny přidělovaný periferiím se dočtete v datasheetu.

Co nikdy nedělat

Vzhledem k rychle se rozrustajícímu křemíkovému nebi je asi vhodné probrat pár zakázaných postupů.
1. Nikdy nespojujte dva výstupy (jedině pokud pracují s otevřeným kolektorem) Pokud totiž jeden z nich bude v log.0 a jeden v log.1 dojde defakto ke zkratu a skoro jistě překročíte limit proudu pro vývod.

2. Nikdy nepřipojujte výstup na jakékoli pevné napětí opět hrozí překročení limitu proudu pro vývod.

3. Nikdy nepřipojujte na výstup LED bez ochranného odporu ... ale tohle snad patří do školky :)

4. Nikdy nepřipojujte na vstupy vyšší napětí než je dovoleno. Nezapomínejte na to hlavně pokud pracujete s nižším napětím. Stačí si nevšimnout že na Atmel napájený 3V cpete výstup z 5V logiky.

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ě.

Co dělat když ?

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.


schema č.1 - posílení výstupů Atmelu

Potřebuji spínat větší proudy ?

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.


schema č.2 - posílení výstupů Atmelu

Malý problém s tlačítky (zákmity)

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).


obrázek x1 - několik průběhů neošetřených zákmitů mechanického tlačítka. Vlevo je tlačítko plně stisknuté, v pravo je plně uvolněné.

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í.


obrázek x2 - Ošetření zákmitů tlačítka. Pull-up rezistor má hodnotu 35kOhm (interní) a filtrační kondenzátor kapacitu 100nF. Doba vzestupné hrany je přibližně 8.5ms. V horním části obrázku je vidět celý průběh dvou krátkých stisknutí tlačítka.

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.


obrázek x3 - Příklad nedostatečného ošetření zákmitů - aneb co leze z tlačítka když ho mačká vaše kočka.

Příklad - Vstup tlačítka (dektece stisku)

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)
  }
 }
}

Trochu zábavného vzdělávání

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.

Odkazy k tématu