STM32自定义USB设备开发详细流程讲解及全套资料源码下载(基于libusb)

前言
USB的用途就不多说了,下面的内容主要就是讲解如何利用ST提供的USB驱动库和libusb上位机驱动库实现一个USB数据传输功能,为了降低开发难度,我们仅仅讲解Bulk传输模式,当然这也是用得比较多的传输模式。

开发流程
1,完成STM32单片机端的USB程序;
2,利用linusb自带的inf-wizard工具生成USB驱动;
3,基于libusb编写USB通信程序;
4,测试PC和单片机的数据通信;

STM32程序编写
1,完成描述符的修改,修改后的描述符如下(在usb_desc.c文件中)
设备描述符:
[C]  纯文本查看 复制代码
?

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

20

21

const

uint8_t CustomHID_DeviceDescriptor[CUSTOMHID_SIZ_DEVICE_DESC] =

{

    0x12,                      
/*bLength
*/

    USB_DEVICE_DESCRIPTOR_TYPE,
/*bDescriptorType*/

    0x00,                      
/*bcdUSB
*/

    0x02,

    0x00,                      
/*bDeviceClass*/

    0x00,                      
/*bDeviceSubClass*/

    0x00,                      
/*bDeviceProtocol*/

    0x40,                      
/*bMaxPacketSize40*/

    LOBYTE(USBD_VID),          
/*idVendor*/

    HIBYTE(USBD_VID),          
/*idVendor*/

    LOBYTE(USBD_PID),          
/*idVendor*/

    HIBYTE(USBD_PID),          
/*idVendor*/

    0x00,                      
/*bcdDevice
rel. 2.00*/

    0x02,

    1,                         
/*Index
of string descriptor describing manufacturer */

    2,                         
/*Index
of string descriptor describing product*/

    3,                         
/*Index
of string descriptor describing the device serial number */

    0x01                       
/*bNumConfigurations*/

};
/*
CustomHID_DeviceDescriptor */


配置描述符:
[C]  纯文本查看 复制代码
?

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

const

uint8_t CustomHID_ConfigDescriptor[CUSTOMHID_SIZ_CONFIG_DESC] =

{

    0x09,
/*
bLength: Configuation Descriptor size */

    USB_CONFIGURATION_DESCRIPTOR_TYPE,
/*
bDescriptorType: Configuration */

    CUSTOMHID_SIZ_CONFIG_DESC,

    /*
wTotalLength: Bytes returned */

    0x00,

    0x01,        
/*
bNumInterfaces: 1 interface */

    0x01,        
/*
bConfigurationValue: Configuration value */

    0x00,        
/*
iConfiguration: Index of string descriptor describing

                                 the
configuration*/

    0xE0,        
/*
bmAttributes: Bus powered */

                  /*Bus
powered: 7th bit, Self Powered: 6th bit, Remote wakeup: 5th bit, reserved: 4..0 bits */

    0xFA,        
/*
MaxPower 500 mA: this current is used for detecting Vbus */

    /**************
Descriptor of Custom HID interface ****************/

    /*
09 */

    0x09,        
/*
bLength: Interface Descriptor size */

    USB_INTERFACE_DESCRIPTOR_TYPE,/*
bDescriptorType: Interface descriptor type */

    0x00,        
/*
bInterfaceNumber: Number of Interface */

    0x00,        
/*
bAlternateSetting: Alternate setting */

    0x04,        
/*
bNumEndpoints */

    0xDC,        
/*
bInterfaceClass: Class code = 0DCH */

    0xA0,        
/*
bInterfaceSubClass : Subclass code = 0A0H */

    0xB0,        
/*
nInterfaceProtocol : Protocol code = 0B0H */

    0,           
/*
iInterface: Index of string descriptor */

    /********************
endpoint descriptor ********************/

    /*
18 */

    0x07,        
/*
endpoint descriptor length = 07H */

    USB_ENDPOINT_DESCRIPTOR_TYPE,
/*
endpoint descriptor type = 05H */

    0x81,        
/*
endpoint 1 IN */

    0x02,                                       
/*
bulk transfer = 02H */

    0x40,0x00,   
/*
endpoint max packet size = 0040H */

    0x00,        
/*
the value is invalid when bulk transfer */

 

    0x07,        
/*
endpoint descriptor length = 07H */

    USB_ENDPOINT_DESCRIPTOR_TYPE,
/*
endpoint descriptor type = 05H */

    0x01,        
/*
endpoint 1 OUT */

    0x02,                                       
/*
bulk transfer = 02H */

    0x40,0x00,   
/*
endpoint max packet size = 0040H */

    0x00,        
/*
the value is invalid when bulk transfer */

                 

    0x07,        
/*
endpoint descriptor length = 07H */

    USB_ENDPOINT_DESCRIPTOR_TYPE,
/*
endpoint descriptor type = 05H */

    0x82,        
/*
endpoint 2 IN */

    0x02,                                       
/*
bulk transfer = 02H */

    0x40,0x00,   
/*
endpoint max packet size = 0040H */

    0x00,        
/*
the value is invalid when bulk transfer */

                 

    0x07,        
/*
endpoint descriptor length = 07H */

    USB_ENDPOINT_DESCRIPTOR_TYPE,
/*
endpoint descriptor type = 05H */

    0x02,        
/*
endpoint 2 OUT */

    0x02,                                       
/*
bulk transfer = 02H */

    0x40,0x00,   
/*
endpoint max packet size = 0040H */

    0x00,        
/*
the value is invalid when bulk transfer */

};
/*
CustomHID_ConfigDescriptor */

