Úvod

Jak již bývá v tomto seriálu zvykem budeme se s ovládáním periférie Atmelu seznamovat na příkladech. Atmelovský TWI modul se ovládá relativně snadno. Klíčové pro jeho zvládnutí je znalost I2C protokolu. Princip jak uvidíte je velice prostý. Zvláště díky tomu, že budeme řešit jednodušší případ, kdy je na sběrnici jen jeden master. Ve většině vašich aplikací bude Atmel hrát roli mastera. Většina slave budou různé integrované obvody, od DA a AD převodníků, přes čidla až po expandéry. Návod, který by prováděl ukázky na nějakém konkrétním I2C obvodu by byl ale málo obecný. K jeho testování by jste potřebovali konkrétní obvod a navíc by jste museli nastudovat práci tohoto obvodu. To by mohlo návod zbytečně "zamlžit". Dále by přišli zkrátka ti, kteří chtějí stavět I2C síť z Atmelů. Proto budou příklady demonstrovat komunikaci Atmel->Atmel (Master->Slave). Zdrojové kódy tedy budou typicky dva. Jeden pro master a jeden nebo více pro slave. Ovládání TWI v master obvodu bude přirozeně stejné ať komunikujete s jiným Atmelem nebo jakýmkoli dalším zařízením, takže vám to práci nijak nezkomplikuje. V budoucnu by vám už mělo stačit pouze dostudovat dokumentaci k cílovému zařízení (akcelerometru, magnetometru nebo čemukoli). Kapitola nejspíš nebude patřit k nejjednodušším a příklady budou možná trochu rozsáhlejší než jste zvyklí, ale věřím, že vám to nebude činit potíže. První příklad popíšu podrobně, další příklady už budu komentovat spíše rámcově, protože podrobný popis by byl únavný (nejen pro mě ale i pro čtenáře).

Seznam příkladů

I2C na Atmelech

Atmel nazývá I2C protokol jako TWI (Two Wire Interface) - kvůli licenčním právům na název I2C. Obecně lze I2C komunikaci realizovat dvěma způsoby. Primitivnější z nich je softwarová implementace (nebo také tzv "bit-banging" I2C). Takový program sám zachází s piny Atmelu a dodržuje příslušné časování a komunikuje podle pravidel I2C. Tento způsob je celkem častý. Běžně nedosahuje velkých datových rychlostí, ale umožňuje vám zvolit si SDA a SCL linku na libovolných pinech procesoru. Krom toho lze touto cestou ovládat více než jen jednu sběrnici (nikdo vám nebrání mít tři různé SCL a SDA). Skoro výhradně se však tento postup využívá pro roli Mastera, což vám nejspíš bude stačit. U slave zařízení se softwarová implementace moc nepoužívá. Složitější a v některých ohledech schopnější je hardwarová implementace (pokud se to tak dá nazvat). Většina čipů rodiny AVR má nějaké hardwarové rozhraní, které I2C komunikaci podporuje. U některých Attiny jde k těmto účelům využít USI (ale tato metoda se podobá spíše softwarové implementaci). Většina ostatních čipů má shodné TWI rozhraní, které řídíte pomocí 5 registrů. Díky němu lze používat rychlosti až 400kb/s, provozovat Atmel jako master i jako slave a dokonce lze slave budit z režimu spánku. Dvojici pinů SDA a SCL najdete na Atmega16 na pinech PC1 a PC0 a je určena pevně.

TWI - obecná prvidla

Používání TWI modulu na atmelu vypadá tak, že nejprve nastavíte základní parametry a modul zapnete. V roli matera je základním parametrem datová rychlost, v roli slave je to jeho adresa. Pak už jen dáváte TWI modulu pokyny, sledujete jeho stav a čekáte na odezvu. V roli mastera dáte například pokyn vygenerovat START sekvenci a až ji TWI modul vygeneruje, dá vám o tom vědět (přerušením nebo vlajkou). Váš program by na to měl zareagovat, zjistit si co TWI modul opravdu udělal a dát mu další pokyn (například odeslat adresu s příznakem zápisu/čtení). A tak stále dokola ve stylu pokyn -> čekání na reakci -> kontrola -> pokyn. O tom jaký pokyn smíte dát najdete v tabulkách v datasheetu (později se dočtete kde je najít). Ty shrnují všechny možnosti které mohou nastat a všechny možné reakce, které smíte udělat a jsou logické. Je třeba logické po start sekvenci odesílat adresu slave zařízení a není logické po start sekvenci posílat data (není přece jasné komu jsou určena). Tabulky, které shrují veškeré chování jsou čtyři a pokrývají tyto čtyři možné role TWI modulu:


V každé ze čtyř rolí máte jiné možnosti. Master transmiter může posílat slave adresu nebo data, generovat START nebo STOP sekvenci. Master receiver může dávat nebo nedávat potvrzení zprávy a generovat START nebo STOP sekvence. Slave transmitter může pouze odesílat data. Slave receiver může potvrzovat nebo nepotvrzovat zprávy. Prostě může dělat to co mu I2C protokol dovoluje.

TWI - registry

K chodu TWI většinou potřebujete pracovat se třemi registry. Zbylé dva (až tři) slouží k nastavení komunikační rychlosti a slave adresy (pokud Atmel pracuje jako slave). Datová rychlost se nastavuje v registru TWBR a je dána vztahem f=(F_CPU)/(16+2*(TWBR)*4^(TWPS)). F_CPU je takt procesoru, TWPS řídí předděličku clocku pro TWI modul a konfiguruje se pomocí bitů TWPS1 a TWPS0 v registru TWSR (viz tab. 1).

Tabulka 1 - předdělička pro TWI
TWPS1TWPS0předdělička
001
014
1016
1164

Registr TWSR krom předděličky obsahuje 5 stavových bitů TWS7TWS3. Ty slouží k indikaci stavu v němž se celý TWI modul nachází. Můžete podle nich poznat jakou operaci naposledy TWI modul provedl, v jaké roli se nachází a podle toho pak dát TWI správný pokyn.

TWSR - TWI Status Register
76543210
TWS7TWS6TWS5TWS4TWS3-TWPS1TWPS0

Řízení TWI modulu se provádí pomocí TWCR registru. Postup ovládání TWI rozhraní budeme provádět pomocí tabulek z datasheetu. Proto vás s jednotlivými bity seznámím jen ve stručnosti. Bitem TWEA nastavujete zda chcete v dalším úkonu dávat nebo nedávat potvrzení zprávy, bitem TWSTA nastavujete zda v příštím úkonu chcete vysílat START sekvenci a analogicky bitem TWSTO řídíte vysílání STOP sekvence. Bit TWWC je vlajka, která vás informuje pokud uděláte chybu a přistupujete k registru TWDR když nemáte. Bitem TWEN celý TWI modul zapínáte nebo vypínáte (a mimo jiné mu přidělujete nebo nepřidělujete piny SDA a SCL). TWINT vás informuje o tom že TWI modul dokončil nějakou činnost a čeká na vaši reakci. Smazáním vlajky TWINT (což se provádí zápisem log.1) dáváte TWI modulu pokyn k další akci.

