Úvod

Návod na použití jednotky UART má formu komentovaných příkladů. V každém příkladu vysvětlím tu funkci nebo část periferie, která je k jeho demonstraci nutná. Budu předpokládat, že příklady čtete všechny od začátku, takže se budu snažit nic nevysvětlovat dvakrát. Připadá mi to jako ten nejvhodnější kompromis mezi tím co si myslím, že se chcete dozvědět a tím co sám umím (a že toho zatím není mnoho). Doufám, že to nebude na škodu. Když přece jen vzniknou nějáké nejasnosti, budete si muset jejich rozuzlení najít v datasheetu. Což nemusí být zrovna nejsnazší. Dokumentace totiž nedosahuje kvalit jako u čipů řad Atmega a Attiny. Není to nutné, ale výhodou pro vás bude pokud se budete orientovat v

Pokud si potřebujete UART obecně trochu zopakovat, můžete to zkusit v tutoriálu pro AVR.

Možnosti USART

Pokud přecházíte z čipů Atmega, nečeká vás při seznamování s USARTem moc práce. Principálně je totiž podobný. Zvládá asynchronní přenos s datovou rychlostí až 4Mb/s a synchronní přenos s 16Mb/s. Formát zprávy lze měnit v rozsahu 5-9 bitů, 1-2 stop bity a lze nastavit paritu. Většinou si ale vystačíte s klasickým formátem 8N1 (8datových bitů, 1 stop bit, žádná parita). Posunu k lepšímu doznal Baudrate generátor, který je nyní zlomkový a je schopen vytvořit defakto libovolnou datovou rychlost bez ohledu na takt procesoru. Takže už nemusíte kvůli vyšším datových rychlostem volit takt na podivných frekvencích jako 9,216MHz nebo 1,8432MHz. USART umí krom běžných režimů pracovat také jako "one-wire", tedy komunikovat po jedné lince, umí komunikovat pomocí SPI a zvládá i IrDA. Mimo to lze využívat MCPM mód a s pomocí XCL modulu lze tvořit Manchaster kódování. Přirozeně většinu výše zmíněných funkcí jsem nikdy nevyužil a u mnohých z nich si ani nedovedu představit jak bych je využít mohl :D Ale pokud se k některé z nich během své praxe dostanu, určitě návod doplním.

Seznam příkladů

A - Odesílání s Printf (Atxmega E)

Prosté odesílání bude asi to nejtypičtější co budete po UARTu chtít. Pokud vám to aplikace dovolí, skoro jistě budete chtít využít komfortu funkcí printf. Tak pojďme na to. Z datasheetu k Atxmega32E5 jste se dočetli že máte dvě USART jednotky. Pojmenované USARTC0 a USARTD0. USARTC0 má k dispozici vývody na portu C a druhý USART analogicky na portu D. V druhém datasheetu v sekci "Alternate Functions" se dočtete kde příslušné piny jsou. Piny příslušející USARTC0 mají označení XCK0 (clock pro práci v synchronním módu), RXC0 a TXC0. A mohou se nacházet buďto na trojici PC1,PC2 a PC3 a nebo PC5,PC6 a PC7. Která z těchto skupin bude platná rozhoduje obsah REMAP registru portu C. Bitem USART0 můžete volit, kterou trojici budete používat. S USARTem na portu D je to analogické. Já ve svém příkladu používám USARTD0 a jako Tx mi slouží PD7. Tento pin by přirozeně měl být nastaven jako výstup.

Vypořádali jsme se s porty a teď je nutné vyřešit otázku Baudrate. Její hodnota se odvíjí od taktu čipu (F_CPU) a od dvou hodnot BSEL a BSCALE. Všechny potřebné matematické vztahy najdete v tabulce 21-1. Pokud budete volit hodnoty ručně snažte se víc pracovat s BSEL (12bitů) a méně s BSCALE (znaménkové 4bity). Vzhledem k omezenému množství kombinací (přibližně 65536) není problém napsat si program, který hrubou silou spočte všechny kombinace a vybere tu, která nejlépe vyhovuje vámi zvolenému Baudrate. Podobnou aplikaci někdo napsal a můžete ji nalézt zde. Pokud budete Atmel provozovat na 32MHz, můžete využít tabulku 21-6 v datasheetu, kde jsou hodnoty BSEL a BSCALE pro typické komunikační rychlosti. Opět si povšimněte že má dva sloupečky, jeden pro běžný provoz a druhý pro "double speed". Režim přepnete bitem CLK2X. Při práci s BSCALE nezapomeňte že jde o 4 bitové znaménkové číslo. Vše najdete v registrech BAUDCTRLA a BAUDCTRLB. My budeme používat baudrate 115200 b/s.

Poslední co zbývá je konfigurace formátu zprávy a pracovního režimu. Formát zprávy volíme klasicky 8N1, tedy 8 datových bitů, jeden stop bit a žádnou paritu. Nastavení se provádí v registru CTRLC. Nakonec už stačí jen v registru CTRLB zapnout vysílač (bitem TXEN). V tom okamžiku UART přebírá kontrolu nad pinem Tx (PD7). Samotné odesílání dat už probíhá klasicky. Stačí zapsat do registru DATA. USART na vysílací straně obsahuje dvojitý buffer. Takže jakmile USART zahájí vysílání, už je datový registr prázdný a může být zaplněn další hodnotou. O tom zda je datový registr volný vás informuje vlajka DREIF. Pokud ji nebudete respektovat a zapíšete do datového registru i přes to že je plný, tak se jeho obsah nepřepíše. Všimněte si proto že odesílací funkce nejprve počká až se datový registr vyprázdní a teprve potom do něj naloží nový znak.

Všechny ostatní funkce které se starají o natavení odesílací funkce uart_putchar() na standardní výstup jsou pro mě spíše kouzelné formule, takže vám jejich podstatu neobjasním. Připomenu ještě, že v knihovně stdio.h pro AVR naleznete krom printf i její varianty pro práci s řetězci v paměti programu (Flash). V příštích příkladech je určitě budeme používat. Aby jste je mohli používat je vhodné includovat ještě knihovnu pgmspace.h. Málem bych zapomněl na funkci clock_init(). Ta slouží k nastavení clocku čipu. Využívá interního 32MHz oscilátoru. Který není nejpřesnější, ale naštěstí je jeho chyba typicky tak malá, že při komunikaci neprojevuje.

