14 RISC-V CPU初探

前面我们介绍了verilog语言的基本语法特征,并讨论了数字电路设计中常用的状态机和流水线结构,然后我们借鉴SystemC的做法,引入了HDL4SE建模语言,以及相应的仿真系统,当然同时提供了从verilog语言到这种建模语言的编译系统(部分功能尚未完成)。这个学习系列的最后一个小目标是实现一个RISC-V CPU核。本节先对RISC-V CPU进行初步的讨论,并实现了一个用HDL4SE语言写的周期级别的可以运行的模型,可以看作是一个RISC-V CPU的行为级的CModel。它虽然简单,然而可以用来验证编译工具链以及做一些RISC-V下的编程实践,并且因为简单,反而更加容易看懂,比较适合于初学者。这个模型不是RTL的,后面我们会逐步将它改造为全部用Verilog RTL实现。
前面我们用状态机做俄罗斯方块以及用流水线做GoogLeNet图像分类时,已经有一个感觉,就是其中的运算部件是比较浪费的,大部分时间都在空转,当然可以将它们设计成共享方式,但是这样做会大大增加设计的复杂程度。另外,设计起来还是非常麻烦,比如状态机设计,每个状态必须非常仔细地去设计在状态中需要完成的工作,并给出状态切换的条件,做个俄罗斯方块游戏,状态只有几个,还可以逐个精雕细琢,如果状态数目多了,设计的工作量就会非常大。如果状态数目成千上万,对设计者而言就是一个灾难了,此时如果还要考虑资源共享,那就几乎是不可完成的任务。
应该设计一种通用的状态机,就是给出一种与具体应用无关的状态表示,状态切换的规则,并构造一个固定的电路来实现,状态的切换不是靠硬编码来实现,而是通过控制数据来控制这个通用状态机的状态切换。这些控制数据存储在存储器中,通用状态机从存储器读控制数据,然后根据读出来的控制数据的内容,与当前状态进行计算后,得到新的状态,实现状态切换。通用状态机可以通过一些通用的外部信号来跟其他电路交换信息,可以设计特别的控制数据来采样通用状态机的输入信号或者生成输出信号。这样做的好处在于可以将状态机的状态数目设计得非常大,状态切换的规则则比较简单,状态机的功能由控制数据来决定,这样就把状态机的设计复杂度转移到控制数据的设计上去。控制数据的设计毕竟不是电路设计,可以用其他的工具来支撑,这样大大降低了应用系统的设计难度。
具体而言,在状态机中,状态一般用一组寄存器来实现,通用状态机也一样。由于通用状态机要从存储器读控制数据,因此通用状态机中都有一个寄存器用来表示当前读控制数据的位置,称为PC寄存器(Program Counter),当然通用状态机中还有其他寄存器,联合起来表示很大的状态空间。通用状态机的运行过程是:从PC寄存器指向的位置读入一个控制数据,控制数据可以分为几类:

  1. 控制数据中某些部分与通用状态机中的状态寄存器的某些指定位进行计算,结果用来修改状态寄存器的某些位,这些寄存器的位的位置,也是控制数据来指定的,控制数据的类型也编码在控制数据中。
  2. 控制数据指定状态寄存器的某些位进行计算,结果用来修改状态寄存器的某些位。其中的某些位以及计算功能也编码在控制数据中。
  3. 一般情况下一个控制数据读出并控制通用状态机进行状态转移后,会取下一个控制数据,也就是PC寄存器会自动递增到下一个控制数据的存储器位置,这样可以实现算法设计中要求的顺序结构。可以设计一类控制数据,计算的结果直接修改PC寄存器,这样可以实现算法设计中要求的分支,特别是带条件分支的功能。有了分支功能和顺序功能,我们就可以实现应用算法中的更多功能。
  4. 还有一类控制数据是用来将外部信号采样用来存储到内部状态寄存器中,或者是用来生成外部的信号。一种特殊的控制数据甚至是先生成一个输出信号,然后等一个时钟周期采样另外一组输入信号存储到状态寄存器中。
    CPU其实就是一个通用的状态机,其状态用它内部的寄存器的值来表达,这种将控制数据存储起来然后用来控制状态转移的结构称为冯诺伊曼计算机架构。比如RISC-V CPU,在32位基本配置下,有一个32位的PC寄存器和31个32位的通用寄存器来表示其状态,理论上可以表达的状态数目非常巨大,状态变量总共1024位,状态数目就是2的1024次方个,足以表达我们需要的最复杂的应用。
    并不是任意的数据都可以作为控制数据,控制数据有规定的格式,控制数据的规范就是CPU指令集规范。符合其指令集规范的数据可以控制这个状态机的状态切换,所谓状态切换就是其寄存器的改变。我们可以通过编制控制数据来控制这个状态机的运行,完成我们想实现的算法。控制数据中有对外I/O的类型,用来生成这个通用状态机与外围电路交互的信号,对外的I/O信号可以是数据读写信号,通过LOAD/STORE控制数据类型(指令类型)生成,可以是特殊的信号,比如中断信号,事先可以规定状态机在收到外部中断信号的状态切换方式即可。
    用通用状态机完成算法可以达到多个状态共享运算单元,另外将电路功能实现从纯粹的电路设计大量转到控制数据的设计上,控制数据系列,一般称为软件,极大地丰富了数字电路的使用场合。设计一个通用的状态机,使用不同的控制数据或者说软件,就可以得到实现完全不同的功能。这样才能将数字电路应用到人类活动的每个方方面面,现在大家手上捏个手机,就可以玩各种各样的游戏,做各种事情,当年为了玩俄罗斯方块游戏,得有个专门的俄罗斯方块掌机,为了听MP3,得有个专门得MP3机器,为了计算,得有个计算器,为了听新闻,得有个收音机,看视频,用VCD机…。

14.1 RISC-V CPU的指令规范

