Autor: ing. C. Botezatu
Introducere
Acest articol se dorește a fi un ghid pentru programarea microcontrolerelor. Desigur că există nenumărate pagini de internet, blog-uri și forum-uri care prezintă aceste circuite electronice, însă majoritatea sunt în limba engleză. Am considerat că un articol în limba română ar fi binevenit. De asemenea, cele mai multe exemple folosesc limbaje de programare de nivel mai înalt, precum C, în timp ce în acest articol voi vorbi despre programarea microcontrolerelor folosind limbajul masină, cunoscut sub denumirea anglo-saxonă ca limbaj “assembly”.
Un microcontroler este un circuit electronic de control. Are o utilizare pe scară largă și poate fi găsit în automobile, avioane, jucării, electrocasnice, calculatoare personale, televizoare, telefoane mobile [1], sisteme de securitate, sisteme de dirijare a traficului etc.
Figura 1 Exemple de aplicații pentru un microcontroler [2]
Arhitectură
Așa cum sugerează și numele, un microcontroler este un circuit integrat. El este practic un “computer pe chip”. [1] Cu mici variații, arhitectura de bază a unui microcontroler conține toate componentele și accesoriile necesare pentru realizarea funcțiilor circuitului, precum:
• unitate centrală de procesare (CPU);
• porturi de intrare și ieșire (Input/Output Ports);
• memorie (Memory);
• circuite de temporizare (Timers);
• un oscilator intern (Internal Oscillator) care furnizează un semnal de ceas;
• etc.
Un microcontroler poate să conțină și alte module, precum unele pentru comunicare serială sau paralelă, convertoare analog-digital (ADC), memorie flash (Flash Memory) etc. Imaginea de mai jos poate oferi o descriere mai clară asupra arhitecturii unui microcontroler [3].
Figura 2 Exemplu de arhitectură pentru un microcontroler [3]
Programarea unui microcontroler. PIC12F629
După cum s-a prezentat mai sus, un microcontroler poate fi integrat în diverse sisteme de control, monitorizare și procesare. Un astfel de circuit interacționează cu mediul din jurul său prin intermediul porturilor de intrare și iesire (I/O Ports). Spre deosebire de un circuit electronic standard, a cărui funcționalitate este fixată în momentul fabricației, funcționalitatea unui microcontroler poate fi modificată dupa fabricație, prin programarea acestuia de către utilizator. [4]
Programarea unui microcontroler înseamna “transcrierea” în memoria sa internă a unui program (cod) creat de către utilizator. Acest cod va modifica, printre altele, și valorile digitale din regiștri (registre) de funcții speciale (SFR → vezi Figura 2, în blocul de memorie internă). Fizic, programarea se realizează folosind un circuit numit programator.
Pentru a exemplifica modul în care se poate programa un microcontroler, am ales PIC12F629, un microcontroler produs de Microchip [5]. Acesta este un circuit cu 8 pini, din care 2 pini sunt folosiți pentru conexiunile de alimentare și masă, iar ceilalți 6 sunt pini de intrare sau ieșire. Figura 3 și Figura 4 ne arată configurația pinilor, dar și funcționalitatea fiecărui pin.
Figura 3 PIC12F629 – diagrama pinilor [5]
Figura 4 PIC12F629 – descrierea funcționalității fiecărui pin [5]
Așa cum am menționat în introducere, în acest articol voi arăta cum se poate programa un microcontroler, folosind limbajul mașină, în acest caz fiind vorba de limbajul de programare numit PIC Assembly Language Programming.
Codul sursă, cu explicații
Proiectul ales este unul clasic pentru microcontrolere, și anume aprinderea și stingerea unui LED (Light Emitting Diode). O să prezint mai întâi codul sursă, după care voi explica rostul fiecărei linii de cod [6].
Codul sursă (în limbaj mașină), scris în programul MPLAB IDE (oferit de Microchip), este următorul:
“ ; File: main_asm.asm; ; PIC12F629 LED blinker; ; Author: Catalin Botezatu; ; Created on January 11, 2020, 5:08 PM; #INCLUDE RES_VECT CODE 0x0000 ; vectorul de reset al procesorului GOTO START ; mergi la inceputul programului CBLOCK 0x20 ; necesar pentru a crea timpi de intarziere COUNT1 COUNT2 ENDC MAIN_PROG CODE ; START BSF STATUS, RP0 MOVLW 0xFE MOVWF TRISIO ; stabilirea porturilor de intrare si iesire BCF STATUS, RP0 MAIN BSF GPIO,0 ; aprindere LED CALL DELAY CALL DELAY BCF GPIO,0 ; stingere LED CALL DELAY CALL DELAY GOTO MAIN ; mergi in bucla principala DELAY LOOP1 DECFSZ COUNT1, 1 GOTO LOOP1 DECFSZ COUNT2,1 GOTO LOOP1 RETURN END”
În continuare, voi explica rolul fiecărei linii sau porțiuni de cod.
“ ; Fisier: main_asm.asm; ; PIC12F629 LED blinker; ; Autor: C. Botezatu; ;Creat pe 11 Januarie 2020, 5:08 PM;”
Folosirea simbolului punct-și-virgulă (;) la începutul unei linii indică faptul că textul de pe această linie este un comentariu. Această linie de cod nu va fi luată în considerare în momentul în care codul va fi compilat.
Primele rânduri dintr-un cod pot fi folosite pentru a indica numele proiectului, numele autorului, data la care a fost scris, sau alte informații relevante.
“ #INCLUDE ”
Această linie îi indică programului de asamblare să includă fișierul menționat, specific tipului de microcontroler. Acest lucru va permite apelarea regiștrilor interni folosind numele registrului, în loc de adresa la care se află acesta în memoria internă a microcontrolerului.
Se poate vedea în Figura 5 o hartă a memoriei de date pentru PIC12F629. Programarea unui microcontroler este axată în general pe manipularea valorii regiștrilor. Vom vedea mai departe cum putem folosi funcția fiecărui registru.
Figura 5 PIC12F629 – harta memoriei de date [5]
“ RES_VECT CODE 0x0000 ; vectorul de reset al procesorului GOTO START ; mergi la inceputul programului”
Cele două linii de cod dau un așa numit “vector de resetare” pentru cod. Un vector de resetare indică partea de program care va fi prima executată. În acest caz, e vorba de cea care are eticheta “START”.
“ CBLOCK 0x20 ; necesar pentru a crea timpi de intarziere COUNT1 COUNT2 ENDC”
Liniile de cod de mai sus folosesc operatorul assembly numit “CBLOCK”. Acesta atribuie denumirile de “COUNT1” și “COUNT2” unor regiștri de uz general care au adresa în memorie 20h, respectiv 21h. Dacă ne uităm la harta memoriei PIC12F629, din Figura 5, putem observa că aceste două adrese sunt parte din SRAM și nu au nume specifice atribuite. Asta înseamnă că le putem folosi după cum dorim. Regiștrii COUNT1 și COUNT 2 vor fi folosiți pentru a obține elemente de întârziere (delay subroutine).
“ BSF STATUS, RP0 MOVLW 0xFE MOVWF TRISIO ; stabilirea porturilor de intrare si iesire BCF STATUS, RP0”
Execuția efectivă a programului începe cu aceste linii. Pentru a putea interacționa cu mediul în care este folosit, microcontrolerul face uz de porturile (pinii) de intrare și de ieșire. În cazul proiectului nostru, vom dori ca un port să fie configurat ca și port de ieșire, deoarece vrem ca acest port să transmită un semnal la ieșire, semnal care va aprinde și va stinge un LED.
Registrul care controlează direcția porturilor (intrare sau ieșire) se numește “TRISIO” și este situat în bancul 1 al memorie, așa cum putem vedea din Figura 5. Înainte de a lucra cu un registru, este necesar să se meargă în bancul în care acesta este localizat. Pentru a putea configura TRISIO este necesar așadar să mergem mai întâi în bancul 1 (bank 1). Acest lucru se realizează prin setarea unui bit, RP0, din registrul STATUS (NB. Registrul STATUS are adresa în ambele bancuri, deci nu este nevoie să setam vreun bit pentru a-l accesa).
Figura 6 PIC12F629 – structură registru special STATUS [5]
Operatorul BSF (Bit Set register F) face tocmai acest lucru, adică trece în 1 logic bitul RP0 din registrul STATUS.
Acum, după ce am ajuns în bancul corect, putem să configurăm registrul TRISIO. Structura sa poate fi văzută în Figura 7.
Figura 7 PIC12F629 – structură registru special TRISIO [5]
În Figura 7 se poate vedea și că setarea unui bit va face ca portul corespunzător să fie configurat ca și port de intrare. Simetric, resetarea unui bit din registrul TRISIO va face ca portul corespunzător să fie configurat drept port de ieșire. Registrul TRISIO are 8 biți, însă doar 6 sunt activi și configurabili. Aceștia corespund celor 6 porturi de semnal pe care le are PIC12F629.
Am ales să configurez portul GP0 ca și port de ieșire, urmând ca celelalte porturi să fie configurate ca și porturi de intrare. Asta înseamnă că voi reseta bitul 0 din TRISIO, în timp ce voi seta ceilalți biți din registru. TRISIO va avea următoarea valoare: “xx111110”. Având în vedere faptul că biții 6 și 7 sunt inactivi, putem configura registrul cu valoarea hexadecimală de “FEh”.
“ MOVLW 0xFE”
Acest lucru s-a realizat în doi pași: întâi am transferat această valoare într-un așa numit “registru de lucru”, folosind operatorul MOVLW (move literal value to W register).
“ MOVWF TRISIO ; stabilirea porturilor de intrare si iesire”
Apoi, valoarea din registrul de lucru a fost transferată în registrul TRISIO, folosind operatorul MOVWF (move value from W to register F).
“ BCF STATUS, RP0”
În final, am revenit la bancul 0, resetând bitul RP0 din registrul STATUS, întrucât aici vom lucra cu următoarele linii de cod:
“ MAIN BSF GPIO,0 ; aprindere LED CALL DELAY CALL DELAY BCF GPIO,0 ; stingere LED CALL DELAY CALL DELAY GOTO MAIN” ; mergi in bucla principala
Cuvântul cheie “MAIN” ne indică faptul că urmează partea principală a codului. Prima linie va seta portul GP0 , ceea ce va duce la aprinderea LED-ului (scopul proiectului este aprinderea și stingerea unui LED). Folosim din nou operatorul BSF pentru a pune valoarea 1 logic pe bitul 0 al registrului GPIO.
Dacă după prima linie ar urma linia “BCF GPIO, 0”, ceea ce înseamnă că bitul 0 al registrului GPIO este resetat (BCF=Bit Clear register F), atunci LED-ul s-ar stinge imediat. De fapt, ar avea un delay foarte mic, dat de durata realizării unei operațiuni (durată care poate fi de ordinul microsecundelor).
Pentru a face vizibilă aprinderea și stingerea LED-ului, vom apela (CALL) la rutine de întârziere. În cazul nostru, rutina de întârziere este numită “DELAY” și va fi explicată mai jos. Până atunci, e suficient să reținem că, între aprinderea și stingerea LED-ului, introducem un timp de întârziere. În exemplul nostru, câte doi timpi de întârziere.
Secvența de cod se încheie prin “GOTO MAIN”, ceea ce va face ca programul să ruleze în buclă. Astfel, după încărcarea codului în microcontroler, acesta va stinge și va aprinde periodic LED-ul conectat la portul GP0.
“ DELAY LOOP1 DECFSZ COUNT1, 1 GOTO LOOP1 DECFSZ COUNT2,1 GOTO LOOP1 RETURN”
Rolul acestei sub-rutine este de a crea timpul de întârziere necesar pentru a face vizibile aprinderea și stingerea LED-ului.
Înainte de a explica liniile de cod de mai sus, este util să explic un alt aspect legat de microcontrolere și procesarea instrucțiunilor. Limbajul masină este procesat linie cu linie [6]. Procesarea unei linii necesită (cu mici excepții), 4 perioade de ceas intern. Oscilatorul intern pentru PIC12F629 este setat la 4 MHz. Ceea ce înseamnă că procesarea fiecărei instrucțiuni va dura 1us (microsecundă). Dacă nu am folosi sub-rutina DELAY, aprinderea și stingerea LED-ului ar dura 1us, ceea ce nu ar permite observarea schimbării. Procesarea unei linii care începe cu operatorul GOTO va dura 8 perioade de ceas, deci 2 us.
“ LOOP1 DECFSZ COUNT1, 1 GOTO LOOP1”
Acum să mergem mai departe și să înțelegem mai bine ce se întâmplă în partea de cod aferentă sub-rutinei DELAY, întai în bucla LOOP1. Operatorul DECFSZ (decrement F, skip if zero) va decrementa valoarea registrului COUNT1 până în momentul în care aceasta va fi nulă, dupa care va trece peste următoarea linie (o va ignora, mai exact). Valoarea “1” de după “COUNT1” indică faptul că rezultatul decrementării va fi salvat tot în COUNT1.
Merită menționat faptul că valoarea inițială a COUNT1 este 255 (FF, în hexadecimal). Instrucțiunea “GOTO LOOP1” va crea o buclă, care va duce așadar la decrementarea COUNT1 de 255 de ori.
“DECFSZ COUNT2,1 GOTO LOOP1 RETURN”
După ce COUNT1 a atins valoarea 0, se iese din bucla LOOP1 și se trece la decrementarea COUNT2. Vedem că următoarea linie de cod face trimitere însa la bucla LOOP1. Asta înseamnă că vom avea din nou decrementare pentru COUNT1, pornind de data asta tot de la valoarea de 255. Acest lucru se va repeta tot de 255 de ori (COUNT2 are o valoare inițială tot de 255), după care operatorul RETURN va realiza ieșirea din rutina DELAY.
În total, am avut parte de 255*255 de cicluri de instrucțiuni. Ținând cont că o instrucțiune standard necesită 1us, în timp ce instrucțiunea GOTO necesită 2us, asta înseamnă că un apel la rutina DELAY va duce la o întârziere de 255*255*3us = 195.075ms (mili-secunde).
Întrucât în partea principală a codului avem 2 rutine de întârziere, atât pentru aprinderea, cât și pentru stingerea LED-ului, rezultă că fiecare din cele 2 stări va dura ~400ms. Suficient pentru ca ochiul uman să perceapă schimbările.
Cuvânt de încheiere
Exemplul de mai sus este unul simplu, mai degrabă cu scop educativ decât cu scop practic. Totuși, acesta ne ajută să înțelegem cum funcționează un microcontroler și cum îl putem programa. Programarea efectivă necesită folosirea unui circuit numit „programator”.
Înainte de a realiza implementarea fizică, se poate face testarea acestui proiect într-un simulator. Eu am făcut acest lucru folosind Proteus [7], un simulator versatil și foarte bine realizat. Puteți vedea mai jos o imagine cu setup-ul folosit de mine.
Figura 8 Setup în Proteus, pentru LED blink folosind PIC12F629
Dacă aveți întrebări, nelămuriri sau sugestii, vă rog să folosiți secțiunea de comentarii.
Mult succes!
Referinte:
[1] Carte Microcontroller Programming;[2] http://www.vlsifacts.com/wp-content/uploads/2015/10/uC-Applications.png
[3] https://electronicsdesk.com/pic-microcontroller.html
[4] https://hobbytronica.ro/ce-este-un-microcontroller/
[5] datasheet PIC12F629.
[6] https://www.teachmemicro.com/pic-assembly-language-intro/
[7] https://www.labcenter.com/simulation/
Multumim pentru articol