STM32 SPI读写FLASH

一、SPI协议

  Serial Peripheral Interface,串行外围设备接口。SPI 是串行同步全双工通信协议。一般用于控制器和控制器之间的数据传输。

1.物理层

  每个从机都有四条线和主机相连,不同从机的 MISO MOSI SCK 连接的是主机统一个端口,SS 线,连接不同端口

  1. SS :(Slave Select)从机选择。当主机想和某个从机通讯时,就把从机的 SS 条线的电平拉低。
  2. SCK :(Serial Clock)时钟信号线。由主机产生,f103的SPI挂载在不同的总线上,最大频率为 f p l k / 2 f_{plk}/2 fplk/2 ,因此时钟最高频率是不一样的。
  3. MOSI:(Master Output,Slave Input
  4. MISO:(Master Input,Slave Output)这两条就是数据线,因此 SPI 是全双工的。

2.协议层

总体讲解

在这里插入图片描述

  1. 主机发出选择信号,拉低某条 SS 线。
  2. SCK 上升沿,数据可以变化。SCK 下降沿,数据采样,进行传输。
  3. 8个时钟后,一个字节的数据就发送出去了。
  4. 发送过程中高位先行和低位先行没有影响规定,一般选择高位先行。
  5. 图中 MSBOUTMSB 意为 最高有效位(most significant bit)。把MSBOUT写在最前面意思就是高位先行。

具体讲解

  但实际上,SPI 并没有硬性规定必须在下降沿采样。这里需要配置时钟极性(CPOL) 和 时钟相位 (CPHA) ,他们决定了空闲时,时钟电平,和采样时刻。

  1. 时钟极性(CPOL)SPI 空闲时,SCK 信号线的电平。 CPOL=1 高电平;CPOL=0 低电平。
  2. 时钟相位 (CPHA) :数据采样时刻。CPHA=1 在偶数边采样,CPHA=0 在奇数边采样。
  3. 如果CPOL=0,CPHA=1 即空闲状态为0,偶数边采样;这样就是上面图中上升沿变化,下降沿采集。

   由此,CPOL,CPHA 的不同组合一共四种,对应了四种模式,其中最常用的是 模式0模式3。(即:低电平,上升沿采集高电平,上升沿采集

模式CPOLCPHA
000
101
210
311

二、STM32 SPI外设

  STM32共有3个 SPI 外设。 其中SPI1挂载在 APB2 总线上,SPI2,SPI3挂载在 APB1 总线上。其中后面两个还支持 I2S。框图在 参考手册 中可以查到。

1.通讯引脚

  通信引脚的对应的端口可以在 数据手册 中查到,现在总结如下:
在这里插入图片描述
  要注意的是 SPI3 的引脚用到了下载引脚,复用功能为SPI,一般不会使用SPI3。一般 NSS 不用这里的硬件控制,而是直接使用 GPIO控制。

2.时钟控制逻辑

  SCK 的时钟信号,其波特率是由 控制寄存器 CR1 的 BR位控制的,控制结果如下表:

BR频率BR频率
000 f p c k l / 2 f_{pckl}/2 fpckl/2100 f p c k l / 32 f_{pckl}/32 fpckl/32
001 f p c k l / 4 f_{pckl}/4 fpckl/4101 f p c k l / 64 f_{pckl}/64 fpckl/64
010 f p c k l / 8 f_{pckl}/8 fpckl/8110 f p c k l / 128 f_{pckl}/128 fpckl/128
011 f p c k l / 16 f_{pckl}/16 fpckl/16111 f p c k l / 256 f_{pckl}/256 fpckl/256

f p c k l f_{pckl} fpcklSPI 所在总线的时钟频率。

3.数据控制

  接收数据时,数据从 MISO进入,串行存到移位寄存器,再并行移动到接收缓存区被读出。
  发送数据,先写入发送缓冲区,经过移位寄存器由 MOSI 发送出去。这两个缓冲区对应的寄存器就是 DR。框图中 LSBFIRST 控制位可以控制高位先行。
  事实上,发送缓冲区和接收缓冲区 都对应了同一个寄存器 DR

  SPI 是全双工通信,硬件端口部分确实有两根线,但这两条线共用了同一个移位寄存器,共用了同一个缓冲区,这难道不会发生冲突吗?
  全双工的整体过程如下:

  1. 发送数据:数据写入 DR,并行传输到移位寄存器,移位寄存器通过 MOSI 串行发出数据。
  2. 此时如果有数据从 MISO 输入,则紧跟在移出寄存器的发送数据后面进入移位寄存器(也就是说移位寄存器一边接收数据,一边吐出数据)
  3. 当发送数据的每一位都移出移位寄存器时,接收的数据也正好填满了移位寄存器。此时,移位寄存器内储存的就是接收数据。
  4. 移位寄存器把数据并行传给 DR。读取DR 就可得到输入数据。
  5. 重复上述过程,就是全双工的工作模式。

4.整体控制逻辑

  1. CR :控制寄存器:控制了 SPI模式,波特率,数据帧格式,LSB先行,主从模式,NSS软控制,使能,时钟等。

  2. SR 状态寄存器,工作过程中会有置位和清零。

  3. DR 数据寄存器,其对应了发送缓冲区和接收缓冲区。

三、通信过程

  和I2C一样,通信过程中,状态寄存器会置位和清零,可以通过读取这些位来了解通信状态。
  先介绍两个标志位,TXE(Transmit buffer empty),当发送缓冲区为空时,其置1;RXEN(Receive buffer not empty) ,当接收缓冲区位空时,其清零。
在这里插入图片描述

上面已经把全双工解释的很详细了,这里结合置位在说一下。

  • 总体上 SCK 产生时钟信号,数据被发送和接收进来;因为是全双工,发送和接收时可以同时进行的。
  • 先看发送部分:首先,当 DR 被写入数据时, 标志位 TXE 会硬件置0。
  • 接着数据并行移动到移位寄存器,此时DR变空;TXE 硬件置1;这时可以传入第二个要发送的数据到 DR 中。
  • 接收部分:当发送的数据完全从移位寄存器移出时,接收的数据正好填满移位寄存器,数据被并行移动到 DR
  • 此时 RXNE置1,标志有数据传入 DR,也表明已经完成发送数据。
  • 最后,只有写入数据后,SCK 才会产生时钟。所以就算只要接收,也要先发送一帧数据以开启时钟。

四、固件库编程

1.结构体

typedef struct
{
  uint16_t SPI_Direction;          //配置SPI的单双向模式
  uint16_t SPI_Mode;              //配置主/从模式

  uint16_t SPI_DataSize;          //数据帧大小,可选8/16

  uint16_t SPI_CPOL;              //时钟极性
  uint16_t SPI_CPHA;              //时钟相位  
 
  uint16_t SPI_NSS;              //NSS是由硬件还是软件控制  
 
  uint16_t SPI_BaudRatePrescaler;  //时钟分频因子

  uint16_t SPI_FirstBit;          //设置高位还是低位先行

  uint16_t SPI_CRCPolynomial;     //校验表达式
}SPI_InitTypeDef;

2.固件库函数

  • SPI_Init :初始化函数
  • SPI_Cmd :使能SPI
  • SPI_I2S_SendData,SPI_I2S_ReceiveData :发送和接收数据
  • SPI_I2S_GetFlagStatus :获取工作状态

五、FLASH介绍

  板子上用的FLASH是 W25Q64 信号的Flash,是一种 NOR FLASH。其中“64” 表示 64M bit,即为 8M Byte8MB 8兆字节的存储器(真小~)。
(注:M 表示 2 20 2^{20} 220

1.芯片引脚图

  通过读芯片手册,可以得知芯片引脚对应的功能。

  • /WP : 为写保护,其中前面的斜杠表示低电平有效。
  • /HOLD:暂停通信。

2.块和扇区

  FLASH 把自己 8MB 的空间分为128个块(Block),这样每个块是64KB。再把每个块分为16个扇区(Sector),每个扇区是 4KB
  由于FLASH的存储特性,在写入数据之前必须先把原有数据擦除(恢复为全1状态);因此对存储单元的划分有利于快速读写。
  擦除时,最小擦除最小单位为扇区。而读写时不同的FLASH写入最小单位不同。NOR FLASH 可以一个字节读写,而 NADN FLASH 只能以块或扇区读写。这也是为什么NOR FLASH可以执行程序。

3.控制FLASH的指令

  不同于EEPROM,FLASH的控制较为复杂。对FLASH的读写和检测需要用预先规定好的指令,FLASH会相应这些指令,并做出操作。
  这些命令在芯片手册和零死角中都有总结。

  表中,第一列是指令名;第二列是发送代码;第三列即以后都是相关的代码传输,其中有括号的表示是从FLASH传入stm32中的。

   举例说明:FLASH写入操作是较耗时的,因此在进行操作时必须得知当前FLASH的状态,而对FLASH状态的读取就是用指令完成的;stm32发出指令,FLASH返回当前自身状态。

   这一个操作对应的指令名是 Read Status Register-1,首先stm32发送代码 05,FLASH返回自身状态(返回状态寄存器低8位的值S7~S0)。
  下面解析一些操作的命令。

Sector Erase(4KB)

在这里插入图片描述

  扇区擦除。前面提到,在对FLASH写入时要先对其进行擦除,而擦除的最小单元就是扇区(Sector);
  这里指令为 20;后面还要连续传入三个字节,这三个字节就要擦除扇区的地址(FLASH的地址是32位的);

Page Program

在这里插入图片描述
  写入数据。操作代码是 02。最多允许写入256个字节。发出写入地址( A23~A0 ),最后传入数据( D7~D0 )。(这里手册上写错了,最后一个字节仍是stm32发送,不用加括号)

Read Data

在这里插入图片描述  读取数据。操作指令 03 。发送读取地址,FLASH返回地址的存储值。

检测指令

在这里插入图片描述

  Manufacturer/Device IDRead Unique IDJEDEC ID。这些指令读取的是FLASH上特定的ID号。

  其作用在于:开机检测时,用于检测FLASH是否工作正常,SPI线能否正常工作。

注:对发送地址的要求

  上面向FLASH发送地址时,要发送最低位的地址。如 Sector Erase 擦除一个扇区的地址,传入这个扇区的最低地址。
  如擦除 Block3 的 Sector2 ;那就传入 020000+002000 = 022000

开始编程

1.硬件电路图

2.初始化GPIO

static void SPI_GPIO_Config(void)
{	
	GPIO_InitTypeDef GPIO_InitStruct;
	
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC|RCC_APB2Periph_GPIOC,ENABLE);
	
	//CLK
	GPIO_InitStruct.GPIO_Pin   = GPIO_Pin_5;
	GPIO_InitStruct.GPIO_Mode  = GPIO_Mode_AF_PP;
	GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitStruct);
	
	//DO/MISO
	GPIO_InitStruct.GPIO_Pin   = GPIO_Pin_6;
	GPIO_Init(GPIOA,&GPIO_InitStruct);
	
	//DIO/MOSI
	GPIO_InitStruct.GPIO_Pin   = GPIO_Pin_7;
	GPIO_Init(GPIOA,&GPIO_InitStruct);
	
	//CS/NSS
	GPIO_InitStruct.GPIO_Pin   = GPIO_Pin_0;
	GPIO_InitStruct.GPIO_Mode  = GPIO_Mode_Out_PP;
	GPIO_Init(GPIOC,&GPIO_InitStruct);
}

  这里把 DO,DIO,CLK 对应的引脚设置成复用推挽,因为我们想要使用软件控制 NSS,因此 NSS 引脚设置位推挽输出。使用或操作同时配置 GPIOA 和 GPIOC 的时钟。

3.初始化SPI

在初始化时,一定事先把 NSS 的电平拉高,让FLASH处于未被选中的状态。否则初始化不会成功。

static void SPI_FLASH_Config(void)
{
	SPI_InitTypeDef SPI_InitStruct;
	
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1,ENABLE);
	
	FLASH_SPI_CS_HIGH();//非常重要!!!!!
	
	SPI_InitStruct.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
	SPI_InitStruct.SPI_Mode      = SPI_Mode_Master;
	
	SPI_InitStruct.SPI_DataSize  = SPI_DataSize_8b;
	
	SPI_InitStruct.SPI_CPOL      = SPI_CPOL_High;
	SPI_InitStruct.SPI_CPHA      = SPI_CPHA_2Edge;
	SPI_InitStruct.SPI_NSS       = SPI_NSS_Soft;
	SPI_InitStruct.SPI_FirstBit  = SPI_FirstBit_MSB;
	
	SPI_InitStruct.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_2;	
	SPI_InitStruct.SPI_CRCPolynomial = 7;
	
	SPI_Init(SPI1,&SPI_InitStruct);
	SPI_Cmd(SPI1,ENABLE);
}

