Όπως και στα προηγούμενα εργαστήρια συνεχίζουμε στο περιβάλλον του 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
Στα προηγούμενα εργαστήρια ασχοληθήκαμε με συνδυαστικά κυκλώματα, λογικές δηλαδή συναρτήσεις που ανάλογα με τις εισόδους που δέχονται παράγουν τις αντίστοιχες εξόδους.
Στα επεξεργαστικά συστήματα όμως η λειτουργία γίνεται βηματικά σε φάσεις που συγχρονίζονται από ένα η περισσότερα σήματα ρολογιού (clock). Μεταξύ των φάσεων πρέπει να κρατάμε τα προσωρινά αποτελέσματα σε "στοιχεία μνήμης" ή αλλιώς καταχωρητές (registers). Τα κυκλώματα που παράγουν αποτελέσματα με βάση όχι μόνο τις εισόδους αλλά και την προηγούμενη κατάσταση ονομάζονται ακολουθιακά κυκλώματα.
Είναι ένα κύκλωμα που "αποθηκεύει" την τιμή μιας ομάδας bits. Το πόσα bits θα έχει ένας καταχωρητής εξαρτάται από τον σχεδιασμό μας για να καλύπτει τις ανάγκες μας. Υποθέτοντας ότι τα bits είναι N
στον αριθμό, ένας καταχωρητής έχει τις ακόλουθες εισόδους-εξόδους:
Ν
bitsΝ
bitsκαι ενδεχομένως τα πρόσθετα σήματα εισόδου:
Μπορούμε να ορίσουμε έναν καταχωρητή με συγκεκριμένο εύρος στην Chisel ως εξής:
val testReg = Reg(UInt(8.W))
Το προηγούμενο ορίζει έναν καταχωρητή των 8 bits.
Στη συνέχεια συνδέουμε την είσοδο του καταχωρητή σε κάποιο άλλο σήμα, π.χ.
testReg := io.in
όπως επίσης και την έξοδο του καταχωρητή, π.χ.
io.out := testReg
Προσοχή: τα σήματα clock
(και reset
) δεν τα γράφουμε εμείς, τα παράγει αυτόματα η Chisel!
Δείτε το επόμενο παράδειγμα, όπου χρησιμοποιείται ένας καταχωρητής των 8 bits. Η είσοδος του module απλά συνδέεται στην είσοδο του καταχωρητή και, αντίστοιχα, η έξοδος του καταχωρητή συνδέεται στην έξοδο του module:
class RegisterExample extends Module {
val io = IO(new Bundle {
val in = Input(UInt(8.W))
val out = Output(UInt(8.W))
})
val testReg = Reg(UInt(8.W))
testReg := io.in
io.out := testReg
}
Στο test που ακολουθεί, χρησιμοποιούμε τη μέθοδο peek()
για να τυπώσουμε την τιμή που υπάρχει στην έξοδο του κυκλώματος.
Παρατηρήστε πώς δίνουμε έναν παλμό ρολογιού:
c.clock.step()
test(new RegisterExample()) { c =>
println(c.io.out.peek()) // βλέπω την αρχική τιμή του καταχωρητή (δηλαδή 0)
c.io.in.poke(123.U) // αλλάζω την είσοδο
println(c.io.out.peek()) // όσο το clock δεν ενεργοποιείται, συνεχίζω να βλέπω την αρχική τιμή
c.clock.step() // δημιουργώ έναν παλμό ρολογιού
println(c.io.out.peek()) // τώρα η είσοδος 123 θα αποθηκευτεί στον καταχωρητή και θα φανεί στην έξοδο
}
Αν δεν ορίσουμε τιμή αρχικοποίησης ο καταχωρητής θα έχει μια αρχική τιμή (π.χ. 0) ανάλογα με την τεχνολογία υλοποίησης του κυκλώματος. Αν θέλουμε να ορίσουμε εμείς μια συγκεκριμένη τιμή αρχικοποίησης μπορούμε να δηλώσουμε τον καταχωρητή όπως στο επόμενο παράδειγμα, το οποίο θέτει την τιμή αρχικοποίησης στη δεκαεξαδική τιμή FF:
val testReg = RegInit("hFF".U(8.W))
Στο επόμενο module βλέπουμε το ίδιο παράδειγμα όπως πριν, μόνο που εδώ ο καταχωρητής αρχικοποιείται στην τιμή FF(hex), ή αλλιώς οκτώ δυαδικά ψηφία με τιμή 1.
class RegisterExample extends Module {
val io = IO(new Bundle {
val in = Input(UInt(8.W))
val out = Output(UInt(8.W))
})
val testReg = RegInit("hFF".U(8.W))
testReg := io.in
io.out := testReg
}
Εκτελέστε ξανά το προηγούμενο test
με τον νέο ορισμό του RegisterExample
για να παρατηρήσετε την εκκίνηση από την τιμή FF
(255).
Τα κυκλώματα μετρητή (counters) είναι βασικό στοιχείο των επεξεργαστικών συστημάτων. Μπορούμε να υλοποιήσουμε μετρητές με τη βοήθεια καταχωρητών.
Στο επόμενο παράδειγμα χρησιμοποιται ένας καταχωρητής με αρχική τιμή 0. Η είσοδος του καταχωρητή συνδέεται στην έξοδό του, αυξημένη κατά 1.
Κατά τη λειτουργία του κυκλώματος, σε κάθε παλμό του ρολογιού αποθηκεύεται στον καταχωρητή μια νέα τιμή, η οποία είναι η παλιά + 1. Συνεπώς στην έξοδο του κυκλώματος θα δούμε μια ακολουθία τιμών αυξανόμενη κατά 1.
Σημ.: Όταν η τιμή μέτρησης θα φτάσει στη μέγιστη (255 ή FF(hex) για το εύρος των 8 bits), τότε η επόμενη τιμή θα επιστρέψει στο 0 κ.ο.κ.
class CounterExample extends Module {
val io = IO(new Bundle {
val out = Output(UInt(8.W))
})
val testReg = RegInit(0.U(8.W))
testReg := testReg + 1.U
io.out := testReg
}
Εκτελέστε το πιο κάτω test για να δείτε την αυξανόμενη ακολουθία τιμών στην έξοδο:
test(new CounterExample()) { c =>
println(c.io.out.peek())
for (i <- 0 until 10) {
c.clock.step()
println(c.io.out.peek())
}
}
Υλοποιήστε κύκλωμα μετρητή με εύρος 8 bits, ο οποίος έχει μια είσοδο offset
(με εύρος επίσης 8 bits). Συμπληρώστε στο επόμενο κελί τον κώδικα έτσι ώστε ο μετρητής να αυξάνεται κατά offset αντί για 1. Ο μετρητής θα πρέπει να ξεκινάει από την τιμή 0.
class OffsetCounter extends Module {
val io = IO(new Bundle {
val offset = // ..συμπληρώστε..
val out = // ..συμπληρώστε..
})
// ..συμπληρώστε..
}
Συμπληρώστε τον κατάλληλο κώδικα έτσι ώστε να τυπώνεται η ακολουθία τιμών αυξανόμενη κατά offset = 10.
test(new OffsetCounter()) { c =>
// ..συμπληρώστε..
}
Υλοποιήστε μετρητή με εύρος Ν bits (άρα και έξοδο out
με εύρος N bits) και με σήμα εισοδου updown
με εύρος 1 bit. Όταν το updown
είναι 1 ο μετρητής θα μετράει "προς τα πάνω" (+1). Στην αντίθετη περίπτωση θα μετράει "προς τα κάτω" (-1). Η αρχική τιμή του μετρητή θα είναι 0.
class UpDownCounter(n:Int) extends Module {
val io = IO(new Bundle {
val updown = // ..συμπληρώστε..
val out = // ..συμπληρώστε..
})
// ..συμπληρώστε..
}
Γράψτε τον κατάλληλο κώδικα στο test για δημιουργήσετε ένα module UpDownCounter
των 8 bits και να δείτε να τυπώνεται αυξανόμενη και μειούμενη ακολουθία τιμών στην έξοδο του μετρητή.
test(new UpDownCounter(8)) { c =>
// ..συμπληρώστε..
}
Μπορείτε να κατεβάσετε και να πάρετε μαζί σας το σημερινό notebook για να το έχετε στο αρχείο σας.
Δεν ανήκει στα παραδοτέα του εργαστηρίου.