Dallas termometrų nuskaitymas+programavimo pamokėlė

Posted: 2018-04-06 in Darbeliai
Žymos:, , , , ,

Dallas DS18B20 ir kitų panašių termometrų nuskaitymas su mikrovaldikliu yra tikrai gan trivialus darbelis, o Arduino frameworkui pritaikytos OneWire ir DallasTemperature bibliotekos naudojamos visur kiaurai, kai kur perrašomos, kad tiktų (pvz. NodeMCU eLua firmwarėje) ir jų kokybės bei panaudojimo niekas nekvescionuoja. Na, išskyrus gal vieną kitą bambeklį gyką, tokį, kaip aš.

O man štai tos bibliotekos nebaisiai patinka. Naudoju pats jas, kai tingiu ilgiau krapštytis, kaip ir savo temperatūros stebykloj. Bet net ir joj išsikrapščiau dalį kodo lauk, nes negalėjau pasiekti to, ko norėjau.

Taigi šiam penktadieniui — labai ilgas ir sunkiai įveikiamas įrašas. Atleiskite man tie skaitytojai, kurie laukiate kokio nors mielo ir lyriško rašinėlio iš manęs — šiuo metu man geležėlės rūpi labiau 🙂

Bėda su tomis bibliotekomis yra ta, kad jos skirtos ne programuotojams, o mėgėjams krapštukams. Padarytos jos gerai, veikia viskas stabiliai, bet kiek jose yra bereikalingo kodo ir veiksmų… na, sunku net patikėt. Vien jau be reikalo kartojama daviklių paieška per visą magistralę mane erzina. Arba scratchpado nuskaitymas prieš keičiant konfigūraciją — aliarmo temperatūras ir skiriamąją gebą. Normalus programuotojas norėtų pats nusiskaityti scratchpadą, jį pakoreguoti ir siųstelėti atgal. Aišku, faina, kai biblioteka daro daug veiksmų už tave, bet dažnai dėl to bibliotekos išauga ir pasidaro sunkios. Ypač, kai nemaža dalis visokių vidinių kintamųjų giliai paslėpta, nors būtų puikiai galima išsisukti su tais pačiais, kuriuos naudojam tiesiog savo poreikiams.

Dabar jau nesvarbu, kokie ten vidiniai išgyvenimai mane kankino, bet kai susidomėjau OneWire valdymu per USART, nuo to karto vis gromuliavau mintį iš savo trumpo pabandymo sukurti ką nors įdomesnio. Tai ir sukūriau. Neteigiu, kad mano variantas yra kažkuo geresnis už populiariąsias bibliotekas, bet jį kurdamas daug ką išmokau pats, įsigilinau į STM32F103 (ir šiek tiek STM32F107) procesoriukų vidurius, prisiminiau AVR programavimą ir netgi serial portų krapštymą per Linux failų operacijas.

Neseniai skaitytojas Dovydas atsiliepė apie mano projektėlį, kaip „profo parašytą“. Na, tas konkretus projektėlis tikrai ne toks, kuriuo didžiuočiausi, nes tai buvo tiesiog kodo kratinys iš sukopijuotų pavyzdukų. O štai šis projektėlis — OneWire ir Dallas daviklių bibliotekos — toks, kuriuo aš jeigu ir labai nesididžiuoju, bet man negėda jį parodyti, kaip tikro programuotojo darbą. Panašų kodą ramiai komitinčiau į darbinį git’ą, jeigu tik toks poreikis būtų.

Iš dalies aš šią biblioteką ir norėjau parašyti taip, kad:

  • kode būtų laikomasi nuoseklios praktikos
  • kodas būtų išsluoksniuotas funkcionalumo lygiais
  • funkcinės dalys būtų nepriklausomos nuo architektūros
  • aparatūrinis kodas būtų visiškai abstrakčiai atskirtas

Išsluoksniavimą galima būtų aprašyti maždaug taip:

  1. Vartotojo programa: kad ir temperatūros stebykla, kažkoks „biznio logikos“ kodas
  2. Funkcionalumo biblioteka: šiuo atveju Dallas DS18B20 (ir pan.) temperatūrinių daviklių apdorojimas
  3. Periferijos biblioteka: šiuo atveju OneWire, kuri naudojama Dallas bibliotekos
  4. Draiverių/frameworko API: headeriai ar kažkas panašaus
  5. Frameworkas arba draiveriai: šiuo atveju konkrečios aparatūros draiveriai. Taip pat gali būti uždaro kodo

Be abejo, šis išsluoksniavimas yra tik apytikris. Sluoksnių gali būti ir daugiau, ir mažiau. Mano pirmajame bandyme OneWire iš karto kreipėsi į aparatūrą, taigi atliko ir draiverio vaidmenį. Skirtingos aparatūros (GPIO, USART) palaikymą aš iš pradžių bandžiau išspręsti per visokius #ifdef, bet tai yra tikrai nelabai elegantiška, o plečiantis palaikomai aparatūrai tų ifdefų pradeda rastis gyvas velnias, kodą tampa sunku ir skaityti, ir tobulinti.

Arduino bibliotekoms, be abejo, „draiverių“ sluoksnį atitinka pats Arduino frameworkas. Geri žmonės padaro, kad visokie digitalWrite ir pan. veiktų su visokiais mikrovaldikliais ir devboardais. Tačiau viena „bėda“ su OneWire biblioteka yra ta, kad nepriklausomai nuo mikrovaldiklio galimybių jos kodas yra išimtinai bitbanginis. T.y. imamas GPIO elektrodas ir tampomas su mikrosekundžių uždelsimu. Ir čia kaltas ne frameworkas, o OneWire kodas, kuriame yra priimtinas būtent tik toks darbo su magistrale metodas. Viskas su tuo metodu gerai, visokiems mažiems ir mėgėjiškiems projektams to pakanka. Bėdos prasideda, kai prireikia kokios nors RTOS ar normalaus Linux, o ten ne visada galima tiksliai sukontroliuoti uždelsimus, ypač, jei susivelia daugiau „lygiagrečių“ užduočių ir dispečeris pradeda jas perjunginėti. Tas dažnai matyti naudojant OneWire kernelio modulį ant Avietės: kas kažkiek laiko vietoj temperatūros nuskaitomas kažkoks bardakas — o tai įvyksta dėl to, kad normalioje OS net ir kernelio modulyje paprasto GPIO nesuvaldysi.

Todėl aš nusprendžiau sukurti tokią biblioteką, kurioje „draiveriai“ būtų maksimaliai arti aparatūros (kai tai įmanoma), o OneWire biblioteka jais naudotųsi agnostiškai, neturėdama informacijos apie gilesnę implementaciją.

Susiduriame su nedidele (patyrusiam programuotojui) problema: skirtingai aparatūrai reikia skirtingų parametrų ją inicializuoti. Na, sakykim, tiek AVR, tiek ARM mikrovaldikliams paprastam GPIO įjungimui reikia žinoti GPIO portą ir konkretų GPIO numerėlį. Inicializuojant U(S)ART reikia žinoti, kokį konkrečiai U(S)ARTą naudosim (galima sunumeruoti). Sugalvosim dar kokią nors aparatūrą naudot, pvz. USB-USART keitiklį, reikia nurodyti kelią iki įrenginio Unix failų sistemoje. Windows ten reikės COM porto numerėlio. Na ir panašiai.

Taigi iškyla toks mažas „nesmagumas“, kad inicializuojant draiverį reikia lyg ir skirtingus parametrus paduoti priklausomai nuo aparatūros.

Be abejo, rašydami kodą bei naudodami bibliotekas mes žinosim ir planuosim, kokią aparatūrą naudosim. Bet jei norime parašyti visiškai agnostišką draiverio inicializavimo API, reikia sugalvoti, kaip tai padaryti.

Abstraktūs duomenų tipai

Į pagalbą mums gali ateiti abstraktūs duomenų tipai (abstract data type). T.y. tokie duomenų tipai, apie kurių „vidurius“ mes lyg ir „nežinome“. Aišku, kuriant savo ir naudojant atvirą kodą viską galim „sužinoti“, bet juk būna ir kitaip: nusiperki kokią nors jau sukompiliuotą biblioteką, prie kurios pridedamas API, bet kai kurie to API tipai ir struktūros būna paslėpti — gauni ten atgal kokį (void *) pointerį ir viskas.

(void *) pointeris yra vienas iš paprasčiausių būdų perduoti bet kokią ir netgi nežinomą duomenų krūvelę. Taigi būtų galima sugalvoti kad mūsų draiveris bus irgi koks nors (void *) pointeris ir mes jį inicializavę su duota API naudosime. Bet aš, kaip senas koderis, tokių dalykų nemėgstu. Visada žymiai smagiau matyti kokį nors pavadinimą, o ne (void *).

C kalboje yra toks navarotas: galime užsiduoti struktūros pavadinimą ir apsirašyti duomenų tipą kaip pointerį į ją. O pačios struktūros vidurius paslėpti implementacijoje. Štai ir gimsta abstrakti „draiverio“ struktūra beigi jos inicializavimo funkcija:

typedef struct one_wire_driver * ow_driver_ptr;

int init_driver(ow_driver_ptr*, int);

Pirmasis draiverio inicializavimo parametras yra „nežinoma“, bet gražiai užvadinta struktūra, o antrasis — skaičiukas, nurodantis, ką ten konkrečiai inicializuoti.

one_wire_driver struktūra bus aprašoma konkretaus „draiverio“ kode. Ji bus vienokia STM32F103 mikrovaldiklio GPIO, kitokia USART, dar labiau kitokia AVR mikrovaldikliui ar ESP8266/ESP32 valdikliams. O abstraktesnė OneWire biblioteka naudos tik ow_driver_ptr pointerį be konkrečių „žinių“, kas slypi jo viduje.

Va kitas linksmesnis uždavinys — kaip per vieną int skaičiuką nurodyt, ką konkrečiai mes inicializuojam. Na, bet čia yra tiesiog skaičiukų žaismas. Sakykim, STM32F103 atveju yra konkretūs portai GPIOA, GPIOB, GPIOC ir t.t. Kiekviename iš jų — po šešiolika elektrodų. Taigi galima parinkti skaičiukus taip, kad GPIOB penktam elektrodui draiverį galėtume inicializuoti su kokia nors konstanta:

ow_driver_ptr driver;

status = init_driver(&driver, E_GPIOB+5);

Visiškai analogiškai ir AVR mikrovaldikliui:

status = init_driver(&driver, E_PORTC+3);

O jei inicializuosim kokį nors USART? Tai dar paprasčiau apsirašius konstantas:

status = init_driver(&driver, E_USART2);

Taigi kažkaip taip ir susidėliojo. ow_driver.h faile bendrinis draiverio API, o prie kiekvieno konkrečios aparatūros draiverio — jo headeris su va tokiomis konstantomis ir papildomais pareguliavimais.

