跳至主要内容

MIPS中断在uboot下的实现

摘要:本文阐述了MIPS中断在u-boot下的整个实现过程,并对实践中遇到的问题进行了分析,特别分析了u-boot所采用的位置无关代码及汇编实现的特殊性,对类似应用具有较好的借鉴意义。


随着Linux嵌入式系统的广泛使用,使用uboot作为bootloader越来越多。u-boot发源于ppc架构8xx系列,对ppc架构的支持最为完善,而对其它架构的支持情况则要弱一些,以MIPS为例,目前的最新版本u-boot-2010.03仍然不支持异常和中断的处理。缺少中断机制带来了一些麻烦,比如不能支持统一的点灯规范,更重要的是在无中断的单任务环境下,协议栈缺乏有效的超时重传机制,在复杂的框上环境中,单板容易假死在boot中。


本文描述了MIPS架构中断在u-boot下的实现过程,并对遇到的问题进行了分析和总结。我们首先讨论了通用的异常处理过程,然后结合代码实现对异常处理代码进行了阐述,最后对调试过程中出现的问题进行了分析。u-boot有其自身的特点,本文同时也对位置无关代码进行了一定的分析。


中断处理在实现上并不复杂,其处理过程可以简单地划分为下面几个步骤:


1. 保存中断上下文

2. 处理中断并应答

3. 恢复中断上下文

4. 使能中断


这种处理方式比较简单,整个中断处理是在中断环境中完成的,没有中断嵌套。对于VxWorks这样的实时操作系统,需要允许高优先级中断抢占低优先级中断,因此它的处理步骤要复杂一些,在进入中断后,只保存少量的易失性寄存器,并尽早地开启中断。因为我们只需要实现简单的定时器,所以就按照最简单的方法实现。


MIPS CPU产生中断时,CPU跳转到EBASE+0x180或者0x200(取决于COP0 Cause.IV的设置。IV=1时,中断使用0x200入口,IV=0时,中断使用通用异常入口0x180)处开始执行中断处理程序。一般来说,异常向量处的大小都是受限的,MIPS也不例外,每个异常向量的大小不能超过0x80的长度,如果要在这里进行上下文保存,空间是不够的。惯例是在这里设一处跳转,到一个大小不受限的地方开始保存上下文。


上下文的保存可以分为两种实现,一种是使用当前任务的栈,一种是使用专门的栈,对于只工作在内核态的boot来说,这两者在使用上并无差别。理论上,开辟专门的栈可靠性更高,不容易被破坏。对于不同的架构,上下文的含义不同,对MIPS而言,上下文包括了32个通用寄存器$0~$31,两个整数运算寄存器HI和LO,还有COP0的几个关键寄存器,如STATUS、CAUSE和EPC等,如果支持MMU,则需要保存更多的寄存器。


在MIPS架构下,中断也是一种异常。CPU在进入异常处理程序前,COP0 Status.EXL被置位,表明CPU进入异常状态。EXL在置位的情况下,会禁止软/硬件中断,这使得中断处理程序在一个安全可靠的环境中运行(当然,不包括中断处理程序再次引发异常)。中断处理程序需要确认中断,否则退出中断时,将再将进入中断,CPU会一直运行在中断处理程序中,不再处理其它事务,也就是常见的中断挂死。


中断处理完毕后,将之前保存在内存中的各类寄存器恢复出来,然后返回到被中断的代码继续执行,这就是中断处理的整个过程。


我们先实现一个最简单的中断处理,借以跑通中断流程,中断是采用Timer中断,也即7号中断。


void trap_init(ulong arg)
{
    uint32_t val;
    ebase = K0BASE;
    /* clear SR.BEV */
    val = read_c0_status();
    val &= ~(1<<22);
    write_c0_status(val);
    /* clear CAUSE.IV */
    val = read_c0_cause();
    val &= ~(1<<23);
    write_c0_cause(val);
    /* set ebase */
    write_c0_ebase(ebase);
    /* install generic exception hander */
    set_handler(0x180, except_generic_handler, 0x80);

}


