跟我写ARM处理器之二:主体结构的确定
第一个是MLA R1,R2,R3,R0。它的意思是:R1=R2*R3 + R0。如果我们要实现这一条指令的话,一个32×32的乘法器需要,一个32+32的加法器是跑不了的。现在定义几个节点:Rm = R2; Rs=R3; sec_operand(第二操作数的意思)=mult_rm_rs[31:0](mult_rm_rs的低32位);Rn=R0;则结果等于:Rn + sec_operand。
本文引用地址:https://www.eepw.com.cn/article/201611/317304.htm第二个是:SUB R1,R0, R2, LSL #2。它的意思是:R1=R0 - R2<<2。看了我前面文章的知道,这个指令同样可以像前面一样套入:Rm=R2; Rs=32b100; sec_operand=mult_rm_rs[31:0];Rn=R0;结果等于:Rn - sec_operand。
第三个是:LDR R1,[R0,R2,LSR #2]!。这是一条取RAM的数据进入寄存器的指令,取地址是:R0+R2>>2。并把取地址保存回R0。现在比较难计算的是: R0+R2>>2。但是这个同样也可以往前两个模式一样靠:Rm=R2; Rs=32b0100_0000_0000_0000_0000_0000_0000_0000,那么sec_operand = mult_rm_rs[63:32]正好等于:R2>>2。如果Rn=R0,取地址就等于:Rn+sec_operand。这个地址还要送入R0中。
看到这,大家明白了本核的核心结构了吧。网友先别赞我眼光如炬,目光如神,一眼看出核心所在。实际上我在写第一版的时候,绝没想到把移位交给乘法器来完成,也是傻傻地参考别人文档写了一个桶形移位器。但后来灵光一现,觉得既然乘法器避免不了,如果只让他在MUL指令的时候使用,其他指令的时候闲着,那多么没意思呀。这样乘法器复用起来,让它参与了大部分指令运算。
好了,我们要做的事是这样的。指令到来,准备Rm, Rs, Rn,为生成sec_operand产生控制信号,决定Rn和sec_operand之间是加还是减,那么最后生成的结果要么送入寄存器组,要么作为地址参与读写操作。就这么简单!
前面的这一套完成了,我想ARM核也就成功了大半了。
上面解决了做什么的问题,随之而来的是怎么做的问题。可能大家首先想到的是三级流水线。为什么是三级呢?为什么不是两级呢?两级有什么不好?我告诉你们,两级同样可以,无非是关键路径长一点。我接下来,就要做两级,没有什么能束缚我们!实际上,很多项目用不到30、40MHz的速度,10M,20M也是可以接受,100ns,50ns内,我那一套乘加结构同样能满足。口说无凭,看看我代码中是如何生成:Rm,Rs, sec_operand,Rn的:
注:以下非正式代码,讲解举例所用
/*
always @ ( * )
if ( code_is_ldrh1|code_is_ldrsb1|code_is_ldrsh1 )
code_rm ={code[11:7],code[3:0]};
else if ( code_is_b )
code_rm ={{6{code[23]}},code[23:0],2b0};
else if ( code_is_ldm )
case( code[24:23] )
2d0 : code_rm ={(code_sum_m - 1b1),2b0};
2d1 : code_rm =0;
2d2 : code_rm ={code_sum_m,2b0};
2d3 : code_rm =3b100;
endcase
else if ( code_is_swp )
code_rm =0;
else if ( code_is_ldr0 )
code_rm =code[11:0];
else if ( code_is_msr1|code_is_dp2 )
code_rm =code[7:0];
else if ( code_is_multl & code[22] & code_rma[31] )
code_rm =~code_rma + 1b1;
else if ( ( (code[6:5]==2b10) & code_rma[31] ) & (code_is_dp0|code_is_dp1|code_is_ldr1))
code_rm =~code_rma;
else
code_rm =code_rma;
always @ ( * )
case ( code[3:0] )
4h0 : code_rma =r0;
4h1 : code_rma =r1;
4h2 : code_rma =r2;
4h3 : code_rma =r3;
4h4 : code_rma =r4;
4h5 : code_rma =r5;
4h6 : code_rma =r6;
4h7 : code_rma =r7;
4h8 : code_rma =r8;
4h9 : code_rma =r9;
4ha : code_rma =ra;
4hb : code_rma =rb;
4hc : code_rma =rc;
4hd : code_rma =rd;
4he : code_rma =re;
4hf : code_rma =rf;
endcase
*/
我有if else这个法宝,你不管来什么指令,我都给你准备好Rm。这就像一台脱粒机,你只要在送货口送东西即可。你送麦子脱麦子,你送玉米脱玉米。你的Rm来自于寄存器组,那好我用code_rma来给你选中,送入Rm这个送货口。你的Rm来自代码,就是一套立即数,那我就把code[11:0]送入Rm,下面的程式有了正确的输入,你只要把最后的正确结果,送给寄存器组即可。
再看看Rs的生成:
注:以下非正式代码,讲解举例所用
/*
always @ ( * )
if ( code_is_dp0|code_is_ldr1 )
code_rot_num =( code[6:5] == 2b00 ) ? code[11:7] : ( ~code[11:7]+1b1 );
else if ( code_is_dp1 )
code_rot_num =( code[6:5] == 2b00 ) ? code_rsa[4:0] : ( ~code_rsa[4:0]+1b1 );
else if ( code_is_msr1|code_is_dp2 )
code_rot_num ={ (~code[11:8]+1b1),1b0 };
else
code_rot_num =5b0;
always @ ( * )
if ( code_is_multl )
if ( code[22] & code_rsa[31] )
code_rs =~code_rsa + 1b1;
else
code_rs =code_rsa;
else if ( code_is_mult )
code_rs =code_rsa;
else begin
code_rs =32b0;
code_rs[code_rot_num] = 1b1;
end
always @ ( * )
case ( code[11:8] )
4h0 : code_rsa =r0;
4h1 : code_rsa =r1;
4h2 : code_rsa =r2;
4h3 : code_rsa =r3;
4h4 : code_rsa =r4;
4h5 : code_rsa =r5;
4h6 : code_rsa =r6;
4h7 : code_rsa =r7;
4h8 : code_rsa =r8;
4h9 : code_rsa =r9;
4ha : code_rsa =ra;
4hb : code_rsa =rb;
4hc : code_rsa =rc;
4hd : code_rsa =rd;
4he : code_rsa =re;
4hf : code_rsa =rf;
endcase
*/
Sec_operand的例子就不用举了吧,无非是根据指令选择符合该指令的要求,来送给下一级的加/减法器。
所以说,这样的两级流水线我们同样可以完成。现在使用三级流水线,关键路径是26ns。如果使用两级流水线,绝对在50 ns以内。工作在20MHz的ARM,同样也是受低功耗用户们欢迎的。有兴趣的,在看完我的文章后,把ARM核改造成两级流水线。
现在要转换一个观念。以前的说法:第一级取代码;第二级解释代码,第三级执行代码。现在要转换过来,只有两级,第一级:取代码;第二级执行代码。而现在我做成第三级,是因为一级执行不完,所以要分两级执行。所以是:第一级取代码;第二级执行代码阶段一(主要是乘法);第三级执行代码阶段二(主要是加/减法)。
也许有人要问,那解释代码为什么不安排一级?是因为我觉得解释代码太简单,根本不需要安排一级,这一点,我在下一节会讲到。
既然这个核是三级流水线,还是从三级流水线讲起。我把三级流水线的每一级给了一个标志信号,分别是:rom_en, code_flag, cmd_flag。rom_en对应第一级取代码,如果rom_en==1b1表示需要取代码,那这个代码其实还处在ROM内,我们命名为“胎儿”;如果code_flag==1b1表示对应的code处于执行阶段一,可以命名为“婴儿”;如果cmd_flag==1b1,表示对应的code处于执行阶段二,命名为“小孩”。当这个指令最终执行结束,可以认为它死去了,命名为“幽灵”。
rom_encode_flagcmd_flag
-----------------
|胎儿|婴儿小孩-->幽灵
-----------------
现在,我们模拟一下这个执行过程吧。一般ROM里面从0开始的前几条指令都是跳转指令,以hello这个例程为例,存放的是:LDR PC,[PC,#0x0018];连续五条都是这样的。
刚上电时,rom_en==1b1,表示要取number 0号指令:
rom_en==1b1code_flagcmd_flag
(addr=0)
-----------------
|胎儿|婴儿小孩-->幽灵
-----------------
LDR PC,[PC,#0x0018]
第一个clock后;第一条指令LDR PC,[PC,#0x0018]到了婴儿阶段。
rom_en==1b1code_flagcmd_flag
(addr=4)
-----------------
|胎儿|婴儿小孩-->幽灵
-----------------
LDR PC,[PC,#0x0018]LDR PC,[PC,#0x0018]
第二个clock后,第一条指令LDR PC,[PC,#0x0018]到了小孩阶段。
rom_en==1b1code_flagcmd_flag
(addr=8)
-----------------
|胎儿|婴儿小孩-->幽灵
-----------------
(addr=8)(addr=4)(addr=0)
LDR PC,[PC,#0x0018]LDR PC,[PC,#0x0018]LDR PC,[PC,#0x0018]
当“小孩”== LDR PC,[PC,#0x0018]时,不能再取addr==8的指令了。因为addr=0时的LDR PC,[PC,#0x0018]更改了PC的值,不仅不能取新的code,连处于婴儿阶段的code也不能执行了。如果执行的话,那就是错误执行。为了避免addr=4的LDR PC,[PC,#0x0018]执行,我们可以给每一个阶段打一个标签tag,比如code_flag对应婴儿,cmd_flag对应小孩。只有在cmd_flag==1b1时,指令才执行。如下图所示。
rom_en==1b0code_flagcmd_flag
(addr=8)0-->0 -->
-----------------
|胎儿|婴儿小孩-->幽灵
-----------------
(addr=8)(addr=4)(addr=0)
LDR PC,[PC,#0x0018]LDR PC,[PC,#0x0018]LDR PC,[PC,#0x0018]
(修改PC)
发出读指令
一旦有修改PC,那么rom_en立即赋值为1b0。code_flag, cmd_flag在下一个时钟赋给1b0。表示在下一个时钟“婴儿”和“小孩”都是非法的,不能执行。但是新的PC值不是立即得到的,因为LDR指令是要从RAM取数据,在小孩阶段只能发出读指令,在一个时钟,新的PC值才出现在ram_rdata,但还没有出现在R15里面,所以要等一个时钟。
rom_en==1b0code_flag==1b0cmd_flag==1b0
(addr=8)
-----------------
|胎儿|婴儿小孩-->幽灵
-----------------
(addr=8)(addr=8)(addr=4)(addr=0 )
XLDR PC,[PC,#0x0018]LDR PC,[PC,#0x0018]LDR PC,[PC,#0x0018]
ram_rdata=NEW PC
在空闲的这个周期内,为了让指令不执行,只要赋值:rom_en, code_flag, cmd_flag为1b0就达到目的了。
rom_en, code_flag, cmd_flag在一般情况下都是1b1,但是如果PC值一改变,那么就需要同时被赋值给1b0。不过rom_en和code_flag,cmd_flag有区别: rom_en是立即生效,code_flag/cmd_flag要在下一个时钟生效。rom_en下一个时钟是要有效的,因为要读新的PC值。
改变PC有三种情况:
1,中断发生:我们命名为:int_all。只要中断发生,PC要么等于0,4,8,10,1C等等。
2,从寄存器里给PC赋值:一般情况是:MOV PC,R0。在小孩阶段,已经可以给出新的PC值了,这个和中断类似。我们命名为:to_rf_vld。
3,从RAM里面取值给PC赋值:一般是LDR PC [PC,#0x0018],那么在小孩阶段,发出读指令,我们命名为:cha_rf_vld;在幽灵阶段,新的PC出现,但还没写入PC(R15),这时,也是不能执行任何指令的,我们命名为:go_rf_vld。
下面是我写的rom_en, code_flag, cmd_flag赋值语句,可以对照体会一下。发扬古人“格”物“格”竹子的精神,设想一下,是不是那么回事!
wire rom_en;
assign rom_en =cpu_en & ( ~(int_all | to_rf_vld | cha_rf_vld | go_rf_vld | wait_en | hold_en ) );
regcode_flag;
always @ ( posedge clk or posedge rst )
if ( rst )
code_flag <= #`DEL 1d0;
else if ( cpu_en )
if ( int_all | to_rf_vld | cha_rf_vld | go_rf_vld | ldm_rf_vld )
code_flag <= #`DEL0;
else
code_flag <= #`DEL1;
else;
reg cmd_flag;
always @ ( posedge clk or posedge rst )
if ( rst )
cmd_flag <= #`DEL 1d0;
else if ( cpu_en )
if ( int_all )
cmd_flag <= #`DEL0;
else if ( ~hold_en )
if ( wait_en | to_rf_vld | cha_rf_vld | go_rf_vld )
cmd_flag <= #`DEL0;
else
cmd_flag <= #`DELcode_flag;
else;
else;
ldm_rf_vld是在执行LDM指令时,改变R15的情况,这个情况比较特殊,以后再讲。
除了这个,还有wait_en和hold_en。我还是举例子说明吧。
1,wait_en
如果R0 = 0x0, R1=0x0。紧接着会执行下面两条指令:1, MOV R0,#0xFFFF; 2, ADD R1,R1,[R0,LSL #4]。执行完后,正确的结果应该是:R1=0xFFFF0。
rom_encode_flagcmd_flag
-----------------
|胎儿|婴儿小孩-->幽灵
-----------------
XADD R1,R1,[R0,LSL #4]MOV R0,#0xFFFF
如上图在“小孩”阶段:正在执行MOV R0,#0xFFFF,但是R0这个寄存器里面存放的是0x0,而不是0xFFFF。因为在小孩阶段,只是要写R1,但是并没有写入,在下一个时钟生效。但是“婴儿”阶段,要执行ADD R1,R1,[R0, LSL #4],必须先对R0移位。那么它取得R0的来源是从case语句,是从R0这个寄存器里得来的,而不是“小孩”阶段执行的结果得来的。
所以如果出项这样的情况:上一条指令的输出,正好是下一条指令的输入。那么下一条指令是不能执行,必须要缓一个周期执行。也就是说在两条指令之间插入一个空指令,让R0得到新的值,再执行下一条语句,就不会出错。wait_en就表示这种情况。
如果wait_en == 1b1,那么rom_en==1b0,表示ADD R1,R1,[R0,LSL #4]还没执行呢,先不用取下一条指令。code_flag不受wait_en影响;cmd_flag<=1b0;下一个时钟,表示这是一条空指令,并不执行。
2,hold_en
简而言之,就是在cmd_flag这一阶段的指令一个时钟执行不下去,需要多个时钟。比如说:LDMIA R13! {R0-R3},需要从RAM里面读四个数,送入相应的寄存器。我们只有一个RAM的读写端口,执行这条命令需要启动这个读写端口四次。那么就要告诉rom_en,你不能取新数呐。所以我们在LDMIA R13! {R0-R3}占用的4个周期里,前三个时,让hold_en==1b1。那么在这段时间内,rom_en==1b0, cmd_flag不受影响。因为这时执行有效,cmd_flag必须保持开始的1b1不变。
好了,这一节,先写到这,希望大家也发挥divide & conquer的精神,一点点的解决问题,走向最后的成功,欢迎提出有疑问的地方。
评论