-
Notifications
You must be signed in to change notification settings - Fork 38
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implements a modularized memory module system for the pipeline. As of this moment, the commit includes an asynchronous memory which delays memory accesses by a configurable number of latency cycles. Robust interchangability of the memory's components is a key design facet of this memory system as this permits CPU models to reliably swap memory components to create a suitable memory configuration. To achieve this, the memory is split into three essential components: the pipeline-port IO, the memory ports, and the backing memory. Pipeline-port IO: Serves as the primary interface between the pipeline and memory ports. The pipeline interacts with the memory system by issuing/receiving signals to/from the ports through a MemPortIO. This approach ideally abstracts the implementation of a memory port away from the pipeline, allowing flexible implementations of memory ports that support features like caching. Memory ports: Dedicated modules that translate the pipeline signals into a Request for the backing memory to process, and receive Responses from the memory to process and feed back to memory. Two implementations of a memory port, IMemPort and DMemPort, are included in this commit, which directly passes through the pipeline signals into a Request. * By design, a data memory write first reads the memory at an address, manipulates the memory's response, and writes a modified block of data to the same address one cycle after Note that this implies that a memory port following this design must require one extra cycle to complete a write versus a read. Advanced implementations of these ports should utilize MemPortIO to guarantee intercompatibility with the memory system, and preprocess the input signals as necessary. Backing memory: Holds the memory file and performs basic read/write operations upon its contents. It stores a Pipe for Requests to percolate through, configured with the latency parameter passed into its constructor, to emulate the delay of real DRAM accesses. With this new memory system we observe unprecedented robustness and flexibility compared to the older memory, as we can generate a new memory configuration for our purposes by swapping out ports for caches, async backing memory for synchronous backing memory, and so forth. For those who need to construct the new memory system in a CPU model, the AsyncMemoryTestHarness class in AsyncMemoryUnitTest.scala shows how the memory ports and backing memory should be connected together.
- Loading branch information
Jared Barocsi
committed
Jun 12, 2019
1 parent
86f3c31
commit 2958b5e
Showing
5 changed files
with
717 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,144 @@ | ||
// Asynchronous memory module | ||
|
||
package dinocpu | ||
|
||
import chisel3._ | ||
import chisel3.util._ | ||
import chisel3.internal.firrtl.Width | ||
|
||
import chisel3.util.experimental.loadMemoryFromFile | ||
import firrtl.annotations.MemoryLoadFileType | ||
|
||
/** | ||
* Enumerator to assign names to the UInt constants representing memory operations | ||
*/ | ||
object MemoryOperation { | ||
val Read = 0.U | ||
val Write = 1.U | ||
} | ||
|
||
import MemoryOperation._ | ||
|
||
// A Bundle used for representing a memory access by instruction memory or data memory. | ||
class Request extends Bundle { | ||
val address = UInt(32.W) | ||
val writedata = UInt(32.W) | ||
val operation = UInt(1.W) | ||
} | ||
|
||
// A bundle used for representing the memory's response to a memory port. | ||
class Response extends Bundle { | ||
// The 4-byte-wide block of data being returned by memory | ||
val data = UInt(32.W) | ||
} | ||
|
||
/** | ||
* The generic interface for communication between the IMem/DMemPort modules and the backing memory. | ||
* | ||
* Input: request, the ready/valid interface for a MemPort module to issue Requests to. Memory | ||
* will only accept a request when both request.valid (the MemPort is supplying valid data) | ||
* and request.ready (the memory is idling for a request) are high. | ||
* | ||
* Output: response, the valid interface for the data outputted by memory if it was requested to read. | ||
* the bits in response.bits should only be treated as valid data when response.valid is high. | ||
*/ | ||
class AsyncMemIO extends Bundle { | ||
val request = Flipped(Decoupled (new Request)) | ||
val response = Valid (new Response) | ||
} | ||
|
||
/** | ||
* The modified asynchronous form of the dual ported memory module. | ||
* When io.imem.request.valid or io.imem.request.valid is true and the memory is ready for an operation, | ||
* this memory module simulates the latency of real DRAM by pushing memory accesses into pipes that delay | ||
* the request for a configurable latency. | ||
* | ||
* As with the synced memory module, this memory should only be instantiated in the Top file, | ||
* and never within the actual CPU. | ||
* | ||
* The I/O for this module is defined in [[AsyncMemIO]]. | ||
*/ | ||
class DualPortedAsyncMemory(size: Int, memfile: String, latency: Int) extends Module { | ||
def wireMemPipe(portio: AsyncMemIO, pipe: Pipe[Request]): Unit = { | ||
pipe.io.enq.bits <> DontCare | ||
pipe.io.enq.valid := false.B | ||
portio.response.valid := false.B | ||
|
||
// Memory is technically always ready, but we want to use the | ||
// ready/valid interface so that if needed we can restrict | ||
// executing memory operations | ||
portio.request.ready := true.B | ||
} | ||
|
||
val io = IO(new Bundle { | ||
val imem = new AsyncMemIO | ||
val dmem = new AsyncMemIO | ||
}) | ||
io <> DontCare | ||
|
||
assert(latency > 0) // Check for attempt to make combinational memory | ||
|
||
val memory = Mem(math.ceil(size.toDouble/4).toInt, UInt(32.W)) | ||
loadMemoryFromFile(memory, memfile) | ||
|
||
// Instruction port | ||
val imemPipe = Module(new Pipe(new Request, latency)) | ||
|
||
wireMemPipe(io.imem, imemPipe) | ||
|
||
when (io.imem.request.valid) { | ||
// Put the Request into the instruction pipe and signal that instruction memory is busy | ||
val inRequest = io.imem.request.asTypeOf(new Request) | ||
imemPipe.io.enq.bits := inRequest | ||
imemPipe.io.enq.valid := true.B | ||
} .otherwise { | ||
imemPipe.io.enq.valid := false.B | ||
} | ||
|
||
when (imemPipe.io.deq.valid) { | ||
// We should only be expecting a read from instruction memory | ||
assert(imemPipe.io.deq.bits.operation === Read) | ||
val outRequest = imemPipe.io.deq.asTypeOf (new Request) | ||
// Check that address is pointing to a valid location in memory | ||
assert (outRequest.address < size.U) | ||
io.imem.response.valid := true.B | ||
io.imem.response.bits.data := memory(outRequest.address >> 2) | ||
} .otherwise { | ||
// The memory's response can't possibly be valid if the imem pipe isn't outputting a valid request | ||
io.imem.response.valid := false.B | ||
} | ||
|
||
// Data port | ||
|
||
val dmemPipe = Module(new Pipe(new Request, latency)) | ||
|
||
wireMemPipe(io.dmem, dmemPipe) | ||
|
||
when (io.dmem.request.valid) { | ||
// Put the Request into the data pipe and signal that data memory is busy | ||
val inRequest = io.dmem.request.asTypeOf (new Request) | ||
dmemPipe.io.enq.bits := inRequest | ||
dmemPipe.io.enq.valid := true.B | ||
} .otherwise { | ||
// dmemPipe.io.enq.valid := false.B | ||
} | ||
|
||
when (dmemPipe.io.deq.valid) { | ||
// Dequeue request and execute | ||
val outRequest = dmemPipe.io.deq.asTypeOf (new Request) | ||
val address = outRequest.address >> 2 | ||
// Check that address is pointing to a valid location in memory | ||
assert (outRequest.address < size.U) | ||
|
||
when (outRequest.operation === Read) { | ||
io.dmem.response.valid := true.B | ||
io.dmem.response.bits.data := memory(address) | ||
} .elsewhen (outRequest.operation === Write) { | ||
io.dmem.response.valid := false.B | ||
memory(address) := outRequest.writedata | ||
} | ||
} .otherwise { | ||
// The memory's response can't possibly be valid if the dmem pipe isn't outputting a valid request | ||
io.dmem.response.valid := false.B | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
// Contains the memory port IOs for use in port/cache implementations | ||
|
||
package dinocpu | ||
|
||
import chisel3._ | ||
import chisel3.util._ | ||
|
||
/** | ||
* A generic ready/valid interface for MemPort modules, whose IOs extend this. | ||
* This interface is split into two parts: | ||
* - Pipeline <=> Port: the interface between the pipelined CPU and the memory port | ||
* - Memory <=> Port: the interface between the memory port and the backing memory | ||
* | ||
* Pipeline <=> Port: | ||
* Input: address, the address of a piece of data in memory. | ||
* Input: valid, true when the address specified is valid | ||
* Output: good, true when memory is responding with a piece of data (used to un-stall the pipeline) | ||
* | ||
* | ||
* Port <=> Memory: | ||
* Input: response, the return route from memory to a memory port. This is primarily meant for connecting to | ||
* an AsyncMemIO's response output, and should not be connected to anything else in any circumstance | ||
* (or things will possibly break) | ||
* Output: request, a DecoupledIO that delivers a request from a memory port to memory. This is primarily | ||
* meant for connecting to an AsynMemIO's request input, and should not be connected to anything else | ||
*/ | ||
class MemPortIO extends Bundle { | ||
// Pipeline <=> Port | ||
val address = Input(UInt(32.W)) | ||
val valid = Input(Bool()) | ||
val good = Output(Bool()) | ||
|
||
// Port <=> Memory | ||
val response = Flipped(Valid(new Response)) | ||
val request = Decoupled(new Request) | ||
} | ||
|
||
/** | ||
* The *interface* of the IMemPort module. | ||
* | ||
* Pipeline <=> Port: | ||
* Input: address, the address of an instruction in memory | ||
* Input: valid, true when the address specified is valid | ||
* Output: instruction, the requested instruction | ||
* Output: good, true when memory is responding with a piece of data | ||
*/ | ||
class IMemPortIO extends MemPortIO { | ||
val instruction = Output(UInt(32.W)) | ||
} | ||
|
||
/** | ||
* The *interface* of the DMemPort module. | ||
* | ||
* Pipeline <=> Port: | ||
* Input: address, the address of a piece of data in memory. | ||
* Input: writedata, valid interface for the data to write to the address | ||
* Input: valid, true when the address (and writedata during a write) specified is valid | ||
* Input: memread, true if we are reading from memory | ||
* Input: memwrite, true if we are writing to memory | ||
* Input: maskmode, mode to mask the result. 0 means byte, 1 means halfword, 2 means word | ||
* Input: sext, true if we should sign extend the result | ||
* Output: readdata, the data read and sign extended | ||
* Output: good, true when memory is responding with a piece of data | ||
*/ | ||
class DMemPortIO extends MemPortIO { | ||
val writedata = Input(UInt(32.W)) | ||
val memread = Input(Bool()) | ||
val memwrite = Input(Bool()) | ||
val maskmode = Input(UInt(2.W)) | ||
val sext = Input(Bool()) | ||
|
||
val readdata = Output(UInt(32.W)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,177 @@ | ||
// Asynchronous memory module | ||
|
||
package dinocpu | ||
|
||
import chisel3._ | ||
import chisel3.util._ | ||
import chisel3.internal.firrtl.Width | ||
|
||
import chisel3.util.experimental.loadMemoryFromFile | ||
import firrtl.annotations.MemoryLoadFileType | ||
|
||
import MemoryOperation._ | ||
|
||
// A Bundle used for temporarily storing the necessary information for a read/write in the data memory accessor. | ||
class OutstandingReq extends Bundle { | ||
val address = UInt(32.W) | ||
val writedata = UInt(32.W) | ||
val maskmode = UInt(2.W) | ||
val operation = UInt(1.W) | ||
val sext = Bool() | ||
} | ||
|
||
/** | ||
* The instruction memory port. | ||
* | ||
* The I/O for this module is defined in [[IMemPortIO]]. | ||
*/ | ||
class IMemPort extends Module { | ||
val io = IO (new IMemPortIO) | ||
io := DontCare | ||
|
||
// When the pipeline is supplying a high valid signal | ||
when (io.valid) { | ||
val request = Wire(new Request) | ||
request := DontCare | ||
request.address := io.address | ||
request.operation := Read | ||
|
||
io.request.bits := request | ||
io.request.valid := true.B | ||
} .otherwise { | ||
io.request.valid := false.B | ||
} | ||
|
||
// When the memory is outputting a valid instruction | ||
io.good := io.response.valid | ||
when (io.response.valid) { | ||
io.instruction := io.response.bits.data | ||
} | ||
} | ||
|
||
/** | ||
* The data memory port. | ||
* | ||
* The I/O for this module is defined in [[DMemPortIO]]. | ||
*/ | ||
class DMemPort extends Module { | ||
val io = IO (new DMemPortIO) | ||
io := DontCare | ||
io.good := io.response.valid | ||
|
||
// A register to hold intermediate data (e.g., write data, mask mode) while the request | ||
// is outstanding to memory. | ||
val outstandingReq = RegInit(0.U.asTypeOf(Valid(new OutstandingReq))) | ||
|
||
// Used to set the valid bit of the outstanding request | ||
val sending = Wire(Bool()) | ||
|
||
// When the pipeline is supplying a valid read OR write request, send out the request | ||
// ... on the condition that there isn't an outstanding request in the queue. | ||
// We need to process outstanding request first to guarantee atomicity of the memory write operation | ||
// Ready if either we don't have an outstanding request or the outstanding request is a read and | ||
// it has been satisfied this cycle. Note: we cannot send a read until one cycle after the write has | ||
// been sent. | ||
val ready = !outstandingReq.valid || (io.response.valid && (outstandingReq.valid && outstandingReq.bits.operation === Read)) | ||
when (io.valid && (io.memread || io.memwrite) && ready) { | ||
// Check if we aren't issuing both a read and write at the same time | ||
assert (! (io.memread && io.memwrite)) | ||
|
||
// On either a read or write we must read a whole block from memory. Store the necessary | ||
// information to redirect the memory's response back into itself through a write | ||
// operation and get the right subset of the block on a read. | ||
outstandingReq.bits.address := io.address | ||
outstandingReq.bits.writedata := io.writedata | ||
outstandingReq.bits.maskmode := io.maskmode | ||
outstandingReq.bits.sext := io.sext | ||
when (io.memwrite) { | ||
outstandingReq.bits.operation := Write | ||
} .otherwise { | ||
outstandingReq.bits.operation := Read | ||
} | ||
sending := true.B | ||
|
||
// Program memory to perform a read. Always read since we must read before write. | ||
io.request.bits.address := io.address | ||
io.request.bits.writedata := 0.U | ||
io.request.bits.operation := Read | ||
io.request.valid := true.B | ||
} .otherwise { | ||
// no request coming in so don't send a request out | ||
io.request.valid := false.B | ||
sending := false.B | ||
} | ||
|
||
// Response path | ||
when (io.response.valid) { | ||
assert(outstandingReq.valid) | ||
when (outstandingReq.bits.operation === Write) { | ||
val writedata = Wire (UInt (32.W)) | ||
|
||
// When not writing a whole word | ||
when (outstandingReq.bits.maskmode =/= 2.U) { | ||
// Read in the existing piece of data at the address, so we "overwrite" only part of it | ||
val offset = outstandingReq.bits.address (1, 0) | ||
val readdata = Wire (UInt (32.W)) | ||
readdata := io.response.bits.data | ||
val data = Wire (UInt (32.W)) | ||
// Mask the portion of the existing data so it can be or'd with the writedata | ||
when (outstandingReq.bits.maskmode === 0.U) { | ||
data := io.response.bits.data & ~(0xff.U << (offset * 8.U)) | ||
} .otherwise { | ||
data := io.response.bits.data & ~(0xffff.U << (offset * 8.U)) | ||
} | ||
writedata := data | (outstandingReq.bits.writedata << (offset * 8.U)) | ||
} .otherwise { | ||
// Write the entire word | ||
writedata := outstandingReq.bits.writedata | ||
} | ||
|
||
// Program the memory to issue a write. | ||
val request = Wire (new Request) | ||
request.address := outstandingReq.bits.address | ||
request.writedata := writedata | ||
request.operation := Write | ||
io.request.bits := request | ||
io.request.valid := true.B | ||
} .otherwise { | ||
// Response is valid and we don't have a stored write. | ||
// Perform masking and sign extension on read data when memory is outputting it | ||
val readdata_mask = Wire(UInt(32.W)) | ||
val readdata_mask_sext = Wire(UInt(32.W)) | ||
|
||
val offset = outstandingReq.bits.address(1,0) | ||
when (outstandingReq.bits.maskmode === 0.U) { | ||
// Byte | ||
readdata_mask := (io.response.bits.data >> (offset * 8.U)) & 0xff.U | ||
} .elsewhen (outstandingReq.bits.maskmode === 1.U) { | ||
// Half-word | ||
readdata_mask := (io.response.bits.data >> (offset * 8.U)) & 0xffff.U | ||
} .otherwise { | ||
readdata_mask := io.response.bits.data | ||
} | ||
|
||
when (outstandingReq.bits.sext) { | ||
when (outstandingReq.bits.maskmode === 0.U) { | ||
// Byte sign extension | ||
readdata_mask_sext := Cat(Fill(24, readdata_mask(7)), readdata_mask(7, 0)) | ||
} .elsewhen (outstandingReq.bits.maskmode === 1.U) { | ||
// Half-word sign extension | ||
readdata_mask_sext := Cat(Fill(16, readdata_mask(15)), readdata_mask(15, 0)) | ||
} .otherwise { | ||
// Word sign extension (does nothing) | ||
readdata_mask_sext := readdata_mask | ||
} | ||
} .otherwise { | ||
readdata_mask_sext := readdata_mask | ||
} | ||
|
||
io.readdata := readdata_mask_sext | ||
} | ||
// Mark the outstanding request register as being invalid, unless sending | ||
outstandingReq.valid := sending | ||
} .otherwise { | ||
// Keep the outstanding request valid or invalid unless sending | ||
outstandingReq.valid := outstandingReq.valid | sending | ||
} | ||
} |
Oops, something went wrong.