SPI与W25Q128

1 SPI简介

SPI:即Serial Peripheral interface 串行外设接口(Motorala公司)
SPI是一种高速的,全双工,串行的,同步的通信总线。
全双工意味着至少需要两根数据线,串行意味着按bit发送,同步意味着通信双方共用时钟线。
同样的SPI总线上也可以连接多个SPI设备,那么总线上的时钟由谁产生?
谁产生都可以,只要同一时刻没有多个设备同时产生时钟就可以。

通过谁去控制时钟线我们将设备分成:
Master 主设备:产生时钟信号的设备
Slave 从设备:接收时钟信号的设备

根据主从设备的分类,有时候两个数据接口的命名会有所不同:
MOSI: 主设备发送,从设备接收线
MISO: 主设备接收,从设备发送线
MOSI和MISO只是SDO和SDI的不同的叫法:
SDO :Serial Data Output 串行数据输出口
SDI :Serial Data Input 串行 数据输入
SCK :Serial Clock 时钟线
另外,SPI还有一根线SS(Slave Select)/CS(Chip Select)线。因为SPI总线上的设备不像IIC总线上的设备一样有地址这个概念,所以我们需要额外的一根线来确定到底跟谁通信。这根SS/CS就是片选信号线,此线可以利用一个任何的一个GPIO引脚,发送 0/1进行片选(比如:#SS表示低电平有效,则发送0表示通信)。

所以SPI通信一般需要四根线:SDI/SDO/SCK/SS
在这里插入图片描述

2 SPI协议

协议就是规定数据如何发。

  1. CPOL: Clock Polarity 时钟极性
    时钟极性决定不传数据的时候(空闲的时候)时钟线的电平状态

CPOL = 1 保持高电平

CPOL = 0 保持低电平

  1. CPHA:Clock Phase 时钟相位
    时钟相位决定SPI在何时锁定数据(决定数据采集时刻)

CPHA = 1 在时钟线的第二个边沿锁定数据(采集数据)
CPHA = 0 在时钟线的第一个边沿锁定数据(采集数据)
也就是说CPOL和CPHA的组合决定了数据采集和数据发送的时刻。(根据模块的手册决定)
1234
在这里插入图片描述

  1. DFF: Data Frame Format 数据帧格式
    ​SPI数据的收发也是以帧Frame为单位,那么一帧数据的传送的bit数由什么决定?

8bits 一帧数据8bits
16bits 一帧数据16bits
那么一帧数据先发LSB还是MSB
LSBFIRST = 1 LSB先发
LSBFIRST = 0 MSB先发

在这里插入图片描述

  1. 传输速率由时钟频率决定,有一个原则:就低不就高
    低频设备无法接收高频的信号,但是高频设备能够接收低频信号。


    在这里插入图片描述
    在这里插入图片描述

3 STM32F4xx SPI控制器

STM32F407中文参考手册第723页

在这里插入图片描述

接收和发送缓冲区

在接收过程中,数据收到后,先存储到内部接收缓冲区中;而在发送过程中,先将数据存储

到内部发送缓冲区中,然后发送数据。

对 SPI_DR 寄存器的读访问将返回接收缓冲值,而对 SPI_DR 寄存器的写访问会将写入的数

据存储到发送缓冲区中。

4 STM32固件库SPI函数接口说明

SDO/SDI/SCK都是复用自GPIO(无论是主设备还是从设备)。
如果是作为从设备,则SS(片选)线也是复用的;如果是主设备,不需要GPIO复用,只需要配置通用输出功能

4.1 配置SPI的GPIO引脚 (SPI - W25Q128)

通过原理图知接线情况:
SCK:PB3 MISO:PB4 MOSI:PB5 CS:PB14
在这里插入图片描述

​ a.使能时钟

	RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB,ENABLE);

