使用单片机给nrf52832蓝牙芯片软件升级

需求

在实际项目中使用MCU+蓝牙芯片的方式给产品添加蓝牙功能,由于蓝牙芯片要跑协议栈和一些用户代码,MCU也要跑应用代码,这样一来软件升级就成了一个棘手的问题,分别给蓝牙芯片和主控MCU做软件升级显然对量产十分不友好。
例如:笔者在一个腕表产品的项目中使用了GD32F450+NRF52832的方案,而生产时给蓝牙芯片升级软件非常繁琐,需要在电路板上预留的swd测试焊盘上焊接下载线,然后在电脑上使用jlink下载软件,再拆除swd下载线。
本文介绍一种使用swd接口实现主控MCU给NRF52832芯片做程序升级的方法,用于解决以上问题,此方法不需要更改蓝牙芯片的代码,不需要修改蓝牙芯片的BootLoader程序。并且理论上此方法支持任何支持SWD调试接口的单片机。

原理简单介绍

SWD接口是ARM cortex-M 内核单片机选配的调试接口,内部连接到 CoreSight 调试组件,CoreSight 可以直接访问单片机内部所有可寻址空间,并且可以修改所有内核寄存器,使用这两个特性就可以实现软件编程,keil和jlink下载程序就是使用这个原理。

流程

以下把主控MCU称为主机,NRF52832称为从机,以简化描述。
主机初始化SWD接口->主机把从机的ram BootLoader 搬运到其内存区域->从机执行初始化代码(主机等待从机初始化完成)->主机发送擦除flash命令并等待从机擦除完成(从机擦除flash)->主机循环发送程序数据到从机并等待flash写入结束(从机进行flash写入)->主机发送结束命令(从机重启)->软件升级完成

电路要求

假设nrf52832蓝牙芯片使用串口与主控单片机通信,则至少需要进行如下连接(假设P0.23配置为RX,P0.22配置为TX),从机的swd接口则可以与主机的任意GPIO连接,下图以连接到PA11、PA12为例:
蓝牙芯片连接

软件编写简要说明

1、编写 ram BootLoader

此部分软件的功能是进行数据搬运,接收主机的数据写入flash,擦除flash等。
对于nrf52832,编写代码如下:


#include "nrf_nvmc.h"


#define TYPE_NONE   0x00000000
#define TYPE_INIT   0x00000001
#define TYPE_ERASE  0x00000002
#define TYPE_START  0x00000003
#define TYPE_DONE   0x00000004
#define TYPE_END    0x00000005

// 尺寸,以字为单位
typedef struct{
    volatile uint32_t type;
    volatile uint32_t all_size;
    volatile uint32_t mem_base;
    volatile uint32_t mem_size;
    volatile uint32_t data[4096/4];
}info_struct;

// 限制结构体到指定地址,主控要访问这段内存
static info_struct g_sram __attribute__((at(0x20009000)));

int main(void)
{
	//初始化,初始化完成之后把状态设置为初始化
    g_sram.type=TYPE_INIT;
    while(1)
    {
        if(g_sram.type==TYPE_ERASE)
        {
        	//接收到擦除命令
            int num=g_sram.all_size/1024+1;
            for(int i=0;i<num;i++)
            {
                nrf_nvmc_page_erase(i*4096);
            }
            g_sram.type=TYPE_DONE;
        }
        else if(g_sram.type==TYPE_START)
        {
        	//接收到开始写入命令
            nrf_nvmc_write_words(g_sram.mem_base,(const uint32_t *)g_sram.data,g_sram.mem_size);
            g_sram.type=TYPE_DONE;
        }
        else if(g_sram.type==TYPE_END)
        {
        	//接收到结束命令,重启
            NVIC_SystemReset();
        }
    }
}




2、编写 SWD接口驱动

编写 swd.h 文件如下:



#ifndef SWD_H__
#define SWD_H__


