第一个是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。
第二个是:SUB R1,R0, R2, LSL #2。它的意思是:R1=R0 - R2<<2。看了我前面文章的知道,这个指令同样可以像前面一样套入:Rm=R2; Rs=32'b100; 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=32'b0100_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],2'b0};
else if ( code_is_ldm )
case( code[24:23] )
2'd0 : code_rm = {(code_sum_m - 1'b1),2'b0};
2'd1 : code_rm = 0;
2'd2 : code_rm = {code_sum_m,2'b0};
2'd3 : code_rm = 3'b100;
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 + 1'b1;
else if ( ( (code[6:5]==2'b10) & 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] )
4'h0 : code_rma = r0;
4'h1 : code_rma = r1;
4'h2 : code_rma = r2;
4'h3 : code_rma = r3;
4'h4 : code_rma = r4;
4'h5 : code_rma = r5;
4'h6 : code_rma = r6;
4'h7 : code_rma = r7;
4'h8 : code_rma = r8;
4'h9 : code_rma = r9;
4'ha : code_rma = ra;
4'hb : code_rma = rb;
4'hc : code_rma = rc;
4'hd : code_rma = rd;
4'he : code_rma = re;
4'hf : 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] == 2'b00 ) ? code[11:7] : ( ~code[11:7]+1'b1 );
else if ( code_is_dp1 )
code_rot_num = ( code[6:5] == 2'b00 ) ? code_rsa[4:0] : ( ~code_rsa[4:0]+1'b1 );
else if ( code_is_msr1|code_is_dp2 )
code_rot_num = { (~code[11:8]+1'b1),1'b0 };
else
code_rot_num = 5'b0;
always @ ( * )
if ( code_is_multl )
if ( code[22] & code_rsa[31] )
code_rs = ~code_rsa + 1'b1;
else
code_rs = code_rsa;
else if ( code_is_mult )
code_rs = code_rsa;
else begin
code_rs = 32'b0;
code_rs[code_rot_num] = 1'b1;
end
always @ ( * )
case ( code[11:8] )
4'h0 : code_rsa = r0;
4'h1 : code_rsa = r1;
4'h2 : code_rsa = r2;
4'h3 : code_rsa = r3;
4'h4 : code_rsa = r4;
4'h5 : code_rsa = r5;
4'h6 : code_rsa = r6;
4'h7 : code_rsa = r7;
4'h8 : code_rsa = r8;
4'h9 : code_rsa = r9;
4'ha : code_rsa = ra;
4'hb : code_rsa = rb;
4'hc : code_rsa = rc;
4'hd : code_rsa = rd;
4'he : code_rsa = re;
4'hf : code_rsa = rf;
endcase
*/
Sec_operand的例子就不用举了吧,无非是根据指令选择符合该指令的要求,来送给下一级的加/减法器。
所以说,这样的两级流水线我们同样可以完成。现在使用三级流水线,关键路径是26ns。如果使用两级流水线,绝对在50 ns以内。工作在20MHz的ARM,同样也是受低功耗用户们欢迎的。有兴趣的,在看完我的文章后,把ARM核改造成两级流水线。
现在要转换一个观念。以前的说法:第一级取代码;第二级解释代码,第三级执行代码。现在要转换过来,只有两级,第一级:取代码;第二级执行代码。而现在我做成第三级,是因为一级执行不完,所以要分两级执行。所以是:第一级取代码;第二级执行代码阶段一(主要是乘法);第三级执行代码阶段二(主要是加/减法)。
也许有人要问,那解释代码为什么不安排一级?是因为我觉得解释代码太简单,根本不需要安排一级,这一点,我在下一节会讲到。
既然这个核是三级流水线,还是从三级流水线讲起。我把三级流水线的每一级给了一个标志信号,分别是:rom_en, code_flag, cmd_flag。rom_en对应第一级取代码,如果rom_en==1'b1表示需要取代码,那这个代码其实还处在ROM内,我们命名为“胎儿”;如果code_flag==1'b1表示对应的code处于执行阶段一,可以命名为“婴儿”;如果cmd_flag==1'b1,表示对应的code处于执行阶段二,命名为“小孩”。当这个指令最终执行结束,可以认为它死去了,命名为“幽灵”。
rom_en code_flag cmd_flag
-----------------
| 胎儿 | 婴儿 小孩 --> 幽灵
-----------------
现在,我们模拟一下这个执行过程吧。一般ROM里面从0开始的前几条指令都是跳转指令,以hello这个例程为例,存放的是:LDR PC,[PC,#0x0018];连续五条都是这样的。
刚上电时,rom_en==1'b1,表示要取number 0 号指令:
rom_en==1'b1 code_flag cmd_flag
(addr=0)
-----------------
| 胎儿 | 婴儿 小孩 --> 幽灵
-----------------
LDR PC,[PC,#0x0018]
第一个clock后;第一条指令LDR PC,[PC,#0x0018]到了婴儿阶段。
rom_en==1'b1 code_flag cmd_flag
(addr=4)
-----------------
| 胎儿 | 婴儿 小孩 --> 幽灵
-----------------
LDR PC,[PC,#0x0018] LDR PC,[PC,#0x0018]
第二个clock后,第一条指令LDR PC,[PC,#0x0018]到了小孩阶段。
rom_en==1'b1 code_flag cmd_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==1'b1时,指令才执行。如下图所示。
rom_en==1'b0 code_flag cmd_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立即赋值为1'b0。code_flag, cmd_flag在下一个时钟赋给1'b0。表示在下一个时钟“婴儿”和“小孩”都是非法的,不能执行。但是新的PC值不是立即得到的,因为LDR指令是要从RAM取数据,在小孩阶段只能发出读指令,在一个时钟,新的PC值才出现在ram_rdata,但还没有出现在R15里面,所以要等一个时钟。
rom_en==1'b0 code_flag==1'b0 cmd_flag==1'b0
(addr=8)
-----------------
| 胎儿 | 婴儿 小孩 --> 幽灵
-----------------
(addr=8) (addr=8) (addr=4) (addr=0 )
X LDR PC,[PC,#0x0018] LDR PC,[PC,#0x0018] LDR PC,[PC,#0x0018]
ram_rdata=NEW PC
在空闲的这个周期内,为了让指令不执行,只要赋值:rom_en, code_flag, cmd_flag为1'b0就达到目的了。
rom_en, code_flag, cmd_flag在一般情况下都是1'b1,但是如果PC值一改变,那么就需要同时被赋值给1'b0。不过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 ) );
reg code_flag;
always @ ( posedge clk or posedge rst )
if ( rst )
code_flag <= #`DEL 1'd0;
else if ( cpu_en )
if ( int_all | to_rf_vld | cha_rf_vld | go_rf_vld | ldm_rf_vld )
code_flag <= #`DEL 0;
else
code_flag <= #`DEL 1;
else;
reg cmd_flag;
always @ ( posedge clk or posedge rst )
if ( rst )
cmd_flag <= #`DEL 1'd0;
else if ( cpu_en )
if ( int_all )
cmd_flag <= #`DEL 0;
else if ( ~hold_en )
if ( wait_en | to_rf_vld | cha_rf_vld | go_rf_vld )
cmd_flag <= #`DEL 0;
else
cmd_flag <= #`DEL code_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_en code_flag cmd_flag
-----------------
| 胎儿 | 婴儿 小孩 --> 幽灵
-----------------
X ADD 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 == 1'b1,那么rom_en==1'b0, 表示ADD R1,R1,[R0,LSL #4]还没执行呢,先不用取下一条指令。code_flag不受wait_en影响;cmd_flag<=1'b0;下一个时钟,表示这是一条空指令,并不执行。
2,hold_en
简而言之,就是在cmd_flag这一阶段的指令一个时钟执行不下去,需要多个时钟。比如说:LDMIA R13! {R0-R3},需要从RAM里面读四个数,送入相应的寄存器。我们只有一个RAM的读写端口,执行这条命令需要启动这个读写端口四次。那么就要告诉rom_en,你不能取新数呐。所以我们在LDMIA R13! {R0-R3}占用的4个周期里,前三个时,让hold_en==1'b1。那么在这段时间内,rom_en==1'b0, cmd_flag不受影响。因为这时执行有效,cmd_flag必须保持开始的1'b1不变。
好了,这一节,先写到这,希望大家也发挥divide & conquer的精神,一点点的解决问题,走向最后的成功,欢迎提出有疑问的地方。
上一篇:stm32的 开漏电路 与 推挽输出
下一篇:跟我写ARM处理器之一:从写module arm开始
推荐阅读最新更新时间:2024-03-16 15:00