​ b.初始化 SDO/SDI/SCK都是复用自GPIO,CS是不需要复用

	GPIO_InitTypeDef GPIO_InitStruct;
	GPIO_InitStruct.GPIO_Pin=GPIO_Pin_3|GPIO_Pin_4|GPIO_Pin_5;
	GPIO_InitStruct.GPIO_Mode=GPIO_Mode_AF;
	GPIO_InitStruct.GPIO_OType=GPIO_OType_PP;
	GPIO_InitStruct.GPIO_Speed=GPIO_Speed_100MHz;
	GPIO_InitStruct.GPIO_PuPd=GPIO_PuPd_NOPULL;
	GPIO_Init(GPIOB,&GPIO_InitStruct);

	GPIO_InitStruct.GPIO_Pin=GPIO_Pin_14;
	GPIO_InitStruct.GPIO_Mode=GPIO_Mode_OUT;
	GPIO_Init(GPIOB,&GPIO_InitStruct);

​ c.复用成什么功能

	GPIO_PinAFConfig(GPIOB,GPIO_PinSource3,GPIO_AF_SPI1);
	GPIO_PinAFConfig(GPIOB,GPIO_PinSource4,GPIO_AF_SPI1);
	GPIO_PinAFConfig(GPIOB,GPIO_PinSource5,GPIO_AF_SPI1);

4.2 配置SPI控制器 (SPI --APB2)

a.使能时钟

	RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1,ENABLE);

b.初始化SPI控制器
SPI_InitTypeDef结构体介绍

typedef struct
{
uint16_t SPI_Direction;//设置SPI的通信方式
SPI_Direction_2Lines_FullDuplex 全双工 <— 选全双工
SPI_Direction_2Lines_RxOnly 半双工接收
SPI_Direction_1Line_Rx 串行接收
SPI_Direction_1Line_Tx 串行发送

​ uint16_t SPI_Mode;//设置SPI的主从模式
​ SPI_Mode_Master 主模式
​ SPI_Mode_Slave 从模式 uint16_t SPI_DataSize;//设置SPI传输的数据位长度
​ SPI_DataSize_16b 数据位16位
​ SPI_DataSize_8b 数据位8位

​ uint16_t SPI_CPOL;//时钟极性
​ SPI_CPOL_Low 空闲时时钟线为低
​ SPI_CPOL_High 空闲时时钟线为高

​ uint16_t SPI_CPHA;//时钟相位
​ SPI_CPHA_1Edge 在时钟线的第一个边沿锁定数据
​ SPI_CPHA_2Edge 在时钟线的第二个边沿锁定数据

​ uint16_t SPI_NSS;//设置SPI的NSS信号是由硬件控制还是软件控制
​ SPI_NSS_Soft 由用户软件代码控制
​ SPI_NSS_Hard 由硬件控制

​ uint16_t SPI_BaudRatePrescaler;//设置SPI波特率的预分频值
​ SPI(传输速率) = SPI的输入频率(APB1还是APB2)/SPI_BaudRatePrescaler
​ SPI_BaudRatePrescaler_2 2分频
​ SPI_BaudRatePrescaler_4
​ SPI_BaudRatePrescaler_8
​ …
​ SPI_BaudRatePrescaler_256
​ uint16_t SPI_FirstBit;//指定SPI数据顺序是先发低位还是高位
​ SPI_FirstBit_MSB 先发高位
​ SPI_FirstBit_LSB 先发低位

​ uint16_t SPI_CRCPolynomial;//指定CRC(循环冗余校验)校验多项式 <–课后去拓展
​ 这个系统大于1即可,为了提供通信的可靠性
​}SPI_InitTypeDef;

​ b.1 定义结构体变量

	SPI_InitTypeDef SPI_InitStruct;

​ b.2 结构体成员赋值

	SPI_InitStruct.SPI_BaudRatePrescaler=SPI_BaudRatePrescaler_4;
	SPI_InitStruct.SPI_CPHA=SPI_CPHA_1Edge;
	SPI_InitStruct.SPI_CPOL=SPI_CPOL_Low;
	SPI_InitStruct.SPI_CRCPolynomial=7;
	SPI_InitStruct.SPI_DataSize=SPI_DataSize_8b;
	SPI_InitStruct.SPI_Direction=SPI_Direction_2Lines_FullDuplex;
	SPI_InitStruct.SPI_FirstBit=SPI_FirstBit_MSB;
	SPI_InitStruct.SPI_NSS=SPI_NSS_Soft;
	SPI_InitStruct.SPI_Mode=SPI_Mode_Master;

