好了,上一节界说了端口,基本功能大慨咱们现已了然于胸了,现在来确认一下主体结构。我举几个指令履行的比方吧。
榜首个是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=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的精力,一点点的处理问题,走向终究的成功,欢迎提出有疑问的当地。