原创 跟我写ARM处理器之一:从写module arm( 开始

2010-7-21 16:06 4576 9 9 分类: FPGA/CPLD


http://free-arm.blog.163.com/blog/static/1076779632009497742348/



跟我写ARM处理器之一:从写module arm( 开始

  我决定把我写ARM处理器的经验分享给大家。不是自卖自夸,我这个核是非常好的核。一个证明方法是下载入FPGA,看她是不是能工作。这种证明方法,我已经做到了。大家如果可以下载整个工程文件,稍微改造一下,即能验证是否成功。但是这还不是推广她的方法。因为对于大多数人来说,只是知其然不知其所以然。人们对于不熟悉、不了解的事情,总是觉得神秘,那么对于她的应用,就会大打折扣。推广她的最好的方法,就是让她成为人人能够轻松写的核。我希望以此推动大家对于SOC设计的热情。可以想象,如果最神秘,最复杂的CPU核,都可以轻松的写出来的话,那么还有什么能够难得到我们的呢?

在写之前,我要申明两点精神。这两点精神当然不是我创造的,我只是引用起来,让大家更能读下去,能相信我是能创造这个奇迹的,当然你也能创造这个奇迹的。第一点就是毛主席在军事上经常用的“分而歼之”,在政治上的意思就是“拉一派,打一派”。这一招在毛主席的斗争生涯中屡试不爽,成就了他成了伟人。这一点国外叫做“divide and conqur(没拼写对,懒得查了)",也是很重要的一个方法。所以在面对庞然大物的时候,千万别害怕,一定要祭起这个”法宝“。第一步,先试着“divide",一定让这个庞然大物小起来,然后再小下去,先条分缕析了,已经算成功了一大半了;第二步,从最小的开始,一点点的做,做对了,做好了。好到什么程度?好到这一小点绝不会打扰我们继续征服下一小点为至。那么征服这个庞然大物就只是时间问题了。包括我现在写这个文章,就得用这个精神,不然的话,千头万绪,从哪里说呢?从哪里说的清呢?不用怕,我既然用这个方法征服了ARM core,那么把它讲出来,讲明白,绝对比写arm core要简单,我相信我也能够完成的。

但是我缺一点东西:那就是视角问题。因为我是站在我的立场上,我了解一切,我讲的时候,只是想当然的从我的角度认为大家该了解什么。但实际上,这中间的差距,就像梦想和现实的差距一样远。所以我需要大家有反馈,那一点忽略了,及时提醒我,让我始终站在大家的立场上,讲东西,而不是让我自说自话。如果我自说自话,效果非常差。所以请大家成全我,成全我就是成全自己。如果可能,请及时提出不明白的地方。让我把这今天的这一小节征服到不会打扰我征服下一小节,这是符合我上面的精神。

有网友,可能会问,我费心费力的图什么?为什么要成全我?我图啥呢?反正我从小也算一个调皮的孩子,越是大人不让我干的事情,我还是愿意冒险试一下。当然总体来说,我小时候也算听话,属于只做”建设性“破坏的人,掏马蜂窝、堵烟筒这事不干。现在有一件特别有意义的事情,比如ARM公司的这些达人们,卖ARM7核卖的不亦乐乎。突然,我闯进来,说这东西根本不值钱,你得卖真货,不是特别有意思么?ARM公司越不爽,其实对于大家来说越是爽。因为大家总体的水平提高了,那么ARM公司就必须努力提高自己的水平,这样才能凸显他自己的价值。所以这件事情想起来就特别有意思,所以我就决定做这么一件”建设性“破坏的事情。所以请大家成全我,成全别人其实也是成就自己,请大家和我一起实践吧。所以,我如果是个软件工程师,我一定会写些病毒啥的,有意思呀!但放心我只会做“建设性”的,我做的病毒绝不会给大家带来太多麻烦,因为这不符合我的风格。但我现在既然是一个硬件工程师,就得想方法在硬件上做一些大事。做一些让让某些“大人”不爽的事。人在世上,也就图一乐嘛。呵呵,说多了。

说了半天,还没有说第二点精神。其实第二点也就包括在第一点上了。不过还是有分别的。这第二点精神就是古人的”格物致知“的功夫。古代这些儒学大家们经常对着一花一叶格个半天啥的,就是这种精神的极致。"divide and conqur"只是斗争原则,但是怎么conqur呢?我觉得要用古人的”格物致知“,也就是耐得烦,从最细小的东西”格“起。我在后面对每一个net,每一个wire都描述出来,就是这样”格“的结果。cpu是一套复杂的系统,要做的好,就得用net, wire, register搭起来,听说intel就是这么干的。不过我是和它有分别的。因为我的这一套,还是把很多功夫交给综合器完成,我只是描述出来,让综合器明白而已。

闲话休提,我希望大家看了我这一系列的文章后,能够把我的arm核写出来。当然你能发挥,我希望的是大家发挥,把这个核写的更好。因为我在做离经叛道的事情,哪能希望别人”墨守成规“把我的核当成标杆?我只是希望抛砖引玉。我要抛出一块砖,对arm这个皇帝的新衣说出:你根本没穿衣服!难道我知道这个了,不应该告诉大家么?所以尽情的糟蹋我的arm核吧,因为这样,它存在的意义更大。

开始写了:第一句是: module arm ( 。这恐怕是verilog设计的经典开篇了。然后是什么?是定义接口。接口其实规定了大部分的功能,对于IP核的重用,交代清楚接口,跟下家说明清楚,别人才好用核。
module arm (
clk,
rst,
cpu_en,
clk是时钟,rst是异步复位信号。这个就不需要我多解释了。cpu_en是什么?cpu_en是同步使能信号。也就是说只有在cpu_en==1'b1时,整个核才工作。如果cpu_en==1'b0时,那么这个arm 核就不工作了。简单的说,就是在这个verilog文件里面的所有register都不做什么改变。这就好像有些童话里面说的“时间停止”,城堡里面的人都静住了,一切都停止了。那么寄存器就属于住在城堡里面的人,因为只要他们安静了,没有活物了,那些依靠人而存在的工具就不会动了。这个cpu_en对于整个模块也是重要的,比如你不想让它工作了,就可让cpu_en==1'b0,那么他就不工作。再让他工作,只要让"cpu_en==1'b1"就可。我在这个FPGA的项目中,就应用这个输入连到一个“开关”上。如果扳到"1'b1",那系统开始工作;如果扳到"1'b0",那它就停住,我就通过串口,把新的code下到它的rom中。
cpu_restart,
irq,
fiq,
rom_abort,
ram_abort,
这是ARM的五个中断源,具体的可以参考ARM的文档,我在这里只是简略带一遍。cpu_restart相当于一个同步复位,它只要是高电平,那么系统又重头开始了。irq,fiq是两个中断,只要它等于“1'b1”如果cpsr没关掉它,就得执行相应的中断。rom_abort, ram_abort是对应于rom,ram读错误,则启动一个中断来处理这些事情。
rom_en,
rom_addr,
rom_data,
这三个信号连接到ROM中,是指令的来源。取指令的方法是:rom_en==1'b1时,在下一个时钟,存放在rom_addr位置的code就出现在rom_data了。当然如果rom_en==1'b0,则rom_data保持原来的值不变。这是一个非常普通的ROM。取指令的主动权掌握在ARM核中。如果它要处理非常复杂的指令,一个周期根本处理不过来,那怎么办?那就不启动rom_en,只有当处理完了,有空闲了再启动rom_en,更新rom_data。
ram_cen,
ram_wen,
ram_addr,
ram_wdata,
ram_rdata,
ram_flag
);
前五个“ram_cen,ram_wen,ram_addr,ram_wdata,ram_rdata”是典型的单端口RAM的接口。功能是:在ram_cen==1'b1时,开始执行对RAM的操作,同时ram_wen==1'b1,执行写操作,把ram_wdata写入ram_addr对应的位置;如果ram_wen==1'b0,表示是读操作,就得在下一个时钟,把放在ram_addr的数据出现在ram_rdata。
ram_flag是我新增加的。因为RAM里面出现了LDRB, STRH这样对字节、字的操作。为了操作简单,我给每一个字节对应了一个标志位:如果ram_flag[0]==1'b1表示第一个字节的操作有效;如果ram_flag[0]==1'b1,则读写操作对第一个字节无效。比如我执行STRB 0x4000_0000,则ram_flag==4'b0001, 表示只对地址0x4000_0000的低8bit进行写入。如果STRH 0x4000_0002,则ram_flag==4'b1100, 表示写入0x4000_0000的高 2 byte。
所以我建议连上4块RAM,每块RAM的位宽都是8 bit。让ARM核对每byte的操作是独立的。那每一块的cen就是:ram_cen&ram_flag[3]; ram_cen&ram_flag[2]; ram_cen&ram_flag[1]; ram_cen&ram_flag[0]。
好了,接下来,就是他们位宽的定义:
input clk;
input rst;
input cpu_en;
input cpu_restart;
input irq;
input fiq;
input rom_abort;
input ram_abort;
output rom_en;
output [31:0] rom_addr;
input [31:0] rom_data;
output ram_cen;
output ram_wen;
output [31:0] ram_addr;
output [31:0] ram_wdata;
input [31:0] ram_rdata;
output [31:0] ram_flag;



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

 

好了,上一节定义了端口,基本功能大慨大家已经了然于胸了,现在来确定一下主体结构。我举几个指令执行的例子吧。

第一个是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的精神,一点点的解决问题,走向最后的成功,欢迎提出有疑问的地方。


好了,本篇到此结束,下一节再见!


   复位异常
复位信号到达后,执行下列操作:
l 强制进入管理模式
l 强制进入ARM状态
l 跳转到绝对地址:PC=0x0000_0000处执行
l 禁止IRQ和FRQ中断
l CPSR低8位变为:110_10011
未定义指令异常
在两种情况进入这种中断:(1)无法执行的指令;(2)对协处理器的操作指令,无应答,处理方式:
l 下一条指令的地址拷贝给LR
l 程序状态寄存器CPSR拷贝到SPSR_und
l 强制进入未定义模式
l 强制进入ARM状态
l 跳转到绝对地址:PC=0x0000_0004处执行
l 禁止IRQ中断
l 中断后CPSR状态:1x0_11011
软件中断异常
指令SWI引起,进入管理模式:
l 下一条指令的地址拷贝到LR
l 程序状态寄存器CPSR拷贝给SPSR_svc
l 强制进行管理模式
l 强制进入ARM状态
l 跳转到绝对地址PC=0x0000_0008执行
l 禁止IRQ中断
l 中断后,CPSR状态:1x0_10011
预取指中断异常
如果在访问程序存储器时,程序存储器发出ABORT信号,则这一指令为无效。但在执行这条指令前,程序跳转,则不执行这条指令。否则进入预取指中止异常。
l 中断时的PC拷贝到LR
l CPSR->SPSR_abt
l 进入中止异常模式
l 进入ARM状态
l 跳转到绝对地址:PC=0x0000——000C。
l 禁止IRQ
l CPSR变为:1x010111
数据中止异常
不能获得读数据。
l 中断时PC->LR
l CPSR->SPSR_abt
l 进入中止异常模式
l 进入ARM状态
l 跳转到绝对地址:0x0000_0010
l 禁止IRQ中断
l CPSR->1x0_10111
中断请求异常
中断请求信号:nIRQ。当中断出现后,执行如下操作:
l 把中断时PC的地址值拷贝给LR;
l 把程序状态寄存器CPSR拷贝给SPSR_irq;
l 强制进入IRQ异常模式
l 强制进入到ARM状态
l 跳转到绝对地址PC=0x0000_0018处执行;
l 禁止IRQ中断
进入中断后,程序状态寄存器:1x010010。

完成中断后,程序应该执行SUBS PC,R14,#4恢复到原中断处。

快速中断(FIQ)请求异常
中断请求信号:nFIQ。中断时,执行如下操作:
l 把中断时PC的地址拷贝给LR;
l 把程序状态寄存器拷贝给SPSR_fiq;
l 强制进入FIQ异常模式
l 强制进入到ARM状态;
l 跳转到绝对地址PC=0x0000_001C处执行;
l 禁止IRQ中断;
l 禁止FIQ中断
进入中断后,程序状态寄存器应该为:11010001.
FIQ异常中断是快速的中断,体现在:
(1)有较高的优先级。
(2)更多的专用寄存器R8_fiq- R12_fiq,节约保护工作寄存器所使用的时间。
(3)中断地址向量表中的最后一个提供给FIQ中断使用。可以避免使用跳转指令,直接进入执行。
返回时的指令:SUBS PC,R14,#4    









































































































































































































































































































































































































































PARTNER CONTENT

文章评论0条评论)

登录后参与讨论
EE直播间
更多
我要评论
0
9
关闭 站长推荐上一条 /3 下一条