V tomto díle seriálu se podíváme na to že zprovoznit STM32 v roli slave na SPI sběrnici není tak přímočaré a jednoduché jak by se na první pohled mohlo zdát...
SPI slave s STM32
V tomto seriálu si nekladu za cíl nějakým uceleným způsobem podat vyčerpávající popis SPI na STM32. To by byl nadlidský úkol schopný spolykat měsíce práce. Jde mi jen o to poukázat na zajímavé funkce nebo úskalí některých periferií na STM32 a připravit zdrojové kódy použitelné jako šablony do dalších projektů. Očekával jsem, že práce s SPI bude snadná cesta plná krásných hardwarových vychytávek, které programátorovi zjednodušují práci. Realita tak růžová bohužel není. Pokud vás zajímá na jakou překážku můžete narazit, čtěte dál.
Pro jistotu předem upřesním, že ukázka i tutoriál se váže k čipu F031, ale platit by měl i pro jiné členy rodiny F0 (a nejen pro ně). Letmý pohled do "reference manualu" na kapitolu "SPI main features"vás přesvědčí o tom, že SPI je vskutku rozmanitá periferie. Většinou ji budete používat v roli Masteru a nejspíš se mnou budete souhlasit, že to je ta jednodušší role, neboť máte veškeré dění ve svých rukách. My se ale v rámci tohoto článku zaměříme na obtížnější roli - Slave.
Jako příklad si zkusíme zprovoznit jednoduchou výměnu 4 bytů dat mezi masterem a slave (s využitím SPL knihoven). Masterem bude v mém případě USB->SPI bridge MCP2210, ale může to být klidně jiný mikrokontrolér. Jako Slave Select pin použijeme libovolný I/O (v našem případě PA11), protože využití SPI1_NSS (PA4,PA15 nebo PB12) neskýtá žádnou výhodu. Jako MISO, MOSI a SCK využijeme piny PB4,PB5 a PB3 a čip poběží na 48MHz.
FIFO
Než se pustíme do samotného zdrojového kódu uděláme malou vsuvku do teorie. SPI periferie obsahuje na vysílací i na přijímací straně 32bitovou vyrovnávací FIFO paměť (TxFIFO a RxFIFO). Její smysl je zřejmý. Díky RxFIFO nemusí váš software reagovat na přijatá data okamžitě a snižuje se tak riziko, že při vyšších datových tocích nestihne data vyzvednout. Na vysílací straně je účel TxFIFO podobný. Umožňuje vám dosáhnout plynulého datového toku aniž by to kladlo neúměrné nároky na rychlou reakci vašeho SW. Krom toho obě FIFO slouží k procesu "packing/unpacking", který by vám měl umožňovat zapisovat do SPI 16bit hodnoty i když je přenos nastaven jako 8bitový. Zda to má nějaké praktické výhody ale netuším. Nejspíš najde uplatnění v "TI" módu. Tyto "výhodné" vlastnosti nám zanedlouho zkomplikují práci.
NSS
Chip Select (CS) nebo Slave Select (SS) je pin, který na sběrnici slouží k adresování slave zařízení. Vynulováním této linky slave aktivujete a nastavením linky do log.1 slave deaktivujete. Na STM32 se tato linka jmenuje NSS a lze ji řídit buď z vnějšku skrze vybraný pin (tzv. Hardware managment) a nebo programově ovládáním bitu SSI v registru SPIx->CR1 (tzv. Software managment). V roli masteru může mít NSS pin ještě mnoho dalších účelů, ale o tom až někdy jindy. Jen podotknu známý fakt, že aktivovaný slave přebírá kontrolu nad MISO linkou a deaktivovaný slave ji musí uvolnit (MISO vývod ve vysoké impedanci).
Program
Podívejme se nyní v několika bodech na to co z pohledu našeho programu musíme zvládnout:
Musíme korektně inicializovat využívané periferie
Musíme rozpoznat, že nás master adresuje (sestupnou hranou na CS, tedy na PA11) a předat tuto informaci SPI periferii (Software NSS managment - viz odstavec výše)
Ihned po adresaci (nebo taky ještě před ní) musíme připravit data která chceme odeslat
Postupně jak dochází k odesílání dat, musíme připravovat další
Musíme někam ukládat přijatá data
Musíme rozpoznat že master komunikaci ukončil a deaktivoval nás (vzestupná hrana CS) a opět tuto informaci předat SPI periferii.
Musíme (a teď ještě netušíte proč) rozpoznat situaci, kdy master ukončí komunikaci předčasně.
Inicializace
Inicializace je vcelku přímočará záležitost. Nastavování GPIO a clocku si dovolím přeskočit. S konfigurací externího přerušení nejspíš také nebudete mít problémy, takže ji nebudu nijak komentovat. SPI nastavíme v módu 0 (CPOL = 0, CPHA = 0) a v režimu slave. Nastavení prescaleru ani CRC nemá význam. Pro ovládání NSS vybereme "software managment". Mírné pozastavení si zaslouží funkce SPI_RxFIFOThresholdConfig(). Tou vybíráme jak moc se musí RxFIFO naplnit aby se nastavila vlajka RXNE a vyvolalo přerušení od příjmu dat. Protože je náš přenos 8bitový je vhodné nastavit threshold na 1/4, tedy na jeden byte. Vlajka i přerušení se pak volají ihned po přijetí prvního bytu. Pokud by byl threshold nastaven na 1/2 nebo 3/4, došlo by k volání přerušení až po přijetí 2 respektive 3 bytů a to by nám mohlo (ale nemuselo) komplikovat práci. Dále v rámci inicializace povolíme přerušení od příjmu (RXNE) v periferii i v NVIC. Pro jistotu ještě deaktivujeme vnitřní NSS signál.
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1 , ENABLE); // clock pro SPI spi_is.SPI_Direction = SPI_Direction_2Lines_FullDuplex; // i pro režimy kdy jen vysíláme ! spi_is.SPI_Mode = SPI_Mode_Slave; // budeme hrát slave spi_is.SPI_DataSize = SPI_DataSize_8b; // proč ne třeba 8b data spi_is.SPI_CPOL = SPI_CPOL_Low; // SPI v režimu 0 (Clock neutrálně v log.0) spi_is.SPI_CPHA = SPI_CPHA_1Edge; // SPI v režimu 0 (čteme na vzestupnou hranu) spi_is.SPI_NSS = SPI_NSS_Soft; // o SS pin se postaráme jinak a sami spi_is.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_2; // tohle nemá význam, jsme slave ... spi_is.SPI_FirstBit = SPI_FirstBit_MSB; // pořadí bitů ve zprávě spi_is.SPI_CRCPolynomial = 0; // CRC nepoužíváme, nezáleží na hodnotě SPI_Init(SPI1, &spi_is); // aplikovat konfiguraci
// vlajka RXNE se natavuje přijetím jediného bytu SPI_RxFIFOThresholdConfig(SPI1,SPI_RxFIFOThreshold_QF); SPI_I2S_ITConfig(SPI1, SPI_I2S_IT_RXNE, ENABLE); // povolíme přerušení od příjmu // interní SS pin do log.1 (MISO do Hi-Z) SPI_NSSInternalSoftwareConfig(SPI1,SPI_NSSInternalSoft_Set);
// povolíme přerušení od SPI v NVIC nvic.NVIC_IRQChannel = SPI1_IRQn; nvic.NVIC_IRQChannelPriority = 2; nvic.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&nvic);
// detekce vzestupné i sestupné hrany na lince 11 (PA11) exti.EXTI_Line = EXTI_Line11; exti.EXTI_Mode=EXTI_Mode_Interrupt; exti.EXTI_Trigger=EXTI_Trigger_Rising_Falling; exti.EXTI_LineCmd=ENABLE; EXTI_Init(&exti);
// povolit přerušení od EXTI11 v NVIC nvic.NVIC_IRQChannel=EXTI4_15_IRQn; nvic.NVIC_IRQChannelPriority=3; nvic.NVIC_IRQChannelCmd=ENABLE; NVIC_Init(&nvic); }
Rutiny přerušení
Jak už jsem dříve napsal, stav linky "chip select" hlídáme pomocí externího přerušení. Jako první po jeho volání musíme zjistit zda jde o vzestupnou nebo sestupnou hranu, což jde snadno čtením stavu PA11. Pokud zjistíme sestupnou hranu připravíme se na přenos. Vynulujeme počítadla přijatých a odeslaných dat, aktivujeme vnitřní NSS signál a povolíme přerušení od "prázdného" TxFIFO (TXE). V tom okamžiku se zavolá rutina přerušení od TXE (má nastavenu vyšší prioritu) v jejímž rámci začneme TxFIFO plnit daty. V podstatě ihned naplníme do TxFIFO tři byty (tedy většinu naší zprávy). TxFIFO se považuje za plné při 3/4 své celkové kapacity (proto pouze tři byty). Vraťme se ale k rutině externího přerušení. Pokud zachytíme vzestupnou hranu, znamená to konec komunikace a musíme deaktivovat vnitřní NSS signál. Spolu s tím provedeme kontrolu zda byl master slušný a neukončil komunikaci předčasně. Kdyby se dopustil takové nevychovanosti, zůstala by nám v TxFIFO data, která není snadné odstranit. Takovou nenadálou událost signalizujeme hlavní smyčce proměnnou pruser.
// detekuje aktivaci a deaktivaci SS pinu (nyní PA11) // rychle se musíme připravit na příjem / vysílání void EXTI4_15_IRQHandler(void){ //TEST2_H; // víme že přerušení pochází od PA11, nemusíme zkoumat zdroj EXTI_ClearITPendingBit(EXTI_Line11); // smazat vlajku EXTI // pokud je to sestupná hrana if(!GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_11)){ rxi=0; // přenos začíná, vynulovat počítadla dat txi=0; // vynulovat interní nSS (slave aktivován, MISO je výstupem) SPI_NSSInternalSoftwareConfig(SPI1,SPI_NSSInternalSoft_Reset); // a povolit přerušení... SPI_I2S_ITConfig(SPI1, SPI_I2S_IT_TXE, ENABLE); // od prázdného Tx bufferu } else{ // pokud je to vzestupná hrana, konec příjmu // pokud master udělá chybu a provede méně transakcí než jsem čekali, zůstanou ve FIFO stará data if(SPI_GetTransmissionFIFOStatus(SPI1)>0){ pruser=1; // a ty je potřeba vymazat - což uděláme v hlavní smyčce } // nastavit interní nSS (slave deaktivován, MISO je Hi-Z) SPI_NSSInternalSoftwareConfig(SPI1,SPI_NSSInternalSoft_Reset); } //TEST2_L; }
V rychlosti si ještě prohlédneme rutinu přerušení od SPI. Jako první v ní zjistíme z jakého důvodu nás SPI přerušuje. Pokud je to z důvodu přijatých dat (RXNE), tak je prostě uložíme do pole rx[]. Samozřejmě si nezapomeneme ohlídat přetečení. Pokud je master nevychovaný a transakce je delší jak 4 Byty, tak prostě přebytečná přijatá data zahodíme. Jestliže nás SPI volá kvůli TXE - tedy z toho důvodu že se v TxFIFO uvolnilo místo na další data, tak je tam prostě vložíme. Opět si hlídáme kolik dat jsme do TxFIFO nastrkali neboť víme, že chceme odeslat celkem 4B. Po vložení posledního (4.Bytu) do TxFIFO prostě vypneme přerušení od TXE a dál se o vysílání nestaráme. Kdyby byl Master opět nevychovaný a provedl by transakci delší jak 4B tak obdrží nesmysly ... ale to je jeho problém
void SPI1_IRQHandler(void){ //TEST1_H; // pokud jsme přijali byte if(SPI_I2S_GetITStatus(SPI1, SPI_I2S_IT_RXNE) != RESET){ if(rxi<POCET_DAT){// pokud je ještě místo v přijímacím poli... rx[rxi]=SPI_ReceiveData8(SPI1); // ...uložíme si do něj přijatá data rxi++; } else{SPI_ReceiveData8(SPI1);}// jinak jen vyčistíme přijímací buffer (a tím i RXNE vlajku) }
// pokud máme místo v TxFIFO (zapíšeme tam další data k vysílání) if(SPI_I2S_GetITStatus(SPI1, SPI_I2S_IT_TXE) != RESET){ if(txi<POCET_DAT){// pokud je ještě co vysílat... SPI_SendData8(SPI1,tx[txi]); // ...nakrmme příslušná data do TxFIFO txi++; } else{// když jsme do TxFIFO zapsali (nikoli odeslali) poslední byte dat SPI_I2S_ITConfig(SPI1, SPI_I2S_IT_TXE, DISABLE); // vypneme přerušení od TXE } } //TEST1_L; }
Určitě jste si všimli zakomentovaných maker TEST_H a TEST_L. Ty sloužily pro vytváření názorného oscilogramu na němž si můžete prohlédnout chod programu. Oranžový průběh (R2) zobrazuje zpracování externího přerušení a přerušení od příjmu dat. Zelený průběh (R1) zaznamenává rutinu přerušení od TXE. U něj se na chvíli zdržíme. Všimněte si tří impulzů krátce po začátku komunikace. Ty znázorňují situaci kdy se TxFIFO plní prvními třemi byty. Ihned po zahájení přenosu (který můžete pozorovat v dolní části oscilogramu) přichází další zelený impulz, který znázorňuje doplňování TxFIFO. Poslední zelený impulz odpovídá situaci kdy už nejsou žádná další data k dispozici a dochází k vypnutí přerušení od TXE. Dění na oranžové lince (R2) je celkem přímočaré. RXNE rutina se volá vždy po přijetí jednoho bytu, EXTI rutina pak s aktivitou na PA11 (Slave Select).
Obr.1 Záznam komunikace - Oranžová (R2) indikuje aktivitu externího přerušení a přerušení od RXNE, zelená linka (R1) indikuje aktivitu TXE přerušení
A takto vypadal znázorněný přenos v okně terminálu...
Podraz jménem TxFIFO
Nejproblematičtější část příkladu jsem si nechal nakonec. Asi se mnou budete souhlasit, že v praxi může vcelku snadno nastat situace kdy nebude slave předem vědět kolik dat z něj bude master číst. Konec konců takovým způsobem pracuje celá řada čidel a podobných prvků. Nejprve jim sdělíte kterou část jejich vnitřní paměti chcete číst a pak provádíte tolik SPI transakcí kolik dat vás zajímá. Pokud se ale rozhodnete takto přistupovat ke slave zařízení založeném na STM32, nabouráte. Neboť jakmile slave jednou naplní svou TxFIFO daty tak už neexistuje legální způsob jak je odtamtud dostat ven (Datasheet se totiž takovou situací nezabývá). Slave pak chtě nechtě s každou novou komunikaci nejprve vypustí do světa zastaralá naprosto nevhodná data, jež zbyla v TxFIFO z dávno minulé komunikace. Asi chápete, že to je nepřípustné. Už jen proto, že masterů může být na sběrnici víc a ten který se vás zrovna na něco ptá nemá vůbec představu co s vámi druhý master před tím řešil a kolik nesmyslů vám v TxFIFO zůstalo.
Pokud se budete chtít použití TxFIFO vyhýbat, budete muset do SPI vkládat data jen pokud je TxFIFO i odesílací registr prázdný. Na to vám bude muset Master mezi přenosy vytvářet prostor. To spolu s faktem že přijdete o TXE přerušení odsuzuje tuhle možnost jen pro přenosy s nízkou rychlostí. Jediný rozumný postup se tedy jeví TxFIFO nějak mazat. Já to dělám prostě tak že v rozporu s datasheetem vypnu SPI i když není TxFIFO prázdné, resetuji clock celé SPI periferii a kompletně ji znovu inicializuji. Mám ověřeno že to funguje, ale přirozeně nemůžu zaručit že to bude fungovat za všech okolností...
// komplikovaná oklika jak mazat Tx FIFO void reset_spi(void){ // porušíme doporučení datasheetu vypínat SPI jen s prázdným FIFO SPI_Cmd(SPI1, DISABLE); // vypneme SPI // resetujeme clock celé periferii RCC_APB2PeriphResetCmd(RCC_APB2Periph_SPI1,ENABLE); RCC_APB2PeriphResetCmd(RCC_APB2Periph_SPI1,DISABLE); // znovunastavit a zapnout SPI init_SPI1(); }
Celý zdrojový kód si můžete stáhnout zde. Dotaz jak zacházet s FIFO je na foru STMicroelectronics a pokud by se našel nějaký legálnější postup, tak jej do článku doplním. Doufám, že jste si z příkladu něco odnesli a těším se na setkání u dalších problémů.
Jen mám takový dotaz, proč neskýtá použití NSS pinu žádný výhody? čekal bych, že když ne nic jinýho, že se bude nechat ušetřit pár cyklů na přerušení..?
Ano máte pravdu, cykly se tím dají ušetřit. Ale protože jsem si nedokázal představit (vyjma speciálních situací) jak by se celý provoz obešel bez externího přerušení, přišla mi úspora několika cyklů natolik zanedbatelná, že jsem ji nepovažoval za výhodu.
Spíš jsem myslel kvůli latenci. Protože i přerušení to asi umí obsloužit. Benefit je ale v tom, že procík kvůli tomu nemusí skákat do přerušení. A když se udělá v DMA kruhovej buffer, tak by ani nemusel odchytávat každej bajt, ale provést vyhodnocení až po přijetí kompletního paketu... např. což šetří cykly. A pokud je víc vytíženej, může to být docela k dobru.
S tím souhlasím. Čistý příjem je bezproblémová akce kde je HW ovládání NSS žádoucí a spolupráce s DMA jistě ulehčí procesoru. Nedovedu si ale představit jak aplikace spolehlivě rozpozná konec zprávy bez použití externího přerušení na vzestupné hraně NSS. A podobný problém vyvstane v situaci kdy má slave něco vysílat. Pokud nepoužije externí přerušení od sestupné hrany k přípravě dat bude muset v aplikaci vzniknout nějaký jiný mechanismus který dá slave pokyn aby data připravil ...
Pro psaní komentářů musíte být přihlášen. Prosím přihlaste se, nebo se zaregistrujte zde pro přihlášení
Prohledat MCU-mikroelektronika
Chatbox
Musíte být přihlášení, abyste zde mohli odesílat příspěvky - přihlaste se nebo se zaregistrujte.
Tenhle server porad nese stafetu, se kterou obetave prisel Kosta, a velice si vazim jeho prace, kterou vykonali spolu s Mardem (nejen) pro komunitu STM ARMu. Tak snad dokazeme na tuto (dnes uz vzacnou) ideu navazat.
@pako: suhlasim Fulda ma pekne veci aj preto by mali byt v clankoch, aby k nim bol pristup aj o rok, dva. To ale zalezi na nom, ak by to chcel, tak to asi davno urobi. To je asi bezne aj na inych weboch (aspon kam ja chodim). Chapem, ze tento nas redakcny system je strasny, ale skopirovat embeded code z youtube a vlozit do clanku asi nebude vyzadovat vysokoskolke vzdelanie. Na druhej strane dobra sprava, chiptron ma konecne shoubox, blahozelam. ... idem sa zaregistrovat
to joedoe: Prosim vas, zrovna Fulda ma casto zajimave prispevky, a tady uz se 14 dni web ani nehne, tak budme radi za kazdy uzitecny prispevek. Vzdyt stacilo jen pozadat autory, aby clanecek vytvorili a zpravu v chatboxu ponechat. Takhle si jen vytvarime nepratele.
@bobricius: ... asi mame rozdielny nazor na danu vec. FYI: @gripennn s tym problem nema. Fuldov prispevok nebol zmazany ale blokovany ako vas a moja sprava bola pre vas obidchoch.
Sorry ale robit clanok kvoli 3 riadkom, to nemyslite vazne. A to zmazanie Fuldoveho prispevku je uz kus vela ne? Sak jedine co na tej stranke zije je chatbox. Na novinky chodim k chiptronovi
@fulda & @bobricius: ak chcete robit odkazy na svoje aktivity, tak urobte kratky clanok a tam si dajte odkazy kam chcete. Rad vam s tym pomozem, kludne ma kontaktujte. Tento "pristup" sme tu uz riesili niekolkokrat. Dakujem
V rámci mé přirozené skromnosti vás upozorním na můj malý instructables projekt odkaz vánoční stromek s charlieplexingem. A protože jsem hodně skromnej, tak připomenu, že je součástí PCB contestu a že pro něj můžete hlasovat: odkaz
Neměl byste někdo schéma zapojení (domácí) telefonní ústředny ALPHATEL T-LINE. Po výměně akumulátoru v UPS, resp. jejím vypínaní mi asi v ústředně odešel zdroj. Děkuji.