USB-HID设备中的复合设备

9 篇文章 1 订阅
8 篇文章 1 订阅

一、前言

       最近在公司做Linux 底下的libusb开发,收获非常大,其中得到公司大神指点一下,对于HID 复合设备的理解更加深刻了,以至于在调试无论是调试Android 的USB-HOST、还是Windows 的usb 还是Linux 的libusb 都非常顺利,对于以前的一些不太懂的问题和一些函数的使用都有更深的理解,因此,必须写下这篇文章记录下来,防止以后忘记了,可以重新回来看看,也可以方便其他的看官。  

        这次也是对之前的一些零散的知识进行汇总和梳理。本文会涉及多个平台的相关API,所以有点杂,但是请耐心看下去,对于各位可能有所帮助的。

 

二、 HID 复合设备

        这个大神在他的博客上总结的非常不错 USB复合设备(mass storage&hid),也让我所获了一些。

所谓的复合设备,就是具有两个独立的USB设备功能,但是集中在同一个硬件上的USB-HID设备。例如我们公司常用,

键盘 + 自定义HID  (HID Keyboard  + 自定义 Human Interface Device),如下图,在设备管理器可以看到

本来,一般这两种东西都是分别存在于两个HID 设备上,分两个硬件,如USB 键盘  和 USB 鼠标,但是,现在却存在于同一个硬件中。看上面的图应该非常直观。

借用大神的原话,

 

首先要说的是既然是复合型设备,那么就有多个interface,这里有两个设备,那么就需要两个interface,需要几个设备描述符呢,一个就够了,那配

置描述符呢,也只需要一个就好了,那需要几个端点描述符呢,这个嘛,我就不知道了(开玩笑,你在总结你不知道),这个就得讲讲usb的几种传输

模式了。

大神的这段话,说的没有错,对于一个HID 设备,只需一个设备描述符,一个HID 描述符可以了。但是interface描述符,即接口描述符呢?对于一般单个设备,只需一个就可以了,但是对于复合设备,则需要两个接口描述符,每个接口表示该HID 设备支持的一种功能,如下图,我们公司的复合设备,用UsbTreeView 可以非常直观的看到

而且,我们公司大神有一句让我印象非常深刻话,在hid 协议中,无论是单设备还是复合设备,操作系统都会根据设备的interface 设备描述符认为是独立的,即:

我们公司的产品复合设备是 - 键盘 + 自定义HID  (HID Keyboard  + 自定义 Human Interface Device),那么就会存在两个inerface , 则操作系统就会认为 上面 是两个独立的单设备,无论是Windows或则是Linux 还是 Android 都会这样子识别。

因此,在编程时,获取设备的接口时就需要注意,要操作的设备号是哪个。

 

三、端点描述符

        上面的大神原话中,并没有说清楚端点描述符的个数,端点描述符的个数一般与需求有关系。

对于单设备,一般只有一个设备描述符,如果是单向设备(如 Keyboard)则只有一个输入端点(IN)如下图:

对于单设备,一般只有一个设备描述符,当时双向设备时(如:自定义HID),则有两个端点,IN 和 OUT 如下图:

当然,对于复合型设备,如我们公司的  键盘 + 自定义HID  (HID Keyboard  + 自定义 Human Interface Device)

则有两个接口描述符和三个端点描述符。

 

四、做复合设备开发时的总结

4.1 在Windows编程中,查找USB设备时:

刚才开始接触HID,做的项目是单设备的,所以在查找设备时只需要设备的VID he PID 就可以了。

但是,公司有一个915MHz 的项目时,遇到复合设备。好了,结果遇到问题了,设备时可以找到,但是每次读写时,软件就会崩掉,经过多次调试才发现是复合设备的问题,因为在系统中复合设备是公用一个PID 和 VID,所以,当搜索到一个PID 和 VID 时就直接打开,也没有判断是不是双向设备,结果退出,导致软件崩溃。

所以,在window 编程时,遇到目标相同的VID 和 PID 要进行判断,双向设备(即可以被读写的设备)才是需要找的。