​ b.3 写入控制器

	SPI_Init(SPI1,&SPI_InitStruct);

4.3 使能SPI

	SPI_Cmd(SPI1,ENABLE);

4.4 收发SPI的数据(可以参考USART)

怎么去获取标志位?(假如不在中断里面)

FlagStatus SPI_I2S_GetFlagStatus(SPI_TypeDef* SPIx, uint16_t SPI_I2S_FLAG); 或者SPI_GetFlagStatus(这是一个宏)

标志位:
#define SPI_I2S_FLAG_RXNE ((uint16_t)0x0001)
#define SPI_I2S_FLAG_TXE ((uint16_t)0x0002)
#define SPI_FLAG_CRCERR ((uint16_t)0x0010)
#define SPI_FLAG_MODF ((uint16_t)0x0020)
#define SPI_I2S_FLAG_OVR ((uint16_t)0x0040)
#define SPI_I2S_FLAG_BSY ((uint16_t)0x0080)

a.读数据:只有RXNE==1的时候才能读(可以轮询也可以使用中断)

怎么去读取数据?

uint16_t SPI_I2S_ReceiveData(SPI_TypeDef* SPIx);

例子: 轮询收


/*
	通过SPI接收一个字节的数据
*/
uint8_t SPI1_Recv_a_byte(void)
{
	//
	while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_TXE)!=SET);
	SPI_I2S_SendData(SPI1,0xFF);
	/*
		为什么要先发送一个字节,才可以开始接收?
		因为当MCU没有向SPI上发送数据的时候,总线是空闲的(没有时钟信号的)。
		而MCU被动接收数据是不会产生时钟的。
		所以为了接收数据,必须先发送一个无效的数据,目的就是为了产生时钟,从而方便接收数据。
	*/
	while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_RXNE)!=SET);
	uint8_t ch=SPI_I2S_ReceiveData(SPI1);
	return ch;
}

b.发数据:只有当TXE==1才能发送数据
如何发:
void SPI_I2S_SendData(SPI_TypeDef* SPIx, uint16_t Data);

例子: 轮询发


/*
	SPI1_Send_a_byte:通过SPI1发送一个字节
*/
void SPI1_Send_a_byte(u8 data)
{
	while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_TXE)!=SET);//如果TXE被设置,说明TDR中的数据已经被发送出去了
	SPI_I2S_SendData(SPI1,data);//当数据没发完,卡在while循环,当数据发完跳出循环,发送data一个字节
	/*
		当MCU没有向SPI上发送数据的时候,总线是空闲的(没有时钟信号)
		当MCU向SPI上发送数据的时候,SCK开始产生时钟信号,SPI开始工作。
		因为SPI是全双工的通信,所以在MCU将data通过MOSI引脚发送出去的同时,MCU同时也会通过MISO引脚收到一个字节的数据(这个字节可能是无效的dummy),意思是即使W25Q128没有主动向MCU发送数据,但是MCU上SPI控制器仍然会主动去接收数据,需要处理这个数据。
	*/
	
	while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_RXNE)!=SET);//如果RXNE被设置,说明RDR中已经存入了1个字节的数据(从对方接收过来的),CPU就可以去读取RDR
	u8 ch=SPI_I2S_ReceiveData(SPI1);//当数据没接收完,卡在while循环,当数据接收完跳出循环,接收SPI1过来的数据
}	

开中断去接收数据:

跟串口使用一样

5 W25Q128

5.1 接线说明

引脚: CS ----- PB14 CLK ----- PB3 SO ----- PB4 SI ----- PB5

在这里插入图片描述

​ CS:片选引脚(低电平有效)
​ 当CS引脚处于高电平时,芯片处于不选择状态
​ 当CS引脚处于低电平时,芯片处于选择状态(此时才能与MCU通信)

5.2 内部存储结构

内部存储地址是24bits,这24bits分成了以下的四部分:

8bits 4bits 4bits 8bits
block number sector number page number byte number
块地址 扇地址 页地址 字节地址

那么W25Q128共有:

​ =2^8 =256 block
每块 =2^4 =16 sector
每扇 =2^4 =16 page
每页 =2^8 =256 byte

