STM32 CustomHID 的实现

如何建立一个自定义的HID工程呢?下面就来讲讲。
首先先介绍下工程的架构,工程的总体架构下图所示,按照下图架构建工程:
STM32 CustomHID 的实现 - ziye334 - ziye334的博客

分析下工程布局,首先是APP,这个组里存放着主文件mian.c,管理所有中断服务程序stm3210x_it.c,及其管理外设库头文件的stm32f10x_conf.h。BSP这个组里存放着BSP.c,外设的洗衣初始化都在这个函数中定义,比如说串口的配置,LED灯的配置,系统时钟的配置,各类NVIC的中断配置。在这个文件中,会定义一个BSP_Init()函数,所有配置的都在这个函数中调用,例如:

void BSP_Init(void) { RCC_Configuration(); Set_USBClock(); USB_Init(); USART1_Configuration(115200); LED_Configuration(); NVIC_Configuration(); USB_Interrupts_Config(); }

而这个BSP_Init()函数在main中调用,这样就使主函数简洁漂亮了。至于CMSIS这个组则是关于Cotex-M3内核的相关文件看,如core_cm3.c和system_stm32f10x.c。StartUp这个组放置系统的启动文件,不同系类的处理器使用不同的启动文件,这里有必要了解:

- startup_stm32f10x_ld_vl.s: for STM32 Low density Value line devices - startup_stm32f10x_ld.s: for STM32 Low density devices - startup_stm32f10x_md_vl.s: for STM32 Medium density Value line devices - startup_stm32f10x_md.s: for STM32 Medium density devices - startup_stm32f10x_hd.s: for STM32 High density devices - startup_stm32f10x_xl.s: for STM32 XL density devices - startup_stm32f10x_cl.s: for STM32 Connectivity line devices cl:互联型产品,stm32f105/107系列 vl:超值型产品,stm32f100系列 xl:超高密度产品,stm32f101/103系列 ld:低密度产品,FLASH小于64K md:中等密度产品,FLASH=64 or 128 hd:高密度产品,FLASH大于128

我的这个工程选择高密度型的: startup_stm32f10x_hd.s。USB_User文件组放着USB控制与应用相关的文件,在之前的文章每个文件都详细介绍过。接着是USB-FS-Device_Driver这个组放着USB的驱动,在之前的文章页已经讲述过。最后一个组是STM32F10x_StdPeriph_Driver,它里面存放着外设库文件的驱动代码,很多人为了省事,会把所有的C文件都添加进来,我不建议这么做,还是根据需要添加对应的文件,就拿我们的这个CustomHID工程,我们用到了引脚GPIO、时钟的配置,串口的配置,所以只要添加这几个对应的C库文件就可以了。
  上面的各个文件大部分可以网上下载的。

接下去就讲述如何实现CustomHID功能的。
首先,最重要的文件当然是usb_desc.c这个文件了。这个文件存放着各种描述符,比如说设备描述符、配置描述符等,下面就一一介绍。
设备描述符符的定义如下:

/* USB标准设备描述符*/ const uint8_t CustomHID_DeviceDescriptor[CUSTOMHID_SIZ_DEVICE_DESC] = { 0x12, /*bLength:长度,设备描述符的长度为18字节*/ USB_DEVICE_DESCRIPTOR_TYPE, /*bDescriptorType:类型,设备描述符的编号是0x01*/ 0x00, /*bcdUSB:所使用的USB版本为2.0*/ 0x02, 0x00, /*bDeviceClass:设备所使用的类代码*/ 0x00, /*bDeviceSubClass:设备所使用的子类代码*/ 0x00, /*bDeviceProtocol:设备所使用的协议*/ 0x40, /*bMaxPacketSize:最大包长度为64字节*/ 0x34, /*idVendor:厂商ID为0x1234*/ 0x12, 0x10, /*idProduct:产品ID为0x1010*/ 0x10, 0x00, /*bcdDevice:设备的版本号为2.00*/ 0x02, 1, /*iManufacturer:厂商字符串的索引*/ 2, /*iProduct:产品字符串的索引*/ 3, /*iSerialNumber:设备的序列号字符串索引*/ 0x01 /*bNumConfiguration:设备有1种配置*/ }; /* CustomHID设备描述符 */