Status.BEV控制了异常入口的基地址,清除Status.BEV,让异常入口基地址切换到EBASE,这样在异常产生时,就不会从FLASH上取数据了。Cause.IV则是控制中断的入口偏移,因为我们中断及异常的处理例程是同一个,我们将其清除,使中断入口与通用异常入口一致,这样就不需要分开处理。except_generic_handler是我们的异常处理函数,set_handler的工作就是将函数代码拷贝至异常向量地址并刷新cache。


except_generic_handler的实现很简单,如下:


NESTED(except_generic_handler, 0, sp)

    .set push

    .set noreorder

    .set noat

    la  k1, handle_exception

    jr  k1

    nop

    .set pop

END(except_generic_handler)


前面提到过异常向量的大小限制,因此这里我们直接跳转到handle_exception,这才是我们真正的异常处理程序。


NESTED(handle_exception, PT_SIZE, sp)

    move k1, sp

    addi sp, sp, -PT_SIZE

    SAVE_ALL

    move a0, sp

    la  ra, ret_from_exception

    la  t9, do_exception

    jr t9

    nop

END(handle_exception)


前面提到u-boot有其自身的特点,它需要一个不使用的寄存器来作为引用重要数据的指针,也就是gd。在MIPS架构下,u-boot使用k0作为gd,这也表明,在u-boot下的中断处理里,我们只有一个免费寄存器k1可以使用。少一个可用的寄存器是个麻烦事,在异常处理里,不能在汇编阶段做更多的事,好在我们还足以用它来保存上下文。我们先将堆栈指针赋给k1,然后预留一段栈空间,大小为 PT_SIZE。预留栈的大小取决于需要保存的寄存器数目,以及每个寄存器占用的空间。


SAVE_ALL 是一个宏,用于将寄存器保存至堆栈。MIPS使用a0-a3传递参数,将sp传给a0,使C函数do_exception可以从入参取得已保存的栈数据,方便打印异常信息。设置返回值寄存器ra,在do_exception函数返回时,可以使代码从ret_from_exception继续执行。


一切就绪,就该进入C函数了,这由两条指令实现:加载函数地址到t9,以t9作为跳转寄存器执行跳转(以t9作为跳转寄存器是MIPS ABI的约定)。为什么需要经过寄存器跳转呢?这是由于u-boot使用了位置无关代码(PIC)进行编译。理解位置无关代码对我们的汇编实现非常重要,我们后面将以代码为例对它作一个分析。


我们实现的是一个定时器中断,它涉及两个寄存器:Count和Compare。COP0 Count寄存器以CPU核时钟频率计数,当与Compare寄存器相等时,触发一个Timer中断。在do_exception异常处理函数里,我们读出当前的Count值,加上一个将频率换算为周期的数值,保存到Compare寄存器中,写Compare寄存器将清除Timer中断,中断处理就至此结束了,而Count将持续增长,直到与我们设置的Compare值相等时再次产生中断:


void do_exception(struct pt_mips_regs *regs)

{

    uint32_t count;

    count = read_c0_count();

    count += FREQ/1000;

    write_c0_compare(count);

    counter++;

    return;

}


作为中断流程的实验,do_exception目前只处理一种情况,即只处理定时器中断,后面我们可以继续完善它,现在是该返回的时候了。


由于在handle_exception中,我们将ra设置为ret_from_exception,因此在do_exception函数返回时,将从ret_from_exception继续执行。


FEXPORT(ret_from_exception)

    RESTORE_ALL

    eret

    nop


同样,RESTORE_ALL是一个宏,用于从堆栈恢复上下文,在所有寄存器恢复后,调用eret指令退出异常状态。eret会清除 Status.EXL,在中断处理过程中,并未对中断使能位进行修改,因此EXL的清除将使能所有的中断,同时eret会跳转至EPC寄存器保存的地址(在发生Reset, Soft Reset, NMI及Cache Error异常时,eret使用ErrorEPC的地址,请参考MIPS手册)。