所以W25Q128的存储的大小 = 256 *16 * 16 *256B = 224=24 * 2^10 * 2^10 B= 16M
反推:假设W25Q128的某个单元的地址为x,该字节位于哪里:

(x>>16)&0xFF 块号
(x>>12)&0xF 扇号
(x>>8)&0xF 页码
x&0xFF 字地址(页内偏移地址)

如果要往W25Q128芯片写入数据的话,需要分成两步:

a. erase 擦除
以块,半块,扇或者整个芯片为单位,清楚除数据
b. program 写入
而W25Q128芯片所有的操作都是通过指令完成。
我们只需要根据指令表发送相应的指令就可以了

5.3 W25Q128的指令

W25Q128BV 共有 34 个基本指令。指令都是以/CS下降沿开始的,表示在发送指令之前,必须对应片选引脚(PB14)拉低才能发送指令。
​第一个传输的字节是指令码,指令码起始就是一个十六进制的数。
​在DI上传输的数据是在时钟的上升沿被锁存的,也就是在上升沿采样数据–>CPOL和CPHA怎么配置
​MSB首先被传输,SPI配置结构体里面的SPI_FirstBit选择SPI_FirstBit_MSB
​指令是以/CS上升沿结束的,(MCU将PB14拉高代表指令发送完成)
​所有的擦写指令必须在 /CS 拉高之后还有一个8位的时钟间隔,否则前面的擦写指令将被忽略。
​(发送擦写指令之后,必须再经过8个SPI时钟周期才能发送下一个指令)。

5.3.1 读取状态寄存器指令(指令码: 0x05/0x35)

读取状态寄存器SR1 – 0x05
​读取状态寄存器SR2 – 0x35
​MCU向W25Q128发送一个读取状态寄存器的指令完成后,W25就会回复一个字节的数据给MCU
​ SR1:
​ bit7 bit6 bit5 bit4 bit3 bit2 bit1 bit0
​ SRP-0 SEC TB BP2 BP1 BP0 WEL BUSY
​ BUSY: 为1表示W25Q128繁忙,为0表示空闲
​ 主机在发送指令之前,必须要判断从设备是否忙碌
​ BUSY == 0,才能发送指令
​ WEL : 为1表示写使能,可以写入数据
​ 为0表示写禁止,不可以写入数据


/*
	等待W25Q128不繁忙
*/
void wait_w25q128_not_busy(void)
{
	while(Get_W25Q128_SR1()&0x01);//如果SR1第0bit(BUSY)是1,就在这里等待
}
/*
	使能片选,拉低CS
*/
void W25Q128_ENABLE(void)
{
	GPIO_ResetBits(GPIOB,GPIO_Pin_14);
}
/*
	禁止片选,拉高CS
*/
void W25Q128_DISABLE(void)
{
	GPIO_SetBits(GPIOB,GPIO_Pin_14);
}

/*
	获取状态寄存器SR1
*/
u8 Get_W25Q128_SR1(void)
{
	W25Q128_ENABLE();
	SPI1_Send_a_byte(0x05);//发送读取SR1的指令
	u8 status = SPI1_Recv_a_byte();
	W25Q128_DISABLE();
	return status;
	//if(status & 0x01 == 1)-->表示繁忙
}

5.3.2 读取W25Q128的ID(指令:0x90) 伪代码



/*
	Manufacturer/       1      2      3      4      5         6
		Device ID     0x90  dummy    dummy  0x00  (MF7-MF0) (ID7-ID0)
*/
uint16_t Read_W25Q128_ID(void)
{
	W25Q128_ENABLE();
	SPI1_Send_a_byte(0x90);
	SPI1_Send_a_byte(0x00);
	SPI1_Send_a_byte(0x00);
	SPI1_Send_a_byte(0x00);
	u8 idh = SPI1_Recv_a_byte();//制造商ID
	u8 idl = SPI1_Recv_a_byte();//设备ID
	uint16_t id = idh<<8|idl;
	W25Q128_DISABLE();
	return id;
	//printf("制造商:%x  设备:%x\r\n",id>>8,id&0xFF);
	
}

5.3.3 读数据指令(指令码:0x03)

