USART - AVR

Abstrakt

jednotka USART zpřístupňuje AVR celou třídu nejrozšířenějších komunikačních protokolů (R232, RS485...). Zvu vás na ochutnávku jeho možností. Podíváme se na základy, zprovozníme několik variant komunikací PC - mikrokontrolér a ukážeme si jak mohou komunikovat mikrokontroléry mezi sebou. Vše přirozeně na funkčních příkladech.

Motivace

USART můžete použít pro komunikaci s širokou paletou průmyslových zařízení mezi něž patří například drivery motorů nebo laboratorní zdroje. Jste schopni komunikovat protokoly RS232, RS422 nebo RS485. Dále vám umožní velice snadnou komunikaci s počítačem pomocí převodníků USB-UART. Díky tomu budete moct dávat Atmelu pokyny, případně z něj stahovat nebo uplodovat libovolná data. Arduino dokonce využívá bootloaderu, který pomocí USARTu nahrává program do čipu. V neposlední řadě vám poslouží ke komunikaci s RFID čtečkami, s GSM moduly a obecně se širokou paletou modulů pro bezdrátový přenos (včetně bluetooth). Často se USART používá pro tunelovou komunikaci skrze náročnější rozhraní (jako třeba ethernet). Někdy dokonce narazíte i na kamerky s UART rozhraním. A mimo to všechno můžete USART použít i pro komunikaci mezi čipy. Už z tohoto přehledu je jasné, že použití USARTu patří k základním schopnostem programátora embedded systémů.

Úvod

Celý miniseriál tvoří vlastně komentované příklady. U každého z nich si ukážeme ovládání příslušné části modulu USART. Věřím, že se díky tomuto stylu "neztratíte" v relativně velkém množství informací. Začátečníkům doporučuji postupovat příklad po příkladu od začátku a nejlépe si většinu z nich vyzkoušet a nějak rozvinout. V každém dalším příkladu budu postupně přidávat další funkcionalitu. Zkušenějším radím aby rovnou přeskočili ke kapitole, která je zajímá a případně přejali příklad jako šablonu a pracovali s ní dle potřeb. Nejprve ve stručnosti popíšu teoretické základy. Popis nebude vyčerpávající, takže kdo bude chtít něčemu rozumět do detailu bude muset otevřít datasheet. Pár poznámek věnujeme tomu jak připravit spojení Atmel - PC a dále už se budeme věnovat jen vzorovým příkladům.

Seznam příkladů

Stručná teorie

Než se dostaneme k výbavě Atmelu, měli bychom si v rychlosti projít komunikační protokol. Pokud jste úplní začátečníci doporučuji googlit UART nebo RS232. Tento protokol budeme s drobnými odlišnostmi používat. Ke komunikaci slouží dvě linky - Rx a Tx. RS232 obsahuje ještě další linky RTS,CTS,DTR, které slouží k řízení toku dat. Ty nás nebudou zajímat. Na každém čipu je vývod označený Rx - to je vstup. Vývod označený Tx je výstup. Každý čip má tedy samostatný přijímač a samostatný vysílač. Propojení dvou čipů vždy vypadá tak, že Tx jednoho čipu připojíte do Rx druhého čipu. Když spojíte dva Tx je to špatně (dva výstupy proti sobě), když dva Rx tak je to také špatně - dva vstupy pro ti sobě. Komunikace může probíhat plně duplexně. To znamená že oba spojené čipy mohou kdykoli začít vysílat zprávu jeden druhému (i zároveň). Oproti RS232 budeme používat logické úrovně 0 a 5V (respektive 0 a Ucc pokud budete provozovat čip na jiném napětí). Datová linka je v neutrálním stavu držena výstupem (Tx) v log.1. Pokud je vše v pořádku a nevysíláte, měli by jste na Tx pinu naměřit napětí Ucc. Velice zjednodušeně vypadá komunikace v asynchronním režimu takto: Přijímač stále sleduje linku (svůj RX pin). Jakmile se na ní objeví log.0 (tzv. start bit), spustí "stopky" a v pravidelných okamžicích (třeba každých 104us) si přečte logickou úroveň na lince a uloží si její hodnotu. Jakmile zaznamená 8 hodnot, zastaví stopky a příjem zprávy je hotov. Vysílač i přijímač musí oba předem vědět v jakých okamžicích stav linky číst, jak bude zpráva dlouhá (nemusí to být vždy 8 bitů) a podobně (viz protokol RS232). Časování se neudává v sekundách ale v baudech za sekundu (tzv baudrate), což není žádná složitá jednotka a můžete se na ni dívat jako na bity za sekundu. Nejlépe to pochopíte z ukázek různých zpráv. Několik zpráv v tom nejobyčejnějším formátu můžete vidět na následujícím obrázku.


Obrázek x0 - Ukázky několika zpráv. Konfigurace 9600Bd/s, 1 stop bit, žádná parita. start bit je označený "S" a stop bit "T". Všimněte si že Start bit je vždy v nule, Stop bit vždy v jedničce. Také si všimněte že ve zprávě se LSB odesílá jako první (data tedy musíte číst "zprava").

Na obrázku x0 vidíte příklad několika zpráv. V první zprávě (bílá) odesíláme 0x00, tedy samé nuly. V další zprávě pak postupně 1,2,3 a 4. Díky tomu můžete pěkně vidět že řazení zprávy je "odzadu". Jako první se tedy vysílá LSB - nejméně důležitý bit zprávy. V poslední zprávě (azurová) je 0xFF tedy samé jedničky. Ve všech zprávách je krásně vidět Start bit (označený "S"), který slouží k synchronizaci. Přijímač na sestupné hraně na začátku Start bitu provede synchronizaci (tedy pozná kde zpráva začíná). Stop bit (označený "T") slouží k tomu aby každá zpráva měla jasný konec a aby se linka vrátila do jedničky. Z jedničky totiž musí začínat start bit další zprávy - to je patrné na dalšm obrázku. Na obrázku x1 je několik zpráv s různou konfigurací. První dvě zprávy nesou hodnotu 0x05 (tedy 0b101), zbytek zpráva pak 0x09 (0b1001). Bílý průběh je 5bit zpráva, 1 stop bit a bez parity. Zelený průběh 6bit zpráva,1 stop bit, bez parity. Modrý průběh je asi nejklasičtější uspořádání, 8 datových bitů, jeden stop bit a žádná parita. Na červeném průběhu je konfigurace 8 datových bitů,1 stop bit a paritní bit (označený "P"). Parita je sudá, takže v tomto případ musí být paritní bit nulový. Oranžový průběh jsou dvě zprávy za sebou ve stejné konfiguraci jako modrý průběh, tedy 8 datových bitů, jeden stop bit a žádná parita. Je na něm pěkně vidět k čemu slouží stop bit. Kdyby ve zprávě stop bit nebyl, navazoval by Start bit nové zprávy rovnou za poslední datový bit předešlé zprávy (který je momentálně 0). Přijímač by tak neměl sestupnou hranu na kterou se může synchronizovat a nebyl by schopen rozpoznat začátek nové zprávy. Na posledním průběhu je jen pro zajímavost konfigurace 8 datových bitů, dva stop bity a žádná parita. Opět jsou to dvě zprávy těsně za sebou, protože jinak by jste dvojici stop bitů nemohli přímo vidět :)


Obrázek x1 - Ukázky různé konfigurace zpráv (9600Bd/s).
bílá - 5bit, Žádná parita, 1 stop bit, data 0x05
zelená - 6bit, Žádná parita, 1 stop bit, data 0x05
modrá - 8bit, Žádná parita, 1 stop bit, data 0x09
červená - 8bit, Sudá parita, 1 stop bit, data 0x09 (dvě zprávy)
azurová - 8bit, Žádná parita, 2 stop bit, data 0x09 (dvě zprávy)



Obrázek x2 - při datové rychlosti 9600Bd/s by měl jeden bit odpovídat 1/9600 = 104us. Měříme délku start bitu a vychází přibližně 112us. Neměříme bohužel nejpřesněji (při přesnějším měření by ale nebyla vidět celá zpráva)

Na čem se bude pracovat

