第2部分:RocketChip:连接RAM

上一部分中,我们基于Altera / Intel FPGA组装了没有任何RAM的微控制器。 但是,该板上有一个已安装SO-DIMM DDR2 1Gb的连接器,很明显,我想使用该连接器。 为此,我们需要将DDR2控制器和ALTMEMPHY接口包装在一个模块中,该模块对于整个RocketChip中使用的TileLink存储器协议是可以理解的。 在削减-触觉调试,蛮力编程和耙。


如您所知,计算机科学有两个主要问题:缓存失效和变量命名。 在KDPV,您会看到一个难得的时刻-CS遇到的两个主要问题 并正在密谋


免责声明:除了上一篇文章中的警告,我强烈建议您在重复实验之前仔细阅读本文,以免损坏FPGA,存储器模块或电源电路。


这次,我想,如果不启动Linux,那么至少要连接RAM,在我的板上已经有一个完整的千兆字节(或者最多可以容纳四个)。 提出了一个成功标准,以考虑通过一堆GDB + OpenOCD进行读写的能力,其中包括未按16个字节对齐的地址(一个请求到内存的宽度)。 乍一看,您只需要稍微修改一下配置即可,SoC生成器无法立即支持RAM。 它支持它,但是通过MIG接口(当然还有Microsemi的其他接口)。 通过标准接口,AXI4也支持它,但是据我所知,要获得它并不是那么容易(至少,不是掌握Platform Designer)。


抒情离题:据我了解,ARM开发了一系列颇受欢迎的“芯片内” AXI接口。 在这里,人们会认为它已全部获得专利并已关闭。 但是,当我注册(没有任何“大学计划”和其他任何内容,只需通过电子邮件并填写调查表)并获得规范后,我就感到惊喜。 当然,我不是律师,但是该标准似乎很开放:您要么必须使用ARM许可的内核,要么根本不声称与ARM兼容, 然后一切似乎都还可以 。 但总的来说,当然,请阅读许可证,与律师一起阅读,等等。


猴子和TileLink(寓言)


任务似乎很简单,我打开了供应商已经在项目中提供的ddr2_64bit模块板的说明:


英特尔财产和一般
 module ddr2_64bit ( local_address, local_write_req, local_read_req, local_burstbegin, local_wdata, local_be, local_size, global_reset_n, pll_ref_clk, soft_reset_n, local_ready, local_rdata, local_rdata_valid, local_refresh_ack, local_init_done, reset_phy_clk_n, mem_odt, mem_cs_n, mem_cke, mem_addr, mem_ba, mem_ras_n, mem_cas_n, mem_we_n, mem_dm, phy_clk, aux_full_rate_clk, aux_half_rate_clk, reset_request_n, mem_clk, mem_clk_n, mem_dq, mem_dqs); input [25:0] local_address; input local_write_req; input local_read_req; input local_burstbegin; input [127:0] local_wdata; input [15:0] local_be; input [2:0] local_size; input global_reset_n; input pll_ref_clk; input soft_reset_n; output local_ready; output [127:0] local_rdata; output local_rdata_valid; output local_refresh_ack; output local_init_done; output reset_phy_clk_n; output [1:0] mem_odt; output [1:0] mem_cs_n; output [1:0] mem_cke; output [13:0] mem_addr; output [1:0] mem_ba; output mem_ras_n; output mem_cas_n; output mem_we_n; output [7:0] mem_dm; output phy_clk; output aux_full_rate_clk; output aux_half_rate_clk; output reset_request_n; inout [1:0] mem_clk; inout [1:0] mem_clk_n; inout [63:0] mem_dq; inout [7:0] mem_dqs; ... 

普遍的看法是:“任何俄语文档都必须以以下单词开头:“因此,它不起作用。” 但是这里的界面并不完全直观 ,因此我们仍然阅读它 。 在说明中,我们立即得知使用DDR2并非易事。 您需要配置PLL,进行一些校准, crack-fex- local_init_donelocal_init_done信号,即可工作。 通常,此处的命名逻辑大致如下:带有local_前缀的名称是“用户”接口, mem_mem_端口直接输出到连接至内存模块的pll_ref_clk需要向pll_ref_clk发送时钟信号,该时钟信号应具有配置模块时指定的频率-其余的将从中获取频率,各种输入和输出复位以及频率输出,用户界面应该与之同步工作。


让我们创建对ddr2_64bit模块的内存和接口的外部信号的描述:


特质记忆
 trait MemIf { val local_init_done = Output(Bool()) val global_reset_n = Input(Bool()) val pll_ref_clk = Input(Clock()) val soft_reset_n = Input(Bool()) val reset_phy_clk_n = Output(Clock()) val mem_odt = Output(UInt(2.W)) val mem_cs_n = Output(UInt(2.W)) val mem_cke = Output(UInt(2.W)) val mem_addr = Output(UInt(14.W)) val mem_ba = Output(UInt(2.W)) val mem_ras_n = Output(UInt(1.W)) val mem_cas_n = Output(UInt(1.W)) val mem_we_n = Output(UInt(1.W)) val mem_dm = Output(UInt(8.W)) val phy_clk = Output(Clock()) val aux_full_rate_clk = Output(Clock()) val aux_half_rate_clk = Output(Clock()) val reset_request_n = Output(Bool()) val mem_clk = Analog(2.W) val mem_clk_n = Analog(2.W) val mem_dq = Analog(64.W) val mem_dqs = Analog(8.W) def connectFrom(mem_if: MemIf): Unit = { local_init_done := mem_if.local_init_done mem_if.global_reset_n := global_reset_n mem_if.pll_ref_clk := pll_ref_clk mem_if.soft_reset_n := soft_reset_n reset_phy_clk_n := mem_if.reset_phy_clk_n mem_odt <> mem_if.mem_odt mem_cs_n <> mem_if.mem_cs_n mem_cke <> mem_if.mem_cke mem_addr <> mem_if.mem_addr mem_ba <> mem_if.mem_ba mem_ras_n <> mem_if.mem_ras_n mem_cas_n <> mem_if.mem_cas_n mem_we_n <> mem_if.mem_we_n mem_dm <> mem_if.mem_dm mem_clk <> mem_if.mem_clk mem_clk_n <> mem_if.mem_clk_n mem_dq <> mem_if.mem_dq mem_dqs <> mem_if.mem_dqs phy_clk := mem_if.phy_clk aux_full_rate_clk := mem_if.aux_full_rate_clk aux_half_rate_clk := mem_if.aux_half_rate_clk reset_request_n := mem_if.reset_request_n } } class MemIfBundle extends Bundle with MemIf 

类别dd2_64bit
 class ddr2_64bit extends BlackBox { override val io = IO(new MemIfBundle { val local_address = Input(UInt(26.W)) val local_write_req = Input(Bool()) val local_read_req = Input(Bool()) val local_burstbegin = Input(Bool()) val local_wdata = Input(UInt(128.W)) val local_be = Input(UInt(16.W)) val local_size = Input(UInt(3.W)) val local_ready = Output(Bool()) val local_rdata = Output(UInt(128.W)) val local_rdata_valid = Output(Bool()) val local_refresh_ack = Output(Bool()) }) } 

然后第一堆耙子在等着我:首先,在ROMGenerator类之后,我认为可以通过全局变量将内存控制器从设计的深度中拉出来,而Chisel会以某种方式转发导线本身。 无法解决。 因此,我必须制作一个遍及整个层次结构的MemIfBundle线束。 为什么它不伸出BlackBox ,并且一次也不连接? 事实是,使用BlackBox所有外部端口都填充到val io = IO(new Bundle { ... }) 。 如果将整个MemIfBundle制成捆绑包中的一个变量,则此变量的名称将成为所有端口名称的前缀,并且名称不会与块的接口重合。 也许可以通过某种方式更充分地完成这项工作,但现在让我们这样吧。


此外,通过与其他TileLink设备(主要生活在rocket-chip/src/main/scala/tilelinkrocket-chip/src/main/scala/tilelink ,尤其是BootROM ,我们将描述与内存控制器的接口:


 class AltmemphyDDR2RAM(implicit p: Parameters) extends LazyModule { val MemoryPortParams(MasterPortParams(base, size, beatBytes, _, _, executable), 1) = p(ExtMem).get val node = TLManagerNode(Seq(TLManagerPortParameters( Seq(TLManagerParameters( address = AddressSet.misaligned(base, size), resources = new SimpleDevice("ram", Seq("sifive,altmemphy0")).reg("mem"), regionType = RegionType.UNCACHED, executable = executable, supportsGet = TransferSizes(1, 16), supportsPutFull = TransferSizes(1, 16), fifoId = Some(0) )), beatBytes = 16 ))) override lazy val module = new AltmemphyDDR2RAMImp(this) } class AltmemphyDDR2RAMImp(_outer: AltmemphyDDR2RAM)(implicit p: Parameters) extends LazyModuleImp(_outer) { val (in, edge) = _outer.node.in(0) val ddr2 = Module(new ddr2_64bit) val mem_if = IO(new MemIfBundle) // TODO    } trait HasAltmemphyDDR2 { this: BaseSubsystem => val dtb: DTB val mem_ctrl = LazyModule(new AltmemphyDDR2RAM) mem_ctrl.node := mbus.toDRAMController(Some("altmemphy-ddr2"))() } trait HasAltmemphyDDR2Imp extends LazyModuleImp { val outer: HasAltmemphyDDR2 val mem_if = IO(new MemIfBundle) mem_if <> outer.mem_ctrl.module.mem_if } 

ExtMem标准的ExtMem密钥ExtMem我们从SoC ExtMem提取外部存储器参数( 这种奇怪的语法使我说:“我知道它们将返回case类MemoryPortParameters的实例(这由在编译Scala代码阶段根据条件的密钥类型来保证)我们不会从Option[MemoryPortParams]等于None内容来运行时,但是在System.scala没有创建内存控制器的必要...),因此,我不需要case类,并且需要其中的某些字段”)。 接下来,我们创建TileLink设备的管理器端口(TileLink协议提供了几乎所有与内存相关的交互:DDR控制器和其他内存映射设备,处理器缓存,也许还有其他东西,每个设备可以有多个端口,每个端口该设备既可以是管理员,也可以是客户端)。 beatBytes我了解, beatBytes设置一个事务的大小,并且我们与控制器交换了16个字节。 我们在System.scala中的正确位置HasAltmemphyDDR2Imp HasAltmemphyDDR2HasAltmemphyDDR2Imp ,编写配置


 class BigZeowaaConfig extends Config ( new WithNBreakpoints(2) ++ new WithNExtTopInterrupts(0) ++ new WithExtMemSize(1l << 30) ++ new WithNMemoryChannels(1) ++ new WithCacheBlockBytes(16) ++ new WithNBigCores(1) ++ new WithJtagDTM ++ new BaseConfig ) 

我在AltmemphyDDR2RAMImp做了一个“猫头鹰的素描”, AltmemphyDDR2RAMImp对设计进行了综合(大约在30MHz左右,最好从25MHz开始计时),然后将手指放在存储模块和FPGA芯片上,然后将其上传到板上。 然后,我看到了真正的直观界面:这是当您在gdb中给出命令以写入内存并由冻结的处理器执行以下操作时: 烧了 手指感到强烈的热量,您需要紧急按下板上的重置按钮并固定控制器。


阅读DDR2控制器的文档


显然,是时候阅读端口列表之外的控制器上的文档了。 那么,我们在这里有什么呢?。哎呀,事实证明,不应该使用pll_ref_clk (25MHz)而不是同步设置具有local_前缀的I / local_ ,而应该使用phy_clkphy_clk半速率控制器phy_clk一半的存储频率,或者在我们的情况下, aux_half_rate_clk (也许毕竟是aux_full_rate_clk吗?),它aux_full_rate_clk了完整的内存频率,并且一分钟为166MHz。


因此,有必要跨越频域的边界。 根据旧的记忆,我决定使用闩锁,或更确切地说,使用链锁:


  +-+ +-+ +-+ +-+ --| |--| |--| |--| |---> +-+ +-+ +-+ +-+ | | | | ---+ | | | inclk | | | | | | --------+----+ | outclk | | ------------------+ output enable 

但是,经过一小时的修改,我得出的结论是,我无法处理“标量”锁存器中的两个队列(在高频域,反之亦然),每个队列都将具有方向性信号( readyvalid ),即使如此,也要确保一些Beatik不会落后于沿途一两拍。 一段时间后,我意识到描述ready下的同步(在没有通用时钟信号的情况下valid )也是一项类似于创建非阻塞数据结构的任务,从某种意义上说,您需要考虑并正式证明很多东西,容易犯错误,很难注意到,最重要的是,所有事情已经在我们之前实现的方法:英特尔有一个dcfifo原语,它是可配置长度和宽度的队列,可以从不同的频域读取和写入。 结果,我利用了新鲜凿子的实验机会,即参数化的黑匣子:


 class FIFO (val width: Int, lglength: Int) extends BlackBox(Map( "intended_device_family" -> StringParam("Cyclone IV E"), "lpm_showahead" -> StringParam("OFF"), "lpm_type" -> StringParam("dcfifo"), "lpm_widthu" -> IntParam(lglength), "overflow_checking" -> StringParam("ON"), "rdsync_delaypipe" -> IntParam(5), "underflow_checking" -> StringParam("ON"), "use_eab" -> StringParam("ON"), "wrsync_delaypipe" -> IntParam(5), "lpm_width" -> IntParam(width), "lpm_numwords" -> IntParam(1 << lglength) )) { override val io = IO(new Bundle { val data = Input(UInt(width.W)) val rdclk = Input(Clock()) val rdreq = Input(Bool()) val wrclk = Input(Clock()) val wrreq = Input(Bool()) val q = Output(UInt(width.W)) val rdempty = Output(Bool()) val wrfull = Output(Bool()) }) override def desiredName: String = "dcfifo" } 

他写了一个简单的双目任意数据类型:


 object FIFO { def apply[T <: Data]( lglength: Int, output: T, outclk: Clock, input: T, inclk: Clock ): FIFO = { val res = Module(new FIFO(width = output.widthOption.get, lglength = lglength)) require(input.getWidth == res.width) output := res.io.q.asTypeOf(output) res.io.rdclk := outclk res.io.data := input.asUInt() res.io.wrclk := inclk res } } 

侦错


之后,代码变成通过两个已经单向的队列在域之间传输消息: tl_req / ddr_reqddr_resp / tl_resp (带有tl_前缀的前缀与TileLink一起计时,事实是ddr_与内存控制器一起使用)。 问题是,无论如何,一切都陷入僵局,有时还很温暖。 如果过热的原因是同时设置local_read_reqlocal_write_req ,那么处理死锁就不那么容易了。 同时的代码就像


 class AltmemphyDDR2RAMImp(_outer: AltmemphyDDR2RAM)(implicit p: Parameters) extends LazyModuleImp(_outer) { val addrSize = log2Ceil(_outer.size / 16) val (in, edge) = _outer.node.in(0) val ddr2 = Module(new ddr2_64bit) require(ddr2.io.local_address.getWidth == addrSize) val tl_clock = clock val ddr_clock = ddr2.io.aux_full_rate_clk val mem_if = IO(new MemIfBundle) class DdrRequest extends Bundle { val size = UInt(in.a.bits.size.widthOption.get.W) val source = UInt(in.a.bits.source.widthOption.get.W) val address = UInt(addrSize.W) val be = UInt(16.W) val wdata = UInt(128.W) val is_reading = Bool() } val tl_req = Wire(new DdrRequest) val ddr_req = Wire(new DdrRequest) val fifo_req = FIFO(2, ddr_req, ddr_clock, tl_req, clock) class DdrResponce extends Bundle { val is_reading = Bool() val size = UInt(in.d.bits.size.widthOption.get.W) val source = UInt(in.d.bits.source.widthOption.get.W) val rdata = UInt(128.W) } val tl_resp = Wire(new DdrResponce) val ddr_resp = Wire(new DdrResponce) val fifo_resp = FIFO(2, tl_resp, clock, ddr_resp, ddr_clock) //    TileLink withClock(ddr_clock) { //     } 

为了解决这个问题,我决定正式注释掉withClock(ddr_clock)所有代码(不是,从视觉上看就像创建一个流),并用一个可以正常工作的存根替换:


  withClock (ddr_clock) { ddr_resp.rdata := 0.U ddr_resp.is_reading := ddr_req.is_reading ddr_resp.size := ddr_req.size ddr_resp.source := ddr_req.source val will_read = Wire(!fifo_req.io.rdempty && !fifo_resp.io.wrfull) fifo_req.io.rdreq := will_read fifo_resp.io.wrreq := RegNext(will_read) } 

正如我后来意识到的那样,这个存根也不起作用,因为我为“可靠性”添加了Wire(...)构造以表明它是一个命名的导线,实际上仅将参数用作创建的原型。含义类型, 但未将其绑定到expression-argument 。 另外,当我尝试阅读仍然生成的内容时,我意识到在仿真模式下,有很多关于不遵守TileLink协议的断言。 它们可能稍后会派上用场,但是到目前为止,还没有尝试运行模拟-为什么要开始模拟呢? Verilator可能不了解Alter的IP内核,ModelSim Starter Edition很可能会拒绝模拟如此庞大的项目,但是我也发誓缺乏用于模拟的控制器模型。 为了生成它,您可能需要首先切换到新版本的控制器(因为旧版本已在古代Quartus中配置)。


实际上,这些代码块是从几乎可以运行的版本中获取的,而不是几个小时前进行了积极调试的版本。 但是,您更好;)顺便说一句,如果将WithNBigCores(1)设置替换为WithNSmallCores(1) ,则可以不断更快地重新组装设计-从内存控制器的基本功能来看,似乎没有什么区别。 还有一个小技巧:为了不每次都将相同的命令驱动到gdb中(至少我会话之间没有命令的历史记录),您可以在命令行中简单地输入类似的内容


 ../../rocket-tools/bin/riscv32-unknown-elf-gdb -q -ex "target remote :3333" -ex "x/x 0x80000000" ../../rocket-tools/bin/riscv32-unknown-elf-gdb -q -ex "target remote :3333" -ex "set variable *0x80000000=0x1234" 

并根据需要使用常规Shell方式运行。


总结


结果,获得了以下与控制器一起使用的代码:


  withClock(ddr_clock) { val rreq = RegInit(false.B) //   (  ) val wreq = RegInit(false.B) //   (  ) val rreq_pending = RegInit(false.B) //   ( ) ddr2.io.local_read_req := rreq ddr2.io.local_write_req := wreq // -   :) ddr2.io.local_size := 1.U ddr2.io.local_burstbegin := true.B //    (    q FIFO) ddr2.io.local_address := ddr_req.address ddr2.io.local_be := ddr_req.be ddr2.io.local_wdata := ddr_req.wdata //  ,    ddr_resp.is_reading := ddr_req.is_reading ddr_resp.size := ddr_req.size ddr_resp.source := ddr_req.source //   ,   ** ** val will_read_request = !fifo_req.io.rdempty && !rreq && !wreq && !rreq_pending && ddr2.io.local_ready // ,     val will_respond = !fifo_resp.io.wrfull && ( (rreq_pending && ddr2.io.local_rdata_valid) || (wreq && ddr2.io.local_ready)) val request_is_read = RegNext(will_read_request) fifo_req.io.rdreq := will_read_request fifo_resp.io.wrreq := will_respond //  ,     when (request_is_read) { rreq := ddr_req.is_reading rreq_pending := ddr_req.is_reading wreq := !ddr_req.is_reading } when (will_respond) { rreq := false.B wreq := false.B ddr_resp.rdata := ddr2.io.local_rdata } //    ,    when (rreq && ddr2.io.local_ready) { rreq := false.B } } 

在这里,我们仍然会略微更改完成条件:我已经看到,不用任何内存,记录的数据就好像读取了一样,因为它是一个缓存。 因此,我们编译了一段简单的代码:


 #include <stdint.h> static volatile uint8_t *x = (uint8_t *)0x80000000u; void entry() { for (int i = 0; i < 1<<24; ++i) { x[i] = i; } } 

 ../../rocket-tools/bin/riscv64-unknown-elf-gcc test.c -S -O1 

结果,我们获得了以下汇编列表清单,初始化了前16 MB内存:


  li a5,1 slli a5,a5,31 li a3,129 slli a3,a3,24 .L2: andi a4,a5,0xff sb a4,0(a5) addi a5,a5,1 bne a5,a3,.L2 

bootrom/xip/leds.S的开头。 现在,不可能将所有内容仅保留在一个缓存中。 仍然需要运行Makefile,在Quartus中重建项目,将其填充到面板中,连接OpenOCD + GDB,然后……欢呼雀跃,胜利了:


 $ ../../rocket-tools/bin/riscv32-unknown-elf-gdb -q -ex "target remote :3333" Remote debugging using :3333 warning: No executable has been specified and target does not support determining executable automatically. Try using the "file" command. 0x0000000000010014 in ?? () (gdb) x/x 0x80000000 0x80000000: 0x03020100 (gdb) x/x 0x80000100 0x80000100: 0x03020100 (gdb) x/x 0x80000111 0x80000111: 0x14131211 (gdb) x/x 0x80010110 0x80010110: 0x13121110 (gdb) x/x 0x80010120 0x80010120: 0x23222120 

是这样,我们将在下一个系列中找到(我也不能说性能,稳定性等)。


代码: AltmemphyDDR2RAM.scala

Source: https://habr.com/ru/post/zh-CN456172/


All Articles