Nebudu to složitě rozvádět, ale v jedné aplikaci jsem kvůli úspoře místa použil několik Atmega808 jako expandéry a potřeboval jsem je mezi sebou propojit jedinou linkou. Jednak kvůli, již zmíněné úspoře místa a také kvůli nedostatku pinů. A tady jsem ocenil slušně vybavený UART na moderních AVRkách (0-series, 1-series). V následující ukázce si tedy dovolím představit řešení jak komunikovat UARTem po jednom drátě. Pojďme tedy rovnou na věc a představme si vybrané funkce UARTu.
Režim Half-Duplex One-Wire interně propojí RX a TX. Toto spojení je pak vyvedeno na pin TX. Pin RX se tím uvolní k jiným účelům. Režim se aktivuje bitem LBME. Protože je RX a TX propojený, tak program přijímá i své vlastní zprávy. To může být někdy zbytečná komplikace. V takovém případě stačí před začátkem vysílání přijímač vypnout a po skončení vysílání zase zapnout. Někdy to ale může být žádoucí - například pokud hrozí nějaká komunikační kolize nebo obecně "deformace" zprávy. V takovém případě se vyplatí odposlechnout odeslaná data a vyhodnotit zda jsou v pořádku.
Pokud má po jedné lince komunikovat více zařízení, tak obecně hrozí kolize. Tedy situace kdy se v jednom okamžiku pokouší ovládat sběrnici více zařízení. Takovou kolizi lze částečně ošetřit tak že se výstup MCU nastaví do režimu Open Drain namísto běžného Push Pull a linka se připojí k napájení pullup rezistorem. Nejběžněji je tento mechanismus známý třeba z I2C sběrnice, takže nebudu nosit dříví do lesa a jen zmíním, že tento režim lze aktivovat bitem ODME. Obecně pak komunikační linka umožňuje komunikovat kdykoli komukoli s kýmkoli - pokud se program dokáže vypořádat s kolizemi na sběrnici (viz předchozí odstavec).
MPC režim má odlehčovat práci softwaru pokud se po UARTu přenáší větší objem informací pro více cílových zařízení. Princip komunikace spočívá v tom, že se rozlišují dva typy zpráv. Takzvané "adresy", které určují s komu jsou následující data určená. A pak surová data. Všechny zprávy jsou typicky 9bitové. 8.bit (nejvyšší) slouží jako indikace zda jde o adresu (má hodnotu 1) a nebo zda jde o surová data (má hodnotu 0). Všechny zařízení na lince přijímají "adresy". Pokud přijmou jejich vlastní adresu (a ta nemusí být jen jedna), deaktivují si MPC mód a přijímají surová data. Pokud ale přijmou adresu, která není jejich. Aktivují si MPC mód a UART pak ignoruje všechna surová data. Přesněji řečeno ignoruje všechy zprávy které nemají nastaven 8.bit. Díky tomu se mikrokontrolér nemusí zabývat příjmem (a zahazováním) zpráv které nejsou pro něj a může se místo toho věnovat něčemu užitečnějšímu - třeba spánku. Moderní AVRka mají navíc možnost využít k signalizaci adresa/data místo 8.bitu také jeden ze stop-bitů (pokud je formát zprávy nastaven na dva stop bity). Aktivace MPC mód se provádí nastavením bitu MPCM.
Naše vzorová aplikace bude mít za úkol posílat 16bit hodnoty mezi MCU s různými adresami. Bude využívat všechny tři výše zmíněné funkce. Tedy bude komunikovat po "jednom drátě" s výstupem typu otevřený kolektor (pullup rezistor použijeme pro jednoduchost interní v čipu). Formát zprávy bude 9bitový aby vyhovoval MPC módu. V každém rámci nejprve pošle adresu (8.bit nastaven) a pak dva datové byty nesoucí 16bitovou hodnotu. Nejprve vyšší byte (MSB), poté nižší byte (LSB). Aplikace bude takový rámec schopna jak poslat tak přijmout. Při odesílání si bude hlídat zda nedošlo k "poškození" dat. Pokud dojde tak vysílání ukončí. Bude využívat MPC mód a bude tedy přijímat jen data určená pro ni (nikoli data určená pro MCU s jinou adresou). Pro demonstraci bude na lince připojen jeden "master" vybavený tlačítkem, který po stisku pošle jednu zprávu. A jeden slave, který po přijetí zprávy pošle zpět masteru odpověď.
Na inicializaci není nic komplikovaného. Pro demonstraci jsem použil UART1 s TX na pinu PC0. Aktivuji pullup rezistor. Ten aktivuji jak v master tak slave zařízení. Tedy výsledná hodnota pullup rezistoru bude paralelní kombinaci v našem případě dvou rezistorů a obecně tolika pullup rezistorů kolik je na lince připojených slave zařízení. V praxi je ale asi výhodnější osadit externí pullup a mít tak pod kontrolou jeho hodnotu. Dále nastavím baudrate na konvenčních 115200b/s a povolím všechny výše zmíněné módy. Navíc bitem DBGRUN povolím aby UART běžel i během debugu, kdy je MCU zastavené. Také si povolím přerušení od přijmu.
void init_onewire_uart(void){ PORTC_PIN0CTRL |= PORT_PULLUPEN_bm; // aktivovat pullup na Tx(Rx) pinu USART1.BAUD = 0x0074; // baudrate (115200 při 3.33M) USART1.CTRLA = USART_LBME_bm | USART_RXCIE_bm; // režim "loopback" (propojí Tx a Rx a uvolní Rx pin), povolení přerušení od RX USART1.CTRLC = USART_CHSIZE_9BITH_gc; // 9bit transfer USART1.DBGCTRL = USART_DBGRUN_bm; // UART má běžet i během debugu (kdy je MCU zastavené) USART1.CTRLB = USART_RXEN_bm | USART_TXEN_bm | USART_ODME_bm; // spustit Rx+Tx a zvolit "open drain" mód }
Před vysíláním si musíme deaktivovat přerušení od přijmu - nechceme v přerušení přijímat svou vlastní zprávu. Její příjem, kvůli detekci chyb, budeme provádět "manuálně" ve vysílací funkci. Jako první odešleme adresu. Protože používáme 9.bitovou komunikaci, musíme nejprve zapsat hodnotu 8.bitu do registru TXDATAH a pak hodnotu adresy do registru TXDATAL. V tom okamžiku se zpráva začne vysílat. Obecně bychom před odesíláním měli zkontrolovat zda je v odesílacím bufferu místo. V našem případě to ale není nutné neboť budeme poctivě hlídat konec vysílání. Po odeslání adresy si počkáme až vysílání skončí (vlajka TXCIF - Transfer Complete). V tom okamžiku by v přijímacím bufferu měla čekat kopie toho co jsme poslali. Nejprve tedy přečteme "horní" část z registru RXDATAH. V něm jsou krom 8.bitu zprávy také vlajky jako "frame error" apod. Zkontrolujeme zda hodnota 8.bitu odpovídá té co jsem poslali (tedy 1), poté zkontrolujeme zda není nastaven "frame error" (ten by indikoval "zmršení" zprávy a tedy pravděpodobnou kolizi na sběrnici). Poté si přečteme dolní byte RXDATAL a zkontrolujeme zda obsahuje to co jsme poslali (tedy hodnotu adresy). Pokud některá z kontrol nedopadla úspěšně, tak vysílání raději ukončíme s návratovou hodnotou 0xFF. Pokud proběhlo vysílání adresy úspěšně, vypneme MPC mód (neboť teď chceme přijímat i datové byty) a shodným způsobem pošleme a zkontrolujeme další dva byty rámce (nesoucí 16bitovou hodnotu). Po kompletním úspěšném vysílání zpětně aktivujeme MPC mód a přerušení od přijmu.
// funkce odesílá rámec (adresa + 16bit data), vrací 0 pokud proběhlo vysílání v pořádku, 0xFF pokud nastal problém uint8_t uart_frame_tx(uint8_t address, uint16_t data){ uint8_t err=0; // příznak chyby uint8_t tmp; // pracovní proměnná USART1.CTRLA &=~USART_RXCIE_bm; // deaktivovat přerušení od Rx (budeme přijímat vlastní data) // posíláme adresu USART1.TXDATAH = 0b1; // nastavíme 8.bit - posíláme adresu USART1.TXDATAL = address; while(!(USART1.STATUS & USART_TXCIF_bm)); // počkat na dokončení vysílání USART1.STATUS = USART_TXCIF_bm; // vyčistit vlajku if(USART1.STATUS & USART_RXCIF_bm){ // pokud jsme přijali svůj vlastní byte (což bychom měli) tmp = USART1.RXDATAH; // přečteme si MSB a vlajky if(tmp & USART_FERR_bm || !(tmp & USART_DATA8_bm)){err=0xff;} // Frame error => kolize na sběrnici, Není nataven 8.bit ? => kolize na sběrnici if(USART1.RXDATAL != address){err=0xff;} // neodpovídají přijatá data odvysílaným ? => kolize na sběrnici }else{err=0xff;} // pokud jsme vůbec nepřijali svůj vlastní byte=> (nemožná) kolize na sběrnici if(err){// pokud nastal Error, ukončíme vysílání USART1.CTRLA |= USART_RXCIE_bm; // zpět povolit přerušení od Rx return err; } // vypneme MPC mód - chceme poslouchat svá vlastní data - abychom případně zachytili kolizi na sběrnici USART1.CTRLB &=~USART_MPCM_bm; // poslouchej všechno // posíláme "payload" MSB USART1.TXDATAH = 0b0; USART1.TXDATAL = (uint8_t)(data>>8); while(!(USART1.STATUS & USART_TXCIF_bm)); // počkat na dokončení vysílání USART1.STATUS = USART_TXCIF_bm; // vyčistit vlajku if(USART1.STATUS & USART_RXCIF_bm){ // pokud jsme přijali svůj vlastní byte (což bychom měli) tmp = USART1.RXDATAH; if(tmp & USART_FERR_bm || (tmp & USART_DATA8_bm)){err=0xff;} // Frame error => kolize na sběrnici, Není vynulován 8.bit ? => kolize na sběrnici if(USART1.RXDATAL != (uint8_t)(data>>8)){err=0xff;} // neodpovídají přijatá data odvysílaným ? => kolize na sběrnici }else{err=0xff;} // pokud jsme vůbec nepřijali svůj vlastní byte=> (nemožná) kolize na sběrnici if(err){// pokud nastal Error, ukončíme vysílání USART1.CTRLA |= USART_RXCIE_bm; // zpět povolit přerušení od Rx return err; } // posíláme "payload" LSB USART1.TXDATAH = 0b0; USART1.TXDATAL = (uint8_t)data; while(!(USART1.STATUS & USART_TXCIF_bm)); // počkat na dokončení vysílání USART1.STATUS = USART_TXCIF_bm; // vyčistit vlajku if(USART1.STATUS & USART_RXCIF_bm){ // pokud jsme přijali svůj vlastní byte (což bychom měli) tmp = USART1.RXDATAH; if(tmp & USART_FERR_bm || (tmp & USART_DATA8_bm)){err=0xff;} // Frame error => kolize na sběrnici, Není vynulován 8.bit ? => kolize na sběrnici if(USART1.RXDATAL != (uint8_t)data){err=0xff;} // neodpovídají přijatá data odvysílaným ? => kolize na sběrnici } else{err=0xff;} // pokud jsme vůbec nepřijali svůj vlastní byte=> (nemožná) kolize na sběrnici USART1.CTRLB |= USART_MPCM_bm; // už neposlouchej všechno, poslouchej jen adresy USART1.CTRLA |= USART_RXCIE_bm; // zpět povolit přerušení od Rx return err; }
V rutině přerušení od příjmu jsem využil stavový automat. Stavová proměnná je phase. S příchodem každé zprávy si nejprve přečtu horní a pak dolní byte (zpráva je 9.bitová). Pak vždy zkontroluji stav 8.bitu a "frame error". Zprávy které obsahují "frame error" ignoruji. Pokud zjistím že je 8.bit nastaven a že jde o adresu, otestuji zda je to moje adresa a zda je tedy zpráva určena mě. Pokud ne, aktivuji MPC mód a zablokuji tím příjem datové části rámce (adresy přijímám stále). Pokud je zpráva určena mě, deaktivuji MPC mód, čímž povolím příjem datových bytů. Také nastavím phase=1 což znamená že další byte (s nulovým 8.bitem) bude první datový byte rámce. Pokud přijde, uložím si MSB do pracovní proměnné a přehodím phase=2 a po přijetí dalšího bytu si ukládám LSB a mám příjem kompletní. Zkopíruji přijatou 16bit hodnotu do globální proměnné rx_data a nastavím vlajku rx_flag čímž signalizuji zbytku programu že jsou k dispozici nová data. Nic komplikovaného. Pravděpodobně nebude problém upravit funkci na příjem libovolného počtu bytů.
// příjem po UARTu ISR(USART1_RXC_vect){ static uint8_t phase=0; // proměnná pro "stavový automat" static uint16_t tmp_rxdata; // tady postupně ukládáme přijatá data uint8_t tmpl; // dočasné proměnné pro zpracování přijatých dat uint8_t tmph = USART1.RXDATAH; // přečti nejprve "horní" přijatý byte tmpl = USART1.RXDATAL; // a pak dolní přijatý byte if((tmph & USART_DATA8_bm) && !(tmph & USART_FERR_bm)){ // pokud je nastaven 8.bit (je to adresa) a pokud nemáme frame error if(tmpl == MY_ADDRESS){ // zkontroluj jestli je to naše adresa USART1.CTRLB &=~USART_MPCM_bm; // pokud ano deaktivuj MPC mód (tedy přijímej všechno) phase=1; // přejdi do další fáze příjmu } else{ // pokud nastal problém přenosu nebo nepřišla naše adresa, aktivuj MPC mód (všecko co není adresa ignoruj) a čekej na další zprávu USART1.CTRLB |= USART_MPCM_bm; // poslouchej jen adresy } }else if(phase==1){ // pokud přišel byte a minule přišla naše adresa, přijímáme MSB datové části zprávy tmp_rxdata = (((uint16_t)tmpl) << 8); // uložíme si MSB do lokální proměnné phase++; // přejdi do další fáze }else if(phase==2){ // pokud přišel druhý byte datové části zprávy tmp_rxdata |= tmpl; // je to LSB a uložíme si ho USART1.CTRLB |= USART_MPCM_bm; // dál už nechci přijímat (další datové byty ignoruj, přijmi jen adresu) if(!rx_flag){ // pokud jsou minulá data zpracovaná rx_data = tmp_rxdata; // uložíme si nově přijatá data rx_flag=1; // dáváme vědět zbytku programu že jsme přijali kompletní nová data } } }
Jak už bylo napsáno v úvodu. Master má za úkol poslat po stisku tlačítka jednu zprávu do zařízení s adresou 2. Ve vzorové zprávě posílám hodnotu 0x0102 abych mohl snadno ověřit že slave zařízení přijímá správně oba datové byty. Tlačítko mám připojené na pinu PA0 klasicky proti zemi s interní pullup rezistorem. Clock čipu nechávám ve výchozí konfiguraci tedy 20MHz/6 = 3.333MHz.
// one-wire UART MPCM demonstrace - kód pro "Master" #define F_CPU 3333333 // tovární frekvence #include <util/delay.h> #include <avr/io.h> #include <avr/interrupt.h> #define MY_ADDRESS 1 // moje adresa na které poslouchám void init_onewire_uart(void); uint8_t uart_frame_tx(uint8_t address, uint16_t data); // proměnné pro předání přijaté zprávy z rutiny přerušení volatile uint16_t rx_data=0; // obsah zprávy volatile uint8_t rx_flag=0; // informace že přišla zpráva uint8_t pinstatus=1; // poslední stav tlačítka int main(void){ PORTA.PIN6CTRL |= PORT_PULLUPEN_bm; init_onewire_uart(); // inicializace "one-wire" módu sei(); // globální povolení přerušení while (1){ if(rx_flag){ // pokud přišla zpráva rx_flag=0; // vymaž si vlajku // zpracuj zprávu (teď není potřeba) } if(!(PORTA.IN & PIN6_bm) && pinstatus){ // po stisku tlačítka pošli zprávu do "slave" zařízení pinstatus = 0; uart_frame_tx(2,0x0102); // na adresu 2, pošli hodnotu 0x0102 }else if(PORTA.IN & PIN6_bm){ pinstatus = 1; } } }
Většina kódu je pro slave zařízení shodná. Liší se vlastně jen hlavní smyčka kde místo čekání na stisk tlačítka čekáme na příjem zprávy. Poté obsah zprávy inkremetujeme a pošleme zpátky masterovi (jeho adresa je 1). To by mělo sloužit jako důkaz, že zprávu přijmeme kompletně. Protože nemá slave nic na práci, bude během "nic nedělání" spát. Aby bylo na osciloskopu možné od sebe zprávy snadno odlišit, počká slave s odesláním odpovědi 200us.
#define MY_ADDRESS 2 // moje adresa na které poslouchám int main(void){ //PORTA.PIN6CTRL |= PORT_PULLUPEN_bm; init_onewire_uart(); // inicializace "one-wire" módu sei(); // globální povolení přerušení set_sleep_mode(SLEEP_MODE_IDLE); // když není co dělat, šetříme šťávu v "idle" :) sleep_enable(); // povolíme uspávání while (1){ if(rx_flag){ // pokud přišla zpráva rx_flag=0; // vymaž si vlajku _delay_us(200); // chvíli počkej abychom zprávy oddělili na osciloskopu uart_frame_tx(1,rx_data+1); // ppřijatá data inkrementuj a pošli odpověď } sleep_cpu(); // není co dělat, spíme } }
Hlavní důvod pro zde toto řešení prezentuji je ten abych se k němu mohl za rok vrátit, až budu řešit stejný problém v nějakém jiném projektu. Je tedy možné že jsem některé myšlenky nevysvětlil dost názorně nebo srozumitelně... a taky je možné, a to je možné vždy a u všeho co dělám, že je tam někde nějaká chyba.
Home
| V1.00 26.7.2021 /
| By Michal Dudka (m.dudka@seznam.cz) /