Va většině vašich aplikací budete chtít UART použít pro komunikaci mezi Atmelem a PC. Předpokládám, že dávno nemáte na PC seriový port (COM Port). Pokud náhodou máte a chcete ho použít stačí vám změnit logické úrovně z atmelu na logické úrovně RS232. K tomuto účelu se vyrábí integrované obvody a nebo celé modulky (oboje za pár korun). Googlete RS232-TTL. Velmi často založené na čipu MAX232 nebo podobných. Z číny k tomuto účelu dobře poslouží například toto. Jestliže, ale na PC nemáte seriový port (a to je typičtější) můžete využít virtuální seriový port (VCP). Existují integrované obvody, které s vnějším světem komunikují pomocí UART (RS232 s 5V log. úrovněmi) a s PC komunikují prostřednictvím USB. V PC se pak zase chovají jako seriový port (proto virutální). Jediné co potřebujete je vybavit PC příslušným ovladačem pro daný integrovaný obvod. Mezi nejrozšířenější patří produkty firmy FTDI (FT232R a podobné), ale k sehnání jsou i modulky jiných výrobců. Osobně mám otestované CP2102, PL2303 a CH340G. Při nákupu FTDI modulů je obezřetnost na místě. FTDI své čipy chrání. Koupíte-li neoriginální FTDI čip (nebo modulek osazený neoriginálním čipem), máte velkou šanci, že prostě nebude s aktuálními ovladači na PC pracovat. Nebudu zabíhat do detailů, ale tak to prostě je. Takže pokud chcete sáhnout po FTDI budete si muset nejspíš připlatit a koupit si modulek z renomovanějších míst než z číny. Krom toho je internet plný vykuků, kteří vám klidně budou tvrdit, že modul osazený čipem PL2303 obsahuje FT232RL a podobné hlouposti. Což vám přirozeně ztrpčí život, protože si nainstalujete špatné ovladače a nepoběží vám to. Krom toho vás slušně natáhnou, protože PL2303 stojí v číně okolo 10-20 korun a zdejší prodejce vám ho nacení klidně na 200 ;) Tak se nenechte napálit. Obecně toto téma je zvlášť bohaté u "arduino" výrobků. Spousta výrobců těží z toho, že se začátečnicí neorientují v problematice. Opatří prodávaný předmět popiskem "pro arduino" a přirazí si k ceně pár stovek procent. Většina modulků bude pracovat s 5V. I když čipy umí v drtivé většině případů pracovat i s nižším napětím, hardwarová konfigurace modulku to nebude umožňovat. Pokud tedy budete chtít nižší provozní napětí (třeba kvůli čidlům) poohlížejte se po modulku který umí třeba 3.3V. A opět pozor na ebay (čímž vás nechci odradit od nákupu). Prodejci na e-bayi (a nejen tam) píší do záhlaví různé nesmysly. Chtějí totiž aby se jejich produkt objevoval ve výsledcích vyhledávání co nejčastěji. Takže když bude v hlavičce produktu napsáno 3.3V nejspíš to znamená, že má modul navíc výstup z 3.3V regulátoru. Pokud má opravdu podporovat 3.3V komunikaci, hledejte na něm nějaký jumper pro přepnutí 5V nebo 3.3V jako třeba na tomto nebo na tomto.

Programové vybavení PC

Prvně budete na PC pro svůj modulek potřebovat ovladač. V linuxu se mi všechny tři předešlé modulky chytili hned bez dodávání jakéhokoli dalšího ovladače. Na windows jsem je musel doinstalovat. A i kdyby ne je to trivální a dobře zdokumentovaná záležitost. Pro PL2303 například Zde a zde. Pro CP2102 tady a nebo tady nebo tady. A pro ftdi tady. Dál se vám ke komunikaci bude hodit nějaký terminálový program. Z linuxu je možné trochu krkolomným způsobem posílat na port data přímo z terminálu, ale kvůli přehlednosti si stejně seženete nějaký terminálový program. Windowsovský Hyperterminal kvůli nepřehlednosti moc nedoporučuji. Zaměřte se raději na Realterm, Termite, Coolterm nebo třeba Putty. Putty není sice nejpřehlednější, ale jako jedinou jsme ji přesvědčili aby běžela na pořádných datových rychlostech (12Mbaud). Já osobně preferuji Realterm. Některé terminálové programy umějí krom ručního posílání zpráv také posílat a přijímat celé soubory. Takže pak můžete do atmelu nahrávat různě objemná data, případně z něj stahovat s celkem rozumným komfortem výsledky měření a podobně aniž by jste museli umět programovat PC. Chcete-li podrobnější návod přečtěte si na tomto webu k tomu určený článek.

Pro jaké čipy je tento návod ?

Návod a všechny příklady by měly být bez úprav přenositelné mezi Atmega8, Atmega16, Atmega32 a Atmega64 (včetně verzí s příponou "A"). S drobnou úpravou bitu URSEL by měly být přenositelné na Attiny2313. S přejmenováním registrů pak na Atmega48, 88, 168, 328 a spoustě dalších. Testoval jsem je na Atmega8A a Atmega16A.

Trocha teorie - baudrate

Jednotka USART může ovládat tři piny Rx, Tx a XCK (PD0,PD1 a PB0 na Atmega16) a může pracovat v synchronním a asynchronním režimu. V synchronním režimu může pracovat jako master (vypouští clock na pin XCK) a nebo slave (přijímá clock z pinu XCK). Pokud nastavíte XCK pin jako vstup (v registru DDR) bude pracovat jako slave, pokud nastavíte pin XCK jako výstup, bude pracovat jako master. Sycnhronní režim se zapíná nastavením bitu UMSEL v registru UCSRC. My se ale zatím synchronním režimem zabývat nebudeme - naše modulky ho neumí. Modul také slouží k nastavení datové rychlosti (Baudrate), což je ústřední veličina. Omezíme se ale pouze na povrchní informace jak baudrate nastavit. K tomu slouží registr UBRR, který se skládá z horní (UBRRH) a dolní (UBRRL) části. Do nich můžete zapsat maximálně 12bitovou hodnotu (0-4095). Ta slouží jako "dělička" clocku procesoru a generuje časovací signál. Koho zajímají detaily doporučuji si celou kapitolu USART v datashetu pročíst. Klíčová pro vás bude tabulka č.1 kde máte potřebné rovnice pro správné nastavení baudrate. V asynchronním režimu je možné pomocí bitu U2X v registru UCSRA nastavit tzv. "Double speed". V tabulce máte proto uvedeny vztahy i pro situaci kdy je "double speed" režim zapnutý. Aby vám výrobce ulehčil práci, shrnul nastavení UBRR do tabulek v sekci 19.12 (v datasheetu). Ve většině případů vám bude stačit mrknout do tabulky a opsat číslo :)

Velice pěknou utilitku k těmto účelům najdete zde. V případě potřeby si nahoře můžete zaklikat různé varianty zobrazení tabulek. Můžete vyplnit frekvence na jakých váš čip pracuje a datové rychlosti pro něž chcete vypočítat hodnoty UBRR. Protože dělení clocku probíhá celočíselně jsou některé možnosti nedosažitelné (z šesti dělením celým číslem pět prostě neuděláte). Je proto potřeba sledovat jaká je odchylka reálného baudrate od požadovaného. To je zde znázorněno barevně a je dobré chybu udržovat nízkou dejme tomu pod 1%. Co je a co není kritická chyba rozebírá datasheet v sekci 19.8.3 Asynchronous Operational Range. Vzhledem k tomu, že budete provozovat Atmel typicky s interním RC oscilátorem (který sám o sobě zavádí další chybu) je velmi vhodné volit ty varianty kde je odchylka Baudrate pod 0.2%. Když budete dobře vědět co děláte, můžete si pak dovolit i větší chybu. Pro začátek je to ale nevhodné, protože vám jen přibude prostor pro vznik problémů. Jak pak poznáte jestli problém vznikl chybně napsaným programem, špatnou datovou rychlostí nebo špatným ovládáním terminálu ?

Detailní popis registrů a bitů, kterými se USART nastavuje najdete v jiném článku na tomto webu. Nebudu tedy nosit dříví do lesa a psát již napsané. Projděte si jej prosím. Jak pomocí těchto registrů USART konfigurovat si budeme ukazovat na příkladech.

A) USART - Jednoduché odesílání

