În acest laborator vom învăța ce este un procesor și un Instruction Set Architecture (ISA). Vom învăța cum funcționează arhitectura AVR (Harvard) și vom începe implementarea procesorului nostru prin a decodifica și executa câteva operații simple.
Microprocesorul
Microprocesorul este o componentă principală a unui sistem de calcul care are rolul de a executa instrucțiunile unui program. Instrucțiunile ce pot fi rulate de către un procesor sunt de mai multe tipuri:
Aritmetice/Logice
De control
De transfer de date
Procesoarele pentru sisteme încorporate (embedded) se găsesc în număr foarte mare în jurul nostru (electrocasnice, dispozitive portabile, automobile etc.).
Arhitecturi de microprocesoare
O caracteristică foarte importantă a unui procesor este numărul de biți folosiți pentru a reprezenta un număr întreg (lungimea unui cuvânt sau lățimea procesorului). Aceasta influențează câți biți pot fi citiți/scriși într-o singură operație de citire/scriere și, în general, câtă memorie poate fi adresată direct. Cele mai întâlnite arhitecturi din ziua de azi sunt pe 32 și pe 64 de biți, dar există și procesoare pe 4, 8, 12, 16 sau 24 de biți.
După organizarea memoriei de date/instrucțiuni avem următoarele arhitecturi:
Arhitectura Von Neumann | Arhitectura Harvard |
| |
Componente: o unitate de procesare, o memorie pentru instrucțiuni și date și unități de intrare/ieșire. | Componente: o unitate de procesare, o memorie de date, o memorie de program și unități de intrare/ieșire. |
Are o singură magistrală de adrese și o singură magistrală de date, pe care vor circula și date și instrucțiuni. | Are două magistrale de date și de adrese: una pentru instrucțiuni și una pentru date. |
Având o singură magistrală de date și instrucțiuni, acestea au aceeași dimensiune. | Magistralele de instrucțiuni și date pot avea dimensiuni diferite. |
Fiindcă instrucțiunile și datele se află în aceeași memorie, codul se poate auto-modifica. | Memoria de program poate fi facută non-volatila (odată scris un program acesta nu mai trebuie reîncărcat la fiecare pornire). |
Arhitectura Von Neumann pentru sisteme de calcul
Arhitectura Von Neumann pentru sisteme de calcul
Introdusă în anul 1945 de către matematicianul american John von Neumann, această arhitectură descrie un sistem de calcul ce conține o unitate de procesare, o memorie pentru instrucțiuni și date, și unități de intrare/ieșire. Astfel, un procesor pentru o astfel de arhitectura va avea o singura magistrală de adrese și o singură magistrală de date, pe care vor circula și date și instrucțiuni. Avantajele acestei arhitecturi sunt simplitatea și faptul că, fiindcă instrucțiunile și datele se află în aceeași memorie, codul se poate auto-modifica.
Arhitectura Harvard pentru sisteme de calcul
Arhitectura Harvard pentru sisteme de calcul
Spre deosebire de Von Neumann, arhitectura Harvard descrie un sistem de calcul unde memoria de date și memoria de program sunt separate. Asta înseamnă că procesoarele trebuie sa aibă două magistrale de date și de adrese: una pentru instrucțiuni și una pentru date. Avantajul este că aceste magistrale nu trebuie să aibă aceeași dimensiune, deci putem avea un procesor pe 8 biți care adresează mai mult de 256 de octeți de memorie. Un alt avantaj este ca memoria de program poate fi facută non-volatila, deci odată scris un program acesta nu mai trebuie reîncărcat la fiecare pornire.
Componentele unui microprocesor
Un procesor este format din patru componente principale:
Unitatea aritmetică/logică (UAL): execută operațiile aritmetice și logice.
Registre: cea mai mică unitate de stocare ce face parte dintr-un procesor, în general de dimensiunea unui cuvânt. Registrele care pot fi folosite pentru majoritatea operațiilor se numesc registre de uz general (e.g. AX, BX, CX, DX pe arhitectura x86). Totuși unele registre au un scop particular. Dintre acestea cele mai importante sunt Program Counter (PC), Instruction Register (IR), Stack Pointer (SP) și Flags Register (registru ce indică statusul curent al procesorului).
Porturi de intrare/ieșire (I/O): linii prin intermediul cărora procesorul se interfațează cu periferice (inclusiv cu memoria). Prin intermediul acestora el poate transmite sau recepționa date.
Unitatea de control (UC): decodifică instrucțiunea curentă și pe baza acesteia și a stării interne, generează semnale de control pentru toate resursele procesorului (e.g. registrele de intrare/ieșire și operația aritmetică necesare unei instrucțiuni) și coordonează activarea acestor resurse.
În funcție de arhitectura procesorului numărul și numele registrelor poate varia.
Microcontroller
Un microcontroller este un chip care conține un microprocesor, memorie de date, memorie de program și dispozitive periferice. Accentul în design se pune pe consum redus de energie și costuri mici de producție. De aceea, în general, rulează la frecvente reduse (zeci, sute de MHz). De asemenea pot fi specializate pe o anumită funcționalitate.
Majoritatea microcontrollerelor sunt formate din:
Unitatea centrală de procesare
RAM (memorie volatilă) și/sau Flash/EEPROM (memorie non-volatila)
Porturi de intrare/ieșire
Timere
Interfețe seriale și paralele
ATTiny20
ATTiny20 este un microcontroller produs de firma Microchip. El este construit pe arhitectura AVR și are următoarele caracteristici:
Arhitectura RISC
112 de instrucțiuni – majoritatea executate într-un singur ciclu de ceas
16 registre generale de 8 biți
2048 de octeți de memorie programabilă Flash
128 de octeți de memorie SRAM
12 pini de intrare/ieșire programabili
Frecvența de operare de până la 12
MHz
Tensiune de alimentare 1.8V - 5V
Arhitectura unui set de instrucțiuni
Orice microprocesor are definit un Instruction Set Architecture (ISA):
O interfață între hardware și software. Așa cum în OOP rolul unei interfețe este de a oferi o imagine de ansamblu, fără a oferi detalii de implementare, așa și aici ISA definește operațiile, accesul la memorie, modurile de stocare suportate de hardware, fără a da detalii de implementare.
Definește setul de instrucțiuni recunoscute de către procesor. Orice altă instrucțiune are efecte nedeterminate asupra procesorului.
Definește tipurile de date care pot fi recunoscute de procesor.
Definește contextul în care o instrucțiune operează.
Definește cum o serie de instrucțiuni trebuie să interacționeze între ele (propagare de flaguri, executare de salturi condiționale, etc).
O instrucțiune este reprezentată de un șir de biți, o parte dintre ei fiind codul operatiei. Instrucțiunile pot fi codificate pe un număr constant de biți sau să fie de dimensiune variabilă.
În funcție de ISA numărul, numele, codificarea și funcționarea instrucțiunilor poate varia.
Etapele executării unei instrucțiuni
Orice procesor poate avea un ciclu de prelucrare a instrucțiunilor diferit, în funcție de ISA-ul pe care îl implementează, însă toate vor urma următoarea structură:
IF (Instruction Fetch): următoarea instrucțiune este adusă din memorie de la adresa către care pointează registrul Program Counter (PC) și este stocată în registrul Instruction Register (IR). PC este apoi incrementat pentru a pointa către următoarea instrucțiune de încărcat.
ID (Instruction Decode): instrucțiunea din IR este decodificată.
EX (Execute): execuția efectivă a instrucțiunii. Aceasta etapă variază în funcție de tipul instrucțiunii curente.
MEM (Memory Access): ciclu folosit în cazul în care instrucțiunea accesează memoria.
WB (Write Back): scrierea noilor valori în registre.
Instrucțiuni AVR
Instrucțiunile pentru AVR sunt pe 16 biți sau 32 de biți. Deși procesorul este pe 8 biți, memoria este organizată în rânduri de 16 biți lungime, procesorul putând citi câte un rând la fiecare ciclu de ceas din memoria de program. Astfel, memoria noastră de 2048 de octeți este definită ca 1024 * 2 octeți, adică în 1024 de rânduri, fiecare rând având 2 octeți (16 biți). Exemple de instrucțiuni AVR:
Aritmetice/Logice
De control
De transfer de date
Mai sus puteți vedea un extras din definiția setului de instrucțiuni AVR. Observați numele instrucțiunii (ADD), descrierea funcționalității ei, o descriere matematică a funcționalității, sintaxa în limbaj de asamblare (AVRASM) și codul operației pe 16 biți.
Rd și Rr sunt nume generice date celor două registre folosite, în practică ambele pot fi oricare dintre registrele R0 - R31 (0 <= d <= 31, 0 <= r <= 31). Procesorul știe ce registre să folosească prin concatenarea biților marcați cu d si r din codul operației. Spre exemplu, codificarea operației ADD R16, R1 este 0000 1101 0000 0001.
Registrul SREG
Pentru a reține toate aceste evenimente se folosesc flag-uri
grupate în ceea ce se numește Processor Status Register
sau SREG
(este oarecum echivalentul registrului flags
din arhitectura x86).
Printre flag-urile des folosite din SREG se află:
Z
(Zero) - indică dacă rezultatul unei operații aritmetice este zero.
C
(Carry) - indică faptul că s-a realizat un transport la nivelul bitului cel mai semnificativ in cazul unei operații aritmetice.
V
(Overflow) - arată că, în cazul unei operații aritmetice cu semn, complementul față de doi al rezultatului nu ar încăpea în numărul de biți folosiți pentru a reprezenta operanzii. Mai simplu, ia valoarea 1 dacă:
adun 2 numere pozitive și obțin rezultat negativ, sau
adun 2 numere negative și obțin rezultat pozitiv
N
(Negative) - semnul rezultatului unei operații aritmetice.
S
(Sign) - este un flag unic AVR, calculat după formula S = N xor V
.
Exemplu:
Pentru operatia add 129, 177, încep prin a scrie numerele în binar:
129 = 1000 0001
177 = 1011 0001
și după le adun:
1000 0001 +
1011 0001
-------------
1 0011 0010
^
|
MSB
Verific ce flag-uri sunt active astfel:
Z = 0
→ rezultatul meu nu conține numai 0-uri (cei 8 biți pe care se scrie rezultatul)
C = 1
→ rezultatul final a depășit numărul de 8 biți (cât are procesorul nostru)
V = 1
→ MSB = 0, iar cele două numere adunate au ambele MSB = 1 (din două numere negative am obținut unul pozitiv)
N = 0
→ rezultatul este unul pozitiv (MSB = 0)
S = 1
→ S = N ^ V = 0 ^ 1 = 1
Z
(Zero) - indică dacă rezultatul unei operații aritmetice este zero
C
(Carry) - indică faptul că s-a realizat un transport la nivelul bitului cel mai semnificativ in cazul unei operații aritmetice. Altfel spus, a avut loc o depășire în aritmetica modulo N considerată. În procesorul nostru pe 8 biți, 255 + 1, deși ar trebui să aibă rezultatul 256, de fapt acesta este 0 din cauza aritmeticii modulo 256 (28). Pentru a diferenția dintre un 0 apărut real și unul cauzat de o depășire, se utilizează acest semnal de carry.
V
(Overflow) - arată că, în cazul unei operații aritmetice cu semn, complementul față de doi al rezultatului nu ar încăpea în numărul de biți folosiți pentru a reprezenta operanzii. Cu alte cuvinte, se poate întampla ca adunând două numere pozitive, să obținem unul negativ (127signed + 1signed = -128signed
), dar și adunând două numere negative să obținem unul pozitiv (-128signed + (-1)signed = +127signed
). Evident, rezultatul nu este corect în aceste situații, și semnalarea se face prin flag-ul de overflow. Dacă am aduna un număr pozitiv (MSB = 0) cu unul negativ (MSB = 1), atunci nu obțin overflow.
N
(Negative) - semnul rezultatului unei operații aritmetice (este pur și simplu valoarea bitului de semn al rezultatului)
S
(Sign) - este un flag unic AVR, calculat după formula S = N xor V
, ce reprezintă “care ar fi trebuit să fie semnul corect al rezultatului”. Cu alte cuvinte, dacă N == 1
, dar și V == 1
, înseamnă că rezultatul este negativ, dar din cauza unei depășiri. S
este setat în acest caz pe 0, semnalând că semnul “corect” al operației ar fi trebuit să fie pozitiv.
Scrierea biților cu valorile corespunzătoare din SREG
revine UAL-ului. La execuția unei operații, se calculeaza și valorile fiecărui flag ce poate fi afectat de acel tip de operație.
Practic, ca și la arhitectura x86, putem considera că SREG
este un registru global, a cărui valoare este setată de ultima instrucțiune aritmetico-logică executată de procesor.
În acest laborator vom implementa în Verilog o mică parte a unui procesor. La sfârșitul laboratorului procesul va fi capabil să execute instrucțiuniile din rom.v: nop
, neg
, add
, sub
, and
și or
.
nop
neg r1
add r1, r2
sub r1, r2
and r1, r2
or r1, r2
Folosiți
manualul setului de instrucțiuni AVR pentru a implementa codificările și decodificările comenzilor. Căutați în meniu capitolele aferente fiecărei instrucțiuni.
În scheletul de laborator sunt câteva fișiere de interes:
decode_unit.v se ocupă de decodificarea instrucțiunilor. Aici trebuie să adăugăm instrucțiunile noi.
control_unit.v implementează logica de control. Aici trebuie sa translatăm type în opcode.
alu.v execută operații aritmetice și logice. Aici trebuie calculate rezultatele operațiilor aritmetice.
rom.v conține codul ce va fi executat.
Dacă implementați complet instrucțiunile necesare, în urma simulării checker_view.v toate semnalele vor fi verzi. Codul se află într-o memorie ROM, așadar pentru orice schimbare în cod tot designul trebuie resimulat.
Task 00 Descărcați scheletul de laborator.
Următoarele task-uri necesită modificări în mai multe fișiere din arhivă. Căutați TODO-urile din fișierele decode_unit.v (pentru etapa de ID), alu.v (pentru etapa de EXEC), control_unit.v (pentru etapa de WB).
Task 01 (1p) Implementați instrucțiunea NOP.
Task 02 (1p) Implementați instrucțiunea NEG.
Task 03 (2p) Implementați instrucțiunea ADD.
Task 04 (2p) Implementați instrucțiunea SUB.
Task 05 (2p) Implementați instrucțiunea AND.
Task 06 (2p) Implementați instrucțiunea OR.