Volatile pentru ceapa - pic24


Prin analizarea surselor altor persoane, întâlnesc adesea erori de programatori legate de o neînțelegere a scopului calificării volatile. Rezultatul acestei neînțelegeri este codul care vă oferă rupturi rare, complet imprevizibile și adesea foarte distructive și ireversibile. Acest lucru este valabil mai ales pentru sistemele de microcontroler, în cazul în care, în primul rând, stivuitoare de întrerupere fac parte din codul de aplicare, și, pe de altă parte, periferia registrelor de control apar în scop general RAM. Erori asociate cu neutilizarea (sau utilizarea necorespunzătoare) a calificativului volatil sunt greu de depanat datorită imprevizibilității și nerepetenței. Se întâmplă că codul C, în care utilizarea de volatile, în cazul în care ar trebui să fie prevăzut să folosească, funcționează bine, fiind asamblate cu un compilator, și eșuează (sau nu funcționează deloc), atunci când merge la alta.







Cele mai multe dintre exemplele din acest articol, scris pentru GCC compilator (MPLAB C30), pentru că, având în vedere arhitectura de compilator PIC24 de bază și caracteristici cel mai ușor de a sintetiza ilustrații mici pentru ea, care va fi prezentat manipularea greșită volatilă. Multe dintre aceste exemple se vor compila perfect corect pe alți compilatori (simpli), cum ar fi PICC sau MicroC. Dar acest lucru nu înseamnă că atunci când lucrăm cu acești compilatori, erorile volatile nu apar deloc. Doar codul demonstrativ pentru acești compilatori ar arăta mult mai mare și mai complicat.

(volatile în engleză înseamnă "instabilă", "volatilă")

Astfel, volatile este în C - este un calificativ variabilă, spunând compilatorul că valoarea unei variabile poate fi schimbată în orice moment și că o parte a codului care produce variabila de peste o anumită acțiune (a se citi sau scrie) ar trebui să fie optimizate.

Din punctul de vedere al algoritmului, în variabila a sunt setați doi biți de ordin mic. Optimizatorul poate face înlocuirea unui astfel de cod de către un operator:

câștigând astfel câteva cicluri și câteva celule ROM. Dar să ne imaginăm că facem aceleași acțiuni nu pe o anumită variabilă abstractă, ci pe un registru periferic:

Aici, în acest caz, optimizarea cu înlocuirea pentru "PORTB | = 3" nu ne poate aranja! Controlând stările terminale ale controlerului, avem adesea o secvență importantă de schimbări de semnal. De exemplu, generăm semnale SPI, iar un pin (PORTB.0) este date, iar celălalt (PORTB.1) este impulsurile de sincronizare. În acest caz, nu putem schimba starea acestor concluzii în același timp. În acest caz, nu se poate garanta că cipul controlat de pe ceas primește datele corecte. Și cu atât mai mult, nu am dori să optimizăm codul generând impulsul de sincronizare cu o durată de un ciclu:

Un astfel de cod ar putea fi perceput de compilator ca două acțiuni reciproce, iar prima linie nu a putut intra în codul obiect rezultat. Cu toate acestea, în practică, vedem că această optimizare nu este efectuată. Aceasta se datorează faptului că variabila la care este mapată registrul PORTB este declarată cu calificativul volatil. de exemplu:

(puteți vedea acest lucru uitandu-te la fișierul antet pentru controlerul special furnizat împreună cu compilatorul). Calificatorul volatil previne optimizarea codului care efectuează acțiuni în registrul PORTB. Prin urmare, chiar acțiunile reciproce rămân un optimizator intact și putem fi siguri că o ieșire va forma un impuls.

Astfel, prin realizarea optimizării, compilatorii tind să ne ajute să facem codul cât mai rapid și mai compact posibil. Cu toate acestea, dacă nu utilizați volatile. în unele cazuri, optimizarea poate juca o glumă rău intenționată cu programatorul. Și, trebuie să spun, cu cât compilatorul este mai inteligent și mai puternic, cu atât mai multe probleme pot crea cu utilizarea analfabetică a volatilelor.

Există trei tipuri principale de erori legate de calificarea volatilă.

neutilizarea volatilelor acolo unde este necesar

de obicei realizate de programatori care nu știu despre existența volatilă. sau văzute, dar nu înțelegeți ce este;

utilizați volatile acolo unde este necesar, dar nu la fel de necesar

este inerentă programatorilor care știu cât de volatili sunt importanți atunci când procesează procese paralele sau accesează registre periferice, dar nu țin cont de unele dintre nuanțele lor;

utilizați volatile acolo unde nu aveți nevoie (și, uneori, este)

așa fac cei care au ars o dată pe primele două greșeli. Aceasta nu este o greșeală și nu va duce la un comportament greșit al programului, ci va crea propriile probleme.