Kadangi draiveris abstraktus, tai init_driver faktiškai turi jam išskirti atmintį. Pati aprašyta struktūra yra tik pointeris (na, rodyklė lietuviškai, jei ką). Šioje vietoje su mikrovaldikliais yra niuansėlis: geriau vengti ten visokių malloc ir new. Galima, net ir AVR mikrovaldikliams yra sukurtos bibliotekos su dinaminiu atminties valdymu, bet principas yra toks pat: paimamas static atminties burbulas, iš kurio atmintis ir dalinama. Aš savo mikrovaldiklių draiverių bibliotekose pasielgiau lygiai taip pat. T.y. vidinėms konkrečioms struktūroms sukūriau kelių elementų masyvą ir kviečiant init_driver atmintis paimama iš jo. Toks štai paprastas, bet visai veikiantis atminties valdymas. Kompiuteriuose su tikru atminties valdymu galima drąsiai naudoti ir malloc.

Draiverio API

Pagalvokime, ko reikia iš OneWire draiverio? Magistralės specifikacijoje aprašyta, kaip nuskaityti ir įrašyti bitus bei baitus, taip pat visokių operacijų pradžios impulsas — reset pulse. Tai taip vadinamas fizinis/elektrinis lygis. Bent jau man taip atrodo. Duomenų lygis yra tas aukštesnis 🙂

OneWire bibliotekai reikia sugebėti pasiųsti impulsą taip pat įrašyti ir nuskaityti po baitą. Jei naudosime daviklių paieškos algoritmą — reikės ir pavienių bitų nuskaitymo.

Tad iš draiverio API reikia tokių operacijų:

  • Draiverio inicializavimo, nurodant, kokia aparatinė dalis bus naudojama
  • Draiverio atjungimo, jei reikia. Embedded sistemoms mano galva nereikalingas dalykas, bet Linux OS reikia padaryti tvarkingai
  • Pasiųsti reset pulse. Kodėl tai draiverio reikalas? Ogi todėl, kad šitas impulsas su savo charakteristikom gali būti labai skirtingai įgyvendintas priklausomai nuo aparatūros
  • Įrašyti ir nuskaityti bitą
  • Įrašyti ir nuskaityti baitą. Vėlgi, baitas — aštuoni bitai? Tačiau aparatūriniam lygmeny baito įrašymas gali būti ne tas pats, kas nuoseklios aštuonių bitų įrašymo operacijos!

Draiverio headeryje dar yra toks difainas:

#ifndef OW_YIELD
#define OW_YIELD
#endif

Jei naudojate RTOS ar tą patį ESP8266, galima nurodyti operaciją, kuri atlaisvintų procesorių, kol draiveris pats kažko lūkuriuoja: rašymo/skaitymo pabaigos, baitų masyvo subėgimo į magistralę ar pan. Ką konkrečiu atveju difaininti, turite sugalvoti patys. Sakykim, FreeRTOS atveju tai gali būti taip:

#define OW_YIELD taskYIELD()

Be abejo, tai galima nurodyti ir per Makefile flagus arba savo IDE prisidėti.

ESP8266 atveju tai gali būti:

#define OW_YIELD yield()

Svarbu šio dalykėlio nepamiršti ir jį panaudoti, kai reikia.

STM32F10x GPIO draiveris

Šis draiveris gali būti naudojamas su bet kokiu STM32F10x šeimos procesoriuku. Išbandytas su STM32F103C8T6 — šitie yra mano esminiai mikrovaldikliai visokiems niekučiams. Veikia su GPIOA ir GPIOB — kitų portų mažuose mikrovaldikliuose nėra. Ant GPIOC kabo kvarcai ir yra tik keturios kojos apskritai.

Šitam draiveriui reikalingas mikrosekundžių uždelsimas — mano STM32-ARM-Libs šiukšlynėly yra biblioteka, naudojanti procesoriaus ciklų registrus. Ji nėra tobula su mažais intervalais (iki 5 µs), bet su didesniais veikia pakankamai tiksliai.

Vidinė draiverio struktūra yra štai tokia:

struct one_wire_driver {
	GPIO_TypeDef *gpio;
	volatile uint32_t *cr;
	uint32_t pin;
	uint32_t clear_mask;
	uint32_t in_mask;
	uint32_t out_mask;
};

Iš eilės apie kiekvieną elementą:

  • Pointeris (rodyklė) į konkretų GPIO portą (GPIOA, GPIOB ir kitus GPIOx).
  • Pointeris į konkretų konfigūravimo registrą — CRL arba CRH. Jis nustatomas inicializavimo metu, kad vėliau pagal elektrodą nereikėtų spėliot (kad veiktų viskas greičiau, be abejo).
  • Elektrodo mask’as. Ne numeriukas, o būtent bitmaskas. Paskaičiuojamas vėlgi incializavimo metu, kad įrašymo/nuskaitymo operacijos būtų atominės.
  • Elektrodo režimo reset bitmaskas. Na, kai keičiami režimai tarp skaitymo/rašymo, prieš tuos keitimus tiesiog išvalomi atitinkami konfigūracijos registro bitai.
  • Elektrodo įvesties režimo (floating) nustatymo bitmaskas
  • Elektrodo išvesties (open drain) nustatymo bitmaskas

Įvairiai „tampant“ elektrodą štai visais šiais elementais ir žongliruojama. Kodą pasižiūrėsite patys.

Tiek šitą, tiek kitus STM32F10x procesoriukams skirtą kodą (kuris bus pernaudojamas) stengiuosi rašyti naudodamas tik CMSIS bibliotekas, t.y. kuo mažiau „svorio“. Likučių iš Standard Peripherals, be abejo, ten kažkiek mėtosi, visko neišvaliau, nors stengiausi.

