- 包括 RV32I:寄存器、ABI 和带有 ecall 的流控制。
- 与木星一起练习和练习:否定、因素、链、递归。
- 掌握交叉工具链, 脚本 使用 objdump 进行链接和调试。
如果您对汇编程序感到好奇,并认为 RISC-V 是可行的方法,那么您来对地方了。 在 RISC-V 上开始使用 ASM 比看起来更实惠 如果你了解这些工具, 编程 以及一些关键的建筑规则。
在下面的几行中,我结合了几个来源的最佳内容:使用木星型模拟器的实践, RV32I 基本曲目的约定、循环和递归示例、系统调用,甚至查看 VHDL 中的 RISC-V CPU 设计(带有 ALU、内存控制和状态机),以及对交叉工具链和链接脚本的回顾。
什么是 RISC-V 汇编程序?它与机器语言有何不同?
虽然两者都与 硬件, 机器语言是纯二进制(1 和 0) CPU 直接解释,而汇编器使用助记符和 符号 比汇编程序更易读,然后转换为二进制。
RISC-V 定义了一个具有非常干净的基础指令集的开放 ISA。 RV32I(32 位)配置文件包含 39 条用户指令 具有卓越的正交性,将内存访问与纯计算分开,并在 GCC/LLVM 中提供出色的支持。
记录、协议和入口点
在 RV32I 中,您有 32个通用寄存器(x0–x31) 32 位;x0 始终读取为 0,且无法写入。诸如 a0–a7(参数)、t0–t6(临时变量)或 s0–s11(已保存)之类的别名对于遵循 ABI 也很有用。
根据环境或模拟器,程序可能从特定标签启动。 在 Jupiter 中,程序从全局 __start 标签开始。,您必须将其声明为可见(例如,使用 .globl)以标记入口点。
该 标签以冒号结尾,每行只能放一条指令,注释可以用#或;开头,所以汇编程序会忽略它们。
工具和模拟器:Jupiter 和基本工作流程
为了顺利进行练习,您必须 Jupiter 模拟器/汇编器,一种受 SPIM/MARS/VENUS 启发的图形工具,可在单一环境中方便编辑、组装和执行。
在 Jupiter 中,您可以在编辑器选项卡中创建、编辑和删除文件。 保存后,按F3组装并运行 逐条指令地调试流程,使用寄存器和内存视图来了解机器的状态。
程序必须以对环境的调用结束: 使用代码 10 退出呼叫设置 a0 (退出)。在 RISC-V 中,ecall 相当于系统调用或对环境/系统的陷阱。
程序的最小结构和系统调用
学术示例中的典型结构定义了一个起点,完成了工作,并以 ecall 结束。 ecall 参数通常在 a0–a2 中传输 以及 a7 中的服务选择器,具体取决于环境。
在一个 Linux 例如,RISC-V,您可以使用写入系统调用进行打印,并使用适当的代码退出。 对于写入,a0(fd)、a1(缓冲区)、a2(长度)和a7与服务编号一起使用。最后,将 a0 设置为返回代码,将 a7 设置为退出号。
# Ejemplo mínimo (Linux RISC-V) para escribir y salir
.global _start
_start:
addi a0, x0, 1 # fd = 1 (stdout)
la a1, msg # a1 = &msg
addi a2, x0, 12 # a2 = longitud
addi a7, x0, 64 # write
ecall
addi a0, x0, 0 # return code
addi a7, x0, 93 # exit
ecall
.data
msg: .ascii "Hola mundo\n"
如果你在 Linux 之外工作,例如 拥有自己的物联网服务的教育模拟器,根据环境文档更改ecall号码和寄存器。
帮助您熟悉条件、循环和记忆的初步练习
典型的热身是检测整数是否为负数。 如果为正数则返回 0,如果为负数则返回 1。;使用 RV32I,通过与 0 进行比较并在小于时进行设置,可以通过一条经过深思熟虑的指令解决该问题。
另一个非常有用的练习是列出一个数字的因数: 从 1 到 n 遍历,打印除数,并返回除数的数量您将练习条件分支、除法(或重复减法)以及加法和比较循环。
使用字符串会强制您管理内存: 访问内存中字符串的每个字符,并就地将小写字母转换为大写字母 如果它们在 ASCII 范围内。完成后,返回字符串的原始地址。
循环、函数和递归:阶乘、斐波那契和汉诺塔
设计循环时,请考虑三个块:条件、主体和步骤。 使用 beq/bne/bge 和无条件跳转 jal/j while/for 构建 没有神秘感,依靠附加和比较。
.text
.globl __start
__start:
li t0, 0 # i
li t1, 10 # max
cond:
bge t0, t1, end # si i >= max, salta
# cuerpo: usar a0/a1 segun servicio IO del entorno
addi t0, t0, 1 # i++
j cond
end:
li a0, 10
ecall
在函数调用中,尊重 ABI: 如果您要连续调用更多调用,请保存,如果修改了 s0–s11,则保留它们,并使用 sp 以字的倍数移动的堆栈。
阶乘是经典的递归: 基本情况 n==0 返回 1;否则,调用 factorial(n-1) 并乘以 n。在调用之前,保护堆栈上保存的 ra 和寄存器,并在返回时恢复它们。
factorial:
beq a0, x0, base
addi sp, sp, -8
sw ra, 4(sp)
sw s0, 0(sp)
mv s0, a0
addi a0, a0, -1
jal factorial
mul a0, a0, s0
lw s0, 0(sp)
lw ra, 4(sp)
addi sp, sp, 8
jr ra
base:
li a0, 1
jr ra
斐波那契数列对于练习 两次调用的递归 作为一个高效的迭代版本,带有累加器变量。如果你想要流程和参数控制方面的挑战,可以将解决方案翻译成汇编语言 河内塔 有四个参数:磁盘、源、目标和辅助塔;它尊重调用顺序并显示每个动作。
内存访问、数组和字符串操作
在 RISC-V 中,内存访问是通过加载/存储完成的: lw/sw 表示字,lh/sh 表示半字,lb/sb 表示字节,电荷中有符号或无符号的变体(lb vs lbu、lh vs lhu)。
要遍历整数数组,每个索引使用 4 字节偏移量;对于文本字符串, 逐字节前进,直到找到终止符 如果约定要求的话(例如,\0)。记住保存基地址,并根据需要使用 addi/auipc/la 处理指针。
从头开始设计 RV32I CPU:高级概述
如果你想深入研究硅片,一个教育项目可以构建一个 RV32I CPU 采用 VHDL 语言,可在 FPGA 中综合 中低端。包含程序 ROM、数据 RAM 以及用于点亮 LED 的简单 GPIO。
内核实现了基本指令集(没有 M/A/C 扩展或 CSR), 使用 32 位地址总线 并允许在适当的情况下进行8/16/32位符号扩展内存访问。该设计明确地将寄存器、ALU、内存控制器和状态机分开。
ALU、移位和“延迟加载”的概念
ALU 是通过以下操作组合描述的,例如 加法、减法、异或、或、与、比较(有符号和无符号) 以及逻辑/算术移位。
为了在 FPGA 中保存 LUT,实现了多位移位 由状态机控制的迭代 1 位移位:您消耗了几个周期,但减少了逻辑资源。
在同步电路中,时钟边沿会发生改变。 “延迟加载”的概念让人想起多路复用器选择的内容会影响下一个周期的寄存器,这是设计获取-解码-执行状态机时的一个关键方面。
内存控制器和映射:ROM、RAM 和 GPIO
内存块将 ROM 和 RAM 集成到连续的空间中, 简化处理器接口控制器接收 AddressIn(32 位)、DataIn、宽度(字节/半/字)、符号扩展信号、WE(读/写)和 Start 来启动事务。
手术结束后, ReadyOut 设置为 1,如果已读取,DataOut 包含数据(请求时进行符号扩展)。如果已写入,则数据保留在 RAM 中。
实用地图示例: ROM 从 0x0000 到 0x0FFF,位于 0x1000 的 GPIO 字节(引脚的位 0)和 RAM 从 0x1001 到 0x1FFF通过这个,您可以通过写入和切换输出位来制作闪光灯。
寄存器、多路复用器和状态机
CPU 定义了 32 个通用寄存器,这些寄存器在 VHDL 中以数组形式实例化, 装饰师 从 ALU 中选择写入目的地并保留其余部分。
多路复用器控制 ALU 输入(操作数和操作), 发送到内存控制器的信号 (宽度、地址、启动控制和读/写)和特殊寄存器:PC、IR 和用于迭代移位的辅助计数器。
状态机开始于 重置, 获取 PC 指向的指令 (4 字节读取),它在准备就绪时被加载到 IR 中并传递到执行节点:ALU(1 个周期内一条指令,移位除外)、加载/存储、分支和跳转,以及 ebreak 等特殊指令。
跨工具链、链接和调试
要生成 RV32I 二进制文件,请使用 跨 GCC(目标 riscv32-none-elf)。您编译 C/C++/ASM 源代码,链接定义内存映射的脚本,并将输出转换为 ROM/FPGA 期望的形式。
一个简单的钩子脚本可以放置 ROM 中的 .text 从 0x0000 开始 和 .data 从 0x1004 开始存储在 RAM 中(如果 0x1000–0x1003 被 GPIO 寄存器占用)。启动例程可以“裸露”,并将 RAM 末尾的堆栈指针(例如 0x1FFC) 在调用 main 之前。
/* Mapa simple
* ROM: 0x00000000 - 0x00000FFF
* GPIO: 0x00001000 - 0x00001003
* RAM: 0x00001004 - 0x00001FFF
*/
SECTIONS {
. = 0x00000000;
.text : { *(.startup) *(.text) *(.text.*) *(.rodata*) }
. = 0x00001004;
.data : { *(.data) *(.data.*) }
}
使用 riscv32-none-elf-objdump 你可以 反汇编 ELF 并检查地址;例如,你会看到 引导 在 0x00000000 处,使用 lui/addi/jal 等指令并转换到主程序。对于 VHDL 仿真,GHDL 会生成可用 GtkWave 打开的轨迹。
经过模拟验证后,将设计转移到 FPGA(Quartus 或其他工具链)。 如果 RAM 被推断为内部块,并且代码清晰 RTL,即使在老旧的设备上,您也应该能够毫无意外地进行合成。
实用提醒和初期常见错误
别忘了 x0 始终为零;写入它没有任何效果,读取它则返回 0。在进行加法、比较和注册表清理时,可以利用这一点。
当你实现功能时, 保存您修改的 ra 和 sN 记录,并通过对 sp 进行字对齐的加/减操作来管理堆栈。返回时,它以相反的顺序恢复,并使用 jr ra 进行跳转。
在 Jupiter 等模拟器中,检查 __start 是全局的,你用 ecall 结束它 正确(a0=10 退出)。如果出现问题,请检查标签、全局性并重新编译(F3)。
在与 IO 的练习中, 尊重环境协议:哪些寄存器承载参数、服务编号以及预期地址还是立即值。请使用模拟器或操作系统文档。
有了清晰的 ISA 基础(RV32I、寄存器和 ABI)、像 Jupiter 这样舒适的模拟器以及不断增加的示例(负数、因子、大写、循环、阶乘、斐波那契和汉诺塔),RISC-V 汇编器不再是一堵墙,而是成为了解 CPU 如何思考的丰富地形。 如果您敢于深入研究 VHDL,您将会看到 ALU、内存和控制是如何组合在一起的。:从指令获取和延迟加载到内存接口以及带有 ROM、RAM 和 GPIO 的地图,可让您使用自己的处理器闪烁 LED。
对字节世界和一般技术充满热情的作家。我喜欢通过写作分享我的知识,这就是我在这个博客中要做的,向您展示有关小工具、软件、硬件、技术趋势等的所有最有趣的事情。我的目标是帮助您以简单而有趣的方式畅游数字世界。