Nejtriviálnější co si můžeme ukázat je odeslání jednoho bajtu po stisku tlačítka. Tlačítko si připojíme na PA4 standardně proti zemi. PA4 nakonfigurujeme jako vstup s interním pull-up rezistorem. Problémy se zakmitáváním tlačítka vyřešíme pomocí _delay_ms() funkce. Nejprve je nutné nastavit baudrate. Jedna z nejrozšířenějších datových rychlostí je 9600bps. Je to vcelku pomalá komunikace. Atmel budeme taktovat interním RC oscilátorem na 8MHz (takže pozor kdo ještě neměnil fuses, přehoďte si zdroj clocku z 1MHz na 8MHz). V tabulce v datasheetu nebo na webu si pak najdeme, že UBRR registr musíme naplnit hodnotou 51 (nemáme zapnutý "double speed"). V UCSRB si nastavíme bit TXEN čímž zapneme vysílač. Pin Tx bude od této chvíle přidělen jednotce USART. Pak musíme nakonfigurovat délku zprávy, počet stop bitů a paritu. To se provádí v registru UCSRC. Využijeme toho, že po restartu je celý registr naplněn nulami. Bity, které mají být nulové, tedy explicitně nulovat nebudeme. Nastavíme pouzy ty bity, které potřebujeme v log.1. Tady je hned první úskalí. Při zápisu do registru UCSRC je vždy nutné nastvit bit URSEL. Berte to jak fakt a neřešte zatím proč. Chceme odesílat zprávu dlouhou 8bitů, musíme tedy nastavit bity UCSZ1 a UCSZ0. Paritu nepoužíváme, takže bity UPM0 a UPM1 necháváme nulové. Pracujeme v asynchronním režimu, takže bit UMSEL také necháváme vynulovaný. Ve zprávě chceme mít jen jeden stop bit, USBS tedy necháme v nule. bit UCPOL nás v asynchronním režimu nezajímá. A to je v podstatě vše. Odeslání zprávy pak probíhá tak že do UDR registru zapíšeme odesílaná data. Musíme si však pohlídat zda je volný. Jeho stav nám signalizuje vlajka UDRE v registru UCSRA. Vlajka v log.1 říká, že UDR je prázdný a je možné do něj zapsat data. Funkce pro odeslání dat, tedy nejprve slušně počká než bude registr UDR prázný a teprve pak do něj zapíše data. Tady je na místě poznamenat, že UDR slouží jen jako jakási přestupní stanice. Vysílání se ve zkutečnosti provádí z Transmit Shift registru. Jakmile zapíšete data do UDR čekají tam tak dlouho než je Transmit shift registr volný. Nemůžete se tedy spolehnout na to, že když je UDR volný, že jsou data už odeslána (jejich odesílání může pořád probíhat). Výhoda takového uspořádání je v tom, že vysílání pak nemusí mít prostoje. Zatímco se vysílá z Transmit Shift registru vy už máte UDR volný a můžete ho plnit novými daty. Ale o tom více v dalších příkladech :)

// A) UART - jdnoduché odesílání
#include <avr/io.h>
#define F_CPU 8000000
#include <util/delay.h>

#define STISKNUTO (!(PINA & (1<<PINA4))) // makro na kontrolu stavu tlačítka
char stav_tlacitka=1;

void odesli( unsigned char data );

int main(void){
	UBRRH=0;
	UBRRL=51; // podle tabulky pro clock 8MHz a baud rate 9600Bd/s	
	UCSRB = (1<<TXEN); // zapnout vysílač
	UCSRC = (1<<URSEL) | (1<<UCSZ1) | (1<<UCSZ0); // 1 stop bit,žádná parita, 8bit zpráva
	DDRA &=~(1<<DDA4); // vstup pro tlačítko
	PORTA |=(1<<PORTA4); // pull-up na tlačítko

	while(1){
		if(STISKNUTO && stav_tlacitka){
			stav_tlacitka=0; // tlačítko bylo stisknuto
			odesli('a'); // pošli zprávu
		}
		if(!STISKNUTO){stav_tlacitka=1;} // tlačítko bylo uvolněno
		_delay_ms(100); // ošetření zákmitů tlačítka
	}
}

void odesli( unsigned char data ){
	while (!( UCSRA & (1<<UDRE))); // počkej než bude UDR prázdný
	UDR = data; // odešleme data
}

Obrázek a1 - přenos jednoho bajtu v konfiguraci 8bit + 1stop bit + žádná parita na 9600Bd/s

Na obrázku a1 vidíte výsledek pokusu. Po stisku tlačítka se odešle písmeno 'a'. V ASCII tabulce můžete najít, že malé 'a' má kód 97 což binárně odpovídá 0b01100001. Vysílá se nejprve bit s nejnižší váhou (LSB). Takže na osciloskopu bychom zleva měli vidět sekvenci 0100001101. První je start bit (log.0), pak následuje vysílaný bajt (od konce) a nakonec jeden stop bit (log.1.). Při datové rychlosti 9600bps by jeden bit měl trvat 1/9600 = 104us. Celá zpráva (1+8+1) bitů by měla trvat 1.04ms. Připojíte-li si modulek do PC, mělo by vám po každém stisku tlačítka přibýt jedno písmeno 'a' v terminálu. Tento příklad asi v praxi moc nevyužijete, ale poslouží jako základ pro odesílání komplikovanějších zpráv.

B) USART - Jednoduchý příjem

Další naprosto jednoduchý příklad je příjem jednoho znaku. Ten už najde i nějaké využití tam kde se Atmel může soustředit pouze na příjem a stačí mu jednopísmenné pokyny. My budeme rozsvěcet a zhasínat LED. Konfigurace baudrate je stejná jako v předchozím příkladě. Namísto vysílače zapínáme přijímač (nastavením bitu RXEN). Opět konfigurujeme délku zprávy na 8bitů, jeden stop bit a žádnou paritu. V hlavní smyčce program stále čeká na nastavení vlajky RXC - Receive Complete. Která značí, že byl dokončen příjem zprávy (bajtu). Ihned potom vyčteme registr UDR. Registr má dvě různé role, podle toho zda do něj zapisujete nebo čtete. Nelze si z něj tedy přečíst to co jste tam zapsali. Čtením z UDR ve skutečnosti vyčítáme tzv. Receive Data Buffer Register (RXB). Ten obsahuje dvojúrovňovou FIFO paměť. Jinak řečeno, můžou v něm být uloženy dva bajty. Příchozí zpráva se bit po bitu načítá do tzv. receive Shift Register. Technicky vzato tedy mohou přijít celkem tři byty aniž by jste jakýkoli ztratili. Tato konfigurace vám dává možnost přijímat jeden byte za druhým bez prodlev potřebných na vyčtení. Zatímco jeden přijatý byte zpracováváte, může probíhat příjem dalšího byte (ale o tom až později). Přečtením UDR se navíc automaticky smaže i vlajka RXC (a nejen ona), takže se nemusíte starat o její mazání. Obecně je ale nutné vyzvednout přijatý byte co nejdříve, protože zatím nemůžete nijak přesvědčit vysílač aby s vysíláním počkal. Pokud vám přijdou tři byty (máme FIFO + Shift Register), vy si je nevyzvednete a začne přicházet další byte tak o nejstarší nevyzvednutý byte přijdete. O této nešťastné situaci vás bude informovat vlajka DOR v registru UCSRA.

// B) UART - jednoduché přijímání jednoho znaku
#include <avr/io.h>
#define F_CPU 8000000

char prijato;

int main(void){
	UBRRH=0;
	UBRRL=51; // podle tabulky pro clock 8MHz a baud rate 9600Bd/s
	UCSRB = (1<<RXEN) ; // zapnout vysílač
	UCSRC = (1<<URSEL) | (1<<UCSZ1) | (1<<UCSZ0); // 1 stop bit,žádná parita, 8bit zpráva
	DDRA |=(1<<DDA1); // LED

	while(1){
		while (!(UCSRA & (1<<RXC))); // čekej na příchozí byte
		prijato=UDR; // načti příchozí byte
		if(prijato=='a'){PORTA |=(1<<PORTA1);} // roszviť LED
		if(prijato=='q') {PORTA&=~(1<<PORTA1);} // zhasni LED
	}
}

C) USART - implementace printf

Od posílání jednotlivých znaků vede ještě dlouhá cesta k alespoň trochu inteligentní komunikaci. Zvlášť pokud výstup z jednočipu má číst člověk. Tu cestu už za vás programátoři prošlápli a dali vám k dispozici funkci printf a její ekvivalenty. Vzhledem k tomu, že se učí jako první věc ve všech kurzech jazyka C, nejspíš se s ní už znáte. Její použití v jednočipech má ale svoje úskalí. Je totiž dosti velká a tudíž i pomalá. Je dokonce tak velká, že její plná verze by se vám do paměti u mnoha AVR vůbec nevešla. Standardní knihovny pro AVR ale obsahují "ořezanou" verzi. I ta však zabere v paměti něco okolo 1.5kB (a to bez podpory čísel s plovoucí desetinnou tečkou). Existuje spousta variací na printf. Funkce sprintf, snprintf a spol. nepotřebují nic nastavovat, můžete je po includování knihovny stdio.h používat hned. Ale funkci printf musíte nastavit "standardní výstup". Tím totiž může být kde co. Alfanumerický displej, USART, SPI, I2C nebo obecně jakákoli funkce. My si standardní výstup vytvoříme drobnou modifikací odesílací funkce z příkladu A. A pak pomocí kouzelné formule překladači vysvětlíme, že právě na ni má standardní výstup přesměrovat. Dívejte se na to jako na kouzelné formule, protože já tomu nerozumím :) Když nahlédnete do seznamu funkcí v AVR knihovně stdio.h všimnete si, že tam některé z nich existují s příponou _P (printf_P, sprintf_P). Ty slouží k tomu aby mohl být formátovací řetězec čten z paměti flash (tedy z paměti programu). To se hodí k úspoře RAM. Zavoláte-li funkci printf("ahoj") překladač stejně musí řetězec "ahoj" uložit někde ve flash paměti. Pak ho zkopíruje do RAM (to už ho máte dvakrát) a teprve pak předá funkci printf. Tenhle krok je zbytečný a proto existují funkce pro práci s řetězci přímo z paměti flash. K tomu aby jste mohli obsluhovat paměť flash ale potřebujete knihovnu avr/pgmspace.h. Ta mimo jiné obsahuje makro "PSTR()", které zjistí ukazatel na řetězec v paměti flash. Všechny řetězce z flash paměti tedy budete funkcím předávat skrze toto makro. Tak jak je patrné v našem příkladu a jak bude patrné i v dalších příkladech. Náš zdrojový kód po přeložení zabírá 1.77kB. Daň za použití printf je ale jen jednorázová, takže dál už nás nebude příliš omezovat. Nicméně i tak vám v některých situacích může vadit dlouhá doba zpracovávání printf. Pak vám nezbude nic jiného než si potřebné funkce napsat sami (nebo googlit).