//A) USART - odesílání s Printf (s podporou PGM), Tx at PD7
#define F_CPU 32000000
// baudrate 115200
#define BSEL 131 // Parametry baudrate
#define BSCALE -3 

#include <avr/io.h>
#include <avr/pgmspace.h>
#include <stdio.h>

static int uart_putchar(char c, FILE *stream); // odesílání znaku
void clock_init(void); 
void usart_init(void);
static FILE mystdout = FDEV_SETUP_STREAM (uart_putchar, NULL, _FDEV_SETUP_WRITE); // kouzlo (stdio)
volatile int x=0; // něco zajímavého k odesílání 

int main(void){
	stdout = &mystdout; // mapujeme standartní výstup na USART
	clock_init(); // interní oscilátor 32MHz 
	usart_init(); 
	
    while (1){
		printf("%i\r\n",x); // pošle hodnotu x do PC
		x++;
    }
}

static int uart_putchar (char c, FILE *stream){
	while ( !( USARTD0.STATUS & USART_DREIF_bm)); // čekej než bude Data Registr Empty
	USARTD0.DATA = c; // pošli znak
	return 0;
}

void usart_init(void){
	PORTD.DIR = PIN7_bm; // Tx (PD7) je výstup
	PORTD.OUTSET = PIN7_bm; // Tx do log.1 - není nutné
	PORTD.REMAP = PORT_USART0_bm; // Remaptujeme Tx na pin PD7
	USARTD0.BAUDCTRLA = (char)BSEL; // dolních 8 bitů 12bitové hodnoty BSEL
  // Horní 4 bity hodnoty BSEL + hodnota BSCALE
	USARTD0.BAUDCTRLB = (BSCALE << USART_BSCALE_gp) | ((char)(BSEL>>8) & 0x0f); 
	USARTD0.CTRLC = USART_CHSIZE_8BIT_gc; // 1 stopbit, žádná parita, 8 datových bitů, asynchronní mód
	USARTD0.CTRLB = USART_TXEN_bm; // Povolit vysílač (Tx)
}

void clock_init(void){
	OSC.CTRL |=OSC_RC32MEN_bm; // spustit interní 32MHz oscilátor
	while (!(OSC.STATUS & OSC_RC32MRDY_bm)){};	// počkat než se rozběhne
	CCP=CCP_IOREG_gc; // odemčít zápis do registru CLK.CTRL
	CLK.CTRL = CLK_SCLKSEL_RC32M_gc; // vybrat 32MHz jako systémový clock
}

B - Příjem řetězců s přerušením + printf/scanf (Atxmega E)

Mnoho aplikací vyžaduje nejen odesílání ale i příjem zpráv. Pokud čip komunikuje s uživatelem pomocí terminálu je žádoucí aby komunikace probíhala na bázi řetězců. Jinak řečeno aby byly příkazy "čitelné". Taková forma předávání informací má své pro a proti. Výhodou textové komunikace je možnost posílat "potvrzovací" znaky jako je například stisk klávesy Enter. Zprávy pak mohou být různě dlouhé a přijímači (Atmelu) je vždy jasné které z příchozích znaků tvoří zprávu. A přesně takový příklad si teď předvedeme. Zprávu bude vždy tvořit sekvence znaků ukončená znakem '\n' (0x0A) nebo '\r' (0x0D). Zpráva se ukládá do paměti a je omezena délkou 16 znaků. Tu si přirozeně můžete zvolit různou. Díky tomu napíše uživatel v terminálu zprávu a potvrdí ji Enterem. Mám trochu zmatek v tom který znak je odeslán při stisku klávesy Enter. Mám za to že to závisí na terminálovém programu a operačním systému. Proto ve svém programu oba znaky považuji za ukončovací. A teď k samotnému programu.

Konfiguraci baudrate i nastavení pinů jsem komentoval v předchozím příkladě. Protože zde ale používáme přijímač, musíme nakonfigurovat i pin Rx (PD6) a to jako vstup. A protože příjem provádíme pomocí přerušení, měli bychom si o něm stručně povědět. Přerušení v čipech Atxmega má tři priority - High, Medium a Low. Každé periferii, které zapnete přerušení zároveň i vyberete prioritu. Přerušení se vždy nastavuje dvojicí bitů. Ta nám dává čtyři možnosti - vypnuto a nebo jednu ze tří výše zmíněných priorit. USART má k dispozici tři zdroje přerušení (ve skutečnosti jich je spíš pět, ale nebudeme to komplikovat):

My budeme logicky používat poslední z nich. Padla zmínka o tom, že přijímač má nějáký buffer. USART vždy přijatá data uloží do bufferu. Ten je schopen uchovat dva byty dat. Váš program má díky tomu dost času si data vyzvednout. Vyzvednutí není nic jiného než čtení z DATA registru. Data vyčítá v takovém pořadí v jakém byla do bufferu ukládána. Je potřeba poznamenat že spolu s daty buffer obsahuje i stavové vlajky (registr STATUS). Pokud vás tedy zajímají, musíte je přečíst vždy před čtením z registru DATA. Vlajky vás informují o stavu USARTu. Najdete v nich všechny tři informace, které mohou vyvolat přerušení a ještě některé další jako třeba Frame Error, který naznačuje chybu komunikace. Nebo také Buffer Overflow, která vás informuje o tom, že jste si včas nevyzvedli přijatá data a USART musel nově příchozí byte zahodit. Vlajky přirozeně můžete (a někdy i musíte) mazat ručně (zápisem log.1). Vlajka RXCIF se maže automaticky čtením z DATA registru a analogicky vlajka DREIF se maže zápisem do DATA registru. Vraťme se ale k přerušením. Nastavit je můžete v registru CTRLA. To také v programu provedeme a nastavíme příjmu prioritu medium. Každý level přerušení lze pak globálně povolovat nebo zakazovat (v registru PMIC.CTRL). Takže náš program přirozeně musí medium level přerušení povolit a pak ještě povolit všechna přerušení globálně pomocí funkce sei().

