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:

  1. Musíme korektně inicializovat využívané periferie
  2. 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)
  3. Ihned po adresaci (nebo taky ještě před ní) musíme připravit data která chceme odeslat
  4. Postupně jak dochází k odesílání dat, musíme připravovat další
  5. Musíme někam ukládat přijatá data
  6. Musíme rozpoznat že master komunikaci ukončil a deaktivoval nás (vzestupná hrana CS) a opět tuto informaci předat SPI periferii.
  7. 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.

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);
}

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ů.

Home
V1.00 23.11.2017
By Michal Dudka (m.dudka@seznam.cz)