吃透Chisel语言.24.Chisel时序电路(四)——Chisel内存(Memory)详解

Chisel时序电路(四)——Chisel内存(Memory)详解

上一篇文章介绍了移位寄存器的实现和两种常用的移位寄存器,在实现串口通信的时候会很有用,对后面的高速接口设计可能又会有一定启发。在数字设计中,不仅仅有寄存器可以保存电路的状态信号,内存(Memory,或叫作存储器,本文都用内存称呼)也是可以保存状态信息的,在处理器设计中内存还用于程序代码和数据的存放,因此十足重要。这一篇文章我们就一起来学习Chisel中的内存实现尤其是同步内存的实现和初始化。

内存和Chisel中的同步内存SyncReadMem

内存可以用一组寄存器来构造,比如Chisel中一个VecReg。然而在硬件上用寄存器实现内存代价太大了,所以大一点的内存结构都构建为SRAM(Static Random Access Memory,静态随机访问存储器)。对于ASIC是使用内存编译器来构建内存的。而FPGA上面包含片上存储器存单元(On-chip Memory Block),也叫做block RAM,即块随机存储器。这些片上内存单元可以组合成大的内存。FPGA上的内存一般有一个读端口和一个写端口,或者两个可以在运行时切换读写的端口。

FPGA和ASIC通常都支持同步内存,同步内存在它们的输入端有寄存器,用于存放读写端口、写数据、写使能信号等。这意味着在设置了地址后的一个时钟周期后才能读取到数据。下图就是个同步内存的示意图:

吃透Chisel语言.24.Chisel时序电路(四)——Chisel内存(Memory)详解_第1张图片

这是个双端口内存,有一个读端口,一个写端口。读端口有一个输入rdAddr,即读地址,和一个输出rdData,即读取到的数据。写端口有三个输入,写地址(wrAddr)、要写入的数据(wrData)和写使能信号(wrEna)。需要特别注意的是,对于所有的输入,内存中都有个寄存器用于实现同步行为。

前面说到我们不想用Chisel中的寄存器来实现内存,所以Chisel中为了支持片上内存,提供了一个专门的内存构造器SyncReadMem。下面的代码就实现了一个1KiB的内存Memory,且输入输出为字节宽度且有写使能信号:

class Memory() extends Module {
    val io = IO(new Bundle {
        val rdAddr = Input(UInt(10.W))
        val rdData = Output(UInt(8.W))
        val wrEnable = Input(Bool())
        val wrAddr = Input(UInt(10.W))
        val wrData = Input(UInt(8.W))
    })
    
    val mem = SyncReadMem(1024, UInt(8.W))
    
    io.rdData := mem.read(io.rdAddr)
    
    when(io.wrEnable) {
        mem.write(io.wrAddr, io.wrData)
    }
}

大概可以看得出来,构造器SyncReadMem生成的内存就是带那几个寄存器的同步内存,所以我们直接给模块定义相关的输入输出端口就行了。SyncReadMem的两个参数分别是内存的大小和内存单元的数据类型,例子中就是定义了1024个字节的内存,即1KiB的内存。另外,mem.read(addr)可以读取内存中地址为addr的数据,同理mem.write(addr, data)可以在内存中地址为addr处写入数据data。由于对1024个内存单元进行索引需要10位的地址,左移读写地址都是10位宽度。

带读写转发的同步内存

上面的代码看起来没啥问题,但是存在一个有趣的点,那就是如果在同一个时钟周期对内存的同一个地址进行读写的话,我们读到的值是旧的值还是新的将要写入的值呢?这个内存的写时读(Read-During-Write)行为值得关注,读到的值具体有三种可能性:

  1. 新写入的值;
  2. 旧的值;
  3. 未定义的值(比如说可能是旧值和新值的位的组合);

FPGA上具体是那种可能性取决于FPGA的类型,有时候是可以指定的。不过在Chisel的文档中明确了,这个读数据是未定义的。

因此,如果我们想要读取到新写入的值的话,我们可以构建一个转发电路,这个电路可以检测读写的地址是否一致,如果一致的话,就把写入的数据转发到读数据端口。下图就是带读写转发的同步内存的示意图:

吃透Chisel语言.24.Chisel时序电路(四)——Chisel内存(Memory)详解_第2张图片

可以看到示意图中,读写地址会进行比较,然后结果和写使能信号相与,用于选择读出的数据到底是写数据的值还是内存中读到的值。同样,因为是同步内存,所以选择信号也需要写入一个寄存器里面,读数据会在下一个时钟周期输出。