TWCR - TWI Control Register
76543210
TWINTTWEATWSTATWSTOTWWCTWEN-TWIE

Registr TWDR slouží k zápisu dat, která se mají na sběrnici vysílat nebo ke čtení přijatých dat. A jeho použití se opět odvíjí od aktuálního stavu TWI modulu. Registr TWAR pak slouží ke specifikaci slave adresy, pokud zařízení pracuje jako slave.

TWI - kuchařka na psaní programů

Komunikace po I2C může být rozmanitá a je tedy možné, že si často budete upravovat programy na míru příslušnému slave obvodu. Bude proto vhodnější když dostanete do rukou jakousi kuchařku jak programy psát. Ústřední roli v ní budou hrát tabulky, které najdete v datashetu pod názvy Status Codes for Master Transmitter Mode, Status Codes for Master Receiver Mode, Status Codes for Slave Receiver Mode a Status Codes for Slave Transmitter Mode (pro Atmega16a tabulky 20-2, 20-3, 20-4 a 20-5). Ať už budete TWI ovládat pomocí přerušení nebo hlídat jeho stav pollingem, postup bude stejný. Roli v jaké se TWI nachází rozpoznáte pomocí stavových kódů z výše zmíněných tabulek. Odvysílá-li Atmel START sekvenci, je v roli Master transmitter. Odešle-li slave adresu s příznakem čtení, přepne se sám do role Master Receiver a podobně. Při zadávání pokynu TWI modulu vždy nastavte bit TWINT a přirozeně udržujte bit TWEN nastavený. V roli Mastera musíte komunikaci začít vysíláním START sekvence. K tomuto účelu v TWCR nastavte bity TWINT a TWSTA. Jakmile dáte TWI nějaký pokyn, vždy vyčkejte než jej dokončí - vyčkejte na nastavení vlajky TWINT. Potom vždy přečtěte stavové bity z registru TWSR a zkontrolujte jejich stav s tabulkou. Vyberte jednu z akcí, která smí následovat a do TWCR zapište hodnotu, kterou vyčtete z tabulky. Společně s tím smažte vlajku TWINT (zápisem log.1). To stále opakujte, dokud nechcete komunikaci ukončit. V roli slave obvodu je situace stejná s tím rozdílem, že komunikaci nezačínáte, pouze čekáte na nastavení vlajky TWINT a reagujete na to podle příslušné tabulky. Aby mohly být programy čitelnější a neobsahoval nic neříkající hodnoty stavových kódů jako třeba 0x08 nebo 0x10, máte k dispozici hlavičkový soubor util/twi.h. V něm jsou makra, která slovně vyjadřují každou položku tabulky. Názvy jsou samovysvětlující. "MT" znamená Master Transmitter, "MR" Master Receiver, "ST" Slave Transmitter a "SR" Slave Receiver. Zkratka SLA znamená Slave Addres, +W a +R pak indikuje zda byla adresa odeslána s příznakem zápisu nebo čtení. Makro TW_STATUS vám usnadní kontrolu stavových bitů (ty se totiž nachází v registru společně s bity předděličky, které je nutno maskovat). Já si ve svých příkladech ještě několik užitečných maker dodělal.

Otevřte si datasheet a nejděte některou z tabulek, trochu si je okomentujeme. Probírat všechny buňky tabulek stavových kódů by nemělo smysl, protože jsou v nich nejspíš VŠECHNY možné situace. V levém sloupci je vždy stav TWSR (s maskovanými bity předděličky). Pokud je v TWSR například hodnota 0x20 (TW_MT_SLA_NACK), můžete se z druhého sloupečku dočíst, že byla odeslána adresa slave obvodu s příznakem zápisu a slave zprávu nepotvrdil. Vy jste v roli Master Transmitter (dozvíte se z názvu tabulky). Ve zbylých sloupečcích jsou čtyři možné reakce, které může TWI udělat. První z nich říká že můžete do TWDR načíst data a pak zapsat příslušné hodnoty do TWCR (hodnoty jednotlivých bitů jsou v následujících sloupcích). V posledním sloupci se pak dočtete co TWI udělá. V tomto případě odvysílá data bez ohledu na to že slave svoji adresu nepotvrdil. Což skoro určitě bude znamenat že data ignoruje. Jiná možnost je do TWDR nic nezapisovat a příslušným zápisem do TWCR vygenerovat opakovaný START. Nebo vygenerovat STOP sekvenci a nebo vygenerovat STOP sekvenci bezprostředně následovanou START sekvencí. Volba je na vás. Pokud vygenerujete STOP, TWI svoji činnost ukončí a už na nic nemusíte čekat. Pokud uděláte cokoli jiného, opět by jste měli být připraveni na to, že dojde k nastavení vlajky TWINT a vy budete muset reagovat. Přečtete si stav TWSR a octnete se v jiném políčku tabulky a budete mít na výběr jiné možnosti. A to je všechno :) Docela prosté že ? Předvedeme si to na příkladu.

A) TWI - Přenos 1 byte Master >> Slave

Nejtriviálnější příklad jaký lze ukázat je odeslání jednoho byte z Master do Slave. V roli slave bude Atmel a změnou definice MY_SLAVE_ADDRESS si jich můžete stvořit kolik chcete. Teoreticky tento model můžete využít tam, kde potřebujete řídit složitější proces a je pro vás vhodné rozdělit ho na několik jednodušších. Například když vaše experimentální sestava obsahuje několik motorů a každý nich je řízen samostatným procesorem (slave). Master pak dle požadavků obsluhy nebo podle potřeb měřícího procesu dává pokyny slave zařízením (Atmelům), která jsou "jednoúčelová" a starají se každý o svůj motor. Díky tomu, že se odesílá pouze jeden byte, je komunikace v podstatě triviální. Master odešle adresu slave obvodu s příznakem zápisu a ihned na to naváže odesláním datového byte. Slave jen pasivně poslouchá a potvrzuje zprávy. Master si přirozeně musí kontrolovat jestli mu slave dává potvrzení a patřičně na to reagovat. Náš Master bude po stisku tlačítka na PA7 nebo na PA6 odesílat pokyn jednomu ze dvou slave obvodů. Já budu mít pro testy na sběrnici připojen jen jeden (se 7 bitovou adresou 0x02). Vám ale nic nebrání připojit si bez úprav kódu dva.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// A - Přenos 1 byte Master >> Slave (kód pro master)
#include <avr/io.h>
#define F_CPU 8000000
#include <util/delay.h>
#include <util/twi.h>

#define SLV1_ADDRESS 0x2                      // adresy různých slave zařízení na sběrnici
#define SLV2_ADDRESS 0x3

#define TW_SEND_START TWCR = (1<<TWINT)|(1<<TWSTA)|(1<<TWEN)
#define TW_SEND_STOP TWCR = (1<<TWINT)|(1<<TWEN)|(1<<TWSTO)
#define TW_SEND_DATA TWCR = (1<<TWINT) | (1<<TWEN)
#define TW_WAIT while(!(TWCR & (1<<TWINT)))
#define TLAC1 (PINA & (1<<PINA7))
#define TLAC2 (PINA & (1<<PINA6))