Přijímací rutina sbírá znaky do pole docasne[] tak dlouho než přijde ukončovací znak, nebo zpráva dosáhne limitní délky. Na její konec přidá ukončovací znak 0 aby se s polem dalo pracovat jako s řetězcem. Pak ji zkopíruje do pole zprava[] aby si tak uvolnila pole docasne[] k přijmu další zprávy. A skrze globální proměnnou nova_zprava informuje hlavní vlákno programu že došlo k přijmu celé zprávy. Hlavní vlákno nemá na práci nic jiného než čekat na novou zprávu. Jakmile dorazí pokusí se ji pomocí funkcí scanf() dekódovat. Zprávu očekává ve tvaru "c=13245\n". Všechny funkce scanf/printf jsou modifikované pro práci s řetězci z paměti flash. Proto jsou zapsány s příponou "_P" a řidící řetězec je uzavřen do makra PSTR(). Tento přístup není nutný, ale šetří paměť. Zvláště u rozsáhlejších řetězců by se vyplatil. Vše ostatní je shodné s předchozím příkladem a nestojí za komentář.

// B) USART Příjem řetězců s přerušením + managment zpráv pomocí printf/scanf s podporou PGM

#define F_CPU 32000000
#include <avr/io.h>
#include <avr/pgmspace.h>  // podpora pro práci s Flash pamětí
#include <avr/interrupt.h>
#include <stdio.h> // Printf a ekvivalenty
#include <string.h> // Pro práci s řetězci (např strncpy)

#define BSEL 131 // parametry baudrate 115200
#define BSCALE -3
#define DELKA_ZPRAVY 16 // maximalní délka zprávy

void clock_init(void); // inicializace clocku
void usart_init(void); // inicializace USART modulu
static int uart_putchar(char c, FILE *stream); // funkce odesílání znaků
static FILE mystdout = FDEV_SETUP_STREAM (uart_putchar, NULL, _FDEV_SETUP_WRITE); // kouzlo

volatile char nova_zprava=0; // informuje o dokončení příjmu nové zprávy
volatile char zprava[DELKA_ZPRAVY]; // tady bude uložena celá přijatá zpráva
uint32_t x; // nějáká proměnná na hraní

int main(void){
	stdout = &mystdout; // namapování Printf na USART
	clock_init(); // interních 32MHz
	usart_init();
	PMIC.CTRL = PMIC_MEDLVLEN_bm; // povolit všechna medium level přerušení
	sei(); // globální povolení přerušení
	
	while (1){
		if(nova_zprava){ // pokud přišla nová zpráva
			if(sscanf_P(zprava,PSTR("c=%lu"),&x)){ // pokud zpráva odpovídá formátu
			printf_P(PSTR("Nacteno %lu\n\r"),x); // pro ověření odešleme načtenou hodnotu
			printf_P(PSTR("Dvojnasobek %lu\n\r"),x*2); // a něco navíc 
			}
			else{printf_P(PSTR("Error\n\r")); // jinak vynadáme uživateli
			}
			nova_zprava=0; // zpráva je zpracována, budeme čekat na novou
		}
	}
}

ISR(USARTD0_RXC_vect){
	char prijaty_znak;
	static char docasne[DELKA_ZPRAVY]; // dočasné místo pro přicházející zprávu
	static uint8_t i=0; // počítadlo přijatých znaků
	prijaty_znak = USARTD0.DATA; // 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' - ukončení řetězce
		strncpy(zprava,docasne,DELKA_ZPRAVY); // předáme přijatou zprávu hlavní smyčce (v poli "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
	}
}

static int uart_putchar (char c, FILE *stream){
	while ( !( USARTD0.STATUS & USART_DREIF_bm)); // počkej dokud není odesílací registr prázdný
	USARTD0.DATA = c; // pošli znak
	return 0;
}

void usart_init(void){
	PORTD.DIRSET = PIN7_bm; // PD7 Tx výstup
	PORTD.DIRCLR = PIN6_bm; // PD6 Rx vstup
	PORTD.OUTSET = PIN7_bm; // log.1 na Tx // není nutné
	PORTD.REMAP = PORT_USART0_bm; // Remapujeme Tx na PD7 a Rx na PD6
	USARTD0.BAUDCTRLA = (char)BSEL; // dolních 8 bitů z 12bit hodnoty z BSEL
	USARTD0.BAUDCTRLB = (BSCALE << USART_BSCALE_gp) | ((char)(BSEL>>8) & 0x0f);
	USARTD0.CTRLC = USART_CHSIZE_8BIT_gc; // 1 stopbit, no parity, 8 data bits, asynchronni mod
	USARTD0.CTRLB = USART_TXEN_bm | USART_RXEN_bm; // povolit vysílač i přijímač
	USARTD0.CTRLA = USART_RXCINTLVL_MED_gc; // povolit přerušení od přijímače s prioritou medium
}

void clock_init(void){
	OSC.CTRL |=OSC_RC32MEN_bm; // spustit interní 32MHZ oscilátor
	while (!(OSC.STATUS & OSC_RC32MRDY_bm)){};	// počkat než se rozběhne
	CCP=CCP_IOREG_gc; // odemčít zápis do CLK_CTRL
	CLK.CTRL = CLK_SCLKSEL_RC32M_gc; // vybrat 32MHz jako systémový clock
}

C - odesílání s DMA (Atxmega E)