指令格式:
1 2 3 4 5 …
0x03 A23-A16 A15-A8 A7-A0 (D7-D0)
指令 |<-- 24bit地址 --> | 回复的第一个字节的数据 …


/*
	Read_bytes_from_W25Q128:用来从W25Q128中读取数据
		@addr:W25Q128存储器中的存储单元的地址
		@data:用来保存你读取到的数据
		@count:指定读取的数量,单位为字节
	返回值: 
		失败返回0,成功返回非0
*/
int Read_bytes_from_W25Q128(u32 addr,u8 *data,int count)
{
	//判断count是否合法
	int max_bytes = (1<<24)-addr;//从addr处最多还能读取多少字节到末尾
	count = (count<max_bytes)?count:max_bytes;
	
	W25Q128_ENABLE(); //使能片选
	SPI1_Send_a_byte(0x03);   //读取数据的指令码
	SPI1_Send_a_byte((addr>>16)&0xFF); //块地址 
	SPI1_Send_a_byte((addr>>8)&0xFF);//扇页地址
	SPI1_Send_a_byte(addr&0xFF); //字节地址  //24bits地址
	//循环接收数据
	int i;
	for(i=0;i<count;i++)
	{
		data[i]=SPI1_Recv_a_byte();
	}
	W25Q128_DISABLE();//禁止片选(不想继续接收数据)
}

5.3.4 写入数据(也叫page program 指令:0x02 以页为单位进行写入)

FLASH芯片在写入数据之前,一般要先擦除,所谓的擦除就是将页内所有字节的值全部变成0xFF。

当然在擦除之前,要进行一个写使能(指令: 0x06)。
W25Q128的擦除单位是:
擦除单位: sector(4k) half of block(32k) block(64k) chip(16M)
指令 0x20 0x52 0xD8 0xC7/0x60
所以写入数据的流程:
a.写使能
在erase/program之前,先写使能,才能修改flash中的内容。
b.擦除
在erase/program时,需要时间,所以你在运行的时候需要先判断它是否繁忙
c.program写入
一般的,如果写入的数据不大于(不够)擦除空间,为了使其他的空间的数据不变,一般是要将先要擦除的空间的数据先读取出来保存在数组中,然后将你要写入的数据替换到数组对应的位置,然后把替换掉的数组重新写入W25Q128。

比如:写入0x2C到W25Q128的0x123456地址上。
所以写入的操作为:

先将整个0x123000扇区全部读取出来
u8 data[4096]={0};//定义成全局变量
Read_bytes_from_W25Q128(0x123000,data,0x1000);//读取4K
data[0x456]=0x2C;
先去擦除整个0x123456地址处的扇区
W25Q128_Erase_Sector(0x123000);
//等待擦除完毕
//写入数据
//等待写入完毕

所以写入数据要考虑到两个问题:
1.写入的内容不足一个擦除空间
2.写入的超过一个擦除空间(此时需要多次写入)


/*
	Write_bytes_to_W25Q128:用来向W25Q128中写入数据
		@addr:W25Q128存储器中的存储单元的地址
		@data:用来保存你即将写入的数据
		@count:指定写入的数量,单位为字节
	返回值: 
		失败返回0,成功返回非0
*/
int Write_bytes_to_W25Q128(u32 addr,u8 *data,int count)
{
	//判断count是否合法   //假如0X123456  --  0x1234
	int max_bytes = (1<<24)-addr;//从addr处最多还能读取多少字节到末尾  //0x1000000-0x123456
	count = (count<max_bytes)?count:max_bytes;
	
	u32 first_sector_addr = addr & ~0xFFF;//访问的第一个扇区的地址//0x123000
	u32 last_sector_addr = (addr+count)& ~0xFFF;//访问的最后一个扇区的地址//0x12468A-->0x124000

	u32 bytes = 0;//记录已经写入的字节数
	u32 sector_addr;
	for(sector_addr = first_sector_addr;sector_addr<=last_sector_addr;sector_addr+=4096)
	{
		u8 buffer[4096]={0};//定义成全局变量,记得去改
		//1.先将当前扇区的内容全部读取出来
		Read_bytes_from_W25Q128(sector_addr,buffer,4096);//0x123456
		//2.将要写入的数据替换到数组中对应的位置上去
		//先将当前扇区要替换的字节数计算出来
		u32 modify_bytes = 4096 - (addr&0xFFF);//4096-0x456
		//然后计算出从数组的哪个位置开始替换
		u32 addr_offset = addr - sector_addr; //也可以写成 addr &0xFFF //0x123456-0x123000 === -0x124000
		u32 i;
		//开始替换
		for(i=0;i<modify_bytes&&bytes<count;i++,bytes++)
		{
			buffer[addr_offset+i]=data[bytes];	
		}
		//当前扇区替换完毕后,addr要指向下一个扇区
		addr+=modify_bytes;//下一个扇区的首地址
		//3.擦除扇区 
		W25Q128_Erase_Sector(sector_addr);
		//4.将此扇区数据重新写入(一次写入256个字节就是一页)
		//所以需要一次需要写入16次页写
		u32 page_addr = sector_addr;
		for(i=0;i<16;i++)
		{
			W25Q128_page_program(page_addr,&buffer[i*256],256);
			
			page_addr+=256;
		}
		
	}
	return bytes;
}