char i2c_posli_1B(char adresa, char data);
char tlac1=0, tlac2=0;

int main(void){
 TWBR=32;                                  // žádná předdělička, F_SCL = 8MHz/(16+2*TWBR) = 100kHz.
 TWCR = (1<<TWEN);                         // zapnout TWI modul	
 DDRA &=~((1<<DDA7) | (1<<DDA6));          // dvě tlačítka
 PORTA = (1<<PORTA7) | (1<<PORTA6);        // pull-up pro tlačítka
 while(1){
  if(!TLAC1 && tlac1==0){                   // po stisku tlačítka 1
   tlac1=1;	                            // zamči tlačítko (do uvolnění)
   i2c_posli_1B(SLV1_ADDRESS,1);            //pošli data=1 pro slave 1
  }
  if(!TLAC2 && tlac2==0){
   tlac2=1;	                            // zamči tlačítko (do uvolnění)
   i2c_posli_1B(SLV2_ADDRESS,1);            //pošli data=1 pro slave 2
   }			
  if(TLAC1 && TLAC2){                       // tlačítka uvolněna
   tlac1=0;                                 // odemči tlačítka 
   tlac2=0;
  }
 _delay_ms(100);                            // ošetření zákmitů		
 }
}

char i2c_posli_1B(char adresa, char data){                                   
 adresa = adresa << 1;                      // zarovnání adresy na 8bit formát
 TW_SEND_START;                             // vygenerovat START sekvenci
	
 TW_WAIT;                                   // počkat na odezvu TWI
 if ((TW_STATUS) != TW_START){ return 1;}   // pokud nebyl start vygenerován, máme error a končíme
 TWDR = adresa;                             // nahrát adresu slave s příznakem zápisu (SLA+W)
 TW_SEND_DATA;                              // odeslat adresu  
	
 TW_WAIT;                                   // počkat na odezvu TWI
 if ((TW_STATUS) != TW_MT_SLA_ACK){TW_SEND_STOP; return 2;} // Slave nám nedal potvrzení, případně nastal jiný problém ? končíme komunikaci STOP sekvencí
 TWDR = data;                               // nahrát data která chceme poslat do slave
 TW_SEND_DATA;                              // odeslat data
	
 TW_WAIT;                                   // počkat na odezvu TWI
 if ((TW_STATUS) != TW_MT_DATA_ACK){TW_SEND_STOP; return 2;} // Slave nám nedal potvrzení, případně nastal jiný problém ? končíme komunikaci STOP sekvencí
 TW_SEND_STOP;                              // konec komunikace
 return 0;
}

Zápisem do TWBR nastavím datovou rychlost na 100kb/s. Nastavením bitu TWEN spustím TWI, které od tohoto okamžiku přebírá kontrolu nad SDA a SCL. V Master aplikaci si pak připravuji dvě tlačítka na PA6 a PA7. Po jejich stisku volám funkci i2c_posli_1B(), která obstarává komunikaci. Prvním argumentem funkce je adresa slave obvodu, kterému chci poslat data, druhým argumentem je pak jeden byte dat. Ve funkci se nejprve vypořádám se slave adresou, kterou si potřebuji zarovnat doleva (poslední bit potřebuji jako příznak zápisu / čtení). Funkce tedy vyžaduje adresu v sedmibitovém formátu. Pak dám TWI pokyn vygenerovat START sekvenci. Musím počkat než START vygeneruje. Čekání provádím smyčkou TW_WAIT. Když se na toto makro podíváte, uvidíte že pouze čekám na nastavení vlajky TWINT. Jakmile k tomu dojde, přečtu stav TWSR makrem TW_STATUS a zkontroluji zda je stav takový jaký očekávám. Po odvysílání START sekvence čekám, že byla odvysílána, tedy že stav bude TW_START (0x08). Viz tabulka Status Codes for Master Transmitter Mode. Pokud to tak je, mohu podle tabulky naplnit TWDR slave adresou, které nechám osmý bit vynulovaný jako příznak zápisu. A zase dle tabulky zapíšu příslušnou hodnotu do TWCR a počkám než TWI dokončí svoji činnost. Jakmile skončí opět čtu stav TWSR a kontroluji zda se stalo to co očekávám. Tady se vám problém může typicky rozpadnout na dvě situace. Buď vám nějaký slave adresu potvrdí a nebo nepotvrdí. Typicky tedy dostanete z TWSR hodnotu TW_MT_SLA_ACK (0x18) když vám slave svoji přítomnost potvrdí nebo TW_MT_SLA_NACK (0x20) když vám žádný slave potvrzení nedá. Krom první možnosti nemá žádná další komunikace smysl, takže ve všech ostatních případech nechám funkci odvysílat STOP a končím. Pokud slave svoji adresu potvrdí, tak opět postupuji dle tabulky. Naplním TWDR daty a odešlu je (odpovídajícím zápisem do TWCR). Počkám na odezvu a zjistím si zda slave příjem dat potvrdil nebo ne. Pokud by je nepotvrdil mohl bych na to nějak reagovat. Ale v tomto jednoduchém případě to neřeším a ať je slave potvrdí nebo ne, prostě komunikaci STOP sekvencí ukončím. Nikdo vám ale nebrání informaci o nepotvrzení zprávy od slave nějak zužitkovat. Program na straně slave zařízení je jednodušší. O TWBR se jako slave vůbec starat nemusím. Datovou rychlost stejně určuje master. Do TWAR zapíšu svoji slave adresu a nastavením bitu TWGCE povoluji reagovat i na tzv. General call. Můj slave tedy bude poslouchat i na adresu 0x00. To typicky slouží k hromadnému rozeslání zprávy všem slave na sběrnici. Můj master ale nic takového vysílat nebude :) Slave pak už jen čeká na nastavení vlajky TWINT a pomocí stavu TWSR selektuje co se zrovna na sběrnici odehrálo (opět pomocí tabulek). Pokud TWI modul detekoval volání vlastní slave adresy, dám TWI pokyn přijmout příchozí data a potvrdit zprávu. Pokud TWI modul přijal data, zpracuji je (přepnu stav LED) a nechám modul přijímat a potvrdit cokoli dalšího. Jestliže proběhal STOP sekvence, opět nechám modul čekat na další data a vše potvrzovat.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// A - Přenos 1 byte Master >> Slave (kód pro slave)
#include <avr/io.h>
#define F_CPU 8000000
#include <util/twi.h>
#include <util/delay.h>

#define MY_SLAVE_ADDRESS 0x02                 // adresa tohoto zařízení, změňte ji pokud budete nahrávat kód do více slave
#define TW_WAIT while(!(TWCR & (1<<TWINT)))
char data=0;

