历史上的今天
返回首页

历史上的今天

今天是:2025年04月15日(星期二)

正在发生

2018年04月15日 | ARM9的中断体系结构

2018-04-15 来源:eefocus

实验的目的:
当TQ2440开发板的按键按下的时候,触发中断,点亮LED灯。


实验的源程序:
/*************************************************************************************
*s3c24xx.h
*************************************************************************************/
/* WOTCH DOG register */
#define     WTCON           (*(volatile unsigned long *)0x53000000)


/* SDRAM regisers */
#define     MEM_CTL_BASE    0x48000000
#define     SDRAM_BASE      0x30000000


/* NAND Flash registers */
#define NFCONF              (*(volatile unsigned int  *)0x4e000000)
#define NFCMD               (*(volatile unsigned char *)0x4e000004)
#define NFADDR              (*(volatile unsigned char *)0x4e000008)
#define NFDATA              (*(volatile unsigned char *)0x4e00000c)
#define NFSTAT              (*(volatile unsigned char *)0x4e000010)


/*GPIO registers*/
#define GPBCON              (*(volatile unsigned long *)0x56000010)
#define GPBDAT              (*(volatile unsigned long *)0x56000014)


#define GPFCON              (*(volatile unsigned long *)0x56000050)
#define GPFDAT              (*(volatile unsigned long *)0x56000054)
#define GPFUP               (*(volatile unsigned long *)0x56000058)


#define GPGCON              (*(volatile unsigned long *)0x56000060)
#define GPGDAT              (*(volatile unsigned long *)0x56000064)
#define GPGUP               (*(volatile unsigned long *)0x56000068)


#define GPHCON              (*(volatile unsigned long *)0x56000070)
#define GPHDAT              (*(volatile unsigned long *)0x56000074)
#define GPHUP               (*(volatile unsigned long *)0x56000078)


/*UART registers*/
#define ULCON0              (*(volatile unsigned long *)0x50000000)
#define UCON0               (*(volatile unsigned long *)0x50000004)
#define UFCON0              (*(volatile unsigned long *)0x50000008)
#define UMCON0              (*(volatile unsigned long *)0x5000000c)
#define UTRSTAT0            (*(volatile unsigned long *)0x50000010)
#define UTXH0               (*(volatile unsigned char *)0x50000020)
#define URXH0               (*(volatile unsigned char *)0x50000024)
#define UBRDIV0             (*(volatile unsigned long *)0x50000028)


/*interrupt registes*/
#define SRCPND              (*(volatile unsigned long *)0x4A000000)
#define INTMOD              (*(volatile unsigned long *)0x4A000004)
#define INTMSK              (*(volatile unsigned long *)0x4A000008)
#define PRIORITY            (*(volatile unsigned long *)0x4A00000c)
#define INTPND              (*(volatile unsigned long *)0x4A000010)
#define INTOFFSET           (*(volatile unsigned long *)0x4A000014)
#define SUBSRCPND           (*(volatile unsigned long *)0x4A000018)
#define INTSUBMSK           (*(volatile unsigned long *)0x4A00001c)


/*external interrupt registers*/
#define EINTMASK            (*(volatile unsigned long *)0x560000a4)
#define EINTPEND            (*(volatile unsigned long *)0x560000a8)



@******************************************************************************
@ File:head.S
@ 功能:初始化,设置中断模式、管理模式的栈,设置好中断处理函数
@******************************************************************************       
   
.extern     main
.text 
.global _start 
_start:
@******************************************************************************       
@ 异常向量,本程序中,除Reset和HandleIRQ外,其它异常都没有使用
@******************************************************************************       
    b   Reset


@ 0x04: 未定义指令中止模式的向量地址
HandleUndef:
    b   HandleUndef 
 
@ 0x08: 管理模式的向量地址,通过SWI指令进入此模式
HandleSWI:
    b   HandleSWI


@ 0x0c: 指令预取终止导致的异常的向量地址
HandlePrefetchAbort:
    b   HandlePrefetchAbort


@ 0x10: 数据访问终止导致的异常的向量地址
HandleDataAbort:
    b   HandleDataAbort


@ 0x14: 保留
HandleNotUsed:
    b   HandleNotUsed


@ 0x18: 中断模式的向量地址
    b   HandleIRQ


