打印
[verilog]

直播写一个RISC-V IMC的CPU

[复制链接]
8968|9
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
rgwan|  楼主 | 2017-2-11 22:58 | 只看该作者 |只看大图 回帖奖励 |倒序浏览 |阅读模式
大概是当作业做的一个小玩意,开源的~后续会有AHB-lite/Wishbone支持。想法是在FPGA里以尽量小的面积实现一个RV32IMC指令集的CPU。

大概比8051软核心之类的东西有更强大的性能。求各位的支持和Star~如果有愿意一起开发或者指导开发的大牛就更好了!

这会儿刚写完取指阶段和部分的译码阶段,这个礼拜内应该就能用。不过肯定不可能马上达到开发目标的咯~

https://github.com/rgwan/kamikaze

相关帖子

沙发
rgwan|  楼主 | 2017-2-11 23:00 | 只看该作者
这个CPU的设计是教科书式的4级流水线结构。IF-ID-EX-MEM/WB 因此较为简单,效率也还可以。但是由于RISC-V IMC指令集是16-32位变长指令集。IF阶段需要正确地区分两种指令。不像OpenMIPS一样一个劲儿pc+=4就可以了。

另外求精~这个帖子会慢慢连载,不过学业较重,可能更新不及时。这个帖子适合看过自己动手写CPU——OpenMIPS的人。

由此,我们开始说IF阶段的设计。

先上仿真波形图:


先上传RISC-V的指令集描述资料~

riscv-privileged-v1.9.1.pdf (611 KB)
eetop.cn_riscv-spec-v2.1中文版.pdf (3.65 MB)

首先,RISC-V指令的最后两位表示指令类型。为11是非压缩指令,11则是压缩指令。这样对指令的区分要比连续变长的CISC处理器简单很多。也不会有ARM的Thumb-ARM切换带来的开销。

同步RAM的数据落后于地址一个周期,自然而然我们需要有一个超前1word(4)的pc指针。将它称为pc_4。

同样,由于新版RISC-V规范规定32位指令不必须对齐到32位,因此,很有可能出现一条指令被截为两半的情况,一半存于上次取来的指令的高16位,一半存在新取来的指令中的低16位。因此我们还需要一个32位寄存器来缓冲上一次取来的指令,将它称为last_instr。

在32位指令对齐的情况下,pc直接对应于内存输出的数据。因此,判断是否是压缩指令,只需要判断内存输出最后两位即可。

如果在纯16位指令的情况下,pc也将直接对应于内存输出数据分别的低16位和高16位的最后两位。在判断到取到了16位指令后,将pc的自增速度从4调为2即可。

如果在32位指令和16位指令混合的情况下,我们需要引入一个等待逻辑,由于指令一半存于上次取来的数据的高16位,一半存在新取来的数据中的低16位。于是不能让原先取得的数据由于时钟的作用被新取来的数据覆盖掉。

于是,等待逻辑的执行条件是原先的pc自增量为2,并且当前的指令于32位对齐的情况。

故有代码:

`timescale 1ns/1ps

module kamikaze_fetch(clk_i,
                rst_i,
                im_addr_o,
                im_data_i,
                instr_o,
                instr_valid_o,
                is_compressed_instr_o,
                pc_o);
               
        input clk_i;
        input rst_i;
        input [31:0] im_data_i;
        output reg [31:0] instr_o;
        output [31:0] im_addr_o;
        output reg instr_valid_o;
        output is_compressed_instr_o;
        output [31:0] pc_o;
        //input stall_i; /* IF 停止信号 */
       
        wire [30:0] word_address = im_addr_o[31:2];
        reg stall_i = 0;
       
        reg [31:0] pc;
        reg [31:0] pc_4;
        reg [2:0] pc_add;
        reg [2:0] pc_add_prev;
       
       
        reg [31:0] last_instr; /* 一级缓冲 */
        reg is_compressed_instr;
        reg fetch_start;       
       
        assign is_compressed_instr_o = is_compressed_instr;
       
        localparam CPU_START = 32'h0; /* 启动地址 */
       
        assign im_addr_o = pc_4[1]? (pc_4 + 2'b10): pc_4; /* 舍入 */
        assign stall_requiring = (pc_add_prev == 2) && (pc[1:0] == 2'b00); /* 16位对齐等待,防止冲数据 */
       
        assign pc_o = pc;
       
       
        always @(posedge clk_i or negedge rst_i)
        begin
                if(!rst_i)
                begin
                        pc_4 <= CPU_START;
                        pc <= CPU_START;/* PC 比 pc_4 滞后1 CLK */
                        fetch_start <= 0;
                        pc_add_prev <= 4;
                        last_instr <= 32'h0;
                        instr_valid_o <= 0;
                end
                else
                begin
                        if(!stall_i)
                        begin
                                if(fetch_start == 1'b0)
                                begin
                                        fetch_start <= 1'b1; /* 取 0 指令 */
                                        pc_4 <= pc_4 + 16'h4;
                                        instr_valid_o <= 1;
                                end
                                else
                                begin
                                        pc_4 <= pc_4 + pc_add;
                                        pc <= pc + pc_add;
                               
                                        if(!stall_requiring)
                                                last_instr <= im_data_i;
                                       
                                        pc_add_prev <= pc_add;
                                end
                        end
                end
        end
       
        always @*
        begin
                if(pc[1:0] == 2'b00)
                begin
                        if(stall_requiring)
                        begin
                                if(last_instr[1:0] != 2'b11) /* 对齐的压缩指令 */
                                begin
                                        is_compressed_instr <= 1;
                                        instr_o = last_instr[15:0];
                                end
                                else
                                begin
                                        is_compressed_instr <= 0;
                                        instr_o = last_instr[31:0];
                                end
                        end
                        else
                        begin
                                if(im_data_i[1:0] != 2'b11) /* 对齐的压缩指令 */
                                begin
                                        is_compressed_instr <= 1;
                                        instr_o = im_data_i[15:0];
                                end
                                else
                                begin
                                        is_compressed_instr <= 0;
                                        instr_o = im_data_i[31:0];
                                end
                        end
                end
                else
                begin //pc[1:0] == 10
                        if(last_instr[17:16] != 2'b11) /* 不对齐的压缩指令 */
                        begin
                                is_compressed_instr <= 1;
                                instr_o = last_instr[31:16];
                        end
                        else                        /* 不对齐的非压缩指令 */
                        begin
                                is_compressed_instr <= 0;
                                instr_o = {im_data_i[15:0], last_instr[31:16]};
                        end
                end
                                               
                pc_add = is_compressed_instr? 2: 4;
        end
       
        /* 这地方得有个分支预测器,你说我是直接预测全部不跳好,还是预测全部跳好呢? */
       
endmodule


建议在GitHub上看更佳。

使用特权

评论回复
板凳
rgwan|  楼主 | 2017-2-11 23:00 | 只看该作者
关于分支预测:

于是有人问了,如果一条无条件跳转指令,经过IF-ID-EX后最终改动了IF阶段的PC。执行下来应该是4个周期。那么,ARM/AVR这类处理器是如何在2级/3级流水线的情况下执行无条件跳转指令只要一个周期的呢?

这种情况,在IF阶段,要做一次译码。比如判断到待发送给ID阶段的指令是无条件跳转指令,那就在IF阶段把PC给改掉,指向新的地址,然后ID和EX阶段什么都不做就可以。相当于把跳转提前了。所以无条件跳转指令就可以一个周期实现。

那么,如果是带着指针寄存器偏移的无条件跳转指令。这样的方式就是不能使用的,因为此时ID,EX,WB三个阶段中还有3条指令在同时执行。如果这三条指令中任何一条改动了寄存器的值,就会导致跳转到错误的地址上。这样倒也有应对的解决方案,就是把ID、EX、WB阶段在操作的寄存器地址全部引出来。如果寄存器地址和跳转用的指针寄存器毫无关系。则可以放心跳转。有关系,就得先阻塞住,等到和这个寄存器相关的指令跑完以后,才可以进行跳转。但是这样会在IF阶段引入太多的组合逻辑,降低速度。对我而言,我暂时不会使用这个技术。

如果是有条件的跳转指令,那么也得先确保条件在另外3个正在执行的阶段中不会被改动。如果确认没有被改动的话,就可以放心大胆的取值跳转。但是很不幸运的是,条件跳转指令和判断指令几乎都是伴生的。

因此,我们需要引入一种称为分支预测的技术,来尽量降低流水线阻塞的条件。在单片机用的内核里,不会给你太多的空间放跳转预测表(gshare之类的算法需要)。

不过,没有跳转表和概率计算,也是可以做分支预测的。只是说,穷有穷办法。比如说,一般而言,编译器喜欢将代码的向后跳转优化的容易执行,向前跳转优化的较少执行。比如:

for(i=0;i<100;i++){.....}

所以,不妨假设大多数程序都是这样的。遇到向后跳转的条件跳转指令,即使我不知道条件的结果(估计还卡在EX哪里没算出来呢)。我就先当它应该是跳了。于是把PC改为跳转后的值。老的PC保留备用。

等到条件值过了EX被计算出来了之后,我就来比较和我当时假设的跳转情况做比较。如果预测失败了,那么将IF/ID的寄存器全部清空,把上次的PC放出来,+1。然后继续执行。如果预测成功了,那就接着跑。

向前跳转的条件跳转指令也一样处理,只是反过来而已。

显而易见,这样的分支预测算法在遇到随机代码的时候,预测的正确率对半开。但是!一般的编译器喜欢把向后的跳转指令默认更容易执行。向前的更不容易被执行。所以这样的预测效率还是比较可观的,配合良好的编译器可以上90%。

预测正确的话,条件跳转指令应当是一个周期。预测失败的话,假设改动条件的指令在EX阶段,条件跳转指令在IF阶段。于是执行一个周期,写回寄存器需要一个周期,同时可以再次修改PC,等RAM数据妖一个周期。因此这样的条件跳转指令要执行3个周期。

如果改动条件的指令在ID阶段。那么要等ID走完一个周期,EX走完再一个周期,写回寄存器和修改PC又要一个周期,等RAM也要一个周期。因此这样执行的条件跳转指令就要4个周期。

预测失败后,EX阶段以前的流水线指令必须被抛弃,然后从头重新取。要不然就会得到错误的结果。

所以说,在某些处理器核心手册上看到的某些指令执行周期是1/2/3就是这样来的。带分支预测的流水线的行为是完全可以预测的,而不是像本坛某些人说的那样根本不可控。做计算机底层开发的,还是需要更多了解计算机结构比较好。

关于超标量和乱序执行,我自己也不是非常明白。所以我就不来班门弄斧了。如果有大牛愿意通俗易懂的描述这两个技术,我非常支持!

使用特权

评论回复
地板
linqing171| | 2017-2-12 19:33 | 只看该作者
比8051更强的性能,指乘法运算么?
还是 十进制调整DAA功能?  0x38+0x56调整后为0x94带OV标志位 的那个指令,性能也要和8051比吗? 还是拿哪些例子来测试比较? 读书的时候周家社老师说测性能就是测试内存中一个变量加1的次数,执行100个晶振周期,看谁的数大。

repne scasb 指令如果用你的RISC指令来做,能做到比 x86 只慢一倍吗? 个人认为除了数**算,块复制和比较也是影响效率的一个重要因素。

当flash或者cache的面积已经远大于你的指令部分的时候,尽量小的面积就没有多少意义了。以前用过不少PIC12系列的,也用过不少AVR8的,结果现在Microchip把atmel给收购了。pic面积做的小的同时(记的open core上有PIC12C54在FPGA综合后大约五百个宏单元),没有说性能要高,我记的读书的时候二姨的 张明峰 说他们的卖点是抗干扰强。

支持开源,如果还是十五年前那个秋天的话可能还能参与一下。

使用特权

评论回复
5
rgwan|  楼主 | 2017-2-12 21:13 | 只看该作者
linqing171 发表于 2017-2-12 19:33
比8051更强的性能,指乘法运算么?
还是 十进制调整DAA功能?  0x38+0x56调整后为0x94带OV标志位 的那个指 ...

DA这个BCD码指令基本上已经很少用。可以略过。要以RISC-V指令实现,也不过4条。每条都只是一个周期。8051一条就12个周期。

性能上用CoreMark之类较为均衡的应用,测出来的值才会比较有意义。用所谓测试内存中一个变量加1的次数这种方式本身就不严谨。
就算你愿意比较这一点,8051的还有IRAM和XRAM的区别。
如果是IRAM的话,那就是
LOOP:INC direct ;1
SJMP LOOP ;2
一共要走24个周期。如果是XRAM的话,得是
MOV DPH,...
MOV DPL,...
LOOP:
MOVX A, @DPTR ;2
INC A ;1
MOVX @DPTR,A ;2
SJMP LOOP ;2
一共84个周期
对于RISC-V而言就是
LOOP:LW t1, t2+...
ADDI t1,t1,1
SW t1, t2+...
JALR zero, LOOP
对于我自己的实现而言。每一条指令都只需要一个周期就能执行完。同频的情况上标准8051的性能指标完全不是我的实现的对手。1T的8051在IRAM寻址上,同频性能可能略微胜过RISC-V顺序执行单发射的结构。但是XRAM的寻址能力上是远远比不上的。因为RISC-V是平坦地址模型,只要不插入等待周期,访问任何地址周期都一样。

块复制和块比较,RISC-V规范留好了专门的扩展位,可以自己加指令,GCC也支持RISC-V中用内嵌汇编写入的自定义指令。这一块上8051的可定制性就更不用说了。更何况我这个微架构的实现虽然是教科书式顺序4级流水线加静态分支预测,但是RISC-V的指令格式简洁(不是简单!),译码起来花费的逻辑单元更少。slack更短,可以跑到更高的速度上。比如一般的1T 8051,在FCyclone4上,40M就不行了。我这个预计目标是在Cyclone4/AL3上达到100M的目标。

使用特权

评论回复
6
rgwan|  楼主 | 2017-2-12 21:19 | 只看该作者
linqing171 发表于 2017-2-12 19:33
比8051更强的性能,指乘法运算么?
还是 十进制调整DAA功能?  0x38+0x56调整后为0x94带OV标志位 的那个指 ...

况且非要比微结构性能的话,肯砸钱加面积,就算6502那个ISA都能做好性能。现在学术界公认的常识是,ISA(CISC/RISC)已经被微架构的研究抹平。当年Intel的x86的性能和IBM POWER比是差得毫无悬念。现在微架构早抹平了它——只是实现的代价会比较的高。

如果是对于嵌入式应用的微型处理器,完全不必要达到非常高的性能。使用RISC的ISA可以非常大程度的减小译码的复杂度,提升系统的频率和性能。CISC的话,你看看如果你要让8051真正做到除了分支预测失败以外,一个周期一条指令,并且时钟频率还得上百兆需要花多少逻辑和功夫在微代码部分。你就知道哪个在较低成本要尽量实现高性能的情况下更占优势了。要不然,现在的ARM Cortex-M还吃什么,哈哈哈。

使用特权

评论回复
7
linqing171| | 2017-2-13 22:48 | 只看该作者
也曾在altera新品flex 1k 50优化到20M而不停的通宵。可能是在企业呆太久了,已经枉然了。首先想到的竟然是指标不明确。
先把1楼的内容都实现了吧。

使用特权

评论回复
8
gaoyang9992006| | 2017-9-22 16:24 | 只看该作者
围观。

使用特权

评论回复
9
tjc21| | 2018-2-28 16:47 | 只看该作者
高手,请问还在继续吗 ?

使用特权

评论回复
10
lizhen5754| | 2018-7-23 14:56 | 只看该作者
大侠,最近要用到riscv软核,请教一下这个要消耗多少资源,相对altera的cyclone平台的nios2而言,这个差差大吗?

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

4

主题

92

帖子

2

粉丝