Writing a register file in FIRRTL is straightforward: make a memory of machine words and read/write them.

However, when writing a cache, it is different: you typically have a cache line and when writing, only want to update part of the line, a single element of a line of the cache.

What is the idiomatic way to do this in FIRRTL? (And please do not point me at the Rocket implementation in Chisel as I find Chisel to be completely unreadable.)

I can think of at least two ways to do it:

(1) make the memory contain a Vector or Bundle and then select that member of the memory element, something like this:

cmem mem1 : {x:SInt<64>,y:SInt<64>}[4]
infer mport temp_x_mem1 = mem1[i].x, clock
temp_x_mem1 <= foo

(2) do some sort of read-modify-write, something like this:

cmem mem1 : {x:SInt<64>,y:SInt<64>}[4]
infer mport temp_x_mem1 = mem1[i], clock
bar <= temp_x_mem1
bar.x <= foo
infer mport temp_x_mem1_B = mem1[i], clock
temp_x_mem1_B <= bar

I am generating my FIRRTL from another format and I did not plan for this, so when I generate a memory, the only straightforward way is to read or write an entire memory element, not part of one. Therefore way (1) is difficult, but way (2) is straightforward. Would some layer of the FIRRTL optimizer or a subsequent Verilog optimizer make way (2) work as efficiently as way (1) if all of the code occurred in the same module?

1

There are 1 best solutions below

3
On BEST ANSWER

The simplest approach would be to use a FIRRTL memory construct (mem) and avoid CHIRRTL entirely (cmem/smem). The former gives you an explicit mask port on the memory. This then enables you to describe a masked write which is exactly what you want. FIRRTL memory constructs have no Chisel API, but it sounds like you are using something else so this may not be an issue.

Approach (1) will not work as you can't use a part select to describe a memory port. (infer mport temp_x_mem1 = mem1[i].x, clock is illegal CHIRRTL.) Approach (2) will work, but you pay a cycle penalty to do it.

There is a third, idiomatic approach that involves describing the memory in such a way that a FIRRTL compiler will infer the mask. This is done by guarding the write behind a when statement which contains the enable:

circuit Foo:
  module Foo:
    input clock: Clock
    input i: UInt<2>
    input mask: {x: UInt<1>, y: UInt<1>}
    input data: {x: SInt<64>, y: SInt<64>}

    cmem mem1 : {x:SInt<64>,y:SInt<64>}[4]

    infer mport temp_x_mem1 = mem1[i], clock

    when eq(mask.x, UInt<1>(1)):
      temp_x_mem1.x <= data.x
    when eq(mask.y, UInt<1>(1)):
      temp_x_mem1.y <= data.y

Either the Scala-based FIRRTL Compiler or the MLIR-based FIRRTL Compiler will infer the when conditions as the mask.

MLIR-based FIRRTL Compiler output:

circuit Foo :
  module Foo :
    input clock : Clock
    input i : UInt<2>
    input mask : { x : UInt<1>, y : UInt<1> }
    input data : { x : SInt<64>, y : SInt<64> }

    mem mem1 : 
      data-type => { x : SInt<64>, y : SInt<64> }
      depth => 4
      read-latency => 0
      write-latency => 1
      writer => temp_x_mem1
      read-under-write => undefined
    mem1.temp_x_mem1.addr is invalid 
    mem1.temp_x_mem1.en <= UInt<1>(0) 
    mem1.temp_x_mem1.clk is invalid 
    mem1.temp_x_mem1.data is invalid 
    mem1.temp_x_mem1.mask is invalid 
    mem1.temp_x_mem1.addr <= i
    mem1.temp_x_mem1.en <= UInt<1>(1) 
    mem1.temp_x_mem1.clk <= clock 
    mem1.temp_x_mem1.mask.x <= UInt<1>(0) 
    mem1.temp_x_mem1.mask.y <= UInt<1>(0) 
    when eq(mask.x, UInt<1>(1)) : 
      mem1.temp_x_mem1.mask.x <= UInt<1>(1) 
      mem1.temp_x_mem1.data.x <= data.x 
    when eq(mask.y, UInt<1>(1)) : 
      mem1.temp_x_mem1.mask.y <= UInt<1>(1) 
      mem1.temp_x_mem1.data.y <= data.y