DrvHostSerial.cpp revision 1e1273b11e17928ec3c3a8fff45121aa7a169413
/* $Id$ */
/** @file
* VBox stream I/O devices: Host serial driver
*/
/*
* Copyright (C) 2006-2012 Oracle Corporation
*
* This file is part of VirtualBox Open Source Edition (OSE), as
* available from http://www.virtualbox.org. This file is free software;
* General Public License (GPL) as published by the Free Software
* Foundation, in version 2 as it comes in the "COPYING" file of the
* VirtualBox OSE distribution. VirtualBox OSE is distributed in the
* hope that it will be useful, but WITHOUT ANY WARRANTY of any kind.
*/
/*******************************************************************************
* Header Files *
*******************************************************************************/
#define LOG_GROUP LOG_GROUP_DRV_HOST_SERIAL
#include <iprt/semaphore.h>
#if defined(RT_OS_LINUX) || defined(RT_OS_DARWIN) || defined(RT_OS_SOLARIS) || defined(RT_OS_FREEBSD)
# include <errno.h>
# ifdef RT_OS_SOLARIS
# else
# include <termios.h>
# endif
# include <fcntl.h>
# include <string.h>
# include <unistd.h>
# ifdef RT_OS_DARWIN
# else
# endif
# include <pthread.h>
# ifdef RT_OS_LINUX
/*
* But inclusion of this file however leads to compilation errors because of redefinition of some
* structs. That's why it is defined here until a better solution is found.
*/
# ifndef TIOCM_LOOP
# define TIOCM_LOOP 0x8000
# endif
/* For linux custom baudrate code we also need serial_struct */
# endif /* linux */
#elif defined(RT_OS_WINDOWS)
# include <Windows.h>
#endif
#include "VBoxDD.h"
/*******************************************************************************
* Structures and Typedefs *
*******************************************************************************/
/**
* Char driver instance data.
*
* @implements PDMICHARCONNECTOR
*/
typedef struct DRVHOSTSERIAL
{
/** Pointer to the driver instance structure. */
/** Our char interface. */
/** Receive thread. */
/** Send thread. */
/** Status lines monitor thread. */
/** Send event semaphore */
/** the device path */
char *pszDevicePath;
#if defined(RT_OS_LINUX) || defined(RT_OS_DARWIN) || defined(RT_OS_SOLARIS) || defined(RT_OS_FREEBSD)
/** the device handle */
# ifdef RT_OS_DARWIN
/** The device handle used for reading.
* Used to prevent the read select from blocking the writes. */
# endif
/** The read end of the control pipe */
/** The write end of the control pipe */
# ifndef RT_OS_LINUX
/** The current line status.
* Used by the polling version of drvHostSerialMonitorThread. */
int fStatusLines;
# endif
#elif defined(RT_OS_WINDOWS)
/** the device handle */
/** The event semaphore for waking up the receive thread */
/** The event semaphore for overlapped receiving */
/** For overlapped receiving */
/** The event semaphore for overlapped sending */
/** For overlapped sending */
#endif
/** Internal send FIFO queue */
uint8_t volatile u8SendByte;
bool volatile fSending;
#ifdef RT_OS_DARWIN
/** The number of bytes we've dropped because the send queue
* was full. */
#endif
/** Converts a pointer to DRVCHAR::ICharConnector to a PDRVHOSTSERIAL. */
#define PDMICHAR_2_DRVHOSTSERIAL(pInterface) RT_FROM_MEMBER(pInterface, DRVHOSTSERIAL, ICharConnector)
/* -=-=-=-=- IBase -=-=-=-=- */
/**
* @interface_method_impl{PDMIBASE,pfnQueryInterface}
*/
{
return NULL;
}
/* -=-=-=-=- ICharConnector -=-=-=-=- */
/** @copydoc PDMICHARCONNECTOR::pfnWrite */
static DECLCALLBACK(int) drvHostSerialWrite(PPDMICHARCONNECTOR pInterface, const void *pvBuf, size_t cbWrite)
{
{
return VERR_BUFFER_OVERFLOW;
}
return VINF_SUCCESS;
}
static DECLCALLBACK(int) drvHostSerialSetParameters(PPDMICHARCONNECTOR pInterface, unsigned Bps, char chParity, unsigned cDataBits, unsigned cStopBits)
{
#if defined(RT_OS_LINUX) || defined(RT_OS_DARWIN) || defined(RT_OS_SOLARIS) || defined(RT_OS_FREEBSD)
struct termios *termiosSetup;
int baud_rate;
#elif defined(RT_OS_WINDOWS)
#endif
LogFlow(("%s: Bps=%u chParity=%c cDataBits=%u cStopBits=%u\n", __FUNCTION__, Bps, chParity, cDataBits, cStopBits));
/* Enable receiver */
switch (Bps)
{
case 50:
break;
case 75:
break;
case 110:
break;
case 134:
break;
case 150:
break;
case 200:
break;
case 300:
break;
case 600:
break;
case 1200:
break;
case 1800:
break;
case 2400:
break;
case 4800:
break;
case 9600:
break;
case 19200:
break;
case 38400:
break;
case 57600:
break;
case 115200:
break;
default:
#ifdef RT_OS_LINUX
struct serial_struct serialStruct;
{
if (!serialStruct.custom_divisor)
}
else
#else /* !RT_OS_LINUX */
#endif /* !RT_OS_LINUX */
}
switch (chParity)
{
case 'E':
break;
case 'O':
break;
case 'N':
break;
default:
break;
}
switch (cDataBits)
{
case 5:
break;
case 6:
break;
case 7:
break;
case 8:
break;
default:
break;
}
switch (cStopBits)
{
case 2:
default:
break;
}
/* set serial port to raw input */
#ifdef RT_OS_LINUX
/*
* XXX In Linux, if a thread calls tcsetattr while the monitor thread is
* waiting in ioctl for a modem status change then 8250.c wrongly disables
* modem irqs and so the monitor thread never gets released. The workaround
* is to send a signal after each tcsetattr.
*/
#endif
#elif defined(RT_OS_WINDOWS)
switch (Bps)
{
case 110:
break;
case 300:
break;
case 600:
break;
case 1200:
break;
case 2400:
break;
case 4800:
break;
case 9600:
break;
case 14400:
break;
case 19200:
break;
case 38400:
break;
case 57600:
break;
case 115200:
break;
default:
}
switch (chParity)
{
case 'E':
break;
case 'O':
break;
case 'N':
break;
default:
break;
}
switch (cStopBits)
{
case 1:
break;
case 2:
break;
default:
break;
}
#endif /* RT_OS_WINDOWS */
return VINF_SUCCESS;
}
/* -=-=-=-=- receive thread -=-=-=-=- */
/**
* Send thread loop.
*
* @returns VINF_SUCCESS.
* @param ThreadSelf Thread handle to this thread.
* @param pvUser User argument.
*/
{
return VINF_SUCCESS;
#ifdef RT_OS_WINDOWS
/* Make sure that the halt event semaphore is reset. */
#endif
{
if (RT_FAILURE(rc))
break;
/*
* Write the character to the host device.
*/
{
/* copy the send queue so we get a linear buffer with the maximal size. */
#if defined(RT_OS_LINUX) || defined(RT_OS_DARWIN) || defined(RT_OS_SOLARIS) || defined(RT_OS_FREEBSD)
if (rc == VERR_TRY_AGAIN)
cbWritten = 0;
{
/* ok, block till the device is ready for more (O_NONBLOCK) effect. */
rc = VINF_SUCCESS;
{
/* wait */
# ifdef DEBUG
# endif
/** @todo check rc? */
# ifdef DEBUG
# endif
/* try write more */
if (rc == VERR_TRY_AGAIN)
cbWritten = 0;
else if (RT_FAILURE(rc))
break;
else if (cbWritten >= 1)
break;
rc = VINF_SUCCESS;
}
#elif defined(RT_OS_WINDOWS)
/* perform an overlapped write operation. */
{
dwRet = GetLastError();
if (dwRet == ERROR_IO_PENDING)
{
/*
* write blocked, wait for completion or wakeup...
*/
if (dwRet != WAIT_OBJECT_0)
{
AssertMsg(pThread->enmState != PDMTHREADSTATE_RUNNING, ("The halt event semaphore is set but the thread is still in running state\n"));
break;
}
}
else
}
#endif /* RT_OS_WINDOWS */
if (RT_FAILURE(rc))
{
LogRel(("HostSerial#%d: Serial Write failed with %Rrc; terminating send thread\n", pDrvIns->iInstance, rc));
return rc;
}
break;
} /* write loop */
}
return VINF_SUCCESS;
}
/**
* Unblock the send thread so it can respond to a state change.
*
* @returns a VBox status code.
* @param pDrvIns The driver instance.
* @param pThread The send thread.
*/
{
int rc;
if (RT_FAILURE(rc))
return rc;
#ifdef RT_OS_WINDOWS
return RTErrConvertFromWin32(GetLastError());
#endif
return VINF_SUCCESS;
}
/* -=-=-=-=- receive thread -=-=-=-=- */
/**
* Receive thread loop.
*
* This thread pushes data from the host serial device up the driver
* chain toward the serial device.
*
* @returns VINF_SUCCESS.
* @param ThreadSelf Thread handle to this thread.
* @param pvUser User argument.
*/
{
int rc = VINF_SUCCESS;
int rcThread = VINF_SUCCESS;
return VINF_SUCCESS;
#ifdef RT_OS_WINDOWS
/* Make sure that the halt event semaphore is reset. */
#endif
{
if (!cbRemaining)
{
/* Get a block of data from the host serial device. */
#if defined(RT_OS_DARWIN) /* poll is broken on x86 darwin, returns POLLNVAL. */
# if 1 /* it seems like this select is blocking the write... */
# else
# endif
if (rc == -1)
{
LogRel(("HostSerial#%d: select failed with errno=%d / %Rrc, terminating the worker thread.\n", pDrvIns->iInstance, err, rcThread));
break;
}
/* this might have changed in the meantime */
break;
if (rc == 0)
continue;
/* drain the wakeup pipe */
{
if (RT_FAILURE(rc))
{
LogRel(("HostSerial#%d: draining the wakeup pipe failed with %Rrc, terminating the worker thread.\n", pDrvIns->iInstance, rc));
break;
}
continue;
}
/* read data from the serial port. */
if (RT_FAILURE(rc))
{
LogRel(("HostSerial#%d: (1) Read failed with %Rrc, terminating the worker thread.\n", pDrvIns->iInstance, rc));
break;
}
if (rc < 0)
{
{
/*
* EINTR errors should be harmless, even if they are not supposed to occur in our setup.
*/
Log(("rc=%d revents=%#x,%#x errno=%p %s\n", rc, aFDs[0].revents, aFDs[1].revents, err, strerror(err)));
continue;
}
LogRel(("HostSerial#%d: poll failed with errno=%d / %Rrc, terminating the worker thread.\n", pDrvIns->iInstance, err, rcThread));
break;
}
/* this might have changed in the meantime */
break;
{
break;
/* notification to terminate -- drain the pipe */
continue;
}
if (RT_FAILURE(rc))
{
/* don't terminate worker thread when data unavailable */
if (rc == VERR_TRY_AGAIN)
continue;
LogRel(("HostSerial#%d: (2) Read failed with %Rrc, terminating the worker thread.\n", pDrvIns->iInstance, rc));
break;
}
#elif defined(RT_OS_WINDOWS)
DWORD dwEventMask = 0;
{
dwRet = GetLastError();
if (dwRet == ERROR_IO_PENDING)
{
if (dwRet != WAIT_OBJECT_0)
{
/* notification to terminate */
AssertMsg(pThread->enmState != PDMTHREADSTATE_RUNNING, ("The halt event semaphore is set but the thread is still in running state\n"));
break;
}
}
else
{
LogRel(("HostSerial#%d: Wait failed with error %Rrc; terminating the worker thread.\n", pDrvIns->iInstance, rcThread));
break;
}
}
/* this might have changed in the meantime */
break;
/* Check the event */
if (dwEventMask & EV_RXCHAR)
{
if (!ReadFile(pThis->hDeviceFile, abBuffer, sizeof(abBuffer), &dwNumberOfBytesTransferred, &pThis->overlappedRecv))
{
LogRel(("HostSerial#%d: Read failed with error %Rrc; terminating the worker thread.\n", pDrvIns->iInstance, rcThread));
break;
}
}
else if (dwEventMask & EV_BREAK)
{
Log(("HostSerial#%d: Detected break\n"));
}
else
{
/* The status lines have changed. Notify the device. */
/* Get the new state */
{
if (dwNewStatusLinesState & MS_RLSD_ON)
if (dwNewStatusLinesState & MS_RING_ON)
if (dwNewStatusLinesState & MS_DSR_ON)
if (dwNewStatusLinesState & MS_CTS_ON)
if (RT_FAILURE(rc))
{
/* Notifying device failed, continue but log it */
LogRel(("HostSerial#%d: Notifying device failed with error %Rrc; continuing.\n", pDrvIns->iInstance, rc));
}
}
else
{
/* Getting new state failed, continue but log it */
LogRel(("HostSerial#%d: Getting status lines state failed with error %Rrc; continuing.\n", pDrvIns->iInstance, RTErrConvertFromWin32(GetLastError())));
}
}
#endif
}
else
{
/* Send data to the guest. */
if (RT_SUCCESS(rc))
{
pbBuffer += cbProcessed;
}
else if (rc == VERR_TIMEOUT)
{
/* Normal case, just means that the guest didn't accept a new
* character before the timeout elapsed. Just retry. */
rc = VINF_SUCCESS;
}
else
{
LogRel(("HostSerial#%d: NotifyRead failed with %Rrc, terminating the worker thread.\n", pDrvIns->iInstance, rc));
break;
}
}
}
return rcThread;
}
/**
* Unblock the send thread so it can respond to a state change.
*
* @returns a VBox status code.
* @param pDrvIns The driver instance.
* @param pThread The send thread.
*/
{
#if defined(RT_OS_LINUX) || defined(RT_OS_DARWIN) || defined(RT_OS_SOLARIS) || defined(RT_OS_FREEBSD)
#elif defined(RT_OS_WINDOWS)
return RTErrConvertFromWin32(GetLastError());
return VINF_SUCCESS;
#else
#endif
}
#if defined(RT_OS_LINUX) || defined(RT_OS_DARWIN) || defined(RT_OS_SOLARIS) || defined(RT_OS_FREEBSD)
/* -=-=-=-=- Monitor thread -=-=-=-=- */
/**
* Monitor thread loop.
*
* This thread monitors the status lines and notifies the device
* if they change.
*
* @returns VINF_SUCCESS.
* @param ThreadSelf Thread handle to this thread.
* @param pvUser User argument.
*/
{
int rc = VINF_SUCCESS;
return VINF_SUCCESS;
do
{
unsigned int statusLines;
/*
* Get the status line state.
*/
if (rc < 0)
{
N_("Ioctl failed for serial host device '%s' (%Rrc). The device will not work properly"),
break;
}
uint32_t newStatusLine = 0;
if (statusLines & TIOCM_CAR)
if (statusLines & TIOCM_RNG)
if (statusLines & TIOCM_DSR)
if (statusLines & TIOCM_CTS)
break;
# ifdef RT_OS_LINUX
/*
* Wait for status line change.
*
* XXX In Linux, if a thread calls tcsetattr while the monitor thread is
* waiting in ioctl for a modem status change then 8250.c wrongly disables
* modem irqs and so the monitor thread never gets released. The workaround
* is to send a signal after each tcsetattr.
*/
# else
/* Poll for status line change. */
# endif
}
return VINF_SUCCESS;
}
/**
* Unblock the monitor thread so it can respond to a state change.
* We need to execute this code exactly once during initialization.
* But we don't want to block --- therefore this dedicated thread.
*
* @returns a VBox status code.
* @param pDrvIns The driver instance.
* @param pThread The send thread.
*/
{
# ifdef RT_OS_LINUX
int rc = VINF_SUCCESS;
if (RT_FAILURE(rc))
N_("Suspending serial monitor thread failed for serial device '%s' (%Rrc). The shutdown may take longer than expected"),
# else /* !RT_OS_LINUX*/
/* In polling mode there is nobody to wake up (PDMThread will cancel the sleep). */
# endif /* RT_OS_LINUX */
return VINF_SUCCESS;
}
#endif /* RT_OS_LINUX || RT_OS_DARWIN || RT_OS_SOLARIS */
/**
* Set the modem lines.
*
* @returns VBox status code
* @param pInterface Pointer to the interface structure.
* @param RequestToSend Set to true if this control line should be made active.
* @param DataTerminalReady Set to true if this control line should be made active.
*/
static DECLCALLBACK(int) drvHostSerialSetModemLines(PPDMICHARCONNECTOR pInterface, bool RequestToSend, bool DataTerminalReady)
{
#if defined(RT_OS_LINUX) || defined(RT_OS_DARWIN) || defined(RT_OS_SOLARIS) || defined(RT_OS_FREEBSD)
int modemStateSet = 0;
int modemStateClear = 0;
if (RequestToSend)
else
if (DataTerminalReady)
else
if (modemStateSet)
if (modemStateClear)
#elif defined(RT_OS_WINDOWS)
if (RequestToSend)
else
if (DataTerminalReady)
else
#endif
return VINF_SUCCESS;
}
/**
* Sets the TD line into break condition.
*
* @returns VBox status code.
* @param pInterface Pointer to the interface structure containing the called function pointer.
* @param fBreak Set to true to let the device send a break false to put into normal operation.
* @thread Any thread.
*/
{
#if defined(RT_OS_LINUX) || defined(RT_OS_DARWIN) || defined(RT_OS_SOLARIS) || defined(RT_OS_FREEBSD)
if (fBreak)
else
#elif defined(RT_OS_WINDOWS)
if (fBreak)
else
#endif
return VINF_SUCCESS;
}
/* -=-=-=-=- driver interface -=-=-=-=- */
/**
* Destruct a char driver instance.
*
* Most VM resources are freed by the VM. This callback is provided so that
* any non-VM resources can be freed correctly.
*
* @param pDrvIns The driver instance data.
*/
{
/* Empty the send queue */
{
}
int rc;
#if defined(RT_OS_LINUX) || defined(RT_OS_DARWIN) || defined(RT_OS_SOLARIS) || defined(RT_OS_FREEBSD)
# if defined(RT_OS_DARWIN)
{
{
}
}
# endif
{
}
#elif defined(RT_OS_WINDOWS)
#endif
if (pThis->pszDevicePath)
{
}
}
/**
* Construct a char driver instance.
*
* @copydoc FNPDMDRVCONSTRUCT
*/
static DECLCALLBACK(int) drvHostSerialConstruct(PPDMDRVINS pDrvIns, PCFGMNODE pCfg, uint32_t /*fFlags*/)
{
/*
* Init basic data members and interfaces.
*/
#if defined(RT_OS_LINUX) || defined(RT_OS_DARWIN) || defined(RT_OS_SOLARIS) || defined(RT_OS_FREEBSD)
# ifdef RT_OS_DARWIN
# endif
#elif defined(RT_OS_WINDOWS)
#endif
/* IBase. */
/* ICharConnector. */
/*
* Query configuration.
*/
/* Device */
if (RT_FAILURE(rc))
{
return rc;
}
/*
* Open the device
*/
#ifdef RT_OS_WINDOWS
0, // must be opened with exclusive access
NULL, // no SECURITY_ATTRIBUTES structure
OPEN_EXISTING, // must use OPEN_EXISTING
FILE_FLAG_OVERLAPPED, // overlapped I/O
NULL); // no template file
if (hFile == INVALID_HANDLE_VALUE)
else
{
/* for overlapped read */
{
return VERR_FILE_IO_ERROR;
}
rc = VINF_SUCCESS;
}
#else /* !RT_OS_WINDOWS */
# ifdef RT_OS_LINUX
/* This seems to be necessary on some Linux hosts, otherwise we hang here forever. */
# endif
# ifdef RT_OS_LINUX
/* RTFILE_O_NON_BLOCK not supported? */
if (rc == VERR_INVALID_PARAMETER)
# endif
# ifdef RT_OS_DARWIN
if (RT_SUCCESS(rc))
rc = RTFileOpen(&pThis->hDeviceFileR, pThis->pszDevicePath, RTFILE_O_READ | RTFILE_O_OPEN | RTFILE_O_DENY_NONE);
# endif
#endif /* !RT_OS_WINDOWS */
if (RT_FAILURE(rc))
{
switch (rc)
{
case VERR_ACCESS_DENIED:
#if defined(RT_OS_LINUX) || defined(RT_OS_DARWIN) || defined(RT_OS_SOLARIS) || defined(RT_OS_FREEBSD)
"the group settings of the current user"),
#else
"of that device"),
#endif
default:
N_("Failed to open host device '%s'"),
}
}
/* Set to non blocking I/O */
#if defined(RT_OS_LINUX) || defined(RT_OS_DARWIN) || defined(RT_OS_SOLARIS) || defined(RT_OS_FREEBSD)
# ifdef RT_OS_DARWIN
# endif
#elif defined(RT_OS_WINDOWS)
/* Set the COMMTIMEOUTS to get non blocking I/O */
#endif
/*
*/
if (!pThis->pDrvCharPort)
return PDMDrvHlpVMSetError(pDrvIns, VERR_PDM_MISSING_INTERFACE_ABOVE, RT_SRC_POS, N_("HostSerial#%d has no char port interface above"), pDrvIns->iInstance);
/*
* Create the receive, send and monitor threads plus the related send semaphore.
*/
rc = PDMDrvHlpThreadCreate(pDrvIns, &pThis->pRecvThread, pThis, drvHostSerialRecvThread, drvHostSerialWakeupRecvThread, 0, RTTHREADTYPE_IO, "SerRecv");
if (RT_FAILURE(rc))
return PDMDrvHlpVMSetError(pDrvIns, rc, RT_SRC_POS, N_("HostSerial#%d cannot create receive thread"), pDrvIns->iInstance);
rc = PDMDrvHlpThreadCreate(pDrvIns, &pThis->pSendThread, pThis, drvHostSerialSendThread, drvHostSerialWakeupSendThread, 0, RTTHREADTYPE_IO, "SerSend");
if (RT_FAILURE(rc))
return PDMDrvHlpVMSetError(pDrvIns, rc, RT_SRC_POS, N_("HostSerial#%d cannot create send thread"), pDrvIns->iInstance);
#if defined(RT_OS_LINUX) || defined(RT_OS_DARWIN) || defined(RT_OS_SOLARIS) || defined(RT_OS_FREEBSD)
/* Linux & darwin needs a separate thread which monitors the status lines. */
# ifndef RT_OS_LINUX
# endif
rc = PDMDrvHlpThreadCreate(pDrvIns, &pThis->pMonitorThread, pThis, drvHostSerialMonitorThread, drvHostSerialWakeupMonitorThread, 0, RTTHREADTYPE_IO, "SerMon");
if (RT_FAILURE(rc))
return PDMDrvHlpVMSetError(pDrvIns, rc, RT_SRC_POS, N_("HostSerial#%d cannot create monitor thread"), pDrvIns->iInstance);
#endif
/*
* Register release statistics.
*/
PDMDrvHlpSTAMRegisterF(pDrvIns, &pThis->StatBytesWritten, STAMTYPE_COUNTER, STAMVISIBILITY_USED, STAMUNIT_BYTES, "Nr of bytes written", "/Devices/HostSerial%d/Written", pDrvIns->iInstance);
PDMDrvHlpSTAMRegisterF(pDrvIns, &pThis->StatBytesRead, STAMTYPE_COUNTER, STAMVISIBILITY_USED, STAMUNIT_BYTES, "Nr of bytes read", "/Devices/HostSerial%d/Read", pDrvIns->iInstance);
#ifdef RT_OS_DARWIN /* new Write code, not darwin specific. */
PDMDrvHlpSTAMRegisterF(pDrvIns, &pThis->StatSendOverflows, STAMTYPE_COUNTER, STAMVISIBILITY_USED, STAMUNIT_BYTES, "Nr of bytes overflowed", "/Devices/HostSerial%d/SendOverflow", pDrvIns->iInstance);
#endif
return VINF_SUCCESS;
}
/**
* Char driver registration record.
*/
const PDMDRVREG g_DrvHostSerial =
{
/* u32Version */
/* szName */
"Host Serial",
/* szRCMod */
"",
/* szR0Mod */
"",
/* pszDescription */
"Host serial driver.",
/* fFlags */
/* fClass. */
/* cMaxInstances */
~0U,
/* cbInstance */
sizeof(DRVHOSTSERIAL),
/* pfnConstruct */
/* pfnDestruct */
/* pfnRelocate */
NULL,
/* pfnIOCtl */
NULL,
/* pfnPowerOn */
NULL,
/* pfnReset */
NULL,
/* pfnSuspend */
NULL,
/* pfnResume */
NULL,
/* pfnAttach */
NULL,
/* pfnDetach */
NULL,
/* pfnPowerOff */
NULL,
/* pfnSoftReset */
NULL,
/* u32EndVersion */
};