新聞中心

EEPW首頁 > 嵌入式系統(tǒng) > 設計應用 > 跟我寫ARM處理器之二:主體結構的確定

跟我寫ARM處理器之二:主體結構的確定

作者: 時間:2016-11-10 來源:網絡 收藏
好了,上一節(jié)定義了端口,基本功能大慨大家已經了然于胸了,現(xiàn)在來確定一下主體結構。我舉幾個指令執(zhí)行的例子吧。

第一個是MLA R1,R2,R3,R0。它的意思是:R1=R2*R3 + R0。如果我們要實現(xiàn)這一條指令的話,一個32×32的乘法器需要,一個32+32的加法器是跑不了的?,F(xiàn)在定義幾個節(jié)點:Rm = R2; Rs=R3; sec_operand(第二操作數(shù)的意思)=mult_rm_rs[31:0](mult_rm_rs的低32位);Rn=R0;則結果等于:Rn + sec_operand。

本文引用地址:http://m.butianyuan.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的數(shù)據進入寄存器的指令,取地址是:R0+R2>>2。并把取地址保存回R0?,F(xiàn)在比較難計算的是: 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中。

看到這,大家明白了本核的核心結構了吧。網友先別贊我眼光如炬,目光如神,一眼看出核心所在。實際上我在寫第一版的時候,絕沒想到把移位交給乘法器來完成,也是傻傻地參考別人文檔寫了一個桶形移位器。但后來靈光一現(xiàn),覺得既然乘法器避免不了,如果只讓他在MUL指令的時候使用,其他指令的時候閑著,那多么沒意思呀。這樣乘法器復用起來,讓它參與了大部分指令運算。

好了,我們要做的事是這樣的。指令到來,準備Rm, Rs, Rn,為生成sec_operand產生控制信號,決定Rn和sec_operand之間是加還是減,那么最后生成的結果要么送入寄存器組,要么作為地址參與讀寫操作。就這么簡單!

前面的這一套完成了,我想ARM核也就成功了大半了。

上面解決了做什么的問題,隨之而來的是怎么做的問題。可能大家首先想到的是三級流水線。為什么是三級呢?為什么不是兩級呢?兩級有什么不好?我告訴你們,兩級同樣可以,無非是關鍵路徑長一點。我接下來,就要做兩級,沒有什么能束縛我們!實際上,很多項目用不到30、40MHz的速度,10M,20M也是可以接受,100ns,50ns內,我那一套乘加結構同樣能滿足??谡f無憑,看看我代碼中是如何生成: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來自代碼,就是一套立即數(shù),那我就把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的例子就不用舉了吧,無非是根據指令選擇符合該指令的要求,來送給下一級的加/減法器。

所以說,這樣的兩級流水線我們同樣可以完成?,F(xiàn)在使用三級流水線,關鍵路徑是26ns。如果使用兩級流水線,絕對在50 ns以內。工作在20MHz的ARM,同樣也是受低功耗用戶們歡迎的。有興趣的,在看完我的文章后,把ARM核改造成兩級流水線。

現(xiàn)在要轉換一個觀念。以前的說法:第一級取代碼;第二級解釋代碼,第三級執(zhí)行代碼?,F(xiàn)在要轉換過來,只有兩級,第一級:取代碼;第二級執(zhí)行代碼。而現(xiàn)在我做成第三級,是因為一級執(zhí)行不完,所以要分兩級執(zhí)行。所以是:第一級取代碼;第二級執(zhí)行代碼階段一(主要是乘法);第三級執(zhí)行代碼階段二(主要是加/減法)。

也許有人要問,那解釋代碼為什么不安排一級?是因為我覺得解釋代碼太簡單,根本不需要安排一級,這一點,我在下一節(jié)會講到。

既然這個核是三級流水線,還是從三級流水線講起。我把三級流水線的每一級給了一個標志信號,分別是:rom_en, code_flag, cmd_flag。rom_en對應第一級取代碼,如果rom_en==1b1表示需要取代碼,那這個代碼其實還處在ROM內,我們命名為“胎兒”;如果code_flag==1b1表示對應的code處于執(zhí)行階段一,可以命名為“嬰兒”;如果cmd_flag==1b1,表示對應的code處于執(zhí)行階段二,命名為“小孩”。當這個指令最終執(zhí)行結束,可以認為它死去了,命名為“幽靈”。

rom_encode_flagcmd_flag

-----------------

|胎兒|嬰兒小孩-->幽靈

-----------------