Pro ty, kteří ještě neměli tu čest pracovat s DMA, bude tento příklad asi obzvlášť zajímavý. DMA je totiž mocný nástroj, který v prvé řadě může ve vhodných situacích citelně zvýšit výkon mikrokontroléru. A krom toho může také v mnohých situacích zjednodušit program. Umí totiž bez intervence jádra (a tedy i programu) přesouvat data mezi pamětí a periferiemi (případně mezi pamětí a pamětí a nebo periferií a periferií). Technicky vzato mu stačí říct odkud kam má data přesouvat, po jakých balících a kdy to má dělat. Pak už ho jen spustíte a on se o celý transport dat postará. Nastíním jednoduchou situaci, kdy vám DMa výrazně usnadní život. Představte si že využíváte DA převodník na plný výkon. Tedy situaci kdy generujete dejme tomu na jednom kanále signál s rychlostí 1Msps (tedy 1 milion vzorků za sekundu). V takovém případě musíte DA převodníku každou us přenést nových 12bitů dat. to je i při taktu 32MHz v podstatě limitní záležitost. Na provedení celé akce máte jen 32us tedy při 32MHz jen 32 strojových cyklů. Pokud se vám vůbec podaří napsat program, který to bude stíhat, už mu určitě nezbude čas na cokoli dalšího. Představa že si pomůžete využitím přerušení je také zcestná protože vstup a výstup z rutiny přerušení spolkne několik (až 9) už tak chybějících strojových cyklů. A tohle je přesně situace kde se DMA uplatní ze všeho nejvíce. DMA samo čeká až si DA převodník sám řekne o nová data, v nejkratší možné době mu je přenese a opět čeká. Jádro mezi tím může vykonávat nerušeně program, protože celý transport dat z paměti do DA převodníku probíhá na pozadí. Protože transport vyžaduje přenos dvou bytů (DA převodník je 12bitový)) každou us, blokuje DMA komunikaci s RAM pouze 2 takty ze 32, vytížení RAM je tedy jen něco přes 6% a vytížení jádra nulové. Nebudu zastírat, že celá věc je o něco málo komplikovanější. K RAM nemůžou přistupovat zároveň DMA i jádro, jádro má prioritu a pokud tedy vykonává nějákou hodně nevhodnou činnost, může přístup k RAM zablokovat na dost dlouho aby DMA nestihlo přenést potřebná data k DA převodníku. Ale to jsou vyloženě vyjimečné situace. Protože se ale nyní zabýváme USARTem, ukážeme si jak lze DMA využít k odesílání dat.

Podobná situace jakou jsem výše nastínil s DMA převodníkem vás může čekat i při odesílání USARTem. Situace je tím tíživější čím vyšší datovou rychlost používáte. Nejvyšší datové rychlosti dosahuje USART v synchronním režimu, tam může chrlit data rychlostí až 16Mb/s. Vezmeme-li v úvahu že jedna zpráva ve formátu 8N1 zabírá 10 bitů, musí být data přesunuta do USARTu každých 0.625us. Tedy na přípravu jedné zprávy by jádro mělo jen něco okolo 20 strojových cyklů. Zase platí, že by se při takové činnosti nemohlo věnovat čemukoli dalšímu. Při větším objemu přenášených dat, by tak došlo k zablokování programu na celou dobu odesílání. A i tak by jste museli věnovat značnou péči zdrojovému kódu aby celou akci stíhal. Protože my synchronní přenos použít nechceme (nemáme hardware, kterým by ho bylo možné do PC přijímat), ukážeme si spolupráci DMA a USART na běžném asynchronním přenosu. Přirozeně si zvolíme nějákou slušně vysokou datovou rychlost. Třeba občas používanou 921600b/s.

Konfigurace USART je shodná jako v předchozích příkladech, takže ji není třeba více komentovat. Snad jen poznamenám že v hlavičce je zakomentovaná konfigurace pro přenosovou rychlost 2Mb/s (hodnoty BSEL a BSCALE), můžete ji v případě zájmu také vyzkoušet. Stejně tak nebudu komentovat ani konfiguraci clocku - čip taktujeme na 32MHz. Komentář is zaslouží pouze příprava DMA. Někde v paměti máme připravené pole data, která budeme odesílat. Jedná se o proměnnou test_data[], kterou pomocí funkce snprintf vyplníme textovou zprávou. Největší pozornost budeme muset věnovat nastavnení DMA. Čipy řady Atxmega E mají jiné DMA než ostatní řady (A,AU,B,C) a nese označení EDMA. Může pracovat ve dvou principiálních režimech tzv "periferním" (Peripheral channel) a nebo "standardním" (Standard channel). rozdíl je v tom, že periferní režim může přenášet data pouze mezi periferií a pamětí, kdežto standardní režim mezi čímkoli. Směr přenosu si v periferním režimu uživatel nemůže specifikovat a je dána volbou "requestu" (viz dále). Ve standardním režimu můžete používat celkem dva kanály, v periferním režimu čtyři. My konfigurujeme EDMA v periferním režimu. Dále je potřeba nastavit priritu kanálů. Pokud chtějí dva kanály přistupovat k RAM zároveň, musí kanál s nižší prioritou počkat. Vzhledem k tou že používáme jen jeden kanál (CH0), je nám to celkem jedno a tak používáme "spravedlivou" prioritu "round robin" (každý jednou). Nastavení všech čtyř kanálu je nezávislé, takže všechno co v příkladu nebo zde v textu uvidíte, platí pro každý kanál individuálně. V prvé řadě EDMA musíte vysvětlit jak probíhá triggrování (spouštění). Jsou dvě možnosti, buď na pokyn přenést naráz celý balík paměti (tedy celé pole test_data[]), nebo na jeden pokyn přenést vždy jen krátkou dávku (1 nebo 2 byty). Druhá možnost, nazývaná "one-shot" a nastavovaná bitem SINGLE, je přesně to co potřebujeme. Dále je potřeba nastavit kdo dává DMA pokyny k přenosu (tzv DMA request). V našem případě je to USARTD0_DRE. Jakmile je datový registr USARTu prázdný, dá pokyn DMA a to přenese další byte z paměti do USART. Logicky musíme DMA specifikovat, kterou oblast paměti má odesílat. Adresu mu nastavíme v registru ADDR. DMA také musíme nastavit, že má adresu v paměti postupně inkrementovat (jinak by stále přenášel první byte zprávy). Poslední podstatný parametr je počet přenášených bytů (tedy defakto délka pole test_data[]). Ten musíme DMA také sdělit. Po jeho přenesení se DMA kanál vypne, na další "requesty" nereaguje a vysílání skončí. Bylo by možné nastavit tzv "repeat" mód, pak by DMA po skončení začalo přenášet znovu. V takovém případě by ale bylo nutné specifikovat, že se musí adresa paměti (DMA ji inkrementuje) po skončení přenosu znovu načíst (na původní hodnotu - první prvek pole test_data[]). My to v příkladu nastavujeme, ale repeat mód nepoužíváme. Raději zopakuji, že DMA přenos je ukončen z vůle EDMA, nikoli z vůle USARTu. USART i po skončení přenosu stále vysílá DMA request, že je jeho DATA registr prázdný. DMA ale na jeho výzvy po skončení přenosu nereaguje, protože je příslušný kanál vypnutý. Dokončení celého přenosu je signalizováno vlajkou TRNIF. Spuštění příslušného kanálu se provádí nastavením ENABLE bitu v CTRLA registru. V našem příkladu vždy spustíme DMA, počkáme na dokončení přenosu (sledujeme vlajku TRNIF). Po skončení vlajku vymažeme, připravíme nová data a celou akci zopakujeme. Tento způsob použití DMA není praktický, protože vůbec nevyužíváme toho, že nám DMA uvolnilo ruce k jiné činnosti. Typicky tedy DMA spustíte a budete vykonávat nějakou smysluplnější činnost než jen čekání na to až skončí. Pokud to budete potřebovat, může DMA po skončení vyvolat přerušení a vy pak nemusíte zbytečně zjišťovat zda přenos ještě probíhá nebo už je dokončen. Následuje zdrojový kód.

