Skip to content

Commit

Permalink
Add asynchronous memory (#42)
Browse files Browse the repository at this point in the history
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
Show file tree
Hide file tree
Showing 5 changed files with 717 additions and 4 deletions.
144 changes: 144 additions & 0 deletions src/main/scala/components/memory-async.scala
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
}
}
73 changes: 73 additions & 0 deletions src/main/scala/components/memory-port-io.scala
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))
}
177 changes: 177 additions & 0 deletions src/main/scala/components/memory-ports.scala
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
}
}
Loading

0 comments on commit 2958b5e

Please sign in to comment.