#ifdef __cplusplus
	extern "C" {
#endif


//命令字定义											
#define SWD_REG_DP 		0x00
#define SWD_REG_AP 		0x02
#define SWD_READ	 	0x04
#define SWD_WRITE	 	0x00

//DP寄存器											
#define SWD_REG_IDCODE 			(0x00<<1)//只读
#define SWD_REG_ABORT 			(0x00<<1)//只写
#define SWD_REG_CTRL_STAT		(0x04<<1)//读写
#define SWD_REG_RESEND			(0x08<<1)//只读
#define SWD_REG_SELECT			(0x08<<1)//只写
#define SWD_REG_RDBUFF			(0x0c<<1)//只读

//AP寄存器
#define SWD_REG_CSW					(0x00<<1)
#define SWD_REG_TAR					(0x04<<1)										
#define SWD_REG_BASE				(0x08<<1)
#define SWD_REG_DRW					(0x0c<<1)
#define SWD_REG_IDR					(0x0c<<1)

//通信错误类型
#define SWD_ERR_OK					(0x01)
#define SWD_ERR_WAIT				(0x02)
#define SWD_ERR_FAIL				(0x04)
#define SWD_ERR_OTHER				(0x07)



// Cortex M3 Debug Registers (AHB addresses)
#define CM3_DDFSR   0xE000ED30      // Debug Fault StatusRegister
#define CM3_DHCSR   0xE000EDF0      // Debug Halting Control and Status Register
#define CM3_DCRSR   0xE000EDF4      // Debug Core Register Selector Register
#define CM3_DCRDR   0xE000EDF8      // Debug Core Register Data Register
#define CM3_DEMCR   0xE000EDFC      // Debug Exception and Monitor Control Register
#define CM3_AIRCR   0xE000ED0C      // The Application Interrupt and Reset Control Register

//读写内核寄存器
#define CM3_REG_READ 		0x00000000
#define CM3_REG_WRITE		0x00010000

//内核寄存器选址
#define CM3_REG_R0      0x0000
#define CM3_REG_R1      0x0001
#define CM3_REG_R2      0x0002
#define CM3_REG_R3      0x0003
#define CM3_REG_R4      0x0004
#define CM3_REG_R5      0x0005
#define CM3_REG_R6      0x0006
#define CM3_REG_R7      0x0007
#define CM3_REG_R8      0x0008
#define CM3_REG_R9      0x0009
#define CM3_REG_R10     0x000a
#define CM3_REG_R11     0x000b
#define CM3_REG_R12     0x000c
#define CM3_REG_R13     0x000d
#define CM3_REG_SP      0x000d
#define CM3_REG_R14     0x000e
#define CM3_REG_LR      0x000e
#define CM3_REG_R15     0x000f
#define CM3_REG_PC      0x000f
#define CM3_REG_xPSR    0x0010
#define CM3_REG_MSP     0x0011
#define CM3_REG_PSP     0x0012



//初始化
void SWD_Init (void);

//总线复位
void SWD_LineReset (void);

//读取寄存器
u32 SWD_ReadReg (u8 cmd,u8 *err);

//写入寄存器
void SWD_WriteReg (u8 cmd,u8 *err,u32 data);

//向内存写入数据,成功返回设备id
u32 SWD_WriteSram (u32 addr,u32 *data,u32 len);

//向内存读取数据,成功返回设备id
u32 SWD_ReadSram (u32 addr,u32 *data,u32 len);


//暂停或者启动内核1,暂停,0,启动
u32 SWD_Cm3Halt (u8 value);

//向内核寄存器写入数据
u32 SWD_WriteCm3Reg (u16 reg_select,u32 data);

#ifdef __cplusplus
	}
#endif
		




#endif





编写 swd.c 文件如下:

#include "sys.h"
#include "swd.h"


// 本文以 stm32f103 的GPIO PB6、PB7 为例

#define SWD_CLK 		PBout(6)
#define SWD_DIO_OUT 	PBout(7)
#define SWD_DIO_IN		PBin(7)

//用寄存器方式设置PB7为上拉输入模式
#define SWD_DIN() 	{u32 temp=GPIOB->CRL&0x0fffffff;\
											GPIOB->CRL=temp|(0x8<<(4*(7)));\
											GPIOB->BSRR=1<<7;}

//用寄存器方式设置PB7为推挽输出模式
#define SWD_DOUT() 	{u32 temp=GPIOB->CRL&0x0fffffff;\
											GPIOB->CRL=temp|(0x3<<(4*(7)));}

				
											