Rašydamas šitą draiverį buvau pats susipainiojęs tarp elektrodų numerių bei juos atitinkančių bitmaskų. Tad po geros valandos krapštymosi nusispjoviau ir pasijungiau osciloskopą. O jis, bjaurybė, parodė, kad ant OneWire duomenų linijos ramiai sau kabo 3,3 V ir niekas nevyksta. Tada paprasčiausiai užsukau ciklą su reset pulse ir tol tvarkiau kodą, kol viską atsirinkau ir ekrane pamačiau to impulso vaizdą:

OneWire magistralės reset pulse osciloskopo ekrane | Darau, blė

Čia gražiai matosi, kaip mikrovaldiklis užduota 480 µs trumpinimą į „žemę“, o termometriukas atsako su trumpesniu impulsu.

Va taip atrodo reset pulse ir komandos siuntimas:

OneWire magistralės reset pulse ir duomenų siuntimas osciloskopo ekrane | Darau, blė

Draiveris veikia, galima dalintis 😀

Pastabėlė: šitame draiveryje tarp visokių impulsų siuntimo nėra pertraukimų išjungimo. ARM procesoriuose globalus pertraukimų išjungimas kiek kitoks, nei AVR, nes jie valdomi individualiai. Aš rimtai tuo nesidomėjau, bet kada nors užmesiu akį. Todėl draiveris gali veikti nekorektiškai, jei bus pertraukinėjamas.

Pavyzdinį kodą įdėjau, tačiau nėra Makefile. Aš ARM kodiju pilnai per Eclipse IDE, tik su EABI toolchainu atskirai, tingėjau pilnai veikiančius Makefile’us rašyt. Svarbu, kad būtų CMSIS ir Standard Peripherals priklausomybėse (nors pastarojo stengiuosi maksimaliai atsikratyti, bet likučių vis tiek yra).

STM32F10x USART draiveris

Apie OneWire ir USART aš jau rašiau. Iš to seno bandymo gimė ir šis darbas. Visą su USART susijusią logiką iškėliau į atskirą draiverį. Šis draiveris yra vienas iš geresnių norint naudoti OneWire be papildomos aparatūros. Tiek reset pulse, tiek kiekvieno bito skaitymas ir rašymas yra pilnai aparatūrinis, nepriklauso nuo procesoriaus taktų ir užimtumo, todėl jo labai aukštas patikimumas naudojant su visokiomis RTOS ar gausybe pertraukimų.

Išbandytas su STM32F103C8T6 ir STM32F107VCT6 mikrovaldikliais.

Draiverio struktūra štai tokia:

struct one_wire_driver {
	USART_TypeDef* USARTx;
	uint16_t brr_9600;
	uint16_t brr_115200;
#ifdef OW_MODE_USART_DMA
	DMA_TypeDef *dma;
	DMA_Channel_TypeDef *dma_rx;
	DMA_Channel_TypeDef *dma_tx;
	uint32_t dma_rx_flag;
	uint32_t dma_tx_flag;
	uint32_t dma_ifcr_clear;
#endif
};

Štai šitame draiveryje aš pasilikau vieną esminį ifdefą — ar naudoti DMA, ar ne. Su DMA baitų permėtymu aš gerokai prisižaidžiau, atradau keletą niuansų, bet iki galo nedamušiau. Baitų rašymas į OneWire aparatūrą pavyksta jau puikiai, o va nuskaitymas — niekaip. Na, bet apie viską iš eilės.

Struktūra:

  • Konkretus USART įrenginys: USART1, USART2, USART3, Connectivity Line taip pat UART4 ir UART5.
  • Dvi reikšmės BRR registrui nustatyti USART greitį bodais. Pritaikyta 72 MHz taktiniam dažniui — naudojant kitą dažnį reiktų pasikoreguoti. Kadangi USART1 yra ant greitesnio laikrodžio, tai jo BRR reikšmės (dalikliai) skiriasi. Todėl inicializuojant draiverį pagal USART įrenginį reikia parinkti ir tinkamus bodų greičius.

Jei naudojam DMA:

  • Konkretus DMA įrenginys. USART1, 2 ir 3 tai yra DMA1, kitų dar nebandžiau.
  • DMA priėmimo kanalas (RX nuskaitymas)
  • DMA išsiuntimo kanalas (TX įrašymas)
  • Nuskaitymo operacijos užbaigimo patikrinimo bitmaskas
  • Įrašymo operacijos užbaigimo patikrinimo bitmaskas
  • Visų DMA flagų išvalymas ir pasiruošimas naujai operacijai

Daugiau apie USART veikimo principą galite paskaityti mano senesniame rašinėlyje.

USART inicializavimui padariau va tokį enumą:

enum {
	E_USART1 = 0
	,E_USART2
	,E_USART3
#if defined (STM32F10X_HD) || defined  (STM32F10X_CL) || defined  (STM32F10X_XL)
	,E_UART4
	,E_UART5
#endif
};

Čia tie if defined skirti nustatyti, ar mūsų procesoriukas turi UART4 ir 5, t.y. priklauso Connectivity Line. Šitie berods yra STM32F105 ir STM32F107. Kad if defined negriautų kodo, panaudojau mano nemėgstamą, bet visuotinai pripažintą blogybę kai kuriems atvejams — kablelius prieš konstantų pavadinimus. Tikiuosi, kad aišku, kodėl taip 🙂

O dabar kiek apie DMA.

Pastabėlė: DMA nepalaikomas ant UART5, bent jau su STM32F10x procesoriais. UART5 apskritai toks įdomus, jo RX/TX yra skirtinguose portuose: GPIOC ir GPIOD.