int main(void){
DDRA = (1<<DDA7);                             // ledka na PA7
TWAR = (MY_SLAVE_ADDRESS<<1) | (1<<TWGCE);    // naše slave adresa, zarovnaná vlevo, General call povoleno
TWCR = (1<<TWEN) | (1<<TWEA);                 // spouštíme TWI modul, příští zprávu potvrdíme 
while (1){
 TW_WAIT;                                     // počakat na odezvu TWI
                                              // čteme v jakém stavu se nachází TWI a podle toho reagujeme
 switch(TW_STATUS){	
  case TW_SR_SLA_ACK:                         // někdo nás adresoval s příznakem zápisu (SLA+W) - jsme v režimu Slave Receiver
   TWCR = (1<<TWINT) | (1<<TWEN) | (1<<TWEA); // čekej na data a dávej potvrzení
   break;
  case TW_SR_DATA_ACK:                        // přišla nám data a potvrdili jsme je
   data=TWDR;                                 // přečti co master poslal
   if(data==1){PORTA ^= (1<<PORTA7);}         // pokud přišla správná data přepni LED
   TWCR = (1<<TWINT) | (1<<TWEN) | (1<<TWEA); // čekej na data a dávej potvrzení	
   break;
  case TW_SR_STOP:                            // přišla STOP sekvence nebo opakovaný START
   TWCR = (1<<TWINT) | (1<<TWEA) | (1<<TWEN); // to nás nezajímá, čekej na data a dávej potvrzení
   break;	
  default:
   TWCR = (1<<TWINT) | (1<<TWEA) | (1<<TWEN); // pokud přijde cokoli nečekaného, čekej na data a dávej potvrzení
  }			 		
 }
}


obrázek a1 - foto zapojení. Vpravo master s tlačítky, vlevo slave s LED.

B) TWI - Přenos více bytů Master >> Slave

Stejně jako v prvním příkladě opět půjde o jednosměrnou komunikaci, kde Master odesílá data do Slave. Přenášet budeme tentokrát pole bytů. Abychom se však posunuli trochu dopředu, předvedu jak na straně slave obsluhovat TWI pomocí přerušení. Podívejte se na kód pro master. Inicializace zůstala stejná jako v příkladě A. Změnil jsem jen odesílací funkci. Jejím prvním argumentem je slave adresa, druhým pak ukazatel na pole, které chceme odeslat a třetím je počet bytů které chceme poslat. Přirozeně by počet odesílaných bytů neměl být větší jako pole, protože jinak dojde k přetečení pole a nebudeme mít kontrolu nad tím jaká data od nás slave obdrží. Pro jednoduchou kontrolu mám pole data[] naplněné čísly 0,1,2,3 a 4. Na straně slave obvodu pak budeme kontrolovat jestli přišla správná data. To je přirozeně vhodné jen pro ukázku, jinak by pole mělo obsahovat něco smysluplného :) Odesílací funkce vygeneruje start, pak odešle adresu slave obvodu s příznakem zápisu. Potom ve forcyklu odesílá jednotlivé byty pole a po jejich odeslání generuje STOP. Samozřejmě po každém akci kontroluje stav TWI a v případě nesrovnalostí (například nepotvrzení zprávy od slave) zastaví komunikaci STOP sekvencí.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
//B - transfer více bytů master >> slave (kód pro master)
#include <avr/io.h>
#define F_CPU 8000000
#include <util/delay.h>
#include <util/twi.h>

#define SLV1_ADDRESS 0x2                 // adresy různých slave zařízení na sběrnici
#define SLV2_ADDRESS 0x3
#define DELKA_ZPRAVY 5

#define TW_SEND_START TWCR = (1<<TWINT)|(1<<TWSTA)|(1<<TWEN)
#define TW_SEND_STOP TWCR = (1<<TWINT)|(1<<TWEN)|(1<<TWSTO)
#define TW_SEND_DATA TWCR = (1<<TWINT) | (1<<TWEN)
#define TW_WAIT while(!(TWCR & (1<<TWINT)))
#define TLAC1 (PINA & (1<<PINA7))
#define TLAC2 (PINA & (1<<PINA6))

char i2c_posli_pole(char adresa, char *pole, unsigned int pocet_bytu);
char tlac1=0, tlac2=0;
char data[DELKA_ZPRAVY]={0,1,2,3,4};

int main(void){
TWBR=32;                              // žádná předdělička, F_SCL = 8MHz/(16+2*TWBR) = 100kHz.
TWCR = (1<<TWEN);                     // zapnout TWI modul	
DDRA &=~((1<<DDA7) | (1<<DDA6));      // dvě tlačítka
PORTA = (1<<PORTA7) | (1<<PORTA6);    // pull-up pro tlačítka
while(1){
 if(!TLAC1 && tlac1==0){              // po stisku tlačítka 1
  tlac1=1;                            // zamči tlačítko (do uvolnění)
  i2c_posli_pole(SLV1_ADDRESS,data,DELKA_ZPRAVY); // pošli pole "data" pro slave 1
 }
 if(!TLAC2 && tlac2==0){
  tlac2=1;                                        // zamči tlačítko (do uvolnění)
  i2c_posli_pole(SLV2_ADDRESS,data,DELKA_ZPRAVY); // pošli pole "data" pro slave 2
 }			
 if(TLAC1 && TLAC2){                              // tlačítka uvolněna
  tlac1=0;                                        // odemči tlačítka 
  tlac2=0;
 }
 _delay_ms(100);                                  // ošetření zákmitů		
 }
}

char i2c_posli_pole(char adresa, char *pole, unsigned int pocet_bytu){
 unsigned int i;
 adresa = adresa << 1;                       // zarovnání adresy na 8bit formát
 TW_SEND_START;                              // vygenerovat START sekvenci

 TW_WAIT;                                    // počkat na odezvu TWI
 if ((TW_STATUS) != TW_START){ return 1;}    // pokud nebyl start vygenerován, máme error a končíme
 TWDR = adresa;                              // nahrát adresu slave s příznakem zápisu (SLA+W) 
 TW_SEND_DATA;                               // odeslat adresu 

 TW_WAIT;                                    // počkat na odezvu TWI
 if ((TW_STATUS) != TW_MT_SLA_ACK){TW_SEND_STOP; return 2;}
 // Slave nám nedal potvrzení, případně nastal jiný problém ? končíme komunikaci STOP sekvencí
 for(i=0;i<pocet_bytu;i++){
  TWDR = pole[i];                                // nahrát i-tý prvek pole
  TW_SEND_DATA;                                  // odeslat data	
  TW_WAIT;                                       // počkat na odezvu TWI
  if ((TW_STATUS) != TW_MT_DATA_ACK){TW_SEND_STOP; return 2;} 
  // Slave nám nedal potvrzení, případně nastal jiný problém ? končíme komunikaci STOP sekvencí
 }
 TW_SEND_STOP;                                     // konec komunikace
 return 0;
}

