/* * FreeRTOS Kernel V10.3.1 * Copyright (C) 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in * the Software without restriction, including without limitation the rights to * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of * the Software, and to permit persons to whom the Software is furnished to do so, * subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * http://www.FreeRTOS.org * http://aws.amazon.com/freertos * */ /* Standard includes. */ #include /* Scheduler includes. */ #include "FreeRTOS.h" #include "task.h" #ifdef __GNUC__ #include "mmsystem.h" #else #pragma comment(lib, "winmm.lib") #endif #define portMAX_INTERRUPTS ( ( uint32_t ) sizeof( uint32_t ) * 8UL ) /* The number of bits in an uint32_t. */ #define portNO_CRITICAL_NESTING ( ( uint32_t ) 0 ) /* The priorities at which the various components of the simulation execute. */ #define portDELETE_SELF_THREAD_PRIORITY THREAD_PRIORITY_TIME_CRITICAL /* Must be highest. */ #define portSIMULATED_INTERRUPTS_THREAD_PRIORITY THREAD_PRIORITY_TIME_CRITICAL #define portSIMULATED_TIMER_THREAD_PRIORITY THREAD_PRIORITY_HIGHEST #define portTASK_THREAD_PRIORITY THREAD_PRIORITY_ABOVE_NORMAL /* * Created as a high priority thread, this function uses a timer to simulate * a tick interrupt being generated on an embedded target. In this Windows * environment the timer does not achieve anything approaching real time * performance though. */ static DWORD WINAPI prvSimulatedPeripheralTimer( LPVOID lpParameter ); /* * Process all the simulated interrupts - each represented by a bit in * ulPendingInterrupts variable. */ static void prvProcessSimulatedInterrupts( void ); /* * Interrupt handlers used by the kernel itself. These are executed from the * simulated interrupt handler thread. */ static uint32_t prvProcessYieldInterrupt( void ); static uint32_t prvProcessTickInterrupt( void ); /* * Exiting a critical section will cause the calling task to block on yield * event to wait for an interrupt to process if an interrupt was pended while * inside the critical section. This variable protects against a recursive * attempt to obtain pvInterruptEventMutex if a critical section is used inside * an interrupt handler itself. */ volatile BaseType_t xInsideInterrupt = pdFALSE; /* * Called when the process exits to let Windows know the high timer resolution * is no longer required. */ static BOOL WINAPI prvEndProcess( DWORD dwCtrlType ); /*-----------------------------------------------------------*/ /* The WIN32 simulator runs each task in a thread. The context switching is * managed by the threads, so the task stack does not have to be managed directly, * although the task stack is still used to hold an xThreadState structure this is * the only thing it will ever hold. The structure indirectly maps the task handle * to a thread handle. */ typedef struct { /* Handle of the thread that executes the task. */ void * pvThread; /* Event used to make sure the thread does not execute past a yield point * between the call to SuspendThread() to suspend the thread and the * asynchronous SuspendThread() operation actually being performed. */ void * pvYieldEvent; } ThreadState_t; /* Simulated interrupts waiting to be processed. This is a bit mask where each * bit represents one interrupt, so a maximum of 32 interrupts can be simulated. */ static volatile uint32_t ulPendingInterrupts = 0UL; /* An event used to inform the simulated interrupt processing thread (a high * priority thread that simulated interrupt processing) that an interrupt is * pending. */ static void * pvInterruptEvent = NULL; /* Mutex used to protect all the simulated interrupt variables that are accessed * by multiple threads. */ static void * pvInterruptEventMutex = NULL; /* The critical nesting count for the currently executing task. This is * initialised to a non-zero value so interrupts do not become enabled during * the initialisation phase. As each task has its own critical nesting value * ulCriticalNesting will get set to zero when the first task runs. This * initialisation is probably not critical in this simulated environment as the * simulated interrupt handlers do not get created until the FreeRTOS scheduler is * started anyway. */ static volatile uint32_t ulCriticalNesting = 9999UL; /* Handlers for all the simulated software interrupts. The first two positions * are used for the Yield and Tick interrupts so are handled slightly differently, * all the other interrupts can be user defined. */ static uint32_t (* ulIsrHandler[ portMAX_INTERRUPTS ])( void ) = { 0 }; /* Pointer to the TCB of the currently executing task. */ extern void * volatile pxCurrentTCB; /* Used to ensure nothing is processed during the startup sequence. */ static BaseType_t xPortRunning = pdFALSE; /*-----------------------------------------------------------*/ static DWORD WINAPI prvSimulatedPeripheralTimer( LPVOID lpParameter ) { TickType_t xMinimumWindowsBlockTime; TIMECAPS xTimeCaps; /* Set the timer resolution to the maximum possible. */ if( timeGetDevCaps( &xTimeCaps, sizeof( xTimeCaps ) ) == MMSYSERR_NOERROR ) { xMinimumWindowsBlockTime = ( TickType_t ) xTimeCaps.wPeriodMin; timeBeginPeriod( xTimeCaps.wPeriodMin ); /* Register an exit handler so the timeBeginPeriod() function can be * matched with a timeEndPeriod() when the application exits. */ SetConsoleCtrlHandler( prvEndProcess, TRUE ); } else { xMinimumWindowsBlockTime = ( TickType_t ) 20; } /* Just to prevent compiler warnings. */ ( void ) lpParameter; for( ; ; ) { /* Wait until the timer expires and we can access the simulated interrupt * variables. *NOTE* this is not a 'real time' way of generating tick * events as the next wake time should be relative to the previous wake * time, not the time that Sleep() is called. It is done this way to * prevent overruns in this very non real time simulated/emulated * environment. */ if( portTICK_PERIOD_MS < xMinimumWindowsBlockTime ) { Sleep( xMinimumWindowsBlockTime ); } else { Sleep( portTICK_PERIOD_MS ); } configASSERT( xPortRunning ); /* Can't proceed if in a critical section as pvInterruptEventMutex won't * be available. */ WaitForSingleObject( pvInterruptEventMutex, INFINITE ); /* The timer has expired, generate the simulated tick event. */ ulPendingInterrupts |= ( 1 << portINTERRUPT_TICK ); /* The interrupt is now pending - notify the simulated interrupt * handler thread. Must be outside of a critical section to get here so * the handler thread can execute immediately pvInterruptEventMutex is * released. */ configASSERT( ulCriticalNesting == 0UL ); SetEvent( pvInterruptEvent ); /* Give back the mutex so the simulated interrupt handler unblocks * and can access the interrupt handler variables. */ ReleaseMutex( pvInterruptEventMutex ); } #ifdef __GNUC__ /* Should never reach here - MingW complains if you leave this line out, * MSVC complains if you put it in. */ return 0; #endif } /*-----------------------------------------------------------*/ static BOOL WINAPI prvEndProcess( DWORD dwCtrlType ) { TIMECAPS xTimeCaps; ( void ) dwCtrlType; if( timeGetDevCaps( &xTimeCaps, sizeof( xTimeCaps ) ) == MMSYSERR_NOERROR ) { /* Match the call to timeBeginPeriod( xTimeCaps.wPeriodMin ) made when * the process started with a timeEndPeriod() as the process exits. */ timeEndPeriod( xTimeCaps.wPeriodMin ); } return pdFALSE; } /*-----------------------------------------------------------*/ StackType_t * pxPortInitialiseStack( StackType_t * pxTopOfStack, TaskFunction_t pxCode, void * pvParameters ) { ThreadState_t * pxThreadState = NULL; int8_t * pcTopOfStack = ( int8_t * ) pxTopOfStack; const SIZE_T xStackSize = 1024; /* Set the size to a small number which will get rounded up to the minimum possible. */ /* In this simulated case a stack is not initialised, but instead a thread * is created that will execute the task being created. The thread handles * the context switching itself. The ThreadState_t object is placed onto * the stack that was created for the task - so the stack buffer is still * used, just not in the conventional way. It will not be used for anything * other than holding this structure. */ pxThreadState = ( ThreadState_t * ) ( pcTopOfStack - sizeof( ThreadState_t ) ); /* Create the event used to prevent the thread from executing past its yield * point if the SuspendThread() call that suspends the thread does not take * effect immediately (it is an asynchronous call). */ pxThreadState->pvYieldEvent = CreateEvent( NULL, /* Default security attributes. */ FALSE, /* Auto reset. */ FALSE, /* Start not signalled. */ NULL ); /* No name. */ /* Create the thread itself. */ pxThreadState->pvThread = CreateThread( NULL, xStackSize, ( LPTHREAD_START_ROUTINE ) pxCode, pvParameters, CREATE_SUSPENDED | STACK_SIZE_PARAM_IS_A_RESERVATION, NULL ); configASSERT( pxThreadState->pvThread ); /* See comment where TerminateThread() is called. */ SetThreadAffinityMask( pxThreadState->pvThread, 0x01 ); SetThreadPriorityBoost( pxThreadState->pvThread, TRUE ); SetThreadPriority( pxThreadState->pvThread, portTASK_THREAD_PRIORITY ); return ( StackType_t * ) pxThreadState; } /*-----------------------------------------------------------*/ BaseType_t xPortStartScheduler( void ) { void * pvHandle = NULL; int32_t lSuccess; ThreadState_t * pxThreadState = NULL; SYSTEM_INFO xSystemInfo; /* This port runs windows threads with extremely high priority. All the * threads execute on the same core - to prevent locking up the host only start * if the host has multiple cores. */ GetSystemInfo( &xSystemInfo ); if( xSystemInfo.dwNumberOfProcessors <= 1 ) { printf( "This version of the FreeRTOS Windows port can only be used on multi-core hosts.\r\n" ); lSuccess = pdFAIL; } else { lSuccess = pdPASS; /* The highest priority class is used to [try to] prevent other Windows * activity interfering with FreeRTOS timing too much. */ if( SetPriorityClass( GetCurrentProcess(), REALTIME_PRIORITY_CLASS ) == 0 ) { printf( "SetPriorityClass() failed\r\n" ); } /* Install the interrupt handlers used by the scheduler itself. */ vPortSetInterruptHandler( portINTERRUPT_YIELD, prvProcessYieldInterrupt ); vPortSetInterruptHandler( portINTERRUPT_TICK, prvProcessTickInterrupt ); /* Create the events and mutexes that are used to synchronise all the * threads. */ pvInterruptEventMutex = CreateMutex( NULL, FALSE, NULL ); pvInterruptEvent = CreateEvent( NULL, FALSE, FALSE, NULL ); if( ( pvInterruptEventMutex == NULL ) || ( pvInterruptEvent == NULL ) ) { lSuccess = pdFAIL; } /* Set the priority of this thread such that it is above the priority of * the threads that run tasks. This higher priority is required to ensure * simulated interrupts take priority over tasks. */ pvHandle = GetCurrentThread(); if( pvHandle == NULL ) { lSuccess = pdFAIL; } } if( lSuccess == pdPASS ) { if( SetThreadPriority( pvHandle, portSIMULATED_INTERRUPTS_THREAD_PRIORITY ) == 0 ) { lSuccess = pdFAIL; } SetThreadPriorityBoost( pvHandle, TRUE ); SetThreadAffinityMask( pvHandle, 0x01 ); } if( lSuccess == pdPASS ) { /* Start the thread that simulates the timer peripheral to generate * tick interrupts. The priority is set below that of the simulated * interrupt handler so the interrupt event mutex is used for the * handshake / overrun protection. */ pvHandle = CreateThread( NULL, 0, prvSimulatedPeripheralTimer, NULL, CREATE_SUSPENDED, NULL ); if( pvHandle != NULL ) { SetThreadPriority( pvHandle, portSIMULATED_TIMER_THREAD_PRIORITY ); SetThreadPriorityBoost( pvHandle, TRUE ); SetThreadAffinityMask( pvHandle, 0x01 ); ResumeThread( pvHandle ); } /* Start the highest priority task by obtaining its associated thread * state structure, in which is stored the thread handle. */ pxThreadState = ( ThreadState_t * ) *( ( size_t * ) pxCurrentTCB ); ulCriticalNesting = portNO_CRITICAL_NESTING; /* Start the first task. */ ResumeThread( pxThreadState->pvThread ); /* Handle all simulated interrupts - including yield requests and * simulated ticks. */ prvProcessSimulatedInterrupts(); } /* Would not expect to return from prvProcessSimulatedInterrupts(), so should * not get here. */ return 0; } /*-----------------------------------------------------------*/ static uint32_t prvProcessYieldInterrupt( void ) { /* Always return true as this is a yield. */ return pdTRUE; } /*-----------------------------------------------------------*/ static uint32_t prvProcessTickInterrupt( void ) { uint32_t ulSwitchRequired; /* Process the tick itself. */ configASSERT( xPortRunning ); ulSwitchRequired = ( uint32_t ) xTaskIncrementTick(); return ulSwitchRequired; } /*-----------------------------------------------------------*/ static void prvProcessSimulatedInterrupts( void ) { uint32_t ulSwitchRequired, i; ThreadState_t * pxThreadState; void * pvObjectList[ 2 ]; CONTEXT xContext; /* Going to block on the mutex that ensured exclusive access to the simulated * interrupt objects, and the event that signals that a simulated interrupt * should be processed. */ pvObjectList[ 0 ] = pvInterruptEventMutex; pvObjectList[ 1 ] = pvInterruptEvent; /* Create a pending tick to ensure the first task is started as soon as * this thread pends. */ ulPendingInterrupts |= ( 1 << portINTERRUPT_TICK ); SetEvent( pvInterruptEvent ); xPortRunning = pdTRUE; for( ; ; ) { xInsideInterrupt = pdFALSE; WaitForMultipleObjects( sizeof( pvObjectList ) / sizeof( void * ), pvObjectList, TRUE, INFINITE ); /* Cannot be in a critical section to get here. Tasks that exit a * critical section will block on a yield mutex to wait for an interrupt to * process if an interrupt was set pending while the task was inside the * critical section. xInsideInterrupt prevents interrupts that contain * critical sections from doing the same. */ xInsideInterrupt = pdTRUE; /* Used to indicate whether the simulated interrupt processing has * necessitated a context switch to another task/thread. */ ulSwitchRequired = pdFALSE; /* For each interrupt we are interested in processing, each of which is * represented by a bit in the 32bit ulPendingInterrupts variable. */ for( i = 0; i < portMAX_INTERRUPTS; i++ ) { /* Is the simulated interrupt pending? */ if( ( ulPendingInterrupts & ( 1UL << i ) ) != 0 ) { /* Is a handler installed? */ if( ulIsrHandler[ i ] != NULL ) { /* Run the actual handler. Handlers return pdTRUE if they * necessitate a context switch. */ if( ulIsrHandler[ i ]() != pdFALSE ) { /* A bit mask is used purely to help debugging. */ ulSwitchRequired |= ( 1 << i ); } } /* Clear the interrupt pending bit. */ ulPendingInterrupts &= ~( 1UL << i ); } } if( ulSwitchRequired != pdFALSE ) { void * pvOldCurrentTCB; pvOldCurrentTCB = pxCurrentTCB; /* Select the next task to run. */ vTaskSwitchContext(); /* If the task selected to enter the running state is not the task * that is already in the running state. */ if( pvOldCurrentTCB != pxCurrentTCB ) { /* Suspend the old thread. In the cases where the (simulated) * interrupt is asynchronous (tick event swapping a task out rather * than a task blocking or yielding) it doesn't matter if the * 'suspend' operation doesn't take effect immediately - if it * doesn't it would just be like the interrupt occurring slightly * later. In cases where the yield was caused by a task blocking * or yielding then the task will block on a yield event after the * yield operation in case the 'suspend' operation doesn't take * effect immediately. */ pxThreadState = ( ThreadState_t * ) *( ( size_t * ) pvOldCurrentTCB ); SuspendThread( pxThreadState->pvThread ); /* Ensure the thread is actually suspended by performing a * synchronous operation that can only complete when the thread is * actually suspended. The below code asks for dummy register * data. Experimentation shows that these two lines don't appear * to do anything now, but according to * https://devblogs.microsoft.com/oldnewthing/20150205-00/?p=44743 * they do - so as they do not harm (slight run-time hit). */ xContext.ContextFlags = CONTEXT_INTEGER; ( void ) GetThreadContext( pxThreadState->pvThread, &xContext ); /* Obtain the state of the task now selected to enter the * Running state. */ pxThreadState = ( ThreadState_t * ) ( *( size_t * ) pxCurrentTCB ); /* pxThreadState->pvThread can be NULL if the task deleted * itself - but a deleted task should never be resumed here. */ configASSERT( pxThreadState->pvThread != NULL ); ResumeThread( pxThreadState->pvThread ); } } /* If the thread that is about to be resumed stopped running * because it yielded then it will wait on an event when it resumed * (to ensure it does not continue running after the call to * SuspendThread() above as SuspendThread() is asynchronous). * Signal the event to ensure the thread can proceed now it is * valid for it to do so. Signaling the event is benign in the case that * the task was switched out asynchronously by an interrupt as the event * is reset before the task blocks on it. */ pxThreadState = ( ThreadState_t * ) ( *( size_t * ) pxCurrentTCB ); SetEvent( pxThreadState->pvYieldEvent ); ReleaseMutex( pvInterruptEventMutex ); } } /*-----------------------------------------------------------*/ void vPortDeleteThread( void * pvTaskToDelete ) { ThreadState_t * pxThreadState; uint32_t ulErrorCode; /* Remove compiler warnings if configASSERT() is not defined. */ ( void ) ulErrorCode; /* Find the handle of the thread being deleted. */ pxThreadState = ( ThreadState_t * ) ( *( size_t * ) pvTaskToDelete ); /* Check that the thread is still valid, it might have been closed by * vPortCloseRunningThread() - which will be the case if the task associated * with the thread originally deleted itself rather than being deleted by a * different task. */ if( pxThreadState->pvThread != NULL ) { WaitForSingleObject( pvInterruptEventMutex, INFINITE ); /* !!! This is not a nice way to terminate a thread, and will eventually * result in resources being depleted if tasks frequently delete other * tasks (rather than deleting themselves) as the task stacks will not be * freed. */ ulErrorCode = TerminateThread( pxThreadState->pvThread, 0 ); configASSERT( ulErrorCode ); ulErrorCode = CloseHandle( pxThreadState->pvThread ); configASSERT( ulErrorCode ); ReleaseMutex( pvInterruptEventMutex ); } } /*-----------------------------------------------------------*/ void vPortCloseRunningThread( void * pvTaskToDelete, volatile BaseType_t * pxPendYield ) { ThreadState_t * pxThreadState; void * pvThread; uint32_t ulErrorCode; /* Remove compiler warnings if configASSERT() is not defined. */ ( void ) ulErrorCode; /* Find the handle of the thread being deleted. */ pxThreadState = ( ThreadState_t * ) ( *( size_t * ) pvTaskToDelete ); pvThread = pxThreadState->pvThread; /* Raise the Windows priority of the thread to ensure the FreeRTOS scheduler * does not run and swap it out before it is closed. If that were to happen * the thread would never run again and effectively be a thread handle and * memory leak. */ SetThreadPriority( pvThread, portDELETE_SELF_THREAD_PRIORITY ); /* This function will not return, therefore a yield is set as pending to * ensure a context switch occurs away from this thread on the next tick. */ *pxPendYield = pdTRUE; /* Mark the thread associated with this task as invalid so * vPortDeleteThread() does not try to terminate it. */ pxThreadState->pvThread = NULL; /* Close the thread. */ ulErrorCode = CloseHandle( pvThread ); configASSERT( ulErrorCode ); /* This is called from a critical section, which must be exited before the * thread stops. */ taskEXIT_CRITICAL(); CloseHandle( pxThreadState->pvYieldEvent ); ExitThread( 0 ); } /*-----------------------------------------------------------*/ void vPortEndScheduler( void ) { exit( 0 ); } /*-----------------------------------------------------------*/ void vPortGenerateSimulatedInterrupt( uint32_t ulInterruptNumber ) { ThreadState_t * pxThreadState = ( ThreadState_t * ) *( ( size_t * ) pxCurrentTCB ); configASSERT( xPortRunning ); if( ( ulInterruptNumber < portMAX_INTERRUPTS ) && ( pvInterruptEventMutex != NULL ) ) { WaitForSingleObject( pvInterruptEventMutex, INFINITE ); ulPendingInterrupts |= ( 1 << ulInterruptNumber ); /* The simulated interrupt is now held pending, but don't actually * process it yet if this call is within a critical section. It is * possible for this to be in a critical section as calls to wait for * mutexes are accumulative. If in a critical section then the event * will get set when the critical section nesting count is wound back * down to zero. */ if( ulCriticalNesting == portNO_CRITICAL_NESTING ) { SetEvent( pvInterruptEvent ); /* Going to wait for an event - make sure the event is not already * signaled. */ ResetEvent( pxThreadState->pvYieldEvent ); } ReleaseMutex( pvInterruptEventMutex ); if( ulCriticalNesting == portNO_CRITICAL_NESTING ) { /* An interrupt was pended so ensure to block to allow it to * execute. In most cases the (simulated) interrupt will have * executed before the next line is reached - so this is just to make * sure. */ WaitForSingleObject( pxThreadState->pvYieldEvent, INFINITE ); } } } /*-----------------------------------------------------------*/ void vPortSetInterruptHandler( uint32_t ulInterruptNumber, uint32_t ( * pvHandler )( void ) ) { if( ulInterruptNumber < portMAX_INTERRUPTS ) { if( pvInterruptEventMutex != NULL ) { WaitForSingleObject( pvInterruptEventMutex, INFINITE ); ulIsrHandler[ ulInterruptNumber ] = pvHandler; ReleaseMutex( pvInterruptEventMutex ); } else { ulIsrHandler[ ulInterruptNumber ] = pvHandler; } } } /*-----------------------------------------------------------*/ void vPortEnterCritical( void ) { if( xPortRunning == pdTRUE ) { /* The interrupt event mutex is held for the entire critical section, * effectively disabling (simulated) interrupts. */ WaitForSingleObject( pvInterruptEventMutex, INFINITE ); } ulCriticalNesting++; } /*-----------------------------------------------------------*/ void vPortExitCritical( void ) { int32_t lMutexNeedsReleasing; /* The interrupt event mutex should already be held by this thread as it was * obtained on entry to the critical section. */ lMutexNeedsReleasing = pdTRUE; if( ulCriticalNesting > portNO_CRITICAL_NESTING ) { ulCriticalNesting--; /* Don't need to wait for any pending interrupts to execute if the * critical section was exited from inside an interrupt. */ if( ( ulCriticalNesting == portNO_CRITICAL_NESTING ) && ( xInsideInterrupt == pdFALSE ) ) { /* Were any interrupts set to pending while interrupts were * (simulated) disabled? */ if( ulPendingInterrupts != 0UL ) { ThreadState_t * pxThreadState = ( ThreadState_t * ) *( ( size_t * ) pxCurrentTCB ); configASSERT( xPortRunning ); /* The interrupt won't actually executed until * pvInterruptEventMutex is released as it waits on both * pvInterruptEventMutex and pvInterruptEvent. * pvInterruptEvent is only set when the simulated * interrupt is pended if the interrupt is pended * from outside a critical section - hence it is set * here. */ SetEvent( pvInterruptEvent ); /* The calling task is going to wait for an event to ensure the * interrupt that is pending executes immediately after the * critical section is exited - so make sure the event is not * already signaled. */ ResetEvent( pxThreadState->pvYieldEvent ); /* Mutex will be released now so the (simulated) interrupt can * execute, so does not require releasing on function exit. */ lMutexNeedsReleasing = pdFALSE; ReleaseMutex( pvInterruptEventMutex ); WaitForSingleObject( pxThreadState->pvYieldEvent, INFINITE ); } } } if( pvInterruptEventMutex != NULL ) { if( lMutexNeedsReleasing == pdTRUE ) { configASSERT( xPortRunning ); ReleaseMutex( pvInterruptEventMutex ); } } } /*-----------------------------------------------------------*/