//C) UART - implementace Printf a řetězce v paměti Flash 
#include <avr/io.h>
#define F_CPU 8000000
#include <util/delay.h>
#include <stdio.h> // printf()
#include <avr/pgmspace.h> // chceme pracovat s řetězci ve flash paměti

int usart_putchar(char var, FILE *stream); // odesila znaky na UART

unsigned int pocet=0;
// kouzelná formule
static FILE mystdout = FDEV_SETUP_STREAM(usart_putchar, NULL, _FDEV_SETUP_WRITE); 

int main(void){
// kouzelná formule
stdout = &mystdout; // nastavení standardního výstupu na naši funkci usart_putchar()
UBRRL = 51; // baud rate 9600 s clockem 8MHz
UCSRB = (1<<TXEN); // zapnout vysílač i přijímač
UCSRC = (1<<URSEL) | (1<<UCSZ0) | (1<<UCSZ1); // 8bit přenos, URSEL vybira zapis do UCSRC

while(1){	
 printf_P(PSTR("Uz ti to opakuju po %u\n\r"),pocet);
 _delay_ms(1000);
 pocet++;
 }
}

int usart_putchar(char var, FILE *stream) {
 while (!(UCSRA & (1<<UDRE))); // čekej než se dokončí případná předchozí vysílání
 UDR = var; // odešli data 
 return 0;
}


Obrázek c1 - přenos delší zprávy

D) USART - příjem znakových pokynů s přerušením

V tomto příkladu si ukážeme jak nastavit USART na příjem pomocí přerušení. Aby funkce nebyla složitá, budeme přijímat vždy pouze jeden znak. Po přijetí zprávy si rozsvítíme nebo zhasneme příslušnou LED (na PA1 a PA2). Přijetí zprávy bude program potvrzovat odesláním "OK" zpět na váš terminál. Konfigurace USARTu vychází z předchozích příkladů. Protože chceme provozovat obousměrný přenos, spustíme přijímač i vysílač nastavením bitů RXEN a TXEN, zprávu opět konfigurujeme jako 8bitovou bez parity a s jedním stop bitem. V registru UCSRB si nastavením bitu RXCIE povolíme přerušení od přijímače. V rutině přerušení pak provádíme jednoduchou operaci. Vyčteme co nejrychleji data z UDR abychom tak uvolnili místo pro případný další přijímaný znak a dáme hlavní smyčce vědět, že jsou k dispozici nová data. V hlavní smyčce pak přijatý znak dekódujeme. Pokud by šlo o jednoduchý úkon, mohli by jste jej provádět přímo v rutině přerušení. Ale protože naše funkce obsahuje i vysílání (a to je časově značně náročný úkon) není dobré ji do rutiny přerušení vkládat. Uvědomte si také, že nemáme vytvořen žádný mechanismus, který by řešil vyjimečné situace. Například příchod nové zprávy aniž bychom tu starou stihli zpracovat a podobně. Proto je toto řešení potřeba brát s rezervou a uvažovat zda vám tyto limity nevadí. Kdo chce může si vyzkoušet vyšší datové rychlosti. Zapsáním 0 do UBRR nastavíme rychlost 500kbps a nastavením bitu U2X v registru UCSRA nastavíte 1Mbps. V obou případech komunikace běží. Vyšší datové toky oceníte u přenosu objemnějších dat.

//D) UART - příjem znakových pokynů přerušením
#include <avr/io.h>
#define F_CPU 8000000
#include <stdio.h>
#include <avr/pgmspace.h>
#include <avr/interrupt.h>

int usart_putchar(char var, FILE *stream);

static FILE mystdout = FDEV_SETUP_STREAM(usart_putchar, NULL, _FDEV_SETUP_WRITE);
volatile char prijem=0, prijaty_znak=0;

int main(void){
stdout = &mystdout; // nastavení printf na UART
DDRA = (1<<DDA1) | (1<<DDA2); // indikační LED
UBRRL = 51; // baud rate 9600 s clockem 8MHz
UCSRB = (1<<RXEN)|(1<<TXEN); // zapnout vysílač i přijímač
UCSRC = (1<<URSEL)|(1<<UCSZ0) |(1<<UCSZ1); // upravit na čitelnější
UCSRB |= (1 << RXCIE); // povolit přerušení od přijímače
sei(); // globální povolení přerušení

while(1){	
 if(prijem){ // pokud přišel nový znak
  prijem=0;
  if(prijaty_znak=='1'){PORTA |= (1<<PORTA1);}
  if(prijaty_znak=='2'){PORTA |= (1<<PORTA2);}
  if(prijaty_znak=='0'){PORTA &=~((1<<PORTA1) | (1<<PORTA2));}
  printf_P(PSTR("OK\n\r")); // pošli odpověď
  }
 }
}

ISR(USART_RXC_vect){
 prijaty_znak = UDR; // Vyčíst přijatá data
 prijem=1; // dej hlavní smyčce info o příchodu nového znaku
 }

int usart_putchar(char var, FILE *stream) {
 while (!(UCSRA & (1<<UDRE))); // čekej než se dokončí případná předchozí vysílání
 UDR = var; // odešli data 
 return 0;
}


Obrázek d1 - Příjem znaků s přerušením. Červený průběh Tx Atmelu, modrý průběh Rx Atmelu. Znaky LF ('\n') a CR ('\r') slouží v terminálu k přejití na nový řádek a návratu kurzoru na začátek řádku.

E) USART - příjem textovych pokynů s přerušením

Někdy budete chtít svou aplikaci vybavit trošku inteligentnější komunikací s PC. Budete chtít aby obsluha mohla zadávat příkazy v trochu srozumitelnější formě než jedno písmeno. Může je zadávat třeba řetězcem ("start", "Stop" a podobně). Kromě čitelnosti tu ale neexistuje žádná další výhoda nad příkazy ve formě jednoho znaku. Naopak za příjem takových zpráv musíme zaplatit jistou daň jejich složitějším dekódováním. Nakonec se, ale ukáže že to nebude tak špatný nápad (hlavně v dalším příkladě). USART necháme nastavený stejně jako v příkladě D. Zmodifikujeme ale rutinu přerušení a přidáme funkci dekódující řetězce. Pro práci s řetězci máme k dispozici knihovnu string.h. Ta obsahuje spoustu užitečných funkcí. My z ní použijeme strncpy() a strncmp(). Přirozeně počítám s tím, že si pamatujete jak vypadá v C řetězec. Funkce strncpy() kopíruje obsah jednoho řetězce do druhého s omezením počtu znaků. Stane-li se tedy jakákoli havárie a kopírovaný řetězec nebude obsahovat ukončovací znak '\0' zastaví se funkce po "n" znacích. Funkce strncmp() porovnává dva řetězce (opět s omezením na "n" znaků) a vrací log.0 pokud jsou řetězce shodné. Protože očekáváme zadávání zprávy ručně z terminálu, měli bychom si připomenout, že stiskem klávesy enter se odešle znak '\n' (nebo '\r' podle terminálového programu). Naše funkce tedy bude takový znak očekávat a pochopí podle něj, že je zpráva kompletní. Délku zprávy omezíme na 15 znaků (bez enteru). Rutina přerušení bude vykonávat následující činnost. Nejprve vyzvedne z UDR nově příchozí byte a ověří zda neobsahuje některý z ukončovacích znaků. Pokud ne, uloží příchozí byte do paměti na příslušnou pozici. Postupně tak v poli docasne skladuje přijaté znaky. Jakmile přijde ukončovací znak, nebo jakmile je přijímaná zpráva příliš dlouhá budeme ji považovat za celou a přistoupíme k dalšímu zpracování. Na konec pole doplníme ukončovací znak pro řetězce ('\0'), čímž z pole znaků uděláme řetězec. Pak pole docasne pomocí funkce strncpy() zkopírujeme do pole zprava. Tím si uvolníme podle docasne na příjem nové zprávy. Nakonec dáme hlavní smyčce vědět, že je potřeba zpracovat novou zprávu (pomocí proměnné nova_zprava). Od tohoto okamžiku můžeme přijímat novou zprávu a máme dost času aktuální zprávu zpracovat. Zpracování provádíme pomocí funkce strncmp() a porovnáváme příchozí zprávu s očekávanými možnostmi. Tady je opět na místě připomenout, že pro ušetření RAM bychom mohli použít fci strncmp_P a porovnávaný řetězec držet v paměti flash. Pokud zpráva neodpovídá žádné možnosti dáme o tom vědět obsluze.

