跳至主要内容

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分钟一集的剧情一改韩剧拖沓的风貌,想看就看,容易切入。 反转剧,今天你看了吗?

如何突破自己的界限(ZZ)

大家都听说过温水煮青蛙的故事,也知道生于忧患死于安乐的道理,但是我们在生活中总会自然而然的根据自己的好恶养成一些习惯,在习惯的环境中,会感 觉很舒适,同时,这些习惯会开始渐渐在我们周围建起一座壁垒,哪怕这些习惯本身并没有什么错误,它们会慢慢局限我们的想法和决定,让我们失去很多应该获得 的机会,难以突破自己。 为什么我们常常会觉得工作和生活越来越沉闷,归根结底,是因为我们选择了这样的生活。 我们回避犯错误回避冒险。 不会犯错误的人永远不会创造出新的价值。 如果你想打破这种沉闷,让生活重新充满活力,下面有一些方法: 了解自己各种积习的真相。这些习惯往往曾经为你带来过成功,下一次你仍然会那么做。想要有所突破,必须放弃这些习惯,尝试新的思路模式和行动方法。除此之外别无他法,这些积习已经在阻碍你发现新鲜事物,让你停滞不前。 用不同的方法做事,看看效果。世界每一天都在发生变化,哪怕曾经让你无比成功的习惯,也不可能适应各种情况。 尝试用不同的方法做事,不要害怕失败,不要成为习惯的傀儡。 破除给自己强加的条条框框, 重新审视自己 ,给自己时间。积习难改,首先我们可能没有意识到这些习惯的存在,其次,在调整自我的过程中,这些习惯会不断的反扑,看看有多少人戒烟成功就知道了。 做你自己。很多时候我们不是在做自己,而是在做别人眼中的自己。大多数人都会有取悦别人的想法,希望达到别人的期望,哪怕是让自己变成自己都不愿 意成为的人。 你首先应该做好自己。 每个人都是不同的, 你应该把自己放在第一位,因为没有人会比你自己更在乎你。 把别人放在第二位是说给别人足够的尊重就可 以,不是让你完全忽略他们。 总是活在别人的阴影里是一种负担。 跳出这个圈子,给自己一个成为自己的机会。 放慢脚步,随遇而安。很多人都希望自己是一个充满智慧、优秀、受人爱戴的人。有时你的确是,有时却不一定。现实太复杂了,我们需要不断从别人那里 获取信息和支持来完成任何事情。我们所拥有的东西和知识都来自于别人。有时候,通过这些外界的帮助,我们可以做的更好。但有时却无法很好的利用。所以,要 了解自己是一个思想和感觉的复杂结合体,这些思想和感觉有时候很有用,有时候不。不需要给自己戴一个面具,也 不需要伪装自己什么都行,更没有必要因为发现自己的弱点而感到恐惧。 如果你能真诚的面对自己,会获得一种解脱。可能你不如自己想像得那么伟大,但也不

天将降大任于斯人也,必先折其体肤,洗其脑袋

元宵节就这么被一个半导体扼杀了。一趟大巴把我们拉到了汤山,我在那看到了keywords:温泉、洗浴、桑拿...可是没看到司机踩刹车... 当晚便开始训练,站在操场上,看着万家烟火,寒风凛冽,想着可怜的呱呱在家一个人吃汤圆,老泪纵横。Goodbye me lover, goodbye my friend. ——James Blunt语 当天正是降温的时候,第二天,天空居然飘起了小雪点,毛衣没带,冻得发抖。下午心动过速一小时,顶着,心里在想,如果倒下了,呱呱会怎样。接下来两天气温有些回升,训练也没有那么累了,Thank God。只是冬天的太阳狠,真狠,真是狠,真他妈狠,Yeah!脸皮痛,回到南京遂开始脱皮。一觉睡到大天亮,开始为期十二天的洗脑。生活颇为FB,行动却尤为不便,拜超级BT的领导所赐。好在呱呱也将有长达半月的培训,总算不会担心她会饿死,大饼的故事? 杀!!! ——顺带记念30年的第一次军训