STM32的I2C-EEPROM

I2C协议简介: 
I2C(inter-integrated Circuit)协议是由Phiilps公司开发的,因为它的引脚少,硬件实现简单,可扩展性强(不像USART,CAN的外部收发设备),如今被广泛的使用在系统内多个集成电路(IC)之间的通信. 
(根据I2C总线协议,我们可以把I2C分为物理层和协议层) 
物理层: 
1,使用两条总线线路,一条双向串行数据线(SDA),一条串行时钟线(SCL). 
2,每个连接到总线的设备都有一个独立的地址,主机可以利用这个地址进行不同设备之间的访问. 
3,多主机同时使用总线时,为了防止数据冲突,会利用仲裁方式决定由哪个设备占用总线. 
4,具有三种传输模式: 
标准模式传输速率为100Kbit/s 
快速模式传输速率为400Kbit/s 
高速模式传输速率为3.4Mbit/s(但是目前大多的I2C设备都不支持高速模式) 
5,片上的滤波器可以滤去总线数据线上的毛刺波保证数据完整. 
6,连接到相同总线的IC数量受到总线的最大电容400pF限制. 
这里写图片描述

协议层: 
I2C的协议包括起始和停止条件,数据有效性,响应,仲裁,时钟同步和地址广播等.(STM32有集成的硬件I2C接口,所以不需要用软件去模拟SDA和SCL时序). 
这里写图片描述 
主机写数据到从机 
这里写图片描述 
主机由从机读数据 
(这两幅图表示的是主机和从机通讯时,SDA线的数据包序列)

S:传输开始信号 
SLAVE ADDRESS:从机的地址 
R/~W:传输方向选择 1为读,0为写 
A/~A:应答或者非应答信号 
DATA(带阴影的):数据由主机传输到从机 
DATA(空白的):数据由从机传输到主机 
P:停止传输信号

起始信号S产生后,所有从机就开始等待主机接下来广播的从机地址信号SLAVE ADDRESS,在I2C总线上,每个设备的地址都是唯一的.当主机广播的地址与某个设备地址相同时,这个设备就被选中了,没被选中的设备会直接忽略之后的数据信号.(根据I2C总线协议,这个从机地址可是是7位或10位). 
地址位的后面,是传输方向的选择(读写R/~W),从机接收到匹配的地址后,主机或从机会返回一个应答(A)或者非应答(~A)信号,只有接收到应答信号后,主机才能继续发送或接收数据.(根据I2C总线协议,之后的每一个操作都需要返回一个应答信号)

如果配置方向传输位是写数据: 
广播完地址,接收到应答信号后,主机开始向从机传输数据(DATA),数据包的大小为8位,主机每发送一个数据,都要等待从机的应答信号(A)(重复这个过程可以向从机传输N个数据,这个N没有大小限制),当数据传输结束时,主机向从机发送一个停止传输信号(P),表示传输数据停止.

如果配置方向传输位是读数据: 
广播完地址,接收到应答信号后,从机开始向主机返回数据(DATA),数据包的大小为8位,从机每发送一个数据,都会等待主机的应答信号(A)(重复这个过程可以返回N个数据,这个N没有大小限制),当主机希望停止接收数据时,就向从机返回一个非应答信号(~A),从机自动停止数据传输.

STM32的I2C特性和架构 
I2C接口特性: 
1,STM32的中等容量和大容量型号的芯片均有2个的I2C总线接口. 
2,能够工作于多主模式或从模式,分别为主接收器,主发送器,从接收器,从发送器. 
3,支持标准模式100Kbit/s和快速模式400Kbit/s,不支持高速模式. 
4,支持7位或10位寻址. 
5,内置了硬件CRC发生器/校验器. 
6,I2C的接收和发送都可以使用DMA操作. 
7,支持系统管理总线(SMBus)2.0版