把两者的代码封装以便调用。

void SPI_FLASH_Init(void)
{
	SPI_GPIO_Config();
	SPI_FLASH_Config();
}

4.发送数据

  发送数据可以用库函数中的 SPI_I2S_SendData,注意时序的检测。
  首先检测 TXE位是否置位,当 TXE置位时表示 DR 为空,可以写入数据
  发送完数据后,检测 RXNE是否置位,置位时表示 DR 有接收到数据,也就是发送已经完成。
  最后把接收到的数据用 SPI_I2S_ReceiveData 获得。

uint8_t SPI_FLASH_SentByte(uint8_t byte)
{
	SPITimeout = SPIT_FLAG_TIMEOUT;
	while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_TXE) == RESET)
	{
		if( (SPITimeout--)==0 ) return SPI_TIMEOUT_UserCallback(0);
	}
	
	SPI_I2S_SendData(SPI1,byte);
	SPITimeout = SPIT_FLAG_TIMEOUT;
	while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_RXNE) == RESET)
	{
		if( (SPITimeout--)==0 ) return SPI_TIMEOUT_UserCallback(1);
	}
	
	return SPI_I2S_ReceiveData(SPI1);
}

  函数中超时警告函数和,I2C中编写的一样,这里附上代码不再讲解。

#define SPI_DEBUG_ON         					1