/*
	W25Q128写使能
*/
void W25Q128_write_enable(void)
{
	W25Q128_ENABLE();
	SPI1_Send_a_byte(0x06);
	W25Q128_DISABLE();
}

/*
	W25Q128_Erase_Sector:将sector_addr指向的扇区进行擦除(扇内所有的字节变成0xFF)
*/
void W25Q128_Erase_Sector(u32 sector_addr)
{
	W25Q128_write_enable();//写使能
	wait_w25q128_not_busy();//等待不繁忙
	W25Q128_ENABLE();
	SPI1_Send_a_byte(0x20);   //擦除数据的指令码
	SPI1_Send_a_byte((sector_addr>>16)&0xFF);  
	SPI1_Send_a_byte((sector_addr>>8)&0xFF);
	SPI1_Send_a_byte(sector_addr&0xFF);   //24bits地址
	W25Q128_DISABLE();
	wait_w25q128_not_busy();//等待擦除完成	
}

/*
	W25Q128_page_program:页写   -->0x02
	@page_addr:进行页写的首地址
	@data:保存需要写入的数据
	@count:指定写入的数量,单位为字节
*/
void W25Q128_page_program(u32 page_addr,u8 *data,int count)
{
	//计算count的合理性
	int max_bytes = 256-(page_addr&0xFF);
	count = (count<max_bytes)?count:max_bytes;
	
	W25Q128_write_enable();//写使能
	wait_w25q128_not_busy();//等待不繁忙
	W25Q128_ENABLE();
	SPI1_Send_a_byte(0x02);   //读取数据的指令码
	SPI1_Send_a_byte((page_addr>>16)&0xFF);  
	SPI1_Send_a_byte((page_addr>>8)&0xFF);
	SPI1_Send_a_byte(page_addr&0xFF);   //24bits地址
	int i;
	for(i=0;i<count;i++)
	{
		SPI1_Send_a_byte(data[i]);
	}
	W25Q128_DISABLE();
	wait_w25q128_not_busy();//等待页写完成
	
}

/*ge_addr:进行页写的首地址
	@data:保存需要写入的数据
	@count:指定写入的数量,单位为字节
*/
void W25Q128_page_program(u32 page_addr,u8 *data,int count)
{
	//计算count的合理性
	int max_bytes = 256-(page_addr&0xFF);
	count = (count<max_bytes)?count:max_bytes;
	
	W25Q128_write_enable();//写使能
	wait_w25q128_not_busy();//等待不繁忙
	W25Q128_ENABLE();
	SPI1_Send_a_byte(0x02);   //读取数据的指令码
	SPI1_Send_a_byte((page_addr>>16)&0xFF);  
	SPI1_Send_a_byte((page_addr>>8)&0xFF);
	SPI1_Send_a_byte(page_addr&0xFF);   //24bits地址
	int i;
	for(i=0;i<count;i++)
	{
		SPI1_Send_a_byte(data[i]);
	}
	W25Q128_DISABLE();
	wait_w25q128_not_busy();//等待页写完成
	
}
  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

QJ敬

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值