Luați în considerare un exemplu pentru PIC24:

Dacă optimizarea este dezactivată, acest cod va funcționa. Dar ar trebui să activați optimizarea, deoarece programul va începe să stea în funcția wait (). Ce se întâmplă? Compilatorul, difuzând funcția wait (), nu știe că variabila Counter se poate schimba în orice moment când are loc o întrerupere. El vede doar că îl resetăm și apoi îl comparăm imediat cu parametrul Time. Cu alte cuvinte, compilatorul presupune că variabila Time este întotdeauna comparată cu zero și listarea funcției wait () cu optimizarea activată va arăta astfel:

(Notă: Compilatorul C doar un singur octet trece la funcția prin intermediul registrului w0)

Ceea ce vedem în acest cod: Contor variabilă se face la zero, iar apoi funcția de timp, trecându-l prin W0 registru, în comparație cu zero, nu se mai acordând o atenție la valoarea reală a Coutner variabilă, care crește în mod regulat de fiecare dată când întreruperea cronometrului. Cu alte cuvinte, am ajuns într-un ciclu etern. Așa cum am menționat deja, punctul este că compilatorul nu presupune că funcția va fi întreruptă de un anumit cod care va efectua operații asupra variabilelor implicate în funcții. Aici calificativul volatil vine în ajutorul nostru.







Acum, când este compilat, compilatorul va genera un cod care va accesa de fiecare dată variabila Counter:

Dacă programul rulează sub RTOS, adică executarea funcției poate fi întreruptă la un moment dat și apoi din nou transferată din locul unde a fost întrerupt. Nu contează dacă planificatorul de cooperare din sistemul de operare (de exemplu, programator decide pentru sine, în cazul în care funcțiile de a fi întreruptă) sau deplasarea (aici programator nu rezolvă nimic, iar funcția sa poate fi întreruptă complet în orice moment, o prioritate mai mare). Este important faptul că compilatorul nu știe că un cod străin poate fi executat în mijlocul funcției. Aici este fragmentul de cod dintr-un program real:

Programul a lucrat bine, fiind colectate de către compilator HT-PICC18, dar atunci când transferați același cod pe PIC24 (compilator MCC30) a încetat să funcționeze, și anume, nu au reacționat deloc la butoane. Problema a fost că de optimizare MCC30, spre deosebire de optimizare a HT-PICC18, să ia în considerare faptul că, la momentul de execuție buton de comutare variabilă deja stocate într-unul din registrele de uz general (W0) (în PIC18 doar o singură baterie, astfel încât atunci când se lucrează cu el acest comportament este mai puțin probabil):

Adesea, aceste funcții sunt folosite pentru a crea mici întârzieri:

Cu toate acestea, unii compilatori văd un cod inutil în aceste funcții și nu îl includ în codul obiect rezultat. În cazul în care această întârziere este utilizat pentru a reduce rata de I2C software-ului (sau SPI) pentru compilator, de exemplu, HT-PICC, programul nu va mai funcționa, sau mai degrabă, ar funcționa atât de repede încât cipul de control nu este în transferul către nucleul AVR cu compilator WinAVR capabil să proceseze semnale datorită faptului că toate apelurile către funcția Întârziere vor fi anulate.

Pentru a evita acest lucru, trebuie să utilizați calificativul volatil.

O altă greșeală obișnuită este utilizarea unui pointer non-volatil la variabilă volatilă. De ce ne poate face rău? Luați în considerare un exemplu (dintr-un program real) în care a fost utilizat un indicator pentru registrul de port I / O. Programul SPI a controlat două chips-uri identice conectate la diferite pini ale microcontrolerului, portul la care a fost conectat microcircuitul activ a fost selectat prin intermediul pointerului:

Situația este aceeași ca în exemplul precedent: sub un compilator (MCC18) codul a funcționat, iar sub celălalt (MCC30) sa oprit. Motivul a fost într-un pointer declarat incorect. Dezasamblarea funcției SPI_Send () a arătat astfel:

Să se acorde atenție faptului că compilatorul a "aruncat" instalarea bitului 1 ("* Port | = 2"), considerându-l inutil, deoarece aproape imediat după aceasta, acest bit este resetat din nou, adică el a efectuat optimizarea fără a lua în considerare caracteristicile registrului indicat de variabila Port și a făcut-o deoarece programatorul nu a explicat compilatorului că registrul nu este simplu. Pentru a corecta eroarea, a trebuit să declarați variabila Port ca indicator al variabilei volatile:

Acum, compilatorul știe că optimizarea peste variabila indicată de Port nu poate fi făcută. Iar noua listă reflectă acest lucru:

În caz contrar, vom obține toate erorile ca în exemplul anterior.