#define SPI_INFO(fmt,arg...)           printf("<<-SPI-INFO->> "fmt"\n",##arg)
#define SPI_ERROR(fmt,arg...)          printf("<<-SPI-ERROR->> "fmt"\n",##arg)
#define SPI_DEBUG(fmt,arg...)          do{\
                                          if(SPI_DEBUG_ON)\
                                          printf("<<-SPI-DEBUG->> [%d]"fmt"\n",__LINE__, ##arg);\
                                          }while(0)
static __IO uint32_t  SPITimeout = SPIT_FLAG_TIMEOUT;

static uint8_t SPI_TIMEOUT_UserCallback(uint8_t errorcode)
{
	SPI_ERROR("SPI 等待超时!errorcode=%d",errorcode);
	
	return 0;
}

5.对FLASH的操作

把上面对FLASH的操作表格内容封装为宏,便于使用。

/************************FLASH Instruction***************************/
#define W25X_WriteEnable		      0x06 
#define W25X_WriteDisable		      0x04 
#define W25X_ReadStatusReg		      0x05 
#define W25X_WriteStatusReg		      0x01 
#define W25X_ReadData			      0x03 
#define W25X_FastReadData		      0x0B 
#define W25X_FastReadDual		      0x3B 
#define W25X_PageProgram		      0x02 
#define W25X_BlockErase			      0xD8 
#define W25X_SectorErase		      0x20 
#define W25X_ChipErase			      0xC7 
#define W25X_PowerDown			      0xB9 
#define W25X_ReleasePowerDown	      0xAB 
#define W25X_DeviceID			      0xAB 
#define W25X_ManufactDeviceID   	  0x90 
#define W25X_JedecDeviceID		      0x9F

