windows USB 设备驱动程序开发-USB 设备模拟 (一)

Windows 操作系统中 (UDE) 支持 USB 模拟设备,用于开发模拟通用串行总线 (USB) 主机控制器驱动程序和连接的虚拟 USB 设备。 这两个组件组合成单个 KMDF 驱动程序,该驱动程序可以与 Microsoft 提供的 USB 设备模拟类扩展 (UdeCx) 通信。

Windows 驱动程序工具包 (WDK) 包含开发驱动程序所需的资源,如头文件、库、工具和示例。若要编写功能控制器驱动程序,需要:

UdeCx: (udecx.sys) 函数驱动程序使用的 WDF 扩展。 此扩展包含在 Windows 中。
链接到存根库 (Udecxstub.lib) 。 存根库位于 WDK 中。包括 WDK 中提供的 Udecx.h。

UDE 的体系结构

UDE 驱动程序的结构如下:

在上图中,

  • USB 集线器驱动程序 (Usbhub3.sys) 是 KMDF 驱动程序。 集线器驱动程序负责管理 USB 集线器及其端口、枚举和创建物理设备对象, (PDO) USB 设备和可能连接到其下游端口的其他集线器;
  • USB 主机控制器扩展 (Ucx01000.sys) 是堆栈中上述集线器驱动程序的抽象层,提供一种通用机制,用于将请求排队到基础主机控制器驱动程序;
  • UdeCx) (UDE 类扩展通过客户端实现的回调函数调用到 UDE 客户端驱动程序。 类扩展为客户端驱动程序提供例程,用于创建和管理 UDE 对象;
  • UDE 客户端驱动程序 管理硬件,与 WDF 和 UDE API 交互。 上边缘使用 USB 构造与 WDF 和 UDE 类扩展通信。 其下边缘使用硬件的接口与硬件通信;
  • 自定义硬件:例如,可以模拟 PCI 硬件以用作 USB 设备;
UDE 设备节点

下面是为 UDE 客户端驱动程序加载的设备堆栈:

编写 UDE 客户端驱动程序
本文介绍 USB 设备仿真 (UDE) 类扩展的行为,以及客户机驱动程序必须为仿真主机控制器及其连接的设备执行的任务。 它提供有关类驱动程序和类扩展如何通过一组例程和回调函数与每个函数进行通信的信息。 它还介绍了客户端驱动程序应实现的功能。

UDE对象和句柄

UDE 类扩展和客户端驱动程序使用表示模拟主机控制器和虚拟设备的特定 WDF 对象,包括用于在设备和主机之间传输数据的端点和 URB。 客户端驱动程序请求创建对象,并且对象的生存期由类扩展管理。

  • 模拟主机控制器对象 (WDFDEVICE): 表示模拟的主机控制器,是 UDE 类扩展和客户端驱动程序之间的主句柄;
  • UDE 设备对象 (UDECXUSBDEVICE): 表示连接到模拟主机控制器上的端口的虚拟 USB 设备;
  • UDE 端点对象 (UDECXUSBENDPOINT: 表示 USB 设备的顺序数据管道。 用于接收端点发送或接收数据的软件请求;
初始化模拟主机控制器

下面是客户端驱动程序检索模拟主机控制器的 WDFDEVICE 句柄的顺序的摘要。 我们建议驱动程序在其 EvtDriverDeviceAdd 回调函数中执行这些任务:

1.通过传递对框架传递 WDFDEVICE_INIT 的引用来调用 UdecxInitializeWdfDeviceInit;

2.使用设置信息初始化 WDFDEVICE_INIT 结构,以便此设备与其他 USB 主机控制器类似。 例如,分配 FDO 名称和符号链接,将设备接口注册到 Microsoft 提供的 GUID_DEVINTERFACE_USB_HOST_CONTROLLER GUID 作为设备接口 GUID,以便应用程序可以打开设备的句柄;

3.调用 WdfDeviceCreate 以创建框架设备对象;

4.调用 UdecxWdfDeviceAddUsbDeviceEmulation 并注册客户端驱动程序的回调函数;

下面是与主机控制器对象关联的回调函数,这些函数由 UDE 类扩展调用。 这些函数必须由客户端驱动程序实现。

  • EVT_UDECX_WDF_DEVICE_QUERY_USB_CAPABILITY:确定客户端驱动程序必须向类扩展报告的主控制器支持的功能;
  • EVT_UDECX_WDF_DEVICE_RESET:可选。 重置主机控制器和/或连接的设备;
EVT_WDF_DRIVER_DEVICE_ADD                 Controller_WdfEvtDeviceAdd;

#define BASE_DEVICE_NAME                  L"\\Device\\USBFDO-"
#define BASE_SYMBOLIC_LINK_NAME           L"\\DosDevices\\HCD"

#define DeviceNameSize                    sizeof(BASE_DEVICE_NAME)+MAX_SUFFIX_SIZE
#define SymLinkNameSize                   sizeof(BASE_SYMBOLIC_LINK_NAME)+MAX_SUFFIX_SIZE

NTSTATUS
Controller_WdfEvtDeviceAdd(
    _In_
        WDFDRIVER Driver,
    _Inout_
        PWDFDEVICE_INIT WdfDeviceInit
    )
{
    NTSTATUS                            status;
    WDFDEVICE                           wdfDevice;
    WDF_PNPPOWER_EVENT_CALLBACKS        wdfPnpPowerCallbacks;
    WDF_OBJECT_ATTRIBUTES               wdfDeviceAttributes;
    WDF_OBJECT_ATTRIBUTES               wdfRequestAttributes;
    UDECX_WDF_DEVICE_CONFIG             controllerConfig;
    WDF_FILEOBJECT_CONFIG               fileConfig;
    PWDFDEVICE_CONTEXT                  pControllerContext;
    WDF_IO_QUEUE_CONFIG                 defaultQueueConfig;
    WDF_DEVICE_POWER_POLICY_IDLE_SETTINGS
                                        idleSettings;
    UNICODE_STRING                      refString;
    ULONG instanceNumber;
    BOOLEAN isCreated;

    DECLARE_UNICODE_STRING_SIZE(uniDeviceName, DeviceNameSize);
    DECLARE_UNICODE_STRING_SIZE(uniSymLinkName, SymLinkNameSize);

    UNREFERENCED_PARAMETER(Driver);

    ...

    WdfDeviceInitSetPnpPowerEventCallbacks(WdfDeviceInit, &wdfPnpPowerCallbacks);

    WDF_OBJECT_ATTRIBUTES_INIT_CONTEXT_TYPE(&wdfRequestAttributes, REQUEST_CONTEXT);
    WdfDeviceInitSetRequestAttributes(WdfDeviceInit, &wdfRequestAttributes);

// To distinguish I/O sent to GUID_DEVINTERFACE_USB_HOST_CONTROLLER, we will enable
// enable interface reference strings by calling WdfDeviceInitSetFileObjectConfig
// with FileObjectClass WdfFileObjectWdfXxx.

WDF_FILEOBJECT_CONFIG_INIT(&fileConfig,
                            WDF_NO_EVENT_CALLBACK,
                            WDF_NO_EVENT_CALLBACK,
                            WDF_NO_EVENT_CALLBACK // No cleanup callback function
                            );

...

WdfDeviceInitSetFileObjectConfig(WdfDeviceInit,
                                    &fileConfig,
                                    WDF_NO_OBJECT_ATTRIBUTES);

...

// Do additional setup required for USB controllers.

status = UdecxInitializeWdfDeviceInit(WdfDeviceInit);

...

WDF_OBJECT_ATTRIBUTES_INIT_CONTEXT_TYPE(&wdfDeviceAttributes, WDFDEVICE_CONTEXT);
wdfDeviceAttributes.EvtCleanupCallback = _ControllerWdfEvtCleanupCallback;

// Call WdfDeviceCreate with a few extra compatibility steps to ensure this device looks
// exactly like other USB host controllers.

isCreated = FALSE;

for (instanceNumber = 0; instanceNumber < ULONG_MAX; instanceNumber++) {

    status = RtlUnicodeStringPrintf(&uniDeviceName,
                                    L"%ws%d",
                                    BASE_DEVICE_NAME,
                                    instanceNumber);

    ...

    status = WdfDeviceInitAssignName(*WdfDeviceInit, &uniDeviceName);

    ...

    status = WdfDeviceCreate(WdfDeviceInit, WdfDeviceAttributes, WdfDevice);

    if (status == STATUS_OBJECT_NAME_COLLISION) {

        // This is expected to happen at least once when another USB host controller
        // already exists on the system.

    ...

    } else if (!NT_SUCCESS(status)) {

    ...

    } else {

        isCreated = TRUE;
        break;
    }
}

if (!isCreated) {

    ...
}

// Create the symbolic link (also for compatibility).
status = RtlUnicodeStringPrintf(&uniSymLinkName,
                                L"%ws%d",
                                BASE_SYMBOLIC_LINK_NAME,
                                instanceNumber);
...

status = WdfDeviceCreateSymbolicLink(*WdfDevice, &uniSymLinkName);

...

// Create the device interface.

RtlInitUnicodeString(&refString,
                     USB_HOST_DEVINTERFACE_REF_STRING);

status = WdfDeviceCreateDeviceInterface(wdfDevice,
                                        (LPGUID)&GUID_DEVINTERFACE_USB_HOST_CONTROLLER,
                                        &refString);

...

UDECX_WDF_DEVICE_CONFIG_INIT(&controllerConfig, Controller_EvtUdecxWdfDeviceQueryUsbCapability);

status = UdecxWdfDeviceAddUsbDeviceEmulation(wdfDevice,
                                           &controllerConfig);

// Create default queue. It only supports USB controller IOCTLs. (USB I/O will come through
// in separate USB device queues.)
// Shown later in this topic.

WDF_IO_QUEUE_CONFIG_INIT_DEFAULT_QUEUE(&defaultQueueConfig, WdfIoQueueDispatchSequential);
defaultQueueConfig.EvtIoDeviceControl = ControllerEvtIoDeviceControl;
defaultQueueConfig.PowerManaged = WdfFalse;

status = WdfIoQueueCreate(wdfDevice,
                          &defaultQueueConfig,
                          WDF_NO_OBJECT_ATTRIBUTES,
                          &pControllerContext->DefaultQueue);

...

// Initialize virtual USB device software objects.
// Shown later in this topic.

status = Usb_Initialize(wdfDevice);

...

exit:

    return status;
}
```1.
处理发送到主机控制器的用户模式 IOCTL 请求

初始化期间,UDE 客户端驱动程序公开 GUID_DEVINTERFACE_USB_HOST_CONTROLLER 设备接口 GUID。 这使驱动程序能够接收来自使用该 GUID 打开设备句柄的应用程序的 IOCTL 请求。

为了处理这些请求,客户端驱动程序注册 EvtIoDeviceControl 事件回调。 在实现中,驱动程序可以选择将请求转发到 UDE 类扩展进行处理,而不是处理请求。 若要转发请求,驱动程序必须调用 UdecxWdfDeviceTryHandleUserIoctl。 如果收到的 IOCTL 控制代码对应于标准请求,例如检索设备描述符,则类扩展将处理并成功完成请求。 在这种情况下,UdecxWdfDeviceTryHandleUserIoctl 以 TRUE 作为返回值完成。 否则,调用返回 FALSE,驱动程序必须确定如何完成请求。 在最简单的实现中,驱动程序可以通过调用 WdfRequestComplete 以完成请求,并给出相应的失败代码。

EVT_WDF_IO_QUEUE_IO_DEVICE_CONTROL        Controller_EvtIoDeviceControl;

VOID
Controller_EvtIoDeviceControl(
    _In_
        WDFQUEUE Queue,
    _In_
        WDFREQUEST Request,
    _In_
        size_t OutputBufferLength,
    _In_
        size_t InputBufferLength,
    _In_
        ULONG IoControlCode
)
{
    BOOLEAN handled;
    NTSTATUS status;
    UNREFERENCED_PARAMETER(OutputBufferLength);
    UNREFERENCED_PARAMETER(InputBufferLength);

    handled = UdecxWdfDeviceTryHandleUserIoctl(WdfIoQueueGetDevice(Queue),
                                                Request);

    if (handled) {

        goto exit;
    }

    // Unexpected control code.
    // Fail the request.

    status = STATUS_INVALID_DEVICE_REQUEST;

    WdfRequestComplete(Request, status);

exit:

    return;
}
报告主机控制器的功能

在上层驱动程序可以使用 USB 主机控制器的功能之前,驱动程序必须确定控制器是否支持这些功能。 驱动程序通过调用 WdfUsbTargetDeviceQueryUsbCapability 和 USBD_QueryUsbCapability 进行此类查询。 这些调用将转接到 USB 设备仿真 (UDE) 类扩展。 获取请求后,类扩展将调用客户端驱动程序的 EVT_UDECX_WDF_DEVICE_QUERY_USB_CAPABILITY 实现。 此调用仅在 EvtDriverDeviceAdd 完成之后进行,通常在 EvtDevicePrepareHardware 中,而不是在 EvtDeviceReleaseHardware 之后进行。 这是需要回调函数。

在实现中,客户端驱动程序必须报告它是否支持请求的功能。 UDE 不支持某些功能,例如静态流。

NTSTATUS
Controller_EvtControllerQueryUsbCapability(
    WDFDEVICE     UdeWdfDevice,
    PGUID         CapabilityType,
    ULONG         OutputBufferLength,
    PVOID         OutputBuffer,
    PULONG        ResultLength
)

{
    NTSTATUS status;

    UNREFERENCED_PARAMETER(UdeWdfDevice);
    UNREFERENCED_PARAMETER(OutputBufferLength);
    UNREFERENCED_PARAMETER(OutputBuffer);

    *ResultLength = 0;

    if (RtlCompareMemory(CapabilityType,
                         &GUID_USB_CAPABILITY_CHAINED_MDLS,
                         sizeof(GUID)) == sizeof(GUID)) {

        //
        // TODO: Is GUID_USB_CAPABILITY_CHAINED_MDLS supported?
        // If supported, status = STATUS_SUCCESS
        // Otherwise, status = STATUS_NOT_SUPPORTED
    }

    else {

        status = STATUS_NOT_IMPLEMENTED;
    }

    return status;
}
创建虚拟 USB 服务

虚拟 USB 设备的行为类似于 USB 设备。 它支持具有多个接口的配置,每个接口都支持备用设置。 每个设置可以有一个用于数据传输的端点。 所有描述符(设备、配置、接口、端点)都由 UDE 客户端驱动程序设置,以便设备可以报告与真实 USB 设备非常类似的信息。

 注意: UDE 客户端驱动程序不支持外部集线器

下面是客户端驱动程序为 UDE 设备对象创建 UDECXUSBDEVICE 句柄的顺序的摘要。 驱动程序在检索模拟主机控制器的 WDFDEVICE 句柄后必须执行这些步骤。 我们建议驱动程序在其 EvtDriverDeviceAdd 回调函数中执行这些任务。

1.调用 UdecxUsbDeviceInitAllocate 以获取指向创建设备所需的初始化参数的指针。 此结构由 UDE 类扩展分配;

2.通过设置 UDECX_USB_DEVICE_STATE_CHANGE_CALLBACKS 的成员并调用 UdecxUsbDeviceInitSetStateChangeCallbacks 来注册事件回调函数。 下面是与 UDE 设备对象关联的回调函数,这些函数由 UDE 类扩展调用;

这些函数由客户端驱动程序实现以创建或配置端点。

  • EVT_UDECX_USB_DEVICE_DEFAULT_ENDPOINT_ADD
  • EVT_UDECX_USB_DEVICE_ENDPOINT_ADD
  • EVT_UDECX_USB_DEVICE_ENDPOINTS_CONFIGURE
  • EVT_UDECX_USB_DEVICE_D0_ENTRY
  • EVT_UDECX_USB_DEVICE_D0_EXIT
  • EVT_UDECX_USB_DEVICE_SET_FUNCTION_SUSPEND_AND_WAKE

3.调用 UdecxUsbDeviceInitSetSpeed 以设置 USB 设备速度以及设备类型、USB 2.0 或 SuperSpeed 设备。

4.调用 UdecxUsbDeviceInitSetEndpointsType 以指定设备支持的端点类型:简单或动态。 如果客户端驱动程序选择创建简单端点,驱动程序必须在插入设备之前创建所有端点对象。 设备必须只有一个配置,每个接口只能有一个接口设置。 对于动态端点,驱动程序可以在设备收到 EVT_UDECX_USB_DEVICE_ENDPOINTS_CONFIGURE 事件回调后随时创建端点。 

5.调用上述任一方法,将必要的描述符添加到设备。

  • UdecxUsbDeviceInitAddDescriptor
  • UdecxUsbDeviceInitAddDescriptorWithIndex
  • UdecxUsbDeviceInitAddStringDescriptor
  • UdecxUsbDeviceInitAddStringDescriptorRaw

如果 UDE 类扩展使用上述方法之一收到客户端驱动程序在初始化期间提供的标准描述符的请求,则类扩展会自动完成请求。 类扩展不会将该请求转发到客户端驱动程序。 此设计减少了驱动程序需要处理控制请求的请求数。 此外,它还不需要驱动程序实现描述符逻辑,这些逻辑需要对设置数据包进行广泛的分析,并正确处理 wLength 和 TransferBufferLength。 此列表包括标准请求。 客户端驱动程序不需要为这些请求检查(仅当调用上述方法以添加描述符时):

  • USB_REQUEST_GET_DESCRIPTOR
  • USB_REQUEST_SET_CONFIGURATION
  • USB_REQUEST_SET_INTERFACE
  • USB_REQUEST_SET_ADDRESS
  • USB_REQUEST_SET_FEATURE
  • USB_FEATURE_FUNCTION_SUSPEND
  • USB_FEATURE_REMOTE_WAKEUP
  • USB_REQUEST_CLEAR_FEATURE
  • USB_FEATURE_ENDPOINT_STALL
  • USB_REQUEST_SET_SEL
  • USB_REQUEST_ISOCH_DELAY

但是,对于接口、特定于类或供应商定义的描述符的请求,UDE 类扩展会将它们转发到客户端驱动程序。 驱动程序必须处理这些 GET_DESCRIPTOR 请求。

6.调用 UdecxUsbDeviceCreate 以创建 UDE 设备对象并检索 UDECXUSBDEVICE 句柄。

7.通过调用 UdecxUsbEndpointCreate 创建静态端点。 

8.调用 UdecxUsbDevicePlugIn 以指示设备已附加的 UDE 类扩展,并且可以在端点上接收 I/O 请求。 此调用后,类扩展还可以在端点和 USB 设备上调用回调函数。 请注意如果需要在运行时删除 USB 设备,客户端驱动程序可以调用 UdecxUsbDevicePlugOutAndDelete。 如果驱动程序想要使用设备,则必须通过调用 UdecxUsbDeviceCreate 来创建它。

在此示例中,描述符声明假定为全局变量,此处以 HID 设备为例进行声明:

const UCHAR g_UsbDeviceDescriptor[] = {
    // Device Descriptor
    0x12, // Descriptor Size
    0x01, // Device Descriptor Type
    0x00, 0x03, // USB 3.0
    0x00, // Device class
    0x00, // Device sub-class
    0x00, // Device protocol
    0x09, // Maxpacket size for EP0 : 2^9
    0x5E, 0x04, // Vendor ID
    0x39, 0x00, // Product ID
    0x00, // LSB of firmware version
    0x03, // MSB of firmware version
    0x01, // Manufacture string index
    0x03, // Product string index
    0x00, // Serial number string index
    0x01 // Number of configurations
};

下面是客户端驱动程序通过注册回调函数、设置设备速度、指示端点类型以及最后设置某些设备描述符来指定初始化参数的示例。

NTSTATUS
Usb_Initialize(
    _In_
        WDFDEVICE WdfDevice
    )
{
    NTSTATUS                                status;
    PUSB_CONTEXT                            usbContext;    //Client driver declared context for the host controller object
    PUDECX_USBDEVICE_CONTEXT                deviceContext; //Client driver declared context for the UDE device object
    UDECX_USB_DEVICE_STATE_CHANGE_CALLBACKS callbacks;
    WDF_OBJECT_ATTRIBUTES                   attributes;

    UDECX_USB_DEVICE_PLUG_IN_OPTIONS        pluginOptions;

    usbContext = WdfDeviceGetUsbContext(WdfDevice);

    usbContext->UdecxUsbDeviceInit = UdecxUsbDeviceInitAllocate(WdfDevice);

    if (usbContext->UdecxUsbDeviceInit == NULL) {

        ...
        goto exit;
    }

    // State changed callbacks

    UDECX_USB_DEVICE_CALLBACKS_INIT(&callbacks);
#ifndef SIMPLEENDPOINTS
    callbacks.EvtUsbDeviceDefaultEndpointAdd = UsbDevice_EvtUsbDeviceDefaultEndpointAdd;
    callbacks.EvtUsbDeviceEndpointAdd = UsbDevice_EvtUsbDeviceEndpointAdd;
    callbacks.EvtUsbDeviceEndpointsConfigure = UsbDevice_EvtUsbDeviceEndpointsConfigure;
#endif
    callbacks.EvtUsbDeviceLinkPowerEntry = UsbDevice_EvtUsbDeviceLinkPowerEntry;
    callbacks.EvtUsbDeviceLinkPowerExit = UsbDevice_EvtUsbDeviceLinkPowerExit;
    callbacks.EvtUsbDeviceSetFunctionSuspendAndWake = UsbDevice_EvtUsbDeviceSetFunctionSuspendAndWake;

    UdecxUsbDeviceInitSetStateChangeCallbacks(usbContext->UdecxUsbDeviceInit, &callbacks);

    // Set required attributes.

    UdecxUsbDeviceInitSetSpeed(usbContext->UdecxUsbDeviceInit, UdecxUsbLowSpeed);

#ifdef SIMPLEENDPOINTS
    UdecxUsbDeviceInitSetEndpointsType(usbContext->UdecxUsbDeviceInit, UdecxEndpointTypeSimple);
#else
    UdecxUsbDeviceInitSetEndpointsType(usbContext->UdecxUsbDeviceInit, UdecxEndpointTypeDynamic);
#endif

    // Add device descriptor
    //
    status = UdecxUsbDeviceInitAddDescriptor(usbContext->UdecxUsbDeviceInit,
                                           (PUCHAR)g_UsbDeviceDescriptor,
                                           sizeof(g_UsbDeviceDescriptor));

    if (!NT_SUCCESS(status)) {

        goto exit;
    }

#ifdef USB30

    // Add BOS descriptor for a SuperSpeed device

    status = UdecxUsbDeviceInitAddDescriptor(pUsbContext->UdecxUsbDeviceInit,
                                           (PUCHAR)g_UsbBOSDescriptor,
                                           sizeof(g_UsbBOSDescriptor));

    if (!NT_SUCCESS(status)) {

        goto exit;
    }
#endif

    // String descriptors

    status = UdecxUsbDeviceInitAddDescriptorWithIndex(usbContext->UdecxUsbDeviceInit,
                                                    (PUCHAR)g_LanguageDescriptor,
                                                    sizeof(g_LanguageDescriptor),
                                                    0);

    if (!NT_SUCCESS(status)) {

        goto exit;
    }

    status = UdecxUsbDeviceInitAddStringDescriptor(usbContext->UdecxUsbDeviceInit,
                                                 &g_ManufacturerStringEnUs,
                                                 g_ManufacturerIndex,
                                                 US_ENGLISH);

    if (!NT_SUCCESS(status)) {

        goto exit;
    }

    WDF_OBJECT_ATTRIBUTES_INIT_CONTEXT_TYPE(&attributes, UDECX_USBDEVICE_CONTEXT);

    status = UdecxUsbDeviceCreate(&usbContext->UdecxUsbDeviceInit,
                                &attributes,
                                &usbContext->UdecxUsbDevice);

    if (!NT_SUCCESS(status)) {

        goto exit;
    }

#ifdef SIMPLEENDPOINTS
   // Create the default control endpoint
   // Shown later in this topic.

    status = UsbCreateControlEndpoint(WdfDevice);

    if (!NT_SUCCESS(status)) {

        goto exit;
    }

#endif

    UDECX_USB_DEVICE_PLUG_IN_OPTIONS_INIT(&pluginOptions);
#ifdef USB30
    pluginOptions.Usb30PortNumber = 2;
#else
    pluginOptions.Usb20PortNumber = 1;
#endif
    status = UdecxUsbDevicePlugIn(usbContext->UdecxUsbDevice, &pluginOptions);

exit:

    if (!NT_SUCCESS(status)) {

        UdecxUsbDeviceInitFree(usbContext->UdecxUsbDeviceInit);
        usbContext->UdecxUsbDeviceInit = NULL;

    }

    return status;
}
  • 21
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值