下面是带读写转发的同步内存的Chisel实现:

class ForwardingMemory() extends Module {
    val io = IO(new Bundle {
        val rdAddr = Input(UInt(10.W))
        val rdData = Output(UInt(8.W))
        val wrEnable = Input(Bool())
        val wrAddr = Input(UInt(10.W))
        val wrData = Input(UInt(8.W))
    })
    
    val mem = SyncReadMem(1024, UInt(8.W))
    
    val wrDataReg = RegNext(io.wrData)
    val doForwardReg = RegNext(io.wrAddr === io.rdAddr && io.wrEnabale)
    
    val memData = mem.read(io.rdAddr)
    
    when(io.wrEnable) {
        mem.write(io.wrAddr, io.wrData)
    }
    
    io.rdData := Mux(doForwardReg, wrDataReg, memData)
}

代码依然难度不大,我们需要把写入的数据放到一个寄存器wrDataReg里面,这样下个时钟周期才可用,一方面是适配同步内存,另一方面是为下一周期的读取提供值。同时代码还比较了两个输入地址,即读地址rdAddr和写地址wrAddr,检查了写使能信号wrEnable,用于判断是否满足转发的条件。这个转发条件也是应该延迟一个时钟周期,因此需要整个寄存器doForwardReg来存放转发条件是否成立。最后用一个Mux来选择读取到的数据是转发数据还是内存读到的数据。

关于Chisel中的Mem

Chisel中还提供了另一个构造器Mem,这个构造器构造的内存是同步写、异步读的。但这种类型在FPGA上是不能直接使用的,综合工具会把它用触发器构建出来,而不是用block RAM。因此,我们还是应该使用SyncReadMem。如果确实需要实现异步读的行为,而且你用的FPGA里面有对应的资源(比如类似Xilinx FPGA里面的LUTRAM的结构),那你可以手动实现并封装为一个BlackBox。一般来说供应商会给出代码模板,可以直接拿来用。

Chisel内存的初始化(实验性特性内容)

FPGA上的内存可以用二进制或十六进制初始化文件进行初始化,不过注意这些文件是简单的ASCII文本文件而不是二进制文件,文件的行数应该和相应的内存的条目数是一样的。文件中的每一个字符代表一位或四位的值,其中二进制文件用.bin作为文件后缀,每个字符表示一位二进制,十六进制文件用.hex作为文件后缀,每个字符表示四位二进制数。

在Chisel中可以使用loadMemoryFromFile函数对内存进行初始化,用法如下:

// 需要使用这两个包
import chisel3.util.experimental.loadMemoryFromFile
import firrtl.annotations.MemoryLoadFileType

// 加载十六进制文本文件到内存
loadMemoryFromFile(mem, "mem.hex", MemoryLoadFileType.Hex)
// 加载二进制文本文件到内存
loadMemoryFromFile(mem, "mem.bin", MemoryLoadFileType.Binary)

这个loadMemoryFromFile会在数据文本文件所在文件夹下生成一个单独的Verilog文件,文件内容类似下面的代码:

module BindsTo_0_Memory(
  input        clock,
  input        reset,
  input  [9:0] io_rdAddr,
  output [7:0] io_rdData,
  input        io_wrEnable,
  input  [9:0] io_wrAddr,
  input  [7:0] io_wrData
);

initial begin
  $readmemh("mem.hex", Memory.mem);
end
                      endmodule

bind Memory BindsTo_0_Memory BindsTo_0_Memory_Inst(.*);

这个文件会在ChiselTest中工作,初始化也是基于对Verilog中的readmembreadmemh的调用来实现的。

结语

这一篇文章我们学习了Chisel时序电路中最后的内容,包括了同步内存的实现和使用,还谈及了内存初始化这个实验性的特性。当然了,关于内存的内容并不只有这些,还有一些更加高阶的内容,会在本系列的最后讲到,内容基于Chisel官方文档的Explainations,会涵盖本系列教程基础部分未涉及的高阶内容。下一个部分,我们会学习Chisel中的一些中阶内容,分别面向信号处理、有限状态机和通信状态机,期间会涉及到Chisel的一些中阶特性,在数字设计中很有用。

你可能感兴趣的:(吃透Chisel语言!!!,Chisel,fpga开发,同步内存,SyncReadMem,CPU设计实现)