// C) USART - odesílání s DMA

#define F_CPU 32000000
#include <avr/io.h>
#include <avr/pgmspace.h>  // není nutné
#include <util/delay.h>
#include <stdio.h>

void clock_init(void); // inicializace clocku
void usart_init(void); // inicializace USARTu (Tx na PD7)
void DMA_init(void);

#define BSEL 75 // parametry baudrate (921600 b/s)
#define BSCALE -6
//#define BSEL 0 // parametry baudrate (2Mb/s)
//#define BSCALE 0
#define LEN 10 // délka bloku odesílaných dat

char test_data[LEN] = {0}; // pole k odeslání
int16_t x; // pracovní proměnná

int main(void){
	clock_init(); // interní 32MHz
	usart_init();
	DMA_init();
	
	while (1){
		EDMA.CH0.CTRLA |= EDMA_CH_ENABLE_bm; // povolit channel 0
		while(!(EDMA.CH0.CTRLB & EDMA_CH_TRNIF_bm)){} // počkat na dokončení přenosu - lze využít i přerušení
		EDMA.CH0.CTRLB |= EDMA_CH_TRNIF_bm; //  smaž vlajku dokončení přenosu
		snprintf(test_data,LEN-1,"x=%i\r\n",x); // připrav nová data
		x++; // posíláme něco zajímavého
		_delay_ms(100); // počkej 100ms a pošli znovu
	}
}

void DMA_init(void){
	// konfigurace EDMA
	EDMA.CTRL = EDMA_CHMODE_PER0123_gc | EDMA_PRIMODE_RR0123_gc; // kanály v režimu "peripheral", priorita round-robin
	EDMA.CTRL |= EDMA_ENABLE_bm; // spustit EDMA
	// channel 0 - Peripheral mode
	EDMA.CH0.CTRLA = EDMA_CH_SINGLE_bm; // odesíláme 1 byte na 1 trigger
	EDMA.CH0.ADDRCTRL = EDMA_CH_RELOAD_TRANSACTION_gc | EDMA_CH_DIR_INC_gc; // znovunačíst adresu paměti až po skončení celého přenosu
	EDMA.CH0.TRIGSRC = EDMA_CH_TRIGSRC_USARTD0_DRE_gc; // periferie se kterou probíhá výměna dat
	EDMA.CH0.TRFCNTL = LEN; // kolik dat přenášíme...
	EDMA.CH0.ADDR = (uint16_t)(test_data);  // ...a odkud (respektive kam) 
}

void usart_init(void){
	PORTD.DIR = PIN7_bm; // Tx (PD7) je výstup
	PORTD.OUTSET = PIN7_bm; // Tx do log.1 - není nutné
	PORTD.REMAP = PORT_USART0_bm; // Remaptujeme Tx na pin PD7
	USARTD0.BAUDCTRLA = (char)BSEL; // dolních 8 bitů 12bitové hodnoty BSEL
	// Horní 4 bity hodnoty BSEL + hodnota BSCALE
	USARTD0.BAUDCTRLB = (BSCALE << USART_BSCALE_gp) | ((char)(BSEL>>8) & 0x0f);
	USARTD0.CTRLC = USART_CHSIZE_8BIT_gc; // 1 stopbit, žádná parita, 8 datových bitů, asynchronní mód
	USARTD0.CTRLB = USART_TXEN_bm; // Povolit vysílač (Tx)
}

void clock_init(void){
	OSC.CTRL |=OSC_RC32MEN_bm; // spustit interní 32MHz oscilátor
	while (!(OSC.STATUS & OSC_RC32MRDY_bm)){};	// počkat než se rozběhne
	CCP=CCP_IOREG_gc; // odemčít zápis do registru CLK.CTRL
	CLK.CTRL = CLK_SCLKSEL_RC32M_gc; // vybrat 32MHz jako systémový clock
}

Dodatek

Na obrázku C si můžete prohlédnout jak probíhá přenos. Bystřejší z vás si jistě všimnou, že přenášíme zbytečně moc bytů. Zpráva obsahuje 10 bitů, ale užitečných je jen 8. Správnější verze příkladu, by měla využít návratovou hodnotu funkce snprintf(). Ta by měla vracet číslo odpovídající celkovému počtu znaků v textu. To bychom mohli využít a nastavit délku přenosu EDMA na délku konkrétního textu. Nastavení provádíme v registru TRFCNTL a může nabývat hodnot 1-256. Pro hodnotu 256 je potřeba do registru zapsat nulu. Ti zájemci, kteří by chtěli příklad tímto způsobem modifikovat si musí ohlídat návratovou hodnotu funkce snprintf. Nesmí překročit víše zmiňovaných 256 a krom toho snprintf může v případě nějakého problému také vracet zápornou hodnotu a tu by přirozeně nebylo vhodné nikam zapisovat. Vylepšené odesílání si můžete prohlédnout v příkladu E, kde demonstruji použití DMA na Atxmega řady AU.


Obrázek C - USART s DMA (926100b/s), celý přenos 10 bytů zabere přibližně 110us

D - SPI mód odesílání 16bit s pollingem (Atxmega E)