現(xiàn)在,我們模擬一下這個執(zhí)行過程吧。一般ROM里面從0開始的前幾條指令都是跳轉指令,以hello這個例程為例,存放的是:LDR PC,[PC,#0x0018];連續(xù)五條都是這樣的。

剛上電時,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也不能執(zhí)行了。如果執(zhí)行的話,那就是錯誤執(zhí)行。為了避免addr=4的LDR PC,[PC,#0x0018]執(zhí)行,我們可以給每一個階段打一個標簽tag,比如code_flag對應嬰兒,cmd_flag對應小孩。只有在cmd_flag==1b1時,指令才執(zhí)行。如下圖所示。

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)

發(fā)出讀指令

一旦有修改PC,那么rom_en立即賦值為1b0。code_flag, cmd_flag在下一個時鐘賦給1b0。表示在下一個時鐘“嬰兒”和“小孩”都是非法的,不能執(zhí)行。但是新的PC值不是立即得到的,因為LDR指令是要從RAM取數(shù)據,在小孩階段只能發(fā)出讀指令,在一個時鐘,新的PC值才出現(xiàn)在ram_rdata,但還沒有出現(xiàn)在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

在空閑的這個周期內,為了讓指令不執(zhí)行,只要賦值:rom_en, code_flag, cmd_flag為1b0就達到目的了。

rom_en, code_flag, cmd_flag在一般情況下都是1b1,但是如果PC值一改變,那么就需要同時被賦值給1b0。不過rom_en和code_flag,cmd_flag有區(qū)別: rom_en是立即生效,code_flag/cmd_flag要在下一個時鐘生效。rom_en下一個時鐘是要有效的,因為要讀新的PC值。

改變PC有三種情況:

1,中斷發(fā)生:我們命名為:int_all。只要中斷發(fā)生,PC要么等于0,4,8,10,1C等等。

2,從寄存器里給PC賦值:一般情況是:MOV PC,R0。在小孩階段,已經可以給出新的PC值了,這個和中斷類似。我們命名為:to_rf_vld。

3,從RAM里面取值給PC賦值:一般是LDR PC [PC,#0x0018],那么在小孩階段,發(fā)出讀指令,我們命名為:cha_rf_vld;在幽靈階段,新的PC出現(xiàn),但還沒寫入PC(R15),這時,也是不能執(zhí)行任何指令的,我們命名為:go_rf_vld。

下面是我寫的rom_en, code_flag, cmd_flag賦值語句,可以對照體會一下。發(fā)揚古人“格”物“格”竹子的精神,設想一下,是不是那么回事!

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是在執(zhí)行LDM指令時,改變R15的情況,這個情況比較特殊,以后再講。

除了這個,還有wait_en和hold_en。我還是舉例子說明吧。

1,wait_en

如果R0 = 0x0, R1=0x0。緊接著會執(zhí)行下面兩條指令:1, MOV R0,#0xFFFF; 2, ADD R1,R1,[R0,LSL #4]。執(zhí)行完后,正確的結果應該是:R1=0xFFFF0。

rom_encode_flagcmd_flag

-----------------

|胎兒|嬰兒小孩-->幽靈

-----------------

XADD R1,R1,[R0,LSL #4]MOV R0,#0xFFFF

如上圖在“小孩”階段:正在執(zhí)行MOV R0,#0xFFFF,但是R0這個寄存器里面存放的是0x0,而不是0xFFFF。因為在小孩階段,只是要寫R1,但是并沒有寫入,在下一個時鐘生效。但是“嬰兒”階段,要執(zhí)行ADD R1,R1,[R0, LSL #4],必須先對R0移位。那么它取得R0的來源是從case語句,是從R0這個寄存器里得來的,而不是“小孩”階段執(zhí)行的結果得來的。

所以如果出項這樣的情況:上一條指令的輸出,正好是下一條指令的輸入。那么下一條指令是不能執(zhí)行,必須要緩一個周期執(zhí)行。也就是說在兩條指令之間插入一個空指令,讓R0得到新的值,再執(zhí)行下一條語句,就不會出錯。wait_en就表示這種情況。

如果wait_en == 1b1,那么rom_en==1b0,表示ADD R1,R1,[R0,LSL #4]還沒執(zhí)行呢,先不用取下一條指令。code_flag不受wait_en影響;cmd_flag<=1b0;下一個時鐘,表示這是一條空指令,并不執(zhí)行。

2,hold_en

簡而言之,就是在cmd_flag這一階段的指令一個時鐘執(zhí)行不下去,需要多個時鐘。比如說:LDMIA R13! {R0-R3},需要從RAM里面讀四個數(shù),送入相應的寄存器。我們只有一個RAM的讀寫端口,執(zhí)行這條命令需要啟動這個讀寫端口四次。那么就要告訴rom_en,你不能取新數(shù)吶。所以我們在LDMIA R13! {R0-R3}占用的4個周期里,前三個時,讓hold_en==1b1。那么在這段時間內,rom_en==1b0, cmd_flag不受影響。因為這時執(zhí)行有效,cmd_flag必須保持開始的1b1不變。

好了,這一節(jié),先寫到這,希望大家也發(fā)揮divide & conquer的精神,一點點的解決問題,走向最后的成功,歡迎提出有疑問的地方。



關鍵詞: ARM處理器主體結

評論


技術專區(qū)

關閉