Laboratorul 06 - GPIO: Intrări & Ieșiri

În acest laborator vom învăța ce este GPIO, cum este implementat în microcontroller-ul ATTiny20 și îl vom adăuga implementării noastre a acestui controller.

1. Ce este GPIO?

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.

Atenție! Un pin configurat pentru citire care nu este comandat din exterior (este lăsat 'în aer'), nu va avea o valoare logică concretă de '0' sau '1', iar citirea lui ne va da oricare dintre aceste două valori.

1.1. Ce este un pin?

Un pin este o bucată de metal ce permite crearea unei legături între componente electrice. Î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.

ATtiny20

2. GPIO si ATTiny20

În datasheetul 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!

Observăm 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 - masă (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 microcontrollerele 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.

2.1. Registrele de control al perifericelor și ale porturilor I/O

În cadrul microprocesoarelor ca ATTiny20, avem de-a face cu două tipuri de registre de control:

  • Registre de control a modulelor periferice (e.g. timer, convertor analog-digital, controller USB, placă de rețea, placă video, tastatura, etc.)
  • Registre de control a pinilor de intrare/ieșire (pinii GPIO - ținta noastră în acest laborator)

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:

  • DDRx: Data Direction Register (i.e.: DDRA și DDRB),
  • PORTx: Data Register (i.e.: PORTA și PORTB),
  • PINx: Data Input Register (i.e.: PINA și PINB).

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 microncontrollerului.

Schimbând un bit din 1 în 0 sau invers putem schimba comportamentul pinului. Astfel, registrele menționate anterior au următoarea funcționalitate:

DDRx

Registrul DDRx controleaza direcția pinilor din portul x:

  • DDRxn == 0: dacă bitul n este 0 atunci pinul respectiv va funcționa ca și intrare (microcontroller-ul nu va impune nicio tensiune pe linie, ci îl va lăsa în stare de înaltă impedanță, Z)
  • DDRxn == 1: dacă bitul n este 1 atunci pinul va funcționa ca și ieșire (microcontroller-ul va putea impune o tensiune electrică pe acel pin)
PORTx

Registrul PORTx controlează valoarea pinilor din portul x care au fost configurați ca ieșiri:

  • PORTxn == 0: dacă bitul n este 0 atunci pinul respectiv va lua valoarea LOW
  • PORTxn == 1: dacă bitul n este 1 atunci pinul respectiv va lua valoarea HIGH
PINx

Din registrul PINx putem citi valoarea valoarea pinilor din portul x care au fost configurați ca intrări:

  • PINxn == 0: dacă bitul n este 0 atunci pinul respectiv are valoarea LOW
  • PINxn == 1: dacă bitul n este 1 atunci pinul respectiv are valoare HIGH

Un bit este 0 atunci când pinul corespunzător acestuia ia valoarea LOW (adică GND, care este în general 0V), iar dacă bitul este 1 atunci când pinul ia valoarea HIGH (adică VCC, care poate fi 5V, 3.3V, etc.). Vedeți tabelul 10-1 (pag. 44) din datasheet pentru detalii despre ce se întamplă când DDAn e setat ca port de intrare.

2.2. Spațiul de adrese

Organizarea spatiului de adresa pe ATTiny20

Figura precedentă descrie organizarea spațiului de adresă pentru ATTiny20.

Despre zonele din spațiul de adrese

Despre zonele din spațiul de adrese

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 că 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 IDul.

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
LDS R16, 0x00    ; se încarcă date de la adresa 0x00, adică din registrul PINA

2.3. Instrucțiuni ce operează cu I/O Space

Instrucțiunile IN și OUT

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 ; valoarea din registrul ''R16'' ajunge la adresa 0x02 (registrul PORTA) - este scrisă la **ieșire**
IN R16, 0x00  ; valoarea de la adresa 0x00 (registrul PINA) este încărcată în ''R16'' - este citită de la **intrare**
Instrucțiunile SBI și CBI

Deoarece instrucțiunile IN și OUT nu pot scrie decât valori pe 8 biți. Spre exemplu, dacă dorim să schimbăm doar bitul 5 din DDRA în 1 atunci pașii pe care trebuie sa îi urmăm sunt:

LDI     R17, 0b00100000 ; încărcăm în R17 o mască ce are 1 pe bitul 5
IN      R16, DDRA       ; încărcăm în R16 valoarea din DDRA
OR      R16, R17        ; în urma operației sau între R16 și R17 în R16 bitul 5 va fi pus pe 1
OUT     DDRA, R16       ; stocăm valoarea din R16 înapoi în DDRA

Pentru a putea altera un singur bit din registrul corespunzător unui port folosind instrucțiunile OUT, trebuie să folosim o mască pe 8 biți cu care vom aplica operația & (resetare valoare) sau | (setare valoare), după caz.

Pentru a pune bitul n pe 1 trebuie să folosim o mască de biți ce are 1 pe poziția n și 0 în rest (eg: pentru a pune bitul 5 pe 1 folosim masca 00100000), apoi să folosim operațiu sau pe biți.

Pentru a pune bitul n pe 0 trebuie să folosim o mască de biți ce are 0 pe poziția n și 1 în rest (eg: pentru a pune bitul 5 pe 0 folosim masca 11011111), apoi să folosim operațiu și pe biți.

Fiindcă acțiunea de a schimba un bit în 1 (set) sau în 0 (clear) este una întâlnită des în programele ce rulează pe arhitectura AVR aceasta include două instrucțiuni ce permit schimbarea unui bit dintr-un registru ce se află în primele 32 de adrese din I/O Space în 0 sau în 1: CBI (Clear Bit in Register) și SBI (Set Bit in Register). Ele primesc ca și argumente adresa registrului (în intervalul [0:31]) și numărul bitului (în intervalul [0:7]). Dacă rescriem codul folosind aceste înstrucțiuni el devine:

SBI DDRA, 5 ; mai scurt, nu?

Instrucțiunile CBI și SBI primesc ca și argument numărul bitului pe care dorim să-l schimbăm, nu o mască de biți.

Exemplu de cod

Exemplu de cod

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, putem folosi 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

TL;DR

Folosind GPIO (General-Purpose Input/Output) putem controla un pin al unui circuit integrat. De exemplu, putem aprinde un led folosind instrucțiuni AVR sau putem citi starea unui pin - LOW sau HIGH. Pinii ce suportă GPIO sunt grupați în porturi (grupuri de 8 pini logici numite A sau B) controlate de registre speciale (pe câte 8 biți):

  1. DDRx: Data Direction Register
    • un bit setat pe 0 indică faptul ca va fi folosit pentru 'citire'
    • un bit setat pe 1 indică 'scriere'
  2. PORTx: Data Register: (Stabilesc cum vreau să fie un pin)
    1. Dacă DDRx e setat ca ieșire ('scriere')
      • dacă un bit n este 0 atunci pinul respectiv va fi legat la LOW (GND)
      • dacă un bit n este 1 atunci pinul respectiv va fi legat la HIGH (VCC)
    2. Dacă DDRx e setat ca intrare ('citire')
  3. PINx: Data Input Register (Indică starea reală a unui pin)
    • daca un bit este 0 atunci pinul respectiv are valoarea LOW
    • daca un bit este 1 atunci pinul are valoare HIGH
  • Unde x poate fi A sau B

În memoria microcontrollerului de la 0x00 la 0x3F gasim I/O Space. Aici se află toate registrele de lucru cu periferice și, desigur, DDRx, PORTx și PINx. De exemplu registrul PORTA are adresa 0x02.

Pentru a scrie citi date din I/O Space putem folosi instrucțiunile LDS/STS, însă recomandat este să folosim instrucțiunile speciale pentru lucru de memoria I/O Space ce se execută mai rapid:

  • OUT A, Rr: Scrie (Store) în I/O Space la adresa A valoarea din registrul Rr
  • IN Rd, A : Citește (Load) din I/O Space de la adresa A și pune în registrul Rd

Pentru a modifica un singur bit dintr-un registru de configurare putem folosi instrucțiunile SBI/CBI.

Exerciții

Sugestii laborator

Sugestii laborator

Signals

Sunt o serie de semnale de control (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 - eventual și în 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, 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.

Instrucțiunea IN

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ăspuns
  • STATE_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țiune
    • opcode_imd va fi decodificat din instrucțiune să ia valoarea A
    • opcode_group va face parte din GROUP_IO_READ
    • signals va avea următorii biți activați: CONTROL_IO_READ, CONTROL_REG_RD_WRITE
  • Tot în acest ciclu, 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ă stare
  • STATE_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.

Așa cum am văzut mai sus, în timp ce instrucțiunile load/store accesează memoria pe ciclul 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.

Instrucțiunea SBI

Să exemplificăm comportamentul pe stări al instrucțiunii SBI PORTA, N:

  • STATE_ID: Similar cu instrucțiunea in, doar că în loc de A, opcode_imd va avea valoarea PORTA (adresa registrului de control PORTA). Mai mult, operațiunea de citire din I/O memory se finalizează tot în acest ciclu, iar valoarea din I/O memory, de la adresa PORTA, este legată la intrarea UAL, împreună cu masca necesară schimbării bit-ului N.
  • STATE_EX: Se calculează noua valoare a registrului de control. Imediat ce aceasta este obținută, este legată pe linia de writeback_value pentru a fi scrisă înapoi în memorie. Scrierea se realizează tot în această etapă, fiind declanșată de semnalul GROUP_ALU_AUX.

GROUP_ALU_AUX este semnalul activat de instrucțiunile care folosesc UAL pentru calculul unor valori auxiliare.

Ce trebuie modificat?
Pentru rezolvarea exercițiilor 1 - 4 trebuie să modificați următoarele fișiere:

  • bus_interface_unit.v
  • decode_unit.v
  • signal_generation_unit.v
  • control_unit.v
  • gpio.v

Checker
Scheletul conține un checker minimal doar pentru exercițiile 1 - 4. Simulați fișierul unitTestCpu pentru a-l utiliza.
La finalul rulării nu ar trebui să aveți niciun test picat (FAILED), iar la sfârșitul programului din rom.v în R17 ar trebui să fie valoarea 2.

Task 01 (2p) Implementați logica de selecție a modulului de RAM și a celui de GPIO. Urmăriți comentariile marcate cu TODO 1 din modulul bus_interface_unit.v .

Task 02 (2p) Extindeți modulul GPIO astfel încât să suporte și intrări și ieșiri. Urmăriți comentariile marcate cu TODO 1 și TODO 2 din gpio.v.

Task 03 (3p) Adăugați logica necesară decodificării și executării instrucțiunilor IN și OUT. Urmăriți comentariile marcate cu TODO 3 din decode_unit.v, signal_generation_unit.v și control_unit.v.

Task 04 (3p) Decodificați instrucțiunile SBI și CBI și completați logica necesară execuției lor. Urmăriți comentariile marcate cu TODO 4 din signal_generation_unit.v, decode_unit.v și control_unit.v.

[BONUS] Task 05 (1p) Scrieți un program care setează portul A ca intrare, portul B ca ieșire, apoi, într-o buclă, citește valoarea de pe portul A și o scrie pe portul B. Folosiți tool-ul avrasm pentru a-l scrie în fișierul rom.v. Incarcati codul pe placuta de laborator.

Pentru a putea incarca codul pe placuta este necesar sa comentati prima linie din defines.vh. Macroul de DEBUG este util doar pentru simulare, nu vom atribui pe placuta toate semnalele de debug.

Resurse

Schelet de cod

[0] AVR Instruction Set

[1] ATTiny 20 Datasheet

[2] Wikipedia: Atmel AVR instruction set

[3] Tool generare cod masina AVR
Pentru a folosi tool-ul de generare al codului masina AVR, pentru windows descărcați Java pentru cmd.

apm/laboratoare/06.txt · Last modified: 2024/02/29 15:06 (external edit)
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0