单片机读写FT24C256A详解
一、FT24C256A介绍
产品简介:
FT24C256A系列是262,144位串行电可擦除可编程只读存储器,俗称 EEPROM。它们可存储32,768个字,每个字8位(一个字节)。这些器件采用专有的先进CMOS工艺制造,适用于低功率和低电压应用。这些器件采用标准8引脚DIP、8引脚SOP、8引脚MSOP、8引脚 TSSOP和8引脚UDFN封装。标准的2线串行接口用于处理所有读取和写入功能。我们范围的VCC(1.8V至5.5V)器件支持广泛的应用。
产品特点:
低电压和低功耗操作:
FT24C256A: VCC 1.8V~5.5V
64 字节页写模式。
允许部分页写操作。
内部组织:32768x8(256K)。
标准2线双向串行接口。
施密特触发器,用于噪声保护的滤波输入。
自定时写周期(最大5ms)。
1 MHz(2.5V-5V)、400 kHz(1.8V)兼容性。
写操作前自动擦除。
写保护引脚用于硬件数据保护。
高可靠性:通常1,000,000次循环耐久性。
100年数据保留。
工业温度范围(—40℃至85℃)。
标准8引脚 DIP/SOP/MSOP/TSSOP/UDFN 无铅封装。
上面这段话是从官网上抄下来的,写得很有水平但个人觉得有点美中不足。个人认为有空把这些写得这么详细还不如直接贴几个例程出来,好歹还能让新手可以节省很多前期工作。虽然在他们那些工程师看来使用这个EEPROM不是“有手就会的”,但对于刚入门的来说这个手还可能真没有。
在官网上图片(详见下图)右侧有一行小字:“FT24Cxx系列EEPROM使用I2C接口,芯片容量从2kb到1Mb ”。这样就知道了这个也是使用的IIC通信,至于芯片容量这都是默认的FT24Cxxx后面这个xxx就是说的芯片容量,就这块芯片来说容量有256kb。
![](https://img-blog.csdnimg.cn/img_convert/5bbde4eaf3c74ab7b31ce8d1a1c9fd51.png)
二、FT24C256A的IIC通信搭建
IIC通信这都是老生常谈的话题了,在CSDN上面关于IIC的博文没有1000篇也有800篇。好多讲得很详细的我这里就不再献丑了(我自己能理解,但要很好的讲出来我做不到)。这里就直接贴各部分的代码了。IIC通信开始、停止、应答时序如下:
![](https://img-blog.csdnimg.cn/img_convert/dda752f136264d5b884e31ccaf2874f4.png)
![](https://img-blog.csdnimg.cn/img_convert/cae7eba82193418895bee40edd294f34.png)
CPU发起I2C总线启动信号:
/*
*********************************************************************************************************
* 函 数 名: IIC_Start_EEPROM
* 功能说明: CPU发起I2C总线启动信号
* 形 参:无
* 返 回 值: 无
*********************************************************************************************************
*/
void IIC_Start_EEPROM(void)
{
/* 当SCL高电平时,SDA出现一个下跳沿表示I2C总线启动信号 */
SDA_OUT_EEPROM(); //sda线输出
IIC_SDA_H_EEPROM;
IIC_SCL_H_EEPROM;
mDelayuS(5);
IIC_SDA_L_EEPROM; //START:when CLK is high,DATA change form high to low
mDelayuS(5);
IIC_SCL_L_EEPROM; //钳住I2C总线,准备发送或接收数据
}
CPU发起I2C总线停止信号
/*
*********************************************************************************************************
* 函 数 名: IIC_Stop_EEPROM
* 功能说明: CPU发起I2C总线停止信号
* 形 参:无
* 返 回 值: 无
*********************************************************************************************************
*/
void IIC_Stop_EEPROM(void)
{
/* 当SCL高电平时,SDA出现一个上跳沿表示I2C总线停止信号 */
SDA_OUT_EEPROM(); //sda线输出
IIC_SCL_L_EEPROM;
IIC_SDA_L_EEPROM;
IIC_SCL_H_EEPROM;
mDelayuS(5);
IIC_SDA_H_EEPROM; //发送I2C总线结束信号
mDelayuS(5);
IIC_SDA_L_EEPROM;
}
CPU产生一个ACK信号和一个NACK信号
/*
*********************************************************************************************************
* 函 数 名: IIC_Ack_EEPROM
* 功能说明: CPU产生一个ACK信号
* 形 参:无
* 返 回 值: 无
*********************************************************************************************************
*/
void IIC_Ack_EEPROM(void)
{
IIC_SCL_L_EEPROM;
SDA_OUT_EEPROM();
IIC_SDA_L_EEPROM; /* CPU驱动SDA = 0 */
mDelayuS(2);
IIC_SCL_H_EEPROM; /* CPU产生1个时钟 */
mDelayuS(2);
IIC_SCL_L_EEPROM;
}
/*
*********************************************************************************************************
* 函 数 名: IIC_NAck_EEPROM
* 功能说明: CPU产生1个NACK信号
* 形 参:无
* 返 回 值: 无
*********************************************************************************************************
*/
void IIC_NAck_EEPROM(void)
{
IIC_SCL_L_EEPROM;
SDA_OUT_EEPROM();
IIC_SDA_H_EEPROM; /* CPU驱动SDA = 1 */
mDelayuS(2);
IIC_SCL_H_EEPROM; /* CPU产生1个时钟 */
mDelayuS(2);
IIC_SCL_L_EEPROM;
}
CPU产生一个时钟,并读取器件的ACK应答信号
/*
*********************************************************************************************************
* 函 数 名: IIC_WaitAck_EEPROM
* 功能说明: CPU产生一个时钟,并读取器件的ACK应答信号
* 形 参:无
* 返 回 值: 0:表示正确应答,什么都不做
* 1:接收应答失败,IIC直接退出
*********************************************************************************************************
*/
uint8_t IIC_WaitAck_EEPROM(void)
{
UINT8 ucErrTime=0;
SDA_IN_EEPROM();
IIC_SDA_H_EEPROM; /* CPU释放SDA总线 */
IIC_SCL_H_EEPROM; /* CPU驱动SCL = 1, 此时器件会返回ACK应答 */
mDelayuS(2);
if (READ_SDA_EEPROM) /* CPU读取SDA口线状态 */
{
ucErrTime++;
if(ucErrTime>250)
{
IIC_Stop_EEPROM();
return 1;
}
}
IIC_SCL_L_EEPROM;
return 0;
}
CPU向I2C总线设备发送8bit数据
/*
*********************************************************************************************************
* 函 数 名: IIC_WriteByte_EEPROM
* 功能说明: CPU向I2C总线设备发送8bit数据
* 形 参:_ucByte : 等待发送的字节
* 返 回 值: 无
*********************************************************************************************************
*/
void IIC_WriteByte_EEPROM(uint8_t u8Data)
{
uint8_t i;
SDA_OUT_EEPROM();
IIC_SCL_L_EEPROM;//拉低时钟开始数据传输
for(i=0;i<8;i++)
{
if((u8Data&0x80)>>7) IIC_SDA_H_EEPROM;
else IIC_SDA_L_EEPROM;
u8Data<<=1;
mDelayuS(2);
IIC_SCL_H_EEPROM; //时钟保持高电平
mDelayuS(2);
IIC_SCL_L_EEPROM; //时钟拉低,才允许SDA变化
mDelayuS(2);
}
}
CPU从I2C总线设备读取1个字节
/*
*********************************************************************************************************
* 函 数 名: IIC_ReadByte_EEPROM
* 功能说明: CPU从I2C总线设备读取1个字节
* 形 参:ack=1时,发送ACK,ack=0,发送nACK
* 返 回 值: 读到的数据
*********************************************************************************************************
*/
uint8_t IIC_ReadByte_EEPROM(void)
{
uint8_t i;
uint8_t receive=0;
SDA_IN_EEPROM();//SDA设置为输入
for(i=0;i<8;i++ )
{
IIC_SCL_L_EEPROM;
mDelayuS(2);
IIC_SCL_H_EEPROM;
receive<<=1;
if(READ_SDA_EEPROM)receive++;
mDelayuS(1);
}
return receive;
}
这些就是FT24C256A IIC通信需要用到的函数,里面也就一些管脚的标识不一样其他都是大同小异的(准确来说是一摸一样的)。都可以在其他关于IIC通信的例程里面直接修改。
三、FT24C256A读写数据函数
这一部分就相当于把前面的函数按照一定的顺序(读写协议)排列来实现对数据的读写。
(一) EEPROM写数据代码:
![](https://img-blog.csdnimg.cn/img_convert/18d542f54d894fdfb6168b525d6cafed.png)
上图是FT24C256A写操作时序。上图中上面的时序是写一个字节的时序,下面的时序是写一页的时序。关于写数据时序的说明在FT24C256A数据手册里面有提及,全是用英文写的,我这里偷个懒就直接放截图了。
![](https://img-blog.csdnimg.cn/img_convert/45fce5d493c3499ca0d19078c9793e04.png)
翻译过来就是:
写操作
(A) 字节写入:写操作需要两个8位的数据字地址在设备地址字和ACKNOWLEDGE信号之后。在收到这个地址时,EEPROM将以一个“O”回应,然后在第一个8位数据字中计时。在接收到8位数据字后,EEPROM将再次输出一个“O”。寻址设备,如微控制器,必须在STOP条件下终止写序列。此时,EEPROM进入内部定时写周期状态。在这个写周期中,所有输入都被禁用,EEPROM直到写完成才会响应(图3)。
(B) 页写入:256K EEPROM能够写64字节的页。
页写入与字节写入的方式相同,但微控制器在第一个数据字被写入后不发送STOP条件。在EEPROM承认接收到第一数据字后,微控制器可以再传输多达63个数据字。在接收到每个数据字后,EEPROM将响应一个“O”。微控制器必须以STOP条件终止页写入序列(参见图4)。
数据字地址的下六位位在接收到每个数据字后在内部递增。较高的数据字地址位不增加,保留内存页行位置。如果超过64个数据字被传输到EEPROM,数据字地址将“滚动”,之前的数据将被覆盖。
(C) ACK信号轮询:ACK信号轮询可用于在自我计时的内部编程期间轮询编程状态。通过发出一个有效的读或写地址命令,如果设备仍然在自计时编程模式,EEPROM将不会在9时钟周期确认。然而,如果编程完成并且芯片返回到待机模式,设备将在第9个时钟周期返回一个有效的ACK信号。
按照上面时序可以写出写操作代码:
写一字节:
/*******************************************************************************
* 函数名:x24Cxx_WriteByte
* 功 能:写一个字节
* 参 数:u16Addr要写入的地址
u8Data要写入的数据
* 返回值:无
* 说 明:器件地址(包含写入命令) -> 1或2个字节WORD ADDR -> 数据
*******************************************************************************/
void x24Cxx_WriteByte(uint16_t u16Addr, uint8_t u8Data)
{
// x24Cxx_WriteEnable();//使能写入
IIC_Start_EEPROM();//起始信号
#if (ADDR_BYTE_NUM == 1)//地址只有1个字节
{
IIC_WriteByte_EEPROM(DEV_ADDR | EEPROM_I2C_WR | (((uint8_t)((u16Addr >> 8) & 0x07)) << 1));//器件寻址+写+页选择位
IIC_WaitAck_EEPROM();//等待应答
IIC_WriteByte_EEPROM((uint8_t)(u16Addr & 0xFF));//只取地址的低字节
IIC_WaitAck_EEPROM();//等待应答
}
#endif
#if (ADDR_BYTE_NUM == 2)//地址有2个字节
{
IIC_WriteByte_EEPROM(DEV_ADDR | EEPROM_I2C_WR);//器件寻址+写
IIC_WaitAck_EEPROM();//等待应答
IIC_WriteByte_EEPROM((uint8_t)((u16Addr >> 8) & 0xFF));//地址高字节
IIC_WaitAck_EEPROM();//等待应答
IIC_WriteByte_EEPROM((uint8_t)(u16Addr & 0xFF));//地址低字节
IIC_WaitAck_EEPROM();//等待应答
}
#endif
IIC_WriteByte_EEPROM(u8Data);
IIC_WaitAck_EEPROM();//等待应答
IIC_Stop_EEPROM();
// x24Cxx_WriteDisble();//禁止写入
}
写一页代码:
/*******************************************************************************
* 函数名:x24Cxx_WritePage
* 功 能:页写
* 参 数:u16Addr要写入的首地址;
u8Len写入数据字节数,最大为PAGE_SIZE
pData要写入的数据首地址
* 返回值:无
* 说 明:最多写入1页,防止翻卷,如果地址跨页则去掉跨页的部分
*******************************************************************************/
void x24Cxx_WritePage(uint16_t u16Addr, uint8_t u8Len, uint8_t *pData)
{
uint8_t i;
if (u8Len > PAGE_SIZE)//长度大于页的长度
{
u8Len = PAGE_SIZE;
}
if ((u16Addr + (uint16_t)u8Len) > CAPACITY_SIZE)//超过容量
{
u8Len = (uint8_t)(CAPACITY_SIZE - u16Addr);
}
if (((u16Addr % PAGE_SIZE) + (uint16_t)u8Len) > PAGE_SIZE)//判断是否跨页
{
u8Len -= (uint8_t)((u16Addr + (uint16_t)u8Len) % PAGE_SIZE);//跨页,截掉跨页的部分
}
// x24Cxx_WriteEnable();//使能写入
IIC_Start_EEPROM();//起始信号
#if (ADDR_BYTE_NUM == 1)//地址只有1个字节
{
IIC_WriteByte_EEPROM(DEV_ADDR | EEPROM_I2C_WR | (((uint8_t)((u16Addr >> 8) & 0x07)) << 1));//器件寻址+写+页选择位
IIC_WaitAck_EEPROM();//等待应答
IIC_WriteByte_EEPROM((uint8_t)(u16Addr & 0xFF));//只取地址的低字节
IIC_WaitAck_EEPROM();//等待应答
}
#endif
#if (ADDR_BYTE_NUM == 2)//地址有2个字节
{
IIC_WriteByte_EEPROM(DEV_ADDR | EEPROM_I2C_WR);//器件寻址+写
IIC_WaitAck_EEPROM();//等待应答
IIC_WriteByte_EEPROM((uint8_t)((u16Addr >> 8) & 0xFF));//地址高字节
IIC_WaitAck_EEPROM();//等待应答
IIC_WriteByte_EEPROM((uint8_t)(u16Addr & 0xFF));//地址低字节
IIC_WaitAck_EEPROM();//等待应答
}
#endif
for (i = 0; i < u8Len; i++)
{
IIC_WriteByte_EEPROM(*(pData + i));
IIC_WaitAck_EEPROM();//等待应答
}
IIC_Stop_EEPROM();
// x24Cxx_WriteDisble();//禁止写入
}
(二)EEPROM读取数据代码:
![](https://img-blog.csdnimg.cn/img_convert/772b5527970b49c5809c4f8d526eb470.png)
上图是FT24C256A读操作时序。上图中最上面的时序是读取一个字节的时序,中间的时序是连续读取一段空间的时序,最下面的是读取一页的时序。关于读数据时序的说明在FT24C256A数据手册里面有提及,全是用英文写的,我这里继续偷个懒放截图了。
![](https://img-blog.csdnimg.cn/img_convert/9d828bba79864d5c8552e76a75ba0655.png)
翻译过来就是:
读操作
读命令与写命令类似,只是地址字的第8位读写位设置为1。三种读操作方式说明如下:
(A) 当前地址读取:EEPROM内部地址字计数器维护最后的读或写地址加1,如果设备的电源没有被切断。为了启动当前地址读操作,微控制器发出一个START位和一个读写位(第8位)设置为“1”的有效设备地址字。EEPROM将在串行时钟周期第9位响应ACK信号。然后一个8位数据字将被连续输出。内部地址字计数器将自动增加1。对于当前地址读取,微控制器在时钟周期第18位内不会发出确认信号。微控制器在时钟周期第18位后发出一个有效的STOP位,以终止读取操作。然后设备返回到STANDBY模式(见图5)。
(B) 顺序读取:顺序读取与当前地址读取非常相似。微控制器发出一个START位和一个有效的设备地址字,读写位(第八位)设置为“1”。EEPROM将在串行时钟周期(第9位)上响应ACKNOWLEDGE信号。然后,一个8位数据字将被串行输出。与此同时,内部地址字计数器将自动增加1。
与当前地址读取不同,微控制器在时钟周期第18位发送一个确认信号,向EEPROM设备发出它想要另一个字节的数据。在接收到ACK信号后,EEPROM将串行输出一个基于内部地址计数器的8位数据字。当微控制器需要另一个数据时,它在时钟周期第27位发送一个ACK信号。另一个8位数据字将被连续输出。只要微控制器在l接收到一个新的数据字后发送一个ACK信号,这个顺序读取就会继续。当内部地址计数器达到其最大有效地址时,它将滚动到内存数组地址的开头。与当前地址读取类似,微控制器可以通过不确认接收到的最后一个数据字来终止顺序读取,但随后发送一个STOP位代替(图6)。
(C) 随机读取:随机读分为两步。第一步是用一个“伪写”指令初始化一个目标读地址的内部地址计数器。第二步是读取当前地址。
为了用目标读地址初始化内部地址计数器,微控制器首先发出一个START位,然后是一个有效的设备地址,读写位(第8位)设置为“0”。此时,EEPROM将确认,微控制器将发送两个地址字。EEPROM将再次确认。微控制器执行当前地址读指令来读取数据,而不是向EEPROM发送有效的写入数据。注意,一旦发出START位,EEPROM将重置内部编程过程,并继续执行新指令-即读取当前地址(图7)。
按照上面时序可以写出读操作代码:
读一个字节代码:
/*******************************************************************************
* 函数名:x24Cxx_ReadByte
* 功 能:读一个字节
* 参 数:u16Addr要读取的地址
* 返回值:u8Data读出的数据
* 说 明:无
*******************************************************************************/
uint8_t x24Cxx_ReadByte(uint16_t u16Addr)
{
uint8_t u8Data = 0;
IIC_Start_EEPROM();//起始信号
#if (ADDR_BYTE_NUM == 1)//地址只有1个字节
{
IIC_WriteByte_EEPROM(DEV_ADDR | EEPROM_I2C_WR | (((uint8_t)((u16Addr >> 8) & 0x07)) << 1));//器件寻址+写+页选择位
IIC_WaitAck_EEPROM();//等待应答
IIC_WriteByte_EEPROM((uint8_t)(u16Addr & 0xFF));//只取地址的低字节
IIC_WaitAck_EEPROM();//等待应答
}
#endif
#if (ADDR_BYTE_NUM == 2)//地址有2个字节
{
IIC_WriteByte_EEPROM(DEV_ADDR | EEPROM_I2C_WR);//器件寻址+写
IIC_WaitAck_EEPROM();//等待应答
IIC_WriteByte_EEPROM((uint8_t)((u16Addr >> 8) & 0xFF));//地址高字节
IIC_WaitAck_EEPROM();//等待应答
IIC_WriteByte_EEPROM((uint8_t)(u16Addr & 0xFF));//地址低字节
IIC_WaitAck_EEPROM();//等待应答
}
#endif
IIC_Start_EEPROM();//起始信号
IIC_WriteByte_EEPROM(DEV_ADDR | EEPROM_I2C_RD);//器件寻址+读
IIC_WaitAck_EEPROM();//等待应答
u8Data = IIC_ReadByte_EEPROM();
IIC_NAck_EEPROM();
IIC_Stop_EEPROM();
return u8Data;
}
读一页代码:相当于顺序读取但是有固定长度(长度为一页)
/*******************************************************************************
* 函数名:x24Cxx_ReadPage
* 功 能:页读
* 参 数:u16Addr要读取的首地址;
u8Len读取数据字节数,最大为PAGE_SIZE
pBuff读取数据存入的缓存
* 返回值:无
* 说 明:无
*******************************************************************************/
void x24Cxx_ReadPage(uint16_t u16Addr, uint8_t u8Len, uint8_t *pBuff)
{
uint8_t i;
if (u8Len > PAGE_SIZE)//长度大于页的长度
{
u8Len = PAGE_SIZE;
}
if ((u16Addr + (uint16_t)u8Len) > CAPACITY_SIZE)//超过容量
{
u8Len = (uint8_t)(CAPACITY_SIZE - u16Addr);
}
if (((u16Addr % PAGE_SIZE) + (uint16_t)u8Len) > PAGE_SIZE)//判断是否跨页
{
u8Len -= (uint8_t)((u16Addr + (uint16_t)u8Len) % PAGE_SIZE);//跨页,截掉跨页的部分
}
IIC_Start_EEPROM();//起始信号
#if (ADDR_BYTE_NUM == 1)//地址只有1个字节
{
IIC_WriteByte_EEPROM(DEV_ADDR | EEPROM_I2C_WR | (((uint8_t)((u16Addr >> 8) & 0x07)) << 1));//器件寻址+写+页选择位
IIC_WaitAck_EEPROM();//等待应答
IIC_WriteByte_EEPROM((uint8_t)(u16Addr & 0xFF));//只取地址的低字节
IIC_WaitAck_EEPROM();//等待应答
}
#endif
#if (ADDR_BYTE_NUM == 2)//地址有2个字节
{
IIC_WriteByte_EEPROM(DEV_ADDR | EEPROM_I2C_WR);//器件寻址+写
IIC_WaitAck_EEPROM();//等待应答
IIC_WriteByte_EEPROM((uint8_t)((u16Addr >> 8) & 0xFF));//地址高字节
IIC_WaitAck_EEPROM();//等待应答
IIC_WriteByte_EEPROM((uint8_t)(u16Addr & 0xFF));//地址低字节
IIC_WaitAck_EEPROM();//等待应答
}
#endif
IIC_Start_EEPROM();//起始信号
IIC_WriteByte_EEPROM(DEV_ADDR | EEPROM_I2C_RD);//器件寻址+读
IIC_WaitAck_EEPROM();//等待应答
for (i = 0; i < (u8Len - 1); i++)
{
*(pBuff + i) = IIC_ReadByte_EEPROM();
IIC_Ack_EEPROM();
}
*(pBuff + u8Len - 1) = IIC_ReadByte_EEPROM();
IIC_NAck_EEPROM();//最后一个不应答
IIC_Stop_EEPROM();
}
四、验证读写函数功能
自己编了一个简易的读写例子,具体代码如下:
void Save_Val(UINT16 u16Addr,UINT16 Num,float Val)
{
static UINT16 N1,N2,N3,N4;
N1 = (UINT16)Num/100;
N2 = (UINT16)Num%100;
N3 = (UINT16)((Val*100)/100);
N4 = (UINT16)((int)(Val*100)%100);
// printf("写入地址:0x%04x 执行次数:%4d 电流值:%2.2f\r\n",u16Addr,Num,Val);
x24Cxx_WriteByte((u16Addr + 0),N1);
mDelaymS(5);
x24Cxx_WriteByte((u16Addr + 1),N2);
mDelaymS(5);
x24Cxx_WriteByte((u16Addr + 2),N3);
mDelaymS(5);
x24Cxx_WriteByte((u16Addr + 3),N4);
mDelaymS(5);
}
//读取写入的数据是否正常
void Read_Val(UINT16 u16Addr)
{
static UINT16 N1,N2,N3,N4;
static UINT16 Num;
static float Val;
N1 = (UINT16)x24Cxx_ReadByte(u16Addr + 0);
N2 = (UINT16)x24Cxx_ReadByte(u16Addr + 1);
N3 = (UINT16)x24Cxx_ReadByte(u16Addr + 2);
N4 = (UINT16)x24Cxx_ReadByte(u16Addr + 3);
Num = N1*100 + N2;
Val = N3 + N4/100.0;
if(N1 == 255) printf("地址:0x%04x 组号:%4d 标准值:%2.2f\r\n",u16Addr,N2,Val);
else if(Num != 0) printf("地址:0x%04x 执行次数:%4d 电流值:%2.2f\r\n",u16Addr,Num,Val);
}
//读取写入的数据记录
void Read_Val_List(UINT16 u16Addr)
{
static UINT16 N1,N2,N3,N4;
static UINT16 Num;
static float Val;
N1 = (UINT16)x24Cxx_ReadByte(u16Addr + 0);
N2 = (UINT16)x24Cxx_ReadByte(u16Addr + 1);
N3 = (UINT16)x24Cxx_ReadByte(u16Addr + 2);
N4 = (UINT16)x24Cxx_ReadByte(u16Addr + 3);
Num = N1*100 + N2;
Val = N3 + N4/100.0;
if((N1 == 255)&&\
((N2<100)&&(N2>0))) printf("地址:0x%04x 组号:%4d 标准值:%2.2f\r\n",u16Addr,N2,Val);
else
if(((N1!=255)&&(N2!=255))&&\
((N3!=255)&&(N4!=255)))
printf("地址:0x%04x 执行次数:%4d 电流值:%2.2f\r\n",u16Addr,Num,Val);
}
测试结果如下:
![](https://img-blog.csdnimg.cn/img_convert/0972432e28c34733aa35329484e2bada.png)