I2C架构: 
这里写图片描述 
可以看到,I2C所有的硬件架构都是根据SCL和SDA展开的(其中的SMBALERT用于SMBUS). 
SCL线的时序就是I2C协议中的时钟信号,他由I2C接口根据时钟控制寄存器(CCR)控制,控制的参数主要是时钟频率. 
SDA线的信号是通过一系列数据控制架构,在将要发送的数据的基础上,根据协议添加各种起始信号,应答信号,地址信号,实现以I2C协议的方式发送出去.读取数据则从SDA线上的信号中取出接收到的数据值.(发送和接收的数据都是被保存在数据寄存器(DR)上的).

I2C实例(EEPROM): 
对EEPROM读写最常用的方式是使用I2C协议.STM32F103VET6它有2个I2C接口,本实验使用I2C1,对应地接到EEPROM(AT24CO2)的SCL和SDA线,实现I2C通讯,对EEPROM进行读写. 
本例子是采用主模式,分别用作主发送器和主接收器.通过查询事件的方式来确保正常通讯.

main函数: 
int main(void) 

USART1_Config();//串口初始化

 I2C_EE_Init();//I2C的EEPROM的初始化

 USART1_printf(USART1, "\r\n ("__DATE__ "-"__TIME__ ") \r\n");

 I2C_Test();

 while (1)
 {
 }


初始化完成后由串口向终端输出调试信息,接着是调用用户函数I2C_Test(),把配置好的I2C接口向EEPROM写入数据,再把这些数据读取出来进行校验,检查我们I2C是否正常工作.

I2C接口的初始化: 
void I2C_EE_Init(void) 

I2C_GPIO_Config(); 
I2C_Mode_Config();

/* 根据头文件 i2c_ee.h 中的定义来选择 EEPROM 要写入的地址 */ 
#ifdef EEPROM_Block0_ADDRESS 
/* 选择 EEPROM Block0 来写入 */ 
EEPROM_ADDRESS = EEPROM_Block0_ADDRESS; 
#endif

#ifdef EEPROM_Block1_ADDRESS 
/* 选择 EEPROM Block1 来写入 */ 
EEPROM_ADDRESS = EEPROM_Block1_ADDRESS; 
#endif

#ifdef EEPROM_Block2_ADDRESS 
/* 选择 EEPROM Block2 来写入 */ 
EEPROM_ADDRESS = EEPROM_Block2_ADDRESS; 
#endif

#ifdef EEPROM_Block3_ADDRESS 
/* 选择 EEPROM Block3 来写入 */ 
EEPROM_ADDRESS = EEPROM_Block3_ADDRESS; 
#endif 

这里是先调用了I2C_GPIO_Config()配置好I2C所用的I/O端口,然后再调用 I2C_Mode_Config()配置I2C的工作模式,并且使能相关外设的时钟.

EEPROM的地址: 
这里写图片描述 
根据I2C协议,每一个连接到I2C总线上的设备都需要唯一的地址,EEPROM当然也有(找到EEPROM的数据手册,可以找到每一个块的地址)

这里写图片描述 
这里使用的EEPROM的型号是AT24C02,容量是2K bits,图中EEPROM的高四位是做了硬性规定,最低位为R/W(读写位),能改变的位只有A2,A1,A0(在硬件设计中可以根据需求改变,这里都为接地,也就是0).所以根据读写位的不同,EEPROM的地址为0xA0或者0xA1.

GPIO端口的初始化: 
static void I2C_GPIO_Config(void) 

GPIO_InitTypeDef GPIO_InitStructure; 
/* 使能与 I2C1 有关的时钟 */ 
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE); 
RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1,ENABLE); 
/* PB6-I2C1_SCL、PB7-I2C1_SDA*/ 
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; 
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; 
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD; // 开漏输出 
GPIO_Init(GPIOB, &GPIO_InitStructure); 

在这个函数里面,使能了GPIO时钟,I2C1的时钟,并且对I2C1复用的两个引脚进行了初始化设置,模式设置为开漏输出(不记得工作方式的,可以看之前的博客,或者直接查询《STM32数据手册》的引脚定义部分和《STM32参考手册》的GPIO章节)