USART na čipech Atxmega umí pracovat jako SPI master. Přirozeně se hned můžete zeptat, jaký to má smysl, když pro SPI má Atxmega jinou periferii ? Správnou odpověď neznám. Možná proto aby jste měli vícero SPI rozhraní. Důležité je ale vědět, že se SPI a SPI realizované pomocí USART v několika věcech liší. Běžné SPI obsahuje jeden posuvný registr pro příjem i vysílání, takže je většinou možné SPI zařízení řetězit za sebe (tzv daisy chain). Existence pouze jediného datového registru, znemožňuje jeho "bufferování". A v tom se právě SPI realizované USARTem liší od běžného SPI. SPI realizované USARTem není možné řetězit, ale má k dispozici vyrovnávací buffer (o něm už byla řeč v předchozích příkladech). Vyrovnávací buffer ulehčuje programu, který nemusí tak rychle reagovat na odeslání dat a přenos je plynulejší. Osobně mám ale dojem, že to je čistě estetická stránka věci a moc si nedovedu představit situaci kdy by výhoda vyrovanávacího bufferu nějak výrazně zkvalitnila komunikaci. I přes to si v následujícím příkladu předvedeme jak USART v režimu SPI používat.

Před komunikací je přirozeně potřeba vhodně nakofigurovat piny. Jako clock slouží výstup synchronní verze USARTu XCK (PD5), jako MOSI potom poslouží Tx pin (PD7) a jako MISO vývod Rx (PD6). Dále si připravíme výstup pro CS (PD4). První změny si všimnete v konfiguraci Baudrate. Vzhledem k tomu že jde o synchronní režim, je její výpočet výrazně jednodušší. My se pokusíme z periferie vyždímat co nejvíc a volíme tedy BSEL=0, čímž nastavujeme přenosovou rychlost na 16Mb/s. SPI může pracovat ve čtyřech módech (více zde), jejich volbu provádíme nastavením bitu UCPHA v registru CTRLC a případným invertováním výstupu (XCK). Invertování výstupu se provádí bitem INVEN v příslušném konfiguračním registru příslušného pinu (v našem případě v registru PIN5CTRL). Způsob konfigurace je uveden v tabulce 21-2 v datasheetu. Pořadí bitů ve zprávě se pak nastavuje bitem UDORD (my jej necháme vynulovaný a budeme vysílat jako první MSb). Tyto bity ale nejsou v hlavičkovém souboru definovány, pro přehlednost jsem je dodefinoval přímo ve zdrojovém kódu. Příklad používá mód 0, pokud chcete použít mód 3 odkomentujte si příslušné řádky v inicializační funkci usart_init().

Trochu více pozornosti budeme věnovat samotné odesílací funkci uart_spi(). Ta totiž využívá vyrovnávacího bufferu jak na vysílací tak na přijímací straně. Jakmile je funkce zavolána, nejprve zjistí zda je v odesílacím bufferu volno a případně počká než se vyprázdní. Poté aktivuje slave obvod pomocí pinu CS a nahraje do bufferu první data. Data se typicky okamžitě začnou vysílat a buffer se opět vyprázdní. Funkce ihned naloží i druhou polovinu dat (odesíláme 16bitů, MSB jako první). Pak počká na dokončení celého přenosu a deaktivuje slave obvod (CS do log.1). Teprve pak funkce přečte oba nově příchozí byty. To si může dovolit právě díky vyrovnávacímu bufferu u přijímače (který je schopen pojmout právě tyto dva byty a ještě další začít přijímat). Ty pak funkce složí do 16bitového čísla a vrátí. Nezapomene přirozeně po práci ještě smazat vlajku TXCIF. Tento způsob použití drobně zkrátí chod celé funkce. Pokud by ale k USARTu přistupovala nějaká další funkce a "neuklidila" by po sobě přijímací buffer (tedy nevyzvedla z něj data), měli bychom trochu problém. Jistější by tedy bylo před samotným vysíláním, zkontrolovat zda v přijímacím bufferu nezůstala nějaká nevyzvednutá data. A pokud ano tak buffer vyčistit (třeba přečtením těchto nevyzvednutých dat). Když se podíváte na záznam komunikace z osciloskopu (obrázek D), můžete vidět že už je to docela fofr. Za přibližně 1.5us je celé vysílaní vyřízené. A vzhledem k tomu, že valná většina SPI zařízení má maximální komunikační rychlosti vyšší jak 10MHz, otevírají se vám zajímavé možnosti. Představte si třeba, že tento příklad upravíte aby využíval DMA. V takovém případě budete schopni odesílat datový tok přibližující se k 2MB/s. Zajímavé to může být třeba tehdy, když vám nestačí RAM na čipu. Pak můžete sáhnout po levné externí SRAM s SPI rozhraním. Datová rychlost by měla stačit ukládat data z ADC převodníku běžícího na plné rychlosti (1Msps). Nebo může posloužit naopak, při čtení dat ze "sériové" RAM do DA převodníku. Toto řešení vás oproti klasické "paralelní" RAM nebude stát odhadem 20 nebo i více pinů, ale pouhé čtyři. Je to natolik zajímavá představa, že ji možná v některém z budoucích návodů prakticky vyzkouším. Zdrojový kód, najdete pod obrázkem.



Obrázek D - USART v režimu SPI, 16bit přenos MSb jako první. Datová rychlost 16Mb/s.

// D) USART - SPI mód s pollingem (16bit) - Xmega E
#define F_CPU 32000000
#include <avr/io.h>
#include <util/delay.h>

void clock_init(void); // inicializace clocku
void usart_init(void); 
uint16_t uart_spi(uint16_t datain);

#define BSEL 0 // přenosová rychlost (16Mb/s)
// definování bitů (chybí v iox32e5.h) - zlepšuje čitelnost
#ifndef USART_UCPHA_bm
#  define USART_UCPHA_bm 0x02
#endif
#ifndef USART_DORD_bm
#  define USART_DORD_bm 0x04
#endif

volatile uint16_t z;

int main(void){
	clock_init(); // interní 32MHz
	usart_init();
	
	while (1){
		z=uart_spi(0x0100); // posíláme 16bit data
		_delay_ms(50);		 
	}
}