配置描述符就包含了端点描述符,我们用了4个端点,两个BULK-OUT端点,两个BULK-IN端点。

其他的描述符:
[C]  纯文本查看 复制代码
?

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

/*
USB String Descriptors (optional) */

const

uint8_t CustomHID_StringLangID[CUSTOMHID_SIZ_STRING_LANGID] =

{

    CUSTOMHID_SIZ_STRING_LANGID,

    USB_STRING_DESCRIPTOR_TYPE,

    0x09,

    0x04

};
/*
LangID = 0x0409: U.S. English */

 

const

uint8_t CustomHID_StringVendor[CUSTOMHID_SIZ_STRING_VENDOR] =

{

    CUSTOMHID_SIZ_STRING_VENDOR,
/*
Size of Vendor string */

    USB_STRING_DESCRIPTOR_TYPE, 
/*
bDescriptorType*/

    //
Manufacturer: "STMicroelectronics"

    '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 */

    'B',
0,
'y',
0,
'
'
,
0,
'e',
0,
'm',
0,
'b',
0,
'e',0,'d',0,'-',0,'n',0,'e',0,'t',0

};

uint8_t
CustomHID_StringSerial[CUSTOMHID_SIZ_STRING_SERIAL] =

{

    CUSTOMHID_SIZ_STRING_SERIAL,          
/*
bLength */

    USB_STRING_DESCRIPTOR_TYPE,       
/*
bDescriptorType */

    'x',
0,
'x',
0,
'x',
0,
'x',
0,
'x',
0,
'x',
0,
'x',
0

};


2,根据端点缓冲区大小配置端点缓冲区地址,配置信息如下(在usb_conf.h文件中):
[C]  纯文本查看 复制代码
?

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

/*
buffer table base address */

#define
BTABLE_ADDRESS      (0x00)

 

/*
EP0  */

/*
rx/tx buffer base address */

#define
ENDP0_RXADDR        (0x18)

#define
ENDP0_TXADDR        (0x58)

 

/*
EP1  */

/*
tx buffer base address */

//地址为32位对其,位4的倍数,不能超过
bMaxPacketSize

//EP1

#define
ENDP1_RXADDR        (0x98)

#define
ENDP1_TXADDR        (0x98+64)

EP2

#define
ENDP2_RXADDR        (0xA0+64+64)

#define
ENDP2_TXADDR        (0xA0+64+64+64)


3,初始化每个端点(在usb_prop.c文件中的CustomHID_Reset函数中)
[C]  纯文本查看 复制代码
?

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

/*
Initialize Endpoint 0 */

SetEPType(ENDP0,
EP_CONTROL);

SetEPTxStatus(ENDP0,
EP_TX_STALL);

SetEPRxAddr(ENDP0,
ENDP0_RXADDR);

SetEPTxAddr(ENDP0,
ENDP0_TXADDR);

Clear_Status_Out(ENDP0);

SetEPRxCount(ENDP0,
Device_Property.MaxPacketSize);

SetEPRxValid(ENDP0);

 

/*
Initialize Endpoint 1 */

       SetEPType(ENDP1,
EP_BULK);

       SetEPRxAddr(ENDP1,
ENDP1_RXADDR);

       SetEPTxAddr(ENDP1,
ENDP1_TXADDR);

       SetEPRxCount(ENDP1,
EP_SIZE);

       SetEPRxStatus(ENDP1,
EP_RX_VALID);

 SetEPTxStatus(ENDP1,
EP_TX_NAK);

 