RISC-V CPU是一个非常简洁的,又是扩展能力非常强的通用状态机设计。它的指令(控制数据)格式可以从16位到192位以上,可以提供非常丰富的表达能力。下面的示意图来自于risc-v-2.2的规范文件,展示了不同长度的指令如何编码。

一般而言在一个RISC-V CPU中只支持一种长度的指令。RISC-V的规范定义了一个最小集合,只能完成整数的部分运算。包括32位版本和64位版本。其32位版本用risc v RV32I表示。后面的I表示基本的整数运算。
RV32I配置包括一个32位pc寄存器,表示当前的指令存放的位置,还包括32个32位的通用寄存器,用来存放用户的数据,这些寄存器用x0–x31表示,其中x0是只读寄存器,并且硬件上设置为0,不能修改,因此无法表示状态。RV32I的指令长度为32位,并且要求每条指令的地址对齐到32位。其格式有如下几种:

按照前面的规则,其中opcode有7位,低两位固定为11,其他5位表示指令的类型,其中的[4:2]不能为111。rd是需要修改的寄存器编号,rs1/rs2是修改寄存器参与计算的寄存器编号, imm是指令中包括的立即数,也参与计算,一般代替rs2,在U-type中甚至覆盖了rs1部分。funct3/7是同一类指令中具体的功能编号。注意所有格式中rs1, rs2,rd在指令中的位置是一样的,这样可以在读出指令的同时就很方便的将涉及的寄存器的编号解析出来,电路可以尽早从寄存器文件中将源寄存器的值读出来参与计算,并判断指令能否立即执行(比如上条指令的目标寄存器是否是这条指令的源寄存器)。
RV32I配置下的指令包括以下几种:

  1. 立即数计算:opcode[6:2]=0x04,编码方式是I-type,具体的计算功能在funct3中,完成的功能是用12位立即数进行符号位扩展,然后与rs1表示的寄存器的值进行计算,计算结果存到rd代表的寄存器中。funct3编码的计算功能包括addi(加法), slti(小于), sltiu(无符号小于), xori(异或), ori(或), andi(与), slli(逻辑左移), srli(逻辑右移), srai(算术右移),总共有9个运算,其中在位移运算时,其实立即数中只有低5位参与计算,因此编码时有一位用来区别逻辑右移和算术右移,这样用3位funct3以及这一位可以表达9个运算。
  2. 寄存器计算:opcode[6:2]=0x0c,编码方式是R-type,具体的计算功能在funct3中,由于表达的功能比较多,因此func7中也表达了一部分功能。完成的功能是寄存器rs1,rs2参与计算,然后结果存储到rd中。funct3/funct7编码的计算功能包括:add/sub(加减), sll(左移), slt(比较), sltu(无符号比较), xor(异或), srl/sra(逻辑右移/逻辑左移), or(或), and(与)。
  3. lui指令:opcode[6:2] = 0x0d, 编码方式是U-type,用来将立即数存到rd指定的寄存器中,这个立即数就是指令数据低12位设置位0后得到的数据。
  4. auipc指令: opcode[6:2] = 0x05,编码方式是U-type,用来将立即数加上pc的值存放到rd指定的寄存器中,这个立即数就是指令数据低12位设置位0后得到的数据。这条指令用来生成与当前pc位置为基准的相对位置,可以支持所谓PIC(位置无关代码),这样编制出来的代码可以存放在存储器的任何地方,执行过程使用的都是相对地址。
  5. 无条件跳转指令,包括两条,jal指令,opcode[6:2]=0x1b,编码方式U-type,用来将imm表示的立即数进行符号扩展后与pc相加,作为新的pc值,实现跳转功能,值得注意的是imm的编码虽然从[31:12]中来,但并不是直接拿来使用,而是采用了交织编码的方式,具体请参考规范。由于其表示的最小粒度是2,这样20位立即数可以表达pc前后1MB地址范围,也就是说这条指令的跳转范围是离当前位置1MB的范围内。这条指令还同时将该指令的下一条指令的位置(PC+4)存放到rd表示的寄存器中。这样,可以用这条指令来实现函数调用的功能,函数调用其实就是将返回地址存起来(比如存放到x1寄存器中),然后跳到需要调用的函数起点,被调用的函数一般把这个返回地址存放在局部栈中,以便它能够继续调用其他函数甚至自我调用实现递归。jalr指令,opcode[6:2] = 0x19,编码方式为I-type,将imm表示的立即数进行符号扩展然后与rs1和pc相加,结果存放到pc中,同时将pc+4存放到rd表示的寄存器中。jalr与jal的差别是有一个源寄存器参与计算,这样就可以实现比如返回功能,将返回地址加载到通用寄存器中,然后使用jalr,就可以回到返回地址继续运行。
  6. 带条件跳转指令,opcode[6:2] = 0x18,编码方式S-type,这个格式的指令没有修改通用寄存器,因此rd用来表达立即数,其中参与运算的包括rs1, rs2, imm,funct3指定功能,实现beq, bne, blt, bge, bltu, bgeu 等功能,其中比较是在rs1和rs2之间进行的,注意有带符号和无符号的区别。总共有12位立即数进行符号扩展,以2字节的粒度提供pc前后4KB的跳转。如果比较的结果为真,pc被修改为pc+imm,实现带条件跳转功能。
  7. 读外部数据指令:opcode[6:2] = 0x00,编码方式I-type,读rs1+imm位置的外部数据到rd表示的目标寄存器中。func3表示读入的宽度(1,2,4字节),注意这里可以要求读入的数据地址对齐到读入的大小,但是也可以支持任意地址读入。
  8. 写数据指令:opcode[6:2] = 0x08, 编码方式S-type,这条指令不修改通用寄存器,写地址在rs1+imm中,写数据在rs2中,写宽度在funct3中,同样可以要求写地址对齐在写宽度,也可以支持任意地址写。
  9. 存储栅栏:opcode[6:2] = 0x03,fence/fence-i,用来实现存储系统的同步,在没有高速缓存的系统中,这条指令不做任何事情。
  10. 系统功能:opcode[6:2] = 0x1c,ecall, ebreak, csrrw, csrrs, csrrc, csrrwi, csrrsi, csrrci功能 ,最小系统中可以将这些指令实现为NOP。