Program pro slave využívá přerušení (povoleného pomocí bitu TWIE). TWI modul je po inicializaci nastaven tak aby potvrdil příjem (bit TWEA). Jakmile master zavolá adresu slave zařízení, nastaví se vlajka TWINT a spolu s ní se zavolá přerušení od TWI. V rutině přerušení je pak stejně jako v příkladu A switch který zjišťuje pomocí stavu TWSR jaká operace na I2C sběrnici proběhla a příslušným způsobem na ni reaguje. Jestliže slave přijal a potvrdil svoji adresu, vynuluje se počítadlo přijatých bytů a TWI se nastaví na příjem další zprávy (kterou má potvrdit). Pokud TWI modul přijal a potvrdil datovou zprávu, uloží se přijatá data do pole a počítadlo se inkrementuje. Přirozeně je při ukládání dat dobré hlídat meze pole. Master toho může nedopatřením poslat více než očekáváme. Pak dáme TWI opět pokyn čekat na další data a potvrdit je. Pokud přijde STOP nebo se odehraje nějaká nečekaná událost, mohli bychom na to nějak zareagovat. Náš program tyto situace ale ignoruje a prostě jen čeká na jednu z prvních dvou situací. Použít přerušení u slave obvodu je obecně docela užitečné. Master má situaci jednodušší, protože ví kdy bude vysílat a jisté zdržení z komunikace ho nemusí příliš vytěžovat. Slave ale neví kdy bude volán a měl by reagovat rychle, proto by ho častý polling (pravidelná kontrola vjaky TWINT) dosti zatěžoval. Abychom měli důkaz že slave data správně přijal, přepne stav LED na pinu PA7, pokud je poslední přijatý byte roven čtyřem (což je hodnota jakou master posílá). Jen pro zpestření máte záznam komunikace na obrázku b1.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
//B - transfer více bytů master >> slave (kód pro slave)
#include <avr/io.h>
#define F_CPU 8000000
#include <util/twi.h>
#include <util/delay.h>
#include <avr/interrupt.h>

#define DELKA_ZPRAVY 5
#define MY_SLAVE_ADDRESS 0x02 // adresa tohoto zařízení, změňte ji pokud budete nahrávat kód do více slave

int main(void){
 DDRA = (1<<DDA7);                                      // ledka na PA7
 TWAR = (MY_SLAVE_ADDRESS<<1) | (1<<TWGCE);             // naše slave adresa, zarovnaná vlevo, General call povoleno
 TWCR = (1<<TWEN) | (1<<TWEA) | (1<<TWIE) | (1<<TWINT); // povolit TWI, povolit přerušení, potvrdit příští zprávu
 sei();                                                 // globální povolení přerušení
 while (1){}
}

ISR(TWI_vect){
static char data[DELKA_ZPRAVY]={0};
static unsigned int pocet=0; 

switch(TW_STATUS){                                      // čteme v jakém stavu se nachází TWI a podle toho reagujeme
 case TW_SR_SLA_ACK:                                    // někdo nás adresoval s příznakem zápisu (SLA+W) - jsme v režimu Slave Receiver
  pocet=0;                                              // budeme prijimat prvni byte dat
  TWCR = (1<<TWINT) | (1<<TWEN) | (1<<TWEA) | (1<<TWIE);// čekej na data a dávej potvrzení
  break;
 case TW_SR_DATA_ACK:                                   // přišla nám data a potvrdili jsme je
  data[pocet]=TWDR;                                     // ulož co přišlo
  if(pocet<DELKA_ZPRAVY-1){pocet++;}                    // inkrementuj pocitadlo dat a hlídej aby pole nepřeteklo
  if(data[4]==4){PORTA ^= (1<<PORTA7);}                 // pokud přišla správná data přepni LED
  TWCR = (1<<TWINT) | (1<<TWEN) | (1<<TWEA) | (1<<TWIE);// čekej na data a dávej potvrzení	
  break;
 case TW_SR_STOP:                                       // přišla STOP sekvence nebo opakovaný START ?
  TWCR = (1<<TWINT) | (1<<TWEA) | (1<<TWEN) | (1<<TWIE);// to nás nezajímá, čekej na data a dávej potvrzení
  break;	
 default:
  TWCR = (1<<TWINT) | (1<<TWEA) | (1<<TWEN) | (1<<TWIE);// cokoli dalšího nás nezajímá, čekej na data a dávej potvrzení
 } 
}


Obrázek b1 - odeslání 5 bytů ze slave do master

C) TWI - přenos více bytů Slave >> Master

Předchozí dva příklady nebyly příliš sofistikované a sloužily výhradně k rozdávání pokynů nebo rozesílání dat z master do slave. Slave neměl šanci předat informaci masteru. V tomto příkladě si vyzkoušíme opačnou situaci. Master bude provádět sběr dat ze slave, ale nebude jim posílat žádné pokyny. Typické použití najde tento model tam kde je potřeba provádět sběr dat z autonomních stanic. Slave Atmely mohou například provádět měření několika fyzikálních veličin na různých místech, provádět předzpracování dat a jeden master z nich může data vyčítat a třeba přeposílat do PC. Posílat data po jednom byte jako v příkladě A není moc užitečné takže budeme ze slave číst celá pole. Program na straně slave nepotřeboval příliš mnoho úprav. Opět používá přerušení ve kterém se příkazem switch selektuje stav I2C sběrnice a připravují se příslušná data k odeslání. Jak je vidět ve zdrojovém kódu, slave se po přijetí svojí adresy (s příznakem čtení) přepne do role Slave Transmitter a naplní TWDR prvními daty. S každým dalším odeslaným bytem slave připravuje do TWDR následující datový byte. A přirozeně hlídá aby pole nepřeteklo. Slušně naprogramovaný master by se přirozeně neměl pokoušet číst více dat než je mu k dispozici. Ve všech ostatních situacích slave žádnou akci nedělá a nechá TWI modul čekat na další zprávu kterou potvrdí. Nijak třeba není pokryta situace kdy master nedá potvrzení (což se stane po posledním přijatém bytu). Mohli bychom do switch přidat case TW_ST_DATA_NACK: a na tuto situaci nějak zareagovat. Ale náš program je natolik jednoduchý, že žádná speciální reakce pro tento případ není potřeba.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
//C) Přenos více bytů Slave >> Master (kód pro slave)
#include <avr/io.h>
#define F_CPU 8000000
#include <util/twi.h>
#include <util/delay.h>
#include <avr/interrupt.h>

#define POCET_BYTU 5
#define MY_SLAVE_ADDRESS 0x02							// adresa tohoto zařízení, změňte ji pokud budete nahrávat kód do více slave

int main(void)
{
DDRA = (1<<DDA7);										// ledka na PA7
TWAR = (MY_SLAVE_ADDRESS<<1) | (1<<TWGCE);				// naše slave adresa, zarovnaná vlevo, General call povoleno
TWCR = (1<<TWEN) | (1<<TWEA) | (1<<TWIE) | (1<<TWINT);	// povolit TWI, povolit přerušení, potvrdit příští zprávu
sei();
while (1){}
}