I2C模式初始化: 
static void I2C_Mode_Configu(void) 

I2C_InitTypeDef I2C_InitStructure; 
/* I2C 配置 */ 
I2C_InitStructure.I2C_Mode = I2C_Mode_I2C; 
I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2; 
I2C_InitStructure.I2C_OwnAddress1 =I2C1_OWN_ADDRESS7; 
I2C_InitStructure.I2C_Ack = I2C_Ack_Enable; 
I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit; 
I2C_InitStructure.I2C_ClockSpeed = I2C_Speed; 
/* I2C1 初始化 */ 
I2C_Init(I2C1, &I2C_InitStructure); 
/* 使能 I2C1 */ 
I2C_Cmd(I2C1, ENABLE); 

和所有外设初始化一样,I2C模式的初始化还是对他的初始化结构体进行参数配置。

I2C_InitStructure.I2C_Mode = I2C_Mode_I2C; 
选择I2C的使用方式,这里面有I2C模式(I2C_Mode_I2C)和SMBus模式(I2C_Mode_SMBusDevice或者I2C_Mode_SMBusHost),这里选择了I2C模式

I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2; 
这里是配置I2C的SCL线时钟的占空比.在STM32的I2C占空比配置中有两个选择:(这里使用了2:1) 
高电平和低电平时间比 16:9(I2C_DutyCycle_16_9) 
高电平和低电平时间比 2:1 ( I2C_DutyCycle_2) 
(SCL线的时钟信号的高电平时间与低电平时间是没必要相同的,由于SDA线是在SCL线维持在高电平时读取或者写入数据的,而在SCL的低电平期间SDA的数据发生了变化,所以高电平时间较长就不容易出现数据错误.根据I2C协议,在快速模式和高速模式下SCL的高低电平时间可以不同)

I2C_InitStructure.I2C_OwnAddress1 =I2C1_OWN_ADDRESS7; 
配置STM32的I2C设备自己的地址,根据I2C协议,每一个连接到I2C总线上的设备都需要唯一的地址(主机也一样)这里设置为自定义宏I2C1_OWN_ADDRESS7(0x0A)在STM32数据手册I2C章节可以找到.

I2C_InitStructure.I2C_Ack = I2C_Ack_Enable; 
这里是配置为允许应答(I2C_Ack_Enable),这是绝大多数遵循I2C标准设备通讯的要求(如果改为禁止应答(I2C_Ack_Disable)往往会导致通讯错误).

I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit; 
这里是选择I2C的寻址模式是7位还是10位.根据实际连接到I2C总线上设备的地址进行选择(这里是和EEPROM进行通讯,所以使用7位寻址模式(I2C_AcknowledgedAddress_7bit;)).

I2C_InitStructure.I2C_ClockSpeed = I2C_Speed; 
这里是设置I2C的传输速率,在调用初始化函数时,函数会根据我们输入的数字经过运算后把分频值写入到I2C的时钟控制寄存器(我们写入的这个值不能高于400KHz),这里自定义宏I2C_Speed(400000) 
(实际上由于I2C使用的APB1时钟为36MHz,不是10MHz的整数倍,因此最终分频后输出的SCL线时钟并不是精确的400KHz)

对结构体赋值完毕后,就用I2C_Init()进行初始化,并调用I2C_Cmd()使能I2C外设.

EEPROM的读写: 
void I2C_Test(void) 

u16 i; 
printf(“写入的数据\n\r”); 
for ( i=0; i<=255; i++ ) //填充缓冲 

I2c_Buf_Write[i] = i; 
printf(“0x%02X “, I2c_Buf_Write[i]); 
if(i%16 == 15) 
printf(“\n\r”); 

