JZ2440 裸机驱动 第12章 I2C接口
本章目标:
了解I2C总线协议;
掌握S3C2410/S3C2440中I2C接口的使用方法;
12.1 I2C总线协议及硬件介绍
12.1.1 I2C总线协议
1 I2C总线的概念
2 I2C总线的信号类型
3 I2C总线的数据传输格式12.1.2 S3C2410/S3C2440 I2C总线控制器
1. S3C2410/S3C2440 I2C总线控制器寄存器介绍
S3C2410/S3C2440的I2C接口有4种工作模式:主机发送、主机接收、从机发送、
从机接收。其内部结构如图12.6所示。
从图12.6可知,S3C2410/S3C2440提供4个寄存器来完成所有的I2C操作。SDA线上的数据
从IICDS寄存器发出,或传入IICDS寄存器中;IICADD寄存器中保存S3C2410/S3C2440当做从
机时的地址;IICCON、IICSTAT 两个寄存器用来控制或标识各种状态,比如选择工作模式,发出
S信号、P信号,决定是否发出ACK信号,检测是否收到ACK信号。各寄存器的用法如下:
(1)IICCON寄存器(Multi-master IIC-bus control)
IICCON寄存器用于控制是否发出ACK信号、设置发送器的时钟、开启IIC中断,并标识中断是
否发生。它的各位含义如下表12.2所示。
使用IICCON寄存器时,有如下注意事项:
① 发送模式的时钟频率由位[6]、位[3:0]联合决定。另外,当IICCON[6] = 0时,IICCON[3:0]
不能去0或1。
② IIC中断在以下3种情况下发生:
当发出地址信息或接收到一个从机地址并且吻合时;
当总线仲裁失败时;
当发送/接收完一个字节的数据(包括响应位)时。
③ 基于SDA 、SCL线上时间特性的考虑,要发送数据时,先将数据写入IICDS寄存器,
然后清除中断。
④ 如果IICCON[5] = 0,IICCON[4]将不能正常工作。所以即使不使用IIC中断,也要将
IICCON[5]设为1。
(2)IICSTAT寄存器(Multi-master IIC-bus control/status)。
IICSTAT寄存器用于选择IIC接口的工作模式,发出S信号、P信号,使能接收/发送功能,
并标识各种状态,比如总线仲裁是否成功、作为从机时是否被寻址、是否接收到0地址、是
否接收到ACK信号等。
IICSTAT寄存器的各位如表12.3所示:
(3)IICADD寄存器(Multi-master IIC-bus address)
用到位[7:1],表示从机地址。在IICSTAT[4]为1时,才可以写入;随时都可以读出。
(4)IICDS寄存器(Multi-master IIC-bus Tx/Rx data shift)
用到位[7:0],其中保存的是要发送或已经接收到的数据。在IICSTAT[4]为1时才可写入,
随时可以读出。
2.S3C2410/S3C2440 I2C总线操作方法
启动或复位S3C2410/S3C2440 的IIC传输有以下两种方法。
(1)当IICCON[4]即中断状态位为0时,通过写IICSTAT寄存器启动IIC操作。有以下两种
情况。
① 在主机模式:
令IICSTAT[5:4]等于0b11,将发出S信号和IICDS寄存器的数据(寻址),
令IICSTAT[5:4]等于0b01,将发出P信号。
② 在从机模式,令IICSTAT[4]等于1,将等待其他主机发出S信号及地址信息。
(2)当IICCON[4]即中断状态位为1时,表示I2C操作被暂停。在这期间设置好其他寄存器
之后,向IICCON[4]写入0即可恢复I2C操作。
所谓“设置其他寄存器”,有以下3种情况。
① 对于主机模式,可以按照上面(1)的方法写IICSTAT寄存器,恢复I2C操作后即可发出
S信号和IICDS寄存器的值(寻址),或发出P信号。
② 对于发送器,可以将下一个要发送的数据写入IICDS寄存器中,恢复I2C操作后即可发
出这个数据。
③ 对于接收器,可以从IICDS寄存器中读出接收到的数据。最后向IICCON[4]写入0的同时,
设置IICCON[7]以决定在接收到下一个数据后是否发出ACK信号。
通过中断服务程序驱动I2C传输。
(1)当仲裁失败时,发生中断——本次传输没有抢到总线,可以稍后继续。
(2)对于主机模式,当发出S信号、地址信息并经过一个SCL周期(对应ACK信号)后,发生
中断——主机可在此时判断是否成功寻址到从机。
(3)对于从机模式,当接收到的地址与IICADD寄存器吻合时,先发出ACK信号,然后发生
中断——从机可在此时准备后续的传输。
(4)对于发送器,当发送完一个数据并经过一个SCL周期(对应ACK信号)后,发生中断。这
时可以准备下一个要发送的数据,或发出P信号以停止传输。
(5)对于接收器,当接收到一个数据时,先根据IICCON[7]决定是否发出ACK信号后,然后
发生中断。这是可以读取IICDS寄存器得到数据,并设置IICCON[7]已决定接收到下一个数据
后是否发出ACK。
对于4种工作模式,S3C2410/S3C2440数据手册中都有它们的操作流程图。现在以主机发
送器为例说明,它的工作流程如图12.7所示,其他的工作模式请参考数据手册。
下面结合I2C寄存器的用法,详细讲解图12.7中各步骤的含义。
(1)配置主机发送器的各类参数
设置GPE15、GPE14引脚用于SDA、SCL,设置IICCON寄存器选择I2C发送时钟,最后,
设置IICSTAT[4]为1,这样,后面才能写IICDS寄存器。
注意:初始时IICCON[4]为0,不能将IICSTAT设为主机模式,否则就会立刻发出S信号、
发送IICDS寄存器里的值。
(2)将要寻址的从机地址写入IICDS寄存器。
(3)将0xF0写入IICSTAT寄存器,即设为主机发送器、使能串行输出功能、发出S信号。
(4)发出S信号后,步骤(2)中设置的IICDS寄存器值也将被发出,它用来寻址从机。
(5)在响应周期之后,发生中断,此时IICCON[4]为1,I2C传输暂停。
(6)如果没有数据要发送,则跳到步骤(10);否则跳到步骤(7)。
(7)将下一个要发送的数据写入IICDS寄存器中。
(8)向IICCON[4]中写入0,恢复I2C传输。
(9)这时,IICDS寄存器中的值将被一位一位地发送出去。当8位数据发送完毕,再经过
另一个SCL周期(对应ACK信号)后,中断再次发生,跳到步骤(5)。
步骤(5)~(9)不断循环知道发出所有数据。当要停止传输时,跳到步骤(10)。
(10)将0xF0写入IICSTAT寄存器,即设为主机发送器、使能串行输出功能、发出P信号。
注意:这时的P信号并没有实际发出,只有清除了IICCON[4]后才会发出P信号。
(11)清除IICCON[4],P信号得以发出。
(12)等待一段时间,使得P信号完全发出。
12.2 I2C总线操作实例
12.2.1 I2C接口RTC芯片M41t11的操作方法
本书所用开发板中,通过I2C总线连接RTC(实时时钟)芯片M4lt11,它使用电池供电,系
统断电时也可以维持日期和时间。S3C2410/S3C2440作为I2C主机向M4lt11发送数据以设
置日期和时间、读取M4lt11以获取日期和时间。连接图如图12.8所示。
M4lt11中有8个寄存器,分别对应秒、分、时、天(星期几)、日、月、年、控制寄存器,
其中的数据都是以BCD格式保存(BCD格式例子:0x15表示数值15),如表12.4所示。
除上表的8个寄存器(地址为0~7)之外,M4lt11内部还有56字节的RAM(地址为8~63)。
访问M4lt11前,先设置寄存器地址,以后每次读写操作完成后,M4lt11内部会自动将寄
存器地址加1.
所以读写M4lt11分以下两个步骤:
(1)主机向M4lt11发出要操作的寄存器起始地址(0~7)。
(2)要设置M4lt11时,主机连续发出数据;读取M4lt11时,主机连续读取数据。
M4lt11的I2C从机地址为0xD0。
12.2.2 程序设计
本实例将在串口上输出一个菜单,可以选择设置时间和日期,或者将它们读出来。将
通过本实例验证I2C主机的发送、接收操作。
12.2.3 设置/读取M4lt11的源码详解
本实例的源码在/work/hardware/i2c目录下。
文件i2c.c封装了S3C2410/S3C2440作为主机发送器、主机接收器的4个操作函数:
i2c_init用于初始化,i2c_write用于发起发送数据,i2c_read用于发起读取数据,
I2CIntHandle是I2C中断服务程序,用于完成后续的数据传输。
1.S3C2410/S3C2440 I2C控制器初始化
i2c_init函数对应于图12.7中的步骤(1),初始化I2C,代码如下:
1 行号 2 24行/* 3 25行 *I2C初始化 4 26行 */ 5 27行 void i2c_init(void) 6 28行 { 7 29行 GPEUP |= 0xc000; //禁止内部上拉 8 30行 GPECON |= 0xa0000000; //选择引脚功能,GPE15:IICSDA,GPE14:IICSCL 9 31行 10 32行 INTMSK &= ~(BIT_IIC); 11 33行 12 34行 /*bit[7] = 1,使能ACK 13 35行 *bit[6] = 0,IICCLK = PCLK/16 14 36行 *bit[5] = 1,使能中断 15 37行 *bit[3:0] = 0xf,Tx clock = IICCLK/16 16 38行 *PCLK = 50MHz、IICCLK = 3.125MHz,Tx Clock = 0.195MHz 17 39行 */ 18 40行 IICCON = (1 << 7) | (0 << 6) | (1 << 5) | (0xf); //0xaf 19 41行 20 42行 IICADD = 0x10; //S3C24xx slave address = [7:1] 21 43行 IICSTAT = 0x10; //I2C串行输出使能(Rx/Tx) 22 44行 } 23 45行
第32行在INTMSK寄存器中开启I2C中断,这样,以后调用i2c_read、i2c_write启动传
输时,即可触发中断,进而可以在中断服务程序中进一步完成后续的传输。
第40行用于选择发送时钟,并进行一些设置:使能ACK、使能中断。
第42行用于设置S3C2410/S3C2440作为I2C从机时的地址,本实例未用到。
第43行使能I2C串行输出(设置IICSTAT[4]为1),这样,在i2c_write、i2c_read
函数中就可以写IICDS寄存器了。
2.S3C2410/S3C2440 I2C主机发送函数
初始化完成后,就可以调用i2c_read、i2c_write读写I2C从机了。它们的使用方法从参数
名称就可以看出。这两个函数只是启动I2C传输,然后等待,知道数据在中断服务程序中传
输完毕后再返回。
i2c_write函数的实现如下:
1 行号 2 46行 /* 3 47行 *主机发送 4 48行 *slvAddr:从机地址,buf:数据存放的缓冲区,len:数据长度 5 49行 */ 6 50行 void i2c_write(unsigned int slvAddr, unsigned char *buf, int len) 7 51行 { 8 52行 g_tS3C24xx_I2C.Mode = WRDATA; //写操作 9 53行 g_tS3C24xx_I2C.Pt = 0; //索引值初始化为0 10 54行 g_tS3C24xx_I2C.pDATA = buf; //保存缓冲区地址 11 55行 g_tS3C24xx_I2C.DataCount = len; //传输长度 12 56行 13 57行 IICDS = slvAddr; 14 58行 IICSTAT = 0xf0; //主机发送、启动 15 59行 16 60行 /*等待直至数据传输完毕*/ 17 61行 while(g_tS3C24xx_I2C.DataCount != -1); 18 62行 } 19 63行
第57行将从机地址写入IICDS寄存器,这样,在第58行启动传输并发出S信号后,紧接
着就自动发出从机地址。
第58行设置IICSTAT寄存器,将S3C2410/S3C2440设为主机发送器,并发出S信号。
后续的传输工作将在中断服务程序中完成。
第61行等待g_tS3C24xx_I2C.DataCount在中断服务程序中被设为-1,这表明传输完成,
于是返回。
3.S3C2410/S3C2440 I2C主机接收函数
i2c_read函数的实现与i2c_write类似,代码如下:1 行号 2 64行 /* 3 65行 *主机接收 4 66行 *slvAddr:从机地址,buf:数据存放的缓冲区,len:数据长度 5 67行 */ 6 68行 void i2c_read(unsigned int alvAddr, unsigned char *buf, int len) 7 69行 { 8 70行 g_tS3C24xx_I2C.Mode = RDDATA; //读操作 9 71行 g_tS3C24xx_I2C.Pt = -1; //索引值初始化为-1,表示第一个中断时不接收数据(地址中断) 10 72行 g_tS3C24xx_I2C.pData = buf; //保存缓冲区地址 11 73行 g_tS3C24xx_I2C.DataCount = len; //传输长度 12 74行 13 75行 IICDS = slvAddr; 14 76行 IICSTAT = 0xb0; //主机接收,启动 15 77行 16 78行 /*等待直至数据传输完毕*/ 17 79行 while(g_tS3C24xx_I2C.DataCount != -1); 18 80行 } 19 81行
需要注意的是第71行将索引值设为-1,在中断处理函数中会根据这个值决定是否从
IICDS寄存器中读取数据。读操作时,第1次中断发生时表示发出了地址,这时候不能
读取数据。
4.S3C2410/S3C2440 I2C中断服务程序
I2C操作的主体在中断服务程序,它分为3部分:首先在SRCPND、INTPND中清除中断,后面两部分对应于写操作和读操作。先看清除中断的代码:
1 行号 2 82行 /* 3 83行 *I2C中断服务程序 4 84行 *根据剩余的数据长度选择继续传输或者结束 5 85行 */ 6 86行 void I2CInitHandle(void) 7 87行 { 8 88行 unsigned int iicSt, i; 9 89行 10 90行 //清中断 11 91行 SRCPND = BIT_IIC; 12 92行 INTPND = BIT_IIC; 13 93行 14 94行 iicSt = IICSTAT; 15 95行 16 96行 if(iicSt & 0x8){printf("Bus arbitration failed\n\r");} //仲裁失败
第91、92行用来清除I2C中断的代码。需要注意的是,即使清除中断后,IICCON寄存器
中的位[4](中断标识位)仍为1,这导致I2C传输暂停。
第94行读取状态寄存器IICSTAT,发生中断时有可能时因为仲裁失败,在第96行对它进行
处理。本程序忽略仲裁失败,因为只有一个I2C主机。
接下来是一个switch语句,分别处理写操作、读操作。先看写操作:
1 行号 2 98行 switch(g_tS3C24xx_I2C.Mode) 3 99行 { 4 100行 case WRDATA: 5 101行 { 6 102行 if((g_tS3C24xx_I2C.DataCount--) == 0) 7 103行 { 8 104行 //下面两行用于恢复I2C操作,发出P信号 9 105行 IICSTAT = 0xd0; 10 106行 IICCON = 0xaf; 11 107行 Delay(10000); //等待一段时间以便P信号已经发出 12 108行 break; 13 109行 } 14 110行 15 111行 IICDS = g_tS3C24xx_I2C.pData[g_tS3C24xx_I2C.Pt++]; 16 112行 17 113行 //将数据写入IICDS后,需要一段时间才能出现在SDA线上 18 114行 for(i = 0; i < 10; i++); 19 115行 20 116行 IICCON = 0xaf; //恢复I2C传输 21 117行 break; 22 118行 } 23 119行
g_tS3C24xx_I2C.DataCount表示剩余等待传输的数据个数,第102行判断数据是否已经
全部发送完毕:若是,则通过第105、106行发出P信号,停止传输;
第105行设置IICSTAT寄存器以便发出P信号,但是由于这时IICCON[4]仍为1,P信号还没
有实际发出;
当第106行清除IICCON[4]后,P信号才真正发出去;
第107行等待一段时间,确保P信号已经发送完毕。
如果数据还没有发送完毕,第111行从缓冲区中得到下一个要发送的数据,将它写入IICDS
寄存器中。稍等之后,即可在第116行清除IICCON[4]以恢复I2C传输,这时,IICDS寄存器中
的数据就会发送出去,这将触发下一个中断。
I2C读操作的处理与写操作相似,代码如下:
1 行号 2 120行 case RDDATA: 3 121行 { 4 122行 if(g_tS3C24xx_I2C.Pt == -1) 5 123行 { 6 124行 //这次中断时在发送I2C设备地址后发生的,没有数据 7 125行 //只接收一个数据时,不要发出ACK信号 8 126行 g_tS3C24xx_I2C.Pt = 0; 9 127行 if(g_tS3C24xx_I2C.DataCount == 1) 10 128行 IICCON = 0x2f; //恢复I2C传输,开始接收数据,接收到数据时不发出ACK 11 129行 else 12 130行 IICCON = 0xaf; //恢复I2C传输,开始接收数据 13 131行 break; 14 132行 } 15 133行 16 134行 if((g_tS3C24xx_I2C.DataCount--) == 0) 17 135行 { 18 136行 g_tS3C24xx_I2C.pData[g_tS3C24xx_I2C.Pt++] = IICDS; 19 137行 20 138行 //下面两行恢复I2C操作,发出P信号 21 139行 IICSTAT = 0x90; 22 140行 IICCON = 0xaf; 23 141行 Delay(10000); //等待一段时间以便P信号已经发出 24 142行 break; 25 143行 } 26 144行 27 145行 g_tS3C24xx_I2C.pData[g_tS3C24xx_I2C.Pt++] = IICDS; 28 146行 29 147行 //接收最后一个数据时,不要发出ACK信号 30 148行 if(g_tS3C24xx_I2C.DataCount == 0) 31 149行 IICCON = 0x2f; //恢复I2C传输,接收到下一个数据时无ACK 32 150行 else 33 151行 IICCON = 0xaf; //恢复I2C传输,接收到下一个数据时发出ACK 34 152行 break; 35 153行 }
读操作比写操作多一个步骤:第一次中断发生时,表示发出了地址,这时候还不能读取
数据。在代码中要分辨这点。对应第122~132行:如果g_tS3C24xx_I2C.Pt等于-1,表示
这是第一次中断,然后修改g_tS3C24xx_I2C.Pt为0,并设置IICCON寄存器恢复I2C传输
(第127~130行)。
当数据传输已经开始后,每接收到一个数据就会触发一次中断。后面的代码读取数据,
判断所有数据是否已经完成:如果完成则发出P信号,否则继续下一次传输。
第134行判断数据是否已经发送完毕。
第19行设置IICSTAT寄存器以便发出P信号,但是由于这时IICCON[4]仍为1,P信号还没
有实际发出。
第140行清除IICCON[4]后,P信号才真正发出。
第145~151行用来启动下一次数据的接收。
第148~151行判断是否只剩下最后一个数据了:若是,则通过第149行的清除IICCON[4]、
IICCON[7],这样即可恢复IIC传输,并使得接收到数据后,S3C2410/S3C2440不发出ACK
信号(这样从机即可知道数据传输完毕);否则在第151行中只要清除IICCON[4]以恢复IIC传输。
中断服务程序中,当数据数据传输完毕时,g_tS3C24xx_I2C.DataCount将自减为-1,这样,
i2c_read或i2C_write函数即可跳出等待,直接返回。
5.RTC芯片M4lt11特性相关的操作
m4lt11.c文件中提供两个函数m4lt11_set_datetime、m4lt11_get_datetime。它们都通过调
用i2c_read或i2c_write函数来完成与M4lt11的交互。
前面说过,操作M4lt11只需要两步:发出寄存器地址,发出数据或读取数据。
m4lt11_set_datetime函数把这两个步骤合并为一个I2C写操作,m4lt11_get_datetime函数先发
起一个I2C写传输,再发起一个I2C读传输。
m4lt11_set_datetime函数代码如下:1 行号 2 29行 /* 3 30行 *写m4lt11,设置日期和时间 4 31行 */ 5 32行 int m4lt11_set_datetime(struct rtc_time *dt) 6 33行 { 7 34行 unsigned char leap_yr; 8 35行 struct{ 9 36行 unsigned char addr; 10 37行 struct rtc_registers rtc; 11 38行 }__attribute__ ((packed)) addr_and_regs; 12 ... .../*设置rtc结构,即根据传入的参数构造各寄存器的值*/ 13 76行 i2c_write(0xD0, (unsigned char *)&addr_and_regs, sizeof(addr_and_regs)); 14 77行 15 78行 return 0; 16 79行 }
省略号表示的代码用来设置addr_and_regs结构体。这个结构体分为两部分:
addr_and_regs表示M4lt11寄存器地址(它被设为0);
addr_and_regs.rtc表示M4lt11的8个寄存器——秒、分、时、天(星期几)、日、月、年、控制寄存器。
根据传入参数填充好addr_and_regs结构体后,就可以启动I2C写操作了。
第38行使用“__attribute__((packed))”设置这个结构体为紧凑格式。使得它的大小
为9字节(否则大小为12字节):1字节用来保存寄存器的地址,8字节用来保存8个寄存器的值。
m4lt11_get_datetime函数的代码与m4lt11_set_datetime函数类似,如下所示:
1 行号 2 81行 /* 3 82行 *读取m4lt11,获取日期与时间 4 83行 */ 5 84行 int m4lt11_get_datetime(struct rtc_time *dt) 6 85行 { 7 86行 unsigned char addr[1] = {0}; 8 87行 struct rtc_registers rtc; 9 88行 10 89行 memset(&rtc, 0, sizeof(rtc)); 11 90行 12 91行 i2c_write(0xD0, addr, 1); 13 92行 i2c_read(0xD0, (unsigned char *)&rtc, sizeof(rtc)); 14 93行 15 ... .../*根据读出的各寄存器的值,设置dr结构体*/ 16 110行 return 0; 17 111行 }
第91行发起一次I2C写传输,设置要操作的M4lt11寄存器地址为0;
第92行发起一次I2C读传输,读出M4lt11各寄存器的值;
省略号对应的代码根据读出的各寄存器的值,设置dr结构。M4lt11中以BCD码表示
日期与时间,需要转换为程序使用的一般二进制格式。
12.2.4 I2C实例的连接脚本
本实例要用到第8章NAND Flash控制器的代码将代码从NAND Flash复制到SDRAM
中。由于nand代码中用到全局变量,而全局变量要运行与可读写的内存中,为了方便,
使用连接脚本将这些初始代码放到Steppingstone中。
连接脚本为i2c.lds,内容如下:
1 SECTIONS{ 2 . = 0x00000000; 3 .init : AT(0){head.o init.o nand.o} 4 . = 0x30000000; 5 .text : AT(4096){ *(.text)} 6 .rodata ALIGN(4) : AT((LOADADDR(.text)+SIZEOF(.text)+3)&~(0x03)){*(.rodata*)} 7 .data ALIGN(4) :AT((LOADADDR(.rodata)+SIZEOF(.rodata)+3)&~(0x03)){*(.data)} 8 __bss_start = .; 9 .bss ALIGN(4) :{ *(.bss) *(COMMON)} 10 __bss_end = .; 11 }
第2~3行将head.S和nand.c对应的代码的运行地址设为0,加载地址(存在NAND Flash
上的地址)设为0。从NAND Flash启动时,这些代码被复制到Steppingstone后就可以直接运行。
第4行设置其余代码的运行地址为0x3000 0000;第5行将代码段的加载地址设为4096,
表示代码段将存在NAND Flash地址4096处。
第6~7行的“AT(...)”设置rodata段、data段的加载地址依次位于代码段之后。
“LOADADDR(...)”表示某段的加载地址,SIZEOF(...)表示它的大小。这两行的前面使用
“ALIGN(4)”使得它们的运行地址为4字节对齐。为了使各段之间加载地址的相应偏移值等于
地址的相对偏移,需要将“AT(...)”中的值也设为4字节对齐:先加上3,然后与~(0x03)进
行与操作(将低2位清0)。
12.2.5 实例测试
本程序在main函数中通过串口输出一个菜单,用于设置或读取时间,步骤如下:
(1)使用串口将开发板的COM0和PC的串口相连,打开PC的穿裤工具设为115200、8N1;
(2)在i2c目录下执行make,将可执行文件烧入NAND Flash中运行;
(3)在PC的串口工具上,可以看到如下菜单:
#### RTC Menu #### Data format: 'year.month.day w hour:min:sec', 'w' is week day eg:2007.08.30 4 01:16:57 [S] Set the RTC [R] Read the RTC Enter your selection:
(4)要设置RTC,输入"s"或“S”。可以看到如下字符。
Enter data&time:
在串口工具中按照"year.month.day w hour:min:sec"格式输入日期与时间,比如:"2007.08.30 4 01:16:57",然后按回车键。
注意:只能输入2000.01.1至2099.12.31之间的日期与时间;年月日与星期必须真实存
在,否则RTC芯片无法正常工作。
(5)要可读取RTC,输入"r"或"R",即可看到当前日期与时间,串口上回输出类似
下面的结果。
*** Now is: 2007.08.30 4 01:16:57 ***
(6)断电后重启,输入“R”,仍可看到正确的时间。
附:代码:
链接: https://pan.baidu.com/s/1kV24a9L 密码: tfab