#define WIP_Flag                      0x01
#define Dummy_Byte                    0xFF

读取FLASH的ID

#define FLASH_SPI_CS_LOW              GPIO_ResetBits(GPIOC,GPIO_Pin_0)
#define FLASH_SPI_CS_HIGH             GPIO_SetBits(GPIOC,GPIO_Pin_0)
#define Dummy_Byte                    0xFF
uint32_t SPI_FLASH_ReadID(void)
{
	uint32_t Temp=0,Temp0=0,Temp1=0,Temp2=0;
	FLASH_SPI_CS_LOW;
	
	SPI_FLASH_SentByte(W25X_JedecDeviceID);
	
	Temp0 = SPI_FLASH_SentByte(Dummy_Byte);
	Temp1 = SPI_FLASH_SentByte(Dummy_Byte);
	Temp2 = SPI_FLASH_SentByte(Dummy_Byte);
	
	FLASH_SPI_CS_HIGH;
	
	Temp = (Temp0<<16) | (Temp1<<8) | Temp2;
	
	return Temp;
}

  在发送数据时需要拉低选择线 NSS 的电平,我们使用软件控制,编写一个宏 FLASH_SPI_CS_LOW/HIGH 来控制。在发送数据时拉低电平。
  发送完读取设别ID的指令后,FLASH会返回三个字节的数据,分别是 生产厂商,储存类型,容量。 直接使用 SPI_FLASH_SentByte 接收就好。