如下图代码:

	for(int ik=0; ik<50; ik++)
	{
		bSuccess = SetupDiEnumDeviceInterfaces(hDevInfo, NULL, &guidHID, ik, &strtInterfaceData);
		if (!bSuccess)
		{
			//_T("本次查找USB-HID设备结束... ...  ...\r\n");
			SetupDiDestroyDeviceInfoList(hDevInfo);
			break;
		}
		else
		{
			if(strtInterfaceData.Flags == SPINT_ACTIVE )
			{
				PSP_DEVICE_INTERFACE_DETAIL_DATA strtDetailData;

				DWORD strSzie = 0, requiesize = 0;
				SetupDiGetDeviceInterfaceDetail(hDevInfo,&strtInterfaceData,NULL,0,&strSzie,NULL);
			
				requiesize = strSzie;
				strtDetailData = (PSP_DEVICE_INTERFACE_DETAIL_DATA)malloc(requiesize);
				strtDetailData->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA);

				if (!SetupDiGetDeviceInterfaceDetail(hDevInfo,&strtInterfaceData,
										strtDetailData,strSzie,&requiesize,NULL))
				{
					SetupDiDestroyDeviceInfoList(hDevInfo);
					free(strtDetailData);
					break;
				}

				hidUsbHandle = CreateFile(
						strtDetailData->DevicePath,
						GENERIC_WRITE | GENERIC_READ,        //可以读写
						FILE_SHARE_READ|FILE_SHARE_WRITE, 
						NULL,
						OPEN_EXISTING,
						FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,

						//GENERIC_READ | GENERIC_WRITE,
						//FILE_SHARE_READ | FILE_SHARE_WRITE,
						//NULL,
						//OPEN_EXISTING,
						//0,//FILE_ATTRIBUTE_NORMAL,

						NULL);
			
				if (hidUsbHandle == INVALID_HANDLE_VALUE)
				{			
					//_T("尝试打开设备出错!\r\n")
					free(strtDetailData);
					continue;
				}

				//_T("打开USB-HID设备成功!\r\n");
				
				HIDD_ATTRIBUTES strtAttrib;
				if (!HidD_GetAttributes(hidUsbHandle, &strtAttrib))
				{
					//_T("查找设备结束... ... \r\n");
					CloseHandle(hidUsbHandle);
					SetupDiDestroyDeviceInfoList(hDevInfo);

					free(strtDetailData);
					break;
				}
                                 //目标设备
				if((strtAttrib.VendorID == USB_VENDOR_ID && strtAttrib.ProductID == USB_PRODUCT_ID) || (strtAttrib.VendorID == USB_VENDOR_ID_CTL && strtAttrib.ProductID == USB_PRODUCT_ID_CTL))
				{
					
					if(strtAttrib.VendorID == USB_VENDOR_ID && strtAttrib.ProductID == USB_PRODUCT_ID) 
					{
						mGlobalUsbType = 1;
					}
					else
					{
						mGlobalUsbType = 0;
					}
					
					mglUsbHidWriteHandle = hidUsbHandle;		
					mglUsbHidReadHandle = hidUsbHandle;
						
					free(strtDetailData);
					return TRUE;
				}
				else
				{
					free(strtDetailData);
					CloseHandle(hidUsbHandle);
				}
			}
		}	
	}

4.2 在linux 编程中,查找设备和使用bulktansfer()和 函数注意问题

int LIBUSB_CALL libusb_bulk_transfer(libusb_device_handle *dev_handle,nsigned char endpoint, unsigned char *data, int length,int *actual_length, unsigned int timeout);
int LIBUSB_CALL libusb_kernel_driver_active(libusb_device_handle *dev_handle,
	int interface_number);
int LIBUSB_CALL libusb_detach_kernel_driver(libusb_device_handle *dev_handle,
	int interface_number);
int LIBUSB_CALL libusb_claim_interface(libusb_device_handle *dev_handle,
	int interface_number);
int LIBUSB_CALL libusb_release_interface(libusb_device_handle *dev_handle,
	int interface_number);

上面的几个函数,其中, 都需要注意,单设备和复合设备使用时,传入的参数是不同的。

a、对于单设备:

libusb_claim_interface() 和 libusb_release_interface() 和 libusb_kernel_driver_active()

及libusb_detach_kernel_driver() 这几个函数的参数 interface_number 为 0 ,

因为,一般只有一个interface 接口,而编号一个从 0 开始, interface_number = 0;

而对于libusb_bulk_transfer() 这个函数,edpoint 和 length 这个值, 在IN endpoint = 0, OUT endpoint  = 1,而length 则为64. 

b、对于复合设备,如果如果使用上面的值,那有可能是错的,如:

我们公司的复合设备,用上面的参数绝对是错误的,正确的参数应该是

interface_number = 1, IN   endpoint = 1,  OUT  endpoint = 2.

那如何做到通用呢? 不管是单设备或则复合设备都可以用?