ISR(TWI_vect){
static char data[POCET_BYTU]={0,1,2,3,4};
static unsigned int pocet=0; 

switch(TW_STATUS){								// čteme v jakém stavu se nachází TWI a podle toho reagujeme
case TW_ST_SLA_ACK:								// někdo nás adresoval s příznakem čtení (SLA+R), jsme v roli Slave Transmitter
	TWDR = data[0]; // nahrajeme první data k odeslání
	pocet=1; // příště budeme odesílat druhý byte dat
	TWCR = (1<<TWINT) | (1<<TWEN) | (1<<TWEA) | (1<<TWIE);	// odešli data, očekávej potvrzení
	break;
case TW_ST_DATA_ACK:							// odeslali jsme data a master je potvrdil	
	TWDR=data[pocet];							// připravíme další data k odeslání
	if(pocet<POCET_BYTU-1){pocet++;} 			// hlídejme přetečení pole
	TWCR = (1<<TWINT) | (1<<TWEN) | (1<<TWEA) | (1<<TWIE);	// odešli data, očekávej potvrzení
	break;
case TW_SR_STOP:								// přišla STOP sekvence nebo opakovaný START ?
	TWCR = (1<<TWINT) | (1<<TWEA) | (1<<TWEN) | (1<<TWIE);	// to nás nezajímá, čekej na další akci a dávej potvrzení
	break;	
default:
	TWCR = (1<<TWINT) | (1<<TWEA) | (1<<TWEN) | (1<<TWIE);	// pokud přijde cokoli nečekaného, čekej na další akci a dávej potvrzení
	} 
}

Program na straně mastru je o něco komplikovanější. Musí totiž rozpoznat poslední čtený byte a nedat slave obvodu potvrzení zprávy. Kdyby po posledním bytu zprávu potvrdil, slave by očekával, že bude odesílat další data a ponechal by si ovládání SDA linky. Tím by ale masteru znemožnil vygenerovat STOP sekvenci a ukončit komunikaci. Celá I2C sběrnice by se tak zablokovala. Ne každý slave nutně potřebuje aby poslední byte nebyl potvrzen, ale Atmel jako slave nejspíše ano. Proto by to měl program na straně masteru respektovat. Jinak v programu pro master není žádná záludnost. Po stisku tlačítka se zavolá funkce i2c_precti_pole() jejímž prvním argumentem je slave adresa, druhým argumentem je ukazatel na pole kde máme přijatá data uložit a posledním argumentem je počet bytů které má vyčíst. Uvnitř funkce je generována START sekvence, pak se posílá slave adresa s příznakem čtení. Pokud slave svoji adresu potvrdí dáme pokyn TWI modulu aby provedl příjem jednoho byte a dal slave obvodu potvrzení. To opakujeme tak dlouho než dojdeme k poslednímu byte. Ten musíme přijmout, ale nesmíme dát potvrzení aby slave pochopil že komunikace končí a že má SDA linku uvolnit. Po přijetí posledního byte už jen vygenerujeme STOP sekvenci a přenos je dokončen. Program pak ještě zkontroluje poslední přijatá data a pokud odpovídají předpokládané hodnotě (0x04) tak blikne s LED diodou. Tu máme připojenou na PA5. Opět je to jen výuková záležitost, v praxi pak přijatá data zpracujete jinak. Případně si jako poslední byte zprávy necháte poslat CRC kód a provede kontrolu neporušenosti dat. Funkce je napsaná docela krkolomně, za což se vám omlouvám. Jestli mě napadne elegantnější způsob jak se vypořádat s while smyčkou určitě příklad změním. Kdyby napadl vás, nechte mi vzkaz v komentářích. Potěšíte mě.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
//C) Přenos více bytů Slave >> Master (kód pro master)
#include <avr/io.h>
#define F_CPU 8000000
#include <util/delay.h>
#include <util/twi.h>

#define SLV1_ADDRESS 0x2 // adresy různých slave zařízení na sběrnici
#define SLV2_ADDRESS 0x3
#define POCET_BYTU 5 

#define TW_SEND_START TWCR = (1<<TWINT)|(1<<TWSTA)|(1<<TWEN)
#define TW_SEND_STOP TWCR = (1<<TWINT)|(1<<TWEN)|(1<<TWSTO)
#define TW_SEND_DATA TWCR = (1<<TWINT) | (1<<TWEN)
#define TW_RECEIVE_DATA TWCR = (1<<TWINT) | (1<<TWEN)
#define TW_WAIT while(!(TWCR & (1<<TWINT)))
#define TLAC1 (PINA & (1<<PINA7))
#define TLAC2 (PINA & (1<<PINA6)) 

char i2c_precti_pole(char adresa, char *pole, unsigned int pocet_bytu);
char tlac1=0, tlac2=0;
char data[POCET_BYTU]={};           // do tohoto pole přijímáme byty ze slave

int main(void){
 TWBR=32;                           // žádná předdělička, F_SCL = 8MHz/(16+2*TWBR) = 100kHz.
 TWCR = (1<<TWEN);                  // zapnout TWI modul
 DDRA |= (1<<DDA5);                 // LEDka	
 DDRA &=~((1<<DDA7) | (1<<DDA6));   // dvě tlačítka
 PORTA = (1<<PORTA7) | (1<<PORTA6); // pull-up pro tlačítka
 while(1){
  if(!TLAC1 && tlac1==0){           // po stisku tlačítka 1
   tlac1=1;                         // zamči tlačítko (do uvolnění)
   i2c_precti_pole(SLV1_ADDRESS,data,POCET_BYTU); // přečti ze slave1 POCET_BYTU
   if(data[4]==4){PORTA ^= (1<<PORTA5);}          // pokud jsou data korektní blikni LED
  }
  if(!TLAC2 && tlac2==0){
   tlac2=1;                                       // zamči talčítko (do uvolnění)
   i2c_precti_pole(SLV2_ADDRESS,data,POCET_BYTU); // přečti ze slave2 POCET_BYTU
   if(data[4]==4){PORTA ^= (1<<PORTA5);}          // pokud jsou data korektní blikni LED
  }			
  if(TLAC1 && TLAC2){               // tlačítka uvolněna
   tlac1=0;                         // odemči talčítka 
   tlac2=0;
  }
  _delay_ms(100);                   // ošetření zákmitů		
 }
}


char i2c_precti_pole(char adresa, char *pole, unsigned int pocet_bytu){
unsigned int i;
adresa = adresa << 1;                     // zarovnání adresy na 8bit formát
TW_SEND_START;                            // vygenerovat START sekvenci

TW_WAIT;                                  // počkat na odezvu TWI
if ((TW_STATUS) != TW_START){ return 1;}  // pokud nebyl start vygenerován, máme error a končíme
TWDR = adresa | 1;                        // nahrát adresu slave s příznakem čtení (SLA+R) 
TW_SEND_DATA ;                            // odeslat adresu 

TW_WAIT;                                  // počkat na odezvu TWI
if ((TW_STATUS) != TW_MR_SLA_ACK){TW_SEND_STOP; return 2;} 
// jsme v režimu master-receiver, poslali jsme slave adresu s příznakem čtení a slave nám dal potvrzení
TW_RECEIVE_DATA  | (1<<TWEA);             // vyčti 1 byte ze slave a dávej potvrzení
i=0;                                      // vynulovat počítadlo přijatých bytů
while(i<pocet_bytu){                      // dokud jsme nepřijali všechny byty
 TW_WAIT;                                 // počkáme na odezvu TWI
 switch(TW_STATUS){
  case TW_MR_DATA_ACK:                    // přijali jsme byte a potvrdili zprávu
   pole[i]=TWDR;                          // nahrát i-tý prvek pole
   if(i<pocet_bytu-1){i++;}               // inkrementovat počítadlo přijatých bytů, pokud nedosáhlo maxima
   if(i>=(pocet_bytu-1)){TW_RECEIVE_DATA;}// přijímáme-li poslední byte přijmi 1 byte a NEDÁVEJ potvrzení
   else{TW_RECEIVE_DATA  | (1<<TWEA);}    // vyčti 1 byte a DEJ potvrzení
   break;

  case TW_MR_DATA_NACK:                   // přijali jsme data a nedali potvrzení (poslední byte)
   pole[i]=TWDR;                          // nahrát i-tý prvek pole (poslední)
   i++;                                   // inkrementovat počítadlo přijatých bytů - ukončí while cyklus
   break;

  default:
   TW_SEND_STOP;                          // pokud nastal jakýkoli problém, posíláme STOP a končíme fci
   return 2;
  }  	 	
 }	
TW_SEND_STOP;                             // konec komunikace
return 0;
}

