-
MIPS CPU
体系结构概述
/mips/
陈怀临
1
。序言
本文介绍
MIPS
< br>体系结构,着重于其寄存器约定,
MMU
及存储管理,异
常和中断处理等等。
通过本文,希望能提供一个基本的轮廓概念给对
MIPS CP
U
及之上
OS
有兴趣的读者。
并能开始阅读更详细的归约
(SPECIFICATION)
资料。
MIPS
是最早的,最成功的
RISC(Reduced Instruction Set
Computer)
处理
器之一,起源于
Stanford
Univ
的电机系
.
其创始人
John L. Hennessy
在
1984
年在硅谷创立
了
MIPS INC.
公司
()
。
John L.
Hennessy
目前是
Stanford Univ.
的校长。在此
之前,他是
Stanf
ord
电子工程学院的
Dean
。
p>
CS
专业的学生都知道两本著名的书:
“
Computer Organization and
Design : The Hardware/Software
Interface
”
和
”
Computer Architecture : A
Quantitative Approach
“。其
Co-
author
就是
Hennessy.
MIPS
的名字为“
Microcomputer without interlocked pipeline sta
ges
的缩写。另外一个通
常的非正式的说法是”
Millions of instructions per
second
MIPS
芯片在工业界目前用的比较多的是:
MIPS
INC
。的
R10000
;<
/p>
QED(
。
1996
年从
MIPS
INC
。分
(SPIN OFF)
出来
的
)
的
R5000
,
R7000
等。
指令集
详细的资料请参阅
MIPS
归约。
一般而言,
MIPS
指令系统有:
MIPS
I
;
MIPS
II
;
MIPS
III
和
MIPS IV
。可想而知,指令系
统是
向后兼容的。例如,基于
MIPS
II
的代码可以在
MIP
III
和
MIPS
IV
的处理器上跑一跑:
-)
<
/p>
下面是当我们用
gcc
时,如何指定指令
和
CPU
的选项。
-mcpu=cpu type
Assume the defaults for the machine
type cpu type when schedulin
g
instructions. The
choices for cpu type
are `r2000', `r3000', `r4000', `r4400', `r4600',
and `r6000'. While picking a specific
cpu type will schedule things appropriately for
that particular chip, the compiler will
not generate any code that does
not
meet level 1 of the MIPS ISA (instruction set
architecture) without the `
-mips2'
or `-mips3' switches being used.
-mips1
Issue instructions
from level 1 of the MIPS ISA. This is the default.
`r3000' is the
default cpu type at this
ISA level.
-mips2
Issue
instructions
from
level
2
of
the
MIPS
ISA
(branch
likely,
square
root
instructions).
`r6000' is
the default cpu type at this ISA level.
-mips3
Issue instructions
from level 3 of the MIPS ISA (64 bit
instructions). `r4000' is the
default
cpu type
at this ISA level.
This option does not change the sizes of any of
the
C data types.
< br>读者可能发现,
对于大多数而言,
我们应该是用
MIPS
III
或
-
mips3
。
要提醒的是
R5000<
/p>
和
R10000
也都是
< br>R4000
的延伸产品。
下面是几点补充:
*MIPS
指令是
32
位长
,即使在
64
位的
CPU
上。这对于局部跳转指令的理解很有帮助。
比如:
J
(TARGET)
;
JAL
(TARGET)
。
J
和
JAL
的
OPERCODE
是
6
位,剩下的
26<
/p>
为存放跳转偏移量。
由于任何一个指令都是
32
位
(
或
4
字节
)
对齐
(ALIGN)
的,所以
J
和
JAL
最大的伸缩空间是
2^28=256M
。如果你的程序要作超过
256M
的跳
转,你就必须用
JALR
或
JR
,通过一个
GPR
寄存器
来存放你的跳转地址。由于一个寄存器是
32
或
64
位的,你就没有任何限制了。
*MIPS
CPU
的
SR(STATUS REGISTER)
中有几位是很重要的设置,当我们选择指令系统或要用
64
位的
MIPS
的
CPU
p>
CORE
在
32
模
式下
(
绝大多数情况,
弟兄们
别告诉我你在写
64
位的程序:
--)
)
。
SR[XX]
:
1
:
MIPS IV
INSTRUCTION SET USABLE
0
:
MIPS IV
INSTRUCTION SET UNUSABLE
SR[KX]
SR[SX]
SR[UX]
:
< br>0
:
CPU
工作在
32
位模式下
1
:
CPU
工作在
64
位模式下
一般而言,如果你要从头写一个
MIPS
核心为
32
位程序,最好把上述值设为
0
< br>。为什么最好呢?
因为我在工作中没有去冒风险,设她们为
1
,
who knows what would
happen?:-) And then why
bother:--)?
*
在以后我们会单独的一章讲将流水
线和指令系统,特别是跳转指令的关系。在这里,我们只简
单提一下。
< br>对任何一个跳传指令后面,
FOR
SIMPLITY<
/p>
,
要加上一个空转指令
(NOP)
。
从而使得
CPU
的
PIPELINE
不会错误的执行一个预取
(PRE_FETCH)
得指令。
当然这个
NOP
可以替换为别的。
以后
再讲。放一个
NOP
是最简单和安全的。有兴趣的读者可以用<
/p>
mips64-elf-objdump -d
来反汇编
一个
OBJECT
文件。你就会一目了然了。
*
一定要记住:
MIPS
I
,
II
,
III
和
IV
指令系统不包含
PRIVILEDGED
INSTRUCTIONS
。
换句话说,都是那些在
USER MODE
下可以用的指令
(
当然
KERNE
L
下也能用
)
。对于
< br>CPO
的操作不
属于指令系统。
*
有一点在
MIPS
CPU
下,
要千万注意:
AL
IGN
。
MIPS
对
< br>ALIGN
的要求是严厉的。
这一点与
< br>POWERPC
是天壤之别。指令必须是
32
位对齐。数据类型必须在她们的的
大小边界对齐。简单的比如:
When CPU
running under 32bit mode, int must 32bit aligned;
long 32bit aligned; pointer must be
32bit aligned; char must 8 bit aligned. long long
must 64 bit
aligned;
关于这一点
,我是
吃过苦头的。当然我知道大家还会犯错在这里:
--)
,
即使知道了。有些事情学是没用的:
--)
。
一定要注意。
*
我建议读者阅读
SPECIFICATION
时要花时
间看一看指令系统的定义。其实不难。每一种指令不
外乎几个域
(FIELDS)
。
寄存器约定
对于在一个
CPU
上进行开发,掌握其工作的
CPU
的寄存器约定是非常重要的。
MIPS
体系结构提供了
32
个
GPR(GENERAL PURPOSE
REGISTER)
。这
32
个寄存器
的用法大致如下:
REGISTER NAME USAGE
$$0
$$zero
常量
0(constant value 0)
$$2-$$3 $$v0-$$v1
函数调用返回值
(values for results
and expression evaluation)
$$4-$$7
$$a0-$$a3
函数调用参数
(arguments)
$$8-$$15 $$t0-$$t7
暂
时的
(
或随便用的
)
$$16-$$23 $$s0-$$s7
保存的
(
或如果用,需要
SAVE/RESTORE
的
)(saved)
$$24-$$25
$$t8-$$t9
暂时的
(
或随便用的
)
$$28 $$gp
全局指针
(Global Pointer)
$$29 $$sp
堆栈指针
(Stack Pointer)
$$30 $$fp
帧指针
(Frame
Pointer)
(BNN
:
fp
is stale acutally, and can be simply used as $$t8)
$$31 $$ra
返回地址
(return address)
对一个
CPU
的寄存器约定的正确用法是非常重要的。当然对
C
语言开发者
不需要关心,因为
COMPILER
会
TAKE CARE
。但对于
KERNEL
的开发或
DRIVER
开发的人就
**
必须
**
清楚。
< br>
一般来讲,你通过
objdump
-d
可以清醒的看到寄存器的用法。
下面通过我刚才写的一个简单例子来讲解:
~/ vi Hello.c
/* Example to illustrate
mips register convention
* -Author:
BNN
* 11/29/2001
*/
int addFunc(int,int);
int subFunc(int);
void main()
{
int x,y,z;
x= 1;
y=2;
z = addFunc(x,y);
}
int addFunc(int x,int y)
{
int value1 = 5;
int value2;
value2 = subFunc(value1);
return (x+y+value2);
}
int
subFunc(int value)
{
return value--;
}
上面是一个
C
程序,
main()
函数调用一个加法的子函数。
让我们来看看编译器是如何产生代码的。
~/bnn:74> /bin/mips-elf-gcc -c Hello.o
Hello.c -mips3 -mcpu=r4000 -mgp32 -mfp32 -O1
~/bnn:75> /bin/mips64-elf-
objdump -d Hello.o
Hello.o: file
format elf32-bigmips
Disassembly of
section .text:
/* main
Function */
0000 :
/*create a stack frame by moving the
stack pointer 8
*bytes down and
meantime update the sp value
*/
0: 27bdfff8 addiu $$sp,$$sp,-8
/* Save the return address to the
current sp position.*/
4: afbf0000 sw
$$ra,0($$sp)
8: 0c000000 jal 0
/* nop is for the delay slot */
c: 00000000 nop
/* Fill the
argument a0 with the value 1 */
10:
24040001 li $$a0,1
/* Jump the addFunc
*/
14: 0c00000a jal 28
/*
NOTE HERE: Why we fill the second argument
*behind the addFunc function call?
* This is all about the
*
With mips architecture, the instruciton after jump
* will also be fetched into the pipline
and get
* exectuted. Therefore, we can
promise that the
* second argument
will be filled with the value of
*
integer 2.
*/
18: 24050002
li $$a1,2
/*Load the return address
from the stack pointer
* Note here
that the result v0 contains the result of
* addFunc function call
*/
1c: 8fbf0000 lw $$ra,0($$sp)
/* Return */
20: 03e00008
jr $$ra
/* Restore the stack frame */
24: 27bd0008 addiu $$sp,$$sp,8
/* addFunc Function */
0028 :
/* Create a stack
frame by allocating 16 bytes or 4
*
words size
*/
28: 27bdfff0
addiu $$sp,$$sp,-16
/* Save the return
address into the stack with 8 bytes
*
offset. Please note that compiler does not save
the
* ra to 0($$sp).
*Think
of why, in contrast of the previous PowerPC
* EABI convention
*/
2c: afbf0008 sw $$ra,8($$sp)
/* We save the s1 reg. value into the
stack
* because we will use s1 in this
function
* Note that the 4,5,6,7($$sp)
positions will then
* be occupied by
this 32 bits size register
*/
30: afb10004 sw $$s1,4($$sp)
/* Withe same reason, save s0 reg. */
34: afb00000 sw $$s0,0($$sp)
/* Retrieve the argument 0 into s0 reg.
*/
38: 0080802d move $$s0,$$a0
/* Retrieve the argument 1 into s1 reg.
*/
3c: 00a0882d move $$s1,$$a1
/* Call the subFunc with a0 with 5 */
40: 0c000019 jal 64
/* In
the delay slot, we load the 5 into argument a0 reg
*for subFunc call.
*/
44: 24040005 li $$a0,5
/* s0
= s0+s1; note that s0 and s1 holds the values of
* x,y, respectively
*/
48: 02118021 addu $$s0,$$s0,$$s1
/* v0 = s0+v0; v0 holds the return
results of subFunc
*call; And we let
v0 hold the final results
*/
4c: 02021021 addu $$v0,$$s0,$$v0
/*Retrieve the ra value from stack */
50: 8fbf0008 lw $$ra,8($$sp)
/*!!!!restore the s1 reg. value */
54: 8fb10004 lw $$s1,4($$sp)
/*!!!! restore the s0 reg. value */
58: 8fb00000 lw $$s0,0($$sp)
/* Return back to main func */
5c: 03e00008 jr $$ra
/*
Update/restore the stack pointer/frame */
60: 27bd0010 addiu $$sp,$$sp,16
/* subFunc Function */
0064 :
/* return back to
addFunc function */
64: 03e00008 jr
$$ra
/* Taking advantage of the mips
delay slot, filling the
* result reg
v0 by simply assigning the v0 as the value
*of a0. This is a bug from my c source
* codes--
* like
68: 0080102d move $$v0,$$a0
希望大家静下心来把上面的代码看懂。
一定要注意编译器为什么在使用
s0
和
s1
之前要先把她们
SAVE
起来,
然后再
RESTORE
,虽然在这个例子中虽然
main
函数没用
s0
和<
/p>
s1
。
另外的一点是:由于我们加了“
-O
1
”优化,编译器利用了“
delay
slot
来执行那些必须执行的
指令,而不是简单的塞一个”
nop
指令在那里。非常的漂亮。
最后,考大家一个问题,为了使得大家更加理解寄存器的用法:
*
在写一个核心调度
context
switch()
例程时,我们需要
S
AVE/RESTORE$$t0-$$t7
吗?如果不,为
什么?
*
在写
一个时钟中断处理例程时,我们需要
SAVE/RESTORE$$t0-$$t7
吗?如果是,为什么?
MMU
和
Memory
Management
对于
MI
PS
的
MMU
和
Memory Management, the first and yet important
one we need
always
keep in
mind is: No real-mode
没有实模式。这一点是
MIPS CPU
的一个很重要的特点
(
或缺点
)<
/p>
。
我们会
问了:
BNN
,
Give me a
break. Without CPU running in the real-mode,
how could you boot up a kernel? Well,
here is the thing:
Bydefault, MIPS architecture , when
power on, has enabled/mapped two memory
areas. In other words, those two memory
areas are the places where your
boot
codes HAVE TO resident and run on top of. If you
read the makefiles
of MIPS linux
source tree, you would easily find the infor. For
example,
0x8000xxxx or some things
like that.
* MIPS
存储体系结构
< br>我们在这里不谈
64
位
CPU<
/p>
,只谈
32
位的。
MIPS
将存储空间划分为<
/p>
4
大块
--kuseg,
kseg0,kseg1 and kseg2.
---------------
--------------------------------------------------
-
0xFFFF FFFF
mapped kseg2
0xC000 0000
unmapped uncached kseg1
0xA000 0000
unmapped cached
kseg0
0x8000 0000
2G kuseg
0x0000 0000
---------------
--------------------------------------------------
-
对于上述图表,弟兄们要记住以下几点:
*
当开电
(Power
On)
的时候,只有
kseg0 and kseg1
是可以存取的。
*kseg0 512M(From 0x8000 0000 to 0xA000
0000) are DIRECTLY mapped to phyiscal
memory ranging from 0x0000 0000 to
0x2000 0000, with cache-able(either write
back or write through, which is decided
by SR(Status Register of MIPS CPU)
*kseg1 512M(From 0xA000
0000 to 0xC000 0000) are (also) DIRECTLy mapped
to physical memory ranging from 0x0000
0000 t0 0x2000 0000, with non-cachable.
以上两点对于理解
MIPS OS
的启
动是至关重要的。细心的读者会发现:
kseg1
有点象
其他
CPU<
/p>
的
real-
mode
方式。
*(
虚拟
)
地址
from 0x0000 0000 to 0x8000 0000
是不可以存取的,在加电时
(POWER
ON)
!必须等到
MMU
TLB
初始化之后才可以。
*
同理对地址
from 0xC000
0000 to 0xFFFF 0000
*MIPS
的
CPU
运行
有
3
个态
--User Mode;
Supervisor Mode and Kernel Mode.
For
simplicity, let's just talk about User Mode and
Kernel Mode. Please
always keep this
in mind:
CPU can ONLY
access kuseg memory area when running in User Mode
CPU MUST be in kernel mode or
supervisor mode when visiting kseg0, kseg1
and kseg2 memory area.
* MMU TLB
MIPS
CPU
通过
TLB
来
translates all virtual
addresses generated by the
CPU.
对
于这一点,这里不多废话。
下面谈谈
ASID(Address Space
Identifier). Basically, ASID, plus the VA(Virtual
Address) are composed of the primary
key of an TLB entry.
换句话说,虚拟
地址本身是不能唯一
确定一个
TLB entry
的。一般
而言,
ASID
的值就是相应的
pro
cess ID.
Note that ASID
can minimized TLB re-loads, since several TLB
entries can
have the same virtual page
number, but different ASID's.
对于一个多任务操
作系统来讲,每个任务都有
自己的
4G
虚拟空间,但是有自己的
ASID
。
MMU
控制寄存器
对于一个
Kernel Engineer
来说,对
MMU
的处理主要是通过
MMU
的一些控制寄存器来完成的。
MIPS
体系结构中集成了一个叫做
System Control Coprocessor (CP0)
的部件。
CP0
就是我们常
说
的
MMU
控制器。在
CP0
中,除了
TLB entry(
例如,对
RM5200
,有
48pair,96
个
TLB
entry),
一些控制寄存器提供给
OS KERNEL
p>
来控制
MMU
的行为。
每个
CP0
控制寄存器都对应一个唯一的寄存器号。
MIPS
提供
特殊的指令来对
CP0
进行操作。
mfc0 reg. CP0_REG
mtc0 reg. CP0_REG
< br>我们通过上述的两条指令来把一个
GPR
寄存器的值
p>
assign
给一个
CP0
寄存器,
从而达到控制
MMU
的目的。
下面简单介绍几个与
TLB
相关的
CP0
控制寄存器。
Index Register
这个寄存器是用来指定
TLB
ent
ry
的,
当你进行
TLB
读写的时候。我们已经知道,例如,
MIPS
R5
提供了
48
个
TLB
pair
,所以
index<
/p>
寄存器的值是从
0
到
47
。换句话说,每次
TLB
写的
行为是对
一个
pair
发生的。这一点
是与其他的
CPU MMU TLB
读写不同的。
EntryLo0, EntryLo1
这两个寄存器是用来
specify
一个
TLB pair
的偶
(even)
和奇
(odd)
物理
(Physical)
页面地址。
一定要注意的是:
EntryLo0 is used for
even pages; EntryLo1 is used for odd pages.
Otherwise, the MMU will get exception
fault.
Entry Hi
Entry Hi
寄存器存放
VPN2
,或一个
TLB
的虚拟地址部分。注意的是:
ASID
value
也是在这里被
体现。
Page Mask
MIPS
TLB
提供可变大小的
p>
TLB
地址映射。
一个
PAGE
可以是
4K
,
16K
,
64K
,
256K
,
1M
,<
/p>
4M
或
16M
。
这种可变
PAGE
SIZE
提供了很好的灵活性,
特别是对
Embedde
d
System
Software.
对于
Embedded
System
Softare,
一个很大的区别就是:不允许大量的
Page
Fault.
这一点是传统
OS
或
General
OS
在
Embedded
OS
上的致命缺陷。也是为什么
POSIX 1
。
B
的目的
所在。传统
OS
存储管理的一个原则就是:
Page
On Demand.
这对大多
Embedded
System
是不允许
的。
For
embedded
system,
往往是需要在系统初始化的时刻就对所有的
存储进行
configuration
,
以确保在系统运行时不会有
Page Fault.
上述几个寄存器除了
MAP
一个虚拟页面之外,还包括设置一个页面的属性。其中包括:
writable or not; invalide or not; cache
write back or write through
下面简单谈谈
MIPS
的
JTLB
。
在<
/p>
MIPS
中,如
R5000
,
JTLB is provided. JTLB stands
for Joint TLB.
什么意思呢?就是
TLB
buffer
中包含的
mixed Instruction
and Data
TLB
映射。有的
CPU
的
Instruction TLB
和
Data TLB buffer
是分开的。
当然
MIPS(R5000)
确实还有两个小的,分开的<
/p>
Instruction
TLB
和
Data
< br>TLB
。但其大小很小。主
要是为了
Performance,
而且是对系统软件透明的。
在这里再谈谈
MMU
TLB
和
CPU Level 1
Cache
的关系。
我们知道,
MIPS
,或大多数
< br>CPU
,的
Level
1 <
/p>
Cache
都是采用
Virtually
Indexed
and
Physicall
tagged.
通过这个机制,
OS
就不需要在每次进程切换的时候去
flush CACHE
。为什么呢?
举一个例子吧:
进程
A
的一
个虚拟地址
Addr1
,其对应的物理地址是
< br>addre1
;
进程
B
的一个虚拟地址
Addr1
,其对应的物理地址是
addre2;
在某个时刻,进程
A
在运行中,并且
Addr1
在
Level 1
CACHE
中。
这时候,
OS does a context swith
and bring process B up, having process A sleep.
Now, let's
assume that the
first
instruction/data fetch process B does is to access
its
own virtual address Addr1.
这时候
CPU
会错误的把进程
A
在
Level 1
中的
Addr1
的
addr1
返回给
CPU
吗?
p>
我们的回答应该是:不会的。
原因是:
当进程切换时,
OS
会将进程
B
的
p>
ASID
或
PID
填入
ASID
寄存器中。
请记住:
p>
对
TLB
的访问,
(ASID
+
VPN)
才是
Primary Key.
由于
MIPS
的
CACHE
属性是
Virtual
ly Indexed, Physically tagged.
所以,任何地址的访
问,
CPU
都会
issue the
request to MMU for TLB translation to
get
the correct physical
address, which then will be used for level cache
matching.
与此同时
,
CPU
会把虚拟地址信号传给
Lev
el 1 Cache
控制器。然后,我们必须等待
MMU<
/p>
的
Physical
Address
数据。只有
physical
tag
也
匹配上了,我们才能说一个:
Cache Hit.
所以,我们不需要担心不同的进程有相同的虚拟地址的事情。
弟兄们可以重温一下我们讲过的
Direct Mapped;
Full Associative, and Set Associative.
从而理解为什么
Cache
中可以存在多个具有相同虚拟地址的
entry. For
example,the above
Addr1 for proccess A
and Addr1 for process B.
MIPS
异常和中断处理
(Exception and
Interrupt handling)
任何一个
p>
CPU
都要提供一个详细的异常和中断处理机制。一个软件系统,<
/p>
如操作系统,
就是一个
时序逻辑系统,通
过时钟,
外部事件来驱动整个预先定义好的逻辑行为。
这也是为
什么当写一个
操作系统时如何定义时间的计算是非常重要的原因。
大家都非常清楚
UNIX
p>
提供了一整套系统调用
(System
C
all)
。系统调用其实就是一段
EXCEPTION
处理程序。
我们
可能要问:为什么
CPU
要提供
Exc
petion
和
Interrupt
Handling
呢?
*
处理
illegal
behavior,
例如,
TLB Fault, or,
we say, the Page fault; Cache Error;
* Provide an approach
for
accessing priviledged
resources, for e
xample, CP0
registers.
As we know, for user level
tasks/processes, they are running
with
the User Mode priviledge and are prohibilited to
directly control CPO. CPU need
provide
a mechanism for
them
to trap
to kernel mode and
then
safely manipulate resources
that are only available
when CPU runs in kernel mode.
*
Provide
handling
for
external/internal
interrupts.
For
instance,
the
timer
interrupts
and watch dog exceptions. Those two
interrupt/exceptions are very important for an
embedded system applicances.
Now let's get back to how
MIPS supports its exception and interrupt
handling.
For
simplicty, all information below will be based on
R7K CPU, which is derived from
the R4k
family.
* The first thing
for understanding MIPS exception handling is:
MI
PS adopts **Precise
Exceptions**
mechanisms.
What
that
means?
Here
is
the
explaination
from
the
book
of
MIPS
Run
instruction(the exception victim).
All instructions preceding the exception victim in
execution
sequence are
complete; any work done on the victim and on any
subsequent instructions
(BNN NOTE:
pipeline effects) has no side effects that the
software need worry about.
The software
that handles exceptions can ignore all the timing
effects of the CPU's
implementations
上
面的意思其实很简单:
在发生
EXCEPTION
之前的一切计算行为会
**FINISH**
。
p>
在发生
EXCEPTION
之后的一切计算
行为将不需考虑。
对绝大多数情况而言,如你要写一个系统调用
(System
Call),
你只要记住:
MIP
S
已经把
syscall
这条指令的地
址压在了
EPC
寄存器里。换句话说,在
MIPS
里,
compard
to
the PowerPC CPU srr1 register,
你需要
**explicitely**
refill the EPC register by
EPC<-----EPC+4, before you use the eret
中断返回。只有这
样,你才能从系统调用中正确返回。
异常
/
中断
向量
(Exception/Interrupt Vector)
MIPS
的
Exception/Interrupt
Vector
的
organizaion is not as
good as PowerPC CPUs.
For
PPC, every detailed exception cause is directed to
a unqiue vector address.
MIPS
is otherwise. Below is a recap of MIPS
exception/interrupt vectors.
(We
herein only talk about running MIPS CPU in the 32
bit mode )
Reset, NMI 0x8000 0000
TLB refill 0x8000 0000
Cache
Error 0xA000
00100 (BNN: Why goes to 0xAxxxxx? A
question to
readers. Please
think
about the difference
between Kseg0 and kseg1)
All other
exceptions 0x8000 0180
How MIPS acts when taking an exception?
1. It sets up the EPC to
point to the restart location.
2. CPU
changes into kernel mode and disables the
interrupts (BNN: MIPS does this by
setting EXL bit of SR register)
3. Set up the Cause register to
indicate which is wrong. So that software can tell
the
reason for the exception. If it is
for address exception, for example, TLB miss and
so
on, the BadVaddr register is also
set.
4. CPU starts
fetching
instructions
from the exception
entry point and then goes to the
exception handler.
Returning from exceptions
Up to MIPS III, we use the eret
instruciton to return to the original location
before
falling
into
the
exception.
Note
that
eret
behavior
is:
clear
the
SR[EXL]
bit
and
returns
control to the adress stored in EPC.
An important bit in SR for
interrupt handling
SR[IE]:
This bit is used
to enable/disable
interrupts,, including
the timer
interrupts.
Of couse,
when the SR[EXL] bit is set, this bit
has no effects.
K0 and K1
registers:
These two
registers are mostly used
by kernel
as a
temporary
buffer
to
hold
some values
if necessary. So that you
don't have to find some pre-defined
memories for that purpose.
One thing we
should be
careful
is : When you
are allowing
the nested
exception/interrupt
handling, you need
take care of these two registers' values as
they will be over-written, for example.
I
don't
encouarge
people
to
use
the
AT
register
too
often,
even
though
you
can
use
the
.set
noat directive. I have found a bug in
mips-gcc, which will use the AT register anyway,
even
after we
use
the .set
noat. In
other
wrods, using AT is
dangeous somhow if
you are
not quite familire with the
register convention/usage
流水线
(Pipeline) and Interrupt
Taken
我们知道,
MIPS
是一个
RISC
技术处理器。在某一个
时刻,在流水线上,同时有若干个指令被处
理在不同的阶段
(s
tage)
上
.
MIPS
处理器一般采用
5
级
流水结构。
IF RD ALU
MEM WB
那么我们要问:当一个
Interrupt
发生时,
CPU
到底该
如何
handle
?答案是这样的:
“
On an interrupt in a
typical MIPS CPU, the last instruction to be
completed before
interrupt
processing
starts
will
be
the
one
that
has
just
finished
its
MEM
stage
when
the
interrupt
is
detected.
The
exception
victim
will
be
the
one
that
has
just
finished
its
ALU
stage...
对上述的理解是这样的:
CPU
会<
/p>
**
完成
**
那
条已
**finish**
MEM
stage
的指令。
然后将
excep
tion
victim
定位在下一条
(following)
指令上。要注意的是:我们是在谈
In
terrupt, not the
exception.
在
MIPS
中,这是有区别的。
下面介绍几个重要的
SR(Status Register)
与
Exception
和中断有关的位
。
* SR[EXL]
Exception Level; set by the processor
when any exception other than Reset,
So
ft Reset,
NMI, or Cache
Error exception are taken. 0: normal 1: exception
When EXL is set:
-
Interrupts
are
disabled.
换句话说,
这时
SR[IE]
位是不管用了,
相当于所
有的中断都被
MASK
了。
- TLB refill exceptions
will
use the
general exception vector
instead of the TLB refill
vector.
-
EPC
is
not
updated
if
another
exception
is
taken.
这一点要注意。如果我们想支持
nesting
exceptions,
我们要在
exception
hander
中
clear EXL bit.
当然要先保存
EPC
的值。另外要注
< br>意的:
MIPS
当陷入
Exce
ption/Interrupt
时,并不改变
SR[UX],
SR[KX]
或
SR[SX]
的值。<
/p>
SR[EXL]
为
1
自动的将
CPU mode
运行在
KERNEL
模式下。这一点要注意。
* SR[ERL]
Error
Level; set
by the
processor
when Reset,
Soft Reset, NMI,
or
Cache Error exception
are taken. 0: normal 1: error
When ERL is set:
-
Interrupts are disabled.
- The ERET
instruction uses the return address held in
ErrorEPC instead of EPC.
- Kuseg and
xkuseg
are treated
as
unmapped
and uncached
allows
main memory
to be
accessed in the presence of cache errors.
这时刻,我们可以说,
MIPS CPU
只有在
这个时刻才是一种
**
实模式
(real mode)**.
* SR[IE]
Interrupt
Enable
0:
disable
interrupts
1:
enable
interrupts
。
请记住:
当
SR[EXL]
或
SR[ERL]
被
S
ET
时,
SR[IE]
是无效的。
*
Exception/Interrupt
优先级。
Reset (highest priority)
Soft Reset
Nonmaskable
Interrupt (NMI)
Address error
--Instruction fetch
TLB refill--
Instruction fetch
TLB invalid--
Instruction fetch
Cache error
--Instruction fetch
Bus error
--Instruction fetch
Watch -
Instruction Fetch
Integer overflow,
Trap, System Call, Breakpoint, Reserved
Instruction, Coprocessor
Unus-able, or
Floating-Point Exception Address error--Data
access
TLB refill --Data access
TLB invalid --Data access
TLB modified--Data write
Cache error --Data access
Watch - Data access
Virtual
Coherency - Data access
Bus error --
Data access
Interrupt (lowest
priority)
大家请注意,
所谓的优先级是指:
当在某个时刻,
同时多个
Exception
或
Interrupt
出现时,
CPU
将会按照上述的优先级来
TAKE
。如果
CPU
目前在,
for example,TLB refill
处理
handler
中,
这时,出现了
p>
Bus
Error
的信号,
CPU
不会拒绝。当然,在这次的处理中,
EPC<
/p>
的值不会被更新,
如果
EXL
是
SET
的话。
Nesting
Exceptions
在有的情况下,我们希望在
Exception
或中断中,系统可以继续接送
exception
或中断。
这需要我们小心如下事情:
p>
*
进入处理程序后,我们要设置
CPU
p>
模式为
KERNEL
MODE
然后重新
clear SR[EXL],
从而支持
EPC
会被更新,如果出现新的
Exception/Interrupt
的话。
* EPC
和
SR
的值
EPC
和
S
R
寄存器是两个全局的。任何一个
Exception/Int
errupt
发生时,
CPU
硬件都会
将其
value over-
write.
所以,对于支持
Nesting Excepto
ins
的系统,要妥善保存
EPC
和<
/p>
SR
寄
存器
的
VALUE
。
*
EPC
和
SR
的值
EPC
和
SR
寄存器是两个全局的。任
何一个
Exception/Interrupt
发生时,
p>
CPU
硬件都会
将其
value
over-writ
e.
所以,
对于支持
Nesting
Exceptoins
的系统,
要妥善
保存
EPC
和
SR
寄
存器的
VALUE
。
SR[IE]
是一个很重要的
< br>BIT
来处理
在处理
Nesting Exception
< br>时,值
得注意的,或容易犯错的一点是
(
我在这上面吃过苦头
)
:
一定要注意:在做
restore context
时,要避免重入问题。比如,但要用
eret
返
回时,
我们
要
set up the EPC
value.
在此之前,一定要先
disable
interrupt.
否
则,
EPC value
可能被冲掉。
下面是一段
codes of
mine for illustrating the exception return.
restore_context
/* Retrieve the SR value */
mfc0 t0,C0_SR
/* Fill in a
delay slot instruction */
nop
/* Clear the SR[IE] to disable any
interrupts */
li t1,~SR_IE
and t0,t0,t1
mtc0 t0,C0_SR
nop
/* We can then safely
restore the EPC value * from the stack */
ld t1,R_EPC(sp)
mtc0
t1,C0_EPC
nop
lhu k1, /*
restore old interrupt imask */
or
t0,t0,k1
/* We reset the EXL bit
before returning from the exception/interrupt the
eret
instruction will automatically
clear the EXL then.
一定要理解
我为什么要在前面
clear
EXL.
如果不得话。就不能支持
nesting
exceptions.
为什么,希望读
者能思考并回答。并
且,在清
EXL
之前,我们一定要先把
CPU
模式变为
KERNEL
MODE
。
*/
ori t0,t0,SR_EXL
/* restore
mask and exl bit */
mtc0 t0,C0_SR
nop
ori t0,t0,SR_IE
/* re-set ie bit */
ori
t0,t0,SR_IMASK7
mtc0 t0,C0_SR
nop
/*
恢复
< br>CPU
模式
*/
ori
t0, t0,SR_USERMODE
mtc0, t0, C0_SR
eret /*eret
将
ATOMIC
的将
EXL
清零。所以要注意,如果你在处理程序中改变了
CPU
得模式
,
例如,
一定要确保,
在重新设置
p>
EXL
位后,恢复
CPU
< br>的
original
mode.
Otherwise,for
example,
a task/process will run in kernel mode.
That would be totally mess up your
sys
tem
software.*/
In
summary,
exception/interrupt
handling
is
very
critical
for
any
os
kernel.
For
a
kernel
engineer, you should
be very clear with the exception mechanisms of
your target CPU
provides. Otherwise, it
would cost you bunches of time for bug
f
ixes.
Again, the best way is to read the CPU
specification slowly and clearly. There is no
any better approach there. No genius,
but hard worker, always.
Mips kernel Introduction
1.
硬件知识
* CPU
手册
:
等
.
*
主板资料
,
找你的卖家
.
*
背景知识
:
如
PCI
协议
,
< br>中断概念等
.
2.
软件资源
* /linux,ftp://
*
* mailing lists:
linux-mips@
debian-mips@
* kernel cvs
sgi:
cvs -d
:pserver:cvs@:/cvs login
(Only
needed the first time you use anonymous CVS, the
password is
cvs -d :pserver:cvs@:/cvs co linux
<
/p>
另外
也有另一个内核树,似乎不如
sgi
的版本有影响
.
*
经典书籍
:
*
*
*
Jun
Sun's
mips
porting
guide:
/porting
-howto/
*
交叉编译指南
:/~brad/mips/
* Debian Mips port: /ports/mips
*
系统杂志网站
:
3. mips
kernel
的一般介绍
(
下面一些具体代码基于
2.4.
8
的内核
)
< br>我们来跟随内核启动运行的过程看看
mips
内核有什么
特别之处
.
加电后
,mips kernel
从系
统固件程序
(
类似
bios,
可能烧在
eprom,flash
中
)
得到控制
p>
之后
(head.S),
初始化内核栈
p>
,
调用
init_arch
初始化硬件平台相关的代码
.
init_arch(setup.c)
首先监测使用的
CPU(
通过
MIPS CPU
的
CP0
控制寄存器
PRID)
确定使用的指令集和一些
CPU
参数
,
如
< br>TLB
大小等
.
然后调用
prom_init
做一些底层
参数初始化
.
prom_init
是和具体的硬件相关的
.
使用
MIPS
CPU
的平台多如牛毛
,
所以大家在
arch/mips
下面可以看到很多的子目录
,
每个子目录是一个或者一系列相似的平台<
/p>
.
这里的平台差不多可以理解成一块主板
加上它的系统固件
,
其中很多还包括一些专用的显卡什么的硬件
(
比如
一些工作站
).
这些目录的主要任务是
:
1.
提供底层板子上的一些重要信息
,
包括系统固件传递的参数
,io
的
映射基地址
,
内存的大小的分布等
.
多数还包括提供早期的
信息输入输出接口
(
通常是一个
简单的串口驱动
)
以方便调试
,
因为
pmon
往往不提供键盘和显示卡的支持
.?
2.
底层中断代
码
,
包括中断控制器编程和中断的分派
,
应答等
3. pci
子系统底层代码
. <
/p>
实现
pci
配置空间的读写
,
以及
pci
设备的中断<
/p>
,IO/Mem
空间的分配
4.
其它
,
特定的硬件
.
常见的有实时
时钟等
这里关键是要理解这些硬件平台和熟悉的
x86
不同之处
.
我印象较深的有几个
:
* item MIPS
不象
X86
有很标准的硬件软件接口
,
而是五花八门
,
每个厂家
有一套
,
因为它们很多是嵌入式系统或者专门
的工作站
.
不象
PC
< br>中
,
有了
BIOS
后用同一套的程序
,
就可以使用很多不同的主板和
CPU.
MIPS
中的
'bios'
常用的有
pmon
< br>和
yamon,
都是开放源代码的软件。
很多开发板带的固件功能和
PC BIOS
很不一样
,
它们多数支持串口显示
,
或者网络下载和启动
,
以及类
DEBUG
的调试界面
,
但可能根本不
支持显卡和
硬盘
,
没有
一般的基本
'
输入输出
'
功能
.
*
PCI
系统和地址空间
,
总线等问题<
/p>
.
在
x86
中
,IO
空间用专门的指令访问
,<
/p>
而
PCI
设备的内存空间和物理内存
p>
空间是相同的
,
也就是说
,
在
CPU
看来物理内存从
地址
0
开始的话
,
在
PCI
设备
看来也是一样的
.
反之
,PCI<
/p>
设备的基地址寄存器设定的
PCI
存储地
址
,CPU
用
相同的物理地址访问就行了
.
而在
MIPS
中就很不一样了
,IO
一般是
memory map
的
,map
到哪里就倚赖具体
平台了
.
而
PCI
设备的地址空间和
CPU
所见的物理内存地址空间往往
也不一样
(bus address & physical
address).
所以
mips kernel
的
iob/outb,
以及
bus_to_vi
rt/virt_to_bus,phys_to_virt/virt_to_phys,ioremap
p>
等就
要小心考虑
.
这些问题有时间我会对这些问题做专门的说明
.
*
中断系统
.
PC
中中断控制器先是有<
/p>
8259,
后来是
apic,
而
cpu
的中断处理
38
6
之后好像
也变化不大
,
相对统一
.
mips CPU
的中断处
理方式倒是比较一致
,
但是主板上的控制器就乱七八糟了
怎么鉴别中断源
,
怎么编程控制器等任
务就得各自实现了
.
总的说来
,MIPS CPU
p>
的中断处理方式体现了
RISC
的特点
p>
:
软件做事多
,
硬
件尽量
精简
.
编程控制器
,
提供中断控制接口
,di
spatch
中系
?
这一部分原来很混
乱
,
大家各写各的
,
< br>现在有人试图写一些比较统一的代码
(
实际上就是原来<
/p>
x86
上
*
存储管理
.
MIPS
是典型的
RISC
结构
,
它的
存储管理单元做的事情比象
x86
这种机器少得多
.
例如
,
它的
tlb
是软件管理的
,cache
p>
常常是需要系统程序干预的
.
而且
,
过多的
CPU<
/p>
和主板变种使得这一部分非常复杂
,
容易
出错
.
存储管理的代码主要在
asm-mips
和
arch/mips/mm/
目录下
.
用的
controller/handler
抽象
).
PCI
配置空间的读写和地址空间映射的处理通常都是每个平台不一样的
.
因为缺乏统一接口的
BIOS,
内核经常要自己做<
/p>
PCI
设备的枚举
,
空间分配
,
中断分配
.
include/
*
其它
.
如时间处理
,r4k
以上的
MIPS
CPU
提供
count/compare
寄存器
,
每隔几拍
count
增加
,
到和
compare
相等时发生时钟中断
,
这可以用来提供系统的时钟中断
.
但很多
板子
问
,
在
cache
和
tlb
之前
CPU
怎么工作的
?
在
x86
里有实模式
,
而
MIPS
没有
,
但它的地址空间是特殊的
,
分成几个不同的区域
,
每个区域中的地址在
CPU
里的待遇是不一样的
,
系统刚上电时
CPU
从地址
bfc00000
开始
,
那里的地址既不用
tlb
也不用
cache,
所以
CPU
能工作而不管
cache
和
tlb
是什
么
样子
.
当然
,
这样子效率是很低的
p>
,
所以
CPU
很快
就开始进行
loadmmu.
因为
MIPS
CPU
变种繁多
,
所以代码又臭又长
.
主要
不外是检测
cache
大小
,
选择相应的
cache/tlb
flush
过程
,
还有一些
memcpy/memset
等的高效实现<
/p>
.
这里还很容易出微妙的错误
,
软件管理
tlb
或者
cache
都不简
单
,
要保证效率又要保证正确
.
在开发初期常常先
关掉
CPU
的
cache
以便排除
cache<
/p>
问题
.
MMU
初始化后
,
系统就直接跳转到<
/p>
init/main.c
中的
start
_kernel,
很快吧
?
不过别高兴
,start_ker
nel
虚晃一枪
,
又回到
arch/mips/kernel/setup.c,
调用
自己也提供其它的可编程时钟源
.
具体用什么就取决于开发者了
.
init_arch
后是
lo
admmu,
初始化
cache/tlb.
代码在
arch/mips/mm
里
.
有人可能会
setu
p_arch,
这回就是完成上面说的各平台相关的初始化了
.
平台相关的初始化完成之
后
,mips
内核和其它平台的内核区别就不大了
,
但也还有
p>
不少问题需要关注
.
如许多驱动程序可能因
为倚赖
x86
的特殊属性
(
如
IO
端口
,
自动
的
cache
一致性维护
,
显卡初始化等
)
而不能直接在
MI
PS
下工作
.
例如
,
能直接
(
用现有的内核驱动
)
在
MIPS
p>
下工作的网卡不是很多
,
我知道的有
intel
eepro100,AMD
pcnet32
,Tulip.
3
com
的网卡好像大多不能用
.
显卡则
由于
vga
bios
p>
的问题
,
很少能直接使用
< br>.(
常见的显卡都是为
x86
做
的
,
它们常常带着一块
rom,
里面
含有
vga bios,PC
的
BIOS
的初始化过程中发现它们的化就会先去执行它们以初始化
p>
显卡
,
然后才能很早地在屏幕上输出信息
).
而
vga
bios
里面的代码一般是
for x86,
不能直接在
mips CPU
上运行
.
而且这些代码里常常有一些厂
家相关的特定初始化
,
没有
一个通用的代码可以替换
.
只有少数比较开放的厂家提供足够的
资料使得内核
开发人员能够跳过
vga bios
的
执行直接初始化他的显卡
,
如
matr
ox.
除此之外
,
也可能有其它的内核代码由于种种原因
(<
/p>
不对齐访问
,unsigned/signed
等
)
不能使用
,
如一些文件系统
(xfs?
).
关于
linux-mips
内核的问题
,
在
sgi
的
mailing
list
搜索或者提问比较有希望获得
解决
.
如果你足够有钱
,
可以购买
mont
ivista
的服务
..
的异常处理
1.
硬件
mips CPU
的异常处理中
,<
/p>
硬件做的事情很少
,
这也是
RISC
的特点
.
和
p>
x86
系统相比
,
有两点大不一样
:
*
硬件不负责具体鉴别异常
,C
PU
响应异常之后需要根据状态寄存器等来确定
究竟发生哪个异常
p>
.
有时候硬件会做一点简单分类
,CPU<
/p>
能直接到某一类异常
的处理入口
.
*
硬件通常不负责保存
上下文
.
例如系统调用
,
所有的寄存器内容都要由软件
进行必要的保存
.
各种主板的中断控制种类
很多
,
需要根据中断控制器和连线情况来编程
< br>.
实现
*
处理程序什么时候安装
?
traps_init(arch/mips/kernel/traps.c,setup_arch
之后
start_kernel
调用
)
...
/*
Copy
the
generic
exception
handler
code
to
it's
final
destination.
*/
memcpy((void *)(KSEG0 + 0x80),
&except_vec1_generic, 0x80);
memcpy((void *)(KSEG0 + 0x100),
&except_vec2_generic, 0x80);
memcpy((void *)(KSEG0 + 0x180),
&except_vec3_generic, 0x80);
flush_icache_range(KSEG0 + 0x80, KSEG0
+ 0x200);
/*
* Setup default
vectors
*/
for (i = 0; i <= 31; i++)
set_except_vector(i, handle_reserved);
...
*
装的什么
?
except_vec3_generic(head.S)
(
除了
TLB
refill
例外都用这个入口
):
/* General exception vector R4000
version. */
NESTED(except_vec3_r4000, 0, sp)
.set
noat
mfc0
k1, CP_CAUSE
andi
k1, k1, 0x7c /*
从
cause
寄存器取出异常号
*/
li
k0, 31<<2
beq
k1, k0,
handle_vced /*
如果是
vced,
处理之
*/
li
k0, 14><<2
beq
k1, k0, handle_vcei
/*
如果是
vcei,
处理之
*/ /*
这两个异常是和
ca
che
相关的
,cache
出了问题<
/p>
,
不能再在这个
cached
的
位置处理啦
*/
la
k0,
exception_handlers /*
取出异常处理程序表
*/
addu k0, k0, k1
lw
k0, (k0)
/*
处理函数
*/ nop
jr
k0
/*
运行异常处理函数
*/ nop
那个异常处理程序表是如何初始化的呢
?
在
traps_init
中
,
大家会看到
set_exception_vector
(i,handler)
这样的代码
,
填的就是这张表啦
.
可是
,
如果你用
souce insigh
之
类的东西去找那个
handler,
往往
就落空了
,??
怎么没有
handle_ri,handle_tlbl..._?
不着
急
,
只不过是一个小
trick,
还记得
x86
中断处理的
handler
代码吗
?
它们是用宏生成的
: entry.S
...
#define
BUILD_HANDLER(exception,handler,clear,verbose)
.align 5;
SAVE_ALL; /*
保存
现场
,
切换栈
(
如必要
)*/
__BUILD_##verbose(exception);
jal
NESTED(handle_##exception, PT_SIZE,
sp);
.set
noat;
__BUILD_clear_##clear(exception);
/*
关中断
?*/
.set
at;
do_##handler;
/*
干活
*/
move a0, sp;
END(handle_##exception)
/*
生成处理函数
*/
BUILD_HANDLER(adel,ade,ade,silent)
/* #4
*/
BUILD_HANDLER(ades,ade,ade,silent)
/* #5
*/
BUILD_HANDLER(ibe,ibe,cli,verbose)
/* #6
*/
BUILD_HANDLER(dbe,dbe,cli,silent)
/* #7 */
BUILD_HANDLER(bp,bp,sti,silent)
/*
#9
*/
认真追究下去
,
这里的一些宏是很重要的
,
象
SAVE_ALL(include/asm/stackframe.h),
异常处理
要高效
,
正确
,
这里要非常小心
.
这是因为硬件做的事情实在太少了
.
别的暂时先不说了
,
下面我们来看外设中断
(
它是一种特殊的异常
). entry.
S
并没有用
BUILD_HANDLER
生
成中断处理函数
,
因为它是平台相
关的
就以我的板子为例
,
在
arch/mips/algor/p6032/kern
el/
中
(
标准内核没有
)
增加了
p6032IRQ.S
这个汇
j
ret_from_exception; /*
回去
*/
nop;
编文件
< br>,
里面定义了一个
p6032IRQ
的函数
,
它负责
p>
鉴别中断源
,
调用相应的中断控制器
处理代码
,
而在同目录的
irq.c->init_IRQ
中调用
set_except_vector(0,p6032IRQ)<
/p>
填表
(
所有的中断都引发异常
0,
并在
cause
寄存器中设置具体中断原因
).
下面列出这两个文件以便解说
:
p6032IRQ.s
algor p6032(
我用的开发板
)
中断安排如下
:
MIPS IRQ
Source
*
--------
------
*
0
Software (ignored)
*
1
Software (ignored)
*
2
bonito
interrupt (hw0)
*
3
i8259A interrupt (hw1)
*
4
Hardware (ignored)
*
5
Debug
Switch
*
6
Hardware (ignored)
*
7
R4k timer (what we use)
.text
.set
noreorder
.set
noat
.align 5
NESTED(p6032IRQ, PT_SIZE, sp)
SAVE_ALL /*
保存现场
,
切换堆栈
(if
usermode -> kernel mode)*/
CLI
/*
关中断
,mips
有多种方法禁止响应中断
,CLI
用清
status
相应位
的方法
,
如下
:
Move to kernel mode
and disable interrupts.
Set cp0 enable bit as
sign that we're running
on the kernel stack */
#define CLI
mfc0
t0,CP0_STATUS;
li
t1,ST0_CU0|0x1f;
or
t0,t1;
xori
t0,0x1f;
mtc0
t0,CP0_STATUS
.set
at
mfc0
s0, CP0_CAUSE
/*
get irq mask,
中断响应时
cause
寄存器指示
哪类中断发生
-
-
-
-
-
-
-
-
-
上一篇:linux下安装oracle集群
下一篇:消息收发接口规范