使能函数

在写入数据前要进行写使能。

void SPI_FLASH_WriteEnable(void)
{
	FLASH_SPI_CS_LOW;
	SPI_FLASH_SentByte(W25X_WriteEnable);
	FLASH_SPI_CS_HIGH;
}

空闲检测

  写入数据需要时间,要想知道FLASH是否已经写好数据,正在空闲可以写入下一个数据,需要检测FLASH的状态寄存器。
  用到指令Read Status Register,会返回状态寄存器的低8位数据,其中最后一位 BUSY 表明FLASH是否忙,1为忙。

#define WIP_Flag 0x01
void SPI_FLASH_WaiteForWriteEnd(void)
{
	uint8_t FLAG = 0;
	
	FLASH_SPI_CS_LOW;
	
	SPI_FLASH_SentByte(W25X_ReadStatusReg);	
	do
	{
		FLAG = SPI_FLASH_SentByte(Dummy_Byte);
	}
	while((FLAG & WIP_Flag ) == 1);
	
	FLASH_SPI_CS_HIGH;
}

  发送过读取请求后,FLASH会一直返回当前状态的数据,我们就用一个while循环不断读取,直到FLASH不再忙。

擦除扇区

  写入数据前要先擦除扇区,上面也介绍到最小擦除单元为扇区,使用指令 Sector Erase ,先发送指令,在发送24位的地址。在发送擦除指令前要使能写入。

void SPI_FLASH_SectorErase(uint32_t SectorAddr)
{
	SPI_FLASH_WriteEnable();
	SPI_FLASH_WaiteForWriteEnd();
	
	FLASH_SPI_CS_LOW();
	
	SPI_FLASH_SentByte(W25X_SectorErase);
	
	SPI_FLASH_SentByte((SectorAddr>>16)&0xff);	
	SPI_FLASH_SentByte((SectorAddr>>8)&0xff);
	SPI_FLASH_SentByte(SectorAddr&0xff);
	
	FLASH_SPI_CS_HIGH();
	SPI_FLASH_WaiteForWriteEnd();
}

页写入

出于不明原因,SPI_FLASH_WaiteForWriteEnd(); 必须在FLASH_SPI_CS_HIGH(); 下面,数据才能传输有效。

void SPI_FLASH_PageWrite(uint8_t *pBuffer,uint32_t WriteAddr,uint16_t NumberByteToWrite)
{
	SPI_FLASH_WriteEnable();
	//SPI_FLASH_WaiteForWriteEnd();
	
	FLASH_SPI_CS_LOW();
	SPI_FLASH_SentByte(W25X_PageProgram);
	
	SPI_FLASH_SentByte((WriteAddr>>16)&0xff);	
	SPI_FLASH_SentByte((WriteAddr>>8)&0xff);
	SPI_FLASH_SentByte(WriteAddr&0xff);
	
	if(NumberByteToWrite > 256)
	{
		NumberByteToWrite = 256;
		SPI_ERROR("数据过多,已修改为256");
	}
	
	while(NumberByteToWrite--)
	{
		SPI_FLASH_SentByte(*pBuffer);
		pBuffer++;		
	}
	FLASH_SPI_CS_HIGH();
	SPI_FLASH_WaiteForWriteEnd();
}

读取数据

