Όπως και στα προηγούμενα εργαστήρια συνεχίζουμε στο περιβάλλον του 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._
Στο εργαστήριο αυτό θα κάνετε προσθήκες στα υπάρχοντα modules για να υλοποιήσετε την εντολή beq
(διακλάδωση σε περίπτωση ισότητας):
beq Rsrc1,Rsrc2,Offset
όπου είναι:
Rsrc1
: αριθμός καταχωρητή 1 (3 bits)Rsrc2
: αριθμός καταχωρητή 2 (3 bits)Offset
: σταθερά διακλάδωσης (ακέραιος με πρόσημο, 7 bits)Η εντολή αυτή θα έχει την εξής λειτουργικότητα:
εάν το περιεχόμενο του Rscr1 ισούται με το περιεχόμενο του Rsrc2
τότε στην επόμενη τιμή του program counter προστίθεται το Offset
Η κωδικοποίηση της εντολής αυτής έχει ως εξής:
bits 15-13 |
bits 12-9 |
bits 8-6 |
bits 5-3 |
bits 2-0 |
---|---|---|---|---|
010 |
Offset[6:3] |
Rsrc1 |
Rsrc2 |
Offset[2:0] |
Η υλοποίηση θα γίνει βηματικά, μέσα από 4 ασκήσεις:
Άσκηση 1: Θα τροποποιήσετε το module DataPath
έτσι ώστε να προσθέσετε μία έξοδο zero
, η οποία θα είναι αληθής όταν το αποτέλεσμα της ALU είναι 0.
Άσκηση 2: θα υλοποιήσετε ένα νέο module BranchLogic
το οποίο θα ρυθμίζει το πώς και πότε θα γίνει διακλάδωση.
Άσκηση 3: εδώ θα προσθέσετε στο DecodeUnit
τα σήματα ελέγχου για τη λειτουργικότητα την νέας εντολής beq
.
Άσκηση 4: τελική διασύνδεση των νέων σημάτων στο συνολικό module Cpu
.
Στην άσκηση αυτή θα τροποποιήσετε το module DataPath
έτσι ώστε να προσθέσετε μία έξοδο zero
, η οποία θα είναι αληθής όταν το αποτέλεσμα της τρέχουσας πράξης στην ALU είναι 0.
Αρχικά, αντιγράψτε στο επόμενο κελί τον κώδικα του module RegisterFile
από το προηγούμενο εργαστήριο (χωρίς αλλαγές):
class RegisterFile(register_number: Int, register_width: Int) extends Module {
// αντιγράψτε από προηγούμενα
}
Στη συνέχεια αντιγράψτε στο επόμενο κελί τον κώδικα του module Alu
από το προηγούμενο εργαστήριο (ομοίως χωρίς αλλαγές):
class Alu(n: Int) extends Module {
// αντιγράψτε από προηγούμενα
}
Τέλος, αντιγράψτε τον κώδικα του module Datapath
από το προηγούμενο εργαστήριο στο επόμενο κελί και εκτελέστε τις εξής αλλαγές:
Προσθέστε την έξοδο zero
(1 bit) στο bundle io
:
// zero output (1 when results = 0)
val zero = Output(UInt(1.W))
Προσθέστε την αντίστοιχη λειτουργικότητα της εξόδου io.zero
:
«όταν το alu.io.out είναι 0.U τότε το io.zero θα είναι 1.U (και αντίστροφα)»
class DataPath(register_number: Int, register_width: Int) extends Module {
// αντιγράψτε από προηγούμενα και τροποποιήστε σύμφωνα με τις οδηγίες
}
Δοκιμάστε το νέο DataPath
μέσω του επόμενου κώδικα που εκτελεί ορισμένες λειτουργίες και ελέγχει αν το zero είναι το αναμενόμενο:
test(new DataPath(8,16)) { c =>
// σήματα εισόδου για τις δοκιμαστικές "λειτουργίες" (micro-instructions) και την αναμενόμενη έξοδο "zero"
val cmdbits = List(Map("READ_SEL_A" -> 0.U, // λειτουργία: r1 <- 256
"READ_SEL_B" -> 0.U,
"ALU_A_SEL" -> 0.U,
"IM" -> 256.U,
"SEL" -> "b01".U,
"SUB" -> 0.U,
"WRITE_SEL" -> 1.U,
"zero" -> 0.U), // η έξοδος zero πρέπει να είναι 0 (ψευδής)
Map("READ_SEL_A" -> 1.U, // λειτουργία: r0 <- r1 - r1
"READ_SEL_B" -> 1.U,
"ALU_A_SEL" -> 1.U,
"IM" -> 0.U,
"SEL" -> "b11".U,
"SUB" -> 1.U,
"WRITE_SEL" -> 0.U,
"zero" -> 1.U) // η έξοδος zero πρέπει να είναι 1 (αληθής)
)
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"))
// έλεγχος αναμενόμενης τιμής εξόδου zero
c.io.zero.expect(bits("zero"))
// μετάβαση στην επόμενη λειτουργία
c.clock.step()
}
}
println("SUCCESS!!")
Το νέο module BranchLogic
θα έχει 4 εισόδους:
branch
(εύρος 1 bit): αν είναι 1, η εκτελούμενη εντολή είναι εντολή διακλάδωσης, αλλιώς 0.zero
(εύρος 1 bit): αν είναι 1, η τρέχουσα πράξη στην ALU έδωσε μηδενικό αποτέλεσμα, σε αντίθετη περίπτωση 0.branch_offset
(εύρος 16 bits): αν πρέπει να γίνει διακλάδωση, η τιμή αυτή πρέπει να προστεθεί στον επόμενο program counter (pc).next_pc
(εύρος 16 bits): η τιμή του επόμενου pc. Σε αυτή την τιμή θα προστεθεί το branch_offset
.Οι έξοδοι θα είναι 2:
pc_sel
(εύρος 1 bit): εάν και η είσοδος branch
και η είσοδος zero
είναι 1, τότε το pc_sel
θα είναι 1 (δηλ. και η εντολή είναι διακλάδωση υπό συνθήκη και η συνθήκη είναι αληθής, άρα η διακλάδωση πρέπει να εκτελεστεί). Σε κάθε άλλη περίπτωση το pc_sel
θα είναι 0.branch_pc
(16 bits): θα περιέχει τη νέα τιμή του pc σε περίπτωση διακλάδωσης (δηλ. branch_pc = next_pc + branch_offset
).Συμπληρώστε τη λειτουργικότητα του module BranchLogic
στο επόμενο κελί:
class BranchLogic extends Module {
val io = IO(new Bundle {
// συμπληρώστε εισόδους/εξόδους σύμφωνα με την περιγραφή
})
// συμπληρώστε τη ζητούμενη λειτουργικότητα
}
Δοκιμάστε το BranchLogic
εκτελώντας το επόμενο κελί.
test(new BranchLogic) { c =>
// δοκιμή των 4 δυνατών συνδυασμών των εισόδων branch και zero
c.io.branch.poke(0.U) // branch = 0 και zero = 0 -> pc_sel πρέπει να είναι 0
c.io.zero.poke(0.U)
c.io.pc_sel.expect(0.U)
c.io.branch.poke(0.U) // branch = 0 και zero = 1 -> pc_sel πρέπει να είναι 0
c.io.zero.poke(1.U)
c.io.pc_sel.expect(0.U)
c.io.branch.poke(1.U) // branch = 1 και zero = 0 -> pc_sel πρέπει να είναι 0
c.io.zero.poke(0.U)
c.io.pc_sel.expect(0.U)
c.io.branch.poke(1.U) // branch = 1 και zero = 1 -> pc_sel πρέπει να είναι 1
c.io.zero.poke(1.U)
c.io.pc_sel.expect(1.U)
// δοκιμή εάν η έξοδος branch_pc ισούται με branch_offset + next_pc
c.io.next_pc.poke(1.U)
c.io.branch_offset.poke(2.U)
c.io.branch_pc.expect(3.U) // 1+2
c.io.next_pc.poke("b1111111111111111".U)
c.io.branch_offset.poke(1.U)
c.io.branch_pc.expect(0.U) // 65535+1 (ισούται με 0 στα 16 bits)
}
println("SUCCESS!!")
Εδώ θα προσθέσετε στο DecodeUnit
την αποκωδικοποίηση των σημάτων ελέγχου για την υλοποίηση της λειτουργικότητας της νέας εντολής beq
.
Οι λεπτομέρειες της εντολής beq
επαναλαμβάνονται στη συνέχεια για να μην ανατρέχετε συνέχεια στην αρχή του notebook:
Εντολή:
beq Rsrc1,Rsrc2,Offset
Λειτουργικότητα:
if Rscr1 - Rsrc2 == 0 (δηλαδή περιεχόμενο Rscr1 == περιεχόμενο Rsrc2) then pc <- next pc + Offset
Rsrc1
: καταχωρητής εισόδου δεδομένων 1 (3 bits)Rsrc2
: καταχωρητής εισόδου δεδομένων 2 (3 bits)Offset
: σταθερά διακλάδωσης (ακέραιος με πρόσημο, 7 bits), προστίθεται στον next pcΚωδικοποίηση:
bits 15-13 |
bits 12-9 |
bits 8-6 |
bits 5-3 |
bits 2-0 |
---|---|---|---|---|
010 |
Offset[6:3] |
Rsrc1 |
Rsrc2 |
Offset[2:0] |
Αντιγράψτε στο επόμενο κελί τον κώδικα του module DecodeUnit
από το προηγούμενο εργαστήριο και εκτελέστε τις ακόλουθες τροποποιήσεις:
Προσθέστε τις εξόδους branch
και branch_offset
(1 και 16 bits αντίστοιχα) στο bundle io
:
// outputs for branch logic
val branch = Output(UInt(1.W))
val branch_offset = Output(UInt(16.W))
Όπως έχει ήδη αναφερθεί, η έξοδος branch
θα είναι 1 όταν εκτελείται εντολή διακλάδωσης ενώ η έξοδος branch_offset
θα παρέχει τη σταθερά (ακέραιος αριθμός με πρόσημο) που θα προστεθεί στην τιμή του επόμενου pc.
Στις αναθέσεις των default values στα σήματα ελέγχου, δώστε default τιμή στο io.branch
(θα πρέπει να είναι 0.U) ενώ για το io.branch_offset
γράψτε το επόμενο που α) εξάγει από την εντολή (από δύο μέρη) τα bits του Offset, β) τα συνενώνει μέσω του Cat()
σε λέξη των 7 bits και γ) επεκτείνει (με πρόσημο) τη λέξη στα 16 bits:
io.branch := 0.U
io.branch_offset := Cat(io.instruction(12,9),io.instruction(2,0)).asSInt.pad(16).asUInt
Προσθέστε έναν κλάδο στο switch
για να θέσετε τα σήματα της εντολής beq
που διαφέρουν από τα default values:
is("b010".U) { // beq (branch if equal)
// προσθέστε εδώ μόνο τα σήματα που διαφέρουν από τα default values
}
Θυμηθείτε ότι η ζητούμενη λειτουργικότητα είναι r0 <- Rsrc1 - Rsrc2 (θα πρέπει να θέσετε τις κατάλληλες τιμές στα σήματα ελέγχου έτσι ώστε να εκτελεστεί αφαίρεση του Rsrc2 από τον Rsrc1, με το αποτέλεσμα να μην αποθηκεύεται - δηλ. να πηγαίνει στον r0).
Ταυτόχρονα, το io.branch
πρέπει να γίνει 1.U
.
class DecodeUnit extends Module {
// αντιγράψτε από προηγούμενα και τροποποιήστε σύμφωνα με τις οδηγίες
}
Ελέγξτε την ορθότητα των αλλαγών και νέων σημάτων με το ακόλουθο:
test(new DecodeUnit) { c =>
// δοκιμή εντολής αριθμητικής-λογικής πράξης, η έξοδος branch πρέπει να είναι 0 (ψευδής)
c.io.instruction.poke("b0000011001010011".U) // r3 = r1 + r2
c.io.branch.expect(0.U)
// δοκιμή εντολής ανάθεσης σταθεράς σε καταχωρητή, η έξοδος branch πρέπει να είναι 0 (ψευδής)
c.io.instruction.poke("b0010000100001001".U) // r1 = 33
c.io.branch.expect(0.U)
// δοκιμή εντολής beq
c.io.instruction.poke("b0100000001010100".U) // if r1 == r2 then pc += 4
c.io.branch.expect(1.U) // η έξοδος branch πρέπει να είναι 1 (αληθής)
c.io.branch_offset.expect("b0000000000000100".U) // το branch_offset πρέπει να είναι +4
c.io.alu_a_sel.expect(1.U) // το alu_a_sel πρέπει να είναι 1 (πράξη μεταξύ καταχωρητών)
c.io.read_sel_a.expect(1.U) // το read_sel_a πρεπει να είναι 1 (Rscr1 = r1)
c.io.read_sel_b.expect(2.U) // το read_sel_b πρεπει να είναι 2 (Rscr2 = r2)
c.io.sel.expect("b11".U) // το sel πρέπει να είναι b11 (+/-)
c.io.sub.expect(1.U) // το sub πρέπει να είναι 1 (αφαίρεση)
c.io.write_sel.expect(0.U) // το write_sel πρέπει να είναι 0 (προορισμός = r0)
// δοκιμή επέκτασης προσήμου για το branch_offset
c.io.instruction.poke("b0101111001010111".U) // if r1 == r2 then pc += -1
c.io.branch_offset.expect("b1111111111111111".U) // ο αριθμός -1 επεκταμένος στα 16 bits
}
println("SUCCESS!!")
Τελική διασύνδεση των νέων σημάτων στο συνολικό module Cpu
.
Αντιγράψτε (χωρίς αλλαγές) τον κώδικα του module InstructionMemory
από το προηγούμενο εργαστήριο στο επόμενο κελί:
class InstructionMemory(addr_width: Int, instr_width: Int, content: Seq[UInt]) extends Module {
// αντιγράψτε από προηγούμενα
}
Αντιγράψτε (επίσης χωρίς αλλαγές) τον κώδικα του module FetchUnit
από το προηγούμενο εργαστήριο στο επόμενο κελί:
class FetchUnit(addr_width: Int, instr_width: Int, content: Seq[UInt]) extends Module {
// αντιγράψτε από προηγούμενα
}
Τέλος, αντιγράψτε από το προηγούμενο εργαστήριο τον κώδικα του module Cpu
στο επόμενο κελί και στη συνέχεια προσθέστε τα εξής:
Δημιουργήστε ένα instance του module BranchLogic
μέσα στο Cpu
:
val bLogic = Module(new BranchLogic)
Συνδέστε μεταξύ τους τα νέα σήματα που αφορούν την εντολή beq
:
Τα νέα σήματα που πρέπει να προτεθούν φαίνονται στο ακόλουθο σχήμα με κόκινο χρώμα:
dPath
και bLogic
: το σήμα zero
.dUnit
και bLogic
: τα σήματα branch
και branch_offset
.fUnit
και bLogic
: τα σήματα next_pc
και (στην αντίθετη κατεύθυνση) pc_sel
και branch_pc
.class Cpu(instructions: Seq[UInt]) extends Module {
// αντιγράψτε από προηγούμενα και τροποποιήστε σύμφωνα με τις οδηγίες
}
Δοκιμάστε την νέα εκδοχή της Cpu
εκτελώντας το ακόλουθο πρόγραμμα που βρίσκει το άθροισμα από 1 έως 10:
val instructions = List("b0010000001010001".U, // r1 = 10 (limit = 10)
"b0010000000000010".U, // r2 = 0 (sum = 0)
"b0010000000001011".U, // r3 = 1 (counter = 1)
"b0010000000001100".U, // r4 = 1 (constant 1)
"b0000011010011010".U, // r2 = r2 + r1 (sum = sum + counter)
"b0000011011100011".U, // r3 = r3 + r4 (counter++)
"b0000111001100001".U, // r1 = r1 - r4 (limit--)
"b0100000001000001".U, // beq r1,r0,+1 (if limit==0 then pc <- (pc+1)+1)
"b0101111000000011".U, // beq r0,r0,-5 (pc <- (pc+1)-5)
"b0000001010000000".U, // r0 = r2 or r0 (to show r2 contents)
"b0011111111111000".U, // r0 = 0xFFFF (to mark end of program)
)
test(new Cpu(instructions)) { c =>
for (i <- 0 until 54) {
println(c.io.results.peek())
c.clock.step()
}
c.io.results.expect("b1111111111111111".U) // must be the program's end marker
}
println("SUCCESS!!")
Αποθηκεύστε το σημερινό notebook για να το έχετε στο αρχείο σας (θα χρειαστεί σε επόμενα εργαστήρια).
Δεν ανήκει στα παραδοτέα του εργαστηρίου.