D) TWI - jednoduchá obousměrná komunikace

Tento příklad bude asi tím nejdůležitějším z celého návodu, protože se podobá typické komunikaci s čidly a jinou I2C havětí. Připravíme si slave, který má v paměti uloženo 5 bytů. Slave bude schopen přijmout jeden byte, který bude fungovat jako "ukazatel" na některý z těchto 5 bytů. Slave nejprve odešle ten byte který si master vyžádal a pak popořadě všechny další. Pošle-li master například 0x03, slave mu nejprve odešle hodnotu data[3], pak hodnotu data[4]. Master si díky této vlastnosti může vybírat co chce ze slave číst. Stejný postup se používá při vyčítání dat například z ADXL345, LM75, DS3231 a mnoha dalších obvodů. Nejprve odešlete 1 byte do čidla. A čidlo díky tomu ví která část paměti vás zajímá, když pak z něj začnete vyčítat, posílá vám obsah své paměti od místa, které jste specifikovali.

Stejně jako v předchozích příkladech master komunikuje se slave po stisku tlačítka. Změn doznala komunikační funkce transakce(). Jejím prvním argumentem je slave adresa obvodu se kterým chceme komunikovat. Druhý argument je byte, který chceme do slave poslat (adresa dat, které z něj chceme vyčítat). Třetí argument je ukazatel na pole do kterého budeme přijatá data ukládat. Poslední argument je počet bytů, které chceme přijmout. Ten nesmí být menší jak 2. Funkce prostě není chytře napsaná aby mohla přijímat pouze jeden byte. Uvnitř funkce probíhá vše podle očekávání. Generuje se START sekvence, následovaná slave adresou s příznakem zápisu. Následně je do slave zapsán jeden byte. Potom se generuje opakovaný START následovaný slave adresou s příznakem čtení. Dále funkce přijme požadovaný počet bytů a po posledním z nich nedá potvrzení. Komunikace končí STOP sekvencí.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
//D) jednoduchá obousměrná komunikace (kód pro master)
#include <avr/io.h>
#define F_CPU 8000000
#include <util/delay.h>
#include <util/twi.h>

#define SLV1_ADDRESS 0x2 // adresy různých slave zařízení na sběrnici
#define SLV2_ADDRESS 0x3
#define POCET_BYTU 2 

#define TW_SEND_START TWCR = (1<<TWINT)|(1<<TWSTA)|(1<<TWEN)
#define TW_SEND_STOP TWCR = (1<<TWINT)|(1<<TWEN)|(1<<TWSTO)
#define TW_SEND_DATA TWCR = (1<<TWINT) | (1<<TWEN)
#define TW_RECEIVE_DATA TWCR = (1<<TWINT) | (1<<TWEN)
#define TW_WAIT while(!(TWCR & (1<<TWINT)))
#define TLAC1 (PINA & (1<<PINA7))
#define TLAC2 (PINA & (1<<PINA6)) 

char transakce(char slave_adresa, char adresa, char *data, unsigned int pocet_bytu);
char tlac1=0, tlac2=0;
char data[POCET_BYTU]={};           // do tohoto pole přijímáme byty ze slave

int main(void){
 TWBR=32;                           // žádná předdělička, F_SCL = 8MHz/(16+2*TWBR) = 100kHz.
 TWCR = (1<<TWEN);                  // zapnout TWI modul	
 DDRA &=~((1<<DDA7) | (1<<DDA6));   // dvě tlačítka
 PORTA = (1<<PORTA7) | (1<<PORTA6); // pull-up pro tlačítka
 while(1){
  if(!TLAC1 && tlac1==0){           // po stisku tlačítka 1
   tlac1=1;                         // zamči tlačítko (do uvolnění)
   transakce(SLV1_ADDRESS,1,data,POCET_BYTU); // přečti dva byty ze slave1 od adresy 1
  }
  if(!TLAC2 && tlac2==0){
   tlac2=1;                         // zamči tlačítko (do uvolnění)
   transakce(SLV2_ADDRESS,1,data,POCET_BYTU); 	// přečti dva byty ze slave2 od adresy 1
  }			
  if(TLAC1 && TLAC2){               // tlačítka uvolněna
   tlac1=0;                         // odemči tlačítka 
   tlac2=0;
  }
  _delay_ms(100);                   // ošetření zákmitů		
 }
}

