This is an old revision of the document!
GPIO (General-Purpose Input/Output) este calitatea unui pin al unui circuit integrat de a-i putea fi controlat comportamentul, adică direcția de trecere a curentului electric prin el și valoarea acestuia, într-un mod programatic. Astfel, noi putem să configurăm un pin pentru a 'scrie' o valoare, caz în care pinul devine o sursă de '0' sau de '1' logic, controlată prin registre (practic legăm pinul la VCC sau GND cu ajutorul unor tranzistori). În acest mod putem, spre exemplu, să aprindem un led folosind instrucțiuni.
Un pin GPIO poate fi folosit, de asemenea, și pentru 'citire'. Astfel, putem afla dacă pinul este comandat din exterior către '0' sau '1' logic.
Un pin este o bucată de metal ce permite crearea unei legături între o componentă electronică și alta. În cazul circuitelor integrate pinii pot ieși din capsula integratului sau pot face parte dintr-una din fețele sale, caz în care ne referim la ei ca pad-uri.
În datasheet-ul ATTiny20, la pagina 2, sunt descrise configurațiile pinilor. Observați ca exista mai multe configurații, deoarece acest microcontroller vine în diferite capsule ce diferă ca mărime, cost și metodă de lipire.
Să ne concentrăm pe prima varianta, descrisă în capitolul 1.1, SOIC (Small Outline Integrated Circuit) & TSSOP (Thin-Shrink Small Outline Package). Primul lucru pe care îl observăm este că această capsulă are 14 pini. Dintre aceștia, doi sunt necesari pentru alimentarea microcontroller-ului (VCC - pinul 1, si GND - pinul 14). Evident, acești doi pini nu pot fi folosiți ca GPIO, însă restul da.
Al doilea lucru pe care îl observăm este că pinii, pentru a fi mai ușor de folosit, au câte o denumire. Această denumire ne indică și ce funcții poate avea acel pin. În cazul pinilor 1 și 14 este simplu: VCC - alimentare de curent continuu, și GND - masa (ground). Denumirea celorlalți pini este de forma PXn, unde X este în {A, B} și n este în {0, 1, 2, 3, 4, 5, 6, 7} (spre exemplu: PB0, PA5, PB4, PA7). Aceasta este denumirea principală a acestor pini. Între paranteze găsim și denumiri auxiliare, care indică funcții alternative ale pinilor (spre exemplu: pinul PA0 mai este numit și ADC0, ceea ce înseamnă că el poate servi ca și intrare pentru convertorul analog-digital).
În microcontroller-ele Atmel, cum este și ATTiny20, pinii ce suportă GPIO sunt grupați în porturi. Un port are întotdeauna 8 pini logici (ce pot fi folosiți într-un program), dar poate avea mai puțini pini fizici (ce pot fi folosiți pentru a realiza legături fizice). Aceste porturi sunt denumite în ordine alfabetică: A, B, etc. De aici reies și numele pinilor: PA0 înseamnă că pinul face parte din portul A, și este pinul cu numărul 0 în acel port. Observați că portul B nu are decât 4 pini fizici (PB0, PB1, PB2 si PB3). Totuși nu este o eroare să atribuim într-un program o valoare pinului PB4, doar că această atribuire nu va avea niciun efect în exteriorul controller-ului.
În cadrul microprocesoarelor ca ATTiny20, avem de-a face cu două tipuri de registre de control:
Pentru modulele periferice, registrele de control sunt folosite pentru a efectua operații precum: pornește/oprește modulul, schimbă anumite setări (e.g. BAUD rate pentru portul serial), etc. Pe lângă registre, pentru a interacționa cu aceste periferice, mai folosim registre de stare (pentru a afla care este starea perifericului), și registre de date (pentru a transmite/recepționa date către/de la el).
Dacă un pin are funcția de GPIO, atunci vom putea să îi controlăm direcția și valoarea în mod programatic. Vom realiza acestea prin scrieri/citiri în/din registre speciale, ce controlează exact acest comportament.
Registrele care controlează comportamentul unui port sunt:
Fiecare dintre aceste registre are 8 biți, câte unul pentru fiecare pin logic al portului. De exemplu, DDRA (pentru portul A) are biții DDA0, DDA1, …, DDA7, PORTA are biții PORTA0, …, PORTA7 și PINA are biții PINA0, …, PINA7. În continuare ne vom limita doar la portul A al microncontroller-ului.
Schimbând un bit din 1 în 0 sau invers putem schimba comportamentul pinului. Astfel, registrele menționate anterior au următoarea funcționalitate:
Figura precedentă descrie organizarea spațiului de adresă pentru ATTiny20. Observați că memoria RAM nu ocupă decât o mică parte din acest spațiu și aceasta începe de la adresa 0x40. În rest avem diferite zone ce servesc altor scopuri. Ce se întâmplă de fapt este că toate perifericele, inclusiv memoria RAM si ROM (FLASH) sunt legate la aceeași magistrală de adrese, pe 16 biți (noi vom considera ca are doar 8 biți). Fiecare dispozitiv este responsabil să răspundă doar la adresele ce îi aparțin. Deci, pentru orice adresă în intervalul [0x40:0xBF] memoria RAM este responsabilă de tratarea citirii/scrierii, iar celelalte periferice trebuie să elibereze magistrala de date. La fel, pentru adrese în intervalul [0x3FC0:0x3FC3] putem citi dintr-o mică memorie non-volatilă unde este reținut Device ID-ul.
De la 0x00 la 0x3F gasim I/O Space. Aici se află toate registrele de lucru cu periferice și, desigur, PORTA. În datasheet, la capitolul 22.Register Summary (pagina 203) putem vedea adresele tuturor registrelor I/O din controller (care nu sunt de uz general). Registrul PORTA are adresa 0x02 deci pentru a scrie/citi date în/din el trebuie să executăm o instrucțiune STS/LDS cu adresa de scriere/citire 0x02.
LDS R16, 0x40 ; se încarcă date din RAM de la adresa 0x40 în registrul R16 STS 0x02, R16 ; se stochează datele din registrul R16 la adresa 0x02, adică în registrul PORTA
Pentru a lucra cu registrele din I/O Space putem folosi două instrucțiuni speciale: IN și OUT. IN este echivalent cu LDS, iar OUT este echivalent cu STS. Diferența este că, în vreme ce LDS/STS nu pot lucra decât cu registrele R16:R31, IN/OUT pot lucra cu toate cele 32 de registre. De asemenea, pe un core AVR adevărat, LDS/STS se execută în 2 cicli de ceas, iar IN/OUT într-unul singur. Astfel, codul precedent poate fi rescris:
LDS R16, 0x40 OUT 0x02, R16
Pe lângă instrucțiunile dedicate de load și store (denumite in
și out
), mai sunt și alte instrucțiuni ce accesează această zonă de memorie.
În primul rând, toate instrucțiunile aritmetice o vor accesa, fiindcă vor avea nevoie să citească/scrie în SREG
, registru ce este mapat la adresa 0x3F
în I/O space.
Address | Name | Bit 7 | Bit 6 | Bit 5 | Bit 4 | Bit 3 | Bit 2 | Bit 1 | Bit 0 |
---|---|---|---|---|---|---|---|---|---|
0x3F | SREG | I | T | H | S | V | N | Z | C |
În al doilea rând, toate instrucțiunile ce folosesc stiva vor accesa și ele I/O space, fiindcă stack pointer-ul este mapat de asemenea în I/O space, la adresele 0x3E
pentru SPH
și 0x3D
pentru SPL
. În datasheet-ul procesorului ATtiny20 se specifică faptul că doar SPL
este implementat pentru reduced core AVR.
Address | Name | Bit 7 | Bit 6 | Bit 5 | Bit 4 | Bit 3 | Bit 2 | Bit 1 | Bit 0 |
---|---|---|---|---|---|---|---|---|---|
0x3E | SPH | Stack pointer high byte | |||||||
0x3D | SPL | Stack pointer low byte |
Nu în ultimul rand, există instrucțiunile sbi
și cbi
(set/clear bit în I/O register), a căror funcționalitate o vom vedea în cele ce urmează. Să presupunem că vrem sa facem toggle la pinul cu indicele 3 de pe portul A:
while (1) { PORTA |= (1 << 3); PORTA &= ~(1 << 3); }
Am putea traduce secvența de mai sus în următorul cod assembly:
0x00: ldi R17, 0b00001000 0x01: ldi R18, 0b11110111 ; PORTA |= (1 << 3) 0x02: in R16, PORTA 0x03: or R16, R17 0x04: out PORTA, R16 ; PORTA &= ~(1 << 3) 0x05: in R16, PORTA 0x06: and R16, R18 0x07: out PORTA, R16 # Jump back to 0x00 # PC = PC + k + 1 => 0x00 = 0x08 + k + 1 => k = -0x09 0x08: rjmp -9
Așa cum se poate vedea, comportamentul de mai sus este unul de tip read-modify-write: pentru a modifica un singur bit dintr-un registru, el trebuie citit și scris înapoi prin instrucțiuni in
și out
separate. Pentru a economisi cicli de ceas (deci a mari frecvența maximă la care se poate face toggling pinilor de I/O) setul de instrucțiuni AVR beneficiază de instrucțiunile dedicate I/O sbi
(set bit in I/O register) și cbi
(clear bit in I/O register). Să vedem cum ar arăta programul de mai sus utilizându-le:
; PORTA |= (1 << 3) 0x00: sbi PORTA, 3 ; PORTA &= ~(1 << 3) 0x01: cbi PORTA, 3 ; jump back to 0x00 0x02: rjmp -3
<hidden Cod de inițializare a stivei în AVR> De asemenea, fiindcă tot am menționat stiva, într-un program AVR ea trebuie să aibă următorul cod de inițializare:
0x00: ldi R16, low(RAMEND) 0x01: out SPL, R16 0x02: ldi R16, high(RAMEND) 0x03: out SPH, R16
RAMEND
este un macro definit într-o bibliotecă standard AVR și indică ultima adresă valida din RAM (dacă ne uităm peste imaginea cu spațiul de adresă, vom descoperi că aceasta este 0xBF
). Din acest motiv pe reduced core AVR nu este necesar să implementăm decât SPL
, fiindcă SPH
ar fi întotdeauna zero.
</hidden>
Față de laboratoarele precedente, scheletul a fost modificat astfel încât decode_unit
să realizeze o decodificare completă a instrucțiunilor, și să exporte o serie de semnale de control către restul modulelor (mănunchiul de fire denumit signals
). Aceste semnale sunt de tipul one-hot și cuprind:
CONTROL_MEM_READ
, CONTROL_MEM_WRITE
: semnale către bus_interface_unit
care se pun pe 1 în momentul decodificării unui load/store, în funcție de starea curentă (în acest caz, în STATE_MEM
și, eventual, STATE_WB
în cazul în care este o citire, care durează 2 cicli de ceas)CONTROL_REG_RD_READ
, CONTROL_REG_RD_WRITE
, CONTROL_REG_RR_READ
: semnale către register_file_interface_unit
care îi semnalează, pe stările respective (citire pe STATE_ID
și STATE_EX
, respectiv scriere pe STATE_WB
), faptul că ar trebui să acceseze register file-ul.CONTROL_IO_READ
, CONTROL_IO_WRITE
: semnale de asemenea către bus_interface_unit
care sunt active în momentul în care a fost decodificată o instrucțiune de acces explicit la I/O (cum ar fi in
/out
, sbi
/cbi
, push
/pop
etc, dar în mod notabil nu lds
/sts
/ldd
/std
). În cazul unei instrucțiuni load/store, este datoria lui bus_interface_unit
să decidă dacă destinația comunicațiilor sale trebuie să fie memoria RAM de date sau memoria I/O.
Să exemplificăm comportamentul pe stări al instrucțiunii in Rd, A
:
STATE_IF
: procesorul trimite program_counter
pe linia de adrese a ROM-ului, așteaptă răspunsSTATE_ID
: primește răspuns de la ROM pe linia instruction
, îl bufferează în registrul instr_buffer
pe care îl forwardează către modulul decode_unit
. Aceasta ar trebui sa deducă următoarele:opcode_type
va fi TYPE_IN
opcode_rd
va fi decodificat din instrucțiuneopcode_imd
va fi decodificat din instrucțiune să ia valoarea Aopcode_group
va fi GROUP_IO_READ
signals
va avea următorii biți activați: CONTROL_IO_READ
, CONTROL_REG_RD_WRITE
bus_interface_unit
, cu semnalul CONTROL_IO_READ
primit de la unitatea de decodificare de instrucțiuni, sesizează că trebuie să citească date de la memoria I/O, așa că setează semnalele de control io_cs
, io_we
și io_oe
, iar pe linia addr
plasează valoarea opcode_imd
, așa cum este ea primită de la unitatea de decodificare (este fix A-ul din instrucțiune).STATE_EX
: operațiunea de citire din I/O memory se finalizează, iar pe linia data
, exportata și către control_unit
, se va afla valoarea din I/O memory, de la adresa A.STATE_MEM
: nu se întâmplă nimic notabil în această stareSTATE_WB
: register_file_interface_unit
depistează că instrucțiunea curentă necesită writeback în Rd, și poziționează semnalele de cs
, we
și oe
pentru o scriere, la adresa opcode_rd
conținută în instrucțiune, a datelor data
citite din memorie, similar cu instrucțiunea load
.
STATE_MEM
(cu prelungire în STATE_WB
pentru o citire), instrucțiunile de acces la I/O o accesează în același mod ca și pe registre: în STATE_ID
(cu prelungire în STATE_EX
) pentru o citire, respectiv în STATE_WB
pentru o scriere.
Doar astfel ne putem permite un read-modify-write într-o singură instrucțiune pe registre din I/O space, precum modificarea SP-ului la operațiile cu stiva, sau a SREG-ului la operațiile aritmetice.