新闻中心

EEPW首页>嵌入式系统>设计应用> 跟我写ARM处理器之二:主体结构的确定

跟我写ARM处理器之二:主体结构的确定

作者: 时间:2016-11-10 来源:网络 收藏
好了,上一节定义了端口,基本功能大慨大家已经了然于胸了,现在来确定一下主体结构。我举几个指令执行的例子吧。

第一个是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。

本文引用地址://m.amcfsurvey.com/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的精神,一点点的解决问题,走向最后的成功,欢迎提出有疑问的地方。



关键词:ARM处理器主体结

评论


技术专区

关闭