Commit 3d7635a0 authored by Jens Korinth's avatar Jens Korinth
Browse files

Axi4LiteRegisterFile: Implement config register file

* AXI4Lite interface
* flexible ControlRegister class hierarchy: constants, single values,
  virtual registers (callbacks)
* implemented unit test cases
* had to implement Axi4LiteProgrammableMaster for batch testing
parent bf5ca040
package AXILiteDefs
{
import Chisel._
import Literal._
import Node._
// Part I: Definitions for the actual data carried over AXI channels
// in part II we will provide definitions for the actual AXI interfaces
// by wrapping the part I types in Decoupled (ready/valid) bundles
// AXI Lite channel data definitions
class AXILiteAddress(addrWidthBits: Int) extends Bundle {
val addr = UInt(width = addrWidthBits)
val prot = UInt(width = 3)
override def clone = { new AXILiteAddress(addrWidthBits).asInstanceOf[this.type] }
}
class AXILiteWriteData(dataWidthBits: Int) extends Bundle {
val data = UInt(width = dataWidthBits)
val strb = UInt(width = dataWidthBits/8)
override def clone = { new AXILiteWriteData(dataWidthBits).asInstanceOf[this.type] }
}
class AXILiteReadData(dataWidthBits: Int) extends Bundle {
val data = UInt(width = dataWidthBits)
val resp = UInt(width = 2)
override def clone = { new AXILiteReadData(dataWidthBits).asInstanceOf[this.type] }
}
// Part II: Definitions for the actual AXI interfaces
class AXILiteSlaveIF(addrWidthBits: Int, dataWidthBits: Int) extends Bundle {
// write address channel
val writeAddr = Decoupled(new AXILiteAddress(addrWidthBits)).flip
// write data channel
val writeData = Decoupled(new AXILiteWriteData(dataWidthBits)).flip
// write response channel (for memory consistency)
val writeResp = Decoupled(UInt(width = 2))
// read address channel
val readAddr = Decoupled(new AXILiteAddress(addrWidthBits)).flip
// read data channel
val readData = Decoupled(new AXILiteReadData(dataWidthBits))
// rename signals to be compatible with those in the Xilinx template
def renameSignals() {
writeAddr.bits.addr.setName("S_AXI_AWADDR")
writeAddr.bits.prot.setName("S_AXI_AWPROT")
writeAddr.valid.setName("S_AXI_AWVALID")
writeAddr.ready.setName("S_AXI_AWREADY")
writeData.bits.data.setName("S_AXI_WDATA")
writeData.bits.strb.setName("S_AXI_WSTRB")
writeData.valid.setName("S_AXI_WVALID")
writeData.ready.setName("S_AXI_WREADY")
writeResp.bits.setName("S_AXI_BRESP")
writeResp.valid.setName("S_AXI_BVALID")
writeResp.ready.setName("S_AXI_BREADY")
readAddr.bits.addr.setName("S_AXI_ARADDR")
readAddr.bits.prot.setName("S_AXI_ARPROT")
readAddr.valid.setName("S_AXI_ARVALID")
readAddr.ready.setName("S_AXI_ARREADY")
readData.bits.data.setName("S_AXI_RDATA")
readData.bits.resp.setName("S_AXI_RRESP")
readData.valid.setName("S_AXI_RVALID")
readData.ready.setName("S_AXI_RREADY")
}
override def clone = { new AXILiteSlaveIF(addrWidthBits, dataWidthBits).asInstanceOf[this.type] }
}
class AXILiteMasterIF(addrWidthBits: Int, dataWidthBits: Int) extends Bundle {
// write address channel
val writeAddr = Decoupled(new AXILiteAddress(addrWidthBits))
// write data channel
val writeData = Decoupled(new AXILiteWriteData(dataWidthBits))
// write response channel (for memory consistency)
val writeResp = Decoupled(UInt(width = 2)).flip
// read address channel
val readAddr = Decoupled(new AXILiteAddress(addrWidthBits))
// read data channel
val readData = Decoupled(new AXILiteReadData(dataWidthBits)).flip
// rename signals to be compatible with those in the Xilinx template
def renameSignals() {
writeAddr.bits.addr.setName("M_AXI_AWADDR")
writeAddr.bits.prot.setName("M_AXI_AWPROT")
writeAddr.valid.setName("M_AXI_AWVALID")
writeAddr.ready.setName("M_AXI_AWREADY")
writeData.bits.data.setName("M_AXI_WDATA")
writeData.bits.strb.setName("M_AXI_WSTRB")
writeData.valid.setName("M_AXI_WVALID")
writeData.ready.setName("M_AXI_WREADY")
writeResp.bits.setName("M_AXI_BRESP")
writeResp.valid.setName("M_AXI_BVALID")
writeResp.ready.setName("M_AXI_BREADY")
readAddr.bits.addr.setName("M_AXI_ARADDR")
readAddr.bits.prot.setName("M_AXI_ARPROT")
readAddr.valid.setName("M_AXI_ARVALID")
readAddr.ready.setName("M_AXI_ARREADY")
readData.bits.data.setName("M_AXI_RDATA")
readData.bits.resp.setName("M_AXI_RRESP")
readData.valid.setName("M_AXI_RVALID")
readData.ready.setName("M_AXI_RREADY")
}
override def clone = { new AXILiteMasterIF(addrWidthBits, dataWidthBits).asInstanceOf[this.type] }
}
}
package chisel.axiutils
import chisel.axiutils.registers._
import chisel.packaging.{CoreDefinition, ModuleBuilder}
import chisel.packaging.CoreDefinition.root
import chisel.miscutils.DecoupledDataSource
......@@ -95,6 +96,32 @@ object AxiModuleBuilder extends ModuleBuilder {
version = "0.1",
root = root("AxiMux")
)
),
( // AXI Register File
() => Module(
new Axi4LiteRegisterFile(new Axi4LiteRegisterFileConfiguration(
width = 32,
regs = Map(0 -> new ConstantRegister(value = BigInt("10101010", 16)),
4 -> new ConstantRegister(value = BigInt("20202020", 16)),
8 -> new ConstantRegister(value = BigInt("30303030", 16)),
16 -> new ConstantRegister(value = BigInt("40404040", 16), bitfield = Map(
"Byte #3" -> BitRange(31, 24),
"Byte #2" -> BitRange(23, 16),
"Byte #1" -> BitRange(15, 8),
"Byte #0" -> BitRange(7, 0)
)))
))
),
CoreDefinition.withActions(
name = "Axi4LiteRegisterFile",
vendor = "esa.cs.tu-darmstadt.de",
library = "chisel",
version = "0.1",
root = root("Axi4LiteRegisterFile"),
postBuildActions = Seq(_ match {
case m: Axi4LiteRegisterFile => m.dumpAddressMap(root("Axi4LiteRegisterFile"))
})
)
)
)
}
package chisel.axiutils.registers
import chisel.axiutils.AxiConfiguration
import chisel.axiutils.registers._
import Chisel._
import AXILiteDefs._
import scala.util.Properties.{lineSeparator => NL}
/**
* Configuration object for Axi4LiteRegisterFile.
* @param addrGranularity Smallest addressable bit width (default: 8, e.g., 1 byte).
* @param width Register data width (in bits).
* @param regs Map from offsets in addrGranularity to register implementations.
*/
case class Axi4LiteRegisterFileConfiguration(
addrGranularity: Int = 8,
width: Int,
regs: Map[Int, ControlRegister]
) {
/* internal helpers: */
private def overlap(p: (BitRange, BitRange)) = p._1.overlapsWith(p._2)
private def makeRange(a: Int): BitRange = BitRange(a * addrGranularity + width - 1, a * addrGranularity)
private lazy val m = regs.keys.toList.sorted map makeRange
private lazy val o: Seq[Boolean] = (m.take(m.length - 1) zip m.tail) map overlap
/* constraint checking */
require (width > 0 && width <= 1024,
"Axi4LiteRegisterFile: width (%d) must be 0 < width <= 1024"
.format(width))
require (regs.size > 0, "regs must not be empty")
require (regs.size == 1 || !(o reduce (_||_)), "ranges must not overlap: " + regs)
/** Minimum bit width of address lines. */
lazy val minAddrWidth: Int = Seq(log2Up(regs.size * width / addrGranularity), log2Up(regs.keys.max)).max
}
/**
* Axi4LiteRegisterFile bundle.
* @param cfg Configuration object.
* @param axi Implicit AXI configuration.
**/
class Axi4LiteRegisterFileIO(cfg: Axi4LiteRegisterFileConfiguration)(implicit axi: AxiConfiguration) extends Bundle {
val addrWidth: Int = Seq(cfg.minAddrWidth, axi.addrWidth) max
val dataWidth: Int = Seq(cfg.width, axi.dataWidth) max
val saxi = new AXILiteSlaveIF(dataWidth, addrWidth)
}
/**
* Axi4LiteRegisterFile implements a register file:
* Writes currently take at least 3 cycles (addr -> data -> response), reads at least 2 (addr -> response).
* No strobe support, always read/write full register width.
* @param cfg Configuration object.
* @param axi Implicit AXI configuration.
**/
class Axi4LiteRegisterFile(cfg: Axi4LiteRegisterFileConfiguration)(implicit axi: AxiConfiguration) extends Module {
/** Dumps address map as markdown file. **/
def dumpAddressMap(path: String) = {
def mksz(s: String, w: Int) = if (s.length > w) s.take(w) else if (s.length < w) s + (" " * (w - s.length)) else s
val fw = new java.io.FileWriter(java.nio.file.Paths.get(path).resolve("AdressMap.md").toString)
fw.append("| **Name** |**From**|**To** | **Description** |").append(NL)
.append("|:----------------|:------:|:------:|:-----------------------------------------|").append(NL)
for (off <- cfg.regs.keys.toList.sorted; reg = cfg.regs(off))
fw.append("| %s | 0x%04x | 0x%04x | %s |".format(
mksz(reg.name.getOrElse("N/A"), 15), off, off + cfg.width/8 - 1, reg.description//mksz(reg.description, 40)
)).append(NL)
fw.flush()
fw.close
}
/** HARDWARE **/
val io = new Axi4LiteRegisterFileIO(cfg)
/** states: ready for address, transferring **/
val ready :: fetch :: transfer :: response :: Nil = Enum(UInt(), 4)
/** READ PROCESS **/
val r_state = Reg(init = ready) // read state
val r_data = Reg(UInt(width = io.dataWidth)) // read data buffer
val r_addr = Reg(UInt(width = io.addrWidth)) // read address
io.saxi.readAddr.ready := r_state === ready
io.saxi.readData.valid := r_state === transfer
io.saxi.readData.bits.data := r_data
io.saxi.readData.bits.resp := UInt(2) // default: SLVERR
when (reset) {
r_data := UInt("hDEADBEEF")
r_state := ready
io.saxi.readAddr.ready := Bool(false)
io.saxi.readData.valid := Bool(false)
}
.otherwise {
// assign data from reg
for (off <- cfg.regs.keys.toList.sorted)
when (r_addr === UInt(off)) { cfg.regs(off).read() map { v => r_data := v; io.saxi.readData.bits.resp := UInt(0) } }
// address receive state
when (r_state === ready && io.saxi.readAddr.valid) {
assert(io.saxi.readAddr.bits.prot === UInt(0), "Axi4LiteRegisterFile: read does not support PROT")
r_addr := io.saxi.readAddr.bits.addr
r_state := fetch
printf("Axi4LiteRegisterFile: received read address 0x%x\n", io.saxi.readAddr.bits.addr)
}
// wait one cycle for fetch
when (r_state === fetch) { r_state := transfer }
// data transfer state
when (r_state === transfer && io.saxi.readData.ready) {
r_state := ready
printf("Axi4LiteRegisterFile: read 0x%x from address 0x%x\n", r_data, r_addr)
}
}
/** WRITE PROCESS **/
val w_state = Reg(init = ready) // write state
val w_data = Reg(UInt(width = io.dataWidth)) // write data buffer
val w_addr = Reg(UInt(width = io.addrWidth)) // write address
val w_resp = Reg(UInt(width = 2)) // write response
w_resp := UInt(2) // default: SLVERR
io.saxi.writeResp.bits := w_resp
io.saxi.writeResp.valid := w_state === response
io.saxi.writeData.ready := w_state === transfer
io.saxi.writeAddr.ready := w_state === ready
when (reset) {
w_data := UInt("hDEADBEEF")
w_state := ready
io.saxi.writeAddr.ready := Bool(false)
io.saxi.writeData.ready := Bool(false)
io.saxi.writeResp.valid := Bool(false)
}
.otherwise {
// address receive state
when (w_state === ready) {
when (io.saxi.writeAddr.valid) {
assert(io.saxi.readAddr.bits.prot === UInt(0), "Axi4LiteRegisterFile: write does not support PROT")
w_addr := io.saxi.writeAddr.bits.addr
w_state := transfer
printf("Axi4LiteRegisterFile: received write address 0x%x\n", io.saxi.writeAddr.bits.addr)
}
}
// data transfer state
when (w_state === transfer && io.saxi.writeData.valid) {
// TODO assert strobes
w_state := response
// assign data to reg
for (off <- cfg.regs.keys.toList.sorted)
when (w_addr === UInt(off)) {
printf("Axi4LiteRegisterFile: writing 0x%x to register with offset %d\n", w_data, UInt(off))
w_resp := Mux(Bool(cfg.regs(off).write(io.saxi.writeData.bits.data)), UInt(0) /*OKAY*/, UInt(2) /*SLVERR*/)
}
}
// write response state
when (w_state === response && io.saxi.writeResp.ready) { w_state := ready }
}
}
package chisel.axiutils.registers
import Chisel.{Reg, UInt}
/**
* Abstract base class for control registers.
* Provides base methods for describing and accessing control register data.
* @param _name Name of the register (optional).
* @param bitfield Bit partitioning of the value (optional).
**/
sealed abstract class ControlRegister(_name: Option[String], bitfield: BitfieldMap = Map()) {
/** Format description string for bitfield (if any). **/
private def bf: String = bitfield.toList.sortWith((a, b) => a._2.to > b._2.to) map (e =>
"_%d-%d:_ %s".format(e._2.to, e._2.from, e._1)
) mkString (" ")
/** Name of the register. **/
def name: Option[String] = _name
/** Description of the register. **/
def description: String = if (bitfield.size > 0) bf else _name.getOrElse("N/A")
/** Access to named bit range. **/
def apply(s: String): Option[UInt] = read() map { v =>
bitfield getOrElse (s, None) match { case BitRange(to, from) => v(to, from) }
}
/** Perform Chisel wiring to value. **/
def write(v: UInt): Boolean = false
/** Perform Chisel read on value. **/
def read(): Option[UInt]
}
/**
* Control register with an constant value (no write).
* @param name Name of the register (optional).
* @param bitfield Bit partitioning of the value (optional).
* @param value Constant value for the register.
**/
class ConstantRegister(name: Option[String] = None, bitfield: BitfieldMap = Map(), value: BigInt)
extends ControlRegister(name, bitfield) {
override def description: String = "%s - _const:_ 0x%x (%d)".format(super.description, value, value)
def read(): Option[UInt] = Some(UInt(value))
}
/**
* Basic register with internal Chisel Reg (read/write).
* @param name Name of the register (optional).
* @param bitfield Bit partitioning of the value (optional).
**/
class Register[T <: UInt](name: Option[String] = None, bitfield: BitfieldMap = Map(), width: Int)
extends ControlRegister(name, bitfield) {
private lazy val _r = Reg(UInt(width = width))
def read(): Option[UInt] = Some(_r)
override def write(v: UInt) = {
_r := v
true
}
}
/**
* Virtual register: read and write callbacks are triggered on access.
* @param name Name of the register (optional).
* @param bitfield Bit partitioning of the value (optional).
* @param onRead Callback for read access.
* @param onWrite Callback for write access.
**/
class VirtualRegister(name: Option[String] = None, bitfield: BitfieldMap = Map(), onRead: () => Option[UInt], onWrite: UInt => Boolean)
extends ControlRegister(name, bitfield) {
def read() = onRead()
override def write(v: UInt) = onWrite(v)
}
package chisel.axiutils
package object registers {
/** Tuple-type for bit ranges. **/
sealed case class BitRange(to: Int, from: Int) {
require (to >= from && from >= 0, "BitRange: invalid range (%d, %d)".format(to, from))
def overlapsWith(other: BitRange): Boolean = (from <= other.from && to >= other.from) ||
(from > other.from && from <= other.to)
}
/** Names for bit ranges. **/
type BitfieldMap = Map[String, BitRange]
}
package chisel.axiutils
import Chisel._
/**
* AXI4Lite master transaction model.
* @param isRead true for read transactions.
* @param addr address to read from.
* @param value write value (optional)
**/
case class MasterAction(isRead: Boolean = true, addr: Int, value: Option[BigInt])
/**
* Axi4LiteProgrammableMaster is a testing tool to perform a sequence of master actions.
* It automatically performs simple AXI4Lite transactions on slave.
* @param action Sequence of transactions, executed sequentially without delay.
* @param axi implicit AXI configuration.
**/
class Axi4LiteProgrammableMaster(action: Seq[MasterAction])(implicit axi: AxiConfiguration) extends Module {
import AXILiteDefs._
val io = new Bundle {
val maxi = new AXILiteMasterIF(axi.addrWidth, axi.dataWidth)
val out = Decoupled(UInt(width = axi.dataWidth))
val finished = Bool(OUTPUT)
val w_resp = Decoupled(UInt(width = 2))
}
val cnt = Reg(UInt(width = log2Up(action.length + 1))) // current action; last value indicates completion
val s_addr :: s_wtransfer :: s_rtransfer :: s_response :: s_idle :: Nil = Enum(UInt(), 5)
val state = Reg(init = s_addr)
val w_data = Reg(UInt(width = axi.dataWidth))
val r_data = RegNext(io.maxi.readData.bits.data)
val q = Module(new Queue(UInt(width = axi.dataWidth), action.length))
q.io.enq.valid := Bool(false)
q.io.enq.bits := io.maxi.readData.bits.data
io.maxi.readData.ready := q.io.enq.ready
io.out <> q.io.deq
io.maxi.writeData.bits.data := w_data
io.maxi.writeData.valid := state === s_wtransfer
io.maxi.readAddr.valid := Bool(false)
io.maxi.writeAddr.valid := Bool(false)
io.maxi.readAddr.bits.addr := UInt(0)
io.maxi.writeAddr.bits.addr := UInt(0)
io.w_resp <> io.maxi.writeResp
io.maxi.writeResp.ready := Bool(true)
io.finished := cnt === UInt(action.length)
when (reset) {
cnt := UInt(0)
}
.otherwise {
// always assign address from current action
for (i <- 0 until action.length) {
when (UInt(i) === cnt) {
io.maxi.readAddr.bits.addr := UInt(action(i).addr)
io.maxi.writeAddr.bits.addr := UInt(action(i).addr)
}
}
when (state === s_addr) {
for (i <- 0 until action.length) {
when (UInt(i) === cnt) {
io.maxi.readAddr.valid := Bool(action(i).isRead)
io.maxi.writeAddr.valid := Bool(! action(i).isRead)
action(i).value map { v => w_data := UInt(v) }
}
}
when (io.maxi.readAddr.ready && io.maxi.readAddr.valid) { state := s_rtransfer }
when (io.maxi.writeAddr.ready && io.maxi.writeAddr.valid) { state := s_wtransfer }
when (cnt === UInt(action.length)) { state := s_idle }
}
when (state === s_rtransfer) {
for (i <- 0 until action.length) {
val readReady = Bool(action(i).isRead) && io.maxi.readData.ready && io.maxi.readData.valid
when (UInt(i) === cnt && readReady) {
q.io.enq.valid := io.maxi.readData.bits.resp === UInt(0) // response OKAY
cnt := cnt + UInt(1)
state := s_addr
}
}
}
when (state === s_wtransfer) {
for (i <- 0 until action.length) {
val writeReady = Bool(!action(i).isRead) && io.maxi.writeData.ready && io.maxi.writeData.valid
when (UInt(i) === cnt && writeReady) {
cnt := cnt + UInt(1)
state := s_response
}
}
}
when (state === s_response && io.maxi.writeResp.valid) { state := s_addr }
}
}
package chisel.axiutils.registers
import chisel.axiutils.{AxiConfiguration, Axi4LiteProgrammableMaster, MasterAction}
import org.scalatest.junit.JUnitSuite
import org.scalatest.Assertions._
import org.junit.Test
import Chisel._
/**
* Harness for Axi4LiteRegisterFile:
* Creates a register file using the specified register map, connects an Axi4LiteProgrammableMaster
* to the register file and programs it with the specified actions.
* Read data is queued in a FIFO and can be accessed from the outside.
* When all actions are processed, the `finished` flag is driven high.
* @param size number of registers
* @param distance byte distance of registers
* @param regs register map for register file
* @param actions master actions to perform
**/
class RegFileTest(
val size: Int,
val off: Int,
regs: Map[Int, ControlRegister],
actions: Seq[MasterAction]
)(implicit axi: AxiConfiguration) extends Module {
val io = new Bundle { val out = Decoupled(UInt(width = axi.dataWidth)); val finished = Bool(OUTPUT) }
val cfg = new Axi4LiteRegisterFileConfiguration(width = axi.dataWidth, regs = regs)
val saxi = Module(new Axi4LiteRegisterFile(cfg))
val m = Module(new Axi4LiteProgrammableMaster(actions))
m.io.maxi <> saxi.io.saxi
io.out <> m.io.out
io.finished := m.io.finished
m.io.w_resp.ready := Bool(true)
}
/**
* ReadTester checks attempts to read from all registers.
* @param m configured RegFileTest module
* @param isTrace turns on debug output (default: true)
**/
class ReadTester(m: RegFileTest, isTrace: Boolean = true) extends Tester(m, isTrace) {
reset(10)
poke(m.io.out.ready, true)
var steps = m.size * 10 // no more than 10 clock cycles per read
for (i <- 1 until m.size + 1 if steps > 0) {
// wait til output queue is ready
while (steps > 0 && peek(m.io.out.ready) == 0 || peek(m.io.out.valid) == 0) {
steps -= 1
step(1)
}
val v = peek(m.io.out.bits)
val e = BigInt("%02x".format(i) * 4, 16)
val resp = peek(m.m.io.maxi.readData.bits.resp)
expect (resp == 0, "read #%d: resp is 0x%x (%d), should be 0 (OKAY)".format(i, resp, resp))
expect(v == e, "at action #%d, expected: 0x%x (%d) but found %x (%d)".format(i, e, e, v, v))
step(1)
}
expect(peek(m.io.finished) != 0, "finished signal should be true at end of test")
}
/**
* WriteTester checks attempts to write to all registers.
* @param m configured RegFileTest module
* @param isTrace turns on debug output (default: true)
**/
class WriteTester(m: RegFileTest, isTrace: Boolean = true) extends Tester(m, isTrace) {
reset(10)
poke(m.io.out.ready, true)
println("running for a total of %d steps max ...".format(m.size * 20))
var steps = m.size * 20 // no more than 10 clock cycles per read+write
for (i <- 1 until m.size + 1 if steps > 0) {
while (steps > 0 && (peek(m.io.out.ready) == 0 || peek(m.io.out.valid) == 0)) {
steps -= 1
step(1)
}
val v = peek(m.io.out.bits)
val e = BigInt("%02x".format(i) * 4, 16)
expect(v == e, "at output #%d, expected: 0x%x (%d), found %x (%d)".format(i, e, e, v, v))
step(1)
}