前篇介绍了如何编写RISC-V汇编语言程序,但是在实际工程中,目前的编程主要使用C/C++这样的高级语言,因此使用汇编语言的情形更多是将汇编程序嵌入C/C++语言编写的程序中。
系统环境
Windows 10-64bit
软件平台
Nuclei Studio IDE 202102版
硬件需求
RV-STAR开发板
GCC内联汇编简述
这里介绍的是GCC的RISC-V工具链,在C/C++程序中嵌入汇编程序遵循GCC内联汇编(inline asm )语法规则,其格式由如下部分组成:
asm volatile (
汇编指令列表
: 输出操作数 // 非必需
: 输入操作数 // 非必需
: 可能影响的寄存器或存储器 // 非必需
);
下面分别予以简述:
“关键字asm”为GCC的关键字,表示进行内联汇编操作。
注意:也可以使用前后各带两个下划线的__asm__,__asm__是GCC 关键字asm的宏定义。
“关键字volatile”或“__volatile__”。__volatile__或volatile 是可选的。如果添加了该关键字,则要求编译器对后续括号内添加的汇编程序不进行任何优化以保持其原状;如果没有添加此关键字,则编译器可能会将某些汇编指令优化掉。
一个典型的完整内联汇编程序格式如下:
__asm__ __volatile__(
"Instruction_1\n"
"Instruction_2\n"
...
"Instruction_n\n"
:[out1]"=r"(value1), [out2]"=r"(value2), ... [outn]"=r"(valuen)
:[in1]"r"(value1), [in2]"r"(value2), ... [inn]"r"(valuen)
:"r0", "r1", ... "rn"
);
“输出操作数”
和
“输入操作数”
C/C++中使用的是抽象层次较高的变量或者表达式,如下所示:
sum = add1 + add2;//将变量add1和add2相加,得到的结果赋给sum
而汇编指令中直接操作的是寄存器,以RISC-V指令集为例,一个加法指令的汇编指令如下:
add x2,x3,x4;将x3和x4寄存器相加得到x2。
那么,在C/C++程序中添加了汇编程序时,程序员如何将其所需要操作的C/C++变量与汇编指令的操作数对应起来呢?那就需要使用到GCC内联汇编的“输出操作数”和“输入操作数”部分来指定。
GCC内联汇编语法的“输入操作数”和“输出操作数”部分用来指定当前内联汇编程序的输入和输出操作符列表,遵循的语法如下:
每一个输入或者输出操作符都由以下3部分组成。
方括号[]中的符号名用于将内联汇编程序中使用的操作数(由%[字符]指定)和此操作符(由[字符]指定)通过同名“字符”绑定起来。
除了“%[字符]”中明确的符号命名指定外,还可以使用“%数字”的方式进行隐含指定。“数字”从0开始,依次表示输出操作数和输入操作数。
假设包含“输出操作数”列表中有2个操作数,“输入操作数”列表中有2个操作数,则汇编程序中%0表示第一个输出操作数,%1表示第二个输出操数,%2表示第一个输入操作数,%3表示第二个输入操作数。
引号中的限制字符串,用于约束此操作数变量的属性,常用的约束如下。
a)字母“r”表示使用编译器自动分配的寄存器来存储该操作数变量;字母“m” 表示使用内存地址来存储该操作数变量。如果同时指明“rm”,则编译器自动选择最优方案。。
b)对于“输出操作数”而言,等号“=”代表输出变量用作输出,原来的值会被新值替换;“+”代表输出变量不仅作为输出,而且作为输入。
注意:此约束对不适用于“输入操作数”。
圆括号()中的C/C++变量或者表达式。
输出操作符之间需使用逗号分隔。
可以结合后文的实践代码加深对这部分的理解。
“可能影响的寄存器
或存储器”部分
如果内联汇编中的某个指令会更新某些寄存器的值,则必须在asm中第三个冒号后的“可能影响的寄存器或存储器”中指定出这些寄存器,通知GCC编译器不再假定之前存入这些寄存器中的值依然合法。指定出这些寄存器由逗号分隔开,每个寄存器由引号包含住,如下所示:
:"x1", "x2"
对于那些已经由“输入操作数”和“输出操作数”部分约束指定了的变量,由于编译器自动分配寄存器,因此编译器知道哪些寄存器会被更新,所以无须担心这部分寄存器,不用在“可能影响的寄存器或存储器”进行显示地指定。
如果内联汇编中的某个指令以无法预料的形式修改了存储器中的值,则必须在asm中第三个冒号后的“可能影响的寄存器或存储器”中显示地加上“memory”,从而通知GCC编译器不要将存储器中的值暂存在处理器的通用寄存器中。
GCC内联汇编实践
我们以100加200的加法为例,在c中使用内联汇编计算结果并且检查输出是否正确,如果正确则输出Pass,否则输出Fail。
实践的内联汇编中没有显式地使用任何寄存器,而是用“输出操作数”和“输入操作数”部分的指定,将C/C++中的变量或者表达式映射到汇编指令中充当操作数。在此过程中,我们无须关心真正执行的汇编指令具体使用的寄存器索引,编译器会根据引号中指定的操作数约束,按照编译优化的原则来分配合理的寄存器索引号。我们仅需要关心操作数和变量的映射,无须关心操作数会映射到处理器具体的哪个通用寄存器。
具体操作如下:
在Nuclei Studio中新建一个helloworld工程。芯来科技官网的文档与工具页面可以下载Nuclei Studio,下载后解压缩,在Nuclei Studio解压缩的目录下双击NucleiStudio.exe即可启动IDE。
第一次启动Nuclei Studio将会弹出对话框要求设置Workspace目录路径,该目录将用于存放后续创建的项目工程文件。设置好Workspace路径,再单击“Launch”启动Nuclei Studio。
启动后推荐打开Launch Bar功能,方便快速编译和调试。打开菜单栏“Window -> Preferences”,搜索“bar”,勾选第一个选项“Enable the Launch Bar”即可启用Launch Bar功能。
在菜单栏中,选择“File-> New -> C/C++ Project”开始新建工程,在弹窗中双击选择“C Managed Build”。
新的页面中“Project name”填写“iasm”,“Project type” 选择“Nuclei SDK Project For GD32VF103 SoC”和“RISC-V Cross GCC”,如下图,点击“Next”。新的页面不用修改,直接点击“Next”即可。
在选择模板工程页面修改“Project Example”选项为“baremetal_helloworld”,后续页面不需要修改,点击“Next”直到最后一页,点击“Finish”完成新建helloworld工程。
接下来获取汇编例程代码,在Github打开board labs代码仓库,位置在:
https://github.com/Nuclei-Software/nuclei-board-labs/blob/master/rvstar/demo_iasm/main.c。
将main.c的内容全部复制替换到新建的工程中,参考效果如下:
替换后右击工程,选择“Build Project”,如果工程之前编译过,需要先选择“Clean Project”然后再编译。编译结果参考如下:
按照之前的教程运行或调试工程,将程序下载至开发板中,断开openocd。之后打开串口并连接,因为程序下载至flash中,断电不会消失,所以点击开发板的reset按键可以在串口查看到如下输出: