TQ2440中断系统
1.1 S3C2440系统中断
CPU和外设构成了计算机系统,CPU和外设之间通过总线进行连接,用于数据通信和控制,CPU管理监视计算机系统中所有硬件,通常以两种方式来对硬件进行管理监视:
- 查询方式:CPU不停的去查询每一个硬件的当前状态,根据硬件的状态决定处理与否。好比是工厂里的检查员,不停的检查各个岗位工作状态,发现情况及时处 理。这种方式实现起来简单,通常用在只有少量外设硬件的系统中,如果一个计算机系统中有很多硬件,这种方式无疑是耗时,低效的,同时还大量占用CPU资 源,并且对多任务系统反应迟钝。
- 中断方式:当某个硬件产生需要CPU处理的事件时,主动通过一根信号线“告知”CPU,同时设置某个寄存器里对应的位,CPU一旦发现这根信号线上的电平 有变化,就会中断当前程序,然后去处理发出该中断请求。这就像是医院重危病房,病房每张病床床头有一个应急按钮,该按钮连接到病房监控室里控制台一盏指示 灯,只要该张病床出现紧急情况病人按下按钮,病房监控室里电铃会响起,通知医护人员有紧急情况,医护人员这时查看控制台上的指示灯,找出具体病房,病床 号,直接过去处理紧急情况。中断处理方式相对查询方式要复杂的多,并且需要硬件的支持,但是它处理的实时性更高,嵌入式系统里基本上都使用这种方式来处 理。
系统中断是嵌入式硬件实时地处理内部或外部事件的一种机制。对于不同CPU而言,中断的处理只是细节不同,大体处理流程都一样,S3C2440A的中断控制器结构如下图所示:
图3-3 S3C2440中断控制器
中断请求由硬件产生,根据中断源类型分别将中断信号送到SUBSRCPND(SubSourcePending)和 SRCPND(SourcePending)寄存器,SUBSRCPND是子中断源暂存寄存器,用来保存子中断源信号,SRCPND是中断源暂存寄存器, 用来保存中断源信号。中断信号可通过编程方式屏蔽掉,SUBMASK是子中断源屏蔽寄存器,可以屏蔽指定的子中断信号, MASK功能同SUBMASK用来屏蔽中断源信号。中断分为两种模式:一般中断的和快速中断,MODE是中断模式判断寄存器,用来判断当前中断是否为快速 中断,如果为快速中断直接将快速中断信号送给ARM内核,如果不是快速中断,还要将中断信号进行仲裁选择。S3C2440A支持多达60种中断,很有可能 多个硬件同时产生中断请求,这时要求中断控制器做出裁决,Priority是中断源优先级仲裁选择器,当多个中断产生时,选择出优先级最高的中断源进行处 理,INTPND是中断源结果寄存器,里面存放优先级仲裁出的唯一中断源。
1 中断的产生-中断源
S3C2440A支持60种中断源,基本上满足了开发板内部,外部设备等对中断的需求。其中每一个中断源对应寄存器中的一位,显然要支持60种中断 至少需要二个32位寄存器,SUBSRCPND和SRCPND分别保存中断源信号。S3C2440A对60种中断源的管理是按层级分的。如图3-4所示:
图3-4中断源信号复合示意图
S3C2440A将中断源分为两级:中断源和子中断源,中断源里包含单一中断源和复合中断源,复合中断源是子中断源的复合信号。如实时时钟中断,该 硬件只会产生一种中断,它是单一中断源,直接将其中断信号线连接到中断源寄存器上。对于复合中断源,以UART串口为例进行说明,S3C2440A可以支 持3个UART串口,每个串口对应一个复合中断源信号INT_UARTn,每个串口可以产生三种中断,也就是三个子中断:接收数据中断INT_RXDn, 发送数据中断INT_TXDn,数据错误中断INT_ERRn,这三个子中断信号在中断源寄存器复合为一个中断信号,三种中断任何一个产生都会将中断信号 传递给对应的中断源INT_UARTn,然后通过中断信号线传递给ARM内核。
图3-5 UART串口中断源信号复合示意图
总中断源详下面表中列出了S3C2440A部分中断源,它分别对应中断源寄存器里某个位:详细中断源请查看S3C2440A硬件手册。
表3-5 部分中断源信号
中断源 |
描述 |
优先级仲裁分组 |
INT_ADC |
数模转换和触摸屏中断 |
ARB5 |
INT_RTC |
实时时钟中断 |
ARB5 |
INT_UART0 |
UART0中断(包含子中断) |
ARB5 |
INT_NFCON |
NandFlash控制中断 |
ARB4 |
INT_WDT_AC97 |
看门狗中断 |
ARB1 |
EINT8-23 |
外部中断8~23(包含外部子中断) |
ARB1 |
EINT4-7 |
外部中断4~7(包含外部子中断) |
ARB1 |
EINT3 |
外部中断3 |
ARB0 |
EINT2 |
外部中断2 |
ARB0 |
EINT1 |
外部中断1 |
ARB0 |
EINT 0 |
外部中断0 |
ARB0 |
中断信号除上述分法之外,还可以按照硬件位置分为:外部中断源和内部中断源。
- 内部中断源:它是嵌入式系统中常见硬件产生的中断信号,比如:UART串口中断源,时钟Timer中断源,看门狗中断源等
- 外部中断源:有时嵌入式系统里要在外部接口上挂载一些外部设备,这些设备并不是一个通用嵌入式系统里必备硬件,比如:蓝牙模块,各种传感器,WIFI无线 通信模块,这些硬件也要产生中断让CPU来处理数据,因此这些外设硬件通过中断信号线连接到中断控制器上,它们产生的中断叫做外部中断信号。它们有着和内 部中断一样的处理机制,只不过,它没有一个固定的中断号与之对应,硬件与嵌入式系统的连接方式与中断处理完全由系统硬件与软件设计者实现。
外设硬件通过输入输出接口I/O Ports挂接到嵌入式系统上,I/O Ports向外设提供外部中断信号线,输出电源,频率时钟和输入输出信号线,外部硬件根据自己需要连接到I/O Ports上,产生中断时向外部中断信号线上送出中断信号,通过外部中断信号线传递到中断控制器。
按键Key可以认为最为简单的一种硬件设备了,如下图所示:
图3-6按键硬件接线原理图
它功能很简单,可以将电路接通,按键K1~K6一端接地为低电平,另外一端接电源正极为高电 平,EINT8,EINT11,EINT13,EINT14,EINT15,EINT19六根中断信号线分别和高电平端按键相连,当按键按下时电路接通, 整个电路变成低电平,中断信号线上电压产生变化,通过设置中断触发方式,产生外部中断请求,输入到CPU内部,从而实现按键中断控制。
S3C2440A可以支持EINT0~EINT23共24种外部中断,完全可以满足小型嵌入式设备外设硬件的需求。
外部中断源也分为外部中断源和外部子中断源,其处理方式和内部中断源基本一样。
1.1.1 中断优先级
S3C2440A支持60种中断,多个硬件可能同时产生中断请求,由于CPU只能处理一个中断,中断控制器怎么选择出一个最佳的中断,交给ARM内 核进行处理呢? 中断控制器采用优先级仲裁比较的方式进行选择,找出优先级最高的中断源。中断控制器将60种中断源分成7组,如下图所示,它类似体育赛事里的比赛方式,所 有参赛选手在小组赛PK,选择出小组赛最优秀选手,然后进入决赛阶段和其它小组最优先选择再PK,最后优胜者就是总冠军。其中 ARBITER0~ARBITER5为“小组赛”阶段,中断源信号在各自小组里进行优先级仲裁,选择出最高优先级中断信号,每小组选出的中断信号送到 ARBITER6,也就是决赛阶段,选择出最高优先级中断信号,交给ARM内核。
图3-7 S3C2440优先级仲裁示意图
中断信号在7个分组里PK时的优先级是可编程的,通过PRIORITY寄存器进行优先级设置。如下表(只列出PRIORITY寄存器部分位):
表3-6 中断优先级控制寄存器(PRIORITY)
寄存器名 |
地址 |
是否读写 |
描述 |
复位默认值 |
PRIORITY |
0x4A00000C |
R/W |
中断优先级控制寄存器 |
0x7F |
PRIORITY |
位 |
描述 |
初始值 |
ARB_SEL6 |
[20:19] |
仲裁组6优先级排序方式 00 = REQ 0-1-2-3-4-5 01 = REQ 0-2-3-4-1-5 10 = REQ 0-3-4-1-2-5 11 = REQ 0-4-1-2-3-5 |
0x00 |
ARB_SEL5 |
[18:17] |
仲裁组5优先级排序 00 = REQ 1-2-3-4 01 = REQ 2-3-4-1 10 = REQ 3-4-1-2 11 = REQ 4-1-2-3 |
00 |
… |
… |
… |
… |
ARB_MODE6 |
[6] |
仲裁组6优先级是否轮转: 0 = 不轮转, 1 = 轮转 |
1 |
ARB_MODE5 |
[5] |
仲裁组5优先级是否轮转: 0 = 不轮转, 1 = 轮转 |
1 |
… |
… |
… |
… |
通过设置仲裁组n优先级排序方式位,设置每个仲裁组内中断信号的优先级顺序,比如:ARB_SEL5分组时包含四个中断信号:REQ1 INT_UART0, REQ2 INT_SPI1, REQ3 INT_RTC, REQ4 INT_ADC,ARB_SEL5位采用默认值:00,当INT_UART0和INT_RTC中断信号同时产生时,INT_UART0会被选出,通过可编 程方式改变优先级排序方式来改变中断信号优先级。
ARB_MODE0~ ARB_MODE6为每个仲裁分组的优先级轮转设置位,采用默认值时,当前中断信号被选择处理之后,再次产生中断请求时,它的优先级自动轮转到该组最低, 这样可以保证优先级低的中断信号可以被及时处理,不至于出现优先级高且中断请求频繁的中断每次都被优先处理,而优先级低的被“饿死”的情况。显然,这种方 式更民主,实时性更佳。
2 中断控制器相关寄存器
(1)SUBSRCPND子中断源暂存寄存器
表3-7 子中断源暂存寄存器(SUBSRCPND)
寄存器名 |
地址 |
是否读写 |
描述 |
复位默认值 |
SUBSRCPND |
0x4A000018 |
R/W |
子中断源暂存寄存器,保存中断请求状态: 0:没有中断请求信号 1:中断请求信号产生 |
0x00000000 |
SUBSRCPND |
对应SRCPND |
位 |
描述 |
初始值 |
Reserved |
无 |
[31:15] |
未使用 |
0 |
INT_AC97 |
INT_WDT_AC97 |
[14] |
0 = 未产生中断 1 = 产生中断 |
0 |
… |
… |
… |
… |
… |
INT_RXD0 |
INT_UART0 |
[0] |
0 = 未产生中断 1 = 产生中断 |
0 |
该寄存器用来标识保存子中断源信号,当某个子中断信号产生之后,SUBSRCPND对应位被自动置1,该位会一直保持被置位,只到中断处理程序将其清除为止,需要注意一下,清除中断是通过向对应位写入1来清除,而不是写入0,写入0无效。
(2)INTSUBMSK子中断源屏蔽寄存器
表3-8 子中断源屏蔽寄存器(INTSUBMSK)
寄存器名 |
地址 |
是否读写 |
描述 |
复位默认值 |
INTSUBMSK |
0x4A00001C |
R/W |
子中断源信号屏蔽存寄存器,设置相应位来屏蔽中断信号: 0:未屏蔽,中断可用 1:屏蔽中断信号 |
0xFFFF |
INTSUBMSK |
位 |
描述 |
初始值 |
Reserved |
[31:15] |
未使用 |
0 |
INT_AC97 |
[14] |
0 = 未屏蔽1 = 屏蔽中断 |
1 |
… |
… |
… |
… |
INT_RXD0 |
[0] |
0 = 未屏蔽1 = 屏蔽中断 |
1 |
该寄存器用来屏蔽子中断源信号,默认值为全部子中断都被屏蔽掉,因此要想处理某个硬件中断,必须要打开中断屏蔽位,通过写入0来取消屏蔽中断。
(3)SRCPND中断源暂存寄存器
表3-9中断源暂存寄存器(SRCPND)
寄存器名 |
地址 |
是否读写 |
描述 |
复位默认值 |
SRCPND |
0x4A000000 |
R/W |
中断源暂存寄存器,保存中断请求状态: 0:没有中断请求信号 1:中断请求信号产生 |
0x00000000 |
SRCPND |
位 |
描述 |
初始值 |
INT_ADC |
[31] |
0 = 未产生中断 1 = 产生中断 |
0 |
… |
… |
… |
… |
EINT0 |
[0] |
0 = 未产生中断 1 = 产生中断 |
0 |
该寄存器用来保存中断源信号,当某个中断信号产生之后, SRCPND对应位被自动置1,该位会一直保持被置位,只到中断处理程序将其清除为止,需要注意一下,清除中断是通过向对应位写入1来清除,而不是写入0,写入0无效。
(4)INTMSK中断源屏蔽寄存器
表3-10中断源屏蔽寄存器(INTMSK)
寄存器名 |
地址 |
是否读写 |
描述 |
复位默认值 |
INTMSK |
0x4A000008 |
R/W |
中断源信号屏蔽存寄存器,设置相应位来屏蔽中断信号: 0:未屏蔽,中断可用 1:屏蔽中断信号 |
0xFFFFFFFF |
INTMSK |
位 |
描述 |
初始值 |
INT_ADC |
[31] |
0 = 未屏蔽1 = 屏蔽中断 |
1 |
… |
… |
… |
… |
EINT0 |
[0] |
0 = 未屏蔽1 = 屏蔽中断 |
1 |
该寄存器用来屏蔽中断源信号,默认值为全部中断都被屏蔽掉,因此要想处理某个硬件中断,必须要打开中断屏蔽位,通过写入0来取消屏蔽中断。
(5)INTPND最高优先级中断暂存寄存器
表3-11最高优先级中断暂存寄存器(INTPND)
寄存器名 |
地址 |
是否读写 |
描述 |
复位默认值 |
INTPND |
0x4A000010 |
R/W |
最高优先级中断暂存寄存器里面保存有经过优先级仲裁的结果: 0:没有中断请求信号 1:中断请求信号产生 |
0x00000000 |
INTPND |
位 |
描述 |
初始值 |
INT_ADC |
[31] |
0 = 未产生中断 1 = 产生中断 |
0 |
… |
… |
… |
… |
EINT0 |
[0] |
0 = 未产生中断 1 = 产生中断 |
0 |
该寄存器保存了经过优先级仲裁出的中断信号位,它是所有当前中断请求里优先级别最高的中断,因此该寄存器里的值最多有一位被置1,通常中断处理程序中会通过读取该寄存器的值来获得当前正在处理的中断请求。中断处理完成之后,通过写入1来清除中断。
(6)INTOFFSET中断号偏移量寄存器
表3-12中断号偏移量寄存器(INTOFFSET)
寄存器名 |
地址 |
是否读写 |
描述 |
复位默认值 |
INTOFFSET |
0x4A000014 |
R |
中断号偏移量寄存器,用来保存当前处理的中断号 |
0x0000000 |
该寄存器里存放的是经过优先级仲裁出的中断信号对应的中断号,是一个0~31之间的整数,其实它就是INTPND里对应的位号,比 如:INT_UART0产生了中断,INTPND里第28位置1,INTOFFSET里保存的整数就是28,多出来这个寄存器的目的主要是方便中断处理程 序查询中断源,清除中断源:
1 #define TIMER0_IRQ_OFT 10 // 时钟0定时中断 2 3 #define EINT0_IRQ_OFT 0 // 开发板K1按键1对应外部中断EINT0 4 5 void handle_irq() 6 7 { 8 9 unsigned long irqOffSet = INTOFFSET; // 取得中断号 10 11 switch(irqOffSet) 12 13 { 14 15 case TIMER0_IRQ_OFT: // 当前中断为定时器0中断 16 17 do_timer(); // 跳入定时器0处理程序 18 19 break; 20 21 case EINT0_IRQ_OFT: // 当前中断为K1按键触发 22 23 do_key1_pressed(); // 处理K1按下事件 24 25 break; 26 27 } 28 29 30 31 SRCPND &= (1<<irqOffSet); // 清除中断源 32 33 INTPND = INTPND; // 清除最高优先级中断暂存寄存器中断 34 35 }
(7)INTMOD中断模式寄存器
表3-13中断模式寄存器(INTMOD)
寄存器名 |
地址 |
是否读写 |
描述 |
复位默认值 |
INTMOD |
0x4A000004 |
R/W |
中断模式寄存器,指定对应中断模式: 0 = IRQ一般中断模式 1 = FIQ快速中断模式 |
0x0000000 |
INTMOD |
位 |
描述 |
初始值 |
INT_ADC |
[31] |
0 = IRQ 1 = FIQ |
0 |
… |
… |
… |
… |
EINT0 |
[0] |
0 = IRQ 1 = FIQ |
0 |
通过设置INTMOD寄存器对应位,来指定对应中断模式,如果指定为一般中断,那么中断信号会进行优先级仲裁,如果指定为快速中断,那么中断信号直接送给ARM内核产生中断。需要注意的是,快速中断不存在优先级仲裁,只能有一位被设置为FIQ模式。
**********************************************************************************************************************************************************
*************************************************************************************************************************************************************
1.1 ARM处理器异常处理
所谓异常就是正常的用户程序被暂时中止,处理器就进入异常模式,例如响应一个来自外设的中断,或者当前程序非法访问内存地址都会进入相应异常模式。
1.1.1 异常分类
(1)复位异常
当CPU刚上电时或按下reset重启键之后进入该异常,该异常在管理模式下处理。
(2)一般/快速中断请求
CPU和外部设备是分别独立的硬件执行单元,CPU对全部设备进行管理和资源调度处理,CPU要想知道外部设备的运行状态,要么CPU定时的去查看 外部设备特定寄存器,要么让外部设备在出现需要CPU干涉处理时“打断”CPU,让它来处理外部设备的请求,毫无疑问第二种方式更合理,可以让CPU“专 心”去工作,这里的“打断”操作就叫做中断请求,根据请求的紧急情况,中断请求分一般中断和快速中断,快速中断具有最高中断优先级和最小的中断延迟,通常 用于处理高速数据传输及通道的中数据恢复处理,如DMA等,绝大部分外设使用一般中断请求。
(3)预取指令中止异常
该异常发生在CPU流水线取指阶段,如果目标指令地址是非法地址进入该异常,该异常在中止异常模式下处理。
(4)未定义指令异常
该异常发生在流水线技术里的译码阶段,如果当前指令不能被识别为有效指令,产生未定义指令异常,该异常在未定义异常模式下处理。
(5)软件中断指令(swi)异常
该异常是应用程序自己调用时产生的,用于用户程序申请访问硬件资源时,例如:printf()打印函数,要将用户数据打印到显示器上,用户程序要想 实现打印必须申请使用显示器,而用户程序又没有外设硬件的使用权,只能通过使用软件中断指令切换到内核态,通过操作系统内核代码来访问外设硬件,内核态是 工作在特权模式下,操作系统在特权模式下完成将用户数据打印到显示器上。这样做的目的无非是为了保护操作系统的安全和硬件资源的合理使用,该异常在管理模 式下处理。
(6)数据中止访问异常
该异常发生在要访问数据地址不存在或者为非法地址时,该异常在中止异常模式下处理。
1.1.2 异常发生的硬件操作
在异常发生后,ARM内核会自动做以下工作:
- 保存执行状态:将CPSR复制到发生的异常模式下SPSR中;
- 模式切换:将CPSR模式位强制设置为与异常类型相对应的值,同时处理器进入到ARM执行模式,禁止所有IRQ中断,当进入FIQ快速中断模式时禁止FIQ中断;
- 保存返回地址:将下一条指令的地址(被打断程序)保存在LR(异常模式下LR_excep)中。
- 跳入异常向量表:强制设置PC的值为相应异常向量地址,跳转到异常处理程序中。
(1)保存执行状态
当前程序的执行状态是保存在CPSR里面的,异常发生时,要保存当前的CPSR里的执行状态到异常模式里的SPSR里,将来异常返回时,恢复回CPSR,恢复执行状态。
(2)模式切换
硬件自动根据当前的异常类型,将异常码写入CPSR里的M[4:0]模式位,这样CPU就进入了对应异常模式下。不管是在ARM状态下还是在 THUMB状态下发生异常,都会自动切换到ARM状态下进行异常的处理,这是由硬件自动完成的,将CPSR[5] 设置为 0。同时,CPU会关闭中断IRQ(设置CPSR 寄存器I位),防止中断进入,如果当前是快速中断FIQ异常,关闭快速中断(设置CPSR寄存器F位)。
(3)保存返回地址
当前程序被异常打断,切换到异常处理程序里,异常处理完之后,返回当前被打断模式继续执行,因此必须要保存当前执行指令的下一条指令的地址到 LR_excep(异常模式下LR,并不存在LR_excep寄存器,为方便读者理解加上_excep,以下道理相同),由于异常模式不同以及ARM内核 采用流水线技术,异常处理程序里要根据异常模式计算返回地址。
(4)跳入异常向量表
该操作是CPU硬件自动完成的,当异常发生时,CPU强制将PC的值修改为一个固定内存地址,这个固定地址叫做异常向量(详见3.2.4章节)。
1.1.3 异常返回地址
由1.1.3节可知,一条指令的执行分为:取指,译码,执行三个主要阶段, CPU由于使用流水线技术,造成当前执行指令的地址应该是PC – 8(32位机一条指令四个字节),那么执行指令的下条指令应该是PC – 4。在异常发生时,CPU自动会将将PC – 4 的值保存到LR里,但是该值是否正确还要看异常类型才能决定。
各模式的返回地址说明如下:
(a)一般/快速中断请求:
快速中断请求和一般中断请求返回处理是一样的。通常处理器执行完当前指令后,查询FIQ/IRQ中断引脚,并查看是否允许FIQ/IRQ中断,如果 某个中断引脚有效,并且系统允许该中断产生,处理器将产生FIQ/IRQ异常中断,当FIQ/IRQ异常中断产生时,程序计数器pc的值已经更新,它指向 当前指令后面第3条指令(对于ARM指令,它指向当前指令地址加12字节的位置;对于Thumb指令,它指向当前指令地址加6字节的位置),当 FIQ/IRQ异常中断产生时,处理器将值(pc-4)保存到FIQ/IRQ异常模式下的寄存器lr_irq/lr_irq中,它指向当前指令之后的第2 条指令,因此正确返回地址可以通过下面指令算出:
1 SUBS PC,LR_irq,#4 ; 一般中断 2 3 SUBS PC,LR_fiq,#4 ; 快速中断
注:LR_irq/LR_fiq分别为一般中断和快速中断异常模式下LR,并不存在LR_xxx寄存器,为方便读者理解加上_xxx
(b)预取指中止异常:
在指令预取时,如果目标地址是非法的,该指令被标记成有问题的指令,这时,流水线上该指令之前的指令继续执行,当执行到该被标记成有问题的指令时, 处理器产生指令预取中止异常中断。发生指令预取异常中断时,程序要返回到该有问题的指令处,重新读取并执行该指令,因此指令预取中止异常中断应该返回到产 生该指令预取中止异常中断的指令处,而不是当前指令的下一条指令。
指令预取中止异常中断由当前执行的指令在ALU里执行时产生,当指令预取中止异常中断发生时,程序计数器pc的值还未更新,它指向当前指令后面第2 条指令(对于ARM指令,它指向当前指令地址加8字节的位置;对于Thumb指令,它指向当前指令地址加4字节的位置)。此时处理器将值(pc-4)保存 到lr_abt中,它指向当前指令的下一条指令,所以返回操作可以通过下面指令实现:
SUBS PC,LR_abt,#4
注:LR_abt为中止模式下LR,并不存在LR_abt寄存器,为方便读者理解加上_abt
(c)未定义指令异常:
未定义指令异常中断由当前执行的指令在ALU里执行时产生,当未定义指令异常中断产生时,程序计数器pc的值还未更新,它指向当前指令后面第2条指 令(对于ARM指令,它指向当前指令地址加8字节的位置;对于Thumb指令,它指向当前指令地址加4字节的位置),当未定义指令异常中断发生时,处理器 将值(pc-4)保存到lr_und中,此时(pc-4)指向当前指令的下一条指令,所以从未定义指令异常中断返回可以通过如下指令来实现:
MOV PC, LR_und
注:LR_und为未定义模式下LR,并不存在LR_und寄存器,为方便读者理解加上_und
(d)软中断指令(SWI)异常:
SWI异常中断和未定义异常中断指令一样,也是由当前执行的指令在ALU里执行时产生,当SWI指令执行时,pc的值还未更新,它指向当前指令后面 第2条指令(对于ARM指令,它指向当前指令地址加8字节的位置;对于Thumb指令,它指向当前指令地址加4字节的位置),当未定义指令异常中断发生 时,处理器将值(pc-4)保存到lr_svc中,此时(pc-4)指向当前指令的下一条指令,所以从SWI异常中断处理返回的实现方法与从未定义指令异 常中断处理返回一样:
MOV PC, LR_svc
注:LR_svc为管理模式下LR,并不存在LR_svc寄存器,为方便读者理解加上_svc
(e)数据中止异常:
发生数据访问异常中断时,程序要返回到该有问题的指令处,重新访问该数据,因此数据访问异常中断应该返回到产生该数据访问中止异常中断的指令处,而不是当前指令的下一条指令。
数据访问异常中断由当前执行的指令在ALU里执行时产生,当数据访问异常中断发生时,程序计数器pc的值已经更新,它指向当前指令后面第3条指令 (对于ARM指令,它指向当前指令地址加12字节的位置;对于Thumb指令,它指向当前指令地址加6字节的位置)。此时处理器将值(pc-4)保存到 lr_abt中,它指向当前指令后面第2条指令,所以返回操作可以通过下面指令实现:
SUBS PC, LR_abt, #8
注:LR_abt为中止模式下LR,并不存在LR_abt寄存器,为方便读者理解加上_abt
上述每一种异常发生时,其返回地址都要根据具体异常类型进行重新修复返回地址,再次强调下,被打断程序的返回地址保存在对应异常模式下的LR_excep里。
1.1.4 异常向量表
异常向量表是一段特定内存地址空间,每种ARM异常对应一个字长空间(4Bytes),正好是一条32位指令长度,当异常发生时,CPU强制将PC的值设置为当前异常对应的固定内存地址。如表3-4所示是S3C2440的异常向量表。
表3-4 异常向量表
注:
异常向量也可以出现在高地址0xFFFF0000处,当今操作系统为了控制内存访问权限,通常会开启虚拟内存,开启了虚拟内存之后,内存的开始空间通常为内核进程空间,和页表空间,异常向量表不能再安装在0地址处了
ARM的例外优先级从高到低依次为Reset→Data abort→FIQ→IRQ→Prefetch abort→Undefined instruction/SWI。
跳入异常向量表操作是异常发生时,硬件自动完成的,剩下的异常处理任务完全交给了程序员。由上表可知,异常向量是一个固定的内存地址,我们可以通过向该地址处写一条跳转指令,让它跳向我们自己定义的异常处理程序的入口,就可以完成异常处理了。
正是由于异常向量表的存在,才让硬件异常处理和程序员自定义处理程序有机联系起来。异常向量表里0x00000000地址处是reset复位异常, 之所以它为0地址,是因为CPU在上电时自动从0地址处加载指令,由此可见将复位异常安装在此地址处也是前后接合起来设计的,不得不感叹CPU设计师的伟 大,其后面分别是其余7种异常向量,每种异常向量都占有四个字节,正好是一条指令的大小,最后一个异常是快速中断异常,将其安装在此也有它的意义,在 0x0000001C地址处可以直接存放快速中断的处理程序,不用设置跳转指令,这样可以节省一个时钟周期,加快快速中断处理时间。
我们可以通过简单的使用下面的指令来安装异常向量表:
b reset ;跳入reset处理程序
b HandleUndef ;跳入未定义处理程序
b HandSWI ;跳入软中断处理程序
b HandPrefetchAbt ;跳入预取指令处理程序
b HandDataAbt ;跳入数据访问中止处理程序
b HandNoUsed ;跳入未使用程序
b HandleIRQ ;跳入中断处理程序
b HandleFIQ ;跳入快速中断处理程序
通常安装完异常向量表,跳到我们自己定义的处理程序入口,这时我们还没有保存被打断程序的现场,因此在异常处理程序的入口里先要保存打断程序现场。
保存执行现场:
异常处理程序最开始,要保存被打断程序的执行现场,程序的执行现场无非就是保存当前操作寄存器里的数据,可以通过下面的栈操作指令实现保存现场:
STMFD SP_excep!, {R0 – R12, LR_excep}
注:LR_abt,SP_excep分别为对应异常模式下LR和SP,为方便读者理解加上_abt
需要注意的是,在跳转到异常处理程序入口时,已经切换到对应异常模式下了,因此这里的SP是异常模式下的SP_excep了,所以被打断程序现场 (寄存器数据)是保存在异常模式下的栈里,上述指令将R0~R12全部都保存到了异常模式栈,最后将修改完的被打断程序返回地址入栈保存,之所以保存该返 回地址就是将来可以通过类似:MOV PC, LR的指令,返回用户程序继续执行。
异常发生后,要针对异常类型进行处理,因此,每种异常都有自己的异常处理程序,异常处理过程通过下节的系统中断处理来进行分析。
1.1.5 异常处理的返回
异常处理完成之后,返回被打断程序继续执行,具体操作如下:
- 恢复被打断程序运行时寄存器数据
- 恢复程序运行时状态CPSR
- 通过进入异常时保存的返回地址,返回到被打断程序继续执行
异常发生后,进入异常处理程序时,将用户程序寄存器R0~R12里的数据保存在了异常模式下栈里面,异常处理完返回时,要将栈里保存的的数据再恢复 回原先R0~R12里,毫无疑问在异常处理过程中必须要保证异常处理入口和出口时栈指针SP_excep要一样,否则恢复到R0~R12里的数据不正确, 返回被打断程序时执行现场不一致,出现问题,虽然将执行现场恢复了,但是此时还是在异常模式下,CPSR里的状态是异常模式下状态,因此要恢复 SPSR_excep里的保存状态到CPSR里,SPSR_excep是被打断程序执行时的状态,在恢复SPSR_excep到CPSR的同时,CPU的 模式和状态从异常模式切换回了被打断程序执行时的模式和状态。此刻程序现场恢复了,状态也恢复了,但PC里的值仍然指向异常模式下的地址空间,我们要让 CPU继续执行被打断程序,因此要再手动改变PC的值为进入异常时的返回地址,该地址在异常处理入口时已经计算好,直接将PC = LR_excep即可。
上述操作可以一步一步实现,但是通常我们可以通过一条指令实现上述全部操作:
LDMFD SP_excp!, {r0-r12, pc}^
注:SP_excep为对应异常模式下SP,^符号表示恢复SPSR_excep到CPSR
以上操作可以用下图来描述:
图3-2中断处理示意图
****************************************************************************************************************************************************************
****************************************************************************************************************************************************************
分类: ARM体系结构 2011-06-20 14:40 247人阅读评论(2) 收藏举报
中断源按照硬件位置分为外部中断源和内部中断源,外部中断源和内部中断源又包含子外部中断源和子内部中断源,如上图所示(画了一整天)。
1. 子内部中断源的产生
以UART0接收数据产生INT_RXD0中断为例,INT_RXD0产生后进入SUBSRCPND子中断源暂存寄存器,设置INT_RXD0对应 的中断位,中断信号经过INTSUBMSK子中断屏蔽寄存器,如果INT_RXD0信号对应位没有被置位(屏蔽掉),中断信号继续向前传递,经过子内部中 断源聚合器,将INT_RXD0聚合成对应的中断源信号INT_UART0,设置SRCPND中断源暂存寄存器里INT_UART0位,经过INTMSK 中断屏蔽寄存器,如果INT_UART0信号没有被屏蔽掉,中断信号进入INTMOD中断模式寄存器判断是否为快速中断,如果被编程为快速中断,直接打断 ARM内核,进入中断处理,如果中断信号为一般中断,进入中断优先级仲裁器进入优先级仲裁,如果INT_UART0信号为最高优先级或只有 INT_UART0中断信号产生,则该中断信号被记录到INTPND最高优先级中断暂存寄存器,同时设置INTOFFSET的值为中断号28,最终将中断 信号打断ARM内核进行中断处理。如果同时产生多个中断且INT_UART0不是最高优先级,则该中断信号不会被处理,等最高优先级信号处理完后,再次进 行优先级仲裁,也就是说中断信号不消失,一直保存在SRCPND里,只到被处理为止。
2. 内部中断源的产生
该过程在子内部中断处理过程中已经包含,中断信号产生后直接进入SRCPND里,然后经历上述子内部中断后期处理过程。
3. 子外部中断的产生
外部中断源共有24个,其中EINT0~EINT3为外部中断源,EINT4_7,EINT8_23为复合中断源,他们包含有子外部中断源。
由于外部硬件直接挂接到I/O Ports(详见S3C2440A硬件手册第9章)上的,我们要想让外设硬件中断得到处理,要先从EINT0~EINT23里选择中断信号,我们以EINT11为例,介绍子外部中断处理过程。
通常CPU内部引出引脚都是复用的,也就是说一根CPU引脚可以有多种功能,可以设置其为输入信号线,输出信号线或中断信号线,要想让硬件产生中 断,首先要对可以产生中断的引脚进行编程,设置该引脚为中断信号线。EINT11中断信号对应CPU引脚为GPG3,通过设置GPGCON[7:6] = 0b10,可以设置该引脚为中断信号线。
表3-14 GPGCON寄存器
设置了CPU管脚为中断信号线之后,还要通过设置EXTINT0寄存器来指定中断信号的触发方式:高电平触发,低电平触发,电平上升沿,下除沿,双沿触发。
图3-9 电平信号触发示意图
由于按键按下时让它产生中断,也就是从高电平变为低电平时产生(上节按键中断原理),因此我们设置EINT11中断信号的触发方式为下降沿触发,EXTINT1[14:12] = 0b01x
表3-15 EXTINT1寄存器
设置完触发方式之后,当外设中断信号线上的电平达到触发条件时,通过外部中断产生器产生中断信号,然后将子外部中断暂存寄存器EINTPND中对应 的EINT11位置1,中断信号再进入EINTMSK子外部中断屏蔽寄存器,如果EINT11中断源信号没有被屏蔽,则EINT11中断信号进入子外部中 断聚合器,复合成EINT8_23中断信号,然后再经历与前面子内部中断信号一样的处理机制。
(1)EINTPEND外部中断暂存寄存器
表3-16外部中断暂存寄存器(EINTPEND)
寄存器名 |
地址 |
是否读写 |
描述 |
复位默认值 |
EINTPEND |
0x560000A8 |
R/W |
外部中断信号暂存寄存器 0:没有中断请求信号 1:中断请求信号产生 |
0x0000000 |
EINTPEND |
位 |
描述 |
初始值 |
EINT23 |
[23] |
0 = 未产生中断 1 = 产生中断 |
0 |
… |
… |
… |
… |
EINT4 |
[4] |
0 = 未产生中断 1 = 产生中断 |
0 |
保留位 |
[3:0] |
无 |
0000 |
(2)EINTMASK外部中断屏蔽寄存器
表3-17外部中断屏蔽寄存器(EINTMASK)
寄存器名 |
地址 |
是否读写 |
描述 |
复位默认值 |
EINTMASK |
0x560000A4 |
R/W |
外部中断信号屏蔽寄存器 0:未屏蔽,中断可用 1:屏蔽中断信号 |
0x000FFFFF |
EINTMASK |
位 |
描述 |
初始值 |
EINT23 |
[23] |
0 = 未屏蔽1 = 屏蔽中断 |
1 |
… |
… |
… |
… |
EINT4 |
[4] |
0 = 未屏蔽1 = 屏蔽中断 |
1 |
保留位 |
[3:0] |
无 |
1111 |
4. 外部中断源的产生
外部中断产生过程读者可以根据上面中断图自行分析。
按键控制LED灯实验
本实验分三个版本,分别针对三种开发板:友善之臂QQ2440,友善之臂MINI2440,天嵌TQ2440。每种开发板对应工程在:“sys_irq_开发板名”目录下。下面实验内容为针对MINI2440开发板。
head.s:
主要实现安装异常向量表,处理复位异常,初始化必要硬件,中断入口处理等功能。
1 ;********************************************************************** 2 3 ; 系统中断实验(MINI2440) 4 5 ;********************************************************************** 6 7 GPBCON EQU 0x56000010 8 9 GPBDAT EQU 0x56000014 10 11 EXPORT SYS_IRQ 12 13 AREA SYS_IRQ,CODE,READONLY 14 15 ENTRY 16 17 ;********************************************************************** 18 19 ; 设置中断向量,除Reset和HandleIRQ外,其它异常都没有使用(如果不幸发生了, 20 21 ; 将导致死机) 22 23 ;********************************************************************** 24 25 ; 0x00: 复位Reset异常 26 27 b Reset 28 29 30 31 ; 0x04: 未定义异常(未处理) 32 33 HandleUndef 34 35 b HandleUndef 36 37 38 39 ; 0x08: 软件中断异常(未处理) 40 41 HandleSWI 42 43 b HandleSWI 44 45 46 47 ; 0x0c: 指令预取异常(未处理) 48 49 HandlePrefetchAbt 50 51 b HandlePrefetchAbt 52 53 54 55 ; 0x10: 数据访问中止异常(未处理) 56 57 HandleDataAbt 58 59 b HandleDataAbt 60 61 62 63 ; 0x14: 未使用异常(未处理) 64 65 HandleNotUsed 66 67 b HandleNotUsed 68 69 70 71 ; 0x18: 一般中断异常,跳往HandleIRQ 72 73 b HandleIRQ 74 75 76 77 ; 0x1c: 快速中断异常(未处理) 78 79 HandleFIQ 80 81 b HandleFIQ 82 83 84 85 Reset ; 复位异常处理入口 86 87 ; 关闭看门狗 88 89 ldr r0, = 0x53000000 90 91 mov r1, #0 92 93 str r1, [r0] 94 95 bl initmem 96 97 ldr sp, =0x32000000 ; 设置管理模式栈指针 98 99 IMPORT uart_init 100 101 bl uart_init ; UART串口初始化 102 103 IMPORT irq_init 104 105 bl irq_init ; 系统中断初始化 106 107 IMPORT key_init 108 109 bl key_init ; 按键初始化 110 111 IMPORT led_init 112 113 bl led_init ; LED灯初始化 114 115 msr cpsr_cxsf, #0xd2 ; 切换到中断模式下 116 117 ldr sp, =0x31000000 ; 设置中断模式栈指针 118 119 120 121 msr cpsr_cxsf, #0x13 ; 返回管理模式 122 123 124 125 ldr lr, =halt_loop ; 设置管理模式下返回地址 126 127 IMPORT main 128 129 ldr pc, =main ; 跳入主函数main里执行 130 131 ;*********************************************************************** 132 133 ; 中断处理 134 135 ;*********************************************************************** 136 137 HandleIRQ 138 139 sub lr,lr,#4 ; 修正返回地址 140 141 stmdb sp!,{r0-r12,lr} ; 保存程序执行现场 142 143 ldr lr,=int_return ; 设置中断处理程序返回地址 144 145 IMPORT handle_irq 146 147 ldr pc,=handle_irq ; 跳入中断处理程序 148 149 150 151 int_return ; 中断处理返回标签 152 153 ldmia sp!,{r0-r12,pc}^ 恢复程序执行现场,返回继续执行 154 155 156 157 halt_loop 158 159 b halt_loop 160 161 initmem 162 163 ldr r0, =0x48000000 164 165 ldr r1, =0x48000034 166 167 ;ldr r2, =memdata 168 169 adr r2, memdata 170 171 initmemloop 172 173 ldr r3, [r2], #4 174 175 str r3, [r0], #4 176 177 teq r0, r1 178 179 bne initmemloop 180 181 mov pc,lr 182 183 184 185 memdata 186 187 DCD 0x22000000 ;BWSCON 188 189 DCD 0x00000700 ;BANKCON0 190 191 DCD 0x00000700 ;BANKCON1 192 193 DCD 0x00000700 ;BANKCON2 194 195 DCD 0x00000700 ;BANKCON3 196 197 DCD 0x00000700 ;BANKCON4 198 199 DCD 0x00000700 ;BANKCON5 200 201 DCD 0x00018005 ;BANKCON6 202 203 DCD 0x00018005 ;BANKCON7 204 205 DCD 0x008e07a3 ;REFRESH 206 207 DCD 0x000000b1 ;BANKSIZE 208 209 DCD 0x00000030 ;MRSRB6 210 211 DCD 0x00000030 ;MRSRB7 212 213 END ; 代码结束
该程序主要设置异常向量表,除了Reset异常和中断处理被处理以外,其它异常都未被处理,如果发生时,会产生死循环,Reset异常里主要实现了 硬件的基本初始化,如:按键,LED灯等,设置栈指针,用于执行C程序,最后跳入C程序的main函数。在中断处理异常处理中首先修正返回地址,保存用户 执行现场,跳入到中断处理例程中执行。
sys_init.c:
硬件初始化文件,里面包含LED,KEY的初始化函数。
1 #include "register.h" 2 3 #include "comm_fun.h" 4 5 6 7 #define TXD0READY (1<<2) //发送数据状态OK 8 9 #define RXD0READY (1) //接收数据状态OK 10 11 12 13 /* UART串口初始化 */ 14 15 void uart_init() 16 17 { 18 19 GPHCON |= 0xa0; //GPH2,GPH3 used as TXD0,RXD0 20 21 GPHUP = 0x0; //GPH2,GPH3内部上拉 22 23 ULCON0 = 0x03; //8N1 24 25 UCON0 = 0x05; //查询方式为轮询或中断;时钟选择为PCLK 26 27 UFCON0 = 0x00; //不使用FIFO 28 29 UMCON0 = 0x00; //不使用流控 30 31 UBRDIV0 = 12; //波特率为57600,PCLK=12Mhz 32 33 } 34 35 36 37 /* UART串口单个字符打印函数 */ 38 39 extern void putc(unsigned char c) 40 41 { 42 43 while( ! (UTRSTAT0 & TXD0READY) ); 44 45 UTXH0 = c; 46 47 } 48 49 50 51 /* UART串口接受单个字符函数 */ 52 53 extern unsigned char getc(void) 54 55 { 56 57 while( ! (UTRSTAT0 & RXD0READY) ); 58 59 return URXH0; 60 61 } 62 63 64 65 /* UART串口字符串打印函数 */ 66 67 extern int printk(const char* str) 68 69 { 70 71 int i = 0; 72 73 while( str[i] ){ 74 75 utc( (unsigned char) str[i++] ); 76 77 } 78 79 return i; 80 81 } 82 83 84 85 /* 按键初始化 */ 86 87 int key_init() 88 89 { 90 91 // 设置K1,K2,K3,K4,K5,K6对应控制寄存器为中断模式 92 93 GPGCON = (2<<0) | (2<<6) | (2<<10) | (2<<12) | (2<<14) | (2<<22); 94 95 /* 96 97 01x falling edge triggered下降沿触发 98 99 10x Rising edge triggered上升沿触发 100 101 11x Both edge triggered双沿触发 102 103 */ 104 105 // 设置K1,K2,K3,K4,K5按键中断触发方式为上升沿触发 106 107 EXTINT1 = (3<<0) | (3<<12) | (3<<20) | (3<<24) | (3<<28); 108 109 EXTINT2 = (3<<12); // 设置K6按键中断触发方式为上升沿触 110 111 printk("按键初始化OK/r/n"); 112 113 return 0; 114 115 } 116 117 118 119 /* Led1~Led4初始化*/ 120 121 #define LED1 (1<<5) //LED1 GPBDAT[5] 122 123 #define LED2 (1<<6) //LED2 GPBDAT[6] 124 125 #define LED3 (1<<7) //LED3 GPBDAT[7] 126 127 #define LED4 (1<<8) //LED4 GPBDAT[8] 128 129 130 131 /* 点亮对应num号led灯 */ 132 133 extern int led_on(int num) 134 135 { 136 137 switch(num) 138 139 { 140 141 case 1: 142 143 GPBDAT = GPBDAT & ~LED1; break; 144 145 case 2: 146 147 GPBDAT = GPBDAT & ~LED2; break; 148 149 case 3: 150 151 GPBDAT = GPBDAT & ~LED3; break; 152 153 case 4: 154 155 GPBDAT = GPBDAT & ~LED4; break; 156 157 default: 158 159 return 0; 160 161 } 162 163 return num; 164 165 } 166 167 168 169 /* 关闭num号led灯 */ 170 171 extern int led_off(int num) 172 173 { 174 175 switch(num) 176 177 { 178 179 case 1: 180 181 GPBDAT = GPBDAT | LED1; break; 182 183 case 2: 184 185 GPBDAT = GPBDAT | LED2; break; 186 187 case 3: 188 189 GPBDAT = GPBDAT | LED3; break; 190 191 case 4: 192 193 GPBDAT = GPBDAT | LED4; break; 194 195 default: 196 197 return 0; 198 199 } 200 201 return num; 202 203 } 204 205 206 207 /* 关闭全部led灯 */ 208 209 extern int all_led_off(void) 210 211 { 212 213 GPBDAT = GPBDAT | LED1 | LED2 | LED3 | LED4; 214 215 return 0; 216 217 } 218 219 220 221 /* led灯初始化 */ 222 223 int led_init(void) 224 225 { 226 227 GPBCON = 0x15400; //设置GPB7为输出口 228 229 all_led_off(); 230 231 printk("led初始化OK/r/n"); 232 233 return 0; 234 235 } 236 237 238 239 /* 中断初始化 */ 240 241 void irq_init(void) 242 243 { 244 245 // 打开KEY1~KEY6的屏蔽位 246 247 INTMSK &= ~(1<<5); 248 249 EINTMASK &= ~((1<<8) | (1<<11) | (1<<13) | (1<<14) | (1<<15) | (1<<19)); 250 251 printk("中断初始化OK/r/n"); 252 253 }
该文件是相关硬件初始化程序,主要包含了看门狗驱动,按键驱动,系统中断驱动,LED驱动。
handle_irq.c:
中断处理函数,查出中断源,中断处理,清除中断源。
1 #include "register.h" 2 3 #include "comm_fun.h" 4 5 6 7 #define EINT_Key_REQUEST 5 // Key中断源中断号(6个按键全部使用外部子中断) 8 9 #define K1_EINT_BIT (1<<8) // K1外部子中断位 10 11 #define K2_EINT_BIT (1<<11) // K2外部子中断位 12 13 #define K3_EINT_BIT (1<<13) // K3外部子中断位 14 15 #define K4_EINT_BIT (1<<14) // K4外部子中断位 16 17 #define K5_EINT_BIT (1<<15) // K5外部子中断位 18 19 #define K6_EINT_BIT (1<<19) // K6外部子中断位 20 21 /* 系统中断处理函数 */ 22 23 void handle_irq() 24 25 { 26 27 unsigned long irqOffSet = INTOFFSET; // 取得中断号 28 29 all_led_off(); // 关闭全部Led灯 30 31 if(EINT_Key_REQUEST==irqOffSet){ // Key中断产生(6个按键使用一个总中断号) 32 33 if(K1_EINT_BIT & EINTPEND){ 34 35 led_on(1); // 点亮Led1 36 37 printk("Key1 pressed/r/n"); 38 39 EINTPEND &= K1_EINT_BIT; // 清除外部子中断源 40 41 }else if(K2_EINT_BIT & EINTPEND){ 42 43 led_on(2); // 点亮Led2 44 45 printk("Key2 pressed/r/n"); 46 47 EINTPEND &= K2_EINT_BIT; // 清除外部子中断源 48 49 }else if(K3_EINT_BIT & EINTPEND){ 50 51 led_on(3); // 点亮Led3 52 53 printk("Key3 pressed/r/n"); 54 55 EINTPEND &= K3_EINT_BIT; // 清除外部子中断源 56 57 }else if(K4_EINT_BIT & EINTPEND){ 58 59 led_on(4); // 点亮Led4 60 61 printk("Key4 pressed/r/n"); 62 63 EINTPEND &= K4_EINT_BIT; // 清除外部子中断源 64 65 }else if(K5_EINT_BIT & EINTPEND){ 66 67 all_led_off(1); // 熄灭全部Led 68 69 printk("Key5 pressed/r/n"); 70 71 EINTPEND &= K5_EINT_BIT; // 清除外部子中断源 72 73 }else if(K6_EINT_BIT & EINTPEND){ 74 75 all_led_on(); // 点亮全部Led 76 77 printk("Key6 pressed/r/n"); 78 79 EINTPEND &= K6_EINT_BIT; // 清除外部子中断源 80 81 } 82 83 } 84 85 SRCPND &= (1<<irqOffSet); // 清除中断源 86 87 INTPND = INTPND; // 清除中断结果 88 89 }
main.c:
包含主函数和延时函数,主要实现字符串的循环打印。
1 #include "register.h" 2 3 #include "comm_fun.h" 4 5 6 7 /* 延时 */ 8 9 void delay(int msec) 10 11 { 12 13 int i, j; 14 15 for(i = 1000; i > 0; i--) 16 17 for(j = msec*10; j > 0; j--) 18 19 /* do nothing */; 20 21 } 22 23 24 25 /* 主函数 */ 26 27 int main() 28 29 { 30 31 while(1) 32 33 { 34 35 printk("main函数在运行.../r/n"); 36 37 delay(5); //delay 38 39 } 40 41 return 0; 42 43 }
完。
本文来自博客园,作者:摩斯电码,未经同意,禁止转载