需要这样子做,使用api 时搜索设备时,需要保存,不管是单设备还是复合设备的接口描述符和端点描述符的信息。

对于复合设备,接口描述符有两个,则需要过滤,保存具有两个端点的interface 和 endpoint 信息,这样子在传参时就可以

做到通用,我调试就是这样子,代码如下:

int BulkTransferForUsbSend(unsigned char *outputStream, int outputStreamLength, int timeout)
{
	unsigned char dataStream[255] = {0};	
	int error = 0, realSendLength = 0;

	if(mHandleDev == NULL)
		return DEVICE_ERROR_NO_CONNECT;

	if(outputStreamLength > mOutEndpoint.wMaxPacketSize)
		return DEVICE_ERROR_OUT_OF_SIZE;

	memcpy(dataStream, outputStream, outputStreamLength);

	if(gUsbDebugEnable)
		PrintHex("Usb send:", dataStream, mOutEndpoint.wMaxPacketSize);

	error = libusb_bulk_transfer(mHandleDev, mOutEndpoint.bEndpointAddress, dataStream, mOutEndpoint.wMaxPacketSize, &realSendLength, timeout);
	if(error < 0)
	{
		if(error == LIBUSB_ERROR_TIMEOUT)
			return DEVICE_ERROR_TIMEOUT;
		else
			return DEVICE_ERROR_USB_SEND_ERROR;
	}

	return DEVICE_ERROR_OK;
}

int BulkTransferForUsbReceive(unsigned char *inputStream, int *inputStreamLength, int maxLength, int timeout)
{
	unsigned char dataStream[255] = {0};	
	int error = 0, realSendLength = 0;

	if(mHandleDev == NULL)
		return DEVICE_ERROR_NO_CONNECT;
	
	if(maxLength < mOutEndpoint.wMaxPacketSize)
		return DEVICE_ERROR_OUT_OF_SIZE;

	error = libusb_bulk_transfer(mHandleDev, mInEndpoint.bEndpointAddress, dataStream, mInEndpoint.wMaxPacketSize, &realSendLength, timeout);	
	if(error < 0)
	{
		if(error == LIBUSB_ERROR_TIMEOUT)
			return DEVICE_ERROR_TIMEOUT;
		else	
			return DEVICE_ERROR_USB_REC_ERROR;
	}

	memcpy(inputStream, dataStream, realSendLength);
	inputStreamLength[0] = realSendLength;

	if(gUsbDebugEnable)
		PrintHex("Usb Rec:", dataStream, mInEndpoint.wMaxPacketSize);

	return DEVICE_ERROR_OK;
}

申请接口时:

	if(libusb_kernel_driver_active(mHandleDev, mInterface.bInterfaceNumber) == 1)
	{
		libusb_detach_kernel_driver(mHandleDev, mInterface.bInterfaceNumber);	
	}

	if(gUsbDebugEnable)
		PrintDebug("libusb_kernel_driver_active succcess", __FILE__, __FUNCTION__, __LINE__);
	
	error = libusb_claim_interface(mHandleDev, mInterface.bInterfaceNumber);
	if (error < 0) 
	{
		if(gUsbDebugEnable)
			PrintError("libusb claim nterface failed", __FILE__, __FUNCTION__, __LINE__, error);

		return DEIVCE_ERROR_OPEN_DEVICE;
	}

可以根据不同设备进行变化,这样子就方便了。

 

4.3 在Android Host 编程中,使用使用时注意

在Android中这几个方法需要注意:

 /**
     * Returns the {@link UsbInterface} at the given index.
     * For devices with multiple configurations, you will probably want to use
     * {@link UsbConfiguration#getInterface} instead.
     *
     * @return the interface
     */
    public UsbInterface getInterface(int index) {
        return getInterfaceList()[index];
    }
/**
     * Returns the {@link android.hardware.usb.UsbEndpoint} at the given index.
     *
     * @return the endpoint
     */
    public UsbEndpoint getEndpoint(int index) {
        return (UsbEndpoint)mEndpoints[index];
    }