/*
Initialize Endpoint 2 */

       SetEPType(ENDP2,
EP_BULK);

       SetEPRxAddr(ENDP2,
ENDP2_RXADDR);

       SetEPTxAddr(ENDP2,
ENDP2_TXADDR);

       SetEPRxCount(ENDP2,
EP_SIZE);

       SetEPRxStatus(ENDP2,
EP_RX_VALID);

       SetEPTxStatus(ENDP2,
EP_TX_NAK);


4,实现端点的回调函数(需要在usb_conf.h中注释掉对应的回调函数宏定义)
[C]  纯文本查看 复制代码
?

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

/*******************************************************************************

*
Function Name  : EP1_OUT_Callback.

*
Description    : EP1 OUT Callback Routine.

*
Input          : None.

*
Output         : None.

*
Return         : None.

*******************************************************************************/

void

EP1_OUT_Callback(
void)

{

        EP1_ReceivedCount
= GetEPRxCount(ENDP1);

        PMAToUserBufferCopy(USB_Receive_Buffer,
ENDP1_RXADDR, EP1_ReceivedCount);

        SetEPRxStatus(ENDP1,
EP_RX_VALID);

}

/*******************************************************************************

*
Function Name  : EP2_OUT_Callback.

*
Description    : EP2 OUT Callback Routine.

*
Input          : None.

*
Output         : None.

*
Return         : None.

*******************************************************************************/

void

EP2_OUT_Callback(
void)

{

        EP2_ReceivedCount
= GetEPRxCount(ENDP2);

        PMAToUserBufferCopy(USB_Receive_Buffer,
ENDP2_RXADDR, EP2_ReceivedCount);

        SetEPRxStatus(ENDP2,
EP_RX_VALID);

}


5,完成主函数的测试程序
[C]  纯文本查看 复制代码
?

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

int

main(
void)

