USB opravdu nedám. Dělám výhradně low-level aplikace (časování, regulace, sběr dat a pod.). Nějaké tutoriály na timery mám v betaverzi zde: http://www.pd-nb.cz/avr.html
Ďakujem za výborne spracovené tutoriály!
Zaujalo ma hlavne riadenie servo motorčeka pomocou PWM módu 1, keďže potrebujem podobné pulzy v blízkej dobe spraviť. Navnadili ste ma na STM32, no potreboval by som synchronizovane riadiť 6-8 kanálov. Rozmýšľal som použiť TIMery dva (trebárs TIM2 a TIM3), čím by som mohol riadiť 8 kanálov/pinov, ale problém ktorý pravdepodobne nastane, že nedokážem oba časovače spustiť naraz.
Alternatívne som rozmýšľal o použití časovača len na generovanie prerušení a GPIO output piny by som nastavoval ručne v obsluhe prerušenia. Keďže ale potrebujem mikrokontroler externe ovládať či už cez USB CDC alebo USART, bojím sa že MCU môže začať obsluhovať prerušenia týchto periférí, čo mi vnesie nepresnosť do dĺžky mojich pulzov. Ako začiatočník s MCU (hlavne Arduino), ocením akékoľvek tipy.
Timery na STM32 lze snadno hardwarově synchronizovat. Jeden timer nakonfigurujete jako master a na interní TRGO výstup mu vyvedete událost "enable". Další timery nastavíte jako slave s tím že na příchod interního TRGI signálu má reagovat spuštěním ("trigger"). Jen je potřeba z tabulek "Internal trigger connection" vybrat slave timerům správný TRGI signál... přirozeně master timer pak musíte spustit až po konfiguraci všech slave timerů. Časem to asi k tutoriálům ( www.pd-nb.cz/avr.html ) přidám.
Ďakujem, naozaj to funguje!
Našiel som doma povaľovať STM32F103 (takzvaný "blue _p.i.l.l_") a podarilo sa mi to vyklikať v CubeMX. Síce mi zopár hodín trvalo kým sa mi to podarilo rozchodiť ale výsledok je presne taký ako ste hovorili. Timery sú krásne zosynchronizované. Takto môžem generovať synchronizované pulzy presnej dĺžky na niekoľkých kanáloch, bez toho aby som sa musel báť že mi časovanie nabúrajú prerušenia napr. vyvolané komunikáciou na perifériách.
Moje predchádzajúce riešenie, ktoré bolo postavené na arduine, som ovládal cez prerušenia vyvolané časovačom. Pre dosiahnutie presnosti som musel, vypnúť všetky ostatné prerušenia a kvôli tomu som musel obetovať riadenie cez sériový port. Toto riešenie je o dosť lepšie.
jj, je to otázka tří funkcí. Na master timeru (např TIM2) vybrat událost:
TIM_SelectOutputTrigger(TIM2,TIM_TRGOSource_Enable); // povolení/spuštění timeru vyvést na TRGO
na slave timeru (např TIM5) vybrat zdroj signálu (dle tabulek z datasheetu) a událost jak s ním naložit:
TIM_SelectInputTrigger(TIM5,TIM_TS_ITR0); // využít input ITR0 (TRGO z TIM2)
TIM_SelectSlaveMode(TIM5,TIM_SlaveMode_Trigger); // s příchodem ITR spustit/povolit timer
pak už jen stačí spustit TIM2 a je synchronizováno (za předpokladu že strop timerů je stejný nebo alespoň soudělný). Možná vás podceňuji, ale pokud generujete PWM nezapomeňte si zapnout "preload":
TIM_OC1PreloadConfig(TIM2, TIM_OCPreload_Enable); // preload na TIM2_CH1
Jinak se vystavujete riziku zmršené waveformy při změně PWM hodnoty.
Takže popořadě.
První věc, všechny knihovny od ST jsou dobrý tak pro inspiraci. Začíná to cyklickýma inkluzema v CMSIS, pokračuje nedomyšlenýma funkcionalitama v středních vrstvách (proč sakra nikdo nepočítá s tak podstatnou věcí, že na SPI mají obvody obvykle uvnitř několik registrů a je potřeba je adresovat?) konče HALem s chybějící funkcionalitou. Dál jsou tyhle funkce nekompatibilní s operačním systémem.
Druhá věc. Jak může autor propagovat TrueStudio? Otravný, ve free verzi reklamama na sebe samo prolezlý balast, s instalací proti registraci a s aktualizací na úrovni widlí. Navíc bez registračního formuláře se člověk nedozví ani detaily o instalaci a požadavcích... :( Tfuj. Zlatý Eclipse + arm-none-eabi-gcc + gdb + Texane.
>> Jak může autor propagovat TrueStudio..
nevidel som sice TrueStudio to bude mozno tym aky je eclipse shit co sa tyka gui. Nikdy som nedokazal eclipse nastavit tak, aby debugger nevyzeral roztahano ako na tablete. Navyse aj eclipse ma nejake problemy ked v ubuntu je stale verzia 3.x a nie najnovsia
Ahoj Petre,
jaky zpusob vyvoje / knihovny tedy preferujes ty? Ja s STM32 teprve zacinam a tak nejak jsem se rozhodl pro CMSIS - prakticky zadny overhead, snad dobra prenositelnost, moznost naucit se vic o tom, jak ten chip funguje...
Druha vec: Jako editor jsem se rozhodl pro Code::Blocks (+ arm-none-eabi-gcc a Texane), mas s nim nejakou primou zkusenost? Hodi se na to?
Diky, Martin
No já mám momentálně knihovny vlastní, založený na CMSIS + RTOS + "objektový C". Takže nečekám ve smyčkách a mezi tím se spustí jiný vlákno (multithread je silně návykový), periferky mají z 80% stejný interface (UART, SPI a I2C jedno jest), moduly si definují závislosti,... Publikovat to ale nesmím, vzniklo to za peníze zaměstnavatele a smlouva to nedovolí.
C::B sice tak nějak funguje, ale je to takový pěkně šišatý kolo. Vypíchl bych několik detailů pro masochsty:
- Konfigurace má několik levelů. Globální, projekt, soubor. Nastavení se mixuje podle ne zcela zřejmých pravidel a je v tom super hokej stylem "přeložím si, jak chci".
- V build logu jsou sice chyby a warningy, ale warningy nejdou rokliknout ve zdrojáku a ani ve zdrojáku není nikde highlight/značka, kde je něco blbě. Docela to brzdí.
- V konfiguraci není vše, třeba tam nenastavíš hardfloat/softfloat, takže hurá editovat XMLko...
- Zobrazení paměti je super, vždycky po bytu, bez textu a ještě si můžeš volit velikost oblasti jenom jako mocninu 2. A samo, že nevidíš, co se děje kolem (takže nevidíš buffer, co ti za chvilku podteče...)
- Lokální proměnný v debuggeru nemají možnost nastavení formátu, takže na dekadický znaky nebo hexa inty rovnou zapomeň.
- Navigace v souborech. Nějaký ichtil tam nacpal tři stromy - source, headers, others. Fakt super, pokud v rámci modulu mám header i zdroják v jednom adresáři.
- Navigace uvnitř souboru je nepochopitelná. Musíš vybrat kategorii (funkce, proměnná, makro) a tam teprve vidíš jejich seznam. Žádný "rozklikni si strukturu" se nekoná,...
- Integrace GITu a SVN je naprosto bez chyby, protože když tam něco není, nemůže tam být chyba
- ...
Napisu Krcmarovi, aby pod tvoje prispevky pridali "donate" tlacitko:)
V tom pripade opoustim C::B a vydam se cestou Eclipse. Coz jsem uplne nechtel, je to nenazranec, ale zrovna ta integrace GITu je pro me dulezita. V aktualnim Debianu je bohuzel pouze 3.8.1, tak to budu tahat bokem...
Ano, HAL v podání ST je čisté zlo, které vyplodil někdo, komu nařídili, že moderní je mít nějakou abstrakci, bez ohledu na to, jestli dává smysl. Stejný projekt (MODBus, USBTMC, nějaké SPI a I2C periferie) přepsaný s "pomocí" HAL nabobtnal o 28% a došlo z pohledu uživatele k viditelnému zpomalení. Celá akce skončila návratem k CMSIS a přímému přístupu k registrům.
No beru, že třeba handle je pointer na strukturu. Beru, že v té struktuře je uložená konfigurace periferky na nějaké úrovni abstrakce, pokud to má smysl. Ale proč u VŠECH musí být KOMPLETNÍ konfigurace jako struktura, ve které je i nastavení jednoho bitu jako uint32_t? To jako kvůli jednomu 32b registru, kde je 32 binárních flagů, musím trpět v paměti strukturu 128B? Ve světě, kde se RAM počítá na kB? Raděj tu RAMku použiju na větší FIFO u UARTu, než na uložení jednou zadané konfigurace, kterou už nebudu potřebovat...
Samo SPI s CMSIS vs HAL 2,5kB FLASH a 130B RAM rozdíl. Funkce stejná. To samo o sobě je na pořádný kuleškub bez umrtvení.
Tfujtajxl HAL pro STM32. Taky na to mám za ty leta vlastní knihovny. Včetně hezky chodícího USB CDC s DMA jako RTOS task. Ne ten humus co vyprodukuje Keil nebo STM32Cube.
Naposledy mě požádal kamarád, že už neví co dál, že řídí nějakej ADC šváb, a když chce přečíst data co mu do něj jdou po SPI, tak to pořád nefunguje. Napsal to s HAL. Jeden by čekal, že když se chce jenom přijímat, tak to bude jenom přijímat. Použil HAL_SPI_Receive. Problém je, že tahle funkce jen využívá HAL_SPI_TransmitReceive, a to co je v bufferu pro příjem se na SPI i vyšle. Chudák ten šváb na druhým konci. Prej Dummy Data. Třikrát si odplivnout.
HAL_StatusTypeDef HAL_SPI_Receive(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout)
{
.
.
/* Call transmit-receive function to send Dummy data on Tx line and generate clock on CLK line */
return HAL_SPI_TransmitReceive(hspi,pData,pData,Size,Timeout);
}
Mě kolikrát přijde, že čím novější verze těch jejich "API", tak tím větší humáč to je.
To si nechám klidně povyprávět. V téhle konkrétní aplikaci máme ADC, který přes sestupnou hranu /DRDY signalizuje že má připravena data na SPI. Dle datasheetu STM32 musí pouze shodit /CS, a načíst 32 x 8-bit hodnot po SPI, na celkem 256 sestupných hran hodin.
O vysílání čehokoliv tam není řeč.
Naopak, pokud se tomu ADC pošle něco po MOSI, tak to začne dělat blbosti, protože se tomu začnou přepisovat konfigurační registry.
Apropo, jak to souvísí s kvalitou HAL ?
Jestliže to ADC má MOSI (SDI) vstup tak bude nevyhnutelně přijímat data kdykoli na SCK poběží clock (tedy i v situaci kdy STM32 čte z ADC). A to dokonce i v případě, že by vás napadlo MOSI "nikam" nepřipojit. Pointa je poslat mu taková data, která "nic nezpůsobí" (dummy), což bude jistě zmíněno v datasheetu k ADC. Představa že vysílání a příjem jsou dvě rozdílné akce je tedy špatná.
Přesně tak, takže HAL funkce pro čtení musí buď 1) na MOSI nehrabat, nebo 2) alespoň nastavit MOSI na definovanou hodnotu.
Ale to, o čem se celou dobu bavíme milý Watsone, je fakt, že HAL pošle na MOSI obsah bufferu, do kterého se má podle dokumentace jen přijímat. Tento buffer obsahuje většinou minulá přijatá data, pokud o této zrůdnosti nevíme.
Kdyby zcela blbě nevytvořili funkci HAL_SPI_Receive, která je vlastně HAL_SPI_TransmitReceive, aniž by to uživatel tušil, tak by k tomu nedošlo. Přímým voláním HAL_SPI_TransmitReceive by uživatel věděl, že má nastavit data k vyslání na dummy hodnoty.
Chcete mi snad vsugerovat myšlenku, že je zcela normální, že pokud je funkce označena jako ČTI, tak že data i ZAPISUJE ? A přitom v dokumentaci k této funkci není o zápisu ani řeči ?
Upozorňuji, že se bavíme o kvalitě HAL, ne o tom, kdo ho má většího, nebo kdo je větší blbec.
Má-li HAL funkce ČTI, ZAPIŠ_a_ČTI a ZAPIŠ, očekává prostý uživatel že funkce bude číst. Ne, že zavolá ZAPIŠ_a_ČTI když nic zapisovat nechci.
Než mě znovu osočíte, zkuste si něco najít o periferiích, které z principu funkce NEPOTŘEBUJÍ, nebo NEMAJÍ nutnost před odesíláním dat do STM32 přijmout adresu či opcode.
Ale neříkejte, to by jeden nepověděl. A o tom jsem se v tom prvním příspěvku vlastně bavil. To jsem to napsal tak blbě ?
Vždyť je to jasné - pokud chci číst, a nezmrvit data v ADC, NESMÍM hrabat na MOSI, a jen číst MISO. A přesně to byste očekával od funkce HAL_SPI_Receive. Z dokumentace:
HAL_SPI_Receive
Receive an amount of data in blocking mode.
hspi: pointer to a SPI_HandleTypeDef structure that contains
the configuration information for SPI module.
pData: pointer to data buffer
Size: amount of data to be received
Timeout: Timeout duration
HAL status
HAL_SPI_TransmitReceive
Transmit and Receive an amount of data in blocking mode.
hspi: pointer to a SPI_HandleTypeDef structure that contains
the configuration information for SPI module.
pTxData: pointer to transmission data buffer
pRxData: pointer to reception data buffer
Size: amount of data to be sent and received
Timeout: Timeout duration
HAL status
Prostě očekáváte, že se pData naplní přijímanými daty, a NIC nehrabe na MOSI. Ne, že se jedná fakticky o HAL_SPI_TransmitReceive, kde pTxData = pRxData. To tam nikde zdokumentované není.
A o tom je celý HAL.
Nepochybuji o tom, že HAL stojí za prd a nepoužívám ho. Výraz "NIC nehrabe na MOSI" je nesmyslný. Nějaká logická hodnota tam musí být. Krom toho SPI se na STM32 dá používat klidně simplexně (tedy jenom příjem, nebo jenom vysílání). Z toho plyne, že existence obou funkcí má opodstatnění a vy jste měl tu smůlu že jste si vybral špatnou. Těžko lze ale házet vinu na autora HALu, on asi předpokládá že uživatel bude mít ponětí o tom že při duplexní konfiguraci data tečou oběma směry ...
Ale to já přeci vím, proč mi pořád podsouváte že to nevím ?
Měl jsem tam připsat velkými tiskacími "vím jak funguje SPI", a protože právě vím že funguje tak jak funguje, tak předpokládám, že mi funkce pro čtení nikam nic nepíše, protože to může rozbít věci ? A když to udělám, proč to nenapíšu do dokumentace ?
Já se přeci celou dobu netočím na SPI a jeho použití, já se točím na té prasárně v HALu.
Proč jste tak natvrdlý....
OK mrkněte na tyto tři situace:
A) SPI máte nakonfigurováno v simplexním režimu v roli vysílače. STM32 tedy může pouze vysílat, linka MISO neexistuje (slave má pouze linky CS,SCK,SDI). V takovém případě voláte funkci HAL_SPI_Transmit()
- typicky případ primitivních ADC a pod.
B) SPI máte nakonfigurováno v simplexním režimu v roli přijímače. Linka MOSI neexistuje. Slave má pouze linky CS,SCK,SDO. V takovém případě voláte funkci HAL_SPI_Receive()
- typický případ primitivních DAC nebo digitální potenciometrů
C) SPI máte nakonfigurováno v duplexním režimu. Linky MOSI i MISO existují, slave má linky CS,SCK,SDO a SDI. V takovém případě voláte funkci HAL_SPI_TransmitReceive()
- typicky pro všechna trochu chytrá zařízení
Co je na těchto funkcích nelogické ? Každá z nich má právě ty argumenty které ke své činnosti ve správné roli potřebuje. Vy jste si vybral špatnou a místo toho aby jste sypal popel na svou hlavu snažíte se hodit vinu za váš výběr autorům HALu. Což vám ale k ničemu nebude, protože to komunikaci s vaším ADC neopraví.
Dalo by se to přirovnat k situaci kdy si člověk nakonfiguruje USART v 9bit režimu (neříkám že to na STM32 přímo jde), zavolá funkci USART_Transmit(uint8_t data), která očividně žere 8bit data a pak se diví že 9.bit zprávy má jinou hodnotu než chtěl. Člověk který protokolu rozumí se pozastaví nad tím, že funkci nejde předat všechna potřebná data (9 bitů) a začne hledat tu správnou... stejně jako člověk který rozumí SPI se pozastaví nad tím, že při duplexním provozu funkci HAL_SPI_Receive() chybí klíčový argument (co odeslat na MOSI) a začne hledat správnou ...
Já tomu pořád moc nerozumím. Koukám do specifikací SPI a nikde tam nevidím možnost, jak druhému zařízení říci "hele, já teď budu hejbat hodinama, ale probohe hlavně se nekoukej, co je na drátu MOSI".
HAL má funkci "receive", která je poměrně jasně specifikovaná: načte data od slave zatímco mu pošle nedefinovaný bordel. Ta funkce má použití tam, kde to slave zařízení žádná data nečte, respektive je ignoruje. Ta funkce má výhodu v tom, že nemusíte alokovat strukturu pro odesílaná data. Ušetříte pár bajtů.
Takže první problém je, že si nějak myslíte, že SPI umí číst bez toho, aby psalo. Neumí. A pak je tam možná problém druhý, že neumíte číst dokumentaci. Konkrétně mám na mysli zaužívanou konvenci "pokud to není definováno, tak je to nedefinované". Což v tomhle případě znamená, že nikde není definováno, co se bude odesílat - takže předjímejte, že to budou náhodné bity. Ve skutečnosti to posílá ven to, co přichází, ale to je nedokumentované chování a nesmíte se na to spolehnout. V nové verzi knihovny to může být jinak.
My se přeci nebavíme o SPI jako takovém, ale o knihovně HAL. HAL = hardware abstraction layer. Já přeci samozřejmě celou dobu _vím_ že SPI, pokud obsahuje obě dvě lajny MOSI i MISO, tak je full duplex, a nejde říct zařízení ať to nepřijímá. Proboha.
Celou dobu jde o to, že HAL definuje TŘI funkce. ČTI, ČTI_a_ZAPIŠ a ZAPIŠ. Vy píšete, že ČTI je jasně specifikovaná. NENÍ. Prostě není, i když se na hlavu postavíte. V dokumentaci se píše, že pouze čte, nepíše se tam to co píšete vy. A teď z logiky věci. HAL má být abstrakce od hardware. Když používáte HAL, tak se spoléháte, že nízkoúrovňově je to uděláno tak, že funkce odpovídají skutečnosti. Selským rozumem i dokumentací, mám-li ČTI, ČTI_a_ZAPIŠ a ZAPIŠ, tak já a další lidi tvrdí, že to má sakra dělat tohle:
a) ČTI = čti MISO, na MOSI nešahej
b) ČTI_a_ZAPIŠ = čti MISO a zároveň piš na MOSI
c) ZAPIŠ = ignoruj co je na MISO, piš na MOSI
To, co mi furt gripen podsouvá a vy nechápete je, že chcete, aby HAL (=abstrakce) fungovala takhle, a považujete to za správné:
a) ČTI = čti MISO, na MOSI posílej náhodný bordel
b) ČTI_a_ZAPIŠ = čti MISO a zároveň piš na MOSI
c) ZAPIŠ = čti náhodný bordel z MISO, piš na MOSI
Správně by HAL měl buď:
a) definovat POUZE funkci ČTI_A_ZAPIŠ, pak by uživatel byl nucen zajistit, aby na linku nešel bordel
nebo
b) definovat všechny tři funkce, ale pak musí HAL zajistit, aby při pouhém zápisu nebo pouhém čtení nebyla brána v úvahu druhá linka
Pořád píšete věci jako "piš" a "čti". Ale tam jsou dráty, ne zprávy. Zkuste pochopit, že na každém tom drátu je nějaké napětí, které reprezentuje LOW nebo HIGH. Čti nedělá nic jiného, než že si zapíše, jaké napětí na tom drátu v tu chvíli bylo. A piš má dělat co? Smysl má pouze: nastav napětí na LOW, nastav napětí na HIGH nebo tam nech napětí, co tam bylo před tím. Kterou z těch tří věcí si za "piš" představíte právě vy? Petr M tu někde zmínil, že on posílá UINT16_MAX, což jsou samé jedničky. Nechat tam poslední stav znamená posílat poslední bit z předchozí zprávy (takže buď samé nuly nebo samé jedničky, podle toho, čím to končilo). Byly by samé nuly v něčem lepší?
A dále mě udivuje vaše víra, že HAL nějakou magií dokáže něco, o čem víte, že fyzicky není možné.
Navíc vámi navrhované řešení A předjímá, že uživatele zajímá, co na linku posílá. Přitom v drtivé většině případů ho to nezajímá. Na SPI má připíchnuto něco, co buď jenom čte (display) nebo jenom píše (senzor). Proč by se měl doprčic zalamovat obsluhou nějakého bordelu, co na nic nemá vliv? Proč plácat paměť na ošetření něčeho, co není důvod řešit? Aby to bylo blbuvzdorné a nutilo lidi myslet? On HAL už takhle stojí za starou belu, ještě aby se tak z něj někdo snažil dělat evangelium za "vývojáři bděte".
A vámi navrhované řešení B je právě ta víra v magii, že by to snad nějak udělat šlo. Nešlo, ten drát tam je a druhá strana si při hodinovém pulzu tu hodnotu přečte a uloží. HAL nemá jak tomu druhému zařízení říct, že nemá brát v úvahu tu druhou linku. To SPI neumí a název knihovny na tom nic nezmění.
Průšvih je v tom, že SPI je full duplex. Vždycky. Podle specifikace jsou to čtyři posuvný registry - jeden v masteru vezme paralelní data a posílá je na MOSI, kde druhý ty data převede na paralelní a naopak, třetí posílá data ze slave na MISO, kde je paralelizuje čtvrtý. Tím končí specifikace. Nikde se neříká, co ty data mají nebo nemají obsahovat.
No a teď si představte jednoduchou periferku, třeba 4x 12b ADC, kde na vstupu je 8b slovo, ve kterým 0001xxxx je maska povolených kanálů (x povoluje/zakazuje příslušný kanál), 0010yyyy přepíná unipolární/bipolární režim pro jednotlivý kanály,... Příkazy 0000xxxx a 1111xxxx ignoruje a příkazy se dají serializovat. To je naprosto korektní chování podle specifikace.
Chceme číst data ze všech čtyř kanálů a nakonfigurujeme si ho. No a protože CH1 načetl poslední hodnotu 0x0012 a nějaký blb mu to teď pomocí HALu pošle zpět, tak už se CH1 nenačte, protože od teď se dojí jenom CH2 - čtyři čtení toho samýho kanálu v řadě. A protože náhodou na něm je hodnota 0x0af5, nic se na tom nezmění...
Tímhle způsobem se obvod může překonfigurovat úplně náhodně (v závislosti na šumu vstupního signálu) a pěkně blbě se pak taková chyba hledá.
To je furt dokola...
Ve vlastním ovladači, pokud čtu, zapisuju do Tx registru zásadně UINT16_MAX a neměním ho.
Soudruzi z STM vezmou buffer náhodné velikosti s náhodnými daty a začnou ho blít ven. Krom toho, že to odporuje jejich vlastní specifikaci, kdyby tohle udělal IP stack a během příjmu začal blít na náhodou IP adresu poslední přijatý paket, tak by to bylo nejenom na CVE, ale i na vlastní web chyby...
Houba to píše správně (a to mikročipům nerozumím) - jestliže má být HAL vysokoúrovňovou vrstvou, tak v případě, že chci POUZE číst a mám na to EXTRA funkci, tak přece nebudu ještě za HAL kurva řešit, jak naplnit jakýsi buffer pro zápis sračkama, aby to fungovalo. To pak nepotřebuju HAL. Teprve až budu potřebovat zároveň zapisovat a číst, použiju na to jinou funkci. Co je na tom panu Gripenovi nejasného?
Tady je problém v tom, že SPI neumí z principu věci nic jako POUZE číst. Tam je prostě pár drátů, které mají nějakou funkci. Jedním z nich jsou hodiny - když tím zahejbám, tak tím říkám druhému zařízení, že má na drátu MISO nastavit další bit a na drátu MOSI si bit načíst. Ty hodiny jsou prostě drát, kde náběžná hrana znamená "teď". Tam není žádný způsob jak druhému zařízení říci "pošli mi další bit ale sám nic nečti". Na to je to rozhraní příliš primitivní.
Houba tvrdí, že tuhle vlastnost SPI zná. Ale tak proč se diví, že to SPI něco posílá, přestože se funkce jmenuje "Receive"? Vždyť ví, že technicky není možné na SPI jen číst nebo jen psát.
A druhá věc je, že v dokumentaci není nikde žádné POUZE. V dokumentaci je přesně popsáno, co to funkce deklaruje, že udělá: přijme data. Nedeklaruje, co pošle. Tedy bude posílat nějaký bordel. K čemu ta funkce je dobrá? Ono řada SPI zařízení (senzory apod.) nic nečte. Kolikrát nemají ten drát zapojený, nebo prostě přijatá data nezpracovávají. Proč bych pak měl tedy plácat pamětí na alokaci výstupního bufferu? A na otázku proč neposílat jen samé 0 nebo 1 je odpověď také snadná: protože nechci mít 3 funkce. Tím, že funkce Receive a Transmit jsou jen aliasy pro TransmitReceive ušetřím docela dost paměti. Daň za to je ta, že Receive posílá to, co přijímá, a Transmit přijímá to, co vysílá. Protože ty dva pointery ukazují na stejný blok paměti.
PS: HAL je zlo a sám bych ho nepoužil. Ale tady v tom konkrétním případě je problém mezi klávesnicí a židlí. Ten pán se diví, že HAL dělá to, co SPI z podstaty věci dělat musí. Buď neví co je SPI, nebo jen doufal, že HAL nějakou magií dokáže něco, co SPI ne.
Mě to celkem jasné je. Mýlíte se v tom, že máte EXTRA funkci sloužící ke čtení. Nemáte ji. Ta funkce slouží k něčemu jinému ! Slouží k práci s SPI v simplexním režimu. Jinak řečeno slouží k něčemu jinému než se vy domníváte. Proč nezkoušíte posílat data po SPI pomocí funkce "HAL_UART_Transmit()" ? No protože očividně k tomuto účelu neslouží. A stejně tak funkce "HAL_SPI_Receive()" očividně neslouží pro "příjem" po duplexním SPI.
Pochopte laskavě, že po HALu (nebo jakékoli jiné knihovně) chcete aby provedla nemožné. Píšete že chcete POUZE číst, ale to u duplexního SPI nejde :) Prostě to není to možné. Duplexní SPI nemá nic jako "příjem" ...
To, že se o to někdo pokouší je jen ukázka toho, že používat knihovny bez porozumění věci (v tomto případě protokolu) je rizikové...
Ach ... už som pochopil ... zrejme narážate na tento kúsok hneď na začiatku HAL_SPI_Receive:
if ((hspi->Init.Mode == SPI_MODE_MASTER) && (hspi->Init.Direction == SPI_DIRECTION_2LINES))
{
hspi->State = HAL_SPI_STATE_BUSY_RX;
/* Call transmit-receive function to send Dummy data on Tx line and generate clock on CLK line */
return HAL_SPI_TransmitReceive(hspi, pData, pData, Size, Timeout);
}
... toto je fakt dosť hnus ...
Že se obsah přijímacího bufferu za určitých okolností pošle na SPI sběrnici je vcelku přirozená věc. Umožňuje vám totiž řetězit SPI slave zařízení za sebe (daisy chain). I když se to na první pohled nezdá SPI není až tak primitivní protokol a je prostě potřeba trochu opatrnosti při používání. U I2C taky člověk z ničeho nic nezavolá funkci "receive" a nezačne nadávat, že místo přijmu to začalo vysílat adresu ....
Posílat náhodný blok dat z náhodné adresy ven z brouka na jakoukoliv lajnu je z principu špatně. I kdyby na to brouk na druhým konci nereagoval. Je to chyba na úrovni heartbleed a dá se takhle osciloskopem chytnout část paměti.
Existují čtyři možnosti, jak MOSI zabít:
a) Pollling (čekání na flag ve smyčce): po přijetí bytu hodit do vysílacího registru UINT16_MAX a odvysílat.
b) Interrupt (obsluha přenosu v přerušení): Přerušovací rutina zajistí odvysílání UINT16_MAX
c) DMA: vyrobím si konstantu va FLASH s hodnotou UINT16_MAX, dám pointer na ni do vysílacího kanálu DMA a přepnu do režimu "konstantní pointer".
d) Univerzální řešení, překonfigurovat MOSI na GPIO OUT a picnout tam hodnotu natvrdo (to už je s HALem na rychlý rozhraní hodně velká drsárna, vyplnění komplet konfigurační struktury a její nahrání do portu).
Takže opravdu není důvod tam cpát jakýkoliv buffer. Ani náhodný, ani inicializovaný. Prostě to zase nezvládli.
A kdyby to soudruhy v ST napadlo, mohli tam do registru hodit bit BLOCK_TX který by tam ty jedničky prostě rval sám... Ale jeden bit v registru a jedno hradlo OR na výstupu shift registru by asi už nedali.
To je marná diskuze. Napřed tu stále dokola čtu, že fce "receive()" nemá na MOSI dávat nic. Po dlouhém přesvědčování vyjde najevo, že je to nesmysl protože na MOSI něco být musí. Pak aby si tu někdo nemusel přiznat, že si prostě vybral špatnou funkci, začne tvrdit, že "receive()" má na MOSI sypat jedničky. Proč proboha jedničky ? :D Proč ne nuly nebo proč ne sekvence 010101010101010101 ...
Zkusím to naposledy, funkce receive() je určena pro SPI v simplexním režimu. Nemáte ji volat v duplexním režimu, k tomu účelu ji nikdo neprogramoval. Nemá smysl jí dávat jakékoli parametry pro MOSI, protože když ji voláte v situacích k nimž je určena, žádný MOSI vývod neexistuje :)
Ale jestli si chcete zanadávat mám pro vás pár tipů:
a) Vypněte timer a volejte fci "TIM_SetCompare1()" - můžete pak nadávat že na výstupu není PWM ačkoli volání fce by ji mělo přece nastavit... zase to v ST zvorali...
b) nastavte DAC na trigrování timerem a volejte fci "DAC_SoftwareTriggerCmd()" - můžete nadávat že i když voláte trigger na DAC, tak on ne a ne vyhodit příslušné napětí ... zase to v ST zvorali
a takovými ukázkami použití funkce, která nemá při dané konfiguraci periferie smysl, by se dalo pokračovat ještě dlouho. A do tohoto seznamu patří i SPI_receive() s SPI nastaveným na duplex...
1. Kde v dokumentaci je zmínka o simplexním nebo duplexním režimu?
2. Samý jedničky odpovídají nezapojené lince s pullupem. Je to nejbezpečnější varianta a nepsaný standard pro ošetření.
3. Obvody na SPI jsou z pohledu specifikace dlouhý shift registr a když do nich začnu nasouvat nějakou hodnotu, v okamžiku uvolnění CS se tato změna aplikuje. Pokud tam během přenosu nashiftuješ něco, co vypadá smysluplně, máš problém.
4. Blít ven náhodný data je vždycky špatně, i pro simplex. Protože SPI se dá sdílet (několik CS na GPIO, například) a data tak můžou být vyvedený z pouzdra do jinýho brouka. A teď si představ, že je to třeba na STM32F439 (LAN + display + ext bus + crypto) a takhle ti to náhodně vybleje klíč k update firmware, který je jinak uložený interně ve FLASH. Na tohle diletantství prostě není omluva.
On ale nebleje náhodně data z paměti. Ven posílá to, co přijímá. Odesílací buffer se neodkazuje na náhodné místo v paměti, ale je shodný s bufferem pro příjem. Takže po MOSI nejde nic, co nepřišlo po MISO. Nejsem si jistý, že by to bezpečnost nějak zhoršovalo, protože připojení brouci vidí MOSI i MISO. Pokud tedy nějaká periferie po MISO posílá klíč k update firmware, tak už je celkem jedno, že to master zopakuje i na MOSI. Pletu se?
Můžu se Vás optat, kde jste přišel na to, že funkce HAL_SPI_Receive je jen pro simplexní režim ?
Ta funkce začíná takhle, a pro master režim se provede tohle (všimněte si, SPI_MODE_MASTER a SPI_DIRECTION_2LINES):
HAL_StatusTypeDef HAL_SPI_Receive(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout)
{
#if (USE_SPI_CRC != 0U)
__IO uint16_t tmpreg = 0U;
#endif /* USE_SPI_CRC */
uint32_t tickstart = 0U;
HAL_StatusTypeDef errorcode = HAL_OK;
if((hspi->Init.Mode == SPI_MODE_MASTER) && (hspi->Init.Direction == SPI_DIRECTION_2LINES))
{
hspi->State = HAL_SPI_STATE_BUSY_RX;
/* Call transmit-receive function to send Dummy data on Tx line and generate clock on CLK line */
return HAL_SPI_TransmitReceive(hspi,pData,pData,Size,Timeout);
}
Já tam naopak vidím, že funkce Receive pro dvě lajny provede tu nebetyčnou prasárnu, o které se tu celou dobu bavíme. Způsob, jakým to měli soudruzi v ST ošetřit, Vám tu podal vyčerpávajícím způsobem Petr M.
Proč se proboha pořád snažíte obhájit _takovou_ prasečinu ?
Vždyť je to přece totálně _naopak_, než se tu snažíte prezentovat. Každý alespoň trochu při smyslech prostě _očekává_, že MOSI je na definované úrovni, pokud jen čtu MISO, a používám na to HAL (=abstrakci). A ne, že vysypu ven buffer s pitomostma.
Než budete pokračovat, tak nikde v dokumentaci není že ta funkce je jen pro simplex. Nikde v dokumentaci není, co ta funkce sype ven na MOSI. Naopak pohled do kódu HAL Vás usvědčuje ze lži. Kdybyste to hledal, je to version V1.7.1 ze 14-April-2017 pro STM32F4.
Ach jo. SPI simplex neumí. Takže těžko bude někdo v dokumentaci psát, že je ta funkce pro simplex. Když si přečtete váš první příspěvek, tak jste to právě vy, kdo čekal, že ta funkce simplex bude.
Fakt, že dokumentace vůbec nepopisuje, co odesílá, neznamená, že neodesílá. Znamená jen to, že vám neslibuje, co se bude posílat.
Co posílá vidíte sám (pData):
return HAL_SPI_TransmitReceive(hspi,pData,pData,Size,Timeout);
Prasečina to je, to jsem nikdy nevyvracel. HAL prostě je špatně udělaný. V čem vám celou dobu oponuji je to, že vás překvapilo, co ta funkce dělá. To vás nemělo jak překvapit. Zaprvé znáte SPI, takže víte, že to (navzdory názvu) něco posílat bude. A za druhé vidíte, že v dokumentaci není popsáno co, takže je vám jasné, že to bude posílat bordel, na který nebude spoleh. Vy jste tuhle funkci prostě neměl použít - a to vám mělo být jasné už z její dokumentace, která se vůbec nezmiňuje o tom, co to posílá. Pokud víte, že to nesmí posílat bordel, tak máte sáhnout po funkci, která pošle to, co chcete.
V textu zmiňujete "MOSI je na definované úrovni". To jste si opravdu nikdy nevšiml, že vy žádnou úroveň nedefinujete? A že ani SPI specifikace nic takového nemá? Když podle vás existuje taková definovaná úroveň, tak kde tedy přesně tu definici máte?
Valná většina periferií, kromě speciálních případů, na MOSI v pull-upu na H, bude buď nereagovat, nebo reagovat nedestruktivně (=nebude třeba do paměti nebo vnitřních registrů zapisovat). Jak psal Petr M., je to nepsaný standard.
Návrháři periferií nejsou magoři, pro to aby byl do periferie vyslán příkaz, tak je potřeba v 99 procentech případů vyslat něco jiného než je 0x00 nebo 0xFF. A to je celé jádro pudla té prasárny v HAL !!!!!
Ta tam totiž pošle zcela náhodný buffer.
To je to fakt tak strašně složité na pochopení ?
Ok. Funkce receive() má vysílat jedničky ... jen doufejte, že na druhém konci planety není někdo zastávající opačný názor, tedy že funkce receive() má vysílat nuly. Konec konců, běžně se programuje tak, že se člověk podívá na funkci, řekne si ... "Kdybych ji psal já tak by na MOSI cpala jedničky, v komentáři o tom nic není, takže to určitě dělá." ... a pak se funkce použije.
Nicméně souhlasím, že by hodně lidí ocenilo funkci, která čte z MISO a na MOSI cpe volitelnou konstantu...
Stejně tak souhlasím že fce HALu (resp LL nebo SPL) jsou málo komentované (člověk se z nich moc nedozví k jakému účelu fungují). Ale pořád je to o poznání lepší jak na ARMech od ATMELu, jejich funkce nemají vůbec žádný komentář, ani seznam maker, která mohou být argumentem ...
Taky se v tom ztrácím. Měl jsem za to že vám vadí, že fce SPI_receive() něco vysype na MOSI. Asi jsme se nějak shodli na tom, že když je to full duplex SPI, tak to na to MOSI něco vysypat musí, protože slave chtě nechtě linku MOSI bude číst. Dál jsem vaši argumentaci pochopil tak, že když už to musí něco sypat tak ať to sype samé jedničky. Je to tak ?
Pokud ano, pak je to funkce "naprd" protože třeba při čtení z tohoto ADC zařve
(LTC2488) http://cds.linear.com/docs/en/datasheet/2488fb.pdf
Pokud ne, tak co si tedy představujete, že by SPI_receive() měla při duplexním SPI udělat ?
Já tomu teda dám šanci. Mě vadí to, že ta přijímací funkce blije ven náhodná data. Důraz je na to slovo náhodná. Také jsem Vám dokázal, že ta přijímací funkce není jen pro simplexní použití, ale dokonce plně počítá s duplexním provozem. Pak jsem Vám taky napsal, že by úplně stačilo, kdyby ta funkce buď vyžadovala po uživateli ten buffer co se bude vysílat (tedy nutila uživatele použít funkci pro příjem i vysílání zároveň), nebo použila pro výstup trvalou definovanou úroveň (1 nebo 0). Pak jsem Vám napsal, že návrháři periferií nejsou idioti. No a vy jste kontroval datasheetem, že když to pohrne samé jedničky tak je to k ničemu.
No vida, a když otevřete ten datasheet, tak skutečně zjistíte, že návrhář nebyl idiot. Já tam vidím pro bajt přijatý po SPI tohle:
1 | 0 | EN | SGL | ODD | A2 | A1 | A0
Aby ten čip reagoval na vstup po MOSI, tak ty první dva bity musí být 10. Ne 11, a ne 00. Pokud tam tedy bude trvalá úroveň, tak ten šváb nijak reagovat nebude. Pokud se ale vyskytnou na sběrnici náhodná data, tak po detekci 10 v těch náhodných datech se Vám právě začne přepisovat registr. Vždyť tohle byl přesně ten problém, na který jsem upozorňoval v tom prvním postu.
Vy budete argumentovat, že v tomhle případě ale použijete funkci TransmitAndReceive, tedy zapisuj a čti zároveň. Ano, v tomto případě je to vyžadováno, protože ADC nepošle žádná data na MISO, dokud mu nedojde ten jeden bajt. Jestli dobře vidím, tak posílá 4 bajty zpět. Pro řízení tohohle švába teda stačí naalokovat 4-byte buffer, do prvního bajtu hodit ten konfigurační bajt, do zbytku 0x00 nebo 0xFF protože šváb to bere jako don't care, a po provedení transferu máte v těch čtyřech bajtech výsledek. Easy, nelze tady použít špatnou funkci, protože funkce pouze pro čtení se Vám sem nehodí.
Ale ten ADC, o kterém jsem mluvil já, nic takového neměl. Vy jste ho nakonfiguroval, pomocí TransmitAndReceive, a pak Vám zcela automaticky připravoval data pro přenos po MISO. A tam kamarád udělal onu osudnou chybu, když zcela logicky, abstraktně od hardware, použil k tomu přímo určenou funkci ČTI. Přitom by úplně stačilo, kdyby ta funkce čti držela MOSI na 0x00 nebo 0xFF. Na Váš ADC by to taky nemělo žádný vliv.
Takže jádro sporu je v tom jak by ta fce "receive()" měla vypadat. Vy navrhujete aby fce "receive()" sypala 0 nebo jedničky aniž by nad tím měl programátor kontrolu. Já říkám, že je to v podstatě stejné jako když sype náhodná data. Když má něco sypat, nechť o tom rozhoduje programátor a předá tu "dummy" hodnotu funkci jako argument.
Ten datasheet měl poukázat právě na to, že dummy byte složený z log.1 by dělal problémy. Explicitně se tam totiž píše o první trojici bitů, že "Valid settings for these three bits are 000, 100, and 101. Other combinations should be avoided." Jinak řečeno 111 = problém.
Když tedy bude existovat fce "receive()" bez vstupního argumentu a bude sama sypat někým zvolenou hodnotu (samé jedničky nebo samé nuly) vznikne stejný problém, jaký byl na začátku. Programátor čeká, že receive bude "pouze" číst a ona něco vysype (programátor neví co, protože si stejně jako váš kamarád neprohlídl vnitřnosti funkce). Shodou okolností si zrovna vybere slave který jedničky nebo nuly "nesnáší"(viz výše zmíněné ADC) a požár je na střeše.
Když bude mít fce "receive()" vstupní argument jaká dummy data má sypat, nastane zase jiný problém. Programátor který má SPI nastavené v režimu "receive only" (tedy simplex) bude hledat fci "receive()" bez argumentu (on přece nic posílat nemůže ani nechce). Protože takovou fci nenajde, bude nadávat na autory HALu, že tam není.
O tom jestli stávající "receive()" funkce je nebo není pro simplexní použití se můžeme jen přít. V dokumentaci k HALu o tom není zmínka. Já vycházím z předpokladu, že k ničemu jinému rozumně sloužit nemůže (v duplexu sype "náhodná" data), takže proč by tam jinak byla ?
"Takže jádro sporu je v tom jak by ta fce "receive()" měla vypadat."
No konečně, o tohle jde celou dobu. Bavíme se o tom, že je sprasený HAL, ne HW.
"Explicitně se tam totiž píše o první trojici bitů, že "Valid settings for these three bits are 000, 100, and 101. Other combinations should be avoided." Jinak řečeno 111 = problém."
Ne, 111x xxxx bude ignorováno (avoided). Proto tam cpu UINT8_MAX nabo UINT16_MAX podle režimu, nepsaná dohoda.
"Když tedy bude existovat fce "receive()" bez vstupního argumentu a bude sama sypat někým zvolenou hodnotu (samé jedničky nebo samé nuly) vznikne stejný problém, jaký byl na začátku."
Na C-čku je krásný mj. to, že pokud k funkci dám míň argumentů, tak se program nepřeloží. Stačí přidat argument pro tu hodnotu a je hotovo.
"Když bude mít fce "receive()" vstupní argument jaká dummy data má sypat, nastane zase jiný problém. Programátor který má SPI nastavené v režimu "receive only" (tedy simplex) bude hledat fci "receive()" bez argumentu (on přece nic posílat nemůže ani nechce). Protože takovou fci nenajde, bude nadávat na autory HALu, že tam není."
Tak mu napíšu do dokomentace, ať tam střelí libovolnou hodnotu, pokud je MOSI ignorovaný. Nebo zapouzdřím funkci pomocí makra, ať to nežere tolik paměti.
Eventuálně kdy už se periferka inicializuje vyplněním struktury, která je trvale uložena v handle, může být možnost si tam tu hodnotu předdefinovat...
Možností, jak neudělat takovou prasárnu, je dost. Jenom myslet (ale to soudruhy od ST bolí).
"O tom jestli stávající "receive()" funkce je nebo není pro simplexní použití se můžeme jen přít. V dokumentaci k HALu o tom není zmínka. Já vycházím z předpokladu, že k ničemu jinému rozumně sloužit nemůže (v duplexu sype "náhodná" data), takže proč by tam jinak byla ?"
Je pro univerzální použití. Díval jsem se schválně na dema od ST a byla použita i pro paměti řady 25xx, který v simplex nemají šanci (nutnost zapsat příkaz + adresu).
Prostě HAL je žumpa a tím bych to asi uzavřel.
Dovolil bych si poznamenat, že implementace čekací smyčky (delay) postrádá kvalifikator volatile u iterační proměnné. Při kompilaci s optimalizací by se totiž mohl překladač rozhodnout, že to je celkem zbytečný kód, co nic nedělá a vlastně i zbytečná funkce. https://en.m.wikipedia.org/wiki/Volatile_(computer_programming)
Pokud tím HW myslíte STM32 tak nepochybně ano. Z mého úhlu pohledu jsou STMka užitečná i když na nich neběží žádný sofistikovaný kód a to hlavně kvůli periferiím. Často dělám přesné časování (STM zvládá na jednotky až desítky ns), občas nějaký rychlejší sběr dat (tam člověk ocení DMA a slušnou RAM). Někdy je zase potřeba chrlit z DA převodníku nějaký průběh. A to všechno se schopností rychle reagovat (~1us) na nějaké vnější změny. Žádná rozumnější platforma mě nenapadá (FPGA je na spoustu věcí overkill). A to vše jde většinou ovládat relativně primitivními programy...
Přiznám se, že prakticky to teprve budu na STM32F1 v následujících měsících řešit, zatím jsem si s tím jen celkem úspěšně hrál, ale nepředpokládám výkonové problémy s http://www.chibios.org/dokuwiki/doku.php?id=chibios:kb:timing . Bez vláken bych se do svého projektu neodvážil.
Takováto čísla jsem při testování na Maple mini za $3 taky dostal (např. > 900tis. kontext switchů vláken/s - http://chibios.sourceforge.net/reports/STM32F103-72-GCC.txt , zdroják testu https://github.com/ChibiOS/ChibiOS/blob/master/test/rt/source/test/test_sequence_013.c#L338
Jen tak pro zajímavost, k čemu tam ta vlákna potřebujete? Procesor je jen jeden a všechny vnější události se projeví jako přerušení (i "poll style" může být přerušení časovače). "Hlavní vlákno" pak buď nedělá nic nebo zpracovává v přerušeních a DMA kanály nasbíraná data, případně řeší power management.
V projektu s dost rozsáhlým GUI (touchscreen TFT) běží řada tasků:
* příjem a zpracování zpráv z několika sériových portů - na některé se reaguje změnou GUI, některé se jen přeposílají dál na jiné porty
* čte analogové porty a posílá zprávy na porty
* čte touchscreen a reaguje na to - sama knihovna UGFX https://ugfx.io/ používá řadu vláken
* GUI má řadu současně běžících časovaných prvků
Když třeba vstupním vláknem zpracovávám message obdrženou do mailboxu od IRS sériového portu ze vstupu, mohu být klidně přerušen jiným vláknem, které se právě dočkalo v mailboxu dat od IRS AD konverze, jež vlákno zabalí do zprávy a pošle sériákem někam dál. Obě vlákna mohou mít stejnou prioritu a prohazovat se po uplynutí timeslicu - vykreslování GUI bude pomalejší, ale poběží. Nebo může mít jedno prioritu vyšší (třeba zrovna GUI kvůli plynulosti vykreslení), dle potřeby.
Každý odchozí port má své vlákno se vstupním mailboxem. Když do něj dostane data, probudí se a zprávu odešle. Vlákna generující zprávy nemusí nic zásadního řešit, prostě zprávu pošlou do mailboxu.
Zprávy jsou brané z předdefinovaného memory poolu, kam se po odeslání zase vrací. OS má statickou správu paměti, nemá malloc/free. Memory pooly mohou případně vlákno bloknout na nastavený čas, pokud je zrovna prázdný. Vlákno paměť buď stihne dostat, nebo má smůlu a musí se zařídit jinak (třeba zkusit to znovu). Ale to je klasika...
Nechci vše míchat do jednoho kódu, když mohu pro jednotlivé funkčnosti vytvořit vlákna a miniOS si je sám časuje/přepíná. Vláknům lze nastavovat různé priority, pre-emptivně se přepnou dle potřeby (přijde interrupt od periférie, "vyprší" softwarový timer od scheduleru, přijdou očekávaná data do mailboxu atd.).
Samozřejmě to vyžaduje správně pracovat se zamykáním, ale to je normální, to samé musím řešit v pythonním prototypu celého řešení (několik STM32 + PC), na kterém zatím dělám.
Co jste popsal výše vlákna zcela určitě nepotřebuje a nezbývá než popřát, aby Vám nehráblo později, až budete v tom "OS přímo určeném pro STM32" hledat nějaké podivné chování, které se projevuje pouze u zákazníka. STM32F103 a "rozsáhlý program" se tak trochu vylučují, ne? ;-) Pro zajímavost potom napište, zda se zvolené řešení osvědčilo neboť moje zkušenost říká, že u podobných projektů je každá další abstrakce spíše na škodu.
Každý si ve finále píše nějakou knihovnu funkcí, viz post Petra M výše o vlastním přepínání vláken. Ten "OS" je také jen sada funkcí a maker od člověka, který práci s tímhle MCU rozumí nesrovnatelně víc, než já, a ten OS dělá přes 10 let.
Potřebuji zpracovávat paralelně běžící tasky se stejnou i různou prioritou (tj. i načasované přehazování) a nemíním to přepínání psát sám, když mohu využít něco hotového odladěného s ochotným supportem (vyzkoušeno). Do 120kB fleš se vejde už docela složitý program.
Rád se pak podělím o zkušenosti. Zdrojáky budou stejně na githubu.
Asi takhle... Mám program s FreeRTOSem na STM32F4, kde běží 15 vláken. Dělá grafiku na TFT 800x480 s touchscreenem, komunikaci po dvou UARTech, měření pěti analogových kanálů, ovládání výstupů, správa několika rozhraní, FLASH disk v externí NOR FLASH a obsluha NFC. Sakum prdum včetně fontů, tabulek zvuků přes PWM generovaných a podobných blblin 130kB FLASH + 40kB RAM.
A ten komfort, když všechno řeším nezávisle... Třeba audio. Přijde zpráva, vlákno se probere, vezme číslo zvuku, podívá se do tabulky, kde jsou data jako vektory významem (tón, délka), a jeden po druhým pouští do PWM a schrupne si. Mezi tím se děje něco jinýho, ale to nemusím při psaní audia řešit. Co mě zajímá je jenom fronta, do které spadne kód zvuku, jinak je to autonomní program a mimo initSound()m, playSound() a enum s předdefinovanýma zvukama o něm nevím. Je to těžce návykový.
Přesně takhle to budu psát. Vlákna komunikující frontami, každé si řeší to svoje. Tu část prototypu v pythonu, která bude běžet na MCU, dělám s ohledem na možnosti a principy programování toho MCU. Zatím skoro všechno mám v chibios k dispozici, samozřejmě kromě správy paměti.
"Vícevláknový systém" je široký pojem zahrnující celou řadu nástrojů s velmi různou náročností na implementaci. Podle subjektivních požadavků na tu množinu se dá trvrdit cokoliv od "to je triviální" přes "to bude hodně zatěžovat" až po "to ten procesor neumí".
Pár z těch nástrojů:
- context switch - snadné - vyvolá se přerušení, uloží se registry jednoho procesu, načtou se registry jiného procesu a obnoví se běh
- ochrana paměťového prostoru - nemožné bez MMU - aby jeden proces nemohl číst a přepisovat paměť jiného procesu
- kooperativní multitasking - triviální - svůj kód prošpikujete příkazem "yield" (SWI), což je volání systémové funkce, kterým říkáte "jestli chce běžet někdo jiný, tak teď je vhodná doba"
- preemptivní multitasking - může být snadné i těžké - pokud obsluha periferií běží v přerušení, pak je tohle stejně snadné jak kooperativní multitasking. Pokud obsluha běží ve vlákně, tak může být problém, když procesor vlákno přeruší ve špatnou chvíli. To se pak řeší pomocí zákazů přerušení apod., což jsme ale krok zpět ke kooperativnímu multitaskingu
- virtuální paměť - bez MMU velmi těžké - umět část paměti vlákna odsypat někam jinam a uvolnit tak paměť pro jiné vlákno
- dynamická alokace paměti - snadné, ale - to ale je v ochraně paměťového prostoru - bez MMU je to celé o dobrovolnosti a bezchybnosti
- překrývání paměti - bez MMU velmi těžké - to aby vlákna mohla běžet ve stejné oblasti. Paměť se musí při context switchi celá překopírovat. S MMU triviální. Pokud píšu programy relokovatelné, tak to řešit nemusím. Stejně tak když každé vlákno bude jinde
- mezivláknová komunikace - snadné až těžké - podle toho, zda to chci jet v režii vláken (dohodnou se na semaforech) nebo v režii OS
- dynamické zavádění programů - netriviální plus problémy uvedené výše - natáhnout kus kódu do paměti a ten spustit jako další vlákno
Takže pokud je požadavek: mám jen jeden program, který obsahuje více vláken, která se mezi sebou dobrovolně přepínají (kooperativní multitasking), přístup ke sdíleným datům si řeší semafory ve vlastní režii a nepotřebují vyšší funkce OS jako je dynamická alokace paměti, pak se to dá napsat snadno i na STM32 a na výkonu se to nepozná. Pokud ale někdo čeká, že bude mít na SD kartě kusy kódu, které bude zavádět do paměti a spouštět, tak to už ho čeká pořádný kus práce. STM32 má Supervisor mód, takže toho dost udělat půjde. Ale nemá MMU. Na to už by byl potřeba Cortex-A.
Nejsem žádný specialista, ale preemptivní přepínání v tom OS řeší přerušení od hw časovače, v němž běží kód scheduleru. Má to time slicy, příp. umí tickless režim - ví, kdy se má co probudit, tak si časovač nastaví na daný čas. Takže není potřeba, aby se vlákno vzdalo kooperativně, scheduler si je sám přehodí, pokud zrovna neběží v nějaké kritické sekci.
MMU to samozřejmě nemá, tudíž nemůže alokovat paměť, je to celé dělané staticky.
ad ochrana - Cortexy M0+ a M3 a výš sice nemají MMU, ale mají MPU. Je ovšem otázka, je-li to k něčemu v praxi dobré. Nejsme na desktopech, aby si BFU instalovali a spouštěli neznámé programy, obvykle přesně víme, co v daném zařízení máme a proč a selže-li daný kus softwaru, znamená to znefunkčnění celého zařízení (jinak by tam daný kus kódu vůbec nemusel být). Nějaký význam bych viděl především při ladění, ovšem psát MPU-aware kód jen kvůli tomu, aby mi při ladění vyskočil MemFault, mi připadá jako zbytečné. V embedded světě je důležité dodržovat určitou sebekázeň a zásady, které by na desktopech mohly snad působit poněkud rigidně a zbytečně, ovšem tady nepředstavují zásadní problémy, ale zásadním způsobem právě tyto možné chybové stavy omezují. Mimochodem velké procento potenciálních MemFaultů zachytí i HardFault, který je na všech Cortexech, protože chybná hodnota pointer mnohdy namíří do neobsazené paměti nebo přepíše data v zásobníku, což následně HardFault způsobí. Chce to určitý cvik, zkušenosti a intuici při ladění. Začátečníci z toho mohou mít noční můry, ale po nějakém čase už člověk pozná, odkud vždycky vítr vane.
ad kooperativní - na první pohled triviální, na druhý méně. Yield je jen vyvolání plánovače, které v preemptivním případě neřeším, zatímco v kooperativním to řešit musím já místo nějaké obsluhy časovače. I v kooperativním multitaskingu musím mít plánovač, který naplánuje další proces k běhu, i zde musím mít rutinu pro přepnutí kontextu. Z hlediska jádra OS je rozdíl minimální, z hlediska aplikačního vývojáře je kooperativní multitasking náročnější. Sice se zjednoduší kritické sekce, ale za to se zkomplikuje vše ostatní, protože aby to celé fungovalo, musí kooperovat každý z procesů, nejen ty, co vzájemně sdílejí nějaké prostředky, jako tomu je u preemptivního multitaskingu. Kooperativní multitasking má IMHO smysl jedině na platformách, kde chybí systém přerušení nebo časovač, což jsou v dnešní době prakticky jen případy různých zákaznických chipů obsahujících procesorová jádra v nějaké minimalističtější variantě.
ad virtuální paměť - na mikrořadičích mi tato věc připadá poněkud perverzní, nicméně i to je řešitelné čistě programovými prostředky pomocí různých memory poolů apod., pokud by bez toho někdo nemohl žít
ad dynamická alokace - na MCU je vše otázkou dobrovolnosti a bezchybnost. Je to opravdu úplně jiný svět oproti desktopům a přenášení některých (zlo)zvyků z desktopového prostředí na mikrořadiče je cesta do pekel. Na MCU se obvykle toto nedělá a profi standardy dynamickou alokaci paměti mnohdy explicitně zakazují, což přispívá k předvídatelnosti a testovatelnosti daného řešení. Nicméně i malloc() a free() jsou otázkou dobrovolnosti a bezchybnosti, pokud chceme, aby náš desktopový program fungoval. U mikrořadičů je třeba vnímat to celé en bloc jako jeden program - mikrořadičem něco řídím a nefunguje-li nějaká část jeho programového vybavení, tak to zařízení zkrátka nebude fungovat správně - a tedy stručněji: nebude fungovat. Správně navržený a fungující program ale zase nepotřebuje ochrany paměti.
ad překrývání - ve skutečnosti to je ještě těžší. Nestačí překopírovat, je třeba mít i nějaký GOT. Dost záleží na překladači, zda-li vůbec umí generovat takový kód. Obecně platí co v minulých bodech - jsme na MCU, ne na desktopech, ergo má-li někdo nutkání toto potřebovat, asi přemýšlí nějak špatně. Jinak sdílení kódu více vlákny je samozřejmě triviální - stačí nemít žádná statická data.
ad komunikace - ta je snadná tak i tak
ad dynamické zavádění - viz předchozí body, nicméně někdy se i v tomto světě dynamické zavádění vyžaduje, pak záleží spíš na bezpečnostních standardech, bude-li to náročné nebo jednoduché (viz např. instalace appletů na smart karty dle ETSI).
MMU v embedded světě je poměrně zbytečná věc; lidé z desktopů všeobecně mají velmi zkreslené představy o tom, jak se dá co řešit, protože na desktopech se dané věci musejí řešit velmi univerzálně a tedy komplikovaně, aby byly dostatečně robustní, a pro aplikační vývojáře jsou to obvykle jen jakési černé skříňky, nebo spíše obrovské černé skříně. Proto si pod stejnými pojmy představí tyto černé skříně i v embedded světě, nechápou, jak je možné navrhnout řešení, které dostane certifikát bezpečnosti, který by Linux nikdy nedostal, přestože daná platforma nemá žádnou MMU apod. Je to dané vysokou specifičností a úzkým záběrem, který dané řešení musí obsáhnout. Díky tomu v embedded řešení spousta kódu vůbec nemusí být přítomna a kód, který neexistuje, nemůže být zabuggovaný, ani ho nelze exploitovat. V tom to celé vězí. V jednoduchosti je krása.
I v kooperativním multitaskingu musím mít plánovač, který naplánuje další proces k běhu, i zde musím mít rutinu pro přepnutí kontextu. Z hlediska jádra OS je rozdíl minimální, z hlediska aplikačního vývojáře je kooperativní multitasking náročnější.
Ten plánovač v kooperativním multitaskingu je mnohem jednodušší, protože nemusí řešit čekání na podmíněné proměnné či čekající mutexy.
Sice se zjednoduší kritické sekce, ale za to se zkomplikuje vše ostatní, protože aby to celé fungovalo, musí kooperovat každý z procesů, nejen ty, co vzájemně sdílejí nějaké prostředky, jako tomu je u preemptivního multitaskingu.
Jenže správné zamykání je hodně komplikované a náchylné na vážné a velmi obtížně laditelné chyby (třeba různé Heisenbugy), zatímco kooperaci lze velmi snadno vyřešit třeba smyčkou zpráv, a protože je synchronní, nemění debugging její chování. Navíc bez MMU stejně musí procesy kooperovat a, jak sám uvádíte, „mikrořadičem něco řídím a nefunguje-li nějaká část jeho programového vybavení, tak to zařízení zkrátka nebude fungovat správně“.
Kooperativní multitasking má IMHO smysl jedině na platformách, kde chybí systém přerušení nebo časovač, což jsou v dnešní době prakticky jen případy různých zákaznických chipů obsahujících procesorová jádra v nějaké minimalističtější variantě.
Přerušení a časovač má i Arduino, přesto se tam preemptivní multitasking nepoužívá.
> Ten plánovač v kooperativním multitaskingu je mnohem jednodušší, protože nemusí řešit čekání na podmíněné proměnné či čekající mutexy.
Plánovač se zabývá jen procesy připravenými k běhu a procesem běžícím. Ostatní pro něj neexistují, takže ve skutečnosti vůbec žádný rozdíl mezi kooperativním a preemptivním plánovačem není.
> Jenže správné zamykání je hodně komplikované a náchylné na vážné a velmi obtížně laditelné chyby
Je-li třeba v prostředí kooperativního multitaskingu dodržovat nějaké časování, např. kvůli síťové komunikaci nebo periodickému měření, stejně se kritickým sekcím obvykle nelze vyhnout - yield se prostě zavolat musí, nikoli protože už to s klidným srdcem lze, ale protože čas si hlídám sám místo chybějícího přerušení časovače. Dalším případem je reentrance - typicky u různých event handlerů, operujících nad statickými objekty.
> Přerušení a časovač má i Arduino, přesto se tam preemptivní multitasking nepoužívá.
Arduino je především amatérský hobbybastl, dětská hračka. Stejně tak odrážedlo pro děti taky nemá převodovku.
No kdysi jsem si zkoušel napsat OS core na MSP430. Vyšlo to na cca 350B FLASH + 20+(počet vláken * 4)B RAM + kousek zásobníku + 1x timer. Fakt primtivní. I bez MPU a podobných kravin.
Překrývání paměti nepotřebuješ. Co chceš překrývat ve třeba 64kB RAM? Dynalická alokace se taky omezuje na nějaký pool (třeba widgety a texty na displeji, který s novou obrazovkou mažeš). A na vytvoření dál statických struktur jako thread, fronta, mutex,... při initu a ani to není povinný.
Předávání dat je taky easy. Pokud je to jednosměrně z jednoho vlákna, tak stačí volatile proměnná. Když se má vlákno probudit a nebloudit ve smyčce, tak event (jako signalizace pro scheduler, že má zařadit vlákno na seznam) nebo semafor nebo mailbox. Když má vlákno nižší prioritu a může tam být těch dat víc, tak fronta.
Pokud něco sdílím (třeba společnou EEPROM s konfigurací několika modulů a současný přístup by vedl k problémům, zamknu si to mutexem<a>.
No a pokud mám vláknu říct, že má pokračovat (třeba se čeká na přerušení od DMA), zařídí to semafor.
No a víc vlastně většinou ani člověk nevyužije...
Tohle je zajímavé téma. STM knihovny údajně byly napsány tak obecně, aby umožňovaly snadnou migraci na jinou rodinu MCU. Rutiny pro zápis do flash obsahují, ale abyste je mohli použít, je třeba o tom něco vědět. Například že u některých typů tyto rutiny mohou klidně běžet v jiné oblasti flash a nic dramatického se neděje (maximálně se zakoktá nějaká periferie), u jiných musí běžet v ram, jinak to zhavaruje. Možná to někde v komentářích je, ale hledat to nebudu, ty knihovny taky nepoužívám. Periferie těchto obvodů jsou sice složité, ale pro běžné fungování stačí zapsat do registrů několik málo bitů a je nastaveno. Zkoumat všechny možnosti je nad síly běžného člověka, takže to studuje až když to opravdu potřebuje.
Používám oboje, podle účelu. Když zapisuji třeba konfigurační bajty, které se až tak často nemění, tak klidně využiji flash. Ale pokud mám nějaké měření třeba (měřič strojového času, který musí vždy zapisovat poslední čas, kdy ještě byl naživu), tak raději použiji sériovou eeprom, které stojí pár kaček a připojí se dvěma dráty. Stejně tak použiji sériovou eeprom pro větší množství zaznamenávaných dat (datalogger). A pro obrovské objemy dat (třeba snímání fotek přes ccd kameru) je asi nejlepší použít levnou SD kartu s filesystémem.
Na aktuálním projektu (STM32F4xx) je zálohovaný RTC a k němu 4kB zálohované SRAMky. Stačí povolit v registrech, definovat sekci v ldscriptu a je hotovo... Mrkni do datasheetu.
FLASH má obrovskou nevýhodu v tom, že je třeba mazat třeba celý 16kB blok...
Btw, 24Cxx stojí tolik, co baterka a pokud nepotřebuješ RTC, vyjde to mnohem líp.