void SWD_Init (void)
{
	GPIO_InitTypeDef  GPIO_InitStructure;

	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);	 
	RCC_APB2PeriphClockCmd(	RCC_APB2Periph_AFIO, ENABLE );//PORTB时钟使能 
	GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable,ENABLE);
	
	//使用GPIO来模拟SWD接口
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6|GPIO_Pin_7; //PB6、PB7
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;	//推挽输出
	GPIO_Init(GPIOB, &GPIO_InitStructure); 
	
	SWD_CLK=0;
	SWD_DIO_OUT=1;
}




//低位在前,高位在后
//上升沿刷新,下降沿锁存
void SWD_SendByte (u8 byte)
{
	SWD_DOUT();
	for (u8 i=0;i<8;i++)
	{
		SWD_DIO_OUT=byte>>i;
		SWD_CLK=1;
		SWD_CLK=0;
	}
}


u8 SWD_RecvByte (void)
{
	u8 byte=0;
	SWD_DIN();
	for (u8 i=0;i<8;i++)
	{
		byte>>=1;
		byte|=(SWD_DIO_IN<<7);
		SWD_CLK=1;
		SWD_CLK=0;
	}
	return byte;
}


void SWD_Turn (void)
{
	SWD_CLK=1;
	SWD_CLK=0;
}


u8 SWD_Ask (void)
{
	u8 ask=0;
	SWD_DIN();
	for (u8 i=0;i<3;i++)
	{
		ask>>=1;
		ask|=(SWD_DIO_IN<<2);
		SWD_CLK=1;
		SWD_CLK=0;
	}
	return ask;
}


u8 SWD_Praity (void)
{
	u8 par=0;
	SWD_DIN();
	par=SWD_DIO_IN;
	SWD_CLK=1;
	SWD_CLK=0;
	return par;
}

void SWD_SendPraity (u8 pra)
{
	SWD_DOUT();
	SWD_DIO_OUT=pra;
	SWD_CLK=1;
	SWD_CLK=0;

}


void SWD_LineReset (void)
{
	for (u8 i=0;i<7;i++)
	{
		SWD_SendByte(0xff);
	}
	SWD_SendByte(0x9e);//低字节在前
	SWD_SendByte(0xe7);
	for (u8 i=0;i<7;i++)
	{
		SWD_SendByte(0xff);
	}
	SWD_SendByte(0xb6);//低字节在前
	SWD_SendByte(0xed);
	for (u8 i=0;i<7;i++)
	{
		SWD_SendByte(0xff);
	}
	SWD_SendByte(0x00);//低字节在前
	SWD_SendByte(0x00);

}


u32 SWD_ReadReg (u8 cmd,u8 *err)
{
	cmd|=0x81|SWD_READ; 
	u8 _1num=0;
	for (u8 i=0;i<4;i++)
	{
		if (cmd&(1<<(i+1)))
		{
			_1num++;
		}
	}
	if (_1num%2) cmd|=1<<5;
	
	
	u8 ask=0;
	do 
	{
		SWD_SendByte(cmd);
		SWD_Turn();
		ask=SWD_Ask();
	}while (ask==SWD_ERR_WAIT);
	*err=ask;
	
	u32 data=0;
	u8 praity=0;
	data|=SWD_RecvByte();
	data|=SWD_RecvByte()<<8;
	data|=SWD_RecvByte()<<16;
	data|=SWD_RecvByte()<<24;
	praity=SWD_Praity();
	SWD_SendByte(0);
	//SWD_Turn();
	_1num=0;
	for (u8 i=0;i<32;i++)
	{
		if (data&(1<<i))
		{
			_1num++;
		}
	}
	//delay_us(170);
	if ((_1num%2)==(praity&1))//校验成功
	{
		return data;
	}
	else 
	{
		*err=0x07;
		return 0;
	}	

}