使用-fpic选项,可以指示gcc以位置无关的方式编译代码。这对于C语言编写来说通常是不可见的,编译器隐藏了所有的细节,然而在编写汇编代码时,我们必须要了解它,否则就无法理解变量的引用及函数的跳转。


经过PIC编译的代码,对外部符号的引用都是根据pc相对地址的寻址方式,这使得u-boot代码在relocation到任意地址后都可以正确运行,因为相对地址早在编译期间就被编译器所确定了。通过反汇编do_exception,我们可以看到这个过程。


bfc09c30 <do_exception>:

bfc09c30: 3c1c0006  lui     gp,0x6

bfc09c34: 279c1c10  addiu   gp,gp,7184

bfc09c38: 0399e021  addu    gp,gp,t9

bfc09c3c: 40034800  mfc0    v1,$9

bfc09c40: 3c02000e  lui     v0,0xe

bfc09c44: 34427ef0  ori     v0,v0,0x7ef0

bfc09c48: 00621821  addu    v1,v1,v0

bfc09c4c: 40835800  mtc0    v1,$11

bfc09c50: 8f83859c  lw      v1,-31332(gp)

bfc09c54: 8c620000  lw      v0,0(v1)

bfc09c58: 24420001  addiu   v0,v0,1

bfc09c5c: 03e00008  jr      ra

bfc09c60: ac620000  sw      v0,0(v1)


C代码中的counter是一个全局变量,对比反汇编代码,我们可以看到counter变量的地址是通过gp加偏 移量确定的。首先gp被赋为0x61c10,然后与t9相加,也即与do_exception的地址0xbfc09c30相加,得到0xbfc6b840,再减去-31332得到0xbfc63ddc,这就是counter符号在GOT表中的地址,我们可以用readelf –x.got u-boot打印出GOT表中的内容,同时用nm查得counter的实际地址进行对照:


GOT: 0xbfc63dd0 bfd3ea80 bfc65418 bfc1ec68 bfc65400

NM:  bfc65400 B counter


可见,0xbfc63ddc中存放的正是counter的地址。


一切看起来都那么顺利,不是吗?遗憾的是,现实总是那么地残酷。在每秒钟产生1~5个中断时,还比较稳定,一旦超过10Hz,单板比较容易挂死,而设为500Hz~1000Hz时,简直就是必死无疑。单板跑死时,串口没有打印,CPU目前处于一个什么状态,不得而知,只能添加少量打印来判断,在中断上下文打印,有两个限制:


* 中断频率也不能设置过快,否则就是满屏的打印。

* 打印的位置不是随意的,因为打印需要用到寄存器,在寄存器未被保存之前是不能使用的。


因此通过打印是不太可行的,经过分析后,认为CPU应该是跑飞了。影响pc的有哪些?除了epc外,很重要的就是gp,因为前面我们也提到过,gp用于索引GOT表,有了这个明确的提示,脑海中突然浮现出中断入口函数的代码实现,这个可能有问题!我们再回头看看这两个指令:


la  k1, handle_exception

jr  k1


话不多说,直接看反汇编:


bfc0a2ac:    8f9b832c    lw k1,-31956(gp)

bfc0a2b0:    03600008    jr k1


一切都恍然大悟。我们在取handle_exception的地址时,已经默认地使用了gp寄存器的值,而如之前的反汇编所见,在pic代码中,所有的符号地址,都是通过gp计算的,那么在中断进入时,gp的值是不确定的。假设我们刚进入一个函数,gp已被修改,突然中断来临,中断入口取出的 handle_exception的地址就肯定是错误的,必然会跑飞。


知道了问题所在,是一个大的突破,下面的问题就是如何设置正确的gp,并且不能破坏被中断代码的gp。从u-boot的链接脚本,我们可以看到_gp被设计为指向GOT表:


_gp = ALIGN(16) +0x7ff0;

.got  : {

__got_start = .;

*(.got)

__got_end = .;

}


而在start.S的最初代码里,进行第一个函数调用前,将gp设置成了GOT表的首地址:


    bal     1f

    nop

    .word   _gp

1:

    lw      gp, 0(ra)


在relocation时,gp与GOT表也会被修改,他们的相对地址仍然保持不变。如果我们在relocation后,保存gp的值,是否可以留给中断处理程序使用呢?答案是肯定的,不过需要想办法把gp保存在某个地方。很自然地,想到了k0。k0指向gd_t数据,一旦设置后,在整个boot阶段都不会发生改变,我们可以在gd_t中增加一个成员来保存gp值,为了方便索引,就把它作为第一个成员吧:


typedef struct  global_data {

    unsigned long   gp;  /* saved gp */

    bd_t            *bd;

    unsigned long   flags;

    ...



board_init_r是u-boot在relocation后执行的第一个函数,我们在这里保存重定向后的gp寄存器。


asm __volatile__("move %0, $28\n":"=r"(gd->gp));



异常向量入口except_generic_handler修改如下:


/* trade gp */

lw k1, 0(k0)

sw gp, 0(k0)

move gp, k1

la  k1, handle_exception

jr  k1

nop



先将gd->gp的值读出到k1,然后将被中断的代码的旧gp保存到gd->gp中,将GOT表基地址赋给gp指针,此时gp已经是确定的值了,对handle_exception的地址解析也不会再出错了。


在ret_from_exception中,还需要做相同的工作,将旧的gp恢复,GOT表基址保存回gd->gp中去。至此,中断处理程序已经可以稳定正常工作了,目前在5MHz的中断频率下,也可以稳定工作并正常下载版本。


中断处理已经跑通,我们可以继续完善异常处理了。由于我们在汇编环境下只有一个k1寄存器可以使用,因此判断异常类型的工作就交到了do_exception中。入参regs是在handle_exception传入的,它是预留了栈后的sp指针,我们以图说明。


 +-------------------------+

 |                         |

 |  Stack before exception |

 |                         |

 +-------------------------+    <---------- old sp

 |      cp0 registers      |  \

 +-------------------------+   |

 |           ...           |   |

 +-------------------------+   |

 |           $31           |   |

 +-------------------------+   |  PT_SIZE

 |            $2           |   |

 +-------------------------+   |

 |            $1           |   |

 +-------------------------+   |

 |            $0           |  /

 +-------------------------+    <---------- new sp

 |                         |

 |  Stack for do_exception |

 |                         |

 +-------------------------+



old sp是中断产生时的栈指针,new sp是我们开辟了PT_SIZE大小的栈空间后,调整后的栈指针,而PT_SIZE所包含的栈空间则是用于保存我们的寄存器,new sp作为入参传入do_exception。


struct pt_mips_regs是一个结构体,只要我们将此结构体的定义与栈结构相同,就可以直接利用编译器产生的偏移取用栈数据。最终的处理框架如下:是中断产生时的栈指针,new sp是我们开辟了PT_SIZE大小的栈空间后,调整后的栈指针,而PT_SIZE所包含的栈空间则是用于保存我们的寄存器,new sp作为入参传入do_exception:


void do_exception(struct pt_mips_regs *regs)

{

    unsigned int excode;

    unsigned int int_mask;

    unsigned int status;

    int irq;

    status = regs->c0_status;

    excode = (regs->c0_cause >> 2) & 0x1f;

    int_mask = (status >> 8) & 0xff;

    switch(excode)

    {

        case 0:

            irq = 7;

            while(int_mask != 0)

            {

                if((int_mask & (1<<irq)) != 0)

                {

                    do_int(irq);

                }

                int_mask &= ~(1<<irq);

                --irq;

            }

            break;

        default:

            /* unhandled exception goes to default,

             * if you can handle certain exceptions,

             * process it above.

             */

            printf("\n\nUnable to handle %s exception:\n\n", exc_desc[excode]);

            dump_stackframe(regs);

            printf("\n\nAttempting to reboot ...\n");

            _machine_restart();

            while(1);

    }

       

    return;

}



当异常码为0时,我们进入中断处理,这由do_int完成。当异常码为其它值时,打印出异常信息,这由dump_stackframe实现,它将 struct pt_mips_regs结构中的数值全都打印出来,类似于内核的panic打印。值得一提的是u-boot在重定向后,如果产生异常,它的epc是重定向后的RAM地址,而通过反汇编u-boot得到的是ROM地址,这给精确定位带来不便,好在问题的解决比较容易,u-boot在重定向后,会将偏移保存在gd->reloc_off中。


gd->reloc_off = dest_addr - CFG_MONITOR_BASE;



在出现异常时,可以利用该值对epc进行换算,得到异常地址对应的ROM地址,只需要简单地用epc减去gd->reloc_off就可以了。由于 CFG_MONITOR_BASE一般是0xbfc00000,而u-boot重定向后一般运行在K0段,这样得到的reloc_off是一个补码,只要统一按照等位宽的无符号数处理,则无需关心这个细节。


是否需要在bootloader中实现中断,一直是一个有争议的问题,大多数人认为bootloader的功能应当最简单,它的任务就是尽可能早地启动操作系统,也有不少人因为实际应用需要,希望在boot里实现更多的功能,这是个哲学问题,这里不打算深入讨论。从应用来看,有时需要实现MIPS下的中断,以解决一些实际问题。目前还没有开源boot支持MIPS下的中断,因此本文的工作是一个新的尝试,相信对类似应用会有很好的指导作用。


关于MIPS架构的知识,请参考架构手册

* MIPS64 Architecture for Programmers Volume II - The MIPS64 Instruction Set v2.50

* MIPS64 Architecture for Programmers Volume III - The MIPS64 Privileged Resource Architecture v2.50


评论

此博客中的热门博文

反转剧

这两天明显感到天气转冷,呱呱的家里也已经下起了大雪,南京则是阴冷潮湿,让人没有了出行的欲望。没想到躲在被子里看反转剧也成了度过寒冬的一剂良药。在PPLive越来越让人失望的时候,PPStream横空出世,虽然广告仍是少不了的主题,但从视频质量和播放连续性上来说都超过PPLive,实为居家必备之良品(由此可见,新事物一定会战胜旧事物......)。韩国的反转剧最近似乎比较流行,称之为反转剧就在于其结果总是让人出乎意料,不合常理,其间又不乏各种搞怪搞笑的镜头,各种当红帅哥美女也一定让DDMM们爱不释手,20~30分钟一集的剧情一改韩剧拖沓的风貌,想看就看,容易切入。 反转剧,今天你看了吗?

from cpp to java

when i start to study java with a cpp background, i find it is very difficult to convert my mind. i always think how some features in cpp was implemented in java, this give me a little reject to java language. though java is born from c, i think they still have different applicable domains, so try to study both is good for me, and java is a pure OO language, i believe it will give me a better understanding on OOD.

Personalized Google Home

Using your google account, you can create a personalized homepage. You can customize several modules, including various news, gmail, bookmarks. This service is similar to what http://www.netvibes.com/ provides. The drawback is the inconvenience of bookmark module. It doesn't provide grouping while netvibes provide tags to implement this. Wishing google will improve this defect in the near future. Meanwhile, the Live plan of Microsoft is also going on. MS presents several several servicies to counterattack google's counterparts. Virtual Earth (see http://preview.local.live.com ) is a recent service. It provides map service just like google earth, provides a virtual tour better than google. Imaging driving a car in the map and seeing everything real, maybe you can do a virtual europe tour now. Seems interesting. MS also provides a customized home, you can log into www.live.com via your msn accounts. I haven't tried that yet.