Kako hitro in enostavno razvijati? Kako testirati in dopolnjevati ugnezdeno programsko opremo? Kaj narediti, da bo zanesljivo delovalo tudi, ko gre kaj narobe? Kdaj začeti vsakodnevno uporabljati?
Bralcem in avtorju se opravičujemo, ker je to nadaljevanje izpadlo iz 300. številke zaradi obilice sponzorskih člankov.
Avtor: dr. Simon Vavpotič
2021-301-37
V preteklem nadaljevanju (SE299) smo odsluženi ESP8266-13 Wi-Fi modul zamenjali z novejšim in zmogljivejšim ESP32-WROOM, 5-voltni HC SR-04 modul za ultrazvočno merjenje razdalj bomo na različne načina povezali s 3,3-voltnim mikrokontrolerjem (npr. Raspberry Pi, ESP32, PIC32,..), ne da bi slednjega poškodovali….
Ta vsebina je samo za naročnike
Če hočemo zares narediti boljši izdelek, moramo povečati njegovo splošnost in prilagodljivost. Prav s tem in še vrsto drugimi problemi s področja uporabe in zanesljivosti strojnih rešitev, kot je natančno krmiljenje smernih krtačnih motorjev s PWM modulacijo, se bomo ukvarjali tokrat. A tokrat začnimo kar s PIC32, iz katerega lahko naredimo izvrsten RS232 programator/razhroščevalnik za Arduino razvojno osnovo. Njegovi bistveni prednosti sta podpora za kontrolne signale, kot sta ENABLE in BOOT MODE, in hitrejše programiranje ugnezdene programske opreme.
Natančno krmiljenje pogonov s krtačnimi elektromotorji
Čeprav za krmiljenje krtačnega motorja zadoščata že dve digitalni signalni liniji, prek katerih določamo smer in hitrost vrtenja, je potrebno za njegovo natančno upravljanje, podobno kot to počnemo s servomotorji, še precej programske logike. Prvo vprašanje je, kako doseči, da se bo motor pri enakem zaporedju digitalnih impulzov vsakokrat zavrtel enako hitro, ne glede na napolnjenost baterij. Če motor napajamo iz klasičnega napetostnega regulatorja, kot je 5-voltni 7805, imamo velike izgube, ko so baterije povsem napolnjene. Namesto regulacije napetosti lahko uporabimo tudi regulacijo moči z dinamično nastavljivo PWM modulacijo glede na trenutno napetost baterijskega vira, ki upada s praznjenjem baterij in ob tokovni obremenitvi.
Prvi korak do rešitve je natančno merjenje napajalne napetosti z A/D pretvornikom, katere vrednost moramo pretvorili v dolžino PWM impulza. K slednji moramo dodati še vhod iz sprotnih nastavitev hitrosti vrtenja. Kaj to pomeni? Ko so baterije povsem napolnjene (npr. 9 V), bomo s skrajšanjem dolžine PWM impulza posnemali delovanje motorja pri izpraznjenih baterijah (npr. pri 7,7 V). Tako se bo motor vrtel enako hitro pri obeh napetostih. Za izračun dolžine PWM impulza pri vmesnih vrednosti napajalne napetosti, med 7,7 V in 9 V potrebujemo ustrezno enačbo. Efektivna moč enosmernega motorja je izražena v ploščini U * I, pri čemer je U trenutna vrednost napajalne napetosti, I pa tok, ki teče skozi motor. Zdaj moramo upoštevati še čas, tako da dobimo porabljeno energijo, E = P * t. Želimo, da je porabljena energija enaka, ko so baterija izpraznjene E2 in ko so povsem napolnjene E1. Torej: E1 = E2, E1 = U1 * I1 * t1, E2 = U2 * I2 t2. Očitno je da moramo pri vsem upoštevati še notranjo upornost R rotorja motorja, saj se tok spreminja glede na napetost, notranja upornost pa je pri tem enaka. Torej: U1 = I1 * R, I1 = U1 / R, potem je I2 = U2 / R. Zdaj lahko izračunamo: E1 = U1 * U1/R x t1 in E2 = U2 * U2 / R * t2. Zanima nas, kako naj izberemo t2, če poznamo t1. Torej: U1 * U1 / R x t1 = U2 * U2 / R * t2. Potem je: t2 = U1* U1 / (U2 * U2) * t1. Iz tega sledi, da moramo pri določanju razmerja dolžine impulza pri različnih napajalnih napetostih upoštevati razmerje kvadratov napajalnih napetosti in ne le razmerja napajalnih napetosti, kot bi morda pomislili v prvem trenutku.
Zdaj lahko ugotovimo, da moramo pri izpraznjenih baterijah s 7,7 V elektromotor krmiliti tako, da je t1 = 1. Potem je: t2 = U1 * U1 / (U2 * U2) = 59.29 / (U2 * U2), če je napetost večja od 7,7 V. Če napetost pade pod 7,7 V, moramo zaščititi baterije, zato vse pogone samodejno ustavimo in dovolimo le delovanje telemetrije.
Pravkar smo izračunali enačbo za enakovredno upravljanje elektromotorja ne glede na njegovo napajalno napetost, zdaj pa moramo upoštevati še želeno hitrost vrtenja. Pri tem v enačbo dodamo še spremenljivko s, ki določa relativno dolžino intervala. Tako dobimo: t2 = 59,29 / (U2 * U2) * s, pri čemer s spremenljivko s določamo hitrost vrtenja motorja, ki je konstanta, ne glede na napolnjenost baterij.
Dodajmo še, da je pomembno tudi dovolj hitro merjenje napajalne napetosti, da lahko sproti učinkovito prilagajamo dolžino trajanja intervala, s katerim krmilimo elektromotor. K sreči je A/D pretvornik za to več kot dovolj hiter, saj je potrebno napetost izmeriti enkrat na vsako periodo PWM. Kljub temu je ugodno, če v tem času naredimo več meritev in izračunamo njihovo povprečno vrednost, saj je tako manj verjetno, da bi bilo krmiljenje elektromotorja neustrezno zaradi morebitne napake meritve.
Obenem je nemalokrat smiselno postopno prilagajanje na napajalno napetost, tako da ne pride do nihanj, ko bi na primer na hitro močno zmanjšali ali povečali moč motorja in tudi s tem sami povzročili nihanje napajalne napetosti, s tem pa potrebo po nepotrebni regulaciji.
Koliko časa poganjati?
Ko dosežemo, da se elektromotor vrti v z enako hitrostjo ne glede na napolnjenost baterij, lažje izdelamo algoritme, pri katerih je potrebno enega od elektromotorjev poganjati natančno določen čas, da dosežemo želeni učinek. Predstavljajmo si, da želimo krmiliti motor za obračanje prednjih koles vozila levo ali desno. Pri tem pri veliki hitrosti obračanja učinkovito zaznamo le skrajna položaja, ko so kolesa obrnjena skrajno levo ali skrajno desno. Zato želimo, da bi se kolesa obračala sorazmerno počasi, da bi jih lahko dovolj hitro zaustavili, ko dosežejo želeni kot zasuka. Zanima nas, kakšna je minimalna moč, ki je potrebna za vrtenje koles na trenutni podlagi.
To ugotovimo s sorazmerno počasnim povečevanjem moči na elektromotorju za obračanje koles, dokler ne zaznamo obračanja koles. Ko se obračanje začne, počakamo, da se kolesa najprej zavrtijo skrajno levo in se ustavijo, ko ni več mogoče vrtenje. Takrat s povečevanjem moči do največje preverimo, ali je morebiti prišlo le do zatikanja. Če mehanizem še vedno stoji, pomeni, da smo dejansko v skrajnem položaju. Zdaj ponovimo postopek in počasi zavrtimo elektromotor v nasprotno smer in merimo čas, ki je potreben, da se mehanizem zavrti skrajno desno. Nato ponovim merjenje časa še za obračanje iz skrajno desne pozicije v skrajno levo. Časa za obračanje v eno in drugo smer morata biti praviloma približno enaka. Če želimo kolesa zavrteti za določen kot, moramo kolesa vrteti le sorazmeren del časa. Pri tem uporabljamo izmerjena časovna intervala glede na to, ali vrtimo kolesa levo ali desno.
Če želimo hitrejše obračanje koles, prej izmerjeno najmanjšo moč za obračanje koles le pomnožimo z določenim faktorjem, pri čemer se moramo zavedati, da s tem hkrati zmanjšamo natančnost.
Bistveno večja natančnost z uporabo enkoderja
Kakršnakoli neposredna indikacija hitrosti vrtenja motorja za obračanje koles bistveno poenostavi algoritem. Odlično je, če lahko neposredno merimo število zasukov osi elektromotorja, ki prek zobniškega prenosa poganja mehanizem za obračanje koles. Principov merjenja hitrosti in smeri vrtenja je več. Najenostavnejši senzor lahko sestavimo na osnovi oddajne infrardeče LED in infrardečega fototranzistorja, ki odda impulz ob vsakem obratu elektromotorja. S tem sicer ne zaznamo smeri vrtenja, a to tako ali tako poznamo, saj vemo, kako smo krmilili motor. Do anomalije pride le v primeru prisilnega vrtenja koles v nasprotno smer od želene, kar pa se v principu zgodi, le če skušamo ročno obračati kolesa, ko je vozilo dvignjeno od tal. Kakorkoli, namesto preprostega zaznavanja, lahko uporabimo tudi pravi enkoder, ki preko dveh digitalnih signalov sporoča ne le hitrost, ampak tudi smer vrtenja koles.
Pametno usmerjanje in zaviranje s pomočjo merjenja hitrosti vseh koles
Kako vemo, da robot zavija v določeno smer? Podatek o smernem kotu prednjih koles lahko preverimo tudi s podatki o hitrosti vrtenja posameznih koles. Pri mojem testnem traktorčku, ki nima diferenciala, se prednji kolesi vrtita prosti, zadnji pa sta togo povezani na prenos motorja. To pomeni, da so dovolj trije enostavni merilniki vrtenja, s katerimi ugotavljamo gibanje vseh štirih koles.
Merilnike lahko izdelamo iz oddajne in sprejemne infrardeče diode, tako da se svetloba infrardeče diode odbija od srebrnih nalepk na notranjem delu vsakega od prednjih koles in enega od zadnjih koles, podobni implementacijo poznamo tudi v večini osebnih dvigal. Kako vemo, kdaj se robot skoraj gotovo dejansko premika? Seveda! Ko se vsa kolesa vrtijo v isto smer. Če se vrti samo eno kolo ali samo zadnja ali samo prednja kolesa, se robot skoraj gotovo ne premika, razen pri zelo hitrem pospeševanju, ko se prednja kolesa za hip odlepijo od tal, a se kljub temu še vedno vrtijo zaradi gibalnega momenta. Torej lahko vseeno sklepamo, da se robot premika le, ko zaznavamo gibanje na vseh treh merilnikih.
Kako vemo, v katero smer in kako močno robot zavija? To ugotovimo iz razlike hitrosti vrtenja prednjih koles. Če je ta enaka, traktorček vozi naravnost, sicer pa zavija v levo ali v desno. Kolo na notranji strani zavijalnega kroga opiše krajšo pot kot kolo na zunanji strani, zato se vrti nekoliko počasneje. Edina težava je, da ne vemo natančno, kako so kolesa obrnjena, ko traktorček stoji, a tu si lahko pomagamo z že omenjenim s trimerjem, ki meri zasuk prednjih koles.
Lotimo se še pametnega zaviranja! Z merilniki vrtenja koles natančno vemo, kako hitro in v katero smer se traktorček premika. Zato je implementacija zaviranja pred oviro z elektromotorjem lahko natančnejša. Če vemo, koliko traktorček tehta, vemo kakšno gibalno maso ima, zato vemo tudi, koliko energije bomo morali vložiti pri zaviranju, če smo prej z ultrazvočnim senzorjem izmerili približno razdaljo do ovire. K slednji moramo dodati še varnostno razdaljo, pri kateri upoštevamo morebiten pogrešek meritve. Zdaj lahko izračunamo, kakšen mora biti zavorni učinek v primeru, da ne bo zdrsavanja koles med zaviranjem. Dobro je tudi to, da merilnih hitrosti delujejo tudi med zaviranjem, zato lahko zavorno silo sproti prilagajamo hitrosti traktorčka.
Alternativa: Optično merjenje premikanja
Glede na to, da traktorček pod sabo pokrije večjo površino kod računalniška miška, je ideja, da bi za merjenje premikov lahko uporabil tipalo odslužene optične Logitechove miške ADNS5020, naravnost odlična. Težava je le prilagoditev optike. Računalniška miška je do podlaga oddaljena dober milimeter, medtem ko je traktorček dvignjen nekaj več kot pol centimetra. Prav tako, je potrebno površino pod traktorčkom primerno osvetliti s svetlečo diodo. To je kar veliko odprtih vprašanj, kljub temu pa za merjenje premikanja traktorčka ne potrebujemo take natančnosti kot pri računalniški miški, kjer ta doseže tudi 1200 dpi. Za traktorček je dovolj desetkrat manjša natančnost, vendar pa potrebuje optični senzor oziroma drugačno lečo kot je tista pri računalniški miški, če ga želimo bolj oddaljiti od tal. Uporabimo lahko lečo za kamero CCTV, ki ima goriščno razdaljo okoli 6 mm, kar poveča razdaljo do tal na okoli 4 cm in obenem omogoča nastavljanje ostrine. Ali je lažje meriti razdaljo z neposrednim spremljanjem vrtenja koles, ali z digitalnim optičnim tipalom, kar pa lahko presodite sami.
USB-PIC32 most/programator/razhroščevalnik
Kaj ima PIC32, kar MCP2200 oziroma PIC18F14K50 nimata? PIC18 je 8-bitni mikrokontroler, ki ima zaradi krajših registrov manj natančne časovnike, hkrati ne zmore tako visokih delovnih frekvenc kot PIC32. Je pa precej cenejši. Res pa je, da evro ali dva razlike pri ceni za domače projekte ne pomeni dosti. Po drugi strani sta PIC32MX250F128B in njegov zmogljivejši brat PIC32MX270F256B zaprta v SDIP-28 ohišja, ki jih je enostavno spajkati na prototipna vezja.
Ker sem imel s kupljenimi TTL USB-RS232 vmesniki kar naprej težave, če sem jih z istim računalnikom hkrati povezal po več, hkrati pa sem želel implementirati tudi krmilne priključke RESET, ENABLE in BOOT MODE, je bil PIC32 odlična izbira. Obenem ima slednji kljub 3,3 V napajanju tudi nekaj na do 5,5 V tolerantnih priključkov, ki so kot nalašč za implementacijo vmesnika. Ker pa sem vmesnik potreboval za programiranje 3,3 V ESP8266 in ESP32 modulov, me 5,5 V tolerantnost niti ni kaj dosti zanimala, temveč sem si želel predvsem višjih baudnih hitrosti.
Zanimivo pa je, da pri gradnji TTL USB-RS232 mostu/programatorja/razhroščevalnika (oz. vmesnika) ni težko izdelati načrta vezja, saj moramo mikrokontrolerskemu čipu dodati le nekaj uporov, kondenzatorjev in 8 MHz kristal. Pri tem je večina 100-ohmskih uporov, ki ščitijo čip pred morebitnimi negativnimi napetostnimi impulzi, prenapetostnimi impulzi in pred nepravilno vezavo, ko izhod PIC32 povežemo z izhodom programirane naprave, pri čemer je na vsakem od izhodov drugačna izhodna napetost. Ostali upori, kondenzatorji in napetostni stabilizator na osnovi LM317T regulatorja, ki se napaja z USB priključka, zagotavljajo delovanje PIC32. Vezje si lahko ogledate na spletni strani PC USB Projects.
Zdaj pa namenimo še nekaj besed ugnezdeni programski opremi. V splošnem lahko USB-RS232 most implementiramo kot USB CDC napravo, vendar standard zanjo v osnovi omogoča le krmiljenje standardnih modemskih RS-232 priključkov, ki jih lahka uporablja katerakoli terminalska programska oprema. Po drugi strani se lahko odločimo tudi za nekoliko zahtevnejšo kombinirano implementacijo, ki v eni fizični USB napravi združuje USB HID in USB CDC vmesnik. Ta princip je uporabljen tudi v Microchipovih standardnih TTL USB-RS232 vmesnikih MCP2200. Z njim pridobimo predvsem možnost uporabe vmesnika tudi kot splošnih vhodni-izhodnih vrat.
Kakorkoli, zaradi enostavnejše programske implementacije, sem se odločil, da raje ostanem samo pri USB CDC napravi, ki sem ji dodal še možnost upravljanja treh izhodnih priključkov, s katerimi nastavljam nivoje izhodov RESET (RB14), BOOT MODE (RB13) in ENABLE (RB3) ter dveh opcijskih izhodov RB15 in RB7. Pri tem je edino RB7 odporen tudi na napetosti do 5,5 V. RS-232 komunikacija poteka pred RP9 (U2TX) in RB8 (U2RX), ki sta prav tako odporna na napetosti do 5,5 V. Pri tem povejmo, da pomeni U2TX signal TxD funkcijske enote UART2 v PIC32, U2RX pa signal RxD iste funkcijske enote.
Rešiti moramo le še vprašanje, kako spreminjati stanje prej omenjenih krmilnih digitalnih izhodov. Ena izmed rešitev, je ukaz za nastavitev baudne hitrosti, ki jo določimo z 32-bitno vrednostjo DTERate. Nekaterih hitrosti (od 191912 baudov do 191920 baudov) za programiranje ESP modulov ne potrebujemo, zato jih lahko prestrežemo in uporabimo za spreminjanje vrednosti krmilnih digitalnih izhodov po naslednji tabeli:
Hitrost Funkcija
191919 Nastavi ESP modul v način za programiranje, samodejno nastavljanje priključkov BOOT MODE, RESET in ENABLE v pravilnem zaporedju
191918 Nastavi ESP modul v način za normalno delovanje, samodejno nastavljanje priključkov BOOT MODE, RESET in ENABLE v pravilnem zaporedju
191917 RESET = 0
191916 RESET = 1
191915 ENABLE = 0
191914 ENABLE = 1
191913 BOOT MODE = 0
191912 BOOT MODE = 1
Kot vidimo, lahko na ta način z izbiro sicer neuporabne baudne hitrosti najenostavneje nastavljamo vrednosti izhodov. Ostale vrednosti uporabljamo za nastavljanje dejanske baudne hitrosti. Vgrajeni programsko opremo za PIC32 najdete na spletni strani PC USB Projects.
Še nekaj opozoril, če se boste lotili gradnje PIC32 mostu/programatorja/razhroščevalnika. TTL RS-232 povezava do priključkov ESP modula mora biti kar se da kratka (do okoli 10 cm) sicer ni mogoče doseči velikih hitrosti prenosa podatkov. Zato je dobro na koncu tiskanine s PIC32 pritrditi vtični konektor, ki ga neposredno vtaknemo v vtičnici na tiskanini z ESP modulom. Morebitna žična povezava navadno podaljša komunikacijsko pot precej prek 10 cm. Dolžina kabla s krmilni signali ni pomembna, oziroma je lahko bistveno daljša, saj se signale ENABLE, RESET in BOOT MODE nastavimo le pred vsakim programiranje ali zagonom v normalnem načinu delovanja.
Programske niti, nujne v zahtevnih implementacijah
Hitrega in tekočega delovanja ne moremo doseči brez dodeljevanja sistemskega časa. K sreči programske knjižnice FreeRTOS omogočajo prav to. Če mislite, da lahko vse naloge optimalneje rešite le s prekinitvami, pomislite, da izkoriščanje prednosti dveh ali več jeder, ki jih imajo zmogljivejši mikrokontrolerji, podprti v Arduino razvojnem okolju. Med njimi tudi nekateri od popularnih ESP32, razen pod-različic S, ki imajo namesto drugega jedra vgrajeno kompleksno funkcijsko enoto za USB komunikacijo. Prednosti dveh jeder izkoriščate tudi projekt spletnega radija z ESP32 in popularna ESP32-CAM.
Katere programske knjižnice in programske strukture potrebujemo?
Arduino razvojno okolje vključuje FreeRTOS (nekakšen zastonjski operacijski sistem za delo v realnem času), od katerega moramo v svojo programsko kodo vključiti vsaj naslednje programske knjižnice:
#include "freertos/FreeRTOS.h" #include "freertos/semphr.h" #include "freertos/task.h"
S tem omogočimo uporabo programskih niti, semaforjev in osnovno funkcionalnost FreeRTOS, ki je strnjena v datoteki FreeRTOS.h. Naslednji korak je definicija programskih struktur, ki jih potrebujemo za upravljanje programskih niti. Na primer:
TaskHandle_t UltrasoundTask; SemaphoreHandle_t Sem_Usound;
Nujno potrebujemo ročico nove programske niti, v konkretnem primeru UltrasoundTask, medtem ko semaforjev za sinhronizacijo v določenih primerih ne potrebujemo, kot bo videli v nadaljevanju.
Inicializacija programske niti
Zakaj je vsako procesorsko jedro pri implementaciji procesne logike skoraj kot dodaten mikrokontroler? Denimo, čakanje na odziv HC SR-04 ultrazvočnega senzorja traja največ okoli 38 µs, ko pa zazna oviro, se odzove še veliko prej. Uporaba prekinitev zahteva shranjevanje programskega konteksta ob vsaki prekinitvi in vračanje konteksta ob vračanju v glavni program. Za tako kratke časovne intervale je to hkrati prepočasi in preveč obremeni procesor. Namesto tega lahko enega od procesorskih jeder zaposlimo s cikličnim preverjanjem tovrstnih senzorjev, medtem ko drugo jedro neodvisno izvaja glavno zanko.
Zato vsekakor ni vseeno, katero procesorsko jedro izberemo pri vzpostavitvi delovanja programska niti, ki ga izvedelo z naslednjim ukazom:
xTaskCreatePinnedToCore(codeUltrasoundTask, "UltrasoundTask",10000,NULL,2, &UltrasoundTask,1);
Kot vidimo, je prvi klicni parameter kazalec na podprogram, ki ga izvaja naloga, drugi ime naloga, tretji parameter je velikost sklada za programsko nit, četrti operand je kazalec na polje s parametri, ki so na voljo drugim programskim nitim. Peti parameter je prioriteta izvajanja programske niti, pri čemer je najnižja prioriteta 0, najvišja pa configMAX_PRIORITIES – 1, ki je določena v jedru FreeOS. Če te strukture ne potrebujemo, lahko vnesemo vrednost NULL. Šesti parameter je spremenljivka, v katero funkcija xTaskCreatePinnedToCore vrne ročico (angl. handle), s pomočjo katere lahko iz glavnega programa upravljamo ustvarjeno programsko nit. Zadnji parameter je zaporedna številka procesorskega jedra, ki izvaja programsko nit. V praksi je pomembno, da programske niti enakomerno obremenijo vsa procesorska jedra, pri čemer je pomembno tudi, da jim pravilno določimo prioritete, s čemer določimo tudi vrstni red in pogostnost njihovega izvajanja. Naloge, kot je merjenje razdalje z HC SR-04 senzorjem, zahtevajo veliko procesorskega časa, zato je smiselno, da jim namenimo eno od procesorskih jeder. Pri ESP32 seveda ni kaj dosti razmišljati, saj imamo na voljo le dve procesorski jedri, zato izberemo tisto, na katerem ne teče glavna programska zanka loop().
Koda programskih niti in njihovo sodelovanje
V večnitnem večopravilnem operacijskem sistemu je potrebno prenos podatkov med programskimi nitmi sinhronizirati, s čemer se izognemo branju napačnih podatkov, posledično pa tudi mrtvim zankam zaradi čakanja na dovoljenje za dostop do resursa, ki ga uporablja druga programska nit. Eden izmed načinov sinhronizacije so semaforji, s katerimi programske niti zaklenejo skupni resurs preden nad njim izvedejo obdelavo, po obdelavi pa resurs spet odklenejo in dovolijo dostop drugim programskim nitim.
Druga možnost sinhronizacije je prehod na zaporedno izvajanje, s katerim namerno za kratek čas preprečimo paralelnost. To tehniko uporabljamo tako, da program programske niti pred začetkom operacije, ki se mora izvesti nedeljivo, prepreči vse prekinitve in obenem drugih procesorskim jedrom prepove dostop do resursa, ki je predmet nedeljive operacije. Po izvedeni operaciji spet omogoči prekinitve in dostop do resursa drugim procesorskim jedrom. Ob tem še povejmo, da velika večina večjedrnih procesorjev problem razvrščanja dostopov posameznih procesorskih jeder rešuje že v strojni opremi, tako da lahko naenkrat do registra posamezne funkcijske enote dostopa le eno procesorsko jedro. Je pa kljub temu nemalokrat potrebna dodatna uporaba semaforjev, če moramo v nedeljivi operaciji nastaviti vrednosti več registrov funkcijske enote.
Če se ponovno vrnemo k problemu samodejnega merjenja oddaljenosti s HC SR-04 ultrazvočnim senzorjem; programsko usklajevanje dostopa (npr. s semaforji) med procesorskimi jedri ni potrebno, v kolikor zagotovimo, da s priključki vsakega z modulom ESP32 povezanega HC SR-04 tipala upravlja le ena programska nit, ki jo vedno izvaja isto procesorsko jedro. Kaj pa dostop do meritev? Če lahko vrednost meritve zapišemo v pomnilnik z eno nedeljivo operacijo, oziroma enim strojnim ukazom, ki se mora vedno izvršiti v celoti, usklajevanje ni potrebno, saj ni mogoče, da bi se izmerjena vrednost v celoti ne zapisala v pomnilniško polje, preden bi iz njega vrednost prebrala druga programska nit. To pa ne velja za mikrokontrolerje in procesorje, ki potrebujejo za obdelavo posameznega podatka zaporedje strojnih ukazov. Denimo, če želimo z 8-bitnim mikrokontrolerjem zapisati 32-bitno vrednost, potrebujemo vsaj 4 strojne ukaze, ki jim moramo izvesti na nedeljiv način tako, da nobena druga programska nit ne more brati, preden ni zapisana celotna vrednost.
Ne verjamete? Poglejmo, kaj se lahko zgodi. Če bi z 8-bitni mikrokontrolerjem z dvema strojnima ukazoma v eni izmed programskih niti zapisali 16-bitno vrednost iz A/D pretvornika, bi se lahko zgodilo, da bi druga programska nit uspela dostopati do 16-bitnega polja, že ko bi osvežili samo prvo polovico 16-bitne beseda. S tem bi programska nit, ki potrebuje meritev A/D pretvornika, prebrala pol novega in pol starega podatka. Predstavljajmo si, da bi A/D pretvornik najprej izmeril 0x00FF, pri drugi meritvi pa nekoliko večjo vrednost 0x010A. V nesrečnem primeru, ko bi bila pišoča programska nit prekinjena, ko bi osvežila zgolj prvi del vrednosti, bi bila v vmesnem času v skupnem pomnilniškem polju ostalim programskim nitim na voljo povsem napačna vrednost 0x01FF. V takih primerih imajo 32-bitni mikrokontrolerji pomembno prednost, saj ločljivost posamičnih meritev (npr. časa) z različnih senzorjev skoraj nikoli ne preseže 32-bitov.
Poglejmo še praktičen primer glavne zanke programske niti, s katero merimo razdaljo z ultrazvočnim senzorje HC SR-04:
void codeUltrasoundTask( void * parameter ){ for (;;) {dist_cm();delay(1);} }
Kot vidimo, sestavljata glavne zanke samo funkciji dist_cm in delay. Prva izmeri razdaljo in meritev shrani kot 32-bitno vrednost v pomnilniško polje, do katerega lahko dostopajo tudi druge programske niti.
Prenos podatkov med programskimi nitmi
Semaforji omogočajo prenos podatkov med programskimi nitmi. Eden od najbolj izmed zanimivih standardno vgrajenih primerov uporabe programskih niti in semaforjev v podpori za ESP32 module v Arduino razvojnem okolju je ESP32/Camera/CameraWebServer. Vendar primer uporablja vnaprej prevedene programske knjižnice, katerih izvorno kodo moramo poiskati na GitHubu, lahko pa namesto tega iz spletne strani PC USB Projects iz rubrike Downloads prenesete primer: VideoRecorder_SftyCAM_v1.0.zip, ki vsebuje datoteko camera.c, ki obenem praktičen primer uporabe programskih niti in semaforjev.
ESP32-CAM vgrajena programska oprema uporablja semafor frame_ready za usklajevanje dostopov do zajetih slikovnih okvirov. Najprej deklariramo ročico semaforja, takole: SemaphoreHandle_t frame_ready; Nakar v programski niti, ki bo lastnica semaforja, ustvarimo semafor in shranimo njegovo ročico: frame_ready = xSemaphoreCreateBinary(). Za upravljanje semaforja uporabljamo funkciji give in take. V programski niti, v kateri generiramo podatke, pokličemo funkcijo give vsakokrat, ko so podatki pripravljeni za prevzem in jih lahko do njih dostopajo druge programske niti. Slednje morajo pred branjem podatkov poklicati funkcijo take, ki pred semaforjem počaka, če podatki še niso na voljo, ali pa v zanki nadaljuje z drugimi nalogami in podatke še enkrat poskuša prevzeti kasneje.
Klic funkcije give je enostaven, saj je edini parameter ročica semaforja: xSemaphoreGive(frame_ready), medtem ko morajo vse programske niti, ki želijo dostopati do shranjenih podatkov, katerih niso lastnice, pred dostopom poklicati funkcijo take, ki počaka dokler semafor ne dovolj dostopa, oziroma se izteče časovna zakasnitev. Poglejmo podoben primer implementacije funkcije za branje z digitalne kamere shranjenega slikovnega okvirja:
camera_fb_t* get_frame(){ if (xSemaphoreTake(frame_ready, TIMEOUT) != pdTRUE){ Serial.print("Failed to get the frame on time!"); return NULL; } return (camera_fb_t*)fb; }
Ob klicu funkcije take podamo ročico semaforja in največjo dovoljeno zakasnitev TIMEOUT, po kateri čakanje na razpoložljivost podatkov zaključimo z napako. Podatki slike z digitalne kamere se v našem primeru prenašajo prek polja fb. Programska nit, ki zajema slikovne okvirje z digitalne kamere in je obenem lastnice semaforja, ne sproži funcije give do konca časovnega intervala TIMEOUT, je rezultat v programski niti, ki čaka na podatke s pomočjo funkcije take, NULL, obenem pa se prek serijske konzole izpiše sporočilo o napaki. V nasprotnem, funkcija get_frame vrne kazalec na podatkovno polje s slikovnim okvirjem.
Prihodnjič
Tudi prihodnjič se lotimo zanimivih tem. Ena izmed njih je tudi merjenje razdalj in premikanja z digitalno kamero ESP32-CAM. Če se da meriti razdalje s sorazmerno enostavno »mišjo« kamero s 361 slikovnimi fotodiodami, zakaj je ne bi mogli meriti tudi s kamero, ko ta deluje pri nizki ločljivosti okoli 160 x 120 pik. Bo dovolj hitra? Kaj pa hitro prototipiranje z gotovo strojno opremo različnih proizvajalcev? Kako programiramo ESP32 pametno uro, kako samo izdelamo domofon in še veliko drugih praktičnih projektov se bomo lotili…