Taigi pasidariau tokį štai setupą: vienas mikrovaldiklis rašo kažką į USART2, kitas iš ten nuskaito, buferizuoja ir vėliau išspausdina. Na, bandžiau tiesiog pradėti nuo pirmos užduoties: rašyti duomenis į USART su DMA pagalba. Vienas OneWire baitas tampa aštuoniais USART baitais, tad tuos baitus paruošus galima palikti dirbti DMA ir atlaisvinti procesorių.

Viskas suveikė iš karto. Na, DMA registrų valdymas tikrai nėra sudėtingas. Ok, jungiam termometrą ir bandom.

Paieška neveikia. OneWire išsiunčia adresų apklausimo komandą ir žiūriu, kas gaunasi. Ogi neranda nieko, vienas USART baitas kažkoks ne toks, kaip turėtų būti.

Galiausiai išsiaiškinau niuansą. Kai paduodu DMA 8 baitus išsiųst, viskas ok. Išsiunčia. Ar operacija užsibaigė, aš tikrinu nuskaitydamas atitinkamą DMA ISR (būsenos) registro bituką. Bitukas pavirsta vienetuku, operacija baigėsi. Kai vien siunčiu baitus, su kitu valdikliu nusiskaito tvarkingai. Kai siunčiu baitus į OneWire, nusisiunčia, bet „susigadina“.

Po DMA operacijos įdėjau debuginį išspausdinimą, kas ten grįžta. O kodas paėmė ir suveikė, rado prijungtus daviklius…

Viskas aišku, po DMA operacijos kažkam dar pritrūksta laiko, kažkas fone vyksta. Debuginis išspausdinimas užima nemažai laiko, per tą laiką tas kažkas įvyksta. Na, bet nedėsi gi mikrosekundžių pauzės iš lempos po DMA operacijos, nekošerinis darbas gautųsi.

Pasirodė, kad reikia dar ir USART išsiuntimo registrą patikrinti, ar jis baigė dirbti. Gaunasi taip, kad DMA siunčia baitus vieną po kito į USART registrą, bet paskutinį baitą „numeta“ ir palieka USARTui. Pats DMA darbą kaip ir baigė, o USARTas tas kelias mikrosekundes dar dirba. Taigi reikia įsitikinti, kad ir DMA, ir USARTas taip pat baigė savo darbą.

Po šitų tikrinimų viskas susitvarkė, gavau veikiantį duomenų rašymą naudodamas DMA. Kūl.

Su skaitymu, deja, nepavyko. Kol kas neišsiaiškinau niuansų. Kažkas susiję su tuo, kad naudojamas tas pats USART registras tiek rašymui, tiek skaitymui. DMA dar pagal vieno ruselio išradimą gali netgi tą patį buferį panaudot. Bandžiau ir su tuo pačiu, ir su atskiru — nepavyko. Tad kol kas pilnas DMA palaikymas dar kabaliuoja. Bet rašymą į magistralę jau galima naudoti. Dar truputį paoptimizavus darbą su termometrais gryno USART lieka visai nedaug.

Tai tiek su šituo draiveriu. Viskas veikia, galime džiaugtis. DMA veikia pusiau, bet tokį variantą su RTOS irgi galima jau naudoti.

Tiesa, gal kas iš skaitytojų norėtų pasigilinti ir damušti, kad DMA veiktų ir skaitymui? 🙂

Setupas su STM32F107 plokšte iš AliExpress UART4 ir UART5 testavimui:

Dallas DS18B20 nuskaitymas su STM32F107 per UART5 | Darau, blė

Pridėjau ir šio reikalo pavyzdinį kodą, bet vėlgi be Makefile, patys galit įsidėt į kokį paruoštukinį projektą ir naudot.

AVR GPIO draiveris

Praktiškai tas pats, kas STM32, tik gerokai paprasčiau. AVR procesoriukai yra labai mieli ir paprasti palyginus, bet, be abejo, jų ir galimybės kur kas ribotesnės. Klausimas tik, ar tai kaip nors pasijunta 🙂

Struktūra:

struct one_wire_driver {
	volatile uint8_t* port;
	uint8_t pin;
};

Viskas, daugiau nieko nereikia, užtenka porto ir konkretaus elektrodo bitmasko. Aš kažkada rašiau ganėtinai didelį projektą (turbūt patį didžiausią mikrovaldiklių darbą neskaitant savo išmanaus šviesyno) ir jame iškapsčiau tokį dalyką, kad turint konkretaus porto pointerį ant AVR procesoriaus galima lengvai „sužinoti“ ir DDR bei PIN registrų pointerius. Pora va tokių makrosų:

#define DDR(x) (*(&x - 1))
#if defined(__AVR_ATmega64__) || defined(__AVR_ATmega128__)
    /* on ATmega64/128 PINF is on port 0x00 and not 0x60 */
    #define PIN(x) ( &PORTF==&(x) ? _SFR_IO8(0x00) : (*(&x - 2)) )
#else
	#define PIN(x) (*(&x - 2))
#endif

Operacijos čia nesudėtingos, vos po cikliuką, tai viskas yzy. Nežinau, iš kur šitą reikalą nusriegiau, pagūglinimas turbūt greitai parodytų. Na, bet labai naudinga.