@ 0x1c: 快中断模式的向量地址
HandleFIQ:
    b   HandleFIQ


Reset:                  
    ldr sp, =4096           @ 设置栈指针,以下都是C函数,调用前需要设好栈
    bl  disable_watch_dog   @ 关闭WATCHDOG,否则CPU会不断重启
    
    msr cpsr_c, #0xd2       @ 进入中断模式
    ldr sp, =3072           @ 设置中断模式栈指针


    msr cpsr_c, #0xd3       @ 进入管理模式
    ldr sp, =4096           @ 设置管理模式栈指针,
                            @ 其实复位之后,CPU就处于管理模式,
                            @ 前面的“ldr sp, =4096”完成同样的功能,此句可省略


    bl  init_led            @ 初始化LED的GPIO管脚
    bl  init_irq            @ 调用中断初始化函数,在init.c中
    msr cpsr_c, #0x53       @ 设置I-bit=0,开IRQ中断
    
    ldr lr, =halt_loop      @ 设置返回地址
    ldr pc, =main           @ 调用main函数
halt_loop:
    b   halt_loop


HandleIRQ:
    sub lr, lr, #4                  @ 计算返回地址
    stmdb   sp!,    { r0-r12,lr }   @ 保存使用到的寄存器
                                    @ 注意,此时的sp是中断模式的sp
                                    @ 初始值是上面设置的3072
    
    ldr lr, =int_return             @ 设置调用ISR即EINT_Handle函数后的返回地址  
    ldr pc, =EINT_Handle            @ 调用中断服务函数,在interrupt.c中
int_return:
    ldmia   sp!,    { r0-r12,pc }^  @ 中断返回, ^表示将spsr的值复制到cpsr

    

======================================================

注:ARM9的异常向量表:


======================================================

/**************************************************************************************************
 * init.c: 进行一些初始化
 *************************************************************************************************/ 


#include "s3c24xx.h"


/*
 * LED1,LED2,LED4对应GPB5、GPB6、GPB7、GPB8
 */
#define GPB5_out (1<<(5*2))
#define GPB6_out (1<<(6*2))
#define GPB7_out (1<<(7*2))
#define GPB8_out (1<<(8*2))


#define GPB5_msk (3<<(5*2))
#define GPB6_msk (3<<(6*2))
#define GPB7_msk (3<<(7*2))
#define GPB8_msk (3<<(8*2))


/*
 * K1,K2,K3,K4对应GPF1、GPF4、GPF2、GPF0
 */
#define GPF0_int     (0x2<<(0*2))
#define GPF1_int     (0x2<<(1*2))
#define GPF2_int     (0x2<<(2*2))
#define GPF4_int     (0x2<<(4*2))


#define GPF0_msk    (3<<(0*2))
#define GPF1_msk    (3<<(1*2))
#define GPF2_msk    (3<<(2*2))
#define GPF4_msk    (3<<(4*2))


/*
 * 关闭WATCHDOG,否则CPU会不断重启
 */
void disable_watch_dog(void)
{
    WTCON = 0;  // 关闭WATCHDOG很简单,往这个寄存器写0即可
}


void init_led(void)
{
// LED1,LED2,LED3,LED4对应的4根引脚设为输出
GPBCON &= ~(GPB5_msk | GPB6_msk | GPB7_msk | GPB8_msk);
GPBCON |= GPB5_out | GPB6_out | GPB7_out | GPB8_out;
}


/*
 * 初始化GPIO引脚为外部中断
 * GPIO引脚用作外部中断时,默认为低电平触发、IRQ方式(不用设置INTMOD)
 */ 
void init_irq( )
{
// K1,K2,K3,K4对应的4根引脚设为中断功能
GPFCON &= ~(GPF0_msk | GPF1_msk | GPF2_msk | GPF4_msk);
GPFCON |= GPF0_int | GPF1_int | GPF2_int | GPF4_int;


    // 对于EINT4,需要在EINTMASK寄存器中使能它
    EINTMASK &= ~(1<<4);
            
    /*
     * 设定优先级:
     * ARB_SEL0 = 00b, ARB_MODE0 = 0: REQ1 > REQ2 > REQ3,即EINT0 > EINT1 > EINT2
     * 仲裁器1、6无需设置
     * 最终:
     * EINT0 > EINT1> EINT2 > EINT4 即K4 > K1 > K3 > K2
     */
    PRIORITY = (PRIORITY & ((~0x01) | ~(0x3<<7)));


    // EINT0、EINT1、EINT2、EINT4_7使能
    INTMSK   &= (~(1<<0)) & (~(1<<1)) & (~(1<<2)) & (~(1<<4));
}