//E) UART - příjem textovych pokynů s přerušením
#include <avr/io.h>
#define F_CPU 8000000
#include <stdio.h>
#include <avr/pgmspace.h>
#include <avr/interrupt.h>
#include <string.h> // funkce pro praci s řetězci strncmp(), strncpy()
#define DELKA_ZPRAVY 16 // maximalni delka zpravy

int usart_putchar(char var, FILE *stream); // odesila znaky na UART

static FILE mystdout = FDEV_SETUP_STREAM(usart_putchar, NULL, _FDEV_SETUP_WRITE);
volatile char nova_zprava=0; // indikuje prichod nove zpravy
volatile char zprava[DELKA_ZPRAVY]; // tady bude uložena celá přijatá zpráva
volatile char docasne[DELKA_ZPRAVY]; // sem se postupně ukládají přicházející znaky

int main(void){
stdout = &mystdout; // nastavení printf na UART
DDRA = (1<<DDA1) | (1<<DDA2); // indikační LED
UBRRL = 0x33; // baud rate 9600 s clockem 8MHz
UCSRB = (1<<RXEN)|(1<<TXEN); // zapnout vysílač i přijímač
UCSRC = (1<<URSEL) | (1<<UCSZ0) | (1<<UCSZ1); // 8bit přenos, URSEL vybira zapis do UCSRC
UCSRB |= (1 << RXCIE); // povolit přerušení od přijímače
sei(); // globální povolení přerušení

while(1){	
 if(nova_zprava){ // pokud přišla nová zpráva
 // porovnáme obsah zpráv
  if(strncmp(zprava,"ahoj",DELKA_ZPRAVY)==0){
   printf_P(PSTR("Nazdar.\n\r")); // pošli odpověď
   }
  else if(strncmp(zprava,"rozsvit",DELKA_ZPRAVY)==0){
   PORTA |= (1<<PORTA1);
   printf_P(PSTR("S radosti.\n\r")); // pošli odpověď
   }
  else if(strncmp(zprava,"rozsvit vic",DELKA_ZPRAVY)==0){
   PORTA |= (1<<PORTA1) | (1<<PORTA2);
   printf_P(PSTR("Jak je libo.\n\r")); // pošli odpověď
   }
  else if(strncmp(zprava,"zhasni",DELKA_ZPRAVY)==0){
   PORTA &=~((1<<PORTA1) | (1<<PORTA2));
   printf_P(PSTR("Zhasnuto.\n\r")); // pošli odpověď
   }
  else{
   printf_P(PSTR("Nerozumim.\n\r")); // žádná známá zpráva
   }
   nova_zprava=0; // zpráva je zpracována, budeme čekat na novou
  }
 }
}

ISR(USART_RXC_vect){
char prijaty_znak;
static char i=0; // pocitadlo znaku v prijimane zprave
 prijaty_znak = UDR; // vytáhneme znak co nejdřív z přijímacího bufferu
 // pokud nepřišel ukončovací znak a ještě není znaků moc
 if((prijaty_znak != '\n') && (prijaty_znak!='\r') && (i<DELKA_ZPRAVY-2)){
  docasne[i]=prijaty_znak; // uložíme nově příchozí znak
  i++; 
  }
 else{
  // pokud je konec zprávy
  docasne[i]='\0'; // uložíme na konec znak '\0' - konec řetězce
  strncpy(zprava,docasne,DELKA_ZPRAVY); // zkopirujeme přijatý text do pole zprava
  nova_zprava=1; // dáme vědět hlavní smyčce že má novou zprávu
  i=0; // připravíme se na příjem nové zprávy 	
  }
}

int usart_putchar(char var, FILE *stream) {
 while (!(UCSRA & (1<<UDRE))); // čekej než se dokončí případná předchozí vysílání
 UDR = var; // odešli data 
 return 0;
}


Obrázek e1 - příjem textovych pokynů s přerušením, zvýrazněn čas potřebný pro zpracování přijaté zprávy v rutině přerušení

Na obrázku e1 máme na tmavě modrém průběhu znázorněnu dobu průběhu rutiny přerušení. Nejdelší část rutiny, tedy příjem ukončovacího znaku spolu s kopírováním celé zprávy z dočasného pole do pole zpráva trvá 19us. Během tohoto času smí přijít nanejvýše dva nové znaky (na přijímači je buffer) abychom je ještě měli šanci zpracovat. Vidíte, že při rychlostech 9600Bd/s je doba zpracování zanedbatelně krátká ve srovnání s dobou potřebnou na přenesení jednoho znaku. Takže si s tím nemusíte lámat hlavu. Při rychlejší přenosech to ale může být problém. Ale pro výrazně rychlejší přenosy si stejně budete muset přestoupit na nějakou jinou platformu jako třeba ARM, která je vybavená DMA.

F) USART - příjem s argumenty

Poslední věc, která nám chybí v naší výbavě je možnost přijímat vstupní hodnoty. Často budete chtít aby obsluha z terminálu mohla zadávat vstupy v podobě "hladina=-15", "time=73", "mode=a". Budete tedy chtít předchozí příklad vybavit nějakým chytrým dekódováním zprávy. Variant jak něco takového vytvořit je mnoho. Můžete řetězec rozdělit pomocí strtok() a jeho části pak zpracovávat pomocí itoa() případně použít jiný přístup. My si ukážeme využití funkce sscanf(). Celé nastavení USARTu i celá přijímací rutina je shodná jako v příkladě E. Na funkci sscanf() se můžete koukat jako na opak printf nebo přesněji sprintf(). Dodáte jí formátovací řetězec, pak jí dodáte přijatý řetězec a ona se ze něj pokusí vytáhnout hodnoty proměnných. Pokud se jí to povede, vrátí vám počet načtených položek. My použijeme její variantu s příponou _P abychom mohli mít řídící řetězec v paměti flash. Zpráva by měla být ve formátu "c=xxxx" + ukončovací znak. Za xxxx můžeme napsat libovolný integer. Tady vás opět odkážu na práci s funkcí printf obecně. Funkcemi printf a sscanf přenášíte veškerou práci se zpracováním textových pokynů na Atmel, počítejte tedy s tím, že mu může vzít singifikantní množství času. Pokud program pracuje výhradně s lidskou obsluhou u terminálu, není si s čím lámat hlavu. Lidské ruce i mozek jsou dost pomalé na to aby zdržení vůbec nezaznamenaly. Při strojovém nahrávání větších bloků dat při vyšších datových rychlostech už ale na dobu zpracování budete muset brát zřetel. V takovém případě je vhodnější nepoužívat pro komunikaci textovou formu a posílat data z PC programu do Atmelu přímo binárně aby se tak vyhnul jejich náročnému zpracování. Ale to je problematika na samostatný článek :) Protože je kód stejný jako v příkladě E, uveřejním jen hlavní smyčku ve které probíhá zpracování zprávy.

//F) USART příjem hodnot - pomocí sscanf_P()
// zbytek kódu shodný s příkladem E)
while(1){	
 if(nova_zprava){ // pokud přišla nová zpráva
	if(sscanf_P(zprava,PSTR("c=%d"),&x)){ // pokud zprava odpovida formatu
	 printf_P(PSTR("Nacteno %d\n\r"),x); // tak mame ulozeno
	 }
	else{printf_P(PSTR("Error\n\r")); // jinak vynadame uzivateli
	 }
   nova_zprava=0; // zpráva je zpracována, budeme čekat na novou
  }
 }

G) USART - odesílání s přerušením