/**
     * Claims exclusive access to a {@link android.hardware.usb.UsbInterface}.
     * This must be done before sending or receiving data on any
     * {@link android.hardware.usb.UsbEndpoint}s belonging to the interface.
     *
     * @param intf the interface to claim
     * @param force true to disconnect kernel driver if necessary
     * @return true if the interface was successfully claimed
     */
    public boolean claimInterface(UsbInterface intf, boolean force) {
        return native_claim_interface(intf.getId(), force);
    }

    /**
     * Releases exclusive access to a {@link android.hardware.usb.UsbInterface}.
     *
     * @return true if the interface was successfully released
     */
    public boolean releaseInterface(UsbInterface intf) {
        return native_release_interface(intf.getId());
    }
 /**
     * Performs a bulk transaction on the given endpoint.
     * The direction of the transfer is determined by the direction of the endpoint.
     *
     * @param endpoint the endpoint for this transaction
     * @param buffer buffer for data to send or receive
     * @param offset the index of the first byte in the buffer to send or receive
     * @param length the length of the data to send or receive
     * @param timeout in milliseconds
     * @return length of data transferred (or zero) for success,
     * or negative value for failure
     */
    public int bulkTransfer(UsbEndpoint endpoint,
            byte[] buffer, int offset, int length, int timeout) {
        checkBounds(buffer, offset, length);
        return native_bulk_request(endpoint.getAddress(), buffer, offset, length, timeout);
    }

在Android 中,网上大部分资料,都没有说明单设备和复合设备区分问题。

我看过的资料,大部分都是在根据设备的PID 和 VID 过滤获取到UsbDevice 对象后,直接对设备进行操作

        //获取USB设备,然后从设备获取接口
        mUsbInterface = mUsbDevice.getInterface(0);
        if(mUsbInterface == null)
            return 2;

        //从端点0, 为输入端点
        mInEndpoint = mUsbInterface.getEndpoint(0);
        if(mInEndpoint == null)
            return 3;

        //端点1,一般为输出端点
        mOutEndpoint = mUsbInterface.getEndpoint(1);
        if(mOutEndpoint == null)
            return 4;

上面的代码对于单设备有可能是对的,因为单设备的接口为一个,所以getInterface(0) 是对。

对于复合设备,这样子就太绝对了,interface不止一个,通用操作还是必须的,这样子的代码灵活性也非常好。

可以发现Android 的各个方法与Linux 的libusb 的同名函数的参数相识,因此,可以看出Android usb 底层应该是libusb,

所以,上面提到的在寻找 复合设备时,将可以读写,并且有两个端点的接口描述符对象和端点描述符对象保存下来。

具体代码如下:

for(int ik=0; ik<mUsbDevice.getInterfaceCount; ik++)
{
	UsbInterface usbInterface = mUsbDevice.getInterface(ik);
	
	//人体输入设备,并且具有输入输出端点  即端点个数大于二
	if(usbInterface.getInterfaceClass == 0x03 && usbInterface.getEndpointCount >= 2)
	{
		for(int jk=0; jk<usbInterface.getEndpointCount; jk++)
		{
			最高位表示为1 表示输入
			if((usbInterface.getEndpoint(jk).getAddress & 0x80) > 0)
			{
				//输入端点
				mUsbInEndpoint = usbInterface.getEndpoint(jk);
			}
			else
			{
				//输出端点
				mUsbOutEndpoint = usbInterface.getEndpoint(jk);
			}
		}
	}
}

mConnection.bulkTransfer(mUsbInEndpoint, inputStream, mUsbInEndpoint.getMaxPacketSize(), timeOut)

mConnection.bulkTransfer(mUsbOutEndpoint, inputStream, mUsbOutEndpoint.getMaxPacketSize(), timeOut)

mConnection.claimInterface(mUsbInterface, true); 

总上所述,关于HID 复合设备相关的记录就到这,希望各位有所帮助。

 

2020年5月16日20:29:21 增加:

1. 最近做二维码的机器项目,被我领导的USB 复合设备给坑了,我通过在Linux 系统下,通过libusb去查找USB二维码复合设备,发送数据竟然没有返回,一开始以为是通讯协议问题,因为的代码是Windows 调试好的,再移植到Linux 直接使用。我没有怀疑是USB 通讯的部分代码问题,以前一直都是这样子使用的,无论是复合设备还是单设备都可以使用,使用代码调试别的设备,一样可以使用。这就问题来了。。。。。。。

2. 于是我怀疑是我领导的USB设备描述符应该跟别的工程师不一样,于是使用查看一下,UsbTreeView 查看发现, 那家伙竟然把键盘设备也作成双向设备(o(╥﹏╥)o  以前的公司的产品,键盘是单向设备,只有输出端点),并且接口描述和端点描述符也和另一个设备几乎一样,难怪的我的代码会把在usb 发送数据时,64字节分8包输出(键盘设备8字节为一包)