void usart_init(void){
	// konfigurace I/O
	PORTC.OUTSET = PIN7_bm | PIN4_bm; // Tx do log.1, CS do log.1
	PORTC.DIRSET = PIN7_bm | PIN5_bm | PIN4_bm; // Tx (PD7),XCK (PD5), CS (PD4) výstupy
	PORTC.DIRCLR = PIN6_bm; // Rx (PD6) vstup
	PORTC.REMAP = PORT_USART0_bm; // Remapujeme Tx na pin PD7
	// konfiurace USART
	USARTC0.BAUDCTRLA = (char)BSEL; // konfigurace přenosové rychlosti
	USARTC0.BAUDCTRLB = ((char)(BSEL>>8) & 0x0f); // konfigurace přenosové rychlosti
	USARTC0.CTRLC = USART_CMODE_MSPI_gc; // mód 0
	//USARTD0.CTRLC = USART_CMODE_MSPI_gc | USART_UCPHA_bm; // mód 3
	//PORTD.PIN5CTRL |= PORT_INVEN_bm; // mód 3
	USARTC0.CTRLB = USART_TXEN_bm | USART_RXEN_bm; // Povolit vysílač i přijímač
}

void clock_init(void){
	OSC.CTRL |=OSC_RC32MEN_bm; // spustit interní 32MHz oscilátor
	while (!(OSC.STATUS & OSC_RC32MRDY_bm)){};	// počkat než se rozběhne
	CCP=CCP_IOREG_gc; // odemčít zápis do registru CLK.CTRL
	CLK.CTRL = CLK_SCLKSEL_RC32M_gc; // vybrat 32MHz jako systémový clock
}

uint16_t uart_spi(uint16_t datain){
  uint16_t byte0,byte1; // pro přijatá data

  while(!( USARTC0.STATUS & USART_DREIF_bm)); // počkej až bude možné naložit data
  PORTC.OUTCLR = PIN4_bm; // CS do log.0
  USARTC0.DATA = (uint8_t)(datain>>8); // nalož buffer daty
  while(!( USARTC0.STATUS & USART_DREIF_bm)); // počkej až bude možné naložit další data...
  USARTC0.DATA = (uint8_t)datain; // ...nalož je	
  while (!(USARTC0.STATUS & USART_TXCIF_bm)); // počkej až se všechno odešle
  PORTC.OUTSET = PIN4_bm; // CS do log.1
  byte0=USARTC0.DATA; // vyzvedni první příchozí byte (typicky nenese informaci)
  byte1=USARTC0.DATA; // vyzvedni druhý příchozí byte
  USARTC0.STATUS = USART_TXCIF_bm; // Smaž vlajku TXCIF
  return (((uint16_t)byte0)<<8) | (uint16_t)byte1; // vrať obě hodnoty ve formě uint16_t
}

E - odesílání s DMA (Atxmega AU)

Protože je DMA na čipech řady E dosti odlišné od ostatních členů rodiny Atxmega, zařadil jsem do tutoriálu tento "polopříklad". Neukážeme si na něm vlastně nic zásadně nového. Pouze si předvedeme jak nakonfigurovat USART s DMA přenosem na čipech řady AU. Samotná konfigurace USARTu, I/O i clocku je shodná. Jen DMA je poněkud plnohodnotnější. Vrhněme se tedy na to. U řady E pracoval EDMA buď v režimu "periferním" nebo "standardním". U "periferního" režimu tekly data pouze mezi periferií (která posílala requesty) a pamětí. Přicházeli jste tak o jistou míru volnosti. Nemohli jste libovolně specifikovat odkud a kam data potečou. Takové možnosti měl u řady E pouze "standardní" režim. Ostatní řady mají DMA, které takto svoje kanály nedělí. Dalo by se říct, že všechny kanály jsou "standarní". Tedy specifikujete odkud a kam data potečou libovolně a stejně tak nejsou žádné restrikce kladeny na to kdo bude posílat requesty a tedy rozhodovat o tom kdy dojde k přenosu dat. No a my si takovou konfiguraci ukážeme na Atxmega128A3U. Přirozeně čím více máte k dispozici možností, tím náročnější je správné nastavení DMA. Než se na něj vrhneme tak jen pro úplnost vyjasníme jak je to s nastavením USARTů, clocku a I/O. Je to totiž snadné, nastavují se úplně stejně jako u řady E. V tomto příkladu použijeme USARTC0 s vývodem TX na PC3. Clock jako obvykle zvolíme interní 32MHz. Ten bohužel není nejpřesnější (odchylka v jednotkách procent) a při vyšších datových rychlostech může způsobovat problémy. Tak to mějte na paměti až se budete divit, že vám do PC lezou místo znaků nesmysly ;)

Aby byla trochu legrace, pustíme si UART v asynchronním režimu na nejvyšší možnou rychlost, tedy na 4Mb/s. S dekódováním takového signálu můžou mít některé převodníky USB->UART problémy. Stíhají ho třeba FT232H a PL2303. A nestíhají například FT230X, FT232 nebo CP2102. Taky některé terminálové programy na PC mohou mít s takovou datovou rychlostí problémy. Já mám zatím ověřenou Putty, která stíhala i 12Mb/s. Nikdo vás však nenutí komunikovat pouze s PC, UART může klidně posloužit pro komunikaci mezi mikrokontroléry. Ještě připomenu, že v příkladu C (USART s DMA na čipech Xmega E), měla naše odesílací funkce malý nedostatek (odesílala zbytečně moc znaků) - tento nedostatek odstraníme. A teď hurá na konfiguraci DMA.