Štai kas dar mane kiek erzina populiariojoje OneWire bibliotekoje: ten bitų operacijose manipuliuojant GPIO yra išjungiami ir vėl įjungiami pertraukimai. Toks veiksmas yra labai nekorektiškas. Sakykim, kad koks jūzeris lūzeris sugalvos išjungti pertraukimus savo kode (gal jis durnas ir nuspręs on the fly rašyti kažką į EEPROM), o su išjungtais pertraukimais naudosis Dallas temperatūrų nuskaitymu. O giliau kode pertraukimai pakartotinai išjungiami, o po to vėl įjungiami. Jūzeris lūzeris apie tai nežinos, jam susipartalins EEPROMas ir jis verks po to. Argi taip gražu?

AVR procesoriukuose bukai naudoti cli ir sei operacijas — didelė nuodėmė (noInterrupts ir interrupts Arduino žodžiais). Teisingas būdas yra išsisaugoti SREG registro (kuriame konfigūruojami pertraukimai) reikšmę, tada išjungti pertraukimus, o po to SREG vėl atstatyti į tai, ką radome:

uint8_t sreg = SREG; // Išsisaugom pertraukimų registro būseną
cli(); // Nudobiam pertraukimus
// Kažką nuveikiam prikodiję
SREG = sreg; // Atstatom pertraukimų registro būseną korektiškai į tai, ką radom

Tai va taip darykit, vaikučiai, su AVR, o ne kitaip, blė.

Užveikė iš pirmo karto, vos tik pavyko sukompiliuoti (mistaipų visada palieku, toks jau tas gyvenimas):

AVR mikrovaldyklyje veikianti OneWire ir Dallas GPIO biblioteka | Darau, blė

Setupas va toks, su senu kinišku Arduino Nano klonu:

Arduino Nano su Dallas DS18B20 temperatūros davikliu | Darau, blė

Kas čia dar… kadangi AVR draiverį krapštinėjau vieną iš paskutiniųjų, teko visas kitas agnostiškąsias kodo dalis pravalyt. Pavyzdžiui, beveik apsiverkiau AVR toolchaine neradęs <sys/cdefs.h> includo. Meh. Tai teko biškį padaryti mažiau gražius headerius, bet užtai labiau portable.

AVR kompiliavimas yra juokingai paprastas, todėl įdėjau pilnai veikiantį pavyzduką į githubą. Jei tik turite Linux ir AVR toolchainą, galit parsisiųst, susikompiliuot ir kišt į kokį Arduino Nano ar pan. Tik prieš tai pasileiskit ./configure, kad parsiųstų Peter Fleury UART bibliotekėles. Biblioteka užkonfigūruota ant PORTC 3 pino (D3 ant Arduino). Beje, ow_driver_avr_gpio.h faile aš sudėjau enumus, kad būtų galima nurodyti tiek AVR, tiek pritemptus Arduino pinukus:

typedef enum {
	E_PORTA = 0x00,
	E_PORTB = 0x08,
	E_PORTC = 0x10,
	E_PORTD = 0x18
} avr_port_t;

typedef enum {
	E_ARDUINO_PIN_0  = 0x18,
	E_ARDUINO_PIN_1  = 0x19,
	E_ARDUINO_PIN_2  = 0x1A,
	E_ARDUINO_PIN_3  = 0x1B,
	E_ARDUINO_PIN_4  = 0x1C,
	E_ARDUINO_PIN_5  = 0x1D,
	E_ARDUINO_PIN_6  = 0x1E,
	E_ARDUINO_PIN_7  = 0x1F,
	E_ARDUINO_PIN_8  = 0x08,
	E_ARDUINO_PIN_9  = 0x09,
	E_ARDUINO_PIN_10 = 0x0A,
	E_ARDUINO_PIN_11 = 0x0B,
	E_ARDUINO_PIN_12 = 0x0C,
	E_ARDUINO_PIN_13 = 0x0D,
	E_ARDUINO_PIN_A0 = 0x10,
	E_ARDUINO_PIN_A1 = 0x11,
	E_ARDUINO_PIN_A2 = 0x12,
	E_ARDUINO_PIN_A3 = 0x13,
	E_ARDUINO_PIN_A4 = 0x14,
	E_ARDUINO_PIN_A5 = 0x15,
} arduino_pin_map_t;

Ant Atmega8/168/328 PORTA, berods, nėra, bet vis tiek įdėjau. Šiaip aš pats turiu tik Atmega8 ir Atmega328 variantus, su kitokiais nesu susidūręs. Na, bet pakoreguoti ten kažką kitiems AVR procesoriukams esant poreikiui neturėtų būt problemų.

Linux UART „draiveris“

O štai čia linksmoji ir biškį crazy dalis. Kadangi ant STM32 įvaldžiau USARTą, tai nusprendžiau, kad nėra ko — reikia su FT232 mikroschema (UART-USB keitikliu) pabandyt nuskaityt termometrus tiesiog iš kokios nors Linux programėlės 🙂 Čia gi baisiai fun. Plius, man labai labai žiauriai nepatinka, kaip termometrus nuskaitinėja Avietė per GPIO su tuo iškrypusiu kernelio draiveriu. Be to, su USB keitikliu dar ir saugiau — jei ką, jis nukeps, o ne Avietės GPIO.

Setupas:

Dallas DS18B20 termometro nuskaitymas iš Linux kompiuterio su FT232 USB-UART keitikliu | Darau, blė

Kaip minėjau įraše apie USART naudojimą, jei nežinom TX elektrodo režimo arba negalime jo kontroliuoti, jungiam per Šotkio diodą:

Daviklis ds18b20 prijungtas prie stm32f103 per Šotkio diodą | Elektronika | Darau, blė

Kitaip ką nors užtrumpinsim ir sugadinsim. FT232 mikroschemos datašyto neskaičiau, nežinau, kiek ji konfigūruojama. Ir dar kitas klausimas, tai kiek ji konfigūruojama iš Linux draiverio — įtariu, kad nė kiek. Todėl apsidraudžiam.

