[verilog] 直播写一个RISC-V IMC的CPU

[复制链接]
 楼主| 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的指令集描述资料~




首先,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位对齐的情况。

故有代码:

  1. `timescale 1ns/1ps

  2. module kamikaze_fetch(clk_i,
  3.                 rst_i,
  4.                 im_addr_o,
  5.                 im_data_i,
  6.                 instr_o,
  7.                 instr_valid_o,
  8.                 is_compressed_instr_o,
  9.                 pc_o);
  10.                
  11.         input clk_i;
  12.         input rst_i;
  13.         input [31:0] im_data_i;
  14.         output reg [31:0] instr_o;
  15.         output [31:0] im_addr_o;
  16.         output reg instr_valid_o;
  17.         output is_compressed_instr_o;
  18.         output [31:0] pc_o;
  19.         //input stall_i; /* IF 停止信号 */
  20.        
  21.         wire [30:0] word_address = im_addr_o[31:2];
  22.         reg stall_i = 0;
  23.        
  24.         reg [31:0] pc;
  25.         reg [31:0] pc_4;
  26.         reg [2:0] pc_add;
  27.         reg [2:0] pc_add_prev;
  28.        
  29.        
  30.         reg [31:0] last_instr; /* 一级缓冲 */
  31.         reg is_compressed_instr;
  32.         reg fetch_start;       
  33.        
  34.         assign is_compressed_instr_o = is_compressed_instr;
  35.        
  36.         localparam CPU_START = 32'h0; /* 启动地址 */
  37.        
  38.         assign im_addr_o = pc_4[1]? (pc_4 + 2'b10): pc_4; /* 舍入 */
  39.         assign stall_requiring = (pc_add_prev == 2) && (pc[1:0] == 2'b00); /* 16位对齐等待,防止冲数据 */
  40.        
  41.         assign pc_o = pc;
  42.        
  43.        
  44.         always @(posedge clk_i or negedge rst_i)
  45.         begin
  46.                 if(!rst_i)
  47.                 begin
  48.                         pc_4 <= CPU_START;
  49.                         pc <= CPU_START;/* PC 比 pc_4 滞后1 CLK */
  50.                         fetch_start <= 0;
  51.                         pc_add_prev <= 4;
  52.                         last_instr <= 32'h0;
  53.                         instr_valid_o <= 0;
  54.                 end
  55.                 else
  56.                 begin
  57.                         if(!stall_i)
  58.                         begin
  59.                                 if(fetch_start == 1'b0)
  60.                                 begin
  61.                                         fetch_start <= 1'b1; /* 取 0 指令 */
  62.                                         pc_4 <= pc_4 + 16'h4;
  63.                                         instr_valid_o <= 1;
  64.                                 end
  65.                                 else
  66.                                 begin
  67.                                         pc_4 <= pc_4 + pc_add;
  68.                                         pc <= pc + pc_add;
  69.                                
  70.                                         if(!stall_requiring)
  71.                                                 last_instr <= im_data_i;
  72.                                        
  73.                                         pc_add_prev <= pc_add;
  74.                                 end
  75.                         end
  76.                 end
  77.         end
  78.        
  79.         always @*
  80.         begin
  81.                 if(pc[1:0] == 2'b00)
  82.                 begin
  83.                         if(stall_requiring)
  84.                         begin
  85.                                 if(last_instr[1:0] != 2'b11) /* 对齐的压缩指令 */
  86.                                 begin
  87.                                         is_compressed_instr <= 1;
  88.                                         instr_o = last_instr[15:0];
  89.                                 end
  90.                                 else
  91.                                 begin
  92.                                         is_compressed_instr <= 0;
  93.                                         instr_o = last_instr[31:0];
  94.                                 end
  95.                         end
  96.                         else
  97.                         begin
  98.                                 if(im_data_i[1:0] != 2'b11) /* 对齐的压缩指令 */
  99.                                 begin
  100.                                         is_compressed_instr <= 1;
  101.                                         instr_o = im_data_i[15:0];
  102.                                 end
  103.                                 else
  104.                                 begin
  105.                                         is_compressed_instr <= 0;
  106.                                         instr_o = im_data_i[31:0];
  107.                                 end
  108.                         end
  109.                 end
  110.                 else
  111.                 begin //pc[1:0] == 10
  112.                         if(last_instr[17:16] != 2'b11) /* 不对齐的压缩指令 */
  113.                         begin
  114.                                 is_compressed_instr <= 1;
  115.                                 instr_o = last_instr[31:16];
  116.                         end
  117.                         else                        /* 不对齐的非压缩指令 */
  118.                         begin
  119.                                 is_compressed_instr <= 0;
  120.                                 instr_o = {im_data_i[15:0], last_instr[31:16]};
  121.                         end
  122.                 end
  123.                                                
  124.                 pc_add = is_compressed_instr? 2: 4;
  125.         end
  126.        
  127.         /* 这地方得有个分支预测器,你说我是直接预测全部不跳好,还是预测全部跳好呢? */
  128.        
  129. 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综合后大约五百个宏单元),没有说性能要高,我记的读书的时候二姨的 张明峰 说他们的卖点是抗干扰强。

支持开源,如果还是十五年前那个秋天的话可能还能参与一下。
 楼主| 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的目标。
 楼主| 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还吃什么,哈哈哈。
linqing171 发表于 2017-2-13 22:48 | 显示全部楼层
也曾在altera新品flex 1k 50优化到20M而不停的通宵。可能是在企业呆太久了,已经枉然了。首先想到的竟然是指标不明确。
先把1楼的内容都实现了吧。

gaoyang9992006 发表于 2017-9-22 16:24 | 显示全部楼层
围观。
tjc21 发表于 2018-2-28 16:47 | 显示全部楼层
高手,请问还在继续吗 ?
lizhen5754 发表于 2018-7-23 14:56 来自手机 | 显示全部楼层
大侠,最近要用到riscv软核,请教一下这个要消耗多少资源,相对altera的cyclone平台的nios2而言,这个差差大吗?
您需要登录后才可以回帖 登录 | 注册

本版积分规则

4

主题

92

帖子

2

粉丝
快速回复 在线客服 返回列表 返回顶部