accesul la o variabilă multibyte;

citiți / modificați / scrieți prin intermediul bateriei.

Luați în considerare exemplul de manipulare o variabilă care ocupă mai mult de o celulă de memorie (16-bit controler este int32, Int64, float, double; 8 biți - și chiar int16). Am dat de multe ori acest exemplu, o voi da din nou (pentru HT-PICC18):

Rețineți că variabila ADCValue are o dimensiune de 2 octeți (pentru a stoca rezultatul pe 10 biți al ADC). Care este pericolul acestui cod? Luați în considerare listarea uneia dintre comparații (de exemplu, prima):

Să presupunem că valoarea tensiunii la intrarea ADC este de așa natură încât rezultatul conversiei este 255 (0x0FF). Și ultima valoare a variabilei ADCValue, respectiv, este, de asemenea, = 0x0FF. Cu această valoare, începe un cod de comparație cu o valoare de 100 (0x064). În primul rând, compara bytes superioare ale variabile și constante (0x00 la 0x00), și apoi - junior (0x64 și 0xFF). Rezultatul, se pare, este evident. Cu toate acestea, aici este problema. Deși rezultatul conversiei AD și egal cu 0xFF, este influențată de mai mulți factori: stabilitatea tensiunii de alimentare (sau tensiune de referință) stabilitatea de intrare a nivelului de tensiune măsurată de apropiere față de unitatea de schimbare de prag LSB, diafonia, zgomot etc. Prin urmare, rezultatul conversiei AD. are un fel de jitter în una sau două unități de ordin inferior. Ie rezultatul ADC poate sări între valorile 0xFF și 0x100. Și dacă intervine o întrerupere între executarea comparațiilor, pot apărea următoarele:

valoare ADCValue = 0x0FF;

Se efectuează compararea octeților de ordin înalt: 0x00 și 0x00;

Sa produs o întrerupere ADIF, în care valoarea variabilei ADCValue a fost actualizată la 0x100;

cele mai mici octeți sunt comparate: 0x64 și deja 0x00!

deoarece programul crede că a existat o comparație de 0x000 <0x064, то она вызывает функцию Alarm.

Și calificativul volatil aici nu salvează. Aici, doar interzicerea întreruperilor va economisi atunci când se compară.

Deci, poate nu este nevoie de volatilă? Întreruperile sunt interzise oricum? Da, întreruperile sunt dezactivate, dar volatile. totusi este necesar. De ce? Luați în considerare aproape același cod pentru compilatorul C30:

Aici este volatilă și utilă! Să acordăm atenție unei linii înaintea unui ciclu etern - atribuirea valorii variabilei ADCValue la o variabilă Temp. În același timp, vom vedea listarea:

După cum puteți vedea, în interiorul bucla nu există acces la variabila ADCValue, iar comparația se face cu registrul W1, unde variabila ADCValue a fost copiată înainte de ciclu. Prin urmare, indiferent de modul în care se va schimba ADCValue, programul nostru nu va observa acest lucru. Deci este volatil în acest caz, nu uitați că acest calificativ nu ne garantează operațiunile atomice pe variabila declarată.

Ambele înregistrări sunt identice, dar ele nu realizează variabila variabilă variabilă p. Ambele intrări înseamnă "variabila p este un indicator pentru caracterele volatile". Consecințele acestei definiții sunt evidente: dacă modificați valoarea indicelui într-o întrerupere sau într-o sarcină paralelă, programul poate să nu observe acest lucru, deoarece va lucra cu pointerul prin RON.

Definiția corectă arată astfel:

Dacă este necesar pointerul volatil la variabilă volatilă, atunci acesta este declarat astfel:

Aici nu pot oferi recomandări clare. În majoritatea cazurilor când scriu programe în C, încerc să respect următoarele reguli:

variabilele globale utilizate în ambele întreruperi și în program (sau în întreruperi ale diferitelor priorități) trebuie declarate ca fiind volatile;

variabilele globale care sunt procesate de două sau mai multe sarcini atunci când se lucrează în cadrul unui sistem de operare multitasking ar trebui declarate ca volatile;

indicatorii la registrele periferice, precum și la variabilele declarate ca fiind volatile. trebuie să declarați ca indicatori volatili;

tot ceea ce nu se încadrează în primele trei reguli și nu este legat de periferie, se recomandă să scrie, abstractizând de la fier. Apoi devine clar că ciclurile formei "pentru (i = 0; i<100; i++) <>; "nu au sarcină algoritmică și pot fi șterse, iar dacă trebuie să fie lăsate, atunci variabilele ar trebui declarate ca volatile.

în toate celelalte cazuri volatile vor fi inutile.

Alte câteva observații:







Articole similare

Trimiteți-le prietenilor: