电子头条

《跟着先楫学习RISCV系列一》RISC-V开放架构设计简读

2024-06-15
    阅读数:


一、前言

感谢eeworld电子工程世界提供《 RISC-V开放架构设计之道 》书籍测评,书中并没有过多繁琐介绍riscv的概念,也没有过多强调riscv的好处,更多介绍的是riscv的简洁、免费、开放的思想。


如同书中一位大佬推荐所说的:这本方便的小书轻松地总结了RISC-V指令集架构所有的基本要素,是学生和从业者的完美参考指南。


RISC-V,是比较大体、宽泛的概念, 但是 书中内容很简短,内容插入了不少插图以及RISCV的参考卡,说是参考指南一点也不过分;加之目录介绍很清晰,从riscv32的基础整数指令集RV32I、乘法和除法指令集M扩展、原子指令集A扩展、单双精度浮点数指令集RV32F/RV32D、压缩指令RV32C、以及汇编语言和向量RV32V。以及后续的RV64介绍。


而刚好先楫的处理器作为典型标准的RISCV处理器,当然也支持上述的指令集,并且还有所扩充。以实际的硬件进行书籍分享是最好不过的思路了。比如先楫的HPM5300系列。


本系列文章更多是讲一些基础知识, 利用HPM5300EVK配合SEGGER Embedded Studio这个IDE通过查看机器码和汇编来分析RISCV的一些指令集,以此能对riscv有一定的了解兴趣。


本书内容目录比较多,系列文章定期分享RISCV的每个指令集基础。

二、导读

如果想要接触riscv,那么典型的reference card(参考卡)不可或缺,你会发现相比其他的指令集架构,riscv真的相对简洁了不少(基础的就只有两页),而且操作码7位,相当可自由扩充127个扩展指令,这也是riscv的开放自由思想。这里截图一小部分,需要了解的可以搜下riscv cheat sheet。

https://www.cl.cam.ac.uk/teaching/1617/ECAD+Arch/files/docs/RISCVGreenCardv8-20151013.pdf


(一)RV32I指令格式


如书中描述,RV32I 基础指令集的一页图形表示。对于每幅图, 将有下划线的字母从左到右连接起来,即可组成完整的 RV32I 指令集。对于每一个图,集合标志{}内列举了指令的所有变体,变体用加下划线的字母或下划线字符_表示。特别的,下划线字符_表示对于此指令变体不需用字符表示。例如,下图表示了这四个 RV32I 指令:slt, slti, sltu, sltiu:


书中展示了6种基本指令格式,分别是用于寄存器-寄存器操作的 R 类型指令,用于短立即数和访存 load 操作的 I 型指令,用于访存 store 操作的 S 型指令,用于条件跳转操作的 B 类型指令,用于长立即数的 U 型指令和用于无条件跳转的 J 型指令。


这里使用最简单的U类型种的lui指令,配合利用HPM5300EVK配合SEGGER Embedded Studio来进行说明上述所说的指令格式是什么意思。

随便打开hpm_sdk一个例子,从左边的汇编窗口找到lui的汇编,比如以下:

可以看到,ses这个IDE对于汇编和机器码呈现还是是否直观的,对于不熟悉gdb调试又想查看汇编的开发者是十分友好的。


对于左边的8000452a指的是存储到flash的机器码的flash地址,中间的E40007B7就是机器码,最右边就是汇编部分。


hpm单片机支持压缩指令集RV32C,所以机器码有些会是16位,这对于固件空间的缩小利用是有一定帮助的。

可以分析下E40007B7这个机器码对应的Lui指令格式,如下图可看到:

E40007B7的低7位代表操作码opcode,也就是lui的操作码(0b0110111),

7到11bit是5位目的寄存器(0b01111),这很奇妙,5bit宽度刚好是32,对应的就是寄存器的长度(32bit),换成十六进制就是15,对应的通用寄存器就是X15,也就是别名a5寄存器。

高11位就是立即数,对应的十六进制就是0xE4000,最终根据该机器码可以反汇编得到: 把一个20位的立即数0xE4000加载到a5寄存器,

对应的汇编指令就是: lui a5, 0xE4000

刚好可以对上ses IDE的汇编显示。


(二)RISCV通用寄存器

无论是RISCV32还是RISCV64,他的通用寄存器的数量都是32个,分别是x0~x31,当然为了方便汇编编写,每个寄存器也有别名,在参考卡中如下:

RISC-V 有足够多的寄存器来达到两全其美的结果: 既能将操作数存放在寄存器中,同时也能减少保存和恢复寄存器的次数。 其中的关键在于,在函数调用的过程中不保留部分寄存器存储的值,称它们为临时寄存器; 另一些寄存器则对应地称为保存寄存器。 不再调用其它函数的函数称为叶函数。 当一个叶函数只有少量的参数和局部变量时,它们可以都被存储在寄存器中,而不会“溢出(spilling)”到内存中。 但如果函数参数和局部变量很多,程序还是需要把寄存器的值保存在内存中,不过这种情况并不多见。


这里说明下caller是调用者,调用(或执行)一个函数的代码段或者函数,是主动发起函数调用的一方,可以理解是"甲方"。


callee是被调用者,被调用的函数本身,是被动接收函数调用并且执行对应操作的一方,可以理解是"乙方"。


从上面的表格知道,从通用寄存器上看,X0到X31,调用者需要保存的通用寄存器16个,剩下的是被调用者需要保存的寄存器。

这里可以从函数调用阶段来进行说明通用寄存器的作用,配合利用HPM5300EVK配合SEGGER Embedded Studio,使用helloworld例子来说明。


函数调用基本分为以下几个阶段,在书中也提到:

1、进入到被调用者函数的开始位置(caller需要保存的)

2、将函数传参存储到指定寄存器上

这里就是上图的x10~x17,相比其他指令集架构参数依赖于内存,也就是需要保存在栈中,riscv反而可以通过寄存器保存参数,最多可以存储8个参数,当然超过的也会存储到内存中。一般应用还是推荐不要多余8个参数传参。

那么我们进入到board_timer_create API中,传参是300和board_led_toggle函数指针。那么将会存储在x10和x11这两个寄存器上。

可以看到,x10现在是300,是符合预期的,对于x11是0x8000669e,我们可以在SES IDE中的汇编窗口查到该函数地址。

0x8000669e对应的就是board_led_toggle,这也是符合预期的。

3、获取函数局部资源变量保存在寄存器,并执行函数中的指令,可以看到寄存器窗口会发生不同的赋值变化

4、执行完毕之后将返回值存储到调用者能够访问的位置(X10寄存器),恢复寄存器,出栈释放局部资源

5、返回进行下一个调用函数位置。

在ses IDE中也展示了main调用者主函数调用被调用者函数的一些汇编代码过程。


三、总结

1、riscv的书籍众多,RISC-V开放架构设计之道这本书籍值得阅读,书中内容简洁但又不缺完整,而且丰富的参考卡可作为riscv的日常参考指南所用。


2、对于技术书籍,需要有一个实操机会可以加深印象,先楫的处理器内核遵守risv规范,具有多种扩展指令集,而其配套的SEGGER Embedded Studio这个IDE通过查看机器码和汇编,让一些不熟悉gdb调试的开发者,是一个值得推荐的实操平台。



展开↓