555 lines
13 KiB
C
555 lines
13 KiB
C
|
/***************************************************************************
|
|||
|
|
|||
|
Copyright (c) 1998 Microsoft Corporation
|
|||
|
|
|||
|
Module Name:
|
|||
|
|
|||
|
WRITE.C
|
|||
|
|
|||
|
Abstract:
|
|||
|
|
|||
|
Routines that perform write functionality
|
|||
|
|
|||
|
Environment:
|
|||
|
|
|||
|
kernel mode only
|
|||
|
|
|||
|
Notes:
|
|||
|
|
|||
|
THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY
|
|||
|
KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
|
|||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR
|
|||
|
PURPOSE.
|
|||
|
|
|||
|
Copyright (c) 1998 Microsoft Corporation. All Rights Reserved.
|
|||
|
|
|||
|
|
|||
|
Revision History:
|
|||
|
|
|||
|
9/25/98 : created
|
|||
|
|
|||
|
Authors:
|
|||
|
|
|||
|
Louis J. Giliberto, Jr.
|
|||
|
|
|||
|
|
|||
|
****************************************************************************/
|
|||
|
|
|||
|
|
|||
|
#include <wdm.h>
|
|||
|
#include <ntddser.h>
|
|||
|
#include <stdio.h>
|
|||
|
#include <stdlib.h>
|
|||
|
#include <usb.h>
|
|||
|
#include <usbdrivr.h>
|
|||
|
#include <usbdlib.h>
|
|||
|
#include <usbcomm.h>
|
|||
|
|
|||
|
#ifdef WMI_SUPPORT
|
|||
|
#include <wmilib.h>
|
|||
|
#include <wmidata.h>
|
|||
|
#include <wmistr.h>
|
|||
|
#endif
|
|||
|
|
|||
|
#include "usbser.h"
|
|||
|
#include "utils.h"
|
|||
|
#include "debugwdm.h"
|
|||
|
|
|||
|
#ifdef ALLOC_PRAGMA
|
|||
|
#pragma alloc_text(PAGEUSBS, UsbSer_Write)
|
|||
|
#pragma alloc_text(PAGEUSBS, UsbSerGiveWriteToUsb)
|
|||
|
#endif // ALLOC_PRAGMA
|
|||
|
|
|||
|
|
|||
|
NTSTATUS
|
|||
|
UsbSerFlush(IN PDEVICE_OBJECT PDevObj, PIRP PIrp)
|
|||
|
{
|
|||
|
NTSTATUS status = STATUS_SUCCESS;
|
|||
|
PDEVICE_EXTENSION pDevExt;
|
|||
|
ULONG pendingIrps;
|
|||
|
|
|||
|
UsbSerSerialDump(USBSERTRACEWR, (">UsbSerFlush(%08X)\n", PIrp));
|
|||
|
|
|||
|
pDevExt = (PDEVICE_EXTENSION)PDevObj->DeviceExtension;
|
|||
|
|
|||
|
//
|
|||
|
// All we will do is wait until the write pipe has nothing pending.
|
|||
|
// We do this by checking the outstanding count, and if it hits 1 or 0,
|
|||
|
// then the completion routine will set an event we are waiting for.
|
|||
|
//
|
|||
|
|
|||
|
InterlockedIncrement(&pDevExt->PendingDataOutCount);
|
|||
|
|
|||
|
pendingIrps = InterlockedDecrement(&pDevExt->PendingDataOutCount);
|
|||
|
|
|||
|
if ((pendingIrps) && (pendingIrps != 1)) {
|
|||
|
//
|
|||
|
// Wait for flush
|
|||
|
//
|
|||
|
|
|||
|
KeWaitForSingleObject(&pDevExt->PendingFlushEvent, Executive,
|
|||
|
KernelMode, FALSE, NULL);
|
|||
|
} else {
|
|||
|
if (pendingIrps == 0) {
|
|||
|
//
|
|||
|
// We need to wake others since our decrement caused the event
|
|||
|
//
|
|||
|
|
|||
|
KeSetEvent(&pDevExt->PendingDataOutEvent, IO_NO_INCREMENT, FALSE);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
PIrp->IoStatus.Status = STATUS_SUCCESS;
|
|||
|
|
|||
|
IoCompleteRequest(PIrp, IO_NO_INCREMENT);
|
|||
|
|
|||
|
UsbSerSerialDump(USBSERTRACEWR, ("<UsbSerFlush %08X \n", STATUS_SUCCESS));
|
|||
|
|
|||
|
return STATUS_SUCCESS;
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
|
|||
|
NTSTATUS
|
|||
|
UsbSer_Write(IN PDEVICE_OBJECT PDevObj, PIRP PIrp)
|
|||
|
/*++
|
|||
|
|
|||
|
Routine Description:
|
|||
|
|
|||
|
Process the IRPs sent to this device for writing.
|
|||
|
|
|||
|
Arguments:
|
|||
|
|
|||
|
PDevObj - Pointer to the device object for the device written to
|
|||
|
PIrp - Pointer to the write IRP.
|
|||
|
|
|||
|
Return Value:
|
|||
|
|
|||
|
NTSTATUS
|
|||
|
|
|||
|
--*/
|
|||
|
{
|
|||
|
KIRQL oldIrql;
|
|||
|
LARGE_INTEGER totalTime;
|
|||
|
SERIAL_TIMEOUTS timeouts;
|
|||
|
NTSTATUS status;
|
|||
|
PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION)PDevObj->DeviceExtension;
|
|||
|
PIO_STACK_LOCATION pIrpSp = IoGetCurrentIrpStackLocation(PIrp);
|
|||
|
|
|||
|
USBSER_ALWAYS_LOCKED_CODE();
|
|||
|
|
|||
|
UsbSerSerialDump(USBSERTRACEWR, (">UsbSer_Write(%08X)\n", PIrp));
|
|||
|
|
|||
|
PIrp->IoStatus.Information = 0L;
|
|||
|
totalTime.QuadPart = (LONGLONG)0;
|
|||
|
|
|||
|
//
|
|||
|
// Quick check for a zero length write. If it is zero length
|
|||
|
// then we are already done!
|
|||
|
//
|
|||
|
|
|||
|
if (pIrpSp->Parameters.Write.Length == 0) {
|
|||
|
status = PIrp->IoStatus.Status = STATUS_SUCCESS;
|
|||
|
IoCompleteRequest(PIrp, IO_NO_INCREMENT);
|
|||
|
goto UsbSer_WriteExit;
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
//
|
|||
|
// Make sure the device is accepting request and then...
|
|||
|
// Calculate the timeout value needed for the
|
|||
|
// request. Note that the values stored in the
|
|||
|
// timeout record are in milliseconds. Note that
|
|||
|
// if the timeout values are zero then we won't start
|
|||
|
// the timer.
|
|||
|
//
|
|||
|
|
|||
|
ACQUIRE_SPINLOCK(pDevExt, &pDevExt->ControlLock, &oldIrql);
|
|||
|
|
|||
|
if (pDevExt->CurrentDevicePowerState != PowerDeviceD0) {
|
|||
|
RELEASE_SPINLOCK(pDevExt, &pDevExt->ControlLock, oldIrql);
|
|||
|
status = PIrp->IoStatus.Status = STATUS_UNSUCCESSFUL;
|
|||
|
IoCompleteRequest(PIrp, IO_NO_INCREMENT);
|
|||
|
goto UsbSer_WriteExit;
|
|||
|
}
|
|||
|
|
|||
|
timeouts = pDevExt->Timeouts;
|
|||
|
RELEASE_SPINLOCK(pDevExt, &pDevExt->ControlLock, oldIrql);
|
|||
|
|
|||
|
if (timeouts.WriteTotalTimeoutConstant
|
|||
|
|| timeouts.WriteTotalTimeoutMultiplier) {
|
|||
|
|
|||
|
//
|
|||
|
// We have some timer values to calculate.
|
|||
|
//
|
|||
|
|
|||
|
|
|||
|
totalTime.QuadPart
|
|||
|
= ((LONGLONG)((UInt32x32To64((pIrpSp->MajorFunction == IRP_MJ_WRITE)
|
|||
|
? (pIrpSp->Parameters.Write.Length)
|
|||
|
: 1,
|
|||
|
timeouts.WriteTotalTimeoutMultiplier)
|
|||
|
+ timeouts.WriteTotalTimeoutConstant))) * -10000;
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
//
|
|||
|
// The Irp may be going to the write routine shortly. Now
|
|||
|
// is a good time to init its ref counts.
|
|||
|
//
|
|||
|
|
|||
|
USBSER_INIT_REFERENCE(PIrp);
|
|||
|
|
|||
|
//
|
|||
|
// We need to see if this Irp should be cancelled.
|
|||
|
//
|
|||
|
|
|||
|
ACQUIRE_CANCEL_SPINLOCK(pDevExt, &oldIrql);
|
|||
|
|
|||
|
if (PIrp->Cancel) {
|
|||
|
RELEASE_CANCEL_SPINLOCK(pDevExt, oldIrql);
|
|||
|
status = PIrp->IoStatus.Status = STATUS_CANCELLED;
|
|||
|
} else {
|
|||
|
// IoMarkIrpPending(PIrp);
|
|||
|
// status = STATUS_PENDING;
|
|||
|
|
|||
|
//
|
|||
|
// We give the IRP to the USB subsystem -- he will need
|
|||
|
// to know how to cancel it himself
|
|||
|
//
|
|||
|
|
|||
|
IoSetCancelRoutine(PIrp, NULL);
|
|||
|
RELEASE_CANCEL_SPINLOCK(pDevExt, oldIrql);
|
|||
|
|
|||
|
status = UsbSerGiveWriteToUsb(pDevExt, PIrp, totalTime);
|
|||
|
}
|
|||
|
|
|||
|
UsbSer_WriteExit:;
|
|||
|
|
|||
|
UsbSerSerialDump(USBSERTRACEWR, ("<UsbSer_Write %08X\n", status));
|
|||
|
|
|||
|
return status;
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
NTSTATUS
|
|||
|
UsbSerWriteComplete(IN PDEVICE_OBJECT PDevObj, IN PIRP PIrp,
|
|||
|
IN PUSBSER_WRITE_PACKET PPacket)
|
|||
|
/*++
|
|||
|
|
|||
|
Routine Description:
|
|||
|
|
|||
|
This routine is the completion routine for all write requests.
|
|||
|
When a write completes, we go through here in order to free up
|
|||
|
the URB.
|
|||
|
|
|||
|
Arguments:
|
|||
|
|
|||
|
PDevObj - Pointer to device object
|
|||
|
|
|||
|
PIrp - Irp we are completing
|
|||
|
|
|||
|
PUrb - Urb which will be freed
|
|||
|
|
|||
|
|
|||
|
Return Value:
|
|||
|
|
|||
|
NTSTATUS -- as stored in the Irp.
|
|||
|
|
|||
|
--*/
|
|||
|
{
|
|||
|
NTSTATUS status;
|
|||
|
PIO_STACK_LOCATION pIrpStack = IoGetCurrentIrpStackLocation(PIrp);
|
|||
|
KIRQL cancelIrql;
|
|||
|
PURB pUrb = &PPacket->Urb;
|
|||
|
PDEVICE_EXTENSION pDevExt = PPacket->DeviceExtension;
|
|||
|
ULONG curCount;
|
|||
|
|
|||
|
UsbSerSerialDump(USBSERTRACEWR, (">UsbSerWriteComplete(%08X)\n", PIrp));
|
|||
|
|
|||
|
status = PIrp->IoStatus.Status;
|
|||
|
|
|||
|
if (status == STATUS_SUCCESS) {
|
|||
|
|
|||
|
// see if we are reusing an IOCTL IRP
|
|||
|
if(pIrpStack->MajorFunction == IRP_MJ_DEVICE_CONTROL)
|
|||
|
{
|
|||
|
PIrp->IoStatus.Information = 0L;
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
PIrp->IoStatus.Information
|
|||
|
= pUrb->UrbBulkOrInterruptTransfer.TransferBufferLength;
|
|||
|
pIrpStack->Parameters.Write.Length = (ULONG)PIrp->IoStatus.Information;
|
|||
|
}
|
|||
|
|
|||
|
} else if (status == STATUS_CANCELLED) {
|
|||
|
//
|
|||
|
// If it comes back as cancelled, it may really have timed out. We
|
|||
|
// can tell by looking at the packet attached to it.
|
|||
|
//
|
|||
|
|
|||
|
if (PPacket->Status) {
|
|||
|
status = PIrp->IoStatus.Status = PPacket->Status;
|
|||
|
UsbSerSerialDump(USBSERTRACEWR, ("Modified Write Status %08X\n",
|
|||
|
PIrp->IoStatus.Status));
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
//
|
|||
|
// Cancel the write timer
|
|||
|
//
|
|||
|
|
|||
|
if (PPacket->WriteTimeout.QuadPart != 0) {
|
|||
|
KeCancelTimer(&PPacket->WriteTimer);
|
|||
|
}
|
|||
|
|
|||
|
DEBUG_MEMFREE(PPacket);
|
|||
|
|
|||
|
//
|
|||
|
// Reset the pend if necessary
|
|||
|
//
|
|||
|
|
|||
|
if (PIrp->PendingReturned) {
|
|||
|
IoMarkIrpPending(PIrp);
|
|||
|
}
|
|||
|
|
|||
|
//
|
|||
|
// See if we should mark the transmit as empty
|
|||
|
//
|
|||
|
|
|||
|
if (InterlockedDecrement(&pDevExt->PendingWriteCount) == 0) {
|
|||
|
UsbSerProcessEmptyTransmit(pDevExt);
|
|||
|
}
|
|||
|
|
|||
|
//
|
|||
|
// Notify everyone if this is the last IRP
|
|||
|
//
|
|||
|
|
|||
|
curCount = InterlockedDecrement(&pDevExt->PendingDataOutCount);
|
|||
|
|
|||
|
if ((curCount == 0) || (curCount == 1)) {
|
|||
|
UsbSerSerialDump(USBSERTRACEWR, ("DataOut Pipe is flushed\n"));
|
|||
|
KeSetEvent(&pDevExt->PendingFlushEvent, IO_NO_INCREMENT, FALSE);
|
|||
|
|
|||
|
if (curCount == 0) {
|
|||
|
UsbSerSerialDump(USBSERTRACEWR, ("DataOut Pipe is empty\n"));
|
|||
|
KeSetEvent(&pDevExt->PendingDataOutEvent, IO_NO_INCREMENT, FALSE);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
//
|
|||
|
// Finish off this IRP
|
|||
|
//
|
|||
|
|
|||
|
|
|||
|
ACQUIRE_CANCEL_SPINLOCK(pDevExt, &cancelIrql);
|
|||
|
|
|||
|
UsbSerTryToCompleteCurrent(pDevExt, cancelIrql, status,
|
|||
|
&PIrp, NULL, NULL,
|
|||
|
&pDevExt->WriteRequestTotalTimer, NULL,
|
|||
|
NULL, USBSER_REF_RXBUFFER, FALSE);
|
|||
|
|
|||
|
|
|||
|
UsbSerSerialDump(USBSERTRACEWR, ("<UsbSerWriteComplete %08X\n", status));
|
|||
|
return status;
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
|
|||
|
NTSTATUS
|
|||
|
UsbSerGiveWriteToUsb(IN PDEVICE_EXTENSION PDevExt, IN PIRP PIrp,
|
|||
|
IN LARGE_INTEGER TotalTime)
|
|||
|
/*++
|
|||
|
|
|||
|
Routine Description:
|
|||
|
|
|||
|
This function passes a write IRP down to USB to perform the write
|
|||
|
to the device.
|
|||
|
|
|||
|
Arguments:
|
|||
|
|
|||
|
PDevExt - Pointer to device extension
|
|||
|
|
|||
|
PIrp - Write IRP
|
|||
|
|
|||
|
TotalTime - Timeout value for total timer
|
|||
|
|
|||
|
|
|||
|
Return Value:
|
|||
|
|
|||
|
NTSTATUS
|
|||
|
|
|||
|
--*/
|
|||
|
{
|
|||
|
NTSTATUS status;
|
|||
|
PURB pTxUrb;
|
|||
|
PIO_STACK_LOCATION pIrpSp;
|
|||
|
KIRQL cancelIrql;
|
|||
|
PUSBSER_WRITE_PACKET pWrPacket;
|
|||
|
|
|||
|
USBSER_ALWAYS_LOCKED_CODE();
|
|||
|
|
|||
|
UsbSerSerialDump(USBSERTRACEWR, (">UsbSerGiveWriteToUsb(%08X)\n",
|
|||
|
PIrp));
|
|||
|
|
|||
|
USBSER_SET_REFERENCE(PIrp, USBSER_REF_RXBUFFER);
|
|||
|
|
|||
|
|
|||
|
pIrpSp = IoGetCurrentIrpStackLocation(PIrp);
|
|||
|
|
|||
|
//
|
|||
|
// Allocate memory for URB / Write packet
|
|||
|
//
|
|||
|
|
|||
|
pWrPacket = DEBUG_MEMALLOC(NonPagedPool, sizeof(USBSER_WRITE_PACKET));
|
|||
|
|
|||
|
if (pWrPacket == NULL) {
|
|||
|
status = PIrp->IoStatus.Status = STATUS_INSUFFICIENT_RESOURCES;
|
|||
|
|
|||
|
ACQUIRE_CANCEL_SPINLOCK(PDevExt, &cancelIrql);
|
|||
|
|
|||
|
UsbSerTryToCompleteCurrent(PDevExt, cancelIrql, status, &PIrp,
|
|||
|
NULL,
|
|||
|
&PDevExt->WriteRequestTotalTimer, NULL, NULL,
|
|||
|
NULL, USBSER_REF_RXBUFFER, TRUE);
|
|||
|
|
|||
|
return status;
|
|||
|
}
|
|||
|
|
|||
|
RtlZeroMemory(pWrPacket, sizeof(USBSER_WRITE_PACKET));
|
|||
|
|
|||
|
pTxUrb = &pWrPacket->Urb;
|
|||
|
pWrPacket->DeviceExtension = PDevExt;
|
|||
|
pWrPacket->Irp = PIrp;
|
|||
|
pWrPacket->WriteTimeout = TotalTime;
|
|||
|
|
|||
|
if (TotalTime.QuadPart != 0) {
|
|||
|
KeInitializeTimer(&pWrPacket->WriteTimer);
|
|||
|
KeInitializeDpc(&pWrPacket->TimerDPC, UsbSerWriteTimeout, pWrPacket);
|
|||
|
KeSetTimer(&pWrPacket->WriteTimer, TotalTime, &pWrPacket->TimerDPC);
|
|||
|
}
|
|||
|
|
|||
|
//
|
|||
|
// Build USB write request
|
|||
|
//
|
|||
|
|
|||
|
BuildReadRequest(pTxUrb, PIrp->AssociatedIrp.SystemBuffer,
|
|||
|
pIrpSp->Parameters.Write.Length, PDevExt->DataOutPipe,
|
|||
|
FALSE);
|
|||
|
|
|||
|
#if DBG
|
|||
|
if (UsbSerSerialDebugLevel & USBSERDUMPWR) {
|
|||
|
ULONG i;
|
|||
|
|
|||
|
DbgPrint("WR: ");
|
|||
|
|
|||
|
for (i = 0; i < pIrpSp->Parameters.Write.Length; i++) {
|
|||
|
DbgPrint("%02x ", *(((PUCHAR)PIrp->AssociatedIrp.SystemBuffer) + i) & 0xFF);
|
|||
|
}
|
|||
|
|
|||
|
DbgPrint("\n\n");
|
|||
|
}
|
|||
|
#endif
|
|||
|
|
|||
|
//
|
|||
|
// Set Irp up for a submit Urb IOCTL
|
|||
|
//
|
|||
|
|
|||
|
IoCopyCurrentIrpStackLocationToNext(PIrp);
|
|||
|
|
|||
|
pIrpSp = IoGetNextIrpStackLocation(PIrp);
|
|||
|
|
|||
|
pIrpSp->MajorFunction = IRP_MJ_INTERNAL_DEVICE_CONTROL;
|
|||
|
pIrpSp->Parameters.Others.Argument1 = pTxUrb;
|
|||
|
pIrpSp->Parameters.DeviceIoControl.IoControlCode
|
|||
|
= IOCTL_INTERNAL_USB_SUBMIT_URB;
|
|||
|
|
|||
|
IoSetCompletionRoutine(PIrp, UsbSerWriteComplete, pWrPacket, TRUE, TRUE,
|
|||
|
TRUE);
|
|||
|
|
|||
|
//
|
|||
|
// Increment the pending write count
|
|||
|
//
|
|||
|
|
|||
|
InterlockedIncrement(&PDevExt->PendingWriteCount);
|
|||
|
InterlockedIncrement(&PDevExt->PendingDataOutCount);
|
|||
|
|
|||
|
//
|
|||
|
// Send IRP down
|
|||
|
//
|
|||
|
|
|||
|
status = IoCallDriver(PDevExt->StackDeviceObject, PIrp);
|
|||
|
|
|||
|
|
|||
|
#if 0
|
|||
|
|
|||
|
// this is done in the completion routine, so we don't need to do it here
|
|||
|
|
|||
|
if (!NT_SUCCESS(status)) {
|
|||
|
ULONG outCount;
|
|||
|
|
|||
|
if (InterlockedDecrement(&PDevExt->PendingWriteCount) == 0) {
|
|||
|
UsbSerProcessEmptyTransmit(PDevExt);
|
|||
|
}
|
|||
|
|
|||
|
outCount = InterlockedDecrement(&PDevExt->PendingDataOutCount);
|
|||
|
|
|||
|
if ((outCount == 0) || (outCount == 1)) {
|
|||
|
KeSetEvent(&PDevExt->PendingFlushEvent, IO_NO_INCREMENT, FALSE);
|
|||
|
|
|||
|
if (outCount == 0) {
|
|||
|
KeSetEvent(&PDevExt->PendingDataOutEvent, IO_NO_INCREMENT, FALSE);
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
#endif
|
|||
|
|
|||
|
UsbSerSerialDump(USBSERTRACEWR, ("<UsbSerGiveWriteToUsb %08X\n", status));
|
|||
|
|
|||
|
return status;
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
VOID
|
|||
|
UsbSerWriteTimeout(IN PKDPC PDpc, IN PVOID DeferredContext,
|
|||
|
IN PVOID SystemContext1, IN PVOID SystemContext2)
|
|||
|
/*++
|
|||
|
|
|||
|
Routine Description:
|
|||
|
|
|||
|
This function is called when the write timeout timer expires.
|
|||
|
|
|||
|
Arguments:
|
|||
|
|
|||
|
PDpc - Unused
|
|||
|
|
|||
|
DeferredContext - Actually the write packet
|
|||
|
|
|||
|
SystemContext1 - Unused
|
|||
|
|
|||
|
SystemContext2 - Unused
|
|||
|
|
|||
|
|
|||
|
Return Value:
|
|||
|
|
|||
|
VOID
|
|||
|
|
|||
|
--*/
|
|||
|
{
|
|||
|
PUSBSER_WRITE_PACKET pPacket = (PUSBSER_WRITE_PACKET)DeferredContext;
|
|||
|
|
|||
|
UNREFERENCED_PARAMETER(PDpc);
|
|||
|
UNREFERENCED_PARAMETER(SystemContext1);
|
|||
|
UNREFERENCED_PARAMETER(SystemContext2);
|
|||
|
|
|||
|
UsbSerSerialDump(USBSERTRACETM, (">UsbSerWriteTimeout\n"));
|
|||
|
|
|||
|
if (IoCancelIrp(pPacket->Irp)) {
|
|||
|
pPacket->Status = STATUS_TIMEOUT;
|
|||
|
}
|
|||
|
|
|||
|
UsbSerSerialDump(USBSERTRACETM, ("<UsbSerWriteTimeout\n"));
|
|||
|
}
|
|||
|
|