RISC-V的指令集可以在RV32I的基础上进行扩展,常用的扩展有:
M: 增加乘法和除法运算,记为RV32IM,其实就是alu指令增加了乘法,除法和求余数的功能
A: 增加原子操作,记为RV32IA,
F:增加32位浮点计算功能,记为RV32IMF(同时有M扩展)
D:增加64位浮点功能,记为RV32IMFD
每个扩展就增加一个字母表示。
如果是64位系统,则将32换为64,其中的所有寄存器都变为64位,部分指令扩展用来操作64位寄存器中的高32位。

14.2 RISC-V 软件工具链

GNU提供了RISC-V的软件工具集合,可以支持c/c++语言,汇编语言到RISC-V指令集的控制数据的编译链接,还提供了linux下和裸机下的标准c库和数学库。这些工具是开源的,可以通过git clone --recursive http://github.com/riscv/riscv-gnu-toolchain下载,其中用–recursive是因为这个工具集由很多个部分组成,用这个命令可以把所有相关的部分一次下载下来。github一般比较堵,可以用国内的镜像网站来下载,比如用csdn的(在linux下运行),同时需要下载一些支持软件,才能编译生成工具集合:

git clone --recursive https://codechina.csdn.net/mirrors/riscv/riscv-gnu-toolchain
sudo apt-get install autoconf automake autotools-dev curl python3 libmpc-dev libmpfr-dev libgmp-dev gawk build-essential bison flex texinfo gperf libtool patchutils bc zlib1g-dev libexpat-dev gawk
export RISCV="/home/xxx/riscv-gnu-toolchain"
export PATH=$PATH:$RISCV/bin
./configure --prefix=$RISCV --with-arch=rv32im --with-abi=ilp32
make

其中的RSICV目录要根据具体的情况设置,如果设置在系统目录中,后面的make就要用sudo make才能运行。编译成功后,在RISCV目录下就存放了编译器连接器汇编器等一系列可执行文件,也包括一些c库。
软件工具链是比较复杂的,如果需要自己扩展CPU指令,需要了解的东西就更多了,这里只是使用,不进行更多的讨论。有了这些工具,我们就能把c语言代码转换成RISC-V的指令,或者说是符合RISC-V指令规范的通用状态机控制数据序列。

14.3 RISC-V CPU的HDL4SE实现

CPU的设计是很复杂的,涉及到数字电路设计的很多方面,后面的章节会接触到,然而,如果只是做个能够运行RISC-V指令序列的状态机出来,却比较简单,本节描述了一个实现过程,不是RTL的,只能看成是一个CModel。

14.3.1 准备可执行文件

我们给这个CPU增加一个外设,就是前面我们做过的计数器的显示和键盘,通过对地址0xf0000000的读,可以读到键盘的状态,通过对地址0xf0000010–0xf0000018的写,我们可以控制数码管的显示,这样可以实现一个简单的计数器的功能。相应的c语言代码如下:

//counter 的c语言实现
//main.c
const unsigned int segcode[10] =
{0x3F,0x06,0x5B,//  8'b01011011, 0x4F,// 8'b01001111, 0x66,// 8'b01100110, 0x6d,// 8'b01101101, 0x7d,// 8'b01111101, 0x07,// 8'b00000111, 0x7f,// 8'b01111111, 0x6f,// 8'b01101111,
};unsigned int num2seg(unsigned int num)
{return segcode[num % 10];
}int main(int argc, char* argv[])
{unsigned long long count, ctemp;int countit = 0;unsigned int* ledkey = (unsigned int*)0xF0000000;unsigned int* leddata = (unsigned int*)0xf0000010;count = 0;do {unsigned int key;key = *ledkey;if (key & 1) {count = 0;}else if (key & 2) {countit = 0;}else if (key & 4) {countit = 1;}if (countit)count++;ctemp = count;leddata[0] = num2seg(ctemp) |((num2seg(ctemp / 10ll)) << 8) |((num2seg(ctemp / 100ll)) << 16) |((num2seg(ctemp / 1000ll)) << 24);ctemp /= 10000ll;leddata[1] = num2seg(ctemp) |((num2seg(ctemp / 10ll)) << 8) |((num2seg(ctemp / 100ll)) << 16) |((num2seg(ctemp / 1000ll)) << 24);ctemp /= 10000ll;leddata[2] = num2seg(ctemp) |((num2seg(ctemp / 10ll)) << 8);} while (1);return 1;
}

这段代码应该是比较容易看懂的,这里不多做解释。我们用前面描述的工具链来编译它:

riscv32-unknown-elf-gcc main.c -o test.elf

生成的运行文件test.elf是一种比较复杂的包括执行代码与数据,各种符号,各种信息的文件,从中我们可以看到内部的信息(通过readelf应用程序):

riscv32-unknown-elf-readelf -a test.elfELF Header:Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 Class:                             ELF32Data:                              2's complement, little endianVersion:                           1 (current)OS/ABI:                            UNIX - System VABI Version:                       0Type:                              EXEC (Executable file)Machine:                           RISC-VVersion:                           0x1Entry point address:               0x1008cStart of program headers:          52 (bytes into file)Start of section headers:          15464 (bytes into file)Flags:                             0x0Size of this header:               52 (bytes)Size of program headers:           32 (bytes)Number of program headers:         2Size of section headers:           40 (bytes)Number of section headers:         22Section header string table index: 21
......
......

其中值得注意的是Entry point address: 0x1008c,表示这个程序的入口地址在0x1008c处,注意elf文件中的代码是可以重定向的,一般而言编译出来的代码使用的地址都是相对地址,可以在任何位置运行,然而为了仿真时能够很方便地与代码对照,我们就按照这个内存布局来运行好了。
代码反汇编出来是这样的:

