这篇是介绍关于rocket-chip rom的内容,也叫bootrom。
bootrom的具体scala代码为:/rocket-chip/src/main/scala/devices/tilelink/BootROM.scala
在rocket-chip功能说明篇中,我生成了一个CPU,我打算按照我的配置,一点一点的说明这些功能是如何使用的,有些功能还会介绍如何修改相应的scala代码。
第一个介绍的是rom,也叫bootrom。我一开始写了rom的代码,想着让CPU从rom中正常启动,然后执行完rom的代码后跳转至sram中的。但在我写好rom的代码后,我发现了一个问题。那就是rocket-chip生成的rom是固化代码的,它是从/rocket-chip/bootrom目录中读取bootrom.img作为rom的原始代码,然后在chisel & firrtl生成代码时,将bootrom.img的内容写入到bootrom模块中。
那么我们的问题是:如何将我们自己写的rom 代码固化在bootrom中?
或者换一个思路:如何让rocket-chip从我们预想的地址中进行初始运行?
针对上面的问题,我想到了三个办法。也都分别做了实验,下面将对这三个办法一一说明。
- 修改/rocket-chip/bootrom/bootrom.img的内容,让rocket-chip从我们的代码开始运行。
- 修改rocket-chip的启动地址,让它不再从bootrom中启动,从其他地址进行启动。
- 修改 /rocket-chip/src/main/scala/devices/tilelink/BootROM.scala的代码,让bootrom中的代码内容可以被自由改写,且能灵活载入。
方法一:
需要修改/rocket-chip/bootrom/bootrom.img的内容,就要知道*.img是什么格式,从什么文件转换过来的。*.img是镜像的格式,可以从二进制文件中转换过来。大家查看/rocket-chip/bootrom/Makefile就能找到转换的命令。
至于二进制文件如何来?二进制文件从elf中来,elf的生成过程可以参考我之前的博客:
https://blog.csdn.net/a_weiming/article/details/89006615
而elf转bin的命令如下:
riscv32-unknown-elf-objcopy XXX.elf -O binary XXX.bin
而bin转img的命令如下:
dd if=XXX.bin of=XXX.img bs=128 count=3
dd指令的详解看这里:https://blog.csdn.net/yanyuan_smartisan/article/details/79543805
到这一步为止,得到了我们自己代码的XXX.img了,下一步就是让rocket-chip吃我们自己的XXX.img。
不修改scala代码的话,就直接命名为bootrom.img,然后覆盖到/rocket-chip/bootrom目录。
修改scala代码的话,可以修改 /home/x/rocket-chip/src/main/scala/subsystem/Configs.scala文件,第42行代码:
case BootROMParams => BootROMParams(contentFileName = "./bootrom/bootrom.img")
修改为:
case BootROMParams => BootROMParams(contentFileName = "./bootrom/XXX.img")
XXX.img就是你自己代码的img文件,然后最后一步就是进入vsim目录,运行make verilog命令生成带有你自己rom代码的RTL。
这是生成后的bootrom模块,我们对一下固化的代码和反汇编的情况。
rom.dump是我生成的反汇编文件,上面的是RTL固化的bootrom.img代码。
因为我采用了支持压缩指令的gcc去编译,所以编译出来的代码是支持16位的压缩代码的。
对比着看,可以看到地址为10000时,32bits的指令为41014081,下一个word的地址为10004,指令为42014181,都是能和反汇编一一对应的。
到这里,我们已经修改完了,可以直接用这份RTL去跑我们的程序了。
至于rom的代码内容是什么?大家可以参照我之前写的program.S
https://blog.csdn.net/a_weiming/article/details/89006615
rom代码就是这个program.S的内容,而rom的main函数中只有一条代码,那就是将PC跳转至0x8000_0000的位置。我设置sram的起始地址就是0x8000_0000。
方法二:
方法二是直接不使用bootrom,将rocket-chip的启动地址直接改为可配置的形式,具体是修改方法可以参考我之前的博客:
https://blog.csdn.net/a_weiming/article/details/93789311
这样修改后,我们就能自由配置rocket-chip的启动地址了,可以直接配置为0x8000_0000,直接从sram中启动,跳过了bootrom的启动过程。
方法三:
首先,我们来分析一下方法一,方法一可行,但每次改bootrom代码,我们都要重新生成bin,再转为img,并再次生成RTL,这个过程相当复杂,相当繁琐,当然你的bootrom code是固定好不再改了,那这种方式还是不错的。至于方法二,也可行,但没有了rom的特性,修改了启动地址后,可以随意指向哪个物理地址,如果指向的物理地址是一个sram,那么就是一个可读可写的空间,而rom的特性就是只读,不能改写,所以本质上还是有差别的。当然你可以在总线上挂一个自己编写的rom,然后将启动地址指向这个rom,这样就能解决这个问题,但额外的工作就是你要自己写一个符合总线协议的rom模块。
基于上面的两个分析,我想了第三个办法,既有rom的只读特性,又能灵活装载测试程序,不用每次都重新生成RTL代码。
方法就是修改 /rocket-chip/src/main/scala/devices/tilelink/BootROM.scala的代码,让bootrom中的代码内容可以被自由改写。修改的内容如下:
lazy val module = new LazyModuleImp(this) {
val contents = contentsDelayed
val wrapSize = 1 << log2Ceil(contents.size)
require (wrapSize <= size)
val (in, edge) = node.in(0)
val words = (contents ++ Seq.fill(wrapSize-contents.size)(0.toByte)).grouped(beatBytes).toSeq
val bigs = words.map(_.foldRight(BigInt(0)){ case (x,y) => (x.toInt & 0xff) | y << 8})
//val rom = Vec(bigs.map(x => UInt(x, width = 8*beatBytes)))
//x_modify
val rom = Mem(4096,UInt(32.W))
in.d.valid := in.a.valid
in.a.ready := in.d.ready
val index = in.a.bits.address(log2Ceil(wrapSize)-1,log2Ceil(beatBytes))
val high = if (wrapSize == size) UInt(0) else in.a.bits.address(log2Ceil(size)-1, log2Ceil(wrapSize))
in.d.bits := edge.AccessAck(in.a.bits, Mux(high.orR, UInt(0), rom(index)))
// Tie off unused channels
in.b.valid := Bool(false)
in.c.ready := Bool(true)
in.e.ready := Bool(true)
}
我屏蔽了val rom = Vec(bigs.map(x => UInt(x, width = 8*beatBytes)))这一句,这一句是将bootrom.img的内容赋值只向量类型(Vec)。修改后为:val rom = Mem(4096,UInt(32.W))。
将rom的类型声明为存储器的类型(Mem)。就改这一句就可以了,其他都不用改。然后生成出来的代码是这样的:
rom的内容不是之前的那种固化的值,而是变为了reg类型。这样的话,我们就能在TB中对它自由赋值了,这样就能自由改变rom的代码而又不用每次都重新编译RTL。下面是我在TB中赋值的代码。
一条是赋值给sram的,一条是赋值给rom的。
最后我们来看一下rom代码运行的情况。
先看cpu从rom中读代码的总线行为:
cpu读rom的总线是Tilelink,具体的协议大家可以自行去查资料,后面有时间我也可能会出些说明。a接口是cpu->rom的,d接口是rom->cpu的,两个接口都是单向的,a接口是请求数据的,d接口是返回数据的,当然这里只是简单的说明,它们还有更复杂的用法。当a接口的ready&valid有效时,address和size是有效的,而且rom会单拍给出数据,也就是d接口会当拍给出a_address的数据,d接口也是当ready&和valid同时有效时,data输出有效,可以看到输出的数据和上面的反汇编是一致的。
然后我们看cpu读取数据后的运行情况:
可以看到读rom的操作在前,运行代码的操作在后,当读了数据后cpu才开始运行,因为宽度的原因,我不能再放大了,所以只能看到压缩的数据,但不难看出,第一个自行的io_pc为0x10000,而起指令io_inst_0为0x4081。还需要补充的是,io_pc & io_inst_0这两个信号都是在csr模块中的。
最后我们看一下rom代码执行完后,跳转至sram的过程:
首先我们看一下反汇编,在0x10104的指令是0x800007b7,将0x80000这个立即数赋值到a5的通用寄存器中(lui—将高20位的立即数赋值);在0x10108的指令是0x8782,跳转至寄存器a5中;在0x1010a的指令是0x8082,从main函数中返回(return),当然这条指令是不会被执行的,因为前一条就是一条跳转指令。
然后再看波形,io_pc 为0x10104和0x10108时,指令是对得上反汇编的。在0x10108后的io_pc变为了0x8000_0000,也就是跳转指令生效了,pc跳转至0x8000_0000这个位置进行执行。
到这里,关于rom的内容已经介绍完毕了,如果觉得还是有用的那就给个赞吧,谢谢。
还有,我尝试过上传方法一和方法三中的RTL,我本来想设置下载的积分为1的,但每次审核后,都会自动改为3了,所以我就不公开下载链接了,如果想要的朋友,可以私下找我,因为后面的一些功能说明也会按照这几份RTL进行说明的。