//将 I2c_Buf_Write 中顺序递增的数据写入 EERPOM 中 
I2C_EE_BufferWrite( I2c_Buf_Write, EEP_Firstpage, 256); 
printf(“\n\r 读出的数据\n\r”); 
//将 EEPROM 读出数据顺序保持到 I2c_Buf_Read 中 
I2C_EE_BufferRead(I2c_Buf_Read, EEP_Firstpage, 256); 
//将 I2c_Buf_Read 中的数据通过串口打印 
for (i=0; i<256; i++) 

if(I2c_Buf_Read[i] != I2c_Buf_Write[i]) 

printf(“0x%02X “, I2c_Buf_Read[i]); 
printf(“错误:I2C EEPROM 写入与读出的数据不一致\n\r”); 
return; 

printf(“0x%02X “, I2c_Buf_Read[i]); 
if(i%16 == 15) 
printf(“\n\r”); 

printf(“I2C(AT24C02)读写测试成功\n\r”); 

这个函数实现的功能是把数值0~255按顺序填入缓冲区数组,并通过串口打印到终端,接着通过函数I2C_EE_BufferWrite()把缓冲区的数据写入EEPROM,写入成功后,再用I2C_EE_BufferRead把数据读取出来,进行校验,判断数据是否正确写入.

I2C_EE_BufferWrite()以及I2C_EE_BuffRead()具有三个输入参数,缓冲区指针(I2c_Buf_Write)(I2c_Buf_Read),写入到或者读出EEPROM的存储地址(EEP_Firstpage),以及字节数. 

EEPROM的数据组织形式: 
EEPROM设备把它的存储矩阵进行了分页处理: 
这里写图片描述

型号是AT24C02的EEPROM分为32页,每一页可以存储8个字节的数据,若在同一页写入超过8字节,则超过的部分会被写在该页的起始地址(也就是一开始写好的部分会被覆盖). 
为了把连续的缓冲区数组按页写入到 EEPROM ,就需要对缓冲区进行分页处理.I2C_EE_BufferWrite()是根据输入的缓冲区大小参数 NumByteToWrite,计算出需要写入多少页,计算写入位置。 
分页处理好之后,调用 I2C_EE_PageWrite(),这个函数是与 EEPROM进行I2C通讯的最底层函数(里面都是调用STM32库函数)

EEPROM写入 I2C_EE_BufferWrite(); 
u8 I2c_Buf_Write[256]; 
//#define EEP_Firstpage 0x00(加个//免得字体放大)

void I2C_EE_BufferWrite(u8* pBuffer, u8 WriteAddr, u16 NumByteToWrite) 

u8 NumOfPage = 0, NumOfSingle = 0, Addr = 0, count = 0;

/*计算出要写的页数和分页*/
Addr = WriteAddr % I2C_PageSize; 
count = I2C_PageSize - Addr;
NumOfPage = NumByteToWrite / I2C_PageSize; 

if(Addr == 0)
{
    if(NumOfPage == 0)
    {
        I2C_EE_PageWrite(pBuffer,WriteAddr,NumOfSingle);
        I2C_EE_WaitEepromStandbyState(); //检测是否为Standby状态,才可以进行下一步操作
    }
    else
    {
        while(NumOfPage--)
        {
            I2C_EE_PageWrite(pBuffer,WriteAddr,I2C_PageSize);
            I2C_EE_WaitEepromStandbyState();
            WriteAddr += I2C_PageSize;
            pBuffer += I2C_PageSize;
        }

        if(NumOfSingle!=0)
        {
            I2C_EE_PageWrite(pBuffer,WriteAddr,NumOfSingle);
            I2C_EE_WaitEepromStandbyState();
        }
    }
}