Toliau viskas paprasta. UART keitiklį atsidarom kaip failą, pamėtom jo baud rate ir lygiai taip pat siuntinėjam bitukus, kaip ir iš mikrovaldiklio. Tik čia jau galima naudoti malloc ir free, nes rimta operacinė tas Linuxas. Draiverio struktūra niekingai paprasta:

struct one_wire_driver {
	int fd;
};

Reikia tik handlerio į atidarytą įrenginį.

UART keitikliai (ar pilnaverčiai RS232) kartais būna kaip /dev/ttyUSB0 (ir kiti skaičiukai) arba kaip /dev/ttyACM0. Pastarieji yra atseit modemai, nors pvz. originalusis Arduino Uno — nu koks jis modemas? O apsireiškia, kaip ACM divaisas…

Tad ir enumas atitinkamas:

enum {
	OW_TTY_USB = 0,
	OW_TTY_ACM = 0x80,
	OW_TTY_S = 0x100,
	OW_TTY_AMA = 0x180,
};

Kaip ir AVR atveju, įdėjau pilnai veikiantį pavyzduką su Makefile, galite tiesiog kompiliuotis ir bandytis. Kompiliuojasi bei veikia ir ant Avietės:

$ ./linux-dallas-uart
Init completed
Device found 1
Device found 2
Device found 3
Convert all
0x28FFB24C74160420  93 01 4B 46 7F FF 0C 10 F6  25.1875
0x28FF16E67316059A  96 01 4B 46 7F FF 0C 10 A0  25.3750
0x28FF43168016049B  8E 01 4B 46 7F FF 0C 10 DE  24.8750
Convert all
0x28FFB24C74160420  93 01 4B 46 7F FF 0C 10 F6  25.1875
0x28FF16E67316059A  96 01 4B 46 7F FF 0C 10 A0  25.3750
0x28FF43168016049B  8E 01 4B 46 7F FF 0C 10 DE  24.8750
^C
Signal 2

Tiesa, mano demonstracinė programulkė ėda procesorių. Įtariu, dėl ko, bet kol kas negalvoju taisyt. Gal kas iš skaitytojų norėtų paoptimizuoti? 🙂

OneWire biblioteka

O dabar apie pačią biblioteką. Jos veikimas daugiausiai nusriegtas nuo populiariosios OneWire, be abejo. Nėra ko išradinėti to, kas jau išrasta, reikia naudotis. Tiesa, CRC8 skaičiavimą pagal konstantų lentukę aš palikau. Kodas paskaičiavimui yra kur kas kompaktiškesnis ir universalesnis. Plius, net ir su AVR procesoriais gan greitas. Be to, aš savo bibliotekose niekur forsuotai to CRC8 nenaudoju — palieku tai padaryti tiems, kas bibliotekomis naudosis. Dauguma atvejų nėra reikalo tą CRC8 skaičiuoti, jį reikėtų naudoti tik kritiniais atvejais: kai kažkokie parametrai (šiuo atveju, be abejo, tai yra tik temperatūra) iššoka už normos ribų arba kai norima patikrinti, ar tikrai daviklių paieška pavyko korektiškai.

Paieškos algoritmas yra gan paprastas, Dallas (o dabar Maxim) labai gudriai sugalvojo, kaip tą daviklių „apklausą“ įgyvendint dvejetainio medžio principu. Ji tiek efektyvi, kad kai kurios bibliotekos tą apklausą naudoja kiekvienam temperatūros išgavimui.

Taigi OneWire bibliotekos funkcijos. Tai, kaip minėjau, yra periferijos biblioteka, tad ji turi atlikti darbą su magistrale:

  • Reset pulse siuntimas (per draiverį)
  • Baito rašymas ir skaitymas iš magistralės (jokiems davikliams nereikia pavienių bitų)
  • Daviklių paieška (čia tik pati OneWire turi turėti galimybę siųsti ir skaityti bitukus)
  • Bendrinės komandos select operacija (kai apklausiamas ar konfigūruojamas konkretus daviklis pagal jo serijinį numerį)
  • Bendrinės komandos skip operacija (kai komanda siunčiama visiems davikliams nepaisant jų adreso)

Kodas, manau, aiškesnis už aiškų, pasižiūrėsite.

Ko trūksta:

  • Daviklių aliarmo būsenoje paieškos. Mano galva, galima pačiam pagal scratchpadą atsifiltruoti nuskaitant. Nebus.

Dallas temperatūros daviklių biblioteka

Kol kas išbandyta su turimais DS18B20 davikliais, nors kodas yra numatytas ir kitiems. Senesniems DS18S20 reikia atkomentuoti DS_SUPPORT_18S20 difainą arba nurodyti jį per kompiliatorių/makefile. Ten yra vieno gudraus vaikino sugalvota fyčia, kaip 9 bitų DS1820 daviklius jų pačių pateikta papildoma informacija perkalkuliuoti į 12 bitų. Tad jei tokių daviklių turite, difainą užsidėkite.

Dar laukiu MAX31850 termoporos skaitytuvo, kuris irgi yra OneWire daviklis, turėtų su mano biblioteka veikti out of the box. Na, sulauksiu — patikrinsiu.

