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.
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.
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).
Podívejme se nyní v několika bodech na to co z pohledu našeho programu musíme zvládnout:
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.
void init_SPI1(void){ SPI_InitTypeDef spi_is; NVIC_InitTypeDef nvic; 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); SPI_Cmd(SPI1, ENABLE); // spustit SPI } void exti_init(void){ EXTI_InitTypeDef exti; NVIC_InitTypeDef nvic; RCC_APB2PeriphClockCmd(RCC_APB2Periph_SYSCFG, ENABLE); // clock pro EXTI SYSCFG_EXTILineConfig(EXTI_PortSourceGPIOA,EXTI_PinSource11); // připojit PA11 // 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); }
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).
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ů.
Home
V1.00 23.11.2017
By Michal Dudka (m.dudka@seznam.cz)