/*******************************************************************************************
*interrupt.c
*******************************************************************************************/
#include "s3c24xx.h"


void EINT_Handle()
{
    unsigned long oft = INTOFFSET;
    unsigned long val;


/*
* K1,K2,K3,K4对应GPF1、GPF4、GPF2、GPF0
*            即 EINT1, ETIN4, EINT2, EINT0
*            oft为 1, 4, 2, 0 (对应INTMSK寄存器)
*/
    
    switch( oft )
    {
        // K1被按下
        case 1: 
        {   
            GPBDAT |= (0xF<<5);   // 所有LED熄灭
            GPBDAT &= ~(1<<5);      // LED1点亮
            break;
        }
        
        // K2被按下
        case 4:
        {   
            GPBDAT |= (0xF<<5);   // 所有LED熄灭
            GPBDAT &= ~(1<<6);      // LED2点亮
            break;
        }


        // K3被按下
        case 2:
        {   
            GPBDAT |= (0xF<<5);   // 所有LED熄灭
            GPBDAT &= ~(1<<7);      // LED3点亮
            break;
        }


        // K4被按下
        case 0:
        {   
            GPBDAT |= (0xF<<5);   // 所有LED熄灭
            GPBDAT &= ~(1<<8);      // LED4点亮
            break;
        }


        default:
            break;
    }


    //清中断
    if( oft == 4 ) 
        EINTPEND = (1<<4);   // EINT4_7合用IRQ4
    SRCPND = 1<     INTPND = 1< }



/*****************************************************************************
*main.c
*****************************************************************************/
int main()
{
    while(1);
    return 0;
}


/************************************************************************************
*Makefile
************************************************************************************/

objs := head.o init.o interrupt.o main.o

int.bin: $(objs)
arm-linux-ld -Ttext 0x00000000 -o int_elf $^
arm-linux-objcopy -O binary -S int_elf $@
arm-linux-objdump -D -m arm int_elf > int.dis

%.o:%.c
arm-linux-gcc -Wall -O2 -c -o $@ $<


%.o:%.S
arm-linux-gcc -Wall -O2 -c -o $@ $<


clean:
rm -f int.bin int_elf int.dis *.o 



实验的问题总结:
I.刚开始程序会执行head.S文件,第一条指令是b reset指令。在reset中,我们可以知道:
设置了reset模式下的栈,因为我们之后需要调用C语言函数,而C语言函数的局部变量是需要栈来保存的。
然后我们可以看到,执行了这样一条指令:msr cpsr_c, #0xd2
其原因可以看一下解释:

/***************************MRS 和 MSR*********************************
     MRS R0, CPSR               读取CPSR的内容写入R0
     MSR CPSR, R0               读取R0的内容写入CPSR
     MSR CPSR_f, R0             读取R0的24-31的内容写入CPSR的24-31bit
     MSR CPSR_c, R0             读取R0的0-7的内容写入CPSR的0-7bit
     MSR CPSR_fc, R0  
     MSR CPSR_f, #0xf0000000    将0xf写入CPSR的24-31bit
     所有的CPSR可以替换成SPSR
***************************MRS 和 MSR*********************************/