void SWD_WriteReg (u8 cmd,u8 *err,u32 data)
{
	cmd|=0x81|SWD_WRITE;
	u8 _1num=0;
	for (u8 i=0;i<4;i++)
	{
		if (cmd&(1<<(i+1)))
		{
			_1num++;
		}
	}
	if (_1num%2) cmd|=1<<5;
	
	u8 ask=0;
	do 
	{
		SWD_SendByte(cmd);		
		SWD_Turn();
		ask=SWD_Ask();
		SWD_Turn();
	}while (ask==SWD_ERR_WAIT);
	*err=ask;
	
	
	_1num=0;
	for (u8 i=0;i<32;i++)
	{
		if (data&(1<<i))
		{
			_1num++;
		}
	}
	SWD_SendByte(data&0xff);
	SWD_SendByte(data>>8);
	SWD_SendByte(data>>16);
	SWD_SendByte(data>>24);
	//SWD_SendPraity(_1num%2);
	SWD_SendByte(_1num%2);
	//delay_us(170);

}



//向内存写入数据,成功返回设备id
u32 SWD_WriteSram (u32 addr,u32 *data,u32 len)
{
	u8 err=0;
	u32 id=0;
	//SWD_LineReset();
	id=SWD_ReadReg(SWD_REG_DP|SWD_REG_IDCODE,&err);
	if (err!=SWD_ERR_OK) return 0;
	SWD_WriteReg(SWD_REG_DP|SWD_REG_ABORT,&err,0x1e);
	if (err!=SWD_ERR_OK) return 0;
	SWD_WriteReg(SWD_REG_DP|SWD_REG_CTRL_STAT,&err,0x50000000);
	if (err!=SWD_ERR_OK) return 0;
	SWD_WriteReg(SWD_REG_DP|SWD_REG_SELECT,&err,0x00);
	if (err!=SWD_ERR_OK) return 0;
	SWD_WriteReg(SWD_REG_AP|SWD_REG_CSW,&err,0x23000012);//地址自动递增
	if (err!=SWD_ERR_OK) return 0;
	SWD_WriteReg(SWD_REG_AP|SWD_REG_TAR,&err,addr);//在这个地址的内存空间写数据
	if (err!=SWD_ERR_OK) return 0;
	while (len--)
	{
		SWD_WriteReg(SWD_REG_AP|SWD_REG_DRW,&err,*data);
		if (err!=SWD_ERR_OK) return 0;
		data++;
	}
	return id;
}



//向内存读取数据,成功返回设备id
u32 SWD_ReadSram (u32 addr,u32 *data,u32 len)
{
	u8 err=0;
	u32 id=0;
	//SWD_LineReset();
	id=SWD_ReadReg(SWD_REG_DP|SWD_REG_IDCODE,&err);
	if (err!=SWD_ERR_OK) return 0;
	SWD_WriteReg(SWD_REG_DP|SWD_REG_ABORT,&err,0x1e);
	if (err!=SWD_ERR_OK) return 0;
	SWD_WriteReg(SWD_REG_DP|SWD_REG_CTRL_STAT,&err,0x50000000);
	if (err!=SWD_ERR_OK) return 0;
	SWD_WriteReg(SWD_REG_DP|SWD_REG_SELECT,&err,0x00);
	if (err!=SWD_ERR_OK) return 0;
	SWD_WriteReg(SWD_REG_AP|SWD_REG_CSW,&err,0x23000012);//地址自动递增
	if (err!=SWD_ERR_OK) return 0;
	SWD_WriteReg(SWD_REG_AP|SWD_REG_TAR,&err,addr);//在这个地址的内存空间数据
	if (err!=SWD_ERR_OK) return 0;
	*data=SWD_ReadReg(SWD_REG_AP|SWD_REG_DRW,&err);//读取一次无效数据
	if (err!=SWD_ERR_OK) return 0;

	while (len--)
	{
		*data=SWD_ReadReg(SWD_REG_AP|SWD_REG_DRW,&err);
		if (err!=SWD_ERR_OK) return 0;
		data++;
	}
	return id;
}