Někdy se vám může stát, že by vás odesílání příliš zdržovalo. Typicky při odesílání větších objemů dat, nebo při nízkých datových rychlostech. V takové situaci můžete (nebo snad i musíte) využít možnost odesílat pomocí přerušení. Filosofie je prostá, odesílanou zprávu si vložíte do pole (nebo do řetězce), spustíte odesílání, po odeslání každého znaku jen rychle skočíte do rutiny přerušení, dáte odesílat další znak a zbytek času se můžete věnovat jiným úkolům. Naše funkce bude odesílat zprávu po stisku tlačítka na PA4. Konfigurace bude opět tradiční, 8bitů zprávy, 1 stop bit a žádná parita. Baudrate 9600 a zapneme jen vysílací modul (nastavením bitu TXEN). Pomocí proměnné provoz budeme signalizovat, že probíhá odesílání zprávy. I když by to náš program asi nepotřeboval, měli by jste na to brát zřetel. Pokud by jste totiž během posílání změnili odesílaná data, mohl by vám do PC dorazit pěkný zmatek. Funkcí sprintf() si vytvoříme odesílaný řetězec v poli zprava. Proměnná i slouží jako index ukazující, který znak zprávy se má odesílat. Ten přirozeně před odesíláním vynulujeme - chceme zprávu odesílat od prvního znaku. Pomocí funkce strlen() zjistíme délku vysílané zprávy abychom věděli kdy vysílání ukončit. Nastavením bitu UDRIE v registru UCSRB povolíme přerušení od prázdného UDR. Jakmile je UDR prázdný je vyvoláno přerušení a my do něj uložíme další znak zprávy. To děláme tak dlouho než odešleme celou zprávu. Po jejím odeslání prostě přerušení vypneme a vše je hotovo. Je potřeba si ale uvědomit, že jakmile předáme poslední znak do UDR ještě to neznamená, že je vše odesláno. V tom okamžiku se může z Transmit buffer registru stále ještě odesílat předchozí znak. USART tedy znak který jsme teď uložili do UDR ještě vůbec nemusel začít odesílat. Všechny znaky zprávy budou odeslány až v okamžiku kdy bude nastavena vlajka TXC (v registru UCSRA). To nastane v okamžiku kdy se Transmit shift registr vyprázdní a v UDR nečekají žádná další data.

//G) USART - odesílání s přerušením
#include <avr/io.h>
#define F_CPU 8000000
#include <stdio.h>
#include <avr/pgmspace.h>
#include <avr/interrupt.h>
#include <string.h> // funkce pro praci s řetězci strlen()
#define DELKA_ZPRAVY 16 // maximalni delka zpravy

volatile char zprava[DELKA_ZPRAVY]; // tady bude uložena celá přijatá zpráva
char stisk=0, provoz=0;
unsigned int x=0;
volatile unsigned int delka_zpravy=0;
volatile unsigned int i;

int main(void){
DDRA &=~(1<<DDA4);
PORTA |= (1<<PORTA4); // pull up na tlačítko
UBRRL = 51; // baud rate 9600 s clockem 8MHz
UCSRB = (1<<TXEN); // zapnout vysílač i přijímač
UCSRC = (1<<URSEL) | (1<<UCSZ0) | (1<<UCSZ1); // 8bit přenos, URSEL vybira zapis do UCSRC
sei(); // globální povolení přerušení

while(1){	
 if(!(PINA & (1<<PINA4)) && stisk==0 && provoz==0){ // první stisk tlačítka
  provoz=1; // dalsi odeslani nelze spustit drive nez toto skonci
  stisk=1; // tlačítko bylo poprvé stisknuto
  x++; // proměnná kterou odesíláme - jen aby zpráva nebyla nudná
  snprintf_P(zprava,DELKA_ZPRAVY,PSTR("Odeslano: %u\n\r"),x); // tak mame ulozeno
  i=0; // index znaků v odesílané zprávě, začínáme odesílat od nultého znaku
  delka_zpravy=strlen(zprava); // kolik znaků budu odesílat ?
  UCSRB |= (1 << UDRIE); // povolit přerušení od prázdného UDR - začínáme odesílat
  }
 if(PINA & (1<<PINA4)){
  stisk=0; // tlačítko bylo uvolněno
  }	
 }
}

ISR(USART_UDRE_vect ){
 UDR = zprava[i]; // odešli další znak zprávy
 i++;
 if (i > delka_zpravy - 1){  // pokud jsou odeslány všechny znaky
  UCSRB &= ~(1 << UDRIE); // vypni přerušení
  provoz=0; // odemci odeslani dalsi zpravy
  }	
 }

H) USART - komunikace procesor - procesor

Kromě běžných 8bitových (a kratších) zpráv můžeme na AVR posílat USARTem i 9bitovou zprávu. Protože však nemáme prostředky jak ji přijímat v PC (v úvodu zmiňované modulky to neumožňují) předvedeme si takovou komunikaci mezi dvěma Atmely. Zabijeme tak dvě mouchy jednou ranou, protože v jednom příkladě budeme mít kód pro příjem i pro vysílání. Komunikaci si předvedeme pouze jednosměrnou. Jeden čip (Master) bude hlídat stisk tří tlačítek. Po stisku některého z nich pošle zprávu druhému Atmelu (Slave), který podle příchozí zprávy rozsvítí nebo zhasne příslušnou LED. Připravíme si tím podhoubí pro další příklad (s použitím MPCM). Navíc si ukážeme jak na straně přijímače zachytit některou z chyb přenosu. Master má zdrojový kód H1. Baudrate necháváme 9600, stop bit jeden a paritu žádnou. Délku zprávy 9 bitů zvolíme nastavením bitů UCSZ2 (v registru UCSRB) a dvojice bitů UCSZ1 a UCSZ0 (v registru UCSRC). 9.bit zprávy je před odesláním nutné nahrát do bitu TXB8 v registru UCSRB. To je také jediná modifikace kterou naší v funkci odesli() provedeme. Zbytek kódu je bez záludností. K programu slave čipu (zdrojový kód H2) budu mít více komentářů. Konfigurace UART je shodná jako s masterem. Přijímací funkci USART_prijem() napíšeme hloupě pomocí pollingu aby jste nebyli zbytečně zmatení. Čekáme v ní dokud není přijata zpráva (což poznáme nastavením vlajky RXC). Ještě před přečtením dat z UDR je nutné přečíst stav UCSRB protože v něm je 9.bit zprávy. Přečtením UDR by hodnoty v UCSRB pozbyly platnost a my bychom už neměli možnost bit přečíst. To samé platí pro registr UCSRA ve kterém jsou vlajky indikující stav přenosu. Teprve pak můžeme UDR přečíst a uvolnit tak registr k příjmu další zprávy. Zkontrolujeme tři vlajky. Vlajka DOR (Data OverRun) signalizuje zda jsme si stihli vyzvednou včas všechny příchozí zprávy nebo zda byl buffer plný a některá ze zpráv je ztracena. Vlajka FE (Frame Error) signalizuje korektní příjem stop bitu. Pokud by stop bit nepřišel tak jak má, máme důvod myslet si, že je přenos porušen. Ať už tím, že vysílač vysílá na jiné rychlosti, nebo byla linka rozpojena. Vlajka PE (Parity Error) by pak signalizovala chybu parity. Protože ale paritu ve zprávě neočekáváme, nebude nás tento bit zajímat. Pokud by některá z vlajek byla nastavena, necháme funkci vrátit hodnotu 0xffff čímž dáme hlavní smyčce na vědomí, že nastal problém. Ta na to později zareaguje rozsvícením příslušné LED. Pokud byl příjem v pořádku a žádná z vlajek nastavena není, složíme 9.bitovou zprávu do integeru a přijímací funkce ji vrátí. Hlavní smyčka už pak zprávu roztřídí. Pokud přišlo 0x101 rozsvítí LED a pokud 0x103 zhasne všechny LED včetně té která signalizuje chybný přenos. Oba čipy pracují na 8MHz (jak je vidět v záhlaví zdrojáku). Master atmel je připojen svým vývodem Tx na Rx slave atmelu. Slave má indikační LED na PB0 a PB1. Master má tlačítka na PA3, PA4 a PA5. Chybnou zprávu si můžete nasimulovat prostě tím, že odpojíte datovou linku od masteru a připojíte ji na zem. Jakmile se na Rx pin dostane log.0 vyhodnotí to přijímač jako začátek vysílání, přijme domnělou zprávu a na jejím konci bude očekávat stop bit (log.1), který ale neobdrží, protože je linka pevně připojena na zem. To vyhodnotí jako Frame Error a dá vám o tom vědět rozsvícením LED.

 // H1) - 9bit multiprocesorová komunikace 1 - Slave
#include <avr/io.h>
#define F_CPU 8000000

unsigned int USART_prijem(void);
volatile unsigned int x=0;

int main(void){
UBRRH=0;
UBRRL=51; // podle tabulky pro clock 8MHz a baud rate 9600Bd/s
UCSRB = (1<<RXEN) | (1<<UCSZ2); // zapnout vysílač, 9bit zpráva
UCSRC = (1<<URSEL) | (1<<UCSZ1) | (1<<UCSZ0); // 1 stop bit,žádná parita;
DDRB = (1<<DDB0) | (1<<DDB1); // výstupy na indikační LED

while(1){
	x=USART_prijem(); // čekej než přijde znak
	if(x==0x101){PORTB |= (1<<PORTB0);} // rozsviť LED1
	if(x==0x103){PORTB &=~((1<<PORTB1) | (1<<PORTB0));} // zhasni obě LED
  if(x==0xffff){PORTB |= (1<<PORTB1);} // rozsviť LED2 - chyba přenosu
 }
}