else
{
    if(NumOfPage == 0)
    {
        I2C_EE_PageWrite(pBuffer,WriteAddr,NumOfSingle);
        I2C_EE_WaitEepromStandbyState();
    }

    else
    {
        NumByteToWrite -= count;
        NumOfPage = NumByteToWrite / I2C_PageSize;
        NumOfSingle = NumByteToWrite % I2C_PageSize;

        if(count != 0)
        {
            I2C_EE_PageWrite(pBuffer,WriteAddr,count)
            I2C_EE_WaitEepromStandbyState();
            WriteAddr += count;
            pBuffer += count;
        }

        while(NumOfPage--)
        {
            I2C_EE_PageWrite(pBuffer,WriteAddr,I2C_PageSize);
            I2C_EE_WaitEepromStandbyState();
            WriteAddr += I2C_PageSize;
            pBuffer += I2C_PageSize;
        }

        if(NumOfSingle != 0)
        {
            I2C_EE_PageWrite(pBuffer,WriteAddr,NumOfSingle);
            I2C_EE_WaitEepromStandbyState();
        }
    }
}


这里每次调用完 I2C_EE_PageWrite()后,都调用了一个I2C_EE_WaitEepromStandbyState();

void I2C_EE_WaitEepromStandbyState(void) 

vu16 SR1_Tmp = 0;

    do
    {
        I2C_GenerateSTART(I2C1, ENABLE);

        SR1_Tmp = I2C_ReadRegister(I2C1, I2C_Register_SR1);

        I2C_Send7bitAddress(I2C1, EEPROM_ADDRESS, I2C_Direction_Transmitter);
    }while(!(I2C_ReadRegister(I2C1, I2C_Register_SR1) & 0x0002));

    I2C_ClearFlag(I2C1, I2C_FLAG_AF);

    I2C_GenerateSTOP(I2C1, ENABLE);


这里是利用了 EEPROM在接收完数据后,启动了周期写入数据的时间内不会对主机的请求做出应答的特性,利用这个库函数循环发送起始信号,若检测到 EEPROM的应答,则说明 EEPROM已经完成上一步的数据写入,进入了Standby状态,可以进行下一步操作。

EEPROM进行I2C通讯的最底层函数I2C_EE_PageWrite() 
void I2C_EE_PageWrite(u8* pBuffer, u8 WriteAddr, u8 NumByteToWrite) 

/确保SDA总线空闲时再做I2C通讯
while(I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY));

I2C_GenerateSTART(I2C1, ENABLE);
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));

I2C_Send7bitAddress(I2C1, EEPROM_ADDRESS, I2C_Direction_Transmitter);
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));

I2C_SendData(I2C1, WriteAddr);
while(! I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));

while(NumByteToWrite--)
{
    I2C_SendData(I2C1, *pBuffer);

    pBuffer++;

    while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMIT)TED));
}
I2C_GenerateSTOP(I2C1, ENABLE);

}

这个 EEPROM 的页写入就是根据 EEPROM的页写入时序来写的: 
这里写图片描述

在 I2C_EE_PageWrite()函数中,先不管while语句的循环检测,就可以很清晰的看到整个代码流程就是 EEPROM 的页写入时序流程

I2C_GenerateSTART(I2C1, ENABLE); 
产生I2C的通讯起始信号S。

I2C_Send7bitAddress(I2C1, EEPROM_ADDRESS, I2C_Direction_Transmitter); 
把前面条件变异中赋值的变量 EEPROM_ADDRESS 地址通过I2C1接口发送出去,数据传输方向为ST32的 I2C发送数据(I2C_Direction_Transmitter). 
(这里的 EEPROM_ADDRESS 地址是 EEPROM作为挂载在I2C总线上设备的寻址,并不是 EEPROM 内存存储矩阵的地址)

I2C_SendData(I2C1, WriteAddr); 
这里是把数据传输到数据寄存器,再由I2C模块根据I2C协议发送出去,但是要注意这里的输入参数是 WriteAddr,根据 EEPROM 的页写入时序,发送完I2C的地址后的第一个数据,并不一定要写入 EEPROM的数据.EEPROM对这个数据解释为将要对存储矩阵写入的地址,WriteAddr是在 I2C_EE_PageWrite()函数时作为参数输入的,在这个实例里是由 I2C_EE_BufferWrite()计算出来的.