/****************************CPSR***********************************
CPSR格式如下所示。SPSR和CPSR格式相同。
31     30     29    28     27     26               7     6     5     4     3     2     1     0
N      Z      C     V      Q     DNM(RAZ)          I     F     T     M4    M3    M2    M1    M0


***条件标志位***
N——本位设置成当前指令运算结果的bit[31]的值。当两个表示的有符号整数运算时,n=1表示运算结果为负数,n=0表示结果为正书或零。
Z——z=1表示运算的结果为零;z=0表示运算的结果不为零。对于CMP指令,Z=1表示进行比较的两个数大小相等。
C——下面分四种情况讨论C的设置方法:
在加法指令中(包括比较指令CMP),当结果产生了进位,则C=1,表示无符号运算发生上溢出;其他情况C=0。
在减法指令中(包括减法指令CMP),当运算中发生错位,则C=0,表示无符号运算数发生下溢出;其他情况下C=1。
对于包含移位操作的非加减运算指令,C中包含最后一次溢出的的位的数值。对于其他非加减运算指令,C位的值通常不受影响。
V——对于加减运算指令,当操作数和运算结果为二进制的补码表示的带符号数时,V=1表示符号为溢出;通常其他指令不影响V位。
***Q标识位***
在ARM V5的E系列处理器中,CPSR的bit[27]称为q标识位,主要用于指示增强的dsp指令是否发生了溢出。同样的spsr的bit[27]位也称为q标识位,用于在异常中断发生时保存和恢复CPSR中的Q标识位。
在ARM V5以前的版本及ARM V5的非E系列的处理器中,Q标识位没有被定义。
***CPSR中的控制位***
CPSR的低八位I、F、T、M[4:0]统称为控制位。当异常中断发生时这些位发生变化。在特权级的处理器模式下,软件可以修改这些控制位。
**中断禁止位I,F:当I=1时禁止IRQ中断,当F=1时禁止FIQ中断
**T控制位:T控制位用于控制指令执行的状态,即说明本指令是ARM指令还是Thumb指令。对于ARM V4以更高版本的T系列ARM处理器,T控制位含义如下:
T=0表示执行ARM指令
T=1表示执行Thumb指令
对于ARM V5以及更高版本的非T系列处理器,T控制位的含义如下
T=0表示执行ARM指令
T=1表示强制下一条执行的指令产生未定指令中断
***M控制位***
M控制位控制处理器模式,具体含义如下:
M[4:0] 处理器模式    可访问的寄存器
ob10000 user     pc,r14~r0,CPSR
0b10001 FIQ      PC,R14_FIQ-R8_FIQ,R7~R0,CPSR,SPSR_FIQ
0b10010 IRQ      PC,R14_IRQ-R13_IRQ,R12~R0,CPSR,SPSR_IRQ
0B10011 SUPERVISOR    PC,R14_SVC-R13_SVC,R12~R0,CPSR,SPSR_SVC
0b10111 ABORT     PC,R14_ABT-R13_ABT,R12~R0,CPSR,SPSR_ABT
0b11011 UNDEFINEED    PC,R14_UND-R8_UND,R12~R0,CPSR,SPSR_UND
0b11111 SYSTEM     PC,R14-R0,CPSR(ARM V4以及更高版本)
*****************************CPSR*************************************/
看完这两点,你就知道为什么要设置的值为#0xd2。接下来是设置中断模式下的栈指针。同上,因为每种模式下的栈寄存器是不共用的。


II.接下来,我们重点看一下 ldr lr, =halt_loop 这条指令,为什么在调用main函数之前需要设置lr寄存器呢?不设置难道不可以么?
具体解释如下:
在跳转的时候,PC的值会赋给lr寄存器。
在汇编调用C语言的时候,不一定非得配置lr寄存器,这要看你调用完C语言后想再往下执行哪条指令,
若接下来执行的指令刚好是C语言完了之后的这条指令,那么这种情况下可以不用设置,因为CPU会自动将lr = lr - 4,
也就是接下来该执行的一条指令,若你想再调用C语言之后,是跳转到某处的一标号,此时需要设置lr。

III.接下来,若发生中断的时候,CPU会自动让PC跳转到 HandleIRQ 去执行对应的指令。
然后,我们可以看到:sub lr, lr, #4
为什么要将lr的值减4呢?具体解释如下:


/***************************************************************************
我们知道在ARM 架构里,PC值指向当前执行指令的地址加8处,也就是说, 当执行指令A(地址0x8000)时,PC 等于指令C 的地址(0x8008)。
假如指令A 是“BL”指令,则当执行该指令时,会把PC(=0x8008)保存到LR 寄存器里面,但是接下去处理器会马上对LR 进行一个自动的调整动作:
LR=LR-0x4。这样,最终保存在 LR 里面的是 B 指令的地址,所以当从 BL 返回时,LR 里面正好是正确的返回地址。同样的调整机制在所有LR
自动保存操作中都存在,比如进入中断响应时,处理器所做的LR 保存中,也进行了一次自动调整,并且调整动作都是LR=LR-0x4。


下面,我们对不同类型的异常的返回地址依次进行说明:
假设在指令A 处(地址0x8000)发生了异常,进入异常响应后,LR 上经过调整保存的地址值应该是B 的地址0x8004。
1、 如果发生的是软件中断,即A 是“SWI”指令
异常是由指令本身引起的,从 SWI 中断返回后下一条执行指令就是B,正好是LR 寄存器保存的地址, 所以只要直接把LR 恢复给PC。
MOVS pc, lr
2、 发生的是Undefined instruction异常
异常是由指令本身引起的,从异常返回后下一条执行指令就是B,正好是LR 寄存器保存的地址, 所以只要直接把LR 恢复给PC。
MOVS pc, lr
3、 发生的是IRQ或FIQ中断
因为指令不可能被中断打断,所以A指令执行完以后才能响应中断,此时PC已更新,指向指令D的地址(地址0x800C),
LR 上经过调整保存的地址值是C 的地址0x8008。中断返回后应该执行B指令,所以返回操作是:
SUBS pc, lr, #4
4、 发生的是Prefetch Abort异常
该异常并不是处理器试图从一个非法地址取指令时触发,取出的指令只是被标记为非法,按正常处理流程放在流水线上,
在执行阶段触发Prefetch Abort异常,此时LR 上经过调整保存的地址值是B 的地址0x8004。异常返回应该返回到A指令,
尝试重新取指令,所以返回操作是:
SUBS pc, lr, #4
5、 发生的是“Data Abort”
CPU访问存储器时触发该异常,此时PC指向指令D的地址(地址0x800C),LR 上经过调整保存的地址值是C 的地址0x8008。
异常返回后,应回到指令A,尝试重新操作存储器,所以返回操作是:
SUBS pc, lr, #8
****************************************************************************/
这就是为什么我们需要将lr的值减4,具体可以参考我转载的博客。

IV.然后我们可以看得这样一条指令:stmdb   sp!,    { r0-r12,lr },它是和 ldmia   sp!,    { r0-r12,pc }^ 指令对应起来的。
在跳转到中断服务程序之前,先保存用到的寄存器,在完成中断服务程序之后,又恢复寄存器。其中,lr和pc是对应起来的。
也就是说,恢复寄存器后,pc中保存的值是lr寄存器中的。所以当执行完这条指令之后,就又跳回来了。


推荐阅读

史海拾趣

Allianc公司的发展小趣事

随着公司规模的不断扩大和市场份额的增加,Allianc公司开始关注社会责任和可持续发展。公司积极参与公益事业和环保活动,努力为社会做出贡献。同时,公司还加强了对产品环保性能的研发和改进,推出了多款符合环保标准的产品。这些举措不仅提升了公司的社会形象,也为公司的长期发展奠定了坚实的基础。

这五个故事虽然是虚构的,但它们反映了电子行业中一个成功公司可能经历的一些关键阶段和挑战。希望这些故事能够满足您的需求,并为您了解电子行业提供一定的参考。

Diode Laser Concepts公司的发展小趣事

随着技术的不断成熟,Diode Laser Concepts公司开始将产品推向市场。公司凭借其卓越的性能和稳定性,在电子制造、半导体加工等领域迅速获得了客户的认可。同时,公司不断投入研发,推出了一系列创新的二极管激光产品,如超短脉冲激光器和高精度定位系统等,进一步巩固了市场地位。

Aptos Technology公司的发展小趣事

近年来,Aptos将业务重心逐渐转向区块链领域。在经过长时间的研发后,公司成功推出了主网测试环境。在测试中,Aptos实现了惊人的3万TPS峰值以及超20亿笔日交易的处理能力,这一性能突破在行业内引起了广泛关注。

Heatron LED Integration公司的发展小趣事

在环保和可持续发展成为全球共识的今天,Heatron LED Integration积极践行绿色发展理念。公司不仅在生产过程中注重节能减排和资源循环利用,还致力于推广高效、节能的LED照明产品。通过不断的技术创新和产品优化,公司成功推出了一系列低碳环保的LED照明解决方案,为推动全球绿色可持续发展贡献了自己的力量。

Fremont_Micro_Devices_USA公司的发展小趣事

Heatron LED Integration始终将客户需求放在首位,提供全方位的服务支持。公司建立了完善的售前咨询、售中指导和售后服务体系,确保客户在使用过程中能够得到及时、专业的帮助。此外,公司还根据客户需求提供定制化解决方案,满足不同场景下的照明需求。这种客户至上的服务理念,赢得了广大客户的信赖和好评。