Na, ką gali mano Dallas temperatūros biblioteka… štai ką:

  • Liepti konvertuoti temperatūrą visiems magistralės davikliams
  • Liepti konvertuoti temperatūrą konkrečiam davikliui pagal serijinį numerį
  • Nuskaityti konkretaus daviklio pilną scratchpadą
  • Nuskaityti konkretaus daviklio scratchpado tik pirmus du baitus — temperatūrą. Na, jei kitų baitų nereikia, taupom ciklus
  • Iš scratchpado išlupti 12 bitų neapdorotą temperatūrą
  • Iš scratchpado paskaičiuoti temperatūrą Celsijaus laipsniais
  • Iš scratchpado paskaičiuoti temperatūrą Farenheito laipsniais (wtf, taip ir nežinau, kodėl tai įdėjau)
  • Iš scratchpado paskaičiuoti tik sveikąją Celsijaus dalį — labai greita operacija tiems atvejams, jei tokio tikslumo užtenka
  • Patikrinti pagal serijinį numerį, ar daviklis yra temperatūrinis įrenginys. Atskira funkcija, kad galėtumėte pasinaudoti, jei turite ir kitokių daviklių. Dažniausiai žmonės neturi ir nėra prasmės tą tikrinimą kišti į kiekvieną kitą funkciją.
  • Patikrinti daviklio skiriamąją gebą ir grąžinti ją suprantamais žmonėms skaičiukais: 9, 10, 11 ir 12.
  • Įrašyti konfigūraciją: per žemą ir per aukštą temperatūrą, skiriamąją gebą. Reikia patiems užpildyti atitinkamus scratchpado baitus su norimomis reikšmėmis — taip tikri programuotojai ir daro.

Maždaug taip. Skaitykite kode komentarus, peržiūrinėkite, išbandykite pavyzdžius ir pamatysite viską.

Kitkas

Visuose draiveriuose išskyrus variantą su DMA write_byte ir read_byte operacijos yra vienodos: rašo arba skaito nuosekliai po bituką. Realiai tą kodo dalį būtų galima iškelti į atskirą failą. Galbūt vėliau tą ir padarysiu. Bet šiuo atveju kodo kopijavimas nėra didelis blogis — visas draiverio kodas guli vienoj vietoj, patogu skaityti ir taisyti.

Žinomas trūkumas: su tokia struktūra vienu metu kode galima naudoti tik vieną draiverio tipą. T.y. neįmanoma naudoti USART ir GPIO tuo pačiu metu.

Tačiau. Programuotojai moka apeiti ir tokius dalykus, be to, gan nesunkiai. Tereikia pagrindinį projektą pasidaryti C++ ir suskirstyti draiverius į skirtingus neimspeisus (namespace). Ir galėsite naudoti kelis draiverius lygiagrečiai. Na, aišku, AVR draiverio STM32 valdiklyje panaudoti nepavyks, bet čia jau turbūt akivaizdu.

Galvojau ir ESP8266 draiverį sukrapštyt, bet patingėjau — man jo kol kas nelabai reikia. Be to, nusimato gan rimtas projektas su ESP32, tai va ten reikės — jam draiverį ir pasirašysiu.

Dar ant ARM ir AVR kompiliatorių labai užknisa, kad standartiškai neišeina spausdinti kablelinių skaičių, pvz. su kokiu “%.7f“. Darydamas naujus projektus nuolat tuos flažokus pamirštu, paskui tenka ilgai knistis, kol vėl atrandu. Reikalas tame, kad šitų skaičių spausdinimas paprastai smarkiai išpučia firmwarę: ARM atveju papildomi 8 kB. Na ir ganėtinai lėtai tas spausdinimas vyksta. Dvejetainius slankaus kablelio skaičiukus paversti į dešimtainius yra labai nejuokingas uždavinys, po šiai dienai naudojami 197x kažkurių metų algoritmai. Nors girdėjau, kad 2010-ais atsirado kažkoks Grisu, bet dar nesidomėjau. Užtai būtų įdomu pasidaryti riboto tikslumo kokį „greitą“ algoritmą.

Beje, pasižiūrėkite, kaip Arduino Serial.print(xx) spausdina float’us. Firmwarės tai gal ir sutaupoma (nors ten apskritai milžiniški blobai gaunasi), bet kiek ten procesoriaus graužimo bereikalingo… bet visgi labai gerai ir paprastai veikia, nereikia supernaglų algoritmų.

Tai kaip ir tiek. Reikalas githube, į sveikatą: https://github.com/darauble/DallasOneWire

Komentarai
  1. entdx parašė:

    Nežinau C kompiliatoriaus turintis standartą nepalaiko enumo su kableliu paskutiniam elemente ir jokių įsikalinėjimų su kableliu prieš enumą nereikia (https://wandbox.org/permlink/nbCsHK7KY7bRdOEj).

  2. mechadrake parašė:

    čia mus n00bus iš pasalų mokina skaityt vadovelius! As dar neperskaites Making Embeded Systems ir C++ pradmenu 😀
    Nu gal ir persakitysiu shita wall of text

  3. giedriuszzz parašė:

    supratau kad nieko nesuprantu, ple

Parašykite komentarą

Įveskite savo duomenis žemiau arba prisijunkite per socialinį tinklą:

WordPress.com Logo

Jūs komentuojate naudodamiesi savo WordPress.com paskyra. Atsijungti /  Pakeisti )

Google photo

Jūs komentuojate naudodamiesi savo Google paskyra. Atsijungti /  Pakeisti )

Twitter picture

Jūs komentuojate naudodamiesi savo Twitter paskyra. Atsijungti /  Pakeisti )

Facebook photo

Jūs komentuojate naudodamiesi savo Facebook paskyra. Atsijungti /  Pakeisti )

Connecting to %s

Brukalų kiekiui sumažinti šis tinklalapis naudoja Akismet. Sužinokite, kaip apdorojami Jūsų komentarų duomenys.