unsigned int USART_prijem(void)
{
unsigned char stav, resh, resl;
while ( !(UCSRA & (1<<RXC)) ); // čekej dokud není dokončen příjem
stav = UCSRA; // ulož vlajky přijímače (Frame error, Overrun a Parity error)
resh = UCSRB; // vytáhni 9.bit zprávy (musí být před čtením UDR !)
resl = UDR; // vytáhni dolních 8 bitů zprávy
if ( stav & ((1<<FE)|(1<<DOR)|(1<<PE)) ){return -1;} // pokud něco nehraje
resh = (resh >> 1) & 0x01; // odfiltruj 9.bit zprávy
return ((resh << 8) | resl); // vrať 9bitovou hodnotu
}

// H2) - 9bit multiprocesorová komunikace 1 - Master
#include <avr/io.h>
#define F_CPU 8000000
#include <util/delay.h>

#define STISKNUTO_A (!(PINA & (1<<PINA3))) // makro na kontrolu stavu tlačítka
#define STISKNUTO_B (!(PINA & (1<<PINA4))) // makro na kontrolu stavu tlačítka
#define STISKNUTO_C (!(PINA & (1<<PINA5))) // makro na kontrolu stavu tlačítka

char stav_tlacitka_a=1,stav_tlacitka_b=1,stav_tlacitka_c=1;
void odesli(unsigned int data); // odesílání 9bitové zprávy

int main(void){
UBRRH=0;
UBRRL=51; // podle tabulky pro clock 8MHz a baud rate 9600Bd/s
UCSRB = (1<<TXEN) | (1<<UCSZ2); // zapnout vysílač, 9bit zpráva
UCSRC = (1<<URSEL) | (1<<UCSZ1) | (1<<UCSZ0); // 1 stop bit,žádná parita;
DDRA &=~((1<<DDA4) |(1<DDA3) | (1<DDA5)); // vstup pro tlačítka
PORTA |=(1<<PORTA4) | (1<<PORTA5) | (1<<PORTA3); // pull-up na tlačítka

while(1){
// sleduj stisk tlačítek a po stisku odešli zprávu
 if(STISKNUTO_A && stav_tlacitka_a){
  stav_tlacitka_a=0; 
  odesli(0x101); 
  }
 if(STISKNUTO_B && stav_tlacitka_b){
  stav_tlacitka_b=0; 
  odesli(0x006); 
  }
 if(STISKNUTO_C && stav_tlacitka_c){
  stav_tlacitka_c=0;
  odesli(0x103); 
  }
  // tlacitka byla uvolnena
  if(!(STISKNUTO_A || STISKNUTO_B || STISKNUTO_C)){stav_tlacitka_a=1;stav_tlacitka_b=1;stav_tlacitka_c=1;}
  _delay_ms(100); // ošetření zákmitů tlačítek
 }
}

void odesli( unsigned int data ){
while (!( UCSRA & (1<<UDRE))); // čekej dokud není buffer volný k zapsání nových dat
UCSRB &= ~(1<<TXB8); // vyčisti 9.bit zprávy
if ( data & 0x0100 ){UCSRB |= (1<<TXB8);} // a pokud má být nastaven, nastav ho
UDR=data; // zapiš dolních 8bitů zprávy a odešli ji
}


Obrázek h1 - vlevo slave s indikačními LED, vpravo master se třemi tlačítky

I) USART - MPCM (Multi-processor Communication Mode)

Atmel umí USART používat ve speciálním MPCM módu. Mějme jeden master a několik slave čipů. Pokud je všechny propojíte jednou linkou (Masterovo Tx se spojí s Rx všech slave) může master dávat pokyny všem slave. Bohužel ho ale musí všechny slave poslouchat. Zprávu pro jednoho slave si musí přečíst všichni ostatní slave a obtěžuje je to při práci. Zvlášť pokud je komunikace hodně aktivní. MPCM mód slouží právě v takových situacích. Čip v MPCM módu ignoruje všechny příchozí zprávy které nemají nastavený 9.bit (neztrácí čas jejich dekódováním). Komunikace pak funguje následovně. Master pošle zprávu s nastaveným 9.bitem. Tu poslouchají a zpracovávají všichni slave. V ní pošle "adresu" (identifikační číslo) toho slave se kterým chce komunikovat. Příslušný slave si vypne MPCM mód a bude přijímat všechny zprávy. Master mu může dávat pokyny a ostatní neposlouchají. Když chce master komunikovat s někým jiným opět pošle zprávu s nastaveným 9.bitem a novou adresou. Všichni slave opět poslouchají a pokud jim adresa nepatří zapnout si MPCM mód a ignorují všechny zprávy opět do té doby než přijde adresa. Toť vše. Nikdy jsem tuto možnost v praxi nevyužil, takže z mé strany jde o příklad čistě akademický - jen si chci vyzkoušet jestli to jde. Konfigurace je trochu náročnější na hardware, na rozumnou demonstraci potřebujete alespoň tři čipy. Já použil dvě Atmega16 a jeden Atmega8 abych alespoň ověřil že je kód přenositelný. Viz obrázek i1. Vysílací program je stejný jako v případě H1 ale pro přehlednost je ve zdrojáku I2. Jeho konfiguraci tedy není nutné rozebírat. Zdrojáky pro slave obvody se liší jen v "adrese", která se nastavuje definicí MOJE_ADRESA. Konfigurace USART je opět 9600bps,1 stop bit,žádná parita, 9.bit komunikace s přerušením od přijímače. Nastavením bitu MPCM v registru UCSRA se slave ihned po startu přepne do MPCM módu a čeká na příjem zprávy. Rutinu přerušení ale vyvolá jen příjem zprávy s nastaveným 9.bitem. Pokud taková zpráva přijde, zkontroluje slave zda odeslaná adresa odpovídá té jeho. Pokud ne nastaví se do MPCM módu, protože datové zprávy nejsou určeny jemu (vůbec nevadí pokud už MPCM módu byl). Pokud se adresy shodují, vypne MPCM mód (vynulováním bitu MPCM) a zajistí si tak, že přerušení přijde s každou příchozí zprávou. Pokud přijde zpráva bez nastaveného 9.bitu jen ten slave, který má vypnuté MPCM ji zaznamená. A bude je zaznamenávat tak dlouho než master odešle "adresovací" zprávu s nastaveným 9.bitem a slave se zase nepřepne do MPCM módu (pokud je odvysílaná adresa odlišná od té jeho). Pro otestování může master odesílat tři zprávy. Zprávy 0x101 a 0x103 jsou adresovací protože mají nastavený 9.bit. Jeden ze slave má adresu 0x01 a druhý 0x03. Master tedy odesláním jedné z těchto zpráv aktivuje jeden ze slave. Master pak může odesílat zprávu 0x06 (bez 9.bitu) tu uslyší jen ten slave, který má vypnutý MPCM mód. Slave po přijetí takové zprávy přepne log. úroveň na PB0 (blikne LEDkou). můžete tak vidět který ze slave obvodů vás poslouchá.

// I0) - 9bit MPCM - slave 1
#include <avr/io.h>
#define F_CPU 8000000
#include <avr/interrupt.h>

#define MOJE_ADRESA 0x01 // adresa pro slave1

void odesli(unsigned int data); // odesílání 9bitové zprávy
volatile char nova_zprava=0; // indikujepříchod nové zprávy
volatile char zprava=0;

ISR(USART_RXC_vect){
char rx_bit,data;
rx_bit = UCSRB; // nejprve je potřeba vyčíst stav 9.bitu zprávy
data = UDR; // vyčtene dolních 8 bitů zprávy
if(rx_bit & (1<<RXB8)){ // pokud je to byte s adresou (nastaven 9.bit)
 if(data==MOJE_ADRESA){
  UCSRA &=~(1<<MPCM); // pokud je to moje adresa, zapnu příjem všech dat (vypnu MPCM)
  }
 else{UCSRA |= (1<<MPCM);} // pokud je to cizí adresa, vypnu příjem dat (zapnu MPCM)
}
else{ // pokud jsou to data, jsou určitě určena mě !
 zprava=data; // data obsahují zprávu
 nova_zprava++; // přišla další zpráva
 }
}

int main(void){
UBRRH=0;
UBRRL=51; // podle tabulky pro clock 8MHz a baud rate 9600Bd/s
UCSRB = (1<<RXEN) | (1<<UCSZ2) | (1<<RXCIE); // zapnout vysílač, 9bit zpráva, přerušení na příjem
UCSRC = (1<<URSEL) | (1<<UCSZ1) | (1<<UCSZ0); // 1 stop bit,žádná parita;
UCSRA |= (1<<MPCM); // sleduj jen zprávy s 9.bitem (adresy)

DDRB |= (1<<DDB0);
sei();

while(1){
 if(nova_zprava){ // pokud přišla nějaká zpráva
  if(zprava==0x06){PORTB ^= (1<<PORTB0);} // pokud je její obsah 0x06 přepni LED
  nova_zprava=0; // zprávu jsme zpracovali
  }
 }
}

