RTL:指令的处理
在RTL里,由于我将指令的分析与处理拆成了两个单元,所以指令的拆解也被分到了两个元件。由于指令的opcode部分没有重叠,所以我直接在组合逻辑里用case来区分指令opcode,在内部再根据funct3 funct7等值进行区分。nemu里的指令分析用到了正则匹配,但在这里我们不需要考虑。
always(*) begin //指令处理 case(opcode)begin // endcase end
同样地,指令处理单元也使用一样的设计。
为了避免出现组合逻辑循环更新的情况(pc也放在组合逻辑里的话,pc更新以后会更新指令,进而又会导致pc更新),把pc和其他寄存器一样也设置为时序逻辑更新,同时准备一个pc_next在组合逻辑里每次更新为pc+4,这样每个周期更新到pc上。同时准备一个跳转标志和跳转地址。如果指令涉及到跳转就更新这两个值,覆盖到pc_next上。
-------------------------------------------
一个好用的指令拆分网站:rvcodec.js · RISC-V Instruction Encoder/Decoder 可以帮助分析指令格式和数值
在指令分析时,一个注意点是立即数,查看手册,有的立即数是有符号,有的立即数是无符号的,我们在把立即数拓展到32位时,根据情况采用符号拓展和零拓展。符号拓展需要把首位符号位重复,填充满需要填充的位数,零拓展则是直接用0填充。两种拓展某些时候不一样,所以最好对照手册,不要混淆。
此外,有的指令本身虽然只有funct3,但是内部还有其他字段用于区分,不作为立即数,比如srli这样的指令。
左侧的红色部分不是立即数,而是shtyp,这个字段充当了类似funct7的效果,配合funct3,用于区分逻辑左移,逻辑右移和算术右移。它的立即数部分实际只有[24:20]这5位,即shamt,而且它还是无符号数,因为移位不存在负数,拓展时也要使用零拓展。
----------------
除此之外,lbu指令也是一个值得关注的地方。lbu指令在测试集的bit movsx 和unalign里经常出现。我在这条指令的处理上头疼了好几天。在讲义中规定:
pmem_read和pmem_write需要对地址进行对齐以后进行4字节读写
extern "C" int pmem_read(int raddr) { // 总是读取地址为`raddr & ~0x3u`的4字节返回 } extern "C" void pmem_write(int waddr, int wdata, char wmask) { // 总是往地址为`waddr & ~0x3u`的4字节按写掩码`wmask`写入`wdata` // `wmask`中每比特表示`wdata`中1个字节的掩码, // 如`wmask = 0x3`代表只写入最低2个字节, 内存中的其它字节保持不变 }
在nemu中,按照vaddr->paddr->pmem->host_read的顺序可以看到,nemu本身是不做地址对齐的,也就是说nemu支持非对齐地址。那么npc这里要求地址对齐难度就会大一些。要通过difftest,这里就必须要支持非对齐地址的读写(后期一生一芯项目里应该会对编译器进行设置,提前进行地址对齐)。
我使用的是uint32数组作为存储,所以字节的排列顺序和uint8不一样,uint32数组单个元素即四字节,除以4会天然对齐,所以这一块其实省了调整字节顺序的代码。比如说
80000000: 00000413 li s0,0
这样一条指令,低地址其实是0x13,不是0x00。riscv内部采用小端序存储,所以如果你用的是uint8,需要关心字节的显示和实际顺序问题。
uint32元素的字节显示顺序 高地址 ------> 低地址 03 02 01 00 !!!!需要注意,lw sw这些指令对应4字节时,他们要的顺序本来就是这样的顺序,不需要再调整
lbu指令和sb指令对应,lbu指令出现问题,未必是lbu实现有问题,也可能是sb实现有问题。由于pmem已经被限制了地址对齐,所以lbu在读取到32位数时,需要根据实际地址偏移进行右移,然后再取8位地址。 同样的,sb指令在写入前也需要调整data,把data和掩码左移到适当的位置。
此外还需注意,lh lb指令,他们用pmem_read读取到再右移的数据只有一部分,相当于是做了零拓展,但这两条指令要求取到的数做符号拓展,所以还需要把符号位拓展占满剩余空间。而lbu lhu指令则不用再单独处理。
另:am-kernel提供的测试集很多bug测不出来,nemu可以使用这些测试集,应该能覆盖大部分情况,但RTL最好使用补充测试集再测试。