/*--- Copyright (c) 1998 - 1999 Microsoft Corporation Module Name: poll.c Abstract: This module contains the routines to poll an analog gameport device Environment: Kernel mode @@BEGIN_DDKSPLIT Author: Eliyas Yakub (Mar, 11, 1997) Revision History: Updated by Eliyas on Feb 5 1998 MarcAnd 02-Jul-98 Quick tidy for DDK MarcAnd 04-Oct-98 Re-org @@END_DDKSPLIT --*/ #include "hidgame.h" /***************************************************************************** * * @doc EXTERNAL * * @func NTSTATUS | HidAnalogPoll | * * Polling routine for analog joysticks. * Polls the analog device for position and button information. * The position information in analog devices is conveyed by the * duration of a pulse width. Each axis occupies one bit position. * The read operation is started by writing a value to the joystick * io address. Immediately thereafter we begin examing the values * returned and the elapsed time. * * This sort of device has a few limitations: * * First, button information is not latched by the device, so if a * button press which occurrs in between polls it will be lost. * There is really no way to prevent this short of devoting * the entire cpu to polling. In reality this does not cause a problem. * * Second, since it is necessary to measure the duration of the axis pulse, * the most accurate results would be obtained using the smallest possible * sense loop and no interruptions of this loop. * The typical range of pulse lengths is from around 10 uSecs to 1500 uSecs * but depending on the joystick and gameport, this could extend to at least * 8000 uSecs. Keeping interrupts disabled for this length of time causes * many problems, like modems losing connections to sound break ups. * * Third, because each iteration of the poll loop requires an port read, the * speed of the loop is largely constrained by the speed of the IO bus. * This also means that when there is contention for the IO bus, the loop * will be slowed down. IO contention is usually caused by DMAs (or FDMAs) * which result in a significant slow down. * * Forth, because of the previous two problems, the poll loop may be slowed * down or interrupted at any time so an external time source is needed to * measure the pulse width for each axis. The only cross-platform high * resolution timer is the read with KeQueryPerformanceCounter. * Unfortunately the implementation of this often uses a 1.18MHz 8253 timer * which requires 3 IO accesses to read, compounding the third problem and * even then, the result may need to be reread if the counters were in the * wrong state. Current CPUs have on board counters that can be used to * provide very accurate timing and more recent HAL implementations tend to * use these to implement KeQueryPerformanceCounter so this will be a problem * on less systems as time goes on. In the majority of cases, a poor * KeQueryPerformanceCounter implementation is made irrelevant by testing * for the availability of a CPU time stamp counter on Intel architechtures * and using it directly if it is available. * * The algorithm implemented here is not the most obvious but works as * follows: * * Once started, the axes read a value of one until the completion of their * pulse. The axes are the four lower bits in the byte read from the port. * The state of the axes in each iteration of the poll loop is therefore * represented as a value between 0 and 15. The important time for each * axis is the time at which it changes from 1 to 0. This is done by using * the value representing the state of the axes to index an array into which * time values are stored. For each axis, the duration of its pulse width is * the latest time stored in the array at an index with the bit for that axis * set. However since interrupts can occur at any time, it is not possible * to simultaneously read the port value and record that time in an atomic * operation the in each iteration, the current time is stored in two arrays, * one using the index before the time was recorded and the other using the * index after the time was recorded. * Once all the axes being monitored have become 0, or a timeout value is * reached, the data left in the arrays is analysed to find the best * estimate for the transition time for each axis. If the times before and * after the transition differ by too much, it is judged that an interrupt * must have occured so the last known good axis value is returned unless * that falls outside the range in which it is known that the transition * occured. * * This routine cannot be pageable as HID can make reads at dispatch-level. * * @parm IN PDEVICE_EXTENSION | DeviceExtension | * * Pointer to the device extension. * * @parm IN UCHAR | resisitiveInputMask | * * Mask that describes the axes lines that are to be polled * * @parm IN BOOLEAN | fApproximate | * * Boolean value indicating if it is OK to approximate some * value of the current axis state with the last axis state * if polling was not successful (we took an interrput during polling) * * @parm IN OUT ULONG | Axis[MAX_AXES] | * * The state of the axes. On entry the last axis state is passed * into this routine. If the fApproximate flag is turned on, we can * make use of the last axis state to "guess" the current axis state. * * @parm OUT UCHAR | Button[PORT_BUTTONS]| * * Receives the state of the buttons. 0x0 specifies the button is not * pressed and 0x1 indicates an armed button state. * * @rvalue STATUS_SUCCESS | success * @rvalue STATUS_DEVICE_NOT_CONNECTED | Device Failed to Quiesce * ( not connected ) This is a failure code. * @rvalue STATUS_TIMEOUT | Could not determine exact transition time for * one or more axis. This is a success code. * *****************************************************************************/ /* * Tell a compiler that we "a" won't use any aliasing and "t" want fast code */ #pragma optimize( "at", on ) /* * Disable warning for variable used before set as it is hard for a compiler * to see that TimeNow is always initialized before it is used. */ #pragma warning( disable:4701 ) NTSTATUS INTERNAL HGM_AnalogPoll ( IN PDEVICE_EXTENSION DeviceExtension, IN UCHAR resistiveInputMask, IN BOOLEAN fApproximate, IN OUT ULONG Axis[MAX_AXES], OUT UCHAR Button[PORT_BUTTONS] ) { ULONG BeforeTimes[MAX_AXES*MAX_AXES]; ULONG AfterTimes[MAX_AXES*MAX_AXES]; ULONGLONG CounterFreq; PUCHAR GameContext; NTSTATUS ntStatus = STATUS_SUCCESS; /* * To improve compiler optimization, we cast the ReadAccessor function to * return a ULONG instead of a UCHAR. This means that the result must * always be masked before use but this would be done anyway to remove * the parts of the UCHAR we are not interested in. */ typedef ULONG (*PHIDGAME_READPORT) ( PVOID GameContext ); PHIDGAME_READPORT ReadPort; ULONG portLast, portMask; HGM_DBGPRINT( FILE_POLL | HGM_FENTRY, \ ("HGM_AnalogPoll DeviceExtension=0x%x, resistiveInputMask=0x%x",\ DeviceExtension, resistiveInputMask )); portMask = (ULONG)(resistiveInputMask & 0xf); /* * Initialize Times to recognizable value */ memset( (PVOID)BeforeTimes, 0, sizeof( BeforeTimes ) ); memset( (PVOID)AfterTimes, 0, sizeof( AfterTimes ) ); /* * Find where our port and data area are, and related parameters */ GameContext = DeviceExtension->GameContext; ReadPort = (PHIDGAME_READPORT)(*DeviceExtension->ReadAccessor); /* * get the buttons (not forgetting that the top 3 bytes are garbage) */ portLast = ReadPort(GameContext); Button[0] = (UCHAR)(( portLast & 0x10 ) == 0x0); Button[1] = (UCHAR)(( portLast & 0x20 ) == 0x0); Button[2] = (UCHAR)(( portLast & 0x40 ) == 0x0); Button[3] = (UCHAR)(( portLast & 0x80 ) == 0x0); portLast = portMask; /* * Start the pots * (any debug output from here until the completion of the * while( portLast ) loop will destroy the axis data) */ (*DeviceExtension->WriteAccessor)(GameContext, JOY_START_TIMERS); /* * Keep reading until all the pots we care about are zero or we time out */ { ULONG TimeNow; ULONG TimeStart; ULONG TimeOut = DeviceExtension->ScaledTimeout/Global.CounterScale; ULONG portVal = portMask; TimeStart = Global.ReadCounter(NULL).LowPart; while( portLast ) { TimeNow = Global.ReadCounter(NULL).LowPart - TimeStart; AfterTimes[portLast] = TimeNow; portLast = portVal; portVal = ReadPort(GameContext) & portMask; BeforeTimes[portVal] = TimeNow; if( TimeNow >= TimeOut ) break; } if( portLast && ( TimeNow >= TimeOut ) ) { HGM_DBGPRINT( FILE_POLL | HGM_BABBLE, \ ("HGM_AnalogPoll: TimeNow: 0x%08x TimeOut: 0x%08x", TimeNow, TimeOut ) ); } } { LONG axisIdx; for( axisIdx = 3; axisIdx>=0; axisIdx-- ) { ULONG axisMask; axisMask = 1 << axisIdx; if( axisMask & portMask ) { if( axisMask & portLast ) { /* * Whether or not a hit was taken, this axis did not * quiesce. So update the axis time so that next poll * the last value will be a timeout in case a hit is * taken over both the transition and the timeout. */ Axis[axisIdx] = AXIS_TIMEOUT; ntStatus = STATUS_DEVICE_NOT_CONNECTED; HGM_DBGPRINT( FILE_POLL | HGM_WARN, \ ("HGM_AnalogPoll: axis %x still set at timeout", axisMask ) ); } else { ULONG timeIdx; ULONG beforeThresholdTime; ULONG afterThresholdTime; ULONG delta; afterThresholdTime = beforeThresholdTime = 0; for( timeIdx = axisMask; timeIdx<= portMask; timeIdx=(timeIdx+1) | axisMask ) { if( BeforeTimes[timeIdx] > beforeThresholdTime ) { beforeThresholdTime = BeforeTimes[timeIdx]; afterThresholdTime = AfterTimes[timeIdx]; } } /* * Convert the CPU specific timing values into 'wall clock' * values so that they can be compared with the previous * poll values and so that the range will be dependent on * the gamecard/joystick characteristics, not the CPU * and counter implementation. * Use a ULONGLONG temp to avoid overflow. */ { ULONGLONG u64Temp; u64Temp = beforeThresholdTime * Global.CounterScale; beforeThresholdTime = (ULONG)(u64Temp >> SCALE_SHIFT); u64Temp = afterThresholdTime * Global.CounterScale; afterThresholdTime = (ULONG)(u64Temp >> SCALE_SHIFT); } delta = afterThresholdTime - beforeThresholdTime; if( delta > DeviceExtension->ScaledThreshold ) { /* * We took an unacceptable hit so only change the value * if we know the last value is no longer correct * Since the real time is somewhere between the before and * after, take the value closer to the last value. */ if( fApproximate ) { /* * Be careful not to turn a failure into a success */ if( NT_SUCCESS(ntStatus) ) { ntStatus = STATUS_TIMEOUT; } } else { ntStatus = STATUS_DEVICE_NOT_CONNECTED; } if( Axis[axisIdx] >= AXIS_FULL_SCALE ) { /* * The previous poll was a timeout */ if( afterThresholdTime < AXIS_FULL_SCALE ) { /* * This poll is not a timeout so split the * difference since there is nothing else * to use for an estimate. * Since these values are scaled, it would * be perfectly legitimate for their sum to * be greater than 32 bits. */ Axis[axisIdx] = (beforeThresholdTime>>1) + (afterThresholdTime>>1); HGM_DBGPRINT( FILE_POLL | HGM_BABBLE2, \ ("HGM_AnalogPoll:Axis=%d, using glitch average %04x",\ axisIdx, Axis[axisIdx] ) ) ; } else { /* * Since the previous poll was a timeout and * there is no evidence that this is not, call * this a timeout. */ ntStatus = STATUS_DEVICE_NOT_CONNECTED; HGM_DBGPRINT( FILE_POLL | HGM_BABBLE2, \ ("HGM_AnalogPoll:Axis=%d, repeating timeout on glitch",\ axisIdx ) ) ; } } else if( beforeThresholdTime > Axis[axisIdx] ) { Axis[axisIdx] = beforeThresholdTime; HGM_DBGPRINT( FILE_POLL | HGM_BABBLE2, \ ("HGM_AnalogPoll:Axis=%d, using smaller glitch limit %04x",\ axisIdx, Axis[axisIdx] ) ) ; } else if( afterThresholdTime < Axis[axisIdx] ) { Axis[axisIdx] = afterThresholdTime; HGM_DBGPRINT( FILE_POLL | HGM_BABBLE2, \ ("HGM_AnalogPoll:Axis=%d, using larger glitch limit %04x",\ axisIdx, Axis[axisIdx] ) ) ; } else { HGM_DBGPRINT( FILE_POLL | HGM_BABBLE2, \ ("HGM_AnalogPoll:Axis=%d, repeating previous on glitch %04x",\ axisIdx, Axis[axisIdx] ) ) ; } } else { if( (delta <<= 1) < DeviceExtension->ScaledThreshold ) { HGM_DBGPRINT( FILE_POLL | HGM_BABBLE2, \ ("HGM_AnalogPoll: Updating ScaledThreshold from %d to %d",\ DeviceExtension->ScaledThreshold, delta ) ) ; /* * Fastest change yet, update */ DeviceExtension->ScaledThreshold = delta; } /* * It is possible that afterThresholdTime is greater * than the timeout limit but since the purpose of * the timeout is to prevent excessive sampling of * the gameport, the success or failure of an * uninterrupted poll around this limit is not * important. * Since these values are scaled, it would be * perfectly legitimate for their sum to be greater * than 32 bits and the 1 bit of lost resolution is * utterly negligable. */ Axis[axisIdx] = (beforeThresholdTime>>1) + (afterThresholdTime>>1); } } } } } HGM_DBGPRINT( FILE_POLL | HGM_BABBLE2, \ ("HGM_AnalogPoll:X=%d, Y=%d, R=%d, Z=%d Buttons=%d,%d,%d,%d",\ Axis[0], Axis[1], Axis[2], Axis[3],\ Button[0],Button[1],Button[2],Button[3] ) ) ; HGM_EXITPROC(FILE_POLL|HGM_FEXIT, "HGM_AnalogPoll", ntStatus); return ntStatus; } /* HGM_AnalogPoll */ #pragma warning( default:4701 ) #pragma optimize( "", on ) /***************************************************************************** * * @doc EXTERNAL * * @func NTSTATUS | HGM_UpdateLatestPollData | * * Do whatever polling is required and possible to update the * LastGoodAxis and LastGoodButton arrays in the DeviceExtension. * Handles synchronization and non-fatal errors. * This routine cannot be pageable as HID can make reads at * dispatch-level. * * @parm IN OUT PDEVICE_EXTENSION | DeviceExtension | * * Pointer to the device extension containing the data to be updated * and the functions and to use. * * @rvalue STATUS_SUCCESS | success * @rvalue STATUS_DEVICE_CONFIGURATION_ERROR | Invalid configuration specified * *****************************************************************************/ #define APPROXIMATE_FAILS TRUE NTSTATUS HGM_UpdateLatestPollData ( IN OUT PDEVICE_EXTENSION DeviceExtension ) { NTSTATUS ntStatus; KIRQL oldIrql; LONG axisIdx; /* * Acquire the global spinlock * Read / Writes are made at dispatch level. */ KeAcquireSpinLock(&Global.SpinLock, &oldIrql ); /* * First gain exclusive access to the hardware */ ntStatus = (*DeviceExtension->AcquirePort)( DeviceExtension->PortContext ); if( NT_SUCCESS(ntStatus) ) { /* * If it's available, let the hardware do the work */ if( DeviceExtension->ReadAccessorDigital ) { ntStatus = (*DeviceExtension->ReadAccessorDigital)(DeviceExtension->GameContext, DeviceExtension->resistiveInputMask, APPROXIMATE_FAILS, &DeviceExtension->LastGoodAxis[0], &DeviceExtension->LastGoodButton[0]); } else { ntStatus = HGM_AnalogPoll(DeviceExtension, DeviceExtension->resistiveInputMask, APPROXIMATE_FAILS, &DeviceExtension->LastGoodAxis[0], &DeviceExtension->LastGoodButton[0]); } /* * Either way, release the hardware ASAP */ (*DeviceExtension->ReleasePort)( DeviceExtension->PortContext ); } /* * Release the global spinlock and return to previous IRQL */ KeReleaseSpinLock(&Global.SpinLock, oldIrql); if( ( ntStatus == STATUS_DEVICE_BUSY ) && APPROXIMATE_FAILS ) { /* * Clashed trying to access the gameport. So work with the same * data as last time unless all failures must be reported or the * last data was a failure for these axes. */ for( axisIdx=3; axisIdx>=0; axisIdx-- ) { if( ( ( 1 << axisIdx ) & DeviceExtension->resistiveInputMask ) &&( DeviceExtension->LastGoodAxis[axisIdx] >= DeviceExtension->ScaledTimeout ) ) { break; } } if( axisIdx<0 ) { ntStatus = STATUS_TIMEOUT; } } if( !NT_SUCCESS( ntStatus ) ) { HGM_DBGPRINT(FILE_IOCTL | HGM_WARN,\ ("HGM_UpdateLatestPollData Failed 0x%x", ntStatus)); } return( ntStatus ); } /* HGM_UpdateLatestPollData */