// I1) - 9bit MPCM - slave 2

// ...
#define MOJE_ADRESA 0x03 // adresa pro slave2
// ...
// zbytek kódu je shodný se slave 1

// I2) - 9bit multiprocesorová komunikace 1 - Master
#include <avr/io.h>
#define F_CPU 8000000
#include <util/delay.h>

#define STISKNUTO_A (!(PINA & (1<<PINA3))) // makro na kontrolu stavu tlačítka
#define STISKNUTO_B (!(PINA & (1<<PINA4))) // makro na kontrolu stavu tlačítka
#define STISKNUTO_C (!(PINA & (1<<PINA5))) // makro na kontrolu stavu tlačítka

char stav_tlacitka_a=1,stav_tlacitka_b=1,stav_tlacitka_c=1;
void odesli(unsigned int data); // odesílání 9bitové zprávy

int main(void){
UBRRH=0;
UBRRL=51; // podle tabulky pro clock 8MHz a baud rate 9600Bd/s
UCSRB = (1<<TXEN) | (1<<UCSZ2); // zapnout vysílač, 9bit zpráva
UCSRC = (1<<URSEL) | (1<<UCSZ1) | (1<<UCSZ0); // 1 stop bit,žádná parita;
DDRA &=~((1<<DDA4) |(1<DDA3) | (1<DDA5)); // vstup pro tlačítka
PORTA |=(1<<PORTA4) | (1<<PORTA5) | (1<<PORTA3); // pull-up na tlačítka

while(1){
// sleduj stisk tlačítek a po stisku odešli zprávu
 if(STISKNUTO_A && stav_tlacitka_a){
  stav_tlacitka_a=0; 
  odesli(0x101); 
  }
 if(STISKNUTO_B && stav_tlacitka_b){
  stav_tlacitka_b=0; 
  odesli(0x006); 
  }
 if(STISKNUTO_C && stav_tlacitka_c){
  stav_tlacitka_c=0;
  odesli(0x103); 
  }
  // tlacitka byla uvolnena
  if(!(STISKNUTO_A || STISKNUTO_B || STISKNUTO_C)){stav_tlacitka_a=1;stav_tlacitka_b=1;stav_tlacitka_c=1;}
  _delay_ms(100); // ošetření zákmitů tlačítek
 }
}

void odesli( unsigned int data ){
while (!( UCSRA & (1<<UDRE))); // čekej dokud není buffer volný k zapsání nových dat
UCSRB &= ~(1<<TXB8); // vyčisti 9.bit zprávy
if ( data & 0x0100 ){UCSRB |= (1<<TXB8);} // a pokud má být nastaven, nastav ho
UDR=data; // zapiš dolních 8bitů zprávy a odešli ji
}


Obrázek i1 - Vlevo master, zbylé dva obvody slave.

J) USART - Synchronní režim

Synchronní komunikace má nad asynchronní několik výhod. Zaplatíte daň v podobě jedné linky navíc (clock) a už se nebudete muset starat o přesnosti hodin. A co víc, budete moct z využít vyšších přenosových rychlostí než v asynchronním režimu. V asynchronním režimu musí přijímač během jednoho přenášeného bitu 8x nebo 16x navzorkovat vstupní signál (podle toho jestli je zapnutý "double speed"). To je daň za to aby se dokázal přesně synchronizovat se začátkem vysílání (které může příjít úplně kdykoli). V synchronním režimu master generuje clock a všichni účastnící komunikace (slave) jej přijímají a jejich USART jednotka clock využívá k příjmu i vysílání. Pokud čipu nastavíte XCK pin jako výstup a zapnete synchronní přenos, stane se z něj master. Pokud je XCK pin vstup, je zařízení slave. XCK piny masteru a slave musí být přirozeně propojeny. U slave nemá baudrate žádný význam, takže hodnota v UBRR se nebere v úvahu. Master generuje clock neustále aby slave mohl kdykoli odesílat zprávy. Frekvence clocku se odvíjí od hodnoty v UBRR viz kapitola o Baudrate. Master může generovat nejvyšší frekvenci až F_CPU/2, ale slave potřebuje aby byla nižší jak 1/4 jeho F_CPU. Takže na 8MHz smíte do slave pustit nejvíce 2MHz clock. Je vhodné mít drobnou rezervu a nechodit v takovém případě přes 1MHz. Komunikace pak probíhá tak, že s nástupnou hranou clocku vysílač mění stav datové linky a se sestupnou hranou přijímač čte stav datové linky. Nebo opačně. Variantu si volíte bitem UCPOL v registru UCRSC. Veškerá ostatní konfigurace funguje stejně jako v asynchronním režimu. V našem příkladu si zkusíme synchronní komunikaci s UCPOL=0 a s baudrate 500kBd/s. Protože USB<->UART modulky nezvládají synchronní komunikaci, tak si ji opět předvedeme na dvou Atmelech. Master (zdrojový kód J2) po startu počká nějaký čas aby se slave stihl nastavit. Poté nastaví XCK (PB0) jako výstup a nakonfiguruje režim synchronního přenosu. Odešle první zprávu a pak už jen čeká na odpověď. Jakmile ji obdrží, drobně ji upraví a s malým zpožděním ji zase odešle. Slave (zdrojový kód J1) dělá to samé s tím rozdílem, že PB0 má nastaven jako vstup a žádnou prvotní zprávu neodesílá. Master si tedy se slave budou střídavě povídat. Jeden pošle zprávu druhém a ten mu po chvíli odpoví a my to všechno budeme sledovat osciloskopem.


Obrázek j1 - Synchronní komunikace pomocí USART. Tmavě modrý clock. Je patrné že data jsou nastavována s náběžnou hranou clocku a při sestupné hraně jsou již stabilní a přijímač je může spolehlivě přečíst. Červená linka je Master a posílá zpávy, slave na světle modré lince mu odpovídá.
// J1) - synchronní režim - Slave
#include <avr/io.h>
#define F_CPU 8000000
#include <util/delay.h>
#include <avr/interrupt.h>

volatile char x=1, nova_zprava=0;

int main(void){
UBRRH=0;
UBRRL=0;  // baudrate nemá u slave význam, vstupní clock musí být menší jak 1/4 F_CPU
UCSRB =  (1<<RXEN) | (1<<TXEN); // přijímáme i vysíláme
// 1 stop bit, žádná parita, 8bit zpráva, synchronní operace s UCPOL=0
UCSRC = (1<<URSEL) | (1<<UMSEL) | (1<<UCSZ1) | (1<<UCSZ0); 
UCSRB |= (1 << RXCIE); // povolení přerušení od přijímače
sei();

while(1){
 if(nova_zprava){
  nova_zprava=0;
  x=x+1; // pošleme zpět přijatá data o jedničku větší
  while (!( UCSRA & (1<<UDRE))); // počkej až budeš moct vysílat
  UDR = x; // odvysílej
  }
 }
}

ISR(USART_RXC_vect){
 x = UDR; 
 nova_zprava=1;
}

 // J2) - synchronní režim - Master
#include <avr/io.h>
#define F_CPU 8000000
#include <util/delay.h>
#include <avr/interrupt.h>

volatile char x=1, nova_zprava=0;

int main(void){
_delay_us(500); // master by měl odeslat svou zprávu až si bude jist že jsou všichni slave připraveni
DDRB = (1<<DDB0); // XCK jako výstup - jsme master a generujeme clock
UBRRH=0;
UBRRL=7;  // baudrate 500Bd/s
UCSRB =  (1<<RXEN) | (1<<TXEN);
// 1 stop bit, žádná parita, 8bit zpráva, synchronní operace s UCPOL=0
UCSRC = (1<<URSEL) | (1<<UMSEL) | (1<<UCSZ1) | (1<<UCSZ0); 
UCSRB |= (1 << RXCIE); // povolení přerušení od přijímače
sei();

// odešli první zprávu
while (!( UCSRA & (1<<UDRE)));
UDR = x;

while(1){
 if(nova_zprava){
  nova_zprava=0;
  x=x+1; // pošli zpět přijatá data o jedničku větší
  _delay_us(20);
  while (!( UCSRA & (1<<UDRE))); // počkej až budeš moct vysílat
  UDR = x; // odvysílej
  }
 }
}

ISR(USART_RXC_vect){
 x = UDR; 
 nova_zprava=1;
}

Závěr

Doufám, že jste v sadě příkladů našli informace které potřebujete a že jste získali jistou sebedůvěru v ovládání USARTu. Pokud vám tutoriál posloužil a nebo pokud odhalíte nějakou chybu, nechte prosím vzkaz v komentářích. Všechny zdrojové kódy jsem testoval, takže by na ně mělo být spolehnutí, ale může se stát, že mi někde při kopírování ujela ruka a do textu se dostala nějaká chybná verze.

Zdroje a relevantní odkazy