riscv32-unknown-elf-objdump -d test.elftest.elf:     file format elf32-littleriscvDisassembly of section .text:00010074 <register_fini>:10074: 00000793            addi    x15,x0,010078:  00078863            beq x15,x0,10088 <register_fini+0x14>1007c:  00011537            lui x10,0x1110080:  aec50513            addi    x10,x10,-1300 # 10aec <__libc_fini_array>10084:   2c50006f            jal x0,10b48 <atexit>10088:   00008067            jalr    x0,0(x1)0001008c <_start>:1008c:  00001197            auipc   x3,0x110090:    7ac18193            addi    x3,x3,1964 # 11838 <__global_pointer$>10094:  c3418513            addi    x10,x3,-972 # 1146c <completed.1>10098:   c5018613            addi    x12,x3,-944 # 11488 <__BSS_END__>1009c:   40a60633            sub x12,x12,x10100a0:   00000593            addi    x11,x0,0100a4:  04d000ef            jal x1,108f0 <memset>100a8:   00001517            auipc   x10,0x1100ac:   aa050513            addi    x10,x10,-1376 # 10b48 <atexit>100b0:  00050863            beq x10,x0,100c0 <_start+0x34>100b4: 00001517            auipc   x10,0x1100b8:   a3850513            addi    x10,x10,-1480 # 10aec <__libc_fini_array>100bc:   28d000ef            jal x1,10b48 <atexit>100c0:   794000ef            jal x1,10854 <__libc_init_array>100c4:    00012503            lw  x10,0(x2)100c8: 00410593            addi    x11,x2,4100cc:  00000613            addi    x12,x0,0100d0:  0ac000ef            jal x1,1017c <main>100d4: 7500006f            jal x0,10824 <exit>000100d8 <__do_global_dtors_aux>:100d8:  ff010113            addi    x2,x2,-16......

代码在内存中的起始点是0x00010074,运行入口点是0x0001008c<_start>。
进入程序后,首先是调用了memset,将BSS段清零(就是代码中声明的static变量所在的位置),然后调用atexit安装exit例程,在调用libc_init_array初始化c库内部的数据结构,然后进入main, 执行应用程序。
我们的应用程序可以通过编程序直接调入elf文件,但是这样太复杂,把眼球都吸引到如何调入elf文件上去了,我们用objcopy例程直接将elf文件转换成verilog中能够读入的格式:

riscv32-unknown-elf-objcopy -O verilog test.elf test.cod
得到的test.cod文件如下:
@00010074
93 07 00 00 63 88 07 00 37 15 01 00 13 05 C5 BB
6F 00 ......
@00010CFC
3F 00 00 00 06 00 00 00 5B 00 00 00 4F 00 00 00
66 00 00 00 6D 00 00 00 7D 00 00 00 07 00 00 00
7F 00 00 00 ......
@00011000
10 00 00 00 00 00 00 00 03 7A 52 00 01 7C 01 01
1B 0D 02 00 10 00 00 00 18 00 00 00 A8 F4 FF FF
30 04 00 00 00 00 00 00 00 00 00 00
@0001102C
74 00 01 00 1C 01 01 00
@00011034
D8 00 01 00
@00011038
00 00 00 00 24 13 ......
@00011460
38 10 01 00 00 00 00 00 38 10 01 00

这个格式就比较简单了,将elf文件分成若干段,每一段用@地址行开始,后面跟着数据。到此,我们就准备好要运行的数据文件了。

14.3.2 顶层模型

我们将使用一个RISC-V CPU的核和一个counter中使用过的ledui的模型来构造顶层模型,用verilog语言写出来就是:

/* riscv_core.v */(* HDL4SE="LCOM", CLSID="638E8BC3-B0E0-41DC-9EDD-D35A39FD8051", softmodule="hdl4se"
*)
module riscv_core(input wClk, nwReset,output wWrite,output [31:0] bWriteAddr,output [31:0] bWriteData,output [3:0]  bWriteMask,output wRead,output [31:0] bReadAddr,input [31:0]  bReadData);
endmodule/* riscv-sim.v */`include "riscv_core.v"(* HDL4SE="LCOM", CLSID="2925e2cf-dd49-4155-b31d-41d48f0f98dc", softmodule="hdl4se"
*)
module digitled(input wClk, nwReset,input wWrite,input [31:0] bWriteAddr,input [31:0] bWriteData,input [3:0]  bWriteMask,input wRead,input [31:0] bReadAddr,output [31:0]  bReadData);
endmodulemodule top(input wClk, nwReset);wire wWrite, wRead;wire [31:0] bWriteAddr, bWriteData, bReadAddr, bReadData;wire [3:0]  bWriteMask;digitled led(wClk, nwReset, wWrite, bWriteAddr, bWriteData, bWriteMask, wRead, bReadAddr, bReadData);riscv_core core(wClk, nwReset, wWrite, bWriteAddr, bWriteData, bWriteMask, wRead, bReadAddr, bReadData);
endmodule

其中riscv_core用一个LCOM的描述,表示这个模型是用软件实现的,将来我们再修改riscv_core.v文件,将它用verilog语言实现。digitled则直接使用counter实例程序中现成城的。这个verilog语言描述编译后,得到我们的顶层c语言设计文件如下:

/*
*  Created by HDL4SE @ Fri Aug 20 08:57:41 2021
*  Don't edit it.
*/#include "stdlib.h"
#include "stdio.h"
#include "string.h"#include "object.h"
#include "dlist.h"
#include "bignumber.h"
#include "hdl4secell.h"
#include "conststring.h"
#include "verilog_parsetree.h"/* Module top */#define M_ID(id) top##idIDLISTVID(wClk),VID(nwReset),VID(bReadData),VID(wWrite),VID(bWriteAddr),VID(bWriteData),VID(bWriteMask),VID(wRead),VID(bReadAddr),
END_IDLISTGEN_MODULE_DECLARE
END_GEN_MODULE_DECLAREGEN_MODULE_INITPORT_IN (wClk, 1);PORT_IN (nwReset, 1);WIRE(bReadData, 32);WIRE(wWrite, 1);WIRE(bWriteAddr, 32);WIRE(bWriteData, 32);WIRE(bWriteMask, 4);WIRE(wRead, 1);WIRE(bReadAddr, 32);CELL_INST("2925e2cf-dd49-4155-b31d-41d48f0f98dc", /* digitled */"led", "", "wClk, nwReset, wWrite, bWriteAddr, bWriteData, bWriteMask, wRead, bReadAddr, bReadData");CELL_INST("638E8BC3-B0E0-41DC-9EDD-D35A39FD8051", /* riscv_core */"core", "", "wClk, nwReset, wWrite, bWriteAddr, bWriteData, bWriteMask, wRead, bReadAddr, bReadData");
END_GEN_MODULE_INIT#undef M_IDIHDL4SEModuleVar* hdl4seCreate_main(IHDL4SEModuleVar * parent, const char* instanceparam, const char* name)
{return TOP_MODULE(top);
}

然后同样构造仿真程序主程序如下:

#include "stdlib.h"
#include "stdio.h"
#include "string.h"
#include "object.h"
#include "bignumber.h"
#include "hdl4secell.h"
#include "hdl4sesim.h"
#include "hdl4sevcdfile.h"
#include "counter.h"
#include "digitled.h"
#include "threadlock.h"IHDL4SESimulator** sim;
IHDL4SEModuleVar* topmodule;
unsigned long long clocks = 0;#define RECORDVCD 1static int running = 1;
int StopRunning()
{running = 0;return 0;
}IHDL4SEModuleVar* hdl4seCreate_main(IHDL4SEModuleVar* parent, const char* instanceparam, const char* name);extern int (*A_u_t_o_registor_digitled)();
extern int (*A_u_t_o_registor_riscv_core)();FILE* recordfile;
THREADLOCK recordfilelock;FILE* recordfileGet()
{threadlockLock(recordfilelock);return recordfile;
}void recordfileRelease()
{threadlockUnlock(recordfilelock);
}int main(int argc, char* argv[])
{int i;int width;IHDL4SEWaveOutput** vcdfile;A_u_t_o_registor_digitled();A_u_t_o_registor_riscv_core();recordfile = fopen("d:/gitwork/recordfile.txt", "w");recordfilelock = threadlockCreate();sim = hdl4sesimCreateSimulator();topmodule = hdl4seCreate_main(NULL, "", "top");objectCall1(sim, SetTopModule, topmodule);objectCall1(sim, SetReset, 0);
#if RECORDVCDvcdfile = hdl4sesimCreateVCDFile("riscv.vcd");objectCall2(vcdfile, AddSignal, "/top/core", "pc");objectCall2(vcdfile, AddSignal, "/top/core", "instr");objectCall2(vcdfile, AddSignal, "/top/core", "wWrite");objectCall2(vcdfile, AddSignal, "/top/core", "bWriteAddr");objectCall2(vcdfile, AddSignal, "/top/core", "bWriteData");objectCall2(vcdfile, AddSignal, "/top/core", "x1");......objectCall2(vcdfile, AddSignal, "/top/core", "x31");objectCall1(vcdfile, SetTopModule, topmodule);objectCall0(vcdfile, StartRecord);
#endifobjectPrintInfo(stdout, fprintf);do {objectCall0(sim, ClkTick);
#if RECORDVCDobjectCall1(vcdfile, ClkTick, clocks);
#endifobjectCall0(sim, Setup);clocks++;if (clocks == 4)objectCall1(sim, SetReset, 1);} while (running);
#if RECORDVCDobjectCall0(vcdfile, StopRecord);objectRelease(vcdfile);
#endifreturn 0;
}

如此就万事具备,只欠CPU核了。

14.3.3 RISC-V CPU核的HDL4SE建模

我们用库模型的方式来对RISC-V CPU核进行建模,以便将来跟verilog语言写的模型能够互换运行,下面是模型的基本结构:


#define riscv_core_MODULE_VERSION_STRING "0.4.0-20210820.0855 RISCV Core cell"
#define riscv_core_MODULE_CLSID CLSID_HDL4SE_RISCV_CORE#define M_ID(id) riscv_core##idIDLISTVID(wClk),VID(nwReset),VID(wWrite),VID(bWriteAddr),VID(bWriteData),VID(bWriteMask),VID(wRead),VID(bReadAddr),VID(bReadData),VID(pc),VID(instr),VID(write),VID(writeaddr),VID(writedata),VID(read),VID(readaddr),VID(readreg),VID(state),VID(x1),
......VID(x31),
END_IDLISTMODULE_DECLARE(riscv_core)unsigned int *ram;unsigned int regs[32];unsigned int ramsize;unsigned int dstreg;unsigned int dstvalue;unsigned int ramdstaddr;unsigned int ramdstvalue;unsigned int ramdstwidth; /* 1, 2, 4 */
END_MODULE_DECLARE(riscv_core)DEFINE_FUNC(riscv_core_rw_sig, "write, writeaddr, writedata, read, readaddr") {VAssign(wWrite, write);VAssign(bWriteAddr, writeaddr);VAssign(bWriteData, writedata);VAssign(wRead, read);VAssign(bReadAddr, readaddr);vput(bWriteMask, 0x0);
} END_DEFINE_FUNCDEFINE_FUNC(riscv_core_register, "pc") {int i;for (i = 1;i<32;i++)vput_idx(VID(x1)+i-1, pobj->regs[i]);
} END_DEFINE_FUNCenum riscv_core_state {RISCVSTATE_READ_INST,RISCVSTATE_WAIT_LD,
};DEFINE_FUNC(riscv_core_read_inst, "") {unsigned int pc = vget(pc);if (pc > pobj->ramsize) {printf("exception: pc[%08x] overflow [ramsize=%d]\n", pc, pobj->ramsize);exit(-3);}unsigned int instr = pobj->ram[pc/4];unsigned int opcode = instr & 0x7f;if ( ((opcode & 0x3) != 3) || ( (opcode >> 2) & 0x7) == 0x7) {printf("instruction format error, we support 32bit instruction only: pc=%08x: %x", pc, instr);exit(-2);}opcode >>= 2;vput(pc, pc+4); /* 默认往后跳一指令 */switch (opcode) {case 0x0d: riscv_core_exec_lui_inst(pobj, pc, instr); break;case 0x05: riscv_core_exec_auipc_inst(pobj, pc, instr); break;case 0x1b: riscv_core_exec_jal_inst(pobj, pc, instr); break;case 0x19: riscv_core_exec_jalr_inst(pobj, pc, instr); break;case 0x18: riscv_core_exec_b_inst(pobj, pc, instr); break;case 0x00: riscv_core_exec_ld_inst(pobj, pc, instr); break;case 0x08: riscv_core_exec_st_inst(pobj, pc, instr); break;case 0x04: riscv_core_exec_alui_inst(pobj, pc, instr); break;case 0x0c: riscv_core_exec_alu_inst(pobj, pc, instr); break;case 0x03: riscv_core_exec_fence_inst(pobj, pc, instr); break;case 0x1c: riscv_core_exec_sys_inst(pobj, pc, instr); break;default: {INSTR_FORMAT_ERROR;exit(-2);}}
} END_DEFINE_FUNCDEFINE_FUNC(riscv_core_ld_done_inst, "") {RISCV_SETREG(vget(readreg), vget(bReadData));vput(state, RISCVSTATE_READ_INST);
} END_DEFINE_FUNCDEFINE_FUNC(riscv_core_clktick, "") {if (vget(nwReset) == 0) {vput(pc, 0x10074);vput(write, 0);vput(read, 0);vput(state, RISCVSTATE_READ_INST);}else {int state = vget(state);vput(write, 0);vput(read, 0);if (state == RISCVSTATE_READ_INST) {CALL_FUNC(riscv_core_read_inst);}else if (state == RISCVSTATE_WAIT_LD){CALL_FUNC(riscv_core_ld_done_inst);}}
} END_DEFINE_FUNCDEFINE_FUNC(riscv_core_deinit, "") {if (pobj->ram != NULL)free(pobj->ram);
} END_DEFINE_FUNCDEFINE_FUNC(riscv_core_setup, "") {if (pobj->dstreg != 0) {pobj->regs[pobj->dstreg] = pobj->dstvalue;}pobj->dstreg = 0;if ( (pobj->ramdstaddr != 0) && (pobj->ramdstwidth > 0) && (pobj->ramdstaddr + pobj->ramdstwidth < pobj->ramsize)) {memcpy(((char *)pobj->ram)+ pobj->ramdstaddr, &pobj->ramdstvalue, pobj->ramdstwidth);}pobj->ramdstaddr = 0;
} END_DEFINE_FUNCint loadExecImage(unsigned char* data, int maxlen)
{unsigned int addr;FILE* pFile = fopen(DATADIR"test_code/test.cod", "rt");if (pFile == NULL) {printf("File %s can not open\n", DATADIR"test_code/test.bin");exit(-1);}addr = 0;while (!feof(pFile)) {......fclose(pFile);
}MODULE_INIT(riscv_core)int i;pobj->ramsize = 1 * 1024 * 1024;pobj->ram = malloc(pobj->ramsize);loadExecImage(pobj->ram, pobj->ramsize);pobj->ramdstaddr = 0;pobj->dstreg = 0;pobj->regs[1] = 0x1008c; /* 入口地址 */pobj->regs[2] = pobj->ramsize - 16;pobj->regs[0] = 0; /* 硬连接到0上 */PORT_IN(wClk, 1);PORT_IN(nwReset, 1);GPORT_OUT(wWrite, 1, riscv_core_rw_sig);GPORT_OUT(bWriteAddr, 32, riscv_core_rw_sig);GPORT_OUT(bWriteData, 32, riscv_core_rw_sig);GPORT_OUT(bWriteMask, 4, riscv_core_rw_sig);GPORT_OUT(wRead, 1, riscv_core_rw_sig);GPORT_OUT(bReadAddr, 32, riscv_core_rw_sig);PORT_IN(bReadData, 32);REG(pc, 32);GWIRE(instr, 32, riscv_core_instr);REG(write, 1);REG(writeaddr, 32);REG(writedata, 32);REG(read, 1);REG(readaddr, 32);REG(readreg, 5);REG(state, 10);GWIRE(x1, 32, riscv_core_register);......GWIRE(x31, 32, riscv_core_register);CLKTICK_FUNC(riscv_core_clktick);SETUP_FUNC(riscv_core_setup);DEINIT_FUNC(riscv_core_deinit);END_MODULE_INIT(riscv_core)

其中省略了很多实现代码,完整的代码请看git的文件。
RISC-V 的CPU内部寄存器包括PC和32个x寄存器,PC直接用寄存器实现,32个x寄存器用c语言的数据结构实现,注意到每个周期最多修改一个通用寄存器,为了实现寄存器的功能,我们把写寄存器的编号和数据分别放在dstreg和dstvalue两个成员中,这两个成员在setup以外的代码中是只写的,就是为了生成写寄存器所需要的信息,在setup中我们根据它们来更新寄存器。CPU读写的内存我们直接用c语言的存储器实现,即pobj->ram,在初始化时申请了1MB作为内存,然后调用loadExecImage把test.cod加载到内存中。pc寄存器的复位值设置为代码段的起始点0x00010074,x1的复位值设置为程序的入口0x0001008c,x2的复位值设置为ram的结束地址,系统作为栈起点(risc-v gcc的栈是往低地址生长的,risc-v中没有特别的寄存器作为栈指针寄存器,编译器一般选择x2,x1是作为调用地址或返回地址)。
执行load/store指令时如果地址落到内存区域,则直接读写ram,否则发送到外部,我们将外部信号包括write, writedata, writeaddr, read, readaddr等用寄存器输出。
为了满足HDL4SE模拟环境中外部总线的要求,读命令发出之后一拍,读数据才能返回,我们将RISC-V核设计为一个两状态的状态机,如果执行load指令并且访问的数据来自于ram外部,则执行时将状态转到等待读数据返回,下一拍读数据返回后该指令才结束。其他指令则总是在一个状态。
每个周期的clktick函数如果在读指令状态,则读入指令并译码执行,大部分指令都在一拍内完成执行。译码执行的过程分两步, 第一步解析出opcode,然后根据opcode执行对应的指令动作,比如opcode[6:2]为0x0c,则调用riscv_core_exec_alu_inst完成alu指令的执行:

void riscv_core_exec_alu_inst(MODULE_DATA_TYPE* pobj, unsigned int pc, unsigned int instr) {/* add, sub, sll, slt, sltu, xor, srl, sra, or, and mul, mulh, mulhsu, mulhu, div, divu, rem, remu*/unsigned int rs2;unsigned int rs1;unsigned int rd;unsigned int func3;unsigned int rst;rs1 = (instr >> 15) & 0x1f;if (rs1 != 0)rs1 = pobj->regs[rs1];rs2 = (instr >> 20) & 0x1f;if (rs2 != 0)rs2 = pobj->regs[rs2];rst = 0;func3 = (instr >> 12) & 0x7;rd = (instr >> 7) & 0x1f;if (instr & (1 << 25)) {/* is M instr*/switch (func3) {case 0: { //mul long long s1, s2;s1 = *(int*)&rs1;s2 = *(int*)&rs2;s1 *= s2;rst = *(unsigned int*)&s1;}break;case 1: { //mulh long long s1, s2;s1 = *(int*)&rs1;s2 = *(int*)&rs2;s1 *= s2;rst = (*(unsigned long long*)&s1) >> 32;}break;case 2: { //mulhsu long long s1, s2;s1 = *(int*)&rs1;s2 = rs2;s1 *= s2;rst = (*(unsigned long long*) & s1) >> 32;}break;case 3: { //mulhu unsigned long long s1, s2;s1 = rs1;s2 = rs2;s1 *= s2;rst = s1 >> 32;}break;case 4: { //div*(int *)&rst = *(int*)&rs1 / *(int*)&rs2;}break;case 5: { //divurst = rs1 / rs2;}break;case 6: { //rem*(int*)&rst = *(int*)&rs1 % *(int*)&rs2;}break;case 7: { //remurst = rs1 % rs2;}break;}}else {switch (func3) {case 0: { //add/subif (instr & (1 << 30))rst = rs1 - rs2;elserst = rs1 + rs2;}break;case 1: { //sll rst = rs1 << rs2;}break;case 2: { //slt rst = (*(int*)&rs1 < *(int*)&rs2) ? 1 : 0;}break;case 3: { //sltu rst = (rs1 < rs2) ? 1 : 0;}break;case 4: { //xorrst = rs1 ^ rs2;}break;case 5: { //srl/sraif (instr & (1 << 30))rst = rs1 >> rs2;else*(int*)&rst = (*(int*)&rs1) >> rs2;}break;case 6: { //orrst = rs1 | rs2;}break;case 7: { //andrst = rs1 & rs2;}break;}}RISCV_SETREG(rd, rst);
}

我们在其中实现了RV32IM配置,因此乘法指令也需要实现。

load/store指令对读写范围进行了判断,可以支持外围的设备的读写,这样我们可以读写digitled设备,完成键盘输入和数码管的控制。比如store指令的运行过程如下:

void riscv_core_exec_st_inst(MODULE_DATA_TYPE* pobj, unsigned int pc, unsigned int instr) {/* sb, sh, sw */unsigned int imm;unsigned int rs1;unsigned int rs2;unsigned int func3;unsigned int v;unsigned int writeaddr;imm = ((instr >> 20) & 0xfe0) | ((instr >> 7) & 0x1f);rs1 = (instr >> 15) & 0x1f;if (rs1 != 0)rs1 = pobj->regs[rs1];rs2 = (instr >> 20) & 0x1f;if (rs2 != 0)rs2 = pobj->regs[rs2];func3 = (instr >> 12) & 0x7;imm = sign_expand(imm, 11);v = 0;/* riscv支持地址不对齐访问 */writeaddr = rs1 + imm;if (writeaddr < pobj->ramsize) {switch (func3) {case 0:/*sb*/pobj->ramdstaddr = writeaddr;pobj->ramdstvalue = rs2;pobj->ramdstwidth = 1;break;case 1:/*sh*/pobj->ramdstaddr = writeaddr;pobj->ramdstvalue = rs2;pobj->ramdstwidth = 2;break;case 2:/*sw*/pobj->ramdstaddr = writeaddr;pobj->ramdstvalue = rs2;pobj->ramdstwidth = 4;break;default:pobj->ramdstaddr = 0; /* no write */DEBUG_CODE_FUNC;break;}}else {vput(writeaddr, writeaddr);vput(write, 1);vput(writedata, rs2);}
}

14.3.4 执行结果

执行的界面与前面的counter差不多,就是计数速度慢了很多,毕竟这是软件运行出来的结果,一个循环周期需要的周期数很多。下面是运行画面截图:

下面是记录下来的VCD文件截图,用VCD把所有信号和寄存器的值记下来,可以作为软件调试的手段之一。

14.4 结论

应该说,RISC-V基本配置的指令集是非常简洁的用c语言实现的核心代码(包括译码和执行)也就500行左右,按说任何一个C语言的入门者都应该能够看懂。CPU设计的难度其实在于两个方面,一方面是CPU本身的设计,这点我们后面会更加详细地来说明。另一个方面是对应的软件工具链设计,这部分往往要求很高,一般的团队都很难独立完成,幸好有开源的gnu工具链,否则很难想象我们这种级别的项目去做一个c/c++编译器包括完整的工具链。我们都站在巨人的肩膀上看世界,巨人已经献出了他们的肩膀,你难道不想站上去看得更远?

【请参考】
01.HDL4SE:软件工程师学习Verilog语言(十三)
02.HDL4SE:软件工程师学习Verilog语言(十二)
03.HDL4SE:软件工程师学习Verilog语言(十一)
04.HDL4SE:软件工程师学习Verilog语言(十)
05.HDL4SE:软件工程师学习Verilog语言(九)
06.HDL4SE:软件工程师学习Verilog语言(八)
07.HDL4SE:软件工程师学习Verilog语言(七)
08.HDL4SE:软件工程师学习Verilog语言(六)
09.HDL4SE:软件工程师学习Verilog语言(五)
10.HDL4SE:软件工程师学习Verilog语言(四)
11.HDL4SE:软件工程师学习Verilog语言(三)
12.HDL4SE:软件工程师学习Verilog语言(二)
13.HDL4SE:软件工程师学习Verilog语言(一)
14.LCOM:轻量级组件对象模型
15.LCOM:带数据的接口
16.工具下载:在64位windows下的bison 3.7和flex 2.6.4
17.git: verilog-parser开源项目
18.git: HDL4SE项目
19.git: LCOM项目
20.git: GLFW项目
21.git: SystemC项目

HDL4SE:软件工程师学习Verilog语言(十四)相关推荐

  1. HDL4SE:软件工程师学习Verilog语言(十一)

    11 流水线 前面一节介绍了状态机的概念.状态机用于描述事务处理的一个程序性流程,可以组成顺序,分支,循环的事务处理流程.这些概念本来在verilog中的行为级描述中是有的,但是由于不是RTL描述,因 ...

  2. HDL4SE:软件工程师学习Verilog语言(六)

    6 表达式与赋值 我们终于可以继续学习了,也是没有办法,其实工作的80%的时间都是在忙杂事,就像打游戏一样,其实大部分时间都在打小怪,清理现场,真正打终极BOSS的时间是很少的,但是不清小怪,打BOS ...

  3. HDL4SE:软件工程师学习Verilog语言(二)

    2 词法和预处理器 2.1 定个小目标 作为一个软件工程师,学习一种语言,最暴力的办法就是做一个这种语言的编译器(或解释器),如果没有做过某种语言的编译器,至少也得仔细看过这种语言的编译器实现,最不济 ...

  4. Udacity机器人软件工程师课程笔记(十四)-运动学-正向运动学和反向运动学(其一)

    正向运动学和反向运动学 目录 2D中的旋转矩阵 sympy包 旋转的合成 旋转矩阵中的欧拉角 平移 齐次变换及其逆变换 齐次变换的合成 Denavit-Hartenberg 参数 DH参数分配算法 正 ...

  5. 智能化软件开发微访谈·第二十四期 大模型时代的智能化软件生态(讨论汇编)...

    CodeWisdom "智能化软件开发沙龙是由CodeWisdom团队组织的围绕智能化软件开发.数据驱动的软件开发质量与效能分析.云原生与智能化运维等相关话题开展的线上沙龙,通过微信群访谈交 ...

  6. JavaScript学习(六十四)—关于JS的浮点数计算精度问题解决方案

    JavaScript学习(六十四)-关于JS的浮点数计算精度问题解决方案 您的语言没有中断,它正在执行浮点数学运算.计算机只能本地存储整数,因此它们需要某种表示十进制数字的方式.此表示并不完全准确.这 ...

  7. Spring Security技术栈学习笔记(十四)使用Spring Social集成QQ登录验证方式

    上一篇文章<Spring Security技术栈开发企业级认证与授权(十三)Spring Social集成第三方登录验证开发流程介绍>主要是介绍了OAuth2协议的基本内容以及Spring ...

  8. 虚拟内存——Windows核心编程学习手札之十四

    虚拟内存 --Windows核心编程学习手札之十四 系统信息 有些操作系统的值是根据主机而定的,如页面大小.分配粒度大小等,这些值不用硬编码形式,进程初始化时应检索这些值以使用.函数GetSystem ...

  9. Windows保护模式学习笔记(十四)—— 阶段测试

    Windows保护模式学习笔记(十四)-- 阶段测试 题目一 解题步骤 题目二 解题步骤 题目一 描述:给定一个线性地址,和长度,读取内容 int ReadMemory(OUT BYTE* buffe ...

最新文章

  1. 忘记了理想等于人生失去了意义。
  2. 域名系统DNS、文件传送协议FTP、动态主机配置协议DHCP、远程登录协议TELNET、电子邮件协议(SMTP/POP3/IMAP)、常用端口
  3. boost::variant2模块实现复制分配的测试程序
  4. 剑指offer——面试题54:表示数值的字符串
  5. 不考虑知识点,考代码段更好
  6. ubuntu facebook/C3D视频特征提取
  7. leetcode 剑指 Offer 12. 矩阵中的路径
  8. 易语言对象--Word之按行定位并写入文本
  9. 20190828笔记
  10. echarts的x轴文字倾斜展示
  11. 苹果iPhone白屏死机?如何修复?
  12. 攻防世界——web新手题
  13. Django新建项目(Linux操作系统)
  14. 实例分割研究综述总结
  15. 使用ARCHPR进行zip明文攻击
  16. 使用Java语言开发微信公众平台(六)
  17. Cycript基本语法与使用-iOS逆向工程
  18. python实现牛顿法_牛顿法求极值及其Python实现
  19. Matlab 模块化编程
  20. Java判断Excel中,空单元格和空行

热门文章

  1. python学习笔记第一篇:Python3使用wordcloud制作词云报错OSError: cannot open resource和制作出的词云图乱码问题
  2. canvas绘图 -实现图片围绕中心点旋转
  3. 如何简单更好的进行文章伪原创
  4. 8,16,32位单片机的区别
  5. java并发学习之六:JCSP(Java Communicating Sequential Processes)实践
  6. XAML四大原则及五种常见布局
  7. 视频点播cdn加速\直播cdn加速解决方案
  8. MPU6050 简介
  9. 酒吧经营你要知道的:企业规划
  10. 动手学习机器学习(Day1)