//暂停或者启动内核1,暂停,0,启动
u32 SWD_Cm3Halt (u8 value)
{
	u8 err=0;
	u32 id=0;
	//SWD_LineReset();
	id=SWD_ReadReg(SWD_REG_DP|SWD_REG_IDCODE,&err);
	if (err!=SWD_ERR_OK) return 0;
	SWD_WriteReg(SWD_REG_DP|SWD_REG_ABORT,&err,0x1e);
	if (err!=SWD_ERR_OK) return 0;
	SWD_WriteReg(SWD_REG_DP|SWD_REG_CTRL_STAT,&err,0x50000000);
	if (err!=SWD_ERR_OK) return 0;
	SWD_WriteReg(SWD_REG_DP|SWD_REG_SELECT,&err,0x00);
	if (err!=SWD_ERR_OK) return 0;
	SWD_WriteReg(SWD_REG_AP|SWD_REG_CSW,&err,0x23000002);
	if (err!=SWD_ERR_OK) return 0;
	SWD_WriteReg(SWD_REG_AP|SWD_REG_TAR,&err,CM3_DHCSR);//暂停CM3内核
	if (err!=SWD_ERR_OK) return 0;
	if (value)
	{
		SWD_WriteReg(SWD_REG_AP|SWD_REG_DRW,&err,0xa05f0001);//写入钥匙,使能调试
		if (err!=SWD_ERR_OK) return 0;
		SWD_WriteReg(SWD_REG_AP|SWD_REG_TAR,&err,CM3_DEMCR);//使能复位后停止
		if (err!=SWD_ERR_OK) return 0;
		SWD_WriteReg(SWD_REG_AP|SWD_REG_DRW,&err,0x1);//
		if (err!=SWD_ERR_OK) return 0;
		SWD_WriteReg(SWD_REG_AP|SWD_REG_TAR,&err,CM3_AIRCR);//复位
		if (err!=SWD_ERR_OK) return 0;
		SWD_WriteReg(SWD_REG_AP|SWD_REG_DRW,&err,0x05fa0004);//
		if (err!=SWD_ERR_OK) return 0;
	}
	else
	{
		SWD_WriteReg(SWD_REG_AP|SWD_REG_DRW,&err,0xa05f0000);//写入钥匙,使能调试
		if (err!=SWD_ERR_OK) return 0;
	}
	return id;
}





//向内核寄存器写入数据
u32 SWD_WriteCm3Reg (u16 reg_select,u32 data)
{
	u8 err=0;
	u32 id=0;
	//SWD_LineReset();
	id=SWD_ReadReg(SWD_REG_DP|SWD_REG_IDCODE,&err);
	if (err!=SWD_ERR_OK) return 0;
	SWD_WriteReg(SWD_REG_DP|SWD_REG_ABORT,&err,0x1e);
	if (err!=SWD_ERR_OK) return 0;
	SWD_WriteReg(SWD_REG_DP|SWD_REG_CTRL_STAT,&err,0x50000000);
	if (err!=SWD_ERR_OK) return 0;
	SWD_WriteReg(SWD_REG_DP|SWD_REG_SELECT,&err,0x00);
	if (err!=SWD_ERR_OK) return 0;
	SWD_WriteReg(SWD_REG_AP|SWD_REG_CSW,&err,0x23000002);
	if (err!=SWD_ERR_OK) return 0;
	SWD_WriteReg(SWD_REG_AP|SWD_REG_TAR,&err,CM3_DCRDR);//内核数据寄存器
	if (err!=SWD_ERR_OK) return 0;
	SWD_WriteReg(SWD_REG_AP|SWD_REG_DRW,&err,data);
	if (err!=SWD_ERR_OK) return 0;
	SWD_WriteReg(SWD_REG_AP|SWD_REG_TAR,&err,CM3_DCRSR);//选择内核寄存器
	if (err!=SWD_ERR_OK) return 0;
	SWD_WriteReg(SWD_REG_AP|SWD_REG_DRW,&err,reg_select|CM3_REG_WRITE);
	if (err!=SWD_ERR_OK) return 0;
	
	u32 dhcsr_data=0;
	SWD_WriteReg(SWD_REG_AP|SWD_REG_TAR,&err,CM3_DHCSR);//
	if (err!=SWD_ERR_OK) return 0;
	dhcsr_data=SWD_ReadReg(SWD_REG_AP|SWD_REG_DRW,&err);
	if (err!=SWD_ERR_OK) return 0;
	do
	{
		dhcsr_data=SWD_ReadReg(SWD_REG_AP|SWD_REG_DRW,&err);
		if (err!=SWD_ERR_OK) return 0;
	}while ((dhcsr_data&0x00010000)==0);
	return id;
}