其中键盘设备信息:

读写设备的信息:

我的代码都是动态扫描读写设备,然后根据读写设备的信息,再通讯,

//获取搜索到的输入和输出端点
int GetInEndpointAndOutEndpoint(const struct libusb_endpoint_descriptor *endpoint, LibusbBulkTransferInfo *libusbBulkTransferInfo)
{
	if(endpoint == NULL || libusbBulkTransferInfo == NULL)
		return -1;

	printf("        =============== Endpoint info:  ================ \n");
	printf("        bEndpointAddress: %02xh\n", endpoint->bEndpointAddress);
	printf("        bmAttributes:     %02xh\n", endpoint->bmAttributes);
	printf("        wMaxPacketSize:   %d\n", endpoint->wMaxPacketSize);
	printf("        bInterval:        %d\n", endpoint->bInterval);
	printf("        bRefresh:         %d\n", endpoint->bRefresh);
	printf("        bSynchAddress:    %d\n", endpoint->bSynchAddress);
	printf("        \n");

	if(endpoint->bEndpointAddress & 0x80)
	{
		//输入端点
		memset(&(libusbBulkTransferInfo->inEndpoint), 0, sizeof(struct libusb_endpoint_descriptor));
		memcpy(&(libusbBulkTransferInfo->inEndpoint), endpoint, sizeof(struct libusb_endpoint_descriptor));
	}
	else
	{
		//输出端点
		memset(&(libusbBulkTransferInfo->outEndpoint), 0, sizeof(struct libusb_endpoint_descriptor));
		memcpy(&(libusbBulkTransferInfo->outEndpoint), endpoint, sizeof(struct libusb_endpoint_descriptor));
	}

	return 0;
}

//获取双向通讯的人体输入设备接口描述符
int GetUsbHidDeviceInterface(const struct libusb_interface_descriptor *interface, LibusbBulkTransferInfo *libusbBulkTransferInfo)
{
	int error = 0;
	uint8_t i;

	if(interface == NULL || libusbBulkTransferInfo == NULL)
		return -1;

	if(interface->bInterfaceClass == 0x03 && interface->bNumEndpoints >= 2)
	{
		if(gUsbDebugEnable)
		{	
			printf(" ----------------  Interface Infor: ----------------\n");
			printf(" bInterfaceNumber:   %d\n", interface->bInterfaceNumber);
			printf(" bAlternateSetting:  %d\n", interface->bAlternateSetting);
			printf(" bNumEndpoints:      %d\n", interface->bNumEndpoints);
			printf(" bInterfaceClass:    %d\n", interface->bInterfaceClass);
			printf(" bInterfaceSubClass: %d\n", interface->bInterfaceSubClass);
			printf(" bInterfaceProtocol: %d\n", interface->bInterfaceProtocol);
			printf(" iInterface:         %d\n", interface->iInterface);

			printf(" ----------------  Interface -----------------\n");
		}

		for (i = 0; i < interface->bNumEndpoints; i++)
		{
			//轮寻端点,获取输入输出端点
			error = GetInEndpointAndOutEndpoint(&interface->endpoint[i], libusbBulkTransferInfo);	
			if(error != 0)
				return -1;		
		}

		memset(&(libusbBulkTransferInfo->interface), 0, sizeof(struct libusb_interface_descriptor));
		memcpy(&(libusbBulkTransferInfo->interface), interface, sizeof(struct libusb_interface_descriptor));

		return 0;
	}

	return -1;
}

上面的代码,针对公司以前的产品,可以区分键盘设备(键盘为单向设备,端点只有一个,而且是输出端点)和读写设备,但是遇到我领导做的奇葩设备也是就是键盘也是双向设备的时候就有问题,

最终,要做到通用,过滤单向的键盘设备,也可以过滤双向的键盘设备,可以根据USB设备的端点描述中的wMaxPacketSize 字段,其中,键盘设备固定为wMaxPacketSize = 0x08,   而我们公司的双向设备一般为 wMaxPacketSize = 0x40,

因此修改代码如下:

//获取搜索到的输入和输出端点
int GetInEndpointAndOutEndpoint(const struct libusb_endpoint_descriptor *endpoint, LibusbBulkTransferInfo *libusbBulkTransferInfo)
{
	if(endpoint == NULL || libusbBulkTransferInfo == NULL)
		return -1;

	printf("        =============== Endpoint info:  ================ \n");
	printf("        bEndpointAddress: %02xh\n", endpoint->bEndpointAddress);
	printf("        bmAttributes:     %02xh\n", endpoint->bmAttributes);
	printf("        wMaxPacketSize:   %d\n", endpoint->wMaxPacketSize);
	printf("        bInterval:        %d\n", endpoint->bInterval);
	printf("        bRefresh:         %d\n", endpoint->bRefresh);
	printf("        bSynchAddress:    %d\n", endpoint->bSynchAddress);
	printf("        \n");

	//描述符奇葩,需要区键盘设备和读写设备, 键盘设备长度 固定 = 0x08
	if(endpoint->wMaxPacketSize <= 0x08)
		return -1;

	if(endpoint->bEndpointAddress & 0x80)
	{
		//输入端点
		memset(&(libusbBulkTransferInfo->inEndpoint), 0, sizeof(struct libusb_endpoint_descriptor));
		memcpy(&(libusbBulkTransferInfo->inEndpoint), endpoint, sizeof(struct libusb_endpoint_descriptor));
	}
	else
	{
		//输出端点
		memset(&(libusbBulkTransferInfo->outEndpoint), 0, sizeof(struct libusb_endpoint_descriptor));
		memcpy(&(libusbBulkTransferInfo->outEndpoint), endpoint, sizeof(struct libusb_endpoint_descriptor));
	}

	return 0;
}

//获取双向通讯的人体输入设备接口描述符
int GetUsbHidDeviceInterface(const struct libusb_interface_descriptor *interface, LibusbBulkTransferInfo *libusbBulkTransferInfo)
{
	int error = 0;
	uint8_t i;

	if(interface == NULL || libusbBulkTransferInfo == NULL)
		return -1;

	//人体输入设备,并且具有输入输出端点,二维码设备比较全,所以,需要增加一个条件,键盘的长度 = 0x08
	if(interface->bInterfaceClass == 0x03 && interface->bNumEndpoints >= 2)
	{
		if(gUsbDebugEnable)
		{	
			printf(" ----------------  Interface Infor: ----------------\n");
			printf(" bInterfaceNumber:   %d\n", interface->bInterfaceNumber);
			printf(" bAlternateSetting:  %d\n", interface->bAlternateSetting);
			printf(" bNumEndpoints:      %d\n", interface->bNumEndpoints);
			printf(" bInterfaceClass:    %d\n", interface->bInterfaceClass);
			printf(" bInterfaceSubClass: %d\n", interface->bInterfaceSubClass);
			printf(" bInterfaceProtocol: %d\n", interface->bInterfaceProtocol);
			printf(" iInterface:         %d\n", interface->iInterface);

			printf(" ----------------  Interface -----------------\n");
		}

		for (i = 0; i < interface->bNumEndpoints; i++)
		{
			//轮寻端点,获取输入输出端点
			error = GetInEndpointAndOutEndpoint(&interface->endpoint[i], libusbBulkTransferInfo);	
			if(error != 0)
				return -1;		
		}

		memset(&(libusbBulkTransferInfo->interface), 0, sizeof(struct libusb_interface_descriptor));
		memcpy(&(libusbBulkTransferInfo->interface), interface, sizeof(struct libusb_interface_descriptor));

		return 0;
	}

	return -1;
}

保存写博客记录是一个好习惯,又学习到了。

/**
 *         ┏┓   ┏┓+ +
 *        ┏┛┻━━━┛┻┓ + +
 *        ┃       ┃
 *        ┃   ━   ┃ ++ + + +
 *        ████━████ ┃+
 *        ┃       ┃ +
 *        ┃   ┻   ┃
 *        ┃       ┃ + +
 *        ┗━┓   ┏━┛
 *          ┃   ┃
 *          ┃   ┃ + + + +
 *          ┃   ┃    Code is far away from bug with the animal protecting
 *          ┃   ┃ +     神兽保佑,代码无bug
 *          ┃   ┃
 *          ┃   ┃  +
 *          ┃    ┗━━━┓ + +
 *          ┃        ┣┓
 *          ┃        ┏┛
 *          ┗┓┓┏━┳┓┏┛ + + + +
 *           ┃┫┫ ┃┫┫
 *           ┗┻┛ ┗┻┛+ + + +
 *
 * @author chenxi
 * @update 2020年5月17日17:35:57
 */
————————————————
 

参数大神文章:

https://blog.csdn.net/plauajoke/article/details/8537740

  • 14
    点赞
  • 87
    收藏
    觉得还不错? 一键收藏
  • 17
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值