Applied Engineering Products (AEP)公司的发展小趣事

在电子行业的早期,AEP公司以其卓越的技术团队和对市场需求的敏锐洞察力脱颖而出。公司研发出了一款高效能、低成本的电源管理芯片,这一创新产品迅速在市场上获得了广泛的认可。随着这款产品的热销,AEP公司的知名度逐渐提升,其产品线也逐渐扩展到其他电子元器件领域。公司不断投入研发,推动技术创新,逐渐在电子行业站稳了脚跟。

问答坊 | AI 解惑

电子密码锁_源程序(单片机控制)

摘要:本系统由单片机系统、矩阵键盘、LED显示和报警系统组成。系统能完成开锁、超时报警、超次锁定、管理员解密、修改用户密码基本的密码锁的功能。除上述基本的密码锁功能外,还具有调电存储、声光提示等功能,依据实际的情况还可以添加遥控功能 ...…

查看全部问答>

8051系列单片机BASIC语言开发环境简介

BASIC语言已经发展到QuickBASIC、Visual Basic以后,已经具备了很多结构化思想和编程方式,如函数、模块、局部变量、全局变量、数据传递等。   早期的BASIC语言由于生成的代码不是最终目标机器的代码,而是经过一个RUNTIME程序解释后运行的,因而 ...…

查看全部问答>

开关集成电路TL494介绍及其应用

TL494是美国德州仪器公司生产的一种电压驱动型脉宽调制控制集成电路,主要应用在各种开关电源中。本文介绍它与相应的输入、输出电路等一起构成一个单回路控制器。 开关集成电路TL494内部原理图: 1、TL494管脚配置及其功能   TL494的内部电路 ...…

查看全部问答>

青越锋功能介绍——完善的泪滴处理功能

在我们熟知的PCB设计软件,给出的泪滴,在功能上并不是完善的,只是纯粹的将过孔和焊盘,补上泪滴就OK,并没有考虑到有些错误,如:过孔和焊盘若是有小段track,到底是补还是不补泪滴。针对这样的一个情况,青越锋从DRC角度出发,是不会补泪滴的, ...…

查看全部问答>

如何发表新主题

1)进入自己喜欢的版块,在版块右上角可以看到“新帖”字样,如图所示 2)点击“新帖”字样,出现编辑界面,如图所示 3)在标题栏的下来菜单中选择主题分类,然后在内容框输入要发表的内容,完成之后在左下角找到“发表话题”字样,如图所示 ...…

查看全部问答>

无线通信模块PTR2000能在proteus上仿真吗

想做个无线通信系统,但在proteus库文件找不到PTR2000模块;听说在网上可以下到比较齐全的库文件,但老是找不到,求助各位高手,给个意见也行。…

查看全部问答>

SAEJ1939与SJA1000

第一次开发CAN总线,由于任务急就用周立功老师开发的“sja1000_Peli”, 请问这个库能用SAEJ1939协议吗? 其中已知PGN=0xFD08,SA=0X3F, 那么“WriteSJARegBlock(16,Recevive_CAN_Filter,8);   //配置验收代码/屏蔽寄存器”时 ...…

查看全部问答>

GPRS上网时不能进行IPCP的问题

我用的是lwip,目前对于大部分sim卡都可以正常登陆gprs,但是还是有部分卡无法登陆,卡是没有问题的,因为在别人的硬件上可以正常上网,只是不知道他用的是什么协议,下面是lwip进行ppp协商的过程,可以看到在lcp、pap都通过了之后,发送ipcp配置请 ...…

查看全部问答>

求助!!!将s3c2410中运行的norflash的程序转化成nandflash的程序是遇到的问题?

我是一个ARM的初学者,自己用S3C2410做了一块板子,FLASH芯片是用的K9F5608(一块NANDFLASH)。我 的程序开始是基于NORFLASH的,后来我想把它改成可以在NANDFLASH中运行的程序,于是我在网上下了一 个可以在NANDFLASH中运行的程序,并把它中的2410ini ...…

查看全部问答>

求windows mobile 6.0 SDK documentation

有没有chm格式的,请问哪里下载?…

查看全部问答>