跟我寫ARM處理器之二:主體結構的確定
第一個是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的精神,一點點的解決問題,走向最后的成功,歡迎提出有疑問的地方。
評論