3、编写从机用户程序代码

本示例用户程序主要用于演示,则编写led闪烁的代码来表示实验结果,如果led如预期闪烁,则实验成功。

#include "nrf_gpio.h"
#include "nrf_radio.h"




int delay(int n)
{
    for(int i=0;i<n;i++)
        for(int j=0;j<1000;j++);
    return 0;
}



int main(void)
{
    nrf_gpio_pin_set(17);
    nrf_gpio_cfg_output(17);
    nrf_gpio_pin_set(18);
    nrf_gpio_cfg_output(18);
    
    while(1)
    {
        nrf_gpio_pin_set(17);
        nrf_gpio_pin_clear(18);
        delay(5000);
        nrf_gpio_pin_clear(17);
        nrf_gpio_pin_set(18);
        delay(5000);
    }
}



4、编写下载从机程序的代码

// 定义 acloader 数据,数组内容是从机的 ram BootLoader 程序
#include "loader.c"
// 定义 acled 数组,数组内容是本示例的从机用户程序
#include "led.c"

#define TYPE_NONE   0x00000000
#define TYPE_INIT   0x00000001
#define TYPE_ERASE  0x00000002
#define TYPE_START  0x00000003
#define TYPE_DONE   0x00000004
#define TYPE_END    0x00000005


// 访问从机 0x20009000 地址的内存
#define TYPE_ADDR       (0x20009000)
#define ALLSIZE_ADDR    (0x20009004)
#define MEMBASE_ADDR    (0x20009008)
#define MEMSIZE_ADDR    (0x2000900c)

int main(void)
{
    u8 err;
    u32 id;
    u32 data;
	// 初始化swd接口,复位总线
    SWD_Init();
    SWD_LineReset();
    
    // 写入从机 ram BootLoader 程序,等待从机初始化
    data=TYPE_NONE;
    SWD_WriteSram(TYPE_ADDR,&data,1);
    id=SWD_Cm3Halt(1);
    id=SWD_WriteSram(0x20000000,(uint32_t *)acloader,sizeof(acloader)/4+1);
    id=SWD_WriteCm3Reg(CM3_REG_MSP,((uint32_t *)acloader)[0]);
    id=SWD_WriteCm3Reg(CM3_REG_PC,((uint32_t *)acloader)[1]);
    SWD_Cm3Halt(0);
    while(SWD_ReadSram(TYPE_ADDR,&data,1),data!=TYPE_INIT);
    
    uint32_t *sram=(uint32_t *)acled;
    uint32_t all_size=sizeof(acled)/4+1;
    uint32_t mem_size=0;
    uint32_t mem_base=0;
    uint32_t times=(all_size+1023)/1024;
    
    // 擦除从机flash,等待擦除完毕
    id=SWD_WriteSram(ALLSIZE_ADDR,&all_size,1);
    data=TYPE_ERASE;
    id=SWD_WriteSram(TYPE_ADDR,&data,1);
    while(SWD_ReadSram(TYPE_ADDR,&data,1),data!=TYPE_DONE);
    
    // 循环写入从机flash
    for(int i=0;i<times;i++)
    {
        mem_size=1024;
        if(mem_size>all_size) mem_size=all_size;
        id=SWD_WriteSram(0x20009000+16,sram,mem_size);
        id=SWD_WriteSram(MEMSIZE_ADDR,&mem_size,1);
        id=SWD_WriteSram(MEMBASE_ADDR,&mem_base,1);
        data=TYPE_START;
        id=SWD_WriteSram(TYPE_ADDR,&data,1);
        while(SWD_ReadSram(TYPE_ADDR,&data,1),data!=TYPE_DONE);
        mem_base+=mem_size*4;
        all_size-=mem_size;
    }
    
    // 从机重启
    data=TYPE_END;
    id=SWD_WriteSram(TYPE_ADDR,&data,1);
    
    // 死循环,不做任何事
    while(1)
    {
        id=SWD_ReadReg(SWD_REG_DP|SWD_REG_IDCODE,&err);
        if(id)
        {
            memset(&id,0,sizeof(int));
        }
    }
}

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值