0. Friendly reminders
Cum citim un datasheet? ATTiny20
Interpretare documentație instrucțiune
Interpretare documentație instrucțiune
Mai jos avem reprezentarea binară a instrucțiunii add R5, R3
:
Bit index | 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
Bit value | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 0 | 1 | 0 | 1 | 0 | 0 | 1 | 1 |
Bit significance | opcode | Rr | Rd | Rr |
Fiindcă register file-ul procesorului nostru (ATtiny20) are doar 16 registre, putem ignora biții Rr
si Rd
de pe pozițiile 9 și 8 (Atmel garantează că, din motive de compatibilitate, vor fi mereu setați pe 1, sau, echivalent, vor fi folosite numai registrele R16
→ R31
).
Așadar, instrucțiunile add R21, R19
și add R5, R3
sunt echivalente în implementarea noastră.
Mereu filtrați de la instrucțiunile cele mai particulare (cele care au partea constanta cea mai mare) până la cele mai generale (au partea constanta mai mica).
Adică? dintre opcode-ul 1) 0000_1111_1010_0101 și opcode-ul 2) 1111_0101_10rr_rddd mai particulara este 1.
Veți observa că în datasheet veți găsi opcode-uri pe 16 și pe 32 de biți (pentru instrucțiuni). În cadrul laboratoarelor de CN veți folosi doar instrucțiuni pe 16 biți, deoarece am ales să implementăm o versiune mai restrânsă a procesorului, care nu are acces la toate resursele sale. De exemplu: nu avem 32 de registre locale, ci doar 16.
1. Overview
1.1. Legătura memorie-microprocesor. System bus
Pentru a asigura comunicația dintre procesor, memorie și periferice le vom interconecta pe toate într-o topologie de tip magistrala. Aceasta este un mediu partajat de comunicație, în sensul că există un singur set de semnale (de control, date și adrese) la care au acces toate modulele conectate (spre deosebire de o topologie punct-la-punct unde există o legătură separată între fiecare modul care dorește să comunice cu altul).
Magistrala de sistem este, pe scurt, o magistrală care conectează procesorul de memoria principala și de periferice. Magistrala de sistem trebuie sa aibă, în general, 3 componente:
linii de control
linii de adrese
linii de date
Soluții conflicte pe magistrală
Soluții conflicte pe magistrală
În general, pentru comunicația mai multor module pe o magistrala, trebuie stabilit un protocol de comunicație (trebuie evitată situația în care există mai mulți vorbitori concomitenți pe aceeași linie, fiindcă acest gen de conflicte pot duce la scurtcircuite și alte probleme de natură electrică. În practică, există un bus master și mai mulți bus slaves. Nu întâmplător, în cazul nostru bus master va fi CPU-ul iar, momentan, singurul slave de pe system bus este memoria.
În sistemele desktop, system bus, adică o singură magistrala general-purpose, a fost demult înlocuita cu o ierarhie de magistrale (unele locale, altele externe) de viteze si lățimi de bandă diferite. Astfel, pentru asigurarea conectivității la memorie, un CPU x86 folosește un memory controller hub, care se ocupă de interfațarea propriu-zisă (generarea semnalelor de control) pentru memoria SDRAM conectată pe placa de bază.
Istoric, acest controller de memorie s-a aflat pe un chip extern procesorului, dar situat pe placa de baza în apropierea acestuia, numit Northbridge. Magistrala de mare viteza dintre CPU și Northbridge se numește Front-side bus (sau FSB, pe scurt) pentru sistemele Intel, sau EV6, pentru implementarea cu același rol de la AMD.
1.2. Moduri de adresare
Modurile de adresare a memoriei descriu metoda prin care este calculată adresa efectivă a datelor accesate, folosind informații constante și/sau conținute în registre. Pentru a fi mai flexibil pentru programatori un ISA va implementa mai multe moduri de adresare a memoriei, fiecare cu avantaje și dezavantaje. Aceste moduri pot fi folosite prin diferite instrucțiuni. Mai departe ne vom uita peste modurile de adresare a memoriei de date disponibile pe arhitectura AVR.
Spațiul de adrese
Figura precedentă descrie organizarea spațiului de adresă pentru ATTiny20. Memoria RAM ocupă o mică parte din acest spațiu de la adresa MEM_START_ADDR
= 0x40 la MEM_STOP_ADDR
= 0xBF. Astfel, memoria RAM este responsabilă de tratarea citirii/scrierii pentru orice adresă primită din acest interval.
1.2.1. Adresare directa
Adresa de memorie dorită se află chiar în corpul instrucțiunii. Un exemplu de instrucțiune ce folosește adresare directa este LDS (16-bit).
Biții marcați cu k
formează o constantă ce reprezintă adresa de memorie de unde sunt citite datele, iar aceste date sunt scrise în registrul Rd.
1.2.2. Adresare indirectă
Adresa de memorie dorită se află în registrul X, Y sau Z. O instrucțiune ce folosește adresare indirectă cu deplasament este LDD (i).
Conform datasheet-ului când ne referim la registrul X, Y sau Z ne referim de fapt la grupări de câte doua registre:
Registrul X
reprezintă concatenarea registrelor R27:R26
Registrul Y
reprezintă concatenarea registrelor R29:R28
Registrul Z
reprezintă concatenarea registrelor R31:R30
Există desigur și variații
Adresare indirectă cu offset:
Adresa de memorie dorită se află în registrul Y sau Z, la care se adaugă un deplasament găsit în corpul instrucțiunii. O instrucțiune ce folosește adresare indirectă cu deplasament este LDD (iv).
Biții marcați cu q
formează o constantă ce reprezintă deplasamentul ce trebuie adăugat adresei de memorie găsită în registrul Y sau Z. Datele sunt scrie în registrul Rd.
Adresare indirectă cu pre-decrementare
Adresa de memorie dorită se afla în registrul X, Y sau Z, care este decrementată înainte de a fi folosită. O instrucțiune ce folosește adresare indirectă cu deplasament este LDD (iii).
Adresare indirectă cu post-incrementare
Adresa de memorie dorita se afla în registrul X, Y sau Z, care este incrementata după ce este folosită. O instrucțiune ce folosește adresare indirectă cu deplasament este LDD (ii).
2.1 Memoria procesorului
Procesorul pe care îl implementăm la laborator are o arhitectura de tip Harvard. Asta înseamnă că vom avea 2 memorii separate pentru instrucțiuni și date.
Procesorul lucrează cu 16 registre “general purpose” de 8 biți. Teoretic există 32 registre, însă implementarea noastră nu va folosi niciodată primele 16. Astfel când scriem cod AVR vom putea folosi numai registrele din intervalul R16 → R31.
Totuși, după cum ați văzut în laboratorul trecut, nu vom lucra direct cu aceste registre, ci vom folosi registrul sursa Rr și registrul sursă/destinație Rd. Aceștia în spate adresează cele 16 registre. Puteți vedea acest lucru în sursa register_file.v în care este implementat un Dual Port SRAM. Acesta garantează accesul concomitent (într-un ciclu de ceas) atât la Rd, cât și la Rr.
Driver-ul de memorie pentru modulul dual_port_sram este reg_file_interface_unit definit în fișierul reg_file_interface_unit.v. Aici vom seta biții de CS, WE si OE, precum și liniile de date și adrese ce sunt necesare pentru lucrul cu memoria.
Accesul la o memorie SRAM cu magistrala de date multiplexată se face prin setarea următorilor biți:
CS : chip select stabilește dacă chip-ul este activ sau nu;
WE : write enable când este activ, se scriu date în RAM;
OE : output enable când este activ și nu se scrie în RAM, se citesc date din RAM;
În cazul memoriei Dual Port SRAM din laborator, vom avea acești biți atât pentru Rd, cât și pentru Rr (rr_cs, rd_cs, etc). Similar, vor exista linii de date și adrese distincte pentru fiecare dintre Rd si Rr.
Reluat: Veți observa în datasheet menționate o serie de registre index: X, Y și Z. Acestea corespund unor perechi de registre generale astfel: XH:XL (R27:R26), YH:YL (R29:R28) și ZH:ZL (R31:R30). În scheletul de cod, aceste registre index sunt definite ca macro-uri în fișierul defines.vh.
2.2 Semnale importante în scheletul de cod
Fata de laboratorul anterior, veți găsi o serie de semnale noi ce au fost adăugate unor modulelor procesorului nostru pentru a putea permite implementarea instrucțiunilor de lucru cu memoria. În această secțiune va fi explicat pe scurt rolul acestor semnale noi și utilitatea lor.
decode.v
reg_file_interface_unit.v
rr_addr & rd_addr - ieșirile modulului care spun ce adresă folosim pentru RD si RR pentru operația curenta. Valorile acestor adrese trebuie stabilite în funcție de tipul operației (scriere sau citire) dar și de modul în care aceasta codifica adresa (indirect, printr-un alt registru sau direct prin OPCODE). O astfel de ieșire este activă doar dacă bit-ul CS corespunzător uneia dintre cele doua este activ.
internal_rr_addr & internal_rd_addr - fire de “lucru” în cadrul modului folosite pentru a determina ce adresă se asignează pe ieșirile rr_addr respectiv rd_addr.
signals - un registru ai cărui biți reprezintă ce fel de operație (READ sau WRITE) se execută asupra memoriei sau registrelor (RD, RR sau MEM). Semnificația fiecărui bit din acest registru se poate găsi în defines.vh.
control_unit.v
writeback_value - valoarea ce trebuie salvată în memorie la finalul ultimului stagiu de pipeline. În funcție de tipul operației, trebuie asignat semnalul corespunzător. Spre exemplu, operațiile UAL atribuie ALU_OUT_BUFFER.
bus_data - valoarea aflata pe magistrala de data în urma ultimei operații de citire din memorie.
alu_rr - valoarea curenta stocata în registrul RR.
alu_rd - valoarea curenta stocata în registrul RD.
2.3 Instrucțiunile ATTiny pentru acces la memorie
Instrucțiunile pe care le veți implementa în acest laborator diferă putin de cele UAL pe care le-ați întâlnit pana acum. În aceasta secțiune va fi explicat pe scurt cum trebuie citit si interpretat datasheet-ul în cazul acestor instrucțiuni.
LDI - Fata de instrucțiunile obișnuite, LDI introduce un nou operator: K. K reprezinta o valoare constanta pe 8 biți ce este salvata în registrul RD.
LDD - Aceasta instrucțiune este un load indirect și folosește registrul special Y pentru a specifica adresa de memorie de la care se face citirea. În datasheet, veți găsi 4 codificări distincte pentru aceasta operație. Acest tip de LOAD poate modifica valoarea adresei înainte sau după accesarea efectiva a registrului. Cele 4 variante descrise in datasheet sunt:
LDD Rd, Y - in aceasta varianta, adresa din registrul Y este nemodificata în urma execuției
LDD Rd, Y+ - Post increment; adresa este incrementata după ce se face accesul
LDD Rd, -Y - Pre increment; adresa este decrementată înainte de a se face accesul
LDD Rd, (Y+q) - Displacement; adresa de la care se citește este specificata de registrul Y plus un deplasament (o constanta pe 6 biți codificata în instrucțiune)
LDS - Instrucțiune pentru load direct din spațiul de date în registru. Asemănător cu LDI, aceasta instrucțiune codifica o constanta k, însă aceasta are o semnificație cu totul diferita. Aici, k codifica adresa din spațiul de date de la care se va face operația de load. Formula pentru calculul acestei adrese o găsim în datasheet: ADDR[7:0] = (~INST[8], INST[8], INST[10], INST[9], INST[3], INST[2], INST[1], INST[0]). biții 10, 9, 8, 3, 2, 1, 0 din instrucțiune reprezinta biții ce îl codifica pe k.
STS - Scrie dintr-un registru la o adresa în memorie. Calculul adresei se face la fel ca cel pentru LDS.
MOV - Copiază valoarea unui registru într-un alt registru. Codificarea acestei instrucțiuni este similara cu cea a instrucțiunilor UAL.
Instrucțiunile prezentate pot fi grupate și după tipurile de memorie intre care transfera date, astfel:
REG-REG: MOV
MEM-REG: LDD, LDS, STS
REG-IMMEDIATE: LDI
Modurile de adresare ale memoriei sunt urmatoarele:
Adresare directa - adresa de memorie dorita se afla in corpul instructiunii
Adresare indirecta - adresa de memorie dorita se afla in registrul X,Y sau Z
Adresare indirecta cu pre-decrementare - ca mai sus, doar ca adresa este decrementata inainte de a fi folosita
Adresare indirecta cu post-incrementare - la fel ca mai sus, adresa de memorie este incrementata dupa ce este folosita
Semnale pentru implementarea instructiunilor de lucru cu memoria:
decode.v
reg_file_interface_unit.v
rr_addr si rd_addr - iesirile modulului care spun ce adresa RD si RR folosim pentru operatia curenta
internal_rr_addr si internal_rd_addr - determina ce adresa se asigneaza pe iesirile rr_addr si rd_addr
signals - un registru ai carui biti reprezinta ce operatie(READ sau WRITE) se executa asupra memoriei sau registrelor (RD,RR)
control_unit.v
writeback_value - valoarea ce trebuie salvata in memorie la finalul ultimului stagiu de pipeline
bus_data - valoarea aflata pe magistrala de date in urma ultimei operatii de citire din memorie
alu_rr - valoarea curenta stocata in registrul RR
alu_rd - valoarea curenta stocata in registrul RD
Instructiunile ATTiny pentru acces la memorie:
LDI - foloseste un operator, k, ce reprezinta o valoare constanta pe 8 biti salvata in registrul RD
LDD - load indirect, foloseste registrul special Y pentru a specifica adresa de memorie de la care se face citirea.
LDS - load direct din spatiul de date in registru. Codifica adresa din spatiul de date cu o constanta k
STS - scrie dintr-un registru la o adresa de memorie
MOV - copiaza valoarea dintr-un registru intr-un alt registru
Folosiți semnale cât mai generice. _ex. Verificați semnalele grupurilor de instrucțiuni in detrimentul tipurilor de instrucțiuni.
Checkerul este hard-codat în aceasta fază.
Pentru punctajul bonus, pe lângă implementarea exercițiilor 6 & 7, trebuie explicat fluxul de execuție al instrucțiunilor LD_Y și STS.
Task 1 (0.5p). Modificați rom.v
Generați codul mașină corespunzător acestor instrucțiuni folosind tool-ul AVR.
input.txt:
ldi r29, 10
sts 10, r29
ldi r29, 0
ldi r28, 138
ld r27, y
lds r28, 10
mov r27, r28
Task 2 (0.5p). Simulați checker_view.v
Unele zone sunt roșii, pentru a investiga ce mai trebuie implementat:
Din fereastra Scope, expandați până ajungeți la instanța _cpu;
Adaugați semnalele din fereastra Objects în fereastra cu semnale (rclick → Add to wave window);
Re-lansați simularea pentru a popula semnalele adăugate (Relaunch Simulation).
Task 3 (2p). Pentru LDI. Modificati decode_unit.v
Task 4 (3p). Pentru STS pe 16 biți. Modificați decode_unit.v
Atenție la calcularea adresei - adică ce valoare atribuim în opcode_imd. Memoria de date e mapată între 0x0040 și 0x00BF.
Pentru a scrie pe bus, trebuie să controlăm 2 semnale: signals[`CONTROL_REG_RR_READ] și signals[`CONTROL_MEM_WRITE] (signal_generation_unit.v).
Inspectați bus_interface_unit.v
Valoarea scrisă pe bus, este transmisă prin data_to_store din control_unit.v. În acest laborator va fi preluată din variabila de buffer pipeline alu_rr.
Task 5 (4p). Pentru LD_Y (i). Modificati decode_unit.v
Ultimele două semnale sunt active și pentru o adresare directă a memoriei? Modificați semnalele corespunzatoare în signal_generation_unit.v
Adresa indirectă este transmisă la bus_interface_unit prin indirect_address (o cuplare a alu_rr și alu_rd), inspectați control_unit.v
Valoarea citită din memorie (scrisă de memorie pe magistrală) este transmisă prin writeback_value din control_unit.v.
Task 6 (0.5p BONUS). Pentru LDS pe 16 biți. Modificati decode_unit.v (Atenție la calcularea adresei)
Pentru a citi de pe bus, trebuie să controlam 2 semnale: signals[`CONTROL_REG_RD_WRITE] și signals[`CONTROL_MEM_READ]
Inspectați bus_interface_unit.v.
Valoarea citită din memorie (scrisă de memorie pe magistrală) este transmisă prin writeback_value din control_unit.v.
Task 7 (0.5p BONUS). Pentru MOV. Modificati decode_unit.v
Greseli comune intalnite in laborator:
Ati setat opcode_rr in loc de opcode_rd.
Ati setat opcode_rd in loc de opcode_rr.
Ati calculat adresa directa considerand 7 biti, in loc de adresa specificata in datasheet. La LDS si STS cautati forma {~8, 8, 10, 9, 3, 2, 1, 0}.
Ati uitat sa setati grupurile in decode_unit.v.
Nu ati setat CONTROL_REG_RR_READ si CONTROL_REG_RD_READ pentru toate instructiunile care au nevoie de ele (de ce are nevoie LD Y sa citeasca 2 registre?).
LDI, STS(16-bit) și LDS(16-bit) folosesc doar 4 biti in loc de 5 biti pentru codificarea opcode_rr/opcode_rd. Totusi, aceste instructiuni pot opera doar pe registri R16-R31, asadar o decodificare corectă seteaza bitul cel mai semnificativ al lui opcode_rr/opcode_rd la valoarea 1.
Ati setat o valoare gresita in decode_unit.v
Nu ati adaugat toate semnalele la wave window. Nu ati inspectat unit testele sa vedeti exact care pas returneaza 1'bx, si ce semnale nu sunt pe valorile asteptate.