Όπως και στα προηγούμενα εργαστήρια συνεχίζουμε στο περιβάλλον του online chisel bootcamp.
Πριν ξεκινήσετε, εκτελέστε τα επόμενα 2 κελιά:
val path = System.getProperty("user.dir") + "/source/load-ivy.sc"
interp.load.module(ammonite.ops.Path(java.nio.file.FileSystems.getDefault().getPath(path)))
import chisel3._
import chisel3.util._
import chisel3.tester._
import chisel3.tester.RawTester.test
import dotvisualizer._
Για την υλοποίηση της τρέχουσας άσκησης θα χρειαστείτε από τα προηγούμενα εργαστήρια τα εξής:
Alu(n: Int)
(εργαστήριο 4, έχει παραδοθεί ως Εργασία 1)RegisterFile(register_number: Int, register_width: Int)
(εργαστήριο 7, έχει παραδοθεί ως Εργασία 2)Αντιγράψτε τα πιο πάνω modules από τα notebooks που έχετε ήδη παραδόσει στο opencourses στα επόμενα 2 κελιά.
class RegisterFile(register_number: Int, register_width: Int) extends Module {
// συμπληρώστε τον κώδικα από το δικό σας module
}
class Alu(n: Int) extends Module {
// συμπληρώστε τον κώδικα από το δικό σας module
}
Θα υλοποιήσετε νέο module DataPath
το οποίο θα συνδυάζει το module RegisterFile
(με register_number
καταχωρητές και εύρος λέξης register_width
bits) και το module Alu
(με εύρος λέξης register_width
bits).
Η υλοποίηση πρέπει να γίνει σύμφωνα με το επόμενο σχήμα:
Τα νέα σήματα στο σχήμα είναι το alu_a_sel
και το im
(για όλα τα υπόλοιπα σήματα ανατρέξτε στο αντίστοιχο εργαστήριο του Alu
ή του RegisterFile
).
Η λειτουργία του σήματος alu_a_sel
(εύρους 1 bit) είναι η ακόλουθη:
Όταν το alu_a_sel
είναι 0, τότε στην είσοδο a της Alu εισάγεται η λέξη της εισόδου im
(με εύρος register_width
bits).
Όταν το alu_a_sel
είναι 1, τότε στην είσοδο a της Alu εισάγεται η έξοδος out_a
του register file.
Στο επόμενο κελί υπάρχει ο βασικός σκελετός κώδικα για το module DataPath
για να συμπληρώσετε σύμφωνα με το προηγούμενο σχήμα. Παρατηρήστε πώς εισάγουμε instances των RegisterFile
και Alu
για χρήση μέσα στο module (προσυμπληρωμένος κώδικας):
val regfile = Module(new RegisterFile(register_number,register_width))
val alu = Module(new Alu(register_width))
α) Συνδέστε τις εισόδους του κεντρικού module (DataPath
) read_sel_a
, read_sel_b
και write_sel
στις αντίστοιχες εισόδους του υπο-module RegisterFile
(μεταβλητή regfile
). Π.χ. για το read_sel_a
θα γράψετε:
regfile.io.read_sel_a := io.read_sel_a
β) Συνδέστε με τον ίδιο τρόπο τις εισόδους του κεντρικού module (DataPath
) sel
και sub
στις αντίστοιχες εισόδους του υπο-module Alu
(μεταβλητή alu
).
γ) Οδηγήστε την έξοδο out
της Alu
στην είσοδο in
του Registerfile
:
regfile.io.in := alu.io.out
δ) Ομοίως, συνδέστε την είσοδο b
της Alu
με την έξοδο out_b
του RegisterFile
.
ε) Ο πολυπλέκτης (MUX) του σχήματος συνδέει την είσοδο a
της Alu
είτε με την είσοδο im
(όταν alu_a_sel
= 0) είτε με την έξοδο out_a
του RegisterFile
(όταν alu_a_sel
= 0). Συμπληρώστε την υπάρχουσα δομή when
για να υλοποιήσετε τη λειτουργία αυτή (δηλ. το σύνολο των μωβ συνδέσεων στο σχήμα).
Στο τέλος του κώδικα του module υπάρχει (προσυμπληρωμένη) η σύνδεση της εξόδου out
της Alu
με την έξοδο results
του DataPath
. Η έξοδος αυτή θα χρησιμοποιηθεί στη φάση αυτή για debugging, για να παρατηρούμε τα αποτελέσματα κατά την εκτέλεση των εντολών.
class DataPath(register_number: Int, register_width: Int) extends Module {
val io = IO(new Bundle {
// immediate value input
val im = Input(UInt(register_width.W))
// alu op selection inputs
val sel = Input(UInt(2.W))
val sub = Input(UInt(1.W))
// immediate (0) or regfileA (1) selection input
var alu_a_sel = Input(UInt(1.W))
// regfile read/write register selection inputs
val read_sel_a = Input(UInt(log2Ceil(register_number).W))
val read_sel_b = Input(UInt(log2Ceil(register_number).W))
val write_sel = Input(UInt(log2Ceil(register_number).W))
// alu output for debugging
val results = Output(UInt(register_width.W))
})
val regfile = Module(new RegisterFile(register_number,register_width))
val alu = Module(new Alu(register_width))
// (α)
regfile.io.read_sel_a := io.read_sel_a
// ...συμπληρώστε με όμοιο τρόπο για τα read_sel_b και write_sel...
// (β)
// ...συμπληρώστε με όμοιο τρόπο για τα sel και sub...
// (γ)
regfile.io.in := alu.io.out
// (δ)
// ...συμπληρώστε με όμοιο τρόπο τη σύνδεση μεταξύ b της Alu και out_b του RegisterFile...
// (ε)
when (io.alu_a_sel === 0.U){
alu.io.a := // ...συμπληρώστε...
}.otherwise{
alu.io.a := // ...συμπληρώστε...
}
// debug only
io.results := alu.io.out
}
Χρησιμοποιήστε τον κώδικα στο επόμενο κελί για να εκτελέσετε δύο δοκιμαστικές λειτουργίες (micro-instructions) σε ένα datapath με 8 καταχωρητές, με εύρος λέξης 16 bits.
(Για την εξήγηση του κώδικα δείτε το κείμενο που ακολουθεί μετά τη δοκιμή)
test(new DataPath(8,16)) { c =>
// σήματα εισόδου για τις δοκιμαστικές "λειτουργίες" (micro-instructions)
val cmdbits = List(Map("READ_SEL_A" -> 0.U, // λειτουργία Α: r1 = 333 or r0 (io.results = 333)
"READ_SEL_B" -> 0.U,
"ALU_A_SEL" -> 0.U,
"IM" -> 333.U,
"SEL" -> "b01".U,
"SUB" -> 0.U,
"WRITE_SEL" -> 1.U,
"RESULTS" -> 333.U),
Map("READ_SEL_A" -> 0.U, // λειτουργία Β: r0 = r0 or r1 (io.results = 333)
"READ_SEL_B" -> 1.U,
"ALU_A_SEL" -> 1.U,
"IM" -> 0.U,
"SEL" -> "b01".U,
"SUB" -> 0.U,
"WRITE_SEL" -> 0.U,
"RESULTS" -> 333.U)
)
for (bits <- cmdbits) {
c.io.read_sel_a.poke(bits("READ_SEL_A"))
c.io.read_sel_b.poke(bits("READ_SEL_B"))
c.io.alu_a_sel.poke(bits("ALU_A_SEL"))
c.io.im.poke(bits("IM"))
c.io.sel.poke(bits("SEL"))
c.io.sub.poke(bits("SUB"))
c.io.write_sel.poke(bits("WRITE_SEL"))
println(c.io.results.peek())
c.io.results.expect(bits("RESULTS"))
c.clock.step()
}
}
println("SUCCESS!!")
Στον έλεγχο χρησιμοποιούμε δομές και τύπους της γλώσσας Scala ως εξής:
Η μεταβλητή cmdbits
είναι μια λίστα (List) από Maps.
Στη Scala, List είναι μια ακολουθία που μπορούμε να διασχίσουμε από την μια άκρη στην άλλη (όχι όμως να πάρουμε το i-οστό στοιχείο). Στον κώδικα έχουμε ένα επαναληπτικό for (bits <- cmdbits) {...}
που σε κάθε επανάληψη αναθέτει στη μεταβλητή bits
ένα-ένα τα περιεχόμενα της λίστας cmdbits
. Συνεπώς, η μεταβλητή bits
σε κάθε επανάληψη του for είναι ένα διαφορετικό Map.
Map στη Scala είναι μια δομή «λεξικού» (κλειδιού/τιμής). Στον κώδικα που ακολουθεί παίρνουμε τις τιμές από κάθε Map και οδηγούμε ανάλογα τα αντίστοιχα σήματα εισόδου του DataPath. Στη συνέχεια δίνουμε έναν παλμό ρολογιού και τυπώνουμε/ελέγχουμε την έξοδο results
. Αυτό επαναλαμβάνεται για κάθε Map της λίστας.
Όπως φαίνεται και από τον αριθμό των Maps της λίστας cmdbits
, στον κώδικα εκτελούνται 2 λειτουργίες (micro-instructions).
Στο επόμενο κελί συμπληρώστε το περιεχόμενο της λίστας cmdbits
(θα έχει τρία Maps, όσα και οι ζητούμενες micro-instructions), έτσι ώστε να υλοποιηθούν οι εξής λειτουργίες:
r1 = 333 or r0 // το io.results πρέπει να είναι 333
r2 = 334 or r0 // το io.results πρέπει να είναι 334
r3 = r1 + r2 // το io.results πρέπει να είναι 667
Θυμηθείτε ότι μπορείτε να τροποποιήσετε στον κώδικα μόνο το περιεχόμενο της λίστας cmdbits
!
test(new DataPath(8,16)) { c =>
// προσθέστε τρία Maps για τις ζητούμενες λειτουργίες
val cmdbits = List(Map(...συμπληρώστε...),
Map(...συμπληρώστε...),
Map(...συμπληρώστε...)
)
// μην αλλάξετε τον κώδικα από εδώ και κάτω!
for (bits <- cmdbits) {
c.io.read_sel_a.poke(bits("READ_SEL_A"))
c.io.read_sel_b.poke(bits("READ_SEL_B"))
c.io.alu_a_sel.poke(bits("ALU_A_SEL"))
c.io.im.poke(bits("IM"))
c.io.sel.poke(bits("SEL"))
c.io.sub.poke(bits("SUB"))
c.io.write_sel.poke(bits("WRITE_SEL"))
println(c.io.results.peek())
c.io.results.expect(bits("RESULTS"))
c.clock.step()
}
}
println("SUCCESS!!")
Κατεβάσετε και πάρετε μαζί σας το σημερινό notebook, είναι απαραίτητο για τα επόμενα εργαστήρια).
Δεν ανήκει στα παραδοτέα του εργαστηρίου.