{

        uint8_t
data[256];

        uint32_t
i=0;

        Set_System();//系统时钟初始化

        USART_Configuration();//串口1初始化

        printf("\x0c\0");printf("\x0c\0");//超级终端清屏

        printf("\033[1;40;32m");//设置超级终端背景为黑色,字符为绿色

        printf("\r\n*******************************************************************************");

        printf("\r\n************************
Copyright 2009-2012, EmbedNet ************************"
);

        printf("\r\n***************************
[url=http://www.embed-net.com]http://www.embed-net.com[/url] **************************"
);

        printf("\r\n*****************************
All Rights Reserved *****************************"
);

        printf("\r\n*******************************************************************************");

        printf("\r\n");

 

        USB_Interrupts_Config();

        Set_USBClock();

        USB_Init();

 

        while(1)

        {

                if(EP1_ReceivedCount
> 0){

                        USB_GetData(ENDP1,data,EP1_ReceivedCount);

                        USB_SendData(ENDP1,data,EP1_ReceivedCount);

                        printf("usb
EP1 get data %d byte data\n\r"
,EP1_ReceivedCount);

                        for(i=0;i<EP1_ReceivedCount;i++){

                                printf("0x%02X
"
,data[i]);

                        }

                        printf("\n\r");

                        EP1_ReceivedCount=0;

                }

                if(EP2_ReceivedCount
> 0){

                        USB_GetData(ENDP2,data,EP2_ReceivedCount);

                        USB_SendData(ENDP2,data,EP2_ReceivedCount);

                        printf("usb
EP2 get data %d byte data\n\r"
,EP2_ReceivedCount);

                        for(i=0;i<EP2_ReceivedCount;i++){

                                printf("0x%02X
"
,data[i]);

                        }

                        printf("\n\r");

                        EP2_ReceivedCount=0;       

                }

        }

}


到此,STM32的程序基本上编写完成,然后编译下载程序,如果一切顺利,系统会检测到一个新的设备并试图加载对应的驱动,由于我们还没做驱动程序,所以肯定会加载驱动失败,如下图所示:
 

驱动程序生成
下面我们就利用libusb自带的inf-wizard工具生成USB驱动程序,该工具可以到本文章的附件下载,其具体过程如下:
 

运行该程序,出现下图对话框,点击“Next”;
 

出现下图对话框后选择我们需要生成驱动程序的设备;
 

这里可以写该Device Name,我们保持默认值,其他的都不需要修改;
 

点击Next后出现下图对话框,我们选择一个目录保存这个inf文件;
 

保存后的文件
 

若要立即安装驱动,可以点击下面对话框的红色框按钮;
 

Win7下可能会出现如下对话框,点击始终安装;
 

到此,USB驱动程序自动生成完毕,若安装了驱动,则在设备管理器里面会看到如下信息
 

基于libusb的上位机驱动程序编写
首先建立一个驱动程序工程,然后将libusb的库(附件有下载)添加到工程里面,编写以下几个函数
设备扫描函数,该函数用来找到插入电脑上的USB设备
[C]  纯文本查看 复制代码
?

01

02

03

04

05

06

07

08

09

10

11

12

13

14

/**

  *
@brief  扫描设备连接数

  *
@param  NeedInit 是否需要初始化,第一次调用该函数需要初始化

  *
@retval 识别到的指定设备个数

  */

int

__stdcall USBScanDev(
int

NeedInit)

{

        if(NeedInit){

                usb_init();
/*
initialize the library */

                usb_find_busses();
/*
find all busses */

                usb_find_devices();
/*
find all connected devices */

        }

        return

scan_dev(pBoard);

}


打开设备
[C]  纯文本查看 复制代码
?

01

02

03

04

05

06

07

08

09

10

11

12

13

14

/**

  *
@brief  打开指定的USB设备

  *
@param  devNum        需要打开的设备号

  *
@retval 打开状态

  */

int

__stdcall USBOpenDev(
int

DevIndex)

{

        pBoardHandle[DevIndex]
= open_dev(DevIndex,pBoard);

        if(pBoardHandle[DevIndex]==NULL){

                return

SEVERITY_ERROR;

        }else{

                return

SEVERITY_SUCCESS;

        }

}


关闭设备
[C]  纯文本查看 复制代码
?

01

02

03

04

05

06

07

08

09

/**

  *
@brief  关闭指定的USB设备

  *
@param  devNum        需要关闭的设备号

  *
@retval 打开状态

  */

int

__stdcall USBCloseDev(
int

DevIndex)

{

        return

close_dev(DevIndex,pBoardHandle);

}


BULK端点写数据
[C]  纯文本查看 复制代码
?

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

/**

  *
@brief  USB Bulk端点写数据

  *
@param  nBoardID 设备号

  *
@param  pipenum 端点号

  *
@param  sendbuffer 发送数据缓冲区

  *
@param  len 发送数据字节数

  *
@param  waittime 超时时间

  *
@retval 成功发送的数据字节数

  */

 

int

__stdcall USBBulkWriteData(unsigned
int

nBoardID,
int

pipenum,
char

*sendbuffer,
int

len,
int

waittime)

{

        int

ret=0;

        if(pBoardHandle[nBoardID]
== NULL){

                return

SEVERITY_ERROR;

        }

#ifdef
TEST_SET_CONFIGURATION

    if

(usb_set_configuration(pBoardHandle[nBoardID], MY_CONFIG) < 0)

    {

        usb_close(pBoardHandle[nBoardID]);

        return

SEVERITY_ERROR;

    }

#endif

 

#ifdef
TEST_CLAIM_INTERFACE

    if

(usb_claim_interface(pBoardHandle[nBoardID], 0) < 0)

    {

        usb_close(pBoardHandle[nBoardID]);

        return

SEVERITY_ERROR;

    }

#endif

 

#if
TEST_ASYNC

    //
Running an async write test

    ret
= transfer_bulk_async(dev, pipenum, sendbuffer, len, waittime);

#else

        ret
= usb_bulk_write(pBoardHandle[nBoardID], pipenum, sendbuffer, len, waittime);

        /*if((len%64)
== 0){

                usb_bulk_write(pBoardHandle[nBoardID],
pipenum, sendbuffer, 0, waittime);

        }*/

#endif

#ifdef
TEST_CLAIM_INTERFACE

    usb_release_interface(pBoardHandle[nBoardID],
0);

#endif

    return

ret;

}


BULK端点读数据
[C]  纯文本查看 复制代码
?

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

/**

  *
@brief  USB Bulk读数据

  *
@param  nBoardID 设备号

  *
@param  pipenum 端点号

  *
@param  readbuffer 读取数据缓冲区

  *
@param  len 读取数据字节数

  *
@param  waittime 超时时间

  *
@retval 读到的数据字节数

  */

int

__stdcall USBBulkReadData(unsigned
int

nBoardID,
int

pipenum,
char

*readbuffer,
int

len,
int

waittime)

{

        int

ret=0;

        if(pBoardHandle[nBoardID]
== NULL){

                return

SEVERITY_ERROR;

        }

#ifdef
TEST_SET_CONFIGURATION

    if

(usb_set_configuration(pBoardHandle[nBoardID], MY_CONFIG) < 0)

    {

        usb_close(pBoardHandle[nBoardID]);

        return

SEVERITY_ERROR;

    }

#endif

 

#ifdef
TEST_CLAIM_INTERFACE

    if

(usb_claim_interface(pBoardHandle[nBoardID], 0) < 0)

    {

        usb_close(pBoardHandle[nBoardID]);

        return

SEVERITY_ERROR;

    }

#endif

 

#if
TEST_ASYNC

    //
Running an async read test

    ret
= transfer_bulk_async(pGinkgoBoardHandle[nBoardID], pipenum, sendbuffer, len, waittime);

#else

        ret
= usb_bulk_read(pBoardHandle[nBoardID], pipenum, readbuffer, len, waittime);

#endif

#ifdef
TEST_CLAIM_INTERFACE

    usb_release_interface(pBoardHandle[nBoardID],
0);

#endif

    return

ret;

}


到此,PC端的驱动程序编写基本完成,下面就是驱动程序的测试,我们可以把之前这个程序生成为一个dll文件,然后单独建立一个测试工程来测试这个dll文件中的函数,测试程序如下:
[C]  纯文本查看 复制代码
?

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

//
USB_DriverTest.cpp : 定义控制台应用程序的入口点。

//

 

#include
"stdafx.h"

 

#define       
EP1_OUT_SIZE        64

#define       
EP1_IN_SIZE        64

 

int

_tmain(
int

argc, _TCHAR* argv[])

{

        int

DevNum;

        int

ret;

        char

WriteTestData[256]={1,2,3,4,5,6,7,8,9};

        char

ReadTestData[256]={0};

        for(int

i=0;i<256;i++){

                WriteTestData[i]
= i;

        }

        //扫描设备连接数,需要初始化

        DevNum
= USBScanDev(1);

        printf("设备连接数为:%d\n",DevNum);

        //打开设备0

        ret
= USBOpenDev(0);

        if(ret
== SEVERITY_ERROR){

                printf("打开设备失败!\n");

                return

SEVERITY_ERROR;

        }else{

                printf("打开设备成功!\n");

        }

 

        //端点1写数据

        ret
= USBBulkWriteData(0,EP1_OUT,WriteTestData,EP1_OUT_SIZE,500);

        if(ret
!= EP1_OUT_SIZE){

                printf("端点1写数据失败!%d\n",ret);

                return

SEVERITY_ERROR;

        }else{

                printf("端点1写数据成功!\n");

        }

        //端点1读数据

        ret
= USBBulkReadData(0,EP1_IN,ReadTestData,EP1_IN_SIZE,500);

        if(ret
!= EP1_IN_SIZE){

                printf("端点1读数据失败!%d\n",ret);

                return

SEVERITY_ERROR;

        }else{

                printf("端点1读数据成功!\n");

                for(int

i=0;i<EP1_IN_SIZE;i++){

                        printf("%02X
"
,ReadTestData[i]);

                        if(((i+1)%16)==0){

                                printf("\n");

                        }

                }

                printf("\n");

        }

        Sleep(100);

        //端点2写数据

        ret
= USBBulkWriteData(0,EP2_OUT,WriteTestData+64,64,500);

        if(ret
!= 64){

                printf("端点2写数据失败!%d\n",ret);

                return

SEVERITY_ERROR;

        }else{

                printf("端点2写数据成功!\n");

        }

        //端点2读数据

        ret
= USBBulkReadData(0,EP2_IN,ReadTestData,64,500);

        if(ret
!= 64){

                printf("端点2读数据失败!%d\n",ret);

                return

SEVERITY_ERROR;

        }else{

                printf("端点2读数据成功!\n");

                for(int

i=0;i<64;i++){

                        printf("%02X
"
,ReadTestData[i]);

                        if(((i+1)%16)==0){

                                printf("\n");

                        }

                }

                printf("\n");

        }

        getchar();

        return

0;

}


到此,整个开发流程基本完成,下面是本套程序的测试图片

串口打印输出
 

PC端测试程序输出
 

Bus Hound抓取到的USB数据
 

程序源码下载
libusb驱动生成工具下载:  inf_tool.rar (778.26 KB, 下载次数: 592) 
STM32程序源码下载:  USB_DriverSTM32F103.rar (677.81 KB, 下载次数: 611) 
PC端USB驱动下载:  USB Driver.rar (266.56 KB, 下载次数: 456) 
PC端USB驱动程序源码下载:  USB_DriverBulk.rar (20.61 KB, 下载次数: 336) 
PC端USB驱动测试程序源码下载:  USB_DriverTest.rar (12.34 KB, 下载次数: 352) 
libusb驱动包下载:  libusb-win32-bin-1.2.6.0.rar (821.57 KB, 下载次数: 529) 
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值