Sharing Hardware Registers: When to replace multiple writers with a shared resource driver.


Posted by Jeremy Cooke 


 


When designing a complex system it is occasionally necessary to coordinate interaction with some sort of shared resource.  Traditionally this is done with synchronization objects provided by the operating system such as critical sections or mutexes.  This technique is fine for a simple resource and when relatively few writers need access to it, but for complicated resources accessed by multiple treads, processes, drivers, and/or applications, a new paradigm may be more appropriate. A good example of a suggested shared resource driver is a GPIO driver. Each driver may share a GPIO register and perform read-modify-writes, which poses a problem if each driver is not synchronized.


 


Rather than accessing the resource directly, it should be abstracted away into its own driver.  All parties interested in accessing the resource can do so by merely opening its driver and utilizing its exposed interface.  Internally, the driver will synchronize access via standard operating system constructs and deal with any error conditions that may arise.  Callers have much less to worry about since all necessary synchronization and bookkeeping has been offloaded to the driver itself.


 


By using this technique the programmer is often able to eliminate critical section constructs which would otherwise be littered throughout the code base.  Every access of the shared resource is now done through a user designed API which both abstracts access and increases program readability across the board.


 


The shared resource driver should also preprocess and error check input data as well as gracefully handle errors with respect to the shared resource it is guarding.  Since error code is now centralized, all developers can be assured that accesses to the resource will be protected.  This is a major plus in joint development scenarios!


 


Case Study:


 


The embedded system has a multi-channel analog to digital converter with devices attached to each channel.  Multiple, disparate drivers are all interested in accessing their respective devices.  The hardware allows software initiated ‘all channel’ conversions with the result vector provided in a common memory array.  If multiple threads request a conversion there is a potential that one may corrupt another if they are not properly synchronized.


 


In this particular system there are analog quantities of interest to the battery driver, the USB driver, and the system’s backlight driver.  The battery driver is concerned with the current voltage and temperature of the battery as well as charging voltage and current.  The USB driver monitors the voltage of the USB VBUS line and the backlight driver monitors ambient light with a photodiode.  Since these functions are handled in their own drivers all access to the shared resource must be controlled in a more advanced manner. Figure 1 below depicts each driver that wants access to the hardware ADC.


 


The analog to digital converter will be abstracted through the use of a dedicated driver.  The driver allows multiple writers to initiate ‘all channel’ conversions and results are returned in a user specified memory array (see Figure 2 below).


 


The driver exposes an interface that includes a function to both perform the A/D conversion and return the results.  Functionality is prototyped as follows:


 


enum ADChannels


{


    AD_CHANNEL_BAT_VOLTAGE = 0,


    AD_CHANNEL_BAT_CHG_VOLTAGE,


    AD_CHANNEL_BAT_CHG_CURRENT,


    AD_CHANNEL_BAT_TEMP,


    AD_CHANNEL_USB_VBUS,


    AD_CHANNEL_PHOTODIODE,


    TOTAL_AD_CHANNELS


};


 


BOOL PerformADConversion(USHORT *pOutBuffer, DWORD OutSize);


 


Here pOutBuffer points to a user supplied array in which results are returned and who’s size must equal TOTAL_AD_CHANNELS.  Care must be taken when copying results to any user data area by using functions such as CeSafeCopyMemory which can handle cases when memory is invalid.


 


The PerformADConversion function can insantiate one critical section for synchronization, as seen in Figure 3:


 


Illustrations


 


In this design all hardware access is protected in a centralized area of code by one critical section.  Performance is increased through the use of a light weight critical section instead of a slower synchronization object (such as a mutex).  The developer can now further fine tune for performance with all gains delivered to each calling driver simultaneously.


 


While there are many positive aspects of using a dedicated driver in this way, it is also true that this approach may be slightly slower.  There is some overhead when calling into a driver through standard Windows CE methods.  Consequently it is best to structure code in a way that the number of driver accesses is minimized.  In any case, the gains achieved through judicious use of this methodology far outweigh the cost.


 


 

ADCpics.JPG

Comments (3)
  1. Sometimes is better to wrap the hardware access code inside a small library instead of using only the driver interface.

    In this way you will e able to change the interface to the driver keeping the library backward compatible and you may also implement some non-critical operations inside the DLL, enhancing overral performances.

    On the PXA2** family, for example, access to the registers that configure GPIO direction, alternate functions and IRQ generation is critical and must be syncronized because a read-modify-write sequence can be interrupted and may change the status of other GPIOS configured at te same time. The simple set/clear operations can be performed with no syncronization (you have a register to set the pin to high or "clear" its state to low and a 1 in the corrisponding bit applies the command while a 0 leaves the pin state unchanged) and doing them inside the lib will make the faster since you will not need a context switch.

  2. Mark Moeller says:

    Jeremy – the ADC is a good example for this sort of heavier weight solution.  For simple GPIO’s we like to leverage the Interlocked APIs using the following two macros as they are very fast (though they require being intentional about thread priority tuning).  Maybe one day CE will support an atomic Read/Modify/Write Interlocked API.(hint hint)

    #define IOW_REG_OR(_type, ptr, val)

       {

           register DWORD dwCur, dwPrev = *(volatile DWORD *)ptr;

           do {  dwCur = dwPrev;

           } while ((dwPrev = (DWORD)InterlockedCompareExchange((PLONG)ptr, (LONG)(dwCur | (DWORD)val), (LONG)dwCur )) != dwCur);

       }

    #define IOW_REG_AND(_type, ptr, val)

       {

           register DWORD dwCur, dwPrev  = *(volatile DWORD *)ptr;

           do { dwCur = dwPrev ;

           } while((dwPrev = (DWORD)InterlockedCompareExchange((PLONG)ptr, (LONG)(dwCur & (DWORD)val), (LONG)dwCur )) != dwCur);

       }

  3. Daniel Yu says:

    Is there a good way to synchronize accesses to a shared hardware between kernel and drivers. For example, a I2c interfaced power management IC is needed for both kernel and drivers (ie. battery driver, backlight driver etc.). Now, i am using a mutex, but in kernel there is a seperate functions for mutex (SC_CreateMutex, SC_WaitForMultiple and SC_ReleaseMutex) which has no any document in PB’s help. Another option I am using is that all drivers access I2c PM IC though a "kernel IO Control". I think you might be able to give me a better solution.

Comments are closed.

Skip to main content