// arg1 - adresa slave zařízení se kterým chceme komunikovat
// arg2 - byte který bude do slave odeslán (info o tom co z něj chceme číst)
// arg3 - pole do kterého má fce přijatá data ukládat
// arg4 - počet bytů která má fce ze zařízení vyčíst, musí být
char transakce(char slave_adresa, char adresa, char *pole, unsigned int pocet_bytu){
unsigned int i;
slave_adresa = slave_adresa << 1;          	// zarovnání adresy na 8bit formát
TW_SEND_START;                             	// vygenerovat START sekvenci

TW_WAIT;                                   	// počkat na odezvu TWI
if ((TW_STATUS) != TW_START){return 1;}   	// pokud nebyl start vygenerován, máme error a končíme
TWDR = slave_adresa;                       	// nahrát adresu slave s příznakem zápisu (SLA+W) 
TW_SEND_DATA;                              	// odešli adresu 

TW_WAIT;                                   	// počkat na odezvu TWI
if ((TW_STATUS) != TW_MT_SLA_ACK){TW_SEND_STOP; return 2;} // Slave nám nedal potvrzení, případně nastal jiný problém ? končíme komunikaci STOP sekvencí
TWDR = adresa;                             	// nahrát data která chceme poslat do slave
TW_SEND_DATA;                              	// odešli data

TW_WAIT;                                   	// počkat na odezvu TWI
if ((TW_STATUS) != TW_MT_DATA_ACK){TW_SEND_STOP; return 2;} // Slave nám nedal potvrzení, případně nastal jiný problém ? končíme komunikaci STOP sekvencí
TW_SEND_START;                              // vygeneruj opakovaný START

TW_WAIT;  
if ((TW_STATUS) != TW_REP_START){return 2;} // nepodařilo se vygenerovat START ?
TWDR = slave_adresa | 1;                    // slave adresa s příznakem čtení (SLA+R)
TW_SEND_DATA;                               // odešli slave adresu	

TW_WAIT;                                    // počkat na odezvu TWI
if ((TW_STATUS) != TW_MR_SLA_ACK){TW_SEND_STOP; return 2;} 
// jsme v režimu master-receiver, poslali jsme slave adresu s příznakem čtení a slave nám dal potvrzení
TW_RECEIVE_DATA  | (1<<TWEA);               // vyčti 1 byte ze slave a potvrď mu zprávu
i=0;										                    // vynulovat počítadlo přijatých bytů

while(i<pocet_bytu){                        // dokud jsme nepřijali všechny byty
 TW_WAIT;                                   // počkáme na odezvu TWI
 switch(TW_STATUS){
  case TW_MR_DATA_ACK:                      // přijali jsme byte a dali slave potvrzení
   pole[i]=TWDR;                            // nahrát i-tý prvek pole
   if(i<pocet_bytu-1){i++;}                 // inkrementovat počítadlo přijatých bytů, pokud nedosáhlo maxima
   if(i>=(pocet_bytu-1)){TW_RECEIVE_DATA;}  // přijímáme-li poslední byte, přijmi 1 byte a NEDÁVEJ potvrzení
   else{TW_RECEIVE_DATA  | (1<<TWEA);}      // vyčti 1 byte ze slave DEJ potvrzení
   break;
  case TW_MR_DATA_NACK:                     // přijali jsme byte a nedali slave potvrzení (poslední byte)
   pole[i]=TWDR;                            // nahrát i-tý prvek pole (poslední)
   i++;                                     // inkrementovat počítadlo přijatých bytů - ukončí while cyklus
   break;
  default:
   TW_SEND_STOP;                            // pokud nastal jakýkoli problém, posíláme STOP a končíme fci
   return 2;
  }  	 	
 }	
TW_SEND_STOP;                               // konec komunikace
return 0;
}

Slave má opět jednodušší roli jako master. V podstatě stačí potvrzovat zprávy a sledovat tři situace. Pokud nám master poslal datový byte, vyčteme přijatá data a uložíme je jako "ukazatel" pocet, který specifikuje jaká data se budou odesílat. Dále kontrolujeme zda nás někdo volal s příznakem čtení. V takovém případě připravíme první data k odeslání. A jako poslední stačí kontrolovat zda jsme odeslali data a dostali potvrzení. V takovém případě se od nás čeká odeslání dalších dat, takže je stačí připravit a nahrát do TWDR. Všechny ostatní situace nás v podstatě nezajímají. I když jsou v programu "ošetřeny" nemáme žádnou zajímavou akci, ktrou bychom při nich mohli udělat. Takže pouze čekáme na některou ze tří výše jmenovaných. Ukázka komunikace je na obrázku d1.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
//D jednoduchá obousměrná komunikace (kód pro slave)
#include <avr/io.h>
#define F_CPU 8000000
#include <util/twi.h>
#include <util/delay.h>
#include <avr/interrupt.h>

#define POCET_BYTU 5
#define MY_SLAVE_ADDRESS 0x02 // adresa tohoto zařízení, změňte ji pokud budete nahrávat kód do více slave

int main(void){
TWAR = (MY_SLAVE_ADDRESS<<1) | (1<<TWGCE);              // naše slave adresa, zarovnaná vlevo, General call povoleno
TWCR = (1<<TWEN) | (1<<TWEA) | (1<<TWIE) | (1<<TWINT);  // povolit TWI, povolit přerušení, potvrdit příští zprávu
sei();
while (1){}
}

ISR(TWI_vect){
static char data[POCET_BYTU]={0,1,2,3,4};
static unsigned int pocet; 

switch(TW_STATUS){                                      // čteme v jakém stavu se nachází TWI a podle toho reagujeme
case TW_SR_SLA_ACK:                                     // někdo nás adresoval s příznakem zápisu (SLA+W)
 TWCR = (1<<TWINT) | (1<<TWEA) | (1<<TWEN) | (1<<TWIE); // to nás nezajímá, čekej na další akci a dávej potvrzení
 break;
case TW_SR_DATA_ACK:                                    // někdo nám poslal data a my mu zprávu potvrdili
 pocet=TWDR;                                            // uložme přijatý byte, chápeme ho jako ukazatel na data, která po nás master chce
 TWCR = (1<<TWINT) | (1<<TWEA) | (1<<TWEN) | (1<<TWIE); // čekej na další akci a dávej potvrzení
 break;
case TW_ST_SLA_ACK:                                     // někdo nás adresoval s příznakem čtení (SLA+R)
 TWDR = data[pocet];                                    // nahrajeme první data k odeslání
 TWCR = (1<<TWINT) | (1<<TWEN) | (1<<TWEA) | (1<<TWIE); // odešli data, očekávej potvrzení
 break;
case TW_ST_DATA_ACK:                                    // odeslali jsme data a master je potvrdil
 if(pocet<POCET_BYTU-1){pocet++;}                       // připravíme další data k odeslání (a hlídáme přetečení)	
 TWDR=data[pocet];                                      // další data k odeslání
 TWCR = (1<<TWINT) | (1<<TWEN) | (1<<TWEA) | (1<<TWIE); // odešli data, očekávej potvrzení
 break;
case TW_SR_STOP:                                        // přišla STOP sekvence nebo opakovaný START ?
 TWCR = (1<<TWINT) | (1<<TWEA) | (1<<TWEN) | (1<<TWIE); // to nás nezajímá, čekej na další akci a dávej potvrzení
 break;	
default:
 TWCR = (1<<TWINT) | (1<<TWEA) | (1<<TWEN) | (1<<TWIE); // pokud přijde cokoli jiného, čekej na další akci a dávej potvrzení
 } 
}


Obrázek d1 - Zápis do slave následovaný vyčtením dvou bajtů. Červený otazník znamená nepotvrzení zprávy (což je správně).

Na tomto místě bych vám doporučil udělat si malý domácí úkol. Zkuste naprogramovat funkci, která ze slave vyčte jeden byte z požadované adresy. Prostě buď napište úplně novou jednoúčelovou funkci, nebo vylepšete tu moji tak aby nebyla limitovaná vyčítáním 2 a více bytů. Určitě se vám v budoucnu bude hodit.

Pár slov závěrem

Tento návod asi patřil k těm složitějším. Nejspíš to je tím, že se setkáváte s velkým množstvím nových pojmů a že většina z nich nemá ustálené české ekvivalenty. Kromě toho je problematika dosti komplexní a nemá "limity", takže se vám může zdát, že je ještě hodně věcí, ve kterých nemáte jasno. Nejspíš budete muset věnovat ještě dost času k získání zkušeností s provozem I2C a já doufám, že vás návod posunul co nejdál. Přiznám se, že sám s I2C čas od času zápasím a to ji používám pouze k jednoduchým úkolům. Tento návod tedy posloužil i mě abych si v některých otázkách udělal jasno.

Zajímavé odkazy

Hotové knihovny