设备描述符的数组的长度一般为9个字节,该描述符定义了USB协议代号、厂商ID(VID),产品ID(PID)、设备的版本号、以及厂商产品序列号描述符的索引。在USB枚举阶段,USB设备需要通过端口0向USB主机发送设备描述符。
配置描述符集合里有着丰富的USB设备的信息,如用了几个接口,用了几个端点,USB设备做什么用等。代码如下:

/* USB配置描述符集合(配置、接口、端点、类、厂商)(Configuration, Interface, Endpoint, Class, Vendor */ const uint8_t CustomHID_ConfigDescriptor[CUSTOMHID_SIZ_CONFIG_DESC] = { 0x09, /*bLength:长度,设备字符串的长度为9字节*/ USB_CONFIGURATION_DESCRIPTOR_TYPE, /*bDescriptorType:类型,配置描述符的类型编号为0x2*/ CUSTOMHID_SIZ_CONFIG_DESC, /*wTotalLength:配置描述符的总长度为41字节*/ 0x00, 0x01, /*bNumInterfaces:配置所支持的接口数量1个*/ 0x01, /*bConfigurationValue:该配置的值*/ 0x00, /*iConfiguration:该配置的字符串的索引值,该值为0表示没有字符串*/ 0xC0, /* bmAttributes:设备的一些特性,0xc0表示自供电,不支持远程唤醒 D7:保留必须为1,D6:是否自供电,D5:是否支持远程唤醒,D4~D0:保留设置为0*/ // 0x32, /*从总线上获得的最大电流为100mA */ 0x96, /*MaxPower:设备需要从总线上获取多少电流,单位为2mA,0x96表示300mA*/ /************** HID接口描述符****************/ 0x09, /*bLength:长度,接口描述符的长度为9字节 */ USB_INTERFACE_DESCRIPTOR_TYPE,/* bDescriptorType:接口描述符的类型为0x4 */ 0x00, /*bInterfaceNumber:该接口的编号*/ 0x00, /*bAlternateSetting:该接口的备用编号 */ 0x02, /*bNumEndpoints:该接口所使用的端点数*/ 0x03, /*bInterfaceClass该接口所使用的类为HID*/ 0x00, /*bInterfaceSubClass:该接口所用的子类 1=BOOT, 0=no boot */ 0x00, /*nInterfaceProtocol :该接口使用的协议0=none, 1=keyboard, 2=mouse */ 0, /*iInterface: 该接口字符串的索引 */ /*****************HID描述符 ********************/ 0x09, /*bLength: HID描述符的长度为9字节 */ HID_DESCRIPTOR_TYPE, /* bDescriptorType: HID的描述符类型为0x21 */ 0x10, /*bcdHID: HID协议的版本为1.1 */ 0x01, 0x00, /*bCountryCode: 国家代号 */ 0x01, /*bNumDescriptors: 下级描述符的数量*/ 0x22, /*bDescriptorType:下级描述符的类型*/ CUSTOMHID_SIZ_REPORT_DESC,/* wItemLength: 下一集描述符的长度*/ 0x00, /********************输入端点描述符******************/ 0x07, /* bLength: 端点描述符的长度为7字节*/ USB_ENDPOINT_DESCRIPTOR_TYPE, /* bDescriptorType: 端点描述符的类型为0x21*/ 0x82, /* bEndpointAddress: 该端点(输入)的地址,D7:0(OUT),1(IN),D6~D4:保留,D3~D0:端点号*/ 0x03, /* bmAttributes: 端点的属性为为中断端点. D0~D1表示传输类型:0(控制传输),1(等时传输),2(批量传输),3(中断传输) 非等时传输端点:D2~D7:保留为0 等时传输端点: D2~D3表示同步的类型:0(无同步),1(异步),2(适配),3(同步) D4~D5表示用途:0(数据端点),1(反馈端点),2(暗含反馈的数据端点),3(保留),D6~D7:保留,*/ 0x40, /* wMaxPacketSize: 该端点支持的最大包长度为64字节*/ 0x00, 0x02, /* bInterval: 轮询间隔(2 ms) */ /********************输出端点描述符******************/ 0x07, /* 端点描述符的长度为7字节 */ USB_ENDPOINT_DESCRIPTOR_TYPE, /* bDescriptorType: 端点描述符的类型为0x21*/ 0x01, /* bEndpointAddress: 该端点(输出)的地址,D7:0(OUT),1(IN),D6~D4:保留,D3~D0:端点号*/ 0x03, /* bmAttributes: 端点的属性为为中断端点 */ 0x40, /* wMaxPacketSize: 该端点支持的最大包长度为64字节 */ 0x00, 0x02, /* bInterval: 轮询间隔(2 ms) */ };

从上面的代码中可以看出,USB设备使用了1个接口、两个端点:一个中断传输输入端点,端点号为2;一个中断传输的输出端点,端点号为1、每个端点能通讯的最大数据包长度为64字节、USB的功能自定义等。配置描述符是在USB主机发送GET_CONFIGURATION请求时,USB设备发送的。
还有一个很重要的当然报告描述符了:

/* HID的报告描述符*/ const uint8_t CustomHID_ReportDescriptor[CUSTOMHID_SIZ_REPORT_DESC] = { /*short Item D7~D4:bTag;D3~D2:bType;D1~D0:bSize **bTag ---主条目 1000:输入(Input) 1001:输出(Output) 1011:特性(Feature) 1010:集合(Collection) 1100:关集合(End Collection) ** 全局条目 0000:用途页(Usage Page) 0001:逻辑最小值(Logical Minimum) 0010:逻辑最大值(Logical Maximum) 0011:物理最小值(Physical Minimum) ** 0100:物理最大值(Physical Maximum) 0101:单元指数(Unit Exponet) 0110:单元(Unit) 0111:数据域大小(Report Size) ** 1000:报告ID(Report ID) 1001:数据域数量(Report Count) 1010:压栈(Push) 1011:出栈(Pop) 1100~1111:保留(Reserved) ** 局部条目 0000:用途(Usage) 0001:用途最小值(Usage Minimum) 0010:用途最大值(Usage Maximum) 0011:标识符索引(Designator Index) ** 0100:标识符最小值(Designator Minimum) 0101:标识符最大值(Designator Maximum) 0111:字符串索引(String Index) 1000:字符串最小值(String Minimum) ** 1001:字符串最大值(String Maximum) 1010:分隔符(Delimiter) 其他:保留(Reserved) **bType---00:主条目(main) 01:全局条目(globle) 10:局部条目(local) 11:保留(reserved) **bSize---00:0字节 01:1字节 10:2字节 11:4字节*/ //0x05:0000 01 01 这是个全局条目,用途页为ST页 0x05, 0x8c, /* USAGE_PAGE (ST Page) */ //0x09:0000 10 01 这是个局部变量,用途为Demo Kit 0x09, 0x01, /* USAGE (Demo Kit) */ //0xa1:1010 00 01 这是一个主条目,集合为应用集合 0xa1, 0x01, /* COLLECTION (Application) */ /* 输入报告*/ //0x09:0000 10 01 这是个局部条目,用途为厂商ID 0x09,0x03, // USAGE ID - Vendor defined //0x15:0001 01 01 这是个全局条目,逻辑最小值为0 0x15,0x00, // LOGICAL_MINIMUM (0) //0x26:0010 01 10 这是个全局条目,逻辑最大值为255 0x26,0x00, 0xFF, // LOGICAL_MAXIMUM (255) //0x75:0111 01 01 这是个全局条目,报告大小为8位 0x75,0x08, // REPORT_SIZE (8bit) //0x95:1001 01 01 这是个全局条目,报告数量为64 0x95,0x40, // REPORT_COUNT (64Byte) //0x81:1000 00 01 这是个主条目,做输入,Data表示这些数据可变,Var表示这些徐居于是独立的变量,Abs表示绝对值 0x81,0x02, // INPUT (Data,Var,Abs) /*输出报告*/ //0x09:0000 10 01 这是个局部条目,用途为厂商ID 0x09,0x04, // USAGE ID - Vendor defined //0x15:0001 01 01 这是个全局条目,逻辑最小值为0 0x15,0x00, // LOGICAL_MINIMUM (0) //0x26:0010 01 10 这是个全局条目,逻辑最大值为255 0x26,0x00,0xFF, // LOGICAL_MAXIMUM (255) //0x75:0111 01 01 这是个全局条目,报告大小为8位 0x75,0x08, // REPORT_SIZE (8bit) //0x95:1001 01 01 这是个全局条目,报告数量为64 0x95,0x40, // REPORT_COUNT (64Byte) //0x91:1001 00 01 这是个全局条目,做输出,Data表示这些数据可变,Var表示这些徐居于是独立的变量,Abs表示绝对值 0x91,0x02, // OUTPUT (Data,Var,Abs) 0xc0 /* END_COLLECTION */ };

关于配置描述符也是至关重要的,它规定了USB通讯的长度,具体格式。据上面的报告描述符说:定义了64*8bit的数据域作为输入,属性是Data、Var、Abs,也就是说USB设备想USB主机每次发送64字节的数据包,每个数据的值(0~255之间)可以用户自定义;还定义了64*8bit的数据域作为输出,属性是Data、Var、Abs,也就是说USB主机箱USB设备每次发送64字节的数据包,每个数据的值(0~255)由USB主机自己定义。
接下的一些说明描述符代码如下,就不详细介绍了:

/* 语言ID描述符 */ const uint8_t CustomHID_StringLangID[CUSTOMHID_SIZ_STRING_LANGID] = { CUSTOMHID_SIZ_STRING_LANGID, /*bLength:本描述符的长度为4字节*/ USB_STRING_DESCRIPTOR_TYPE, /*bDescriptorType:字符串描述符的类型为0x03*/ 0x09, /*bString:语言ID为0x0409,表示美式英语*/ 0x04 }; /* LangID = 0x0409: U.S. English*/ /*厂商字符串描述符*/ const uint8_t CustomHID_StringVendor[CUSTOMHID_SIZ_STRING_VENDOR] = { CUSTOMHID_SIZ_STRING_VENDOR, /*bLength:厂商字符串描述符的长度*/ USB_STRING_DESCRIPTOR_TYPE, /*bDescriptorType:字符串描述符的类型为0x03*/ 'M', 0, 'y', 0, 'U', 0,'S', 0,'B', 0, '_', 0, 'H', 0,'I',0,'D',0 /*自定义*/ }; /*产品的字符串描述符*/ const uint8_t CustomHID_StringProduct[CUSTOMHID_SIZ_STRING_PRODUCT] = { CUSTOMHID_SIZ_STRING_PRODUCT, /* bLength:产品的字符串描述符*/ USB_STRING_DESCRIPTOR_TYPE, /* bDescriptorType:字符串描述符的类型为0x03*/ 'B', 0, 'y', 0, ' ', 0, 'v', 0, 'i', 0, 'e', 0,'w',0,'t',0,'o',0,'o',0,'l',0/*自定义*/ }; /*产品序列号的字符串描述符*/ uint8_t CustomHID_StringSerial[CUSTOMHID_SIZ_STRING_SERIAL] = { CUSTOMHID_SIZ_STRING_SERIAL, /* bLength:产品序列号*/ USB_STRING_DESCRIPTOR_TYPE, /* bDescriptorType:字符串描述符的类型为0x03*/ 'x', 0, 'x', 0, 'x', 0,'x', 0,'x', 0, 'x', 0, 'x', 0 /*自定义*/ };


接下去需要改动的的是usb_prop.c这个文件里的内容。这个文件大部分不需要膝盖,只要修改下CustomHID_Reset()这个函数(名字不定相同)。这个函数的定义如下:

/******************************************************************************* * Function Name : CustomHID_Reset. * Description : CustomHID Mouse reset routine.复位 * Input : None. * Output : None. * Return : None. *******************************************************************************/ void CustomHID_Reset(void) { /* Set CustomHID_DEVICE as not configured */ pInformation->Current_Configuration = 0; //设置当前的配置为0,表示没有配置过 pInformation->Current_Interface = 0;//默认的接口 /* Current Feature initialization */ pInformation->Current_Feature = CustomHID_ConfigDescriptor[7];//当前的属性,bmAttributes:设备的一些特性,0xc0表示自供电,不支持远程唤醒 #ifdef STM32F10X_CL /* EP0 is already configured in DFU_Init() by USB_SIL_Init() function */ /* Init EP1 IN snd EP1 OUT as Interrupt endpoint */ OTG_DEV_EP_Init(EP1_IN, OTG_DEV_EP_TYPE_INT, EP1_SIZE); OTG_DEV_EP_Init(EP1_OUT, OTG_DEV_EP_TYPE_INT, EP1_SIZE); #else SetBTABLE(BTABLE_ADDRESS); /*--------------------------------------------------------------------------*/ /* Initialize Endpoint 0 */ SetEPType(ENDP0, EP_CONTROL); //设置端点1为控制端点 SetEPTxStatus(ENDP0, EP_TX_STALL); //设置端点0发送延时 SetEPRxAddr(ENDP0, ENDP0_RXADDR); //设置端点0的接收缓冲区地址 SetEPTxAddr(ENDP0, ENDP0_TXADDR); //设置端点0的发送缓冲区地址 Clear_Status_Out(ENDP0); //清除端点0的状态 SetEPRxCount(ENDP0, Device_Property.MaxPacketSize);//设置端点0的接收的计数 SetEPRxValid(ENDP0); //使能接收状态 /* Initialize Endpoint 1 */ SetEPType(ENDP1, EP_INTERRUPT); //设置端点1为中断控制端点 SetEPRxAddr(ENDP1, ENDP1_RXADDR); //设置端点1的接收缓冲地址 SetEPRxCount(ENDP1, REPORT_COUNT); //设置端点1的接收计数 SetEPRxStatus(ENDP1, EP_RX_VALID); //设置端点1接收有效 //SetEPTxStatus(ENDP1, EP_TX_DIS); /* Initialize Endpoint 2 */ SetEPType(ENDP2, EP_INTERRUPT); //设置端点2为中断控制端点 SetEPTxAddr(ENDP2, ENDP2_TXADDR); //设置端点2的接收缓冲地址 SetEPTxCount(ENDP2, REPORT_COUNT); //设置端点2的接收计数 SetEPTxStatus(ENDP2, EP_TX_NAK); //设置端点2为接收不响应

/*--------------------------------------------------------------------------*/ bDeviceState = ATTACHED; //设置设备状态为 ATTACHED状态 /* Set this device to response on default address */ SetDeviceAddress(0); //设置设备为默认地址 #endif /* STM32F10X_CL */ bDeviceState = ATTACHED; }

在两根”/*------*/“中间的代码是最重要的,我们根据配置描述可知,端点1作为输出端点,端点2作为输入端点,所以在Reset函数中需要对端点进行初始化:端点0在USB枚举阶段作为通讯的端点,要配置成控制端点,收发有效;端点1配置成中断传输端点,端点接收有效,发送无效;端点2配置成中断传输端点,端点发送有效。

我们在之前关于数据收发流程中说到,数据接收的流程: USB_LP_CAN1_RX0_IRQHandler--->USB_Istr---->CTR_LP--->EPx_OUT_Callback。所以我们这里首先需要定义USB中断服务程序 USB_LP_CAN1_RX0_IRQHandler,再还要编写端点接收回调函数: EPx_OUT_Callback函数。
在hw_config.c中编写USB中断配置函数:

void USB_Interrupts_Config(void) { NVIC_InitTypeDef NVIC_InitStructure; /* 2 bit for pre-emption priority, 2 bits for subpriority */ NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4); #ifdef STM32F10X_CL /* Enable the USB Interrupts */ NVIC_InitStructure.NVIC_IRQChannel = OTG_FS_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); /* Enable the USB Wake-up interrupt */ NVIC_InitStructure.NVIC_IRQChannel = OTG_FS_WKUP_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; NVIC_Init(&NVIC_InitStructure); #else /* Enable the USB interrupt */ NVIC_InitStructure.NVIC_IRQChannel = USB_LP_CAN1_RX0_IRQn; //设置USB中断服务程序 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); #endif /* STM32F10X_CL */ }

我们这里只配置 USB_LP_CAN1_RX0_IRQn这个中断,接着在stm32f10x_it.c中编写 USB_LP_CAN1_RX0_IRQHandler()函数:

void USB_LP_CAN1_RX0_IRQHandler(void) { USB_Istr(); }

这个中断服务程序只调用USB_Istr()函数。经过这个函数的处理,最终程序会执行到 EPx_OUT_Callback函数,由于我们使用端点1作为接受数据的端点,所以我在usb_endp.c中编写 EP1_OUT_Callback()函数:

/******************************************************************************* * Function Name : EP1_OUT_Callback. * Description : 端点1输出回调函数 * Input : None. * Output : None. * Return : None. *******************************************************************************/ void EP1_OUT_Callback(void) { #ifndef STM32F10X_CL PMAToUserBufferCopy(USB_Receive_Buffer, ENDP1_RXADDR, REPORT_COUNT); //PMA缓冲区接收到的数据拷贝到用户自定义缓冲区USB_Receive_Buffer中 SetEPRxStatus(ENDP1, EP_RX_VALID); //设置端点的接收状态为有效,因为端点接收到数据后会端点状态自动设置成停止状态 USB_Received_Flag=1; //设置接收到数据标志位 #else USB_SIL_Read(EP1_OUT,USB_Receive_Buffer); //读取输出端点的数据到USB_Receive_Buffer中 USB_Received_Flag=1; //收到数据的标志 #endif }

这个函数起始很简单,从端点缓冲区中读取数据,保存在 USB_Receive_Buffer[]数组中,到时候,可以直接拿这个数组进行数据操作了。
我们还有编写一个端点发送函数:USB_SendData()。我在自己建的usb_io.c文件中,编写该函数:

/******************************************************************************* * Function Name : EP1_IN_Callback. * Description : USB向主机发送数据 * Input : None. * Output : None. * Return : None. *******************************************************************************/ uint32_t USB_SendData(uint8_t *data,uint32_t dataNum) { #ifndef STM32F10X_CL //将数据通过USB发送出去 UserToPMABufferCopy(data, ENDP2_TXADDR, dataNum); //拷贝dataNum个数据到PMA中 SetEPTxCount(ENDP2, REPORT_COUNT); //从端点2发送64字节数据 SetEPTxValid(ENDP2); //使能端点2的发送状态 #else USB_SIL_Write(EP2_IN, data, dataNum); #endif return dataNum; }

依旧很简单,只要来那个要发送的数据包写入端点的缓冲区中,再使能下短短,数据就看可以发送出去了。

最后,我们来编写我们的main函数:

/******************************************************** 函数:main() 描述:程序入口地址 参数:无 返回:无 ********************************************************/ int main(void) { uint8_t data[64]; uint32_t i=0,ret=0; BSP_Init(); printf(" |===============================================|\r\n"); printf(" USB CustomHID 程序开始 \r\n"); printf("|===============================================|\r\n"); while(1) { if(USB_Received_Flag) { USB_Received_Flag=0; ret = USB_GetData(data,sizeof(data)); //读取数据 printf("usb get data %d byte data\r\n",ret); //答应接收到的字节数 for(i=0;i<ret;i++){ printf("0x%02X ",data[i]); //答应接收到的数据 } printf("\n\r"); USB_SendData(data,sizeof(data)); //发送接收到的数据 } } }

主函数的意思是,USB设备接收到什么数据,就向USB主机发送什么数据,并且在串口打印出收到的数据。

至此,CustomHID程序差不多了。万事俱备只欠东风了:我们还需要一个上位机的HID程序。可惜啊,这里上传不了,只能截个图让大家看看现象了。
STM32 CustomHID 的实现 - ziye334 - ziye334的博客
 
再看看BUS Hound捕获到的数据,可以看到发送的数据和就收到的数据一样:
STM32 CustomHID 的实现 - ziye334 - ziye334的博客
 
  • 2
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值