I2C_SendData(I2C1, *pBuffer); 
这里是向 EEPROM发送要写如的数据,根据 EEPROM 的页写入时序,这些数据被写入到前面发送的页地址中,如果连续写入超过一页的最大字节数(这里是8个),则多出来的数据会重新从该页的起始地址连续写入,覆盖前面的数据.

I2C_GenerateSTOP(I2C1, ENABLE); 
产生I2C的传输结束信号,完成一次I2C通讯.

I2C事件检测:

在 I2C_EE_PageWrite里面还有很多的事件检测,这些都是必须的,根据STM32参考手册的序列图可以看到,在I2C的通讯过程中,会产生一系列的事件,出现时间后相应的寄存器中会产生标志位. 
这里写图片描述

这个图的意思为: 
1,发出了起始信号,会产生事件 5(EV5),即STM32的I2C成为主机模式; 
2,发送完成I2C设备寻址并得到应答后,会产生 EV6,即STM32的I2C成为数据发送端; 
3,发送数据完成会产生EV8; 
所以在做出I2C通讯操作时,可以通过循环调用库函数 I2C_CheckEvent()进行事件查询,确保上一操作完成后才进行下一操作.

例如:在确定SDA总线空闲之后,作为主发送器的STM32发出起始信号,成功后会产生EV5,所以使用语句: 
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)); 
来检测这个事件,确保检测到之后再执行下一操作,I2C_EVENT_MASTER_MODE_SELECT则是事件的类型,在keil环境下可以查找到所有相关事件

EEPROM读取函数 I2C_EE_BufferRead(); 
void I2C_EE_BufferRead(u8* pBuffer, u8 ReadAddr, u16 NumByteToRead) 

while(I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY));

I2C_GenerateSTART(I2C1, ENABLE);//第一次发送起始信号
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));

I2C_Send7bitAddress(I2C1, EEPROM_ADDRESS, I2C_Direction_Transmitter);
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));

I2C_Cmd(I2C1, ENABLE);

I2C_SendData(I2C1, ReadAddr);
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));

I2C_GenerateSTART(I2C1, ENABLE);//这里是第二次发送起始信号了
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));

I2C_Send7bitAddress(I2C1, EEPROM_ADDRESS, I2C_Direction_Receiver);
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED));

while(NumByteToRead)
{
    if(NumByteToRead == 1)
    {
    /* Disable Acknowledgement */
    I2C_AcknowledgeConfig(I2C1, DISABLE);
    /* Send STOP Condition */
    I2C_GenerateSTOP(I2C1, ENABLE);
    }
/* Test on EV7 and clear it */
    if(I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED))
    {
    /* Read a byte from the EEPROM */
    *pBuffer = I2C_ReceiveData(I2C1);
    /* Point to the next location where the byte read will be
    pBuffer++;
    /* Decrement the read bytes counter */
    NumByteToRead--;
    }
}


这里也是利用 I2C_CheckEvent()来确保通讯正常进行的,要注意的是读取数据根据I2C标准协议,主发送器STM32要发出两次起始信号I2C信号才能建立通讯.

PS: 
总结一下I2C读写流程: 
配置I/0端口,确定并配置I2C的模式,使能GPIO和I2C时钟.(首先必须做的) 
写: 
1,检测SDA是否空闲; 
2,按I2C协议发出起始信号; 
3,发出7位期间地址和写模式; 
4,要写如的存储区的首地址; 
5,用页写入方式或者字节写入方式写入数据; 
6,发送I2C通讯结束信号; 
(每个操作之后要检测事件确定是否成功,写完检测 EEPROM是否进入了Standby状态)

读: 
1,检测SDA是否空闲; 
2,按I2C协议发出起始信号; 
3,发出7位期间地址和写模式(伪写); 
4,发出要读取的存储首地址; 
5,重发起始信号(记住); 
6,发出7为器件地址和读模式; 
7,接收数据; 
(每个操作之后要检测事件确定是否成功)



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值