V tomto tutoriálu si nejprve popíšeme jak funguje SPI periferie na AVR a pak si na čtyřech příkladech jeho ovládání předvedeme (Pokud se s SPI setkáváte poprvé, projděte si nejprve tento článek). První tři příklady budou demonstrovat ovládání Atmelů v rolích Master a Slave. Při nich by jste měli získat vhled do ovládání SPI modulu. Poslední příklad bude trochu neobvyklý. Předvedeme si v něm jak ovládat digitální potenciometr. Neobvyklé na něm bude to, že se budeme zabývat více digitálním potenciometrem než Atmelem. Jednak si na něm upevníte znalosti ovládání SPI na Atmelu a zadruhé tím možná získáte schopnost vybavit vaše návrhy velice užitečným obvodem.
Hardware sloužící k SPI komunikaci najdete nejspíš na všech čipech řady Atmega. Attiny používají USI (Universální seriové rozhraní), ale některé mají i SPI rozhraní (Attiny88). Skoro všichni SPI rozhraní Atmelu používáte i když o tom nemusíte vědět. ISP programátory totiž využívají SPI rozhraní čipu k nahrátí programu. Což do některých aplikací zavádí jisté komplikace, ale o tom až později. SPI na Atmelech umožňuje plně duplexní přenos s bitovou rychlostí až polovinu taktu procesoru. Může pracovat jako master i jako slave a pořadí bitů ve zprávě lze nastavovat softwarově. Krom toho může SPI generovat přerušení nebo atmel probrat ze spánku. Používání SPI v roli mastera je velice snadné, takže si ho určitě oblíbíte :D Ke komunikaci používáte dvě až čtyři linky. MISO, MOSI, SCK a v roli slave obvodu ještě SS pin. O funkci prvních třech jste se jistě dočetli v předchozím návodu (odkaz). Proto jen doplním roli SS pinu. Pokud je Atmel v roli slave obvodu, má SS pin přesně tu samou roli jako obecně CS (Chip select). Slouží k aktivaci Slave obvodu. Přivedením log.0 na tento vstup je slave obvod aktivován, nastavý svůj MISO pin jako výstup a může komunikovat. Naopak jestliže je jeho SS vstup držen v log.1, udržuje slave MISO linku ve stavu vysoké impadance (Hi-Z) a neprovádí žádnou komunikaci. To celé obstarává SPI modul v Atmelu sám. Velmi podobné chování má SS pin i když je Atmel v roli Mastera. Tam by se dalo očekávat že signál CS nemá žádný význam. A také nemá, pokud je SS pin (PB4 na Atmega16) nastaven jako výstup. Jestliže je ale SS pin nastaven jako vstup, tak je potřeba zajistit aby na něm byla log.1 (třeba interním pull-up rezistorem). Jakmile se na něm totiž objeví log.0, přepne se master do role slave ! To slouží k tomu aby spolu mohlo komunikovat více Master obvodů. O správě SS pinu se dozvíte více v prvním příkladu.
Fakt, že SPI rozhraní je používáno k programování čipu, přináší drobné komplikace a je dobré se s nimi vypořádat co nejdříve. Ti kteří se plácli přes kapsu, koupili Atmel-ICE a programují čip JTAG rozhraním nebo ti kteří využívají bootloader a programují UARTem mohou tuto kapitolu klidně přeskočit. Nejprve se na věc podíváme z pohledu programátoru. ISP programátor potřebuje možnost používat vývody RST, MISO, MOSI a SCK. Je tedy potřeba zajistit, že žádné další zařízení nebude některou z linek blokovat (třeba tak, že k některé z nich bude mít připojen svůj výstup). To lze zajistit snadno. Postačí když je od programovaného čipu sběrnice oddělena rezistory (viz schéma 1). Díky oddělovacím rezistorům není jakékoli zařízení na sběrnici schopné držet MISO, MOSI nebo SCK tvrdě, protože rezistor jejich výstupy "změkčí". Programovaný Atmel a programátor si na těchto pinech mohou vynutit hodnoty jaké potřebují. Příliš velké rezistory mohou ale snižovat maximální přenosovou rychlost (pokud je kapacita sběrnice větší). Hodnoty je proto dobré volit ve stovkách ohmů. Já používal zlatý střed 470R. Problémy mohou nastat u menších atmelů, kde může být některý z pinů sdílen třeba s AREF. Na ten by jste mohli chtít připojit kondenzátor k filtraci vnitřní reference. Kondenzátor na datové lince ale hrubě omezuje komunikační rychlost. U levných programátorů USBASP nemusí být možné snížit komunikační rychlost. Těm pak jakýkoli kondenzátor na kterékoli komunikační lince znemožňuje programování. Pak vám nezbývá než ručně nebo nějákým přepínačem přepojovat problematické piny mezi programátorem a zbytkem obvodu. To je ale jen okrajová záležitost. Trochu pozornosti by jste měli věnovat také tomu co se děje s SPI když čip restartujete. Pokud totiž čip držíte "v resetu" (RST v log.0) tak by na SPI neměl probíhat žádný provoz... Atmel by to mohl pochopit tak, že ho programujete :D Pokud bude Atmel jediným masterem na sběrnici, lze skoro vždy připojit sběrnici i programátor bez oddělovacích rezistorů. Pokud nechcete aby slave obvody během programování nepřijímaly "nesmysly", musíte pohlídat aby byly během programování deaktivovány. Žádný CS pin nesmí být v log.0, protože pak by slave přijímal a nějak interpretoval data tekoucí z programátoru do Atmelu. Typicky budete mít CS piny připojeny k vývodům Atmelu. Ten během programování držíte v resetu a všechny jeho vývody budou nastaveny jako vstupy bez pullup rezistorů (tak jako bezprostředně po restartu). Bude tedy ovykle nutné vybavit CS linky externím pull-up rezistorem (pokud už není součástí slave obvodu) aby v takovém případě zůstaly CS linky v log.1.
Řízení SPI se provádí pomocí tří registrů SPCR, SPSR a SPDR. SPDR slouží k manipulaci s daty. Zápisem do něj uložíte data do posuvného registru odkud se pak přímo odesílají. Nesmíte tedy do něj nezapisovat během přenosu. Pokud to uděláte, informuje vás o tom vlajka WCOL v registru SPSR. Pro čtení má SPDR vyrovnávací paměť. Pokud tedy z SPDR vyčtete data, tak nevidíte obsah posuvného registru, ale posledního přijatého byte. Díky tomu můžete přijímat zprávy bez prodlev. Jeden byte vám dorazí, a mezi tím než na to zareagujete a vyčtete jej, může probíhat příjem dalšího byte. Protože v roli slave nemáte žádnou možnost vysílací stranu pozdržet aby jste měli dost času si data z SPDR vyzvednout, tak je tato vlastnost nutností. Uvědomte si, že pro příjem i vysílání máte jen jeden posuvný registr. Pokud tedy do něj něco dorazí a vy jeho obsah nepřepíšete (zápisem do SPDR), tak se v příštím přenosu jeho obsah odvysílá. To se vám může hodit, může vás to rozčilovat a nebo vám to může být jedno. Užitečná je tato funkce pokud využíváte řetězení slave obvodů. Jedno vám to může být pokud váš výstup (MOSI pokud jste master a MISO pokud jste slave) nikam nevede a nebo data z něj nikoho nezajímají. V roli mastera se zápisem do SPDR rovnou spustí přenos.
Registr SPCR slouží k řízení periferie. SPI povolujete nebo zakazujete bitem SPE . Povolením převezme SPI kontrolu nad příslušnými vývody (podle režimu slave nebo master). Pomocí bitu SPIE povolujete přerušení, které je vyvoláno nastavením vlajky SPIF. Bit DORD nastavuje řazení dat ve zprávě. V log.1 se nejprve odesílá LSb, v log.0 pak MSb. Bity CPOL a CPHA nastavujete mód (viz obrázek na wiki). Nastavením bitu MSTR zapínáte režim master. Tady je na místě poznamenat, že pokud je vývod SS (PB4) masteru nastaven jako vstup a je na něj přivedena log.0, přepne se do role slave a bit MSTR se vynuluje. Tato funkce může posloužit pokud má pracovat více masterů na jedné sběrnici. Jeden master tak může ostatní mastery dočasně změnit na slave ("Zotročit" :D ) a odeslat jim zprávu. Dvojice bitů SPR1 a SPR0 slouží k nastavení přenosové rychlosti. Tu je dobré udržovat v rozumných mezích. Slave totiž ke spolehlivému přijetí zprávy potřebuje aby frekvence procesoru byla alespoň dvakrát větší jak komunikační rychlost (frekvence na SCK).
Nastavení módu SPI je shrnuto v tabulce 2. O módech jste se mohli dočíst v předešlých článcích zde (! přidat odkaz!). V tabulce 3 je seznam datových rychlostí. Kromě bitů SPR1 a SPR0 má na rychlost také vliv bit SPI2X z registru SPSR, který rychlost násobí dvěma. V roli slave nemá přirozeně nastavení těchto bitů žádný význam.
Registr SPSR obsahuje dvojici vlajek. Vlajku WCOL o které jsme již mluvili a která signalizuje zápis do SPDR během přenosu a vlajku SPIF. Ta signalizuje několik věcí. V prvé řadě dokončení přenosu. Ať v roli master nebo slave, jakmile je přenesen celý byte, tak se vlajka nastaví (a případně se volá přerušení). Pokud je čip v roli masteru a je pomocí SS pinu přepnut do role slave, vlajka SPIF se nastaví také. Smazání obou vlajek se provádí čtením z registru SPSR a následným přístupem k SPDR (čtením nebo zápisem do něj). Tento přístup je logický. Po ukončení přenosu (o němž se dozvíte čtením SPSR) by jste měli z SPDR vyčíst přijatá data a případně do něj zapsat nová. Čímž se vlajka smaže... Pokud máte povolené přerušení tak se o mazání vlajky nemusíte starat, maže se sama při příchodu do rutiny přerušení. Zbytek teorie si vysvětlíme na příkladech.
Protože se tutoriál věnuje tomu jak ovládat Atmel a nezaměřuje se na ovládání různých jiných integrovaných obvodů s SPI, bude vězšina příkladů určena pro komunikaci Atmel - Atmel (Master - slave). Existuje pro to několik důvodů. V prvé řadě nechci volit nějáký konkrétní integrovaný obvod na kterém bych komunikaci ukazoval. Byli by jste totiž závislí na tom si ho opatřit. Dále bych musel návod "zamlžit" vysvětlením jak zmíněný obvod používat a jak s ním komunikovat. V tom všem by se práce s SPI rozhraním na Atmelu mohla ztrácet. A krom toho všeho by jste přišli o ukázky jak používat atmel jako slave. V praxi budete nejčastěji používat Atmel jako master a to budeme v příkladech také, takže nemusíte mít strach, že by jste přišli zkrátka.
Jednosměrný přenos je jednoduchý a příklad (v roli mastera) určitě využijete při ovládání ADC převodníků, digitálních potenciometrů a obecně integrovaných obvodů, kterým ke své činnosti stačí přijímat. Připojení v takovém případě stačí pomocí linky SCK, MOSI a CS. U jednosměrné komunikace Master >> Slave není linka MISO potřebná (slave nemá co vysílat). Některé slave obvody ani žádnou výstupní linku mít nemusí. Aplikace bude vypadat následovně. Master bude obsluhovat tlačítko (na PA0) a posílat pomocí SPI do slave obvodu informaci o tom zda je stisknuto nebo uvolněno. Slave obvod bude svítit LED (na PB1) po dobu kdy je tlačítko stisknuto. Komunikaci uděláme úspornou. Jakmile uživatel talčítko stiskne, odešle master zprávu o tom že je stisknuto a odmlčí se do té doby než uživatel tlačítko uvolní. V tom okamžiku o tom opět informuje slave obvod. Slave obvod budeme aktivovat pinem PB3. PB3 masteru tedy musíme připojit na SS pin slave.
Tady je na místě udělat malou zastávku nad konfigurací MISO, MOSI, SS a SCK pinů. Když pomocí bitu SPE SPI povolíte a jste v roli master, tak Atmel nastaví pin MISO striktně jako vstup. Linky MOSI, SCK nechá nastavané tak jak jste to udělali v DDRB registru. Je proto potřeba nastavit ručně MOSI a SCK jako výstup. Role masteru má ještě jedno úskalí - pin SS (PB4). V teoretické části jsme zmínili, že pomocí SS pinu je možné master přepnout do role slave. To jde jen v případě že je SS pin nastaven jako vstup. Jestliže tuto vymoženost nechceme používat, musíme na pinu SS udržovat log.1 a nebo ho nastavit jako výstup. A to i přes to že bychom ho nechtěli používat vůbec. Buď jej tedy necháme jako vstup a zapneme na něm pull-up rezistor (tím si zachováme možnost nechat se přepnout do role slave) a nebo SS pin nakonfigurujeme jako výstup a pak jej můžeme používat k libovolným účelům. V mém příkladě jsem využil první možnosti a zapnul pull-up. Piny PB5 (MOSI) a PB7 (SCK) musíme nastavit jako výstupy. V roli slave je situace opačná. Tedy po spuštění SPI se piny MOSI, SCK a SS nastaví jako vstupy a pin MISO si ponechá konfiguraci podle DDRB. Tedy jen v situaci že je slave aktivován. Deaktivovaný slave má MISO vždy ve stavu vysoké impadance (prostě odpojený). Pokud má tedy slave odesílat nějáká data, je potřeba mu v DDR registru nastavit pin MISO jako výstup. Těm pinům, které jsou aktivováním SPI nastaveny jako vstup můžeme ještě stále zapnout nebo vypnout pull-up rezistor, pokud to potřebujeme.
Samotný zdrojový kód pro master je docela průhledný. Nejprve konfigurujeme vstupy a výstupy. Tlačítko (PB0) jako vstup s pull-up rezistorem, SCK a MOSI jako výstupy. Pin PB3 používáme k aktivaci a deaktivaci slave, takže jej nastavujeme také jako výstup. Na PB4 (SS) zapínáme pull-up abychom dostáli podmínce že v roli masteru musí být SS pin buď v log.1 nebo nastaven jako výstup. Ihned po konfiguraci pinů nastavíme PB3 do log.1 abychom slave deaktivovali (aktivovat ho budeme až těsně před komunikací). V regisru SPCR nastavíme bity SPE čímž spustíme SPI, MSTR čímž se přepneme do role mastera a SPR0 čímž nastavíme datovou rychlost na F_CPU/16 = 500kbit/s. Ponecháním bitů CPOL a CPHA v nule nastavujeme komunikační mód 0. Od tohoto okamžiku je SPI aktivní. Jakmile bychom zapsali data do SPDR začaly by se okamžitě vysílat. Program pak sleduje stisk a uvolnění tlačítka. Pokud k některé z těchto událostí dojde odvysílá se 1 byte dat. Buď hodnotu 1 nebo 2, podle toho ke které události došlo. Vysílání vypadá jednoduše. Nejprve aktivujeme slave (PB3 do log.0), pak zapíšeme data do SPDR. V tom okamžiku se začnou odesílat. Pak ve smyčce stále skenujeme vlajku SPIF a čekáme na její nastavení. Jakmile je nastavena, víme že přenos skončil. Deaktivujeme slave obvod a vyčteme registru SPDR aby se vlajka smazala. Pokud bychom vyčtení opomenuli, vlajka by zůstala nastavená a při příštím vysílání bychom nebyli schopni správně rozpoznat konec přenosu. S největší pravděpodobností bychom pak slave obvod deaktivovali příliš brzy (ještě před koncem přenosu) a data by se nepřenesla. Takže na to nezapomínejte ... nebo používejte přerušení, které maže vlajku za vás :)
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 | // A jednoducý přenos master >> slave (kód pro master) #include <avr/io.h> #define F_CPU 8000000 #define TLAC (PINB & (1<<PINB0)) #define CS_L PORTB &=~(1<<PORTB3) #define CS_H PORTB |=(1<<PORTB3) char tlac=0; char spi_prenos(char data); int main(void){ // PB3 - výstup pro řízení SS pinu slave obvodu DDRB = (1<<DDB3) | (1<<DDB5) | (1<<DDB7); // MOSI a SCK (PB5 a PB7) výstup PORTB = (1<<PORTB4) | (1<<PORTB0); // pull-up na tlačítko, pull-up na SS pin ! CS_H; // deaktivovat slave SPCR = (1<<SPE) | (1<<MSTR) | (1<<SPR0); // povolit SPI, nastavit Master, clock F_CPU/16 while (1){ if(!TLAC && tlac==0){tlac=1;spi_prenos(1);}// detekce stisku tlačítka + odeslání informace if(TLAC && tlac==1){tlac=0;spi_prenos(2);} // detekce uvolnění tlačítka + odeslání informace } } char spi_prenos(char data){ CS_L; // aktivuje slave obvod SPDR = data; // předej data k odeslání while(!(SPSR & (1<<SPIF)));// počkej na skončení přenosu CS_H; // deaktivuj slave obvod return SPDR; // smaže vlajku SPIF (hodnota nás tady ale nezajímá) } |
Program pro slave obvod je velice jednoduchý. Nastavíme si výstup pro LED. Protože je komunikace jednosměrná a slave nemá co vysílat, nemuseli bychom si nastavovat MISO (PB6) jako výstup. Ale pro zajímavost to uděláme. Protože do SPDR nezapisujeme, uvidíte, že slave bude vysílat data, která během minulého přenosu přijal. SS pin necháváme jako vstup ale pro jistotu zapneme Pull-up rezistor. Tím máme zajištěno že i kdyby master nepracoval nebo byl odpojen, bude slave obvod deaktivován a nebude zasahovat do dění na sběrnici. V našem případě by to ale nemělo mít žádný vliv. Povolení SPI provedeme stejně jako u masteru nastavením bitu SPE. Ponecháním bitů CPOL a CPHA v nule nastavujeme komunikační mód 0. V hlavní smyčce čekáme na nastavení vlajky SPIF která bude signalizovat příjem 1 byte. Jakmile přijde, vyčteme data z SPDR čímž se smaže vlajka a systém je připraven pro přijetí dalších dat. Pak už jen podle přijatého byte rozsvítíme nebo zhasneme LED. Jestliže odpojíte SS pin na slave obvodu, nebude ho master schopen aktivovat a slave nebude zprávy přijímat a zůstane odpojený od linky MISO. Zkuste si.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | // A jednoducý přenos master >> slave (kód pro slave) #include <avr/io.h> #define F_CPU 8000000 char tlac=0, x=0; char spi_prenos(char data); int main(void){ DDRB = (1<<DDB6) | (1<<DDB1); // MISO (PB6) a LED (PB1) výstupy PORTB = (1<<PORTB4); // SS (PB4) pull-up (pro jistotu) SPCR = (1<<SPE); // povolit SPI while (1){ while(!(SPSR & (1<<SPIF))){} // čekej až přijdou data x=SPDR; // přečti je if(x==1){PORTB |= (1<<PORTB1);} // a rozsviť ... else{PORTB &=~(1<<PORTB1);} // nebo zhasni LED } } |
Na obrázku a1 vidíte regulérní komunikaci. Master vysílá kód 1, tedy informaci o tom že jsem stiskl tlačítko. Na MISO výstupu slave obvodu vidíme hodnotu 2. Ta totiž zbyla v posuvném registru od poslední zprávy, kterou master do slave poslal (což bylo uvolnění tlačítka s kódem 2). Bystré oko uvidí lehké zkosení vzestupných a sestupných hran. Ty jsou způsobeny oddělovacími rezistory, které i s minimální kapacitou sběrnice tvoří RC články, které drobně omezují rychlost přeběhu. Při vyšších rychlostech by měl jev větší vliv. Na obrázku 2 pak vidíte situaci, kdy je SS pin slave obvodu odpojen. Master ho není schopen aktivovat a slave data nepřijímá a ani žádné nevysílá. Podle přeslechů na lince MISO je evidentní, že ji slave nechal odpojenou a kapacitní vazbou se na ni částečně přenáší signál ze sousedních linek (MOSI a SKC).
Obrázek a1 - přenos hodnoty 1 do slave (komunikační mód 0) | Obrázek a2 - slave neaktivován (CS v log.1), MISO linka ve vysoké impadanci a podléhá vlivům okolního signálu |
V tomto příkladě si předvedeme jednosměrnou komunikaci ve které bude master přijímat data ze slave. Slave necháme pracovat s přerušením. V této sestavě master nepotřebuje linku MOSI, protože nic nevysílá. To ale neznamená, že ji může volně použít k jiným účelům. Pokud nechceme aby ji SPI ovládal, musíme ji nechat nastavenou jako vstup. Slave obvod bude sledovat stav tlačítka a podle toho zda je stisknuto nebo ne bude odesílat příslušný kód po SPI. Protože ale slave nemůže sám do sebe zahájit komunikaci, musí se ho master pravidelně "doptávat". Tady by bylo na místě dát slave obvodu nějákou možnost informovat master že jsou pro něj k dispozici nová data. A to se také typicky dělá. Zařízení na SPI sběrnici sdílejí ještě jednu samostatnou linku (IRQ linku) ke které jsou připojeni otevřeným kolektorem. Každé zařízení na této lince smí vytvořit log.0 a informovat tím master že má některý ze slave obvodů nová data. Master pak typicky zahájí komunikaci s každým slave obvodem aby zjistil od koho ta informace pochází. Konfigurace ale může vypadat i tak, že každý slave (nebo vybraná skupinka) má svou vlastní linku, kterou dává masterovi najevo přítomnost nových dat. K tomu se typicky využívá externí přerušení. Master díky takové konfiguraci zavčas raguje na jakoukoli změnu a nemusí se stále doptávat slave obvodů zda nemají nová data. Jak jsme ale řekli, mi takovou funikci nemáme a tak necháme master každých 50ms přečíst data ze slave. Čtení z hlediska masteru vypadá v podstatě stejně jako vysílání a proto funkce spi_prenos() nedoznala žádných změn. Opět přenos začíná aktivováním slave obvodu (PB3 do log.0), poté zápisem čehokoli do SPDR. Zapsaná hodnota nemá význam, protože MOSI linka je nastavena jako vstup a žádná data neodejdou. Zapsat do SPDR ale nutné je, protože tím se spustí přenos a master začne generovat clock. Ten je nezbytně nutný aby slave mohl svoje data odeslat. Po skončení komunikace (po nasavení vlajky SPIF) je slave deaktivován (PB3 do log.1) a přijatá data jsou vyčtena z SPDR (čímž se také smaže vlajka SPIF). Master pak podle přijatých dat rozsvítí nebo zhasne led na PB1.
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 | // B jednoducý přenos slave >> master (kód pro master) #include <avr/io.h> #define F_CPU 8000000 #include <util/delay.h> #define TLAC (PINB & (1<<PINB0)) #define CS_L PORTB &=~(1<<PORTB3) #define CS_H PORTB |=(1<<PORTB3) char x=0; char spi_prenos(char data); int main(void){ // PB3 - výstup pro řízení CS pinu slave obvodu, PB1 LEDka DDRB = (1<<DDB3) | (1<<DDB7) | (1<<DDB1); // SCK (PB7) výstup PORTB = (1<<PORTB4); // pull-up na tlačítko, pull-up na SS pin ! CS_H; // deaktivovat slave SPCR = (1<<SPE) | (1<<MSTR); // povolit SPI, nastavit Master, clock F_CPU/4 while (1){ _delay_ms(50); // každých 50ms... x=spi_prenos(0); // přečti data ze slave if(x==1){PORTB &=~(1<<PORTB1);} // rozsviť nebo... else{{PORTB |= (1<<PORTB1);}} // zhasni LED } } char spi_prenos(char data){ CS_L; // aktivuje slave obvod SPDR = data; // předej data k odeslání while(!(SPSR & (1<<SPIF))); // počkej na skončení přenosu CS_H; // deaktivuj slave obvod return SPDR; // vyčti přijatá data // krom toho také smaže vlajku SPIF } |
Program pro slave je tadičně jednodušší. Zapneme pullup na tlačítku (PB0) a na SS pin (PB4), i když zde být nemusí. MISO konfigurujeme jako výstup, protože ze slave chceme číst data. Povolíme SPI a nastavením bitu SPIE povolíme přerušení od SPI (v tomto případě od příjmu dat). Přirozeně nezapomeneme povolit přerušení globálně pomocí funkce sei(). Předplníme SPDR nějákými daty (aby od nás master něco přijal) a necháme slave ať se fláká v prázdné smyčce. Jakmile master dokončí přenos dat, vyvolá to u slave přerušení. V něm slave obvod zjistí stav tlačítek a podle toho naplní SPDR hodnoutou 1 nebo 2. Aby si master v příštím přenosu mohl přečíst jaký byl stav tlačítek. Je evidentní, že master dostává vždy 50ms starou hodnotu. Může vás napadnout, že bychom měli nechat slave v hlavní smyčce stále snímat stav tlačítek a ihned podle toho nastavovat SPDR. Master by pak četl vždy aktuální stav tlačítek a ne 50ms starý. To ale není tak jednoduché. V roli slave nemáme moc prostředků jak poznat, že přenos probíhá. A pokud bychom to udělali špatně, mohlo by se nám stát že bychom do SPDR zapsali během přenosu ! Což by mělo neblahé následky na přenášená data. Dozvěděli bychom se o tom od vljaky WCOL, ale to už by bylo pozdě... master by obdržel nesmysly. Trochu bezpečnější by bylo sledovat stav SS pinu, pokud je v log.1 víme že s námi master nekomunikuje a že můžeme upravovat hodnotu v SPDR. Ale opět nemáme záruku, že master nestihl zahájit přenos mezi okamžikem kdy jsme stav SS pinu přečetli a kdy měníme SPDR (ono to totiž chvíli trvá). Nabízejí se dvě řešení. Nastavit v masteru pevné spoždění mezi aktivováním slave a zahájením přenosu dat. Pak by slave měl časovou rezervu na zápis do SPDR a nebo dát slave obvodu další linku aby mohl informovat master o přítomnosti nových dat... u tlačítka ale 50ms nehraje žádnou roli.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | // B jednoducý přenos slave >> master (kód pro slave) #include <avr/io.h> #define F_CPU 8000000 #include <avr/interrupt.h> #define TLAC (PINB & (1<<PINB0)) int main(void){ DDRB = (1<<DDB6); // MISO (PB6) výstup PORTB = (1<<PORTB4) | (1<<PORTB0); // SS (PB4) pull-up, PB0 tlačítko if(TLAC){SPDR=1;}else{SPDR=2;} // nahrej nynější stav tlačítka SPCR = (1<<SPE) | (1<<SPIE); // povolit SPI sei(); // globální povolení přerušení while (1){} // flákej se } ISR(SPI_STC_vect){ if(TLAC){SPDR=1;}else{SPDR=2;} // připrav pro přenos nynější stav tlačítka } |
Na obrázku b1 vidíte záznam přenosu. Slave odesílá hodnotu 1, takže bylo tlačítko uvolněno. MOSI linka je odpojená (ve vysoké impadanci - je vstupem), takže se na ní parazitně vážou stavy okolních signálů. Rychlost komunikace je nastavena na 8MHz/4 tedy na 2Mbit/s. Což je nejrychlejší možná konfigurace pokud jsou slave i master taktovány 8MHz. Slave běžící na 8MHz vyžaduje aby byla datová rychlost nižší než 4MHz. Master na 8MHz ale může vytvořit přesně 4MHz (nastaven bit SPI2X) nebo 2MHz (viz tabulka 3). A nic mezi tím. Rovné 4MHz použít nemůžeme, protože slave vyžaduje aby to byla frekvence nižší.
Ve třetím příkladě sloučíme obě předchozí varianty do jednoho. Slave i master budou mít tlačítko (PA1) a LED diodu (PA0). Master bude se slave komunikovat každých 50ms. Během přenosu si slave i master navzájem pošlou informaci o stavu tlačítka a každý z nich rozsvítí nebo zhasne LED. Tlačítko připojené ke slave bude tedy ovládat LED u master Atmelu a obráceně. Tak jak ve všech předchozích příkladech musí komunikaci řídit master, slave ji sám zahájit nemůže. Když chce tedy master přijmout nějáká data ze slave musí mu vždy nějáká data poslat ! Odeslaná data mohou a nemusejí dávat smysl, to už záleží na tom jak je slave obvod interpretuje. Prostě to jinak nejde, protože nějáká logická hodnota na lince MOSI vždy je, data tedy VŽDY tečou oběma směry. V našem příkladu dávají všechna data smysl. Master odesílá hodnotu 1 pokud je jeho tlačítko stisknuté a hodnotu 2 pokud není. Slave v tom samém okamžiku přenáší hodnotu podle svého tlačítka (kterou ale zjistil ke konci posledního přenosu, tedy odesílá informaci 50ms starou). Pokud bychom po slave obvodu chtěli aktuální informaci museli bychom mu nějak sdělit ke kterému okamžiku nás stav tlačítka zajímá. Uvažujeme-li pouze SPI sběrnici, máme jen tři události, které na straně slave můžeme detekovat.
// C obousměrný přenos (kód pro slave) #include <avr/io.h> #define F_CPU 8000000 #include <avr/interrupt.h> #define TLAC (PINA & (1<<PINA1)) int main(void){ DDRA = (1<<DDA0); // výstup LED DDRA &=~(1<<DDA1); // vstup tlačítko PORTA |=(1<<PORTA1); // pull-up tlačítko DDRB = (1<<DDB6); // MISO (PB6) výstup (ostatní vstup) PORTB = (1<<PORTB4); // SS (PB4) pull-up if(!TLAC){SPDR=1;}else{SPDR=2;} // nahrej nynější stav tlačítka SPCR = (1<<SPE) | (1<<SPIE); // povolit SPI sei(); // globální povolení přerušení while (1){} // flákej se } ISR(SPI_STC_vect){ // přečti SPDR, zhasni nebo rozsviť LED if(SPDR==1){PORTA |=(1<<PORTA0);}else{PORTA &=~(1<<PORTA0);} // připrav pro přenos nynější stav tlačítka if(TLAC){SPDR=1;}else{SPDR=2;} }
Program pro master také není nijak zvlášť komplikovaný. LEDka je na PA0, tlačítko na PA1. Pin PB4 držíme pullupem v logické 1, čímž umožníme pracovat v režimu master (jako ve všech předchozích příkladech). V hlavní smyčce pak každých 50ms čteme stav tlačítka, posíláme o něm informaci slave obvodu a zároveň přijímáme informaci o stavu jeho tlačítka. Na jejím základě pak rozsvěcíme nebo zhasínáme LED.
// C obousměrný přenos (kód pro master) #include <avr/io.h> #define F_CPU 8000000 #include <util/delay.h> #define TLAC (PINA & (1<<PINA1)) #define CS_L PORTB &=~(1<<PORTB3) #define CS_H PORTB |=(1<<PORTB3) char tx=0,rx=0; char spi_prenos(char data); int main(void){ // PB3 - výstup pro řízení CS pinu slave obvodu DDRB |= (1<<DDB3) | (1<<DDB5) |(1<<DDB7); // SCK (PB7), MOSI (PB5) výstup PORTB |= (1<<PORTB4); // pull-up na SS pin ! PORTA |=(1<<PORTA1); // pull up na tlačítko; DDRA |= (1<<DDA0); // výstup na LED CS_H; // deaktivovat slave SPCR = (1<<SPE) | (1<<MSTR) | (1<<SPR0); // povolit SPI, nastavit Master, clock F_CPU/16 while (1){ _delay_ms(50); // každých 50ms... if(!TLAC){tx=1;}else{tx=2;} // zjisti stav tlačítka rx=spi_prenos(tx); // pošli jej do slave a přečti si jeho informaci if(rx==1){PORTA &=~(1<<PORTA0);} // rozsviť nebo... else{PORTA |= (1<<PORTA0);} // zhasni LED, podle toho co slave poslal } } char spi_prenos(char data){ CS_L; // aktivuje slave obvod SPDR = data; // předej data k odeslání while(!(SPSR & (1<<SPIF))); // počkej na skončení přenosu CS_H; // deaktivuj slave obvod return SPDR; // vyčti přijatá data (a smaž vlajku SPIF) }
Zde bych s příklady Atmel-Atmel skončil. Ne, že by nebylo co ukazovat. Přinejmenším by bylo možné předvést ukázku že slave může spát a probudit se až s přijetím zprávy (což by bylo užitečné v příkladu A). Nebo by se dalo předvést jak se navzájem mohou adresovat dva Master obvody. Případně bychom si mohli předvést protokol, který v první části zprávy pošle slave informaci o tom co po něm master chce a ve zbytku přenosu slave vybranou informaci odešle zpět do masteru. Bohužel všechny tyto příklady obsahují nějáký malý háček, jehož analýza by byla relativně náročná. Určitě by to mohlo být zajímavé, ale ve frontě čekají daleko jednodušší a pro vaši praxi asi i přínosnější příklady a já bych vás od nich nerad zdržoval. Proto tyto problémy nechám nevyřešené.
Většina vašich aplikací bude SPI využívat k ovládání nějákého integrovaného obvodu a ne ke komunikaci Atmel-Atmel, jak jsme si předváděli v předchozích příkladech. Proto jsem mezi příklady zařadil i ukázku ovládání digitálního potenciometru. Volba na MCP4251 padla z několika důvodů. Pro ukázky jsem potřeboval relativně jendoduché zařízení abyste se neztráceli v jeho struktuře. Zároveň jsem také chtěl aby pro vás bylo užitečné a aby bylo k sehnání. Kromě mnoha dalších možností využití může digitální potenciometr sloužit jako DA převodník, který našim 8 bitovým Atmelům chybí. Příklad bude trochu rozsáhlejší než bývá obvyklé. Nejprve se budu ve zkratce věnovat možnostem digitálního potenciometru. Pak stručně proberu způsob ovládání. Nakonec vyberu jednu z připravené knihovny funkcí a popíši co dělá. I když se příklad bude točit kolem potenciometru, mějte na zřeteli že se chcete naučit používat SPI na Atmelu. Příště totiž budete psát ovladač pro něco úplně jiného.
Digitálních potenciometrů existuje celá řada a jejich popis a ukázky by vydaly na několik seriálů. Proto se pokusím být stručný. Pokud se budete dívat na digitální potenciometr jako na black-box (což ve článku budu) tak je potřeba uvědomovat si, že oproti běžnému potenciometru potřebuje jeho digitální verze napájení. A na žádný jeho vývod nesmíte přivést napětí mimo rozsah toho napájecího. To je tvrdý limit. Znemožňuje vám to vzít libovolné zapojení a vyměnit v něm běžné potenciometry za digitální. Běžné digitální potenciometry mají maximální provozní napětí 1.8-5.5V, takže systémy pracující s vyšším napětím musí být navrženy chytře, tak aby potenciometry mohly pracovat s nižším napětím. Naštěstí ale narazíte i na potenciometry na 36V (respektive +-18V). Což je napětí, které umožňuje implementovat potenciometry do valné většiny aplikací s běžnými operačními zesilovači (ty mívají provozní napětí podobná). Kromě provozního napětí existují ještě další kritéria, podle kterých je potenciometry možné rozdělit. Prvním z nich je paměť. Existují volatilní a nevolatilní verze. Volatilní verze ztrácejí po odpojení napájení svoji hodnotu a startují vždy do základního nastavení, kde je jezdec buď na kraji odporové dráhy nebo uprostřed. Nevolatilní verze mají paměť, ve které je možné uchovat polohu potenciometru a po připojení napájení je pak jezdec na hodnotě jakou si určíte. Dalším kritériem je počet kroků. Existují v podstatě pouze varianty s 64,128,256,512 a 1024 kroky. Na jemnější potenciometry jsem nenarazil. Dále je potenciometry možné dělit podle komunikačního rozhraní. Nejčastěji se setkáte s I2C, SPI a UD protokoly. UD protokol je volen tak aby bylo možné potenciometr připojit přímo k rotačnímu enkodéru. Posledním kritériem, které stojí za zmínku je "topologie". Vyrábí se potenciometry se třemi vývody, nebo dvojvývodové verze sloužící jako reostat. Nejlevnější potenciometry začínají přibližně na ceně 15kč za kus. Vyrábí se varianty jeden nebo dva potenciometry v jednom pouzdře. Hodnoty odporu bývají od 1kOhm do 1MOhm a přesnost není vysoká. Typicky se hodnoty pohybují v toleranci +-10%. Tady je situace stejná jako u trimrů, většina zapojení je vytvořena tak aby tyto nejistoty nevadily. Stálost hodnoty je naštěstí vysoká. Pokud vás budou možnosti digitálních potenciometrů zajímat více, zkuste si pobrouzdat obchody jako TME nebo Farnell (Farnell) a podívat se na různé typy.
Pro můj příklad jsem vybral typ MCP4251. Volatilní relativně levný dvojitý potenciometr s rozsahem 256 kroků s SPI rozhraním a možností interního odpojení vývodů. Díky tomu že jde o volatilní typ je jeho ovládání jednodušší. Doporučuji prostudovat si datasheet (už jen proto aby jste si udělali představu o tom jaké informace v něm jsou a jaké ne). Hned v úvodu vás upozorním na jistý nedostatek v datasheetu. Na nákresu rozmístění vývodů vidíte pin označený WP (asi jako Write Protect), dále v textu, ale nenajdete popis a význam tohoto vývodu. Na obrázku je totiž "překlep" a vývod má být označen NC (Not Connected). Zkoušel jsem na něj přivést log.0, ale zápis to neblokovalo. Pin SHDN slouží k "vypnutí" potenciometru a obsahuje pull-up, takže ho nemusíte zapojovat, pokud funkci vypínání nepotřebujete. Během vypnutí jsou všechny jeho vývody odpojeny. Vývody P0B, P0W a P0A tvoří potenciometr 0 a vývody P1B, P1W a P1A tvoří potenciometr 1. Dále už na čipu najdete jen komunikační linky pro SPI. Odpor jezdce je přibližně 70 Ohmů a potenciometr umožňuje nastavit jezdec k oběma krajům, tedy defakto spojit P0W s P0B nebo P0A (pouze s odporem jezdce). Komunikace po SPI může probíhat v módu 0 nebo módu 3 a clock může dosahovat až 10MHz (takže žádné zdržování). Připojení k Atmelu můžete vidět na obrázku d1.
Používání jakéhokoli integrovaného obvodu začíná vždycky čtením datasheetu. Přirozeně nikomu se ho nechce číst celý, je tedy potřeba zaměřit se jen na klíčové informace. V tomto případě jde o to co potenciometru posílat a jak mu to posílat. Co lze potenciometru posílat je v tabulce 7 (v datasheetu tabulka 7-2). Jak vidíte můžete každému ze dvou potenciometrů nastavovat pozici jezdce, číst ji, dávat pokyn k inkrementaci nebo dekrementaci pozice jezdce. Mimo to můžete zapisovat nebo číst z TCON registru. A ještě můžete číst tzv. "Status register" (datasheet 4-1), který obsahuje pouze informaci o tom jestli je potenciometr vypnut pinem SHDN. Pomocí TCON (datasheet 4-2) registru můžete odpojovat / připojovat kterýkoli z vývodů potenciometru. Příkazy inkrementace a dekrementace jsou osmibitové. Všechny ostatní příkazy jsou 16 bitové. Formát 16 bitového příkazu je znázorněn v tabulce 6. V horním řádku jsou data, která vysílá master, v dolním řádku je pak odpověď od potenciometru. Nejvyšších 6 bitů zprávy (C5 až C0) obsahuje kód příkazu (viz tabulka 7). Bitem ERR potenciometr signalizuje chybu v příkazu. Pokud je příkaz neplatný (pošlete něco co není v tabulce 7), tak potenciometr pošle v bitu ERR nulu. Pokud je příkaz v pořádku, posílá jedničku. Vy si pak můžete odpověď přečíst a případně na to reagovat. Zbylých 9 bitů zprávy (D8 až D0) jsou odesílaná nebo přijímaná data (podle toho jestli zapisujete nebo čtete). Všechna data jsou 9 bitová ať už jde o hodnotu jezdce, TCON registr nebo Status registr. Vždy se čte nebo zapisuje 9 bitová hodnota. Poloha jezdce smí nabývat hodnot 0 až 256 (proto je na ni potřeba 9 bitů). Výsledný odpor dráhy od jezdce k terminálu B je Rwb = Rab*N/256 + Rw. Kde Rab je odpor celého potenciometru (mezi vývody R0A a R0B), Rw je odpor jezdce a N je zapsaná hodnota (0 až 256). U osmibitových příkazů (inkrementace a dekrementace) nemají žádné bity kromě příkazu (C5 až C0) a ERR žádný význam. Potenciometr navíc umožňuje "kontinuální zápis", není tedy nutné po každém příkazu vracet CS pin do log.1. Což je funkce, kterou asi neoceníte. Pokud se pokusíme inkrementovat nebo dekrementovat jezdec, který je na konci dráhy a daným směrem se už nemůže "pohnout", tak se jeho poloha přirozeně nezmění.
bit | 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
MOSI | C5 | C4 | C3 | C2 | C1 | C0 | - | D8 | D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 |
MISO | - | - | - | - | - | - | ERR | D8 | D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 |
Příkaz | Kód příkazu | vzor zprávy (MOSI) | odpověď potenciometru (MISO) |
---|---|---|---|
Zapiš polohu jezdce 0 | 000000 | 0000 000d dddd dddd | 1111 1111 1111 1111 |
Přečti polohu jezdce 0 | 000011 | 0000 1100 0000 0000 | 1111 111d dddd dddd |
Inkrementuj jezdec 0 | 000001 | 0000 0100 | 1111 1111 |
Dekrementuj jezdec 0 | 000010 | 0000 1000 | 1111 1111 |
Zapiš polohu jezdce 1 | 000100 | 0001 000d dddd dddd | 1111 1111 1111 1111 |
Přečti polohu jezdce 1 | 000111 | 0001 1100 0000 0000 | 1111 111d dddd dddd |
Inkrementuj jezdec 1 | 000101 | 0001 0100 | 1111 1111 |
Dekrementuj jezdec 1 | 000110 | 0001 1000 | 1111 1111 |
Zapiš do TCON | 010000 | 0100 000d dddd dddd | 1111 1111 1111 1111 |
Přečti TCON | 010011 | 0100 110d dddd dddd | 1111 111d dddd dddd |
Přečti Status Registr | 010111 | 0101 110d dddd dddd | 1111 111d dddd dddd |
Celý zdrojový kód naleznete ke stažení zde. Je rozsáhlejší a nemá smysl ho prezentovat v celku. Příklad je testován na Atmega16A s clockem 8MHz, ale bez úprav půjde nejspíš přenést na libovolný Atmel s SPI rozhraním. Z tabulek 6 a 7 by vám mělo být jasné jakou zprávu chceme potenciometru posílat a případně jakou odpověď od něj očekávat. Tyto informace by vám obecně měly stačit proto aby jste mohli napsat sadu funkcí, které odesílají vybrané příkazy. Což je úkol, který budete provádět často. Ať už budete psát program k ovládání ADC, čidla nebo motorového driveru, skoro vždycky napíšete sadu funkcí, kterou pak budete v rámci programu volat. Většinou nebudete využívat všechny možnosti obvodu, takže se nebudete obtěžovat s programováním všech funkcí. Kdybych chtěl potenciometr ovládat tlačítky, stačili by mi funkce mcp4251_increment(), mcp4251_decrement() a mcp4251_write_tcon() a se zbytkem bych se nemusel zdržovat. Já jsem připravil takovou sadu základních funkcí. Přirozeně hlavně proto aby jste si prohlédli jak se zachází s SPI a jak se takový "ovladač" může programovat.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | // wiper 0 nebo 1, value 0-256, vrací 0 při úspěšném zápisu char mcp4251_set(char wiper, unsigned int value){ char tmp; CS_L; // aktivuj slave obvod if(wiper==0){ SPDR = 0x00 | (char)((value>>8) & 0b1); // příkaz zápisu jezdce 0 (s 9.bitem hodnoty) } else{ SPDR = 0x10 | (char)((value>>8) & 0b1); // příkaz zápisu jezdce 1 (s 9.bitem hodnoty) } while(!(SPSR & (1<<SPIF))); // počkej na dokončení přenosu 1.byte tmp=SPDR; // přečti odpověď potenciometru if(!(tmp & 0b10)){CS_H; return 1;} // pokud je příkaz chybný, (CMDERR v nule), ukonči vysílání SPDR = (char)(value); // odešli dolní byte hodnoty while(!(SPSR & (1<<SPIF))); // počkej na dokončení přenosu 2.byte CS_H; // deaktivuj slave obvod return 0; // vše proběhlo v pořádku } |
První nedostatek který vás může napadnout je to že funkce nemá ošetřené vstupy, ale ona je ošetřeny v podstatě má. V prvním argumentu by jste měli funkci sdělit kterému ze dvou potenciometrů chcete nastavovat polohu jezdce. Předhodíte-li jí nulu, zapíše hodnotu pro jezdec 0, při jakékoli jiné hodnotě zapíše hodnotu do jezdce 1. V druhém argumentu máte funkci předat hodnotu jezdce z rozsahu 0-256. Na to přirozeně nestačí typ char a musí se použít integer. Teoreticky si můžete dovolit vložit do funkce hodnotu větší jak 256, protože se před odesláním ořízne, ale pak asi budete mít guláš v tom co odeslala. Ihned po vstupu do funkce se pomocí CS pinu aktivuje potenciometr. Celý přenos bude 16 bitový, takže bude potřeba odeslat dvakrát 8 bitů. Podmínka rozhodne o tom který kód povelu se má odeslat. Po přenesení horního bytu si přečteme ERR bit v odpovědi z potenciometru. Ten by nás mohl informovat o chybném příkazu. Pokud by tento bit byl nulový, nastala někde chyba a ukončíme komunikaci. Pokud je vše v pořádku odešleme zbytek zprávy. A pak už jen deaktivujeme komunikaci s potenciometrem (CS do log.1). Pominu-li maskování a bitové operace při sestavování prvního byte zprávy tak je funkce naprosto průhledná.
Makra v úvodu programu využijete při ovládání TCON registru. Ale to už bych zbytečně odbočoval do tématu SPI. Doufám, že jste si udělali hrubou představu co vás čeká až si budete psát vlastní ovladač. V závěru přidám ještě jednu radu. Nesnažte se celý kód psát najednou. Najděte si vždy nejjednodušší možný příkaz pomocí kterého dokážete poznat, že vám zařízení rozumí. Protože na 90% uděláte na poprvé někde chybu. Pošlete do zařízení nějáký nesmysl, ono nic neudělá a vy budete muset znovu pročíst datasheet a zjistit co jste si špatně vyložili. Taky vám doporučuji každou funkci trpělivě otestovat. Protože pak se můžete spolehnout na to, že fungují a můžete s klidem zapomenout všechny detaily. Bohužel ne vždycyk budete mít dost klidu a času to dělat poctivě :D