void SPI_FLASH_BufferRead(uint8_t *pBuffer,uint32_t ReadAddr,uint16_t NumberByteToRead)
{	
	FLASH_SPI_CS_LOW();
	SPI_FLASH_SentByte(W25X_ReadData);
	
	SPI_FLASH_SentByte((ReadAddr>>16)&0xff);	
	SPI_FLASH_SentByte((ReadAddr>>8)&0xff);
	SPI_FLASH_SentByte(ReadAddr&0xff);
	
	while(NumberByteToRead--)
	{
		*pBuffer = SPI_FLASH_SentByte(Dummy_Byte);
		pBuffer++;			
	}
	FLASH_SPI_CS_HIGH();
}

main函数

uint8_t Tx_Buffer[]= "JOJO,ÎÒ²»×öÈËÁË£¡";
#define countof(a)      (sizeof(a) / sizeof(*(a)))
#define  BufferSize (countof(Tx_Buffer)-1)
uint8_t Rx_Buffer[BufferSize]; 

int main(void)
{
	uint32_t DeviceID;
	uint8_t  Manufactuer_ID,Memory_Tyep,Capacity;

	USART_Config();
	SPI_FLASH_Init();
	
	DeviceID       = SPI_FLASH_ReadID();
	Manufactuer_ID = (DeviceID >> 16)&0XFF;
	Memory_Tyep    = (DeviceID >> 8)&0XFF;
	Capacity       = DeviceID & 0xff;
	printf("Manufactuer_ID: 0x%x \n Memory_Tyep: 0x%x \n Capacity: 0x%x\n"
	,Manufactuer_ID,Memory_Tyep,Capacity);
	
	
	SPI_FLASH_SectorErase(0x00000);
	
	SPI_FLASH_PageWrite(Tx_Buffer,0X00000,BufferSize);
	
	SPI_FLASH_BufferRead(Rx_Buffer,0x00000,BufferSize);
	printf("接收完成 %s \n",Rx_Buffer);

}
STM32是一款嵌入式芯片,在嵌入式系统中,Flash是一种存储器件,用来存储程序。STM32可以读写Flash,让我们能够修改程序,实现芯片控制。 STM32芯片内部的Flash分为两种:一种是Main Flash,还有一种是System Flash。其中,Main Flash主要用来存放应用程序,而System Flash则保存了一些系统信息,比如Bootloader。 使用STM32读写Flash需要多个步骤。首先,需要进行初始化。初始化代码如下: HAL_FLASH_Unlock(); flash_erase_init.TypeErase = FLASH_TYPEERASE_PAGES; flash_erase_init.PageAddress = ADDRESS; flash_erase_init.NbPages = 1; HAL_FLASHEx_Erase(&flash_erase_init, &PAGE_ERROR); HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, ADDRESS, DATA); HAL_FLASH_Lock(); 其中,HAL_FLASH_Unlock()和HAL_FLASH_Lock()函数用来对Flash进行解锁和锁定操作。 其次,需要擦除Flash,该步骤是很重要的,因为Flash是有限制次数的,反复写入易造成擦除的次数过多,缩短Flash寿命。程序如上: flash_erase_init.TypeErase = FLASH_TYPEERASE_PAGES; flash_erase_init.PageAddress = ADDRESS; flash_erase_init.NbPages = 1; HAL_FLASHEx_Erase(&flash_erase_init, &PAGE_ERROR); 上述程序代码指定了FLASH_TYPEERASE_PAGES为扇区擦除,擦除起始地址ADDRESS,并指定擦除扇区数为1。 然后,使用HAL_FLASH_Program函数向Flash写入数据,程序如下: HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, ADDRESS, DATA); 该函数参数分别代表 需要向Flash写入的数据、Flash地址和写入操作方式等。 最后,需要做好Flash的锁定防止外部因素对Flash的更改操作,使用的函数和解锁步骤相同。程序如下: HAL_FLASH_Lock(); 总的来说,STM32读写Flash的操作需要进行初始化,擦除Flash,向Flash写入数据,并锁定防止外部更改。这些步骤需要仔细核实,但是一旦操作成功,便可以实现 STM32 上关键性的程序修改。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值