Na čipech řady AU máte k dispozici čtyřkanálové DMA. Kanály jsou nezávislé (včetně rutin přerušení) a mají některé nadstandardní funkce oproti EDMA na čipech řady E. Tou funkcí je například "double buffer" DMA, kdy se dva DMA kanály střídají a každý přenáší další balík dat. Jádro pak typicky zpracovává druhou polovinu dat. Na druhou stranu EDMA měl také své bonusy (možnost filtrace dat), ale tyto debaty si necháme na seriál o DMA. Konfigurace DMA je celkem přímočará. Je potřeba nastavit cílovou a zdrojovou adresu a počet přenášených bytů. Dále je potřeba specifikovat zda používáme "one-shot" mód, kdy se na jeden request odešle jedna dávka (burst). To je náš případ. Dále potřebujete specifikovat velikost dávky (1,2,4 nebo 8 bytů). A pak přijde to "nejtěžší". Správně nastavit která adresa se má inkrementovat, která ne a kdy se má znovu načíst její počáteční hodnota. My to nemáme moc těžké, USART potřebuje dostávat data po jednom bytu. Takže cílová adresa (USARTC0.DATA) se inkrementovat nebude a znovu načítat také ne. Zdrojová adresa se bude inkrementovat po každém odeslaném bytu (burst) a znovu se její hodnota načte po skončení celého přenosu. Velikost dávky bude 1 byte. Pak už jen nakonfigurujeme requesty. Ty budou přicházet od Data Register Empty. Tedy v okamžiku kdy je USART připraven přijmout další data. To je vše. Všimněte si že délku odesílaných data v příkladu měníme, podle toho kolik jich aktuálně potřebujeme odeslat. Na obrázku E pak můžete vidět, že už je to docela fofr a celá zpráva se vychrlí za méně jak 20us.


Obrázek E - UART na maximální rychlosti 4Mb/s


Obrázek E1 - Vývojová deska X3-DIL64 Leon Insturments

// E - USART odesílání s DMA (Xmega AU)
#define F_CPU 32000000
#include <avr/io.h>
#include <util/delay.h>
#include <stdio.h>

void clock_init(void);
void usart_init(void);
void DMA_init(void);
// pozor na přesnost clocku u tak vysokých baudrate !
#define BSEL 0 // parametry baudrate (4Mb/s) - double speed USART
#define BSCALE 0
#define LEN 20 // maximální délka řetězce

char odesli[LEN]={0}; // obsah tohoto pole budeme odesílat
int x=0,delka; // pomocné proměnné	

int main(void){
clock_init();
usart_init();
DMA_init();
while (1){
  x++; // něco zajímavého k odesílání
  delka=snprintf(odesli,LEN,"x=%i\n\r",x); // formátujeme zprávu
  if(delka>0 && delka<LEN){ // pokud formátování prošlo korektně
	 DMA.CH0.TRFCNT = delka; // nastavíme DMA délku odesílaného řetězce
	 DMA.CH0.CTRLA |= DMA_CH_ENABLE_bm; // povolit kanál 0 - spustit přenos
  }
  while(!(DMA.CH0.CTRLB & DMA_CH_TRNIF_bm)){} // počkáme na dokončení přenosu
  DMA.CH0.CTRLB |= DMA_CH_TRNIF_bm; // smažeme vlajku dokončení přenosu
  _delay_ms(100); // chvíli počkáme a jedeme znovu
  }
}

void DMA_init(void){
DMA.CTRL = DMA_ENABLE_bm; // povolit DMA
// nastavit channel 0
// cílová adresa pevná bez inkrementace, 
// zdrojovou adresu inkrementovat a znovu načíst až po skončení přenosu
DMA.CH0.ADDRCTRL = DMA_CH_SRCRELOAD_TRANSACTION_gc | DMA_CH_SRCDIR_INC_gc | DMA_CH_DESTRELOAD_NONE_gc | DMA_CH_DESTDIR_FIXED_gc;
DMA.CH0.DESTADDR0 = (((uint16_t) &USARTC0.DATA) >> 0) & 0xFF; // cílová adresa je
DMA.CH0.DESTADDR1 = (((uint16_t) &USARTC0.DATA) >> 8) & 0xFF; // DATA registru USARTu
DMA.CH0.DESTADDR2 = 0;
DMA.CH0.SRCADDR0 = (((uint16_t) (&odesli[0])) >> 0) & 0xFF; // zdrojová adresa je
DMA.CH0.SRCADDR1 = (((uint16_t) (&odesli[0])) >> 8) & 0xFF; // pole v němž skladujeme text
DMA.CH0.SRCADDR2 = 0;
DMA.CH0.TRFCNT = LEN; // bude se měnit...
DMA.CH0.TRIGSRC = DMA_CH_TRIGSRC_USARTC0_DRE_gc; // request pochází od USART0 Data Register Empty
DMA.CH0.CTRLA = DMA_CH_BURSTLEN_1BYTE_gc | DMA_CH_SINGLE_bm; // na jeden request, pošle jeden byte
}

void usart_init(void){
PORTC.DIRSET = PIN3_bm; // Tx (PC3) je výstup
PORTC.OUTSET = PIN3_bm; // Tx do log.1 - není nutné
USARTC0.BAUDCTRLA = (char)BSEL; // dolních 8 bitů 12bitové hodnoty BSEL
// Horní 4 bity hodnoty BSEL + hodnota BSCALE
USARTC0.BAUDCTRLB = (BSCALE << USART_BSCALE_gp) | ((char)(BSEL>>8) & 0x0f);
USARTC0.CTRLC = USART_CHSIZE_8BIT_gc; // 1 stopbit, žádná parita, 8 datových bitů, asynchronní mód
USARTC0.CTRLB = USART_TXEN_bm | USART_CLK2X_bm; // Povolit vysílač (Tx), zapnout double speed
}

void clock_init(void){
OSC.CTRL |=OSC_RC32MEN_bm; // spustit interní 32MHz oscilátor
while (!(OSC.STATUS & OSC_RC32MRDY_bm)){};	// počkat než se rozběhne
CCP=CCP_IOREG_gc; // odemčít zápis do registru CLK.CTRL
CLK.CTRL = CLK_SCLKSEL_RC32M_gc; // vybrat 32MHz jako systémový clock
}

Závěrem

Návod ani zdaleka nevyčerpal všechny možnosti jednotky USART. V podstatě jsem se věnoval jen těm nejzákladnějším režimům použití. Většina ostatních, jako je MPCM, One-wire nebo DMA příjem jsou specifické příklady v podstatě výhradně určené pro komunikaci uP-uP nebo uP-Periferie. Kromě nich, zbývá třeba ještě zajímavá spolupráce USARTu a XCL modulu. Zatím jsem ale bohužel neměl příležitost v těchto režimech pracovat a tak se necháme překvapit co přinese budoucnost a zda se k nim někdy vrátím. Já doufám, že vám návod vnesl trochu světla do práce Xmegami a že se v budoucnu opět setkáme u návodů s podobnou tematikou.