×
Namespaces

Variants
Actions

Symbian OS Internals/12. Device Drivers and Extensions

From Nokia Developer Wiki
Jump to: navigation, search
Article Metadata
Article
Created: hamishwillee (17 Jan 2011)
Last edited: hamishwillee (30 May 2013)

by Stefan Williams with Tony Lofthouse

It is pitch dark. You are likely to be eaten by a grue.
Zork I


In the previous chapters of this book, I have concentrated on the fundamental concepts and services that make up the EKA2 kernel, and have introduced some of the fundamental hardware resources that the kernel requires, such as the interrupt controller provided in the ASSP class to provide the access to the millisecond timer interrupt.

The aim of this chapter is to explain how hardware resources are provided to the system as a whole. For example, the file server requires access to a variety of media storage devices, while the window server requires access to the LCD display and touch screen. This chapter will study the frameworks that exist to allow us to provide support for such devices.

In particular, I will cover:

  • The device driver architecture - an overview of device drivers and their place in Symbian OS
  • Kernel extensions - these are key modules required by the kernel at boot time, and this section explains how they are created and used by Symbian OS
  • The HAL - this section explains how the hardware abstraction layer is used by extensions and device drivers to provide standard device-specific interfaces to user-side code
  • Accessing user memory safely - this section explains how to ensure that you are writing safe kernel-side code, fundamental to the stability of the system
  • Device drivers - the user's interface to hardware and peripherals. This section explains the EKA2 device driver model
  • Differences between EKA1 and EKA2 - how the device driver model changed in the EKA2 release of the kernel.

Contents

Device drivers and extensions in Symbian OS

In Chapter 1, Introducing EKA2, I introduced the various hardware and software components that make up a typical Symbian OS device, and showed the modular architecture as in Figure 12.1.

Figure 12.1 Symbian OS overview

What is a device driver?

The role of a device driver is to give a user-side application access to peripheral resources without exposing the operation of the underlying hardware, and in such a manner that new classes of devices may be introduced without modification of that user-side code.

Also, since access to hardware is usually restricted to supervisor-mode code, the device driver (which runs kernel-side) is the means of access to these resources for user-mode client threads.

Device drivers are dynamically loaded kernel DLLs that are loaded into the kernel process after the kernel has booted (either by user request, or by another layer of the OS). They may be execute-in-place (XIP) or RAM loaded, and like other kernel-side code may use writeable static data. (For more details on the differences between XIP and RAM loaded modules, please refer to Chapter 10, The Loader.)

Extensions are merely special device drivers that are loaded automatically at kernel boot. I shall say more about them later.

Device driver architecture

The Symbian OS device driver model uses two types of kernel DLL - the logical device driver (LDD) and the physical device driver (PDD). See Figure 12.2. This flexible arrangement provides a level of abstraction that assists in porting between platforms and in adding new implementations of device drivers without impacting or modifying common code and APIs.

The logical device driver

The LDD contains functionality that is common to a specific class of devices. User-side code communicates with an LDD via a simple interface class, derived from RBusLogicalChannel, which presents a well-defined driver-specific API. We use the term channel to refer to a single connection between a user-side client and the kernel-side driver.

Figure 12.2 Overview of the device driver architecture

Since hardware interfaces vary across platforms, an LDD is usually designed to perform generic functionality, using a PDD to implement the device-specific code.

LDDs are dynamically loaded from user-side code but may perform some initialization at boot time if they are configured to do so. I'll explain how this is achieved when I discuss the use of extensions.

Symbian provides standard LDDs for a range of peripheral types (such as media drivers, the USB controller and serial communications devices). However, phone manufacturers will often develop their own interfaces for custom hardware.

The physical device driver

The physical device driver is an optional component, which contains functionality that is specific to a particular member of the class of devices supported by the LDD. The PDD typically controls a particular peripheral on behalf of its LDD, and obviously it will contain device-specific code. The PDD communicates only with its corresponding LDD, using an API defined by the logical channel, so it may not be accessed directly from a user-side application. The role of the PDD is to communicate with the variant, an extension, or the hardware itself, on behalf of the LDD.

To illustrate this, consider the example of a serial communications device. The generic serial communications LDD (ECOMM.LDD) defines the user-side API and the associated kernel-side PDD interface for all serial devices. It also provides buffering and flow control functions that are common to all types of UART. On a particular hardware platform, this LDD will be accompanied by one or more PDDs that support the different types of UART present in the system. (A single PDD may support more than one device of the same type; separate PDDs are only required for devices with different programming interfaces.) This is demonstrated in the following .oby file, which specifies that the ROM should contain:

  1. The generic serial communications LDD (ECOMM.LDD)
  2. Two device-specific PDDs (EUART1.PDD, EUART2.PDD).
device[VARID] = \Epoc32\Release\Arm4\Urel\16550.PDD        \System\Bin\EUART1.PDD 
 
device[VARID] = \Epoc32\Release\Arm4\Urel_SSI.PDD \System\Bin\EUART2.PDD
 
device[VARID] = \Epoc32\Release\Arm4\Urel\ECOMM.LDD \System\Bin\ECOMM.LDD

Both PDDs interface with the generic LDD, which presents a common interface to the hardware to any user of the communications device. Further examples include:

Driver LDD Associated PDD
Sound Driver ESOUND ESDRV
Ethernet Driver ENET ETHERNET
Local Media Sub-system ELOCD MEDNAND
MEDLFS
MEDMMC

Similarly to LDDs, PDDs may be configured to perform initialization at boot time.

Kernel extensions

Fundamentally, kernel extensions are just device drivers that are loaded at kernel boot. However, because of this, their use cases are somewhat specialized.

By the time the kernel is ready to start the scheduler, it requires resources that are not strictly defined by the CPU architecture. These are provided by the variant and ASSP extensions, which I have discussed in Chapter 1, Introducing EKA2. These extensions are specific to the particular platform that Symbian OS is running on, and permit the phone manufacturer to port the OS without re-compiling the kernel itself.

After initializing the variant and ASSP extensions, the kernel continues to boot until it finally starts the scheduler and enters the supervisor thread, which initializes all remaining kernel extensions. At this point, all kernel services (scheduling, memory management, object creation, timers) and basic peripheral resources (interrupt controller and other ASSP/variant functionality) are available for use.

Extensions loaded at this late stage are not critical to the operation of the kernel itself, but are typically used to perform early initialization of hardware components and to provide permanently available services for devices such as the LCD, DMA, I2C and peripheral bus controllers.

The final kernel extension to be initialized is the EXSTART extension, which is responsible for loading the file server. The file server is responsible for bringing up the rest of the OS. (If you want to find out more about system boot, turn to Chapter 16, Boot Processes.)

In Figure 12.1, the extension consists of two components - the platform-independent layer (PIL) and platform-specific layer (PSL). These are analogous to the LDD/PDD layering for device drivers that I discussed earlier. To make porting an extension to a new hardware platform easier, the PIL is generally responsible for providing functionality common to versions of the extension (such as state machines and so on) and defining the exported API, with the PSL taking on the responsibility of communicating directly with the hardware. Therefore, when porting to a new hardware platform only the PSL should require modification.

Note: Some device drivers use the same concept and split the PDD into platform-independent and platform-specific layers. One such example is the local media sub-system - this consists of a generic LDD interface suitable for all media drivers, and a PDD interface which is further divided to handle common device interfaces such as ATA/PCMCIA, NAND or NOR Flash.

Shared library DLLs

The most basic way to offer peripheral resources to other components within the kernel (not to user-mode applications) is to develop a simple kernel DLL (by specifying a targettype of KDLL in the extension's MMP file). Kernel DLLs can provide a static interface through which other kernel components gain access to the hardware:

class MyKextIf 
{
public:
IMPORT_C static TUint32 GetStatus();
IMPORT_C static void SetStatus(TUint32 aVal);
};
 
 
EXPORT_C TUint MyKextIf::GetStatus() { return *(volatile TUint32 *)(KHwBaseReg); }
 
 
EXPORT_C void MyKextIf::SetStatus(TUint aVal) { *(volatile TUint32 *)(KHwBaseReg) = aVal; }

Of course, a shared library DLL may offer any functionality that may be of use from other components within the kernel (not only access to peripheral resources). However, a kernel DLL is not defined as an extension so is not initialized by the kernel at boot time, so can't make use of writeable static data. Using a kernel extension opens up the opportunity to provide a much richer interface.

Static data initialization

Kernel-side DLLs, such as device drivers and extensions, are only ever loaded and used by a single process, the kernel itself. Hence, they only need one copy of static data (of course, if several threads within the same process require access to this data, the usual care must be taken to avoid synchronization issues).

Writeable static data for ROM-resident kernel-mode DLLs (that is, those declared in the rombuild.oby file with the keywords variant, device or extension) is appended to the kernel's static data. Initialization of variant and extension data occurs at kernel boot time, while initialization of device data occurs at device driver load time. Writeable static data for RAM-resident device drivers is placed in an extra kernel data chunk which is mapped with supervisor-only access.

It is important to note that since the kernel itself loads extensions, they are never unloaded. Therefore, the destructors for any globally constructed objects will never be called.

Entry points

Each type of kernel DLL has a unique set of characteristics that define how and when the kernel loads them during boot; these are defined by the form of the DLL's entry point. Symbian provides three different entry points, in three different libraries, and you select one by choosing which of these libraries to link against. The tool chain will automatically link in the appropriate library depending on the value of the targettype field in the DLL's MMP file:

Targettype Library DLL type
VAR EVAR.LIB Variant kernel extension
KEXT EEXT.LIB Kernel extension
PDD EDEV.LIB Physical device driver DLL
LDD EDEV.LIB Logical device driver DLL

The main entry point for all kernel DLLs is named _E32Dll, and its address, represented by TRomImageHeader::iEntryPoint, is obtained from the image header. This in turn invokes the DLL specific entry point _E32Dll_Body, the behavior of which depends on the type of kernel DLL being loaded.

Note: There is a fourth library, EKLL.LIB, which is imported when specifying the KDLL keyword for shared library DLLs. Kernel DLLs of this type contain no static data or initialization code, so contain a simple stub entry point.

Before I describe in detail how the kernel controls the initialization of kernel DLLs are during boot, let's take a look at how each library entry point handles construction and destruction of its global C++ objects.

Construction of C++ objects

Variant extensions - EVAR.LIB Since the kernel loads the variant extension once, the variant entry point only constructs the DLL's global C++ objects. Destructors are never called:

GLDEF_C TInt _E32Dll_Body(TInt aReason) // // Call variant and ASIC global constructors // 
{
if (aReason==KModuleEntryReasonVariantInit0)
{
TUint i=1;
while (__CTOR_LIST__[i])
(*__CTOR_LIST__[i++])();
AsicInitialise();
return 0;
}
return KErrGeneral;
}

Kernel extensions - EEXT.LIB As with the variant extension, the kernel loads an extension once during boot, so it only constructs the DLLs global C++ objects and never calls their destructors:

GLDEF_C TInt _E32Dll_Body(TInt aReason) // // Call extension global constructors // 
{
if (aReason==KModuleEntryReasonExtensionInit1)
{
TUint i=1;
while (__CTOR_LIST__[i])
(*__CTOR_LIST__[i++])();
}
return KernelModuleEntry(aReason);
}

Device drivers - EDEV.LIB The kernel loads and unloads device drivers dynamically, so it constructs global C++ objects when loading the DLL, and destroys them when unloading it:

GLDEF_C TInt _E32Dll_Body(TInt aReason) // // Call global constructors or destructors /
{
if (aReason==KModuleEntryReasonProcessDetach)
{
TUint i=1;
while (__DTOR_LIST__[i])
(*__DTOR_LIST__[i++])();
return KErrNone;
}
if (aReason==KModuleEntryReasonExtensionInit1 || aReason==KModuleEntryReasonProcessAttach)
{
TUint i=1;
while (__CTOR_LIST__[i])
(*__CTOR_LIST__[i++])();
}
return KernelModuleEntry(aReason);
}

Calling entry points

As the previous code shows, the kernel invokes _E32Dll with a reason code, which it uses to control how DLLs are loaded. Each reason code is passed during a particular stage in the boot process.

KModuleEntryReasonVariantInit0 Before initializing the variant, the kernel initializes the .data sections for all kernel extensions and passes the reason code KModuleEntryReasonVariantInit0 to all extension entry points. Typically, only the variant extension handles this reason code and, as we have already seen, this is responsible for constructing global C++ objects before invoking AsicInitialise() to initialize the ASSP. In Chapter 1, Introducing EKA2, I pointed out that the base port might be split into an ASSP and a variant. Under this model, the generic ASSP class forms a standard kernel extension that exports its constructor (at the very least). The ASSP class must be initialized at the same time as the variant, so it also exports the function AsicInitialise() to allow its global C++ constructors to be called.

After it has initialized both the variant and the ASSP extensions, the kernel obtains a pointer to the Asic derived variant specific class by calling the variant's first exported function:

EXPORT_C Asic* VariantInitialise()

This class is described in Chapter 5, Kernel Services.

At the end of this process, all of the extensions' .data sections are initialized, and the variant and ASSP extensions are constructed and ready for the kernel to use.

KModuleEntryReasonExtensionInit0 The kernel passes this reason code to all extension entry points that it calls after it has started the scheduler. This reason code is an inquiry, asking whether the extension has already been initialized. It allows extensions that use the KModuleEntryReasonVariantInit0 reason code to perform initialization, as I described earlier. If the extension is already loaded, returning any error code (other than KErrNone) will prevent the next stage from being performed. The ASSP DLL returns KErrGeneral in response to this reason code to report that it has already been initialized by the variant, as does the crash monitor, which hooks into the variant initialization phase to provide diagnostics of the kernel boot process. If the extension is not already loaded, the kernel will invoke its DLL entry point with the reason code KModuleEntryReasonExtensionInit1.

KModuleEntryReasonExtensionInit1 The kernel passes this reason code to all extension entry points after it has verified that the extension has not already been initialized. This causes the DLL entry point to initialize global constructors before calling KernelModuleEntry to initialize the extension itself. Note that the ASSP kernel extension has already been initialized by this point so will not receive this reason code at this point in the boot process.

KModuleEntryReasonProcessAttach and KModuleEntryReasonProcessDetach These reason codes are only handled by EDEV.LIB, which is specifically intended to support dynamically loadable DLLs (device drivers). KModuleEntryReasonProcessAttach is directly equivalent to KModuleEntryReasonExtensionInit1 and is used to initialize the driver's constructors. Conversely, KModuleEntryReasonProcessDetach calls the driver's destructors. These are called when the relevant code segment is created or destroyed, as described in Chapter 10, The Loader.

Accessing user process memory

Kernel drivers and extensions need to ensure that read and write operations involving user process memory are performed safely. There are two scenarios to consider when servicing requests from a user process, depending on whether the request is serviced in the context of the calling thread or a kernel thread.

Servicing requests in calling thread context

Requests may be fully executed in the context of the calling thread with supervisor-mode privileges. Therefore, if a process passes an invalid address to the handler or device driver, and that address happens to be within the memory of another process or even the kernel itself (either as a result of programming error or deliberate intention), then writing to this address could result in memory corruption and become a potential security risk.

Therefore, you should never attempt to write to user memory directly, either by dereferencing a pointer or by calling a function such as memcpy. Instead, you should use one of the following kernel functions:

void kumemget(TAny* aKernAddr, const TAny* aAddr, TInt aLength); 
void kumemget32(TAny* aKernAddr, const TAny* aAddr, TInt aLength);
void kumemput(TAny* aAddr, const TAny* aKernAddr, TInt aLength);
void kumemput32(TAny* aAddr, const TAny* aKernAddr, TInt aLength);
void kumemset(TAny* aAddr, const TUint8 aValue, TInt aLength);
void umemget(TAny* aKernAddr, const TAny* aUserAddr, TInt aLength);
void umemget32(TAny* aKernAddr, const TAny* aUserAddr, TInt aLength);
void umemput(TAny* aUserAddr, const TAny* aKernAddr, TInt aLength);
void umemput32(TAny* aUserAddr, const TAny* aKernAddr, TInt aLength);
void umemset(TAny* aUserAddr, const TUint8 aValue, TInt aLength);

These provide both word and non-word optimized equivalents to a memcpy function. You should use the kumemxxx versions if your code can be called from both user- and kernel-side code; this ensures that the operation is performed with the same privileges as the current thread. You may use the umemxxxversions if the operation is guaranteed not to be called from kernel-side code. The principles behind these functions are explained in detail in Section 5.2.1.5.

The memget/memput methods described previously are useful when the source and destination pointers and the lengths are provided. However, many APIs make use of descriptors, for which the kernel provides optimized functions. These allow descriptors to be safely copied between the user and kernel process, while maintaining the advantages such as runtime bounds checking that descriptors provide:

Kern::KUDesGet(TDes8& aDest, const TDesC8& aSrc); 
Kern::KUDesPut(TDes8& aDest, const TDesC8& aSrc);
Kern::KUDesInfo(const TDesC8& aSrc, TInt& aLength, TInt& aMaxLength);
Kern::KUDesSetLength(TDes8& aDes, TInt aLength);

Finally, if you want to copy a descriptor safely in a way that enables forward and backward compatibility (for example, when communicating capability packages that may evolve between versions), use the following methods:

Kern::InfoCopy(TDes8& aDest, const TDesC8& aSrc); 
Kern::InfoCopy(TDes8& aDest, const TUint8* aPtr, TInt aLength);

These provide compatibility by copying only as much data as required by the target descriptor. If the source is longer than the maximum length of the target, then the amount of data copied is limited to the maximum length of the target descriptor. Conversely, if the source is shorter than the maximum length of the target, then the target descriptor is padded with zeros.

Servicing requests in kernel thread context

If a request is made from user-side code to perform a long running task, control is usually passed back to the user process, while the task completes in a separate kernel thread. Under these circumstances, you are no longer in the context of the user thread when you want to transfer data, so you should use the following methods:

Kern::ThreadDesRead(DThread* aThread, const TAny* aSrc, TDes8& aDest, TInt aOffset, TInt aMode); 
Kern::ThreadRawRead(DThread* aThread, const TAny* aSrc, TAny* aDest, TInt aSize);
Kern::ThreadDesWrite(DThread* aThread, TAny* aDest, const TDesC8& aSrc, TInt aOffset, TInt aMode, DThread* aOrigThread);
Kern::ThreadRawWrite(DThread* aThread, TAny* aDest, const TAny* aSrc, TInt aSize, DThread* aOrigThread=NULL);
Kern::ThreadDesRead(DThread* aThread, const TAny* aSrc, TDes8& aDest, TInt aOffset);
Kern::ThreadDesWrite(DThread* aThread, TAny* aDest, const TDesC8& aSrc, TInt aOffset, DThread* aOrigThread=NULL);
Kern::ThreadGetDesLength(DThread* aThread, const TAny* aDes);
Kern::ThreadGetDesMaxLength(DThread* aThread, const TAny* aDes);
Kern::ThreadGetDesInfo(DThread* aThread, const TAny* aDes, TInt& aLength, TInt& aMaxLength, TUint8*& aPtr, TBool aWriteable);

These all take a handle to the client thread as their first argument. You must obtain this while you are still in user context (such as when the request is first received, or as we shall see later, when a channel to a device driver is first opened) and store it for use later, when the operation has completed:

//request comes in here 
iClient=&Kern::CurrentThread();
((DObject*)iClient)->Open();

Calling Open() on the client thread increments its instance count to ensure that the thread is not destroyed while you are performing the request. When the operation has completed and data has been transferred, you should decrement the threads instance count by calling Close(). Then, if the thread is closed while the operation is in progress, thread destruction will be deferred until you have called Close() and the thread's usage count has dropped to zero.

Validating the capabilities of the calling thread

As we saw in Chapter 8, Platform Security, many APIs must be governed by security capabilities, to avoid an untrusted application gaining access to privileged functionality. You can see this in the LCD HAL handler that I describe in Section 12.3, where the EDisplayHalSetState function requires the client to have power management capabilities. Such API policing prevents untrusted applications from being able to deny the user access to the screen.

You use the following kernel API to validate thread capabilities:

TBool Kern::CurrentThreadHasCapability(TCapability aCapability, const char* aContextText)

This API simply checks the capabilities of the current thread's process against that specified by aCapability and returns EFalse if the test fails, at which point you should return an error code of KErrPermissionDenied to the client and abandon the request. The C style string in the second parameter is an optional diagnostic message that is output to the debug port in debug builds (the __PLATSEC_DIAGNOSTIC_STRING macro is used to allow the string to be removed in release builds without changing the code).

A request may require more than one capability. If this is the case, you should make several calls to Kern::CurrentThreadHasCapability, since the TCapability enumeration is not specified as a bitwise field.

Kernel extensions

Since the ASSP and variant modules are initialized before the kernel is initialized, and the kernel loads all extensions before the file server (which itself is loaded by an extension), then there must be some special mechanism in place to load these modules. And indeed there is - extensions are simply execute-in-place DLLs that are specified at ROM build time, allowing the build tools to place the address of the extension in the ROM header. This allows the kernel to initialize extensions without using the loader.

Installing an extension

To install a kernel extension into a Symbian OS ROM image, you need to specify one of the following keywords in the kernel's OBY file:

Variant: The variant extension

Extension: A standard kernel or ASSP extension

The ROMBUILD tool uses these to build the ROM header (represented by the TRomHeader class), which contains two extension lists - iVariantFile contains the list of variants, and iExtensionFile contains the list of all other extensions.

As a consequence of this, extensions are always initialized in the order at which they appear in the kernel's IBY file. This is an extremely desirable feature, as many extensions and device drivers depend on other extensions being present to initialize. The MMC controller is a good example of this, as it depends on the power management extension and possibly the DMA framework too.

Note that although you can specify more than one variant keyword to include several variant extensions in the ROM, the kernel will only initialize one of them. Each variant DLL contains an identifier which specifies which CPU, ASIC and variant it was built for. The build tools place this information in the ROM header, and the bootstrap may later modify it when it copies it to the kernel superpage, thus providing the ability to create multi-platform ROMs. The same principle applies to standard extensions. However, this feature is rarely used in a production device due to ROM budget constraints.

Extension entry point macros

You should define the extension entry point, KernelModuleEntry, when writing a kernel extension, and interpret the supplied reason codes according to the rules I have described. For example, a standard kernel extension entry point would look something like this:

TInt KernelModuleEntry(Tint aReason) 
{
if (aReason==KModuleEntryReasonExtensionInit0) return KErrNone;
if (aReason!=KModuleEntryReasonExtensionInit1) return KErrArgument;
//... do extension specific initialisation here
}

Since all extensions follow the same pattern (and it's easy to make a mistake and difficult to debug what has gone wrong), the kernel provides you with a set of standard macros (defined in kernel.h) that do the hard work for you:

DECLARE STANDARD EXTENSION This is defined by the kernel as follows:

#define DECLARE_STANDARD_EXTENSION() GLREF_C TInt InitExtension();  TInt KernelModuleEntry(TInt aReason) { if (aReason==KModuleEntryReasonExtensionInit0) return KErrNone; if (aReason!=KModuleEntryReasonExtensionInit1) return KErrArgument; return InitExtension(); } GLDEF_C TInt InitExtension()

Thus reducing the entry point for a standard extension to:

 DECLARE_STANDARD_EXTENSION() { // Initialisation code here }

DECLARE STANDARD ASSP The ASSP extension entry point simply re-invokes the DLL entry point to initialize the extension's constructors:

#defineDECLARE_STANDARD_ASSP() \ extern "C" { GLREF_C TInt _E32Dll(TInt); } \ GLDEF_C TInt KernelModuleEntry(TInt aReason) \ { return (aReason==KModuleEntryReasonExtensionInit1)\ ?KErrNone:KErrGeneral; } \ DECLARESTANDARDEXTENSIONDECLARESTANDARDASSP�490 DEVICE DRIVERS AND EXTENSIONS EXPORT_C void AsicInitialise() \ { E32Dll(KModuleEntryReasonExtensionInit1); }

Thus reducing the entry point for the ASSP to:

 DECLARE_STANDARD_ASSP()

You only need to declare this - you don't need to write any extra code.

Extensions on the emulator

Since the emulator is based on the Windows DLL model and there is no ROMBUILD stage involved, we are not able to specify the order in which extensions are loaded at build time. To solve this problem, the Symbian OS emulator has its own mechanism for loading extensions, which ensures that the behavior of the emulated platform is identical to that of target hardware.

The kernel loads the emulator's variant extension explicitly by name (ECUST.DLL) and invokes the exported VariantInitialise() function. This registers the extensions to be loaded by publishing a Publish/Subscribe key named Extension:

if (iProperties.Append("Extension", "winsgui;elocd.ldd;medint.pdd;medlfs.pdd; epbusv.dll;mednand.pdd") == NULL) return KErrNoMemory;

The emulator-specific parts of the kernel that would be responsible for loading extensions from ROM on target hardware are then able to read this string and explicitly load each DLL in the order it appears in the list.

You should modify this list if you are developing extensions that need to be loaded into the emulator. The code can be found in the Wins::InitProperties() function in the source file \wins\specific\property.cpp.

Uses of extensions

In this section I will explain some of the most common uses of extensions within Symbian OS. Many extensions such as the power framework, peripheral bus and USB controllers provide asynchronous services for multiple clients - which are usually other extensions and device drivers.

The following code demonstrates a common pattern that is used to provide such an interface using an extension.

The interface is usually exported from the extension via a simple static interface class. In this example, the DoIt() function will perform some long running peripheral task, followed by a client callback function being invoked to indicate completion:

class TClientInterface // The Client API 
{
public:
IMPORT_C static TInt DoIt(TCallback& aCb);
};
 
class DMyController : public DBase // Internal API
{
public: DMyController();
TInt Create();
private:
TInt DoIt(TCallback& aCb);
static void InterruptHandler(TAny* aSelfP);
static void EventDfcFn(TAny* aSelfP);
private: TDfc iEventDfc;
};
 
DMyController* TheController = NULL;
EXPORT_C TInt TClientInterface::DoIt(TCallback* aCb)
{
return TheController->DoIt(aCb);
}

The client API uses a global instance of the DMyController object, which is initialized in the extension entry point:

 DECLARE_STANDARD_EXTENSION() 
{
DMyController* TheController = new DMyController();
if (TheController == NULL) return KErrNoMemory;
return TheController->Create();
}

So all we need to do now is provide the functionality. The simple example that follows simply registers the client callback and talks to the hardware to perform the operation (the detailed mechanism isn't shown here). Upon receiving an interrupt, a DFC is queued within which the client's callback function is invoked:

MyController:: DMyController() : iEventDfc(EventDfcFn,this,1) 
{
iEventDfc.SetDfcQ(Kern::DfcQue0());
}
 
TInt DMyController::Create()
{
return RegisterInterruptHandlers();
}
 
void DMyController::DoIt(TCallback& aCb)
{
RegisterClientCallback(aCb); // Implementation
EnableInterruptsAndDoIt(); // not shown
}
 
void DMyController::InterruptHandler(TAny* aSelfP)
{
DMyController& self = *(DMyController*)aSelfP;
self.iEventDfc.Add();
}
 
void DMyController::EventDfcFn(TAny* aSelfP)
{
DMyController& self = *(DMyController*)aSelfP;
self.NotifyClients();
}

Of course, a real example would do a little more than this, but this is the basic concept behind several extensions found in a real system.

To build this example, you would use the following MMP file:

#include <variant.mmh> 
#include "kernel\kern_ext.mmh"
target VariantTarget(mykext,dll)
targettype kext
linkas mykext.dll
systeminclude .
source mykext.cpp
library ekern.lib
deffile ~\mykext.def
epocallowdlldata
capability all

The following keywords are of particular importance when building a kernel-side DLL:

VariantTarget - to enable the same source code and MMP files to produce unique binaries for each variant, the VariantTarget macro is used to generate a unique name. Each variant provides its own implementation of this macro in its exported variant.mmh file. Without this macro, each variant would build a binary with the same name and would overwrite the binaries produced for other variants.

The Lubbock variant defines the VariantTarget macro as:

#define VariantTarget(name,ext) _lubbock_##name##.##ext

targettype - by specifying a targettype of kext in the MMP file, we instruct the build tools to link with EEXT.LIB. This provides the correct version of the _E32Dll_Body entry point to initialize kernel extensions.

epocallowdlldata - This informs the build tools that you intend the DLL to contain static data. If you omit this keyword and attempt to build a DLL that requires a. data or. bss section you will encounter one of the following errors:

Dll '<dllname>' has initialised data.

Dll '<dllname>' has uninitialised data.

Event services

Event services are associated with a single user thread, usually the window server, which calls the UserSvr::CaptureEventHook() interface to register itself as the system wide event handler. The thread registers a TRequestStatus object by calling UserSvr::RequestEvent(); this enables the thread to respond to queued events. Events from hardware such as the keypad, keyboard or digitizer (touch screen) are typically each delivered by their own kernel extension. Taking a keypad as an example, the DECLARE_STANDARD_EXTENSION() entry point will initialize the keypad hardware. Key presses will generate interrupts, and their service routine will queue a DFC, which will add key input events to the event service queue using the Kern::AddEvent() interface.

The following example shows a key down event for the backspace key being added to the event queue:

TRawEvent event; 
event.Set(TRawEvent::EKeyDown, EStdKeyBackspace);
Kern::AddEvent(event);

See Chapter 11, Window Server, for more details on how events are captured and processed.

Optional utilities

Symbian OS provides crash debugger and crash logger utility modules. The crash debugger provides post-mortem analysis of fatal errors that may occur during development - usually it is not present in a production ROM. The crash logger, on the other hand, is often placed in production ROMs so that, for example, the phone manufacturer can diagnose errors that result in a factory return. It is basically a crash debugger that dumps its diagnostic output to a reserved area of persistent storage.

Both modules are implemented as kernel extensions. We want them to be available as early as possible, to provide diagnostics of errors that occur during the early stages of the kernel boot sequence. Because of this, their initialization process is slightly different to other extensions. Rather than using the DECLARE_STANDARD_EXTENSION() macro, they implement their own version of the KernelModuleEntry() interface, which will register the modules with the kernel during the variant initialization phase, the phase in which all kernel extensions are called with the entry point reason KModuleEntryReasonVariantInit0.

See Chapter 14, Kernel-Side Debug, for more details on the crash logger and monitor modules.

System services

Kernel extensions are also used to provide services to systems outside the kernel, as the following examples demonstrate:

The local media sub-system (ELOCD)

The local media sub-system is a logical device driver that registers as an extension to provide early services to user-side code (in particular, the file server and the loader) during the boot process. The local media sub-system's kernel extension provides an exported interface class used by media drivers (which are also device drivers and kernel extensions) to register themselves with the system. Because of this, the local media sub-system must be located earlier in the ROM image than the media drivers. ELOCD also registers a HAL handler for EHalGroupMedia to allow user-side frameworks to query the registered media drivers. For more details on the local media sub-system and media drivers, please refer to Chapter 13, Peripheral Support.

EXSTART EXSTART is another important extension that must be in the ROM. This extension doesn't export any interfaces - instead its entry point simply queues a DFC to run once, after all kernel-side initialization has completed. This DFC is responsible for locating and starting the file server executable (also known as the secondary process). More information on this process is provided in Chapter 16, Boot Processes.

The hardware abstraction layer

I mentioned earlier that the local media sub-system registers a hardware abstraction layer (HAL) handler to publish information about registered drives to user-side processes. In this section, I'll explain what the HAL does and describe the kinds of services it provides.

Symbian OS defines a set of hardware and information services via the HAL interface. HAL functions are typically simple get/set interfaces, and are used by both kernel and user code.

The OS defines a range of HAL groups, each of which can have a HAL handler function installed. Each HAL group represents a different type of functionality. The following table shows the mapping between each HAL entry (enumerated in THalFunctionGroup) to the associated HAL function (defined in u32hal.h):

EHalGroupKernel TkernelHalFunction
EHalGroupVariant TVariantHalFunction
EHalGroupMedia TMediaHalFunction
EHalGroupPower TpowerHalFunction
EHalGroupDisplay TdisplayHalFunction
EHalGroupDigitiser TdigitiserHalFunction
EHalGroupSound TSoundHalFunction
EHalGroupMouse TMouseHalFunction
EHalGroupEmulator TEmulatorHalFunction
EHalGroupKeyboard TKeyboardHalFunction

Note: The maximum number of HAL groups is defined by KMaxHalGroups (currently set to 32). Because of the limited availability of HAL groups, I recommend that if you do need to add a new group, you should allocate a number from the top end of the available range to avoid conflict with any extension that Symbian may make in the future.

At OS boot, the kernel automatically installs the following handlers:

EHalGroupKernel The HAL functions in TKernelHalFunction return kernel specific information such as the amount of free RAM, the kernel startup reason and the platform's tick period. The kernel implements these functions itself.

EHalGroupVariant This HAL group accesses the variant kernel extension. If you are writing a variant extension, then you must provide an implementation of the variant HAL handler within the Asic::VariantHal() method of your variant class. This provides a simple low level interface through which the caller can obtain the processor speed and machine ID, select the active debug port, and control the debug LEDs and switches.

EHalGroupDisplay The LCD extension provides the HAL functions defined by TDisplayHalFunction, which include functions to retrieve the current operating mode of the display, set the contrast, modify the palette and switch the display on or off. This is usually one of the first set of HAL functions that you would implement during a base port.

EHalGroupPower If a power model is registered with the kernel, it will handle the HAL functions defined by TPowerHalFunction. This provides an interface to retrieve information on the state of the power supply, battery capacity and case open/close switches.

Several other HAL groups also have handlers that are implemented and registered by various modules in the OS, depending on the hardware supported by the mobile device.

Registering HAL entries

The Kern class exports the following methods to allow modules to register and deregister a HAL group handler:

TInt AddHalEntry(TInt aId, THalFunc aFunc, TAny* aPtr); 
TInt AddHalEntry(TInt aId, THalFunc aFunc, TAny* aPtr, TInt aDeviceNumber);
TInt RemoveHalEntry(TInt aId);
TInt RemoveHalEntry(TInt aId, TInt aDeviceNumber);

Note: An extension is unlikely to remove a HAL entry as extensions are never unloaded from the system. However, device drivers are dynamically loaded so must remove their handlers as part of their shutdown process.

The arguments to the AddHalEntryAPIs are the ID of a HAL group, a pointer to the handler function and a pointer to a data structure that will be passed to the handler function. A handler may also take a device number as an argument, so that it can be made device-specific. For example, a second video driver could make itself a handler for display attributes by calling:

 Kern::AddHalEntry(EHalGroupDisplay, &handler, this, 1)

The device number for a HAL function is determined by the top 16 bits of the associated HAL group number passed to the function. If a handler already exists for the HAL group, this handler will not be registered.

The HAL handler function prototype is defined by THalFunc.

typedef TInt (*THalFunc)(TAny*,TInt,TAny*,TAny*);

The arguments to this are the pointer registered with the HAL handler, the HAL function number and two optional arguments, the definition of which are dependent on the HAL function. They are usually used to read or write data passed from the client to the handler.

Let's take a look at how the LCD extension registers its HAL handler with the system. This is done when the extension is initialized:

DECLARE_STANDARD_EXTENSION() 
{
// create LCD power handler
TInt r = KErrNoMemory;
DLcdPowerHandler* pH = new DLcdPowerHandler;
// LCD specific initialisation omitted for clarity
if(pH != NULL)
{
r = Kern::AddHalEntry(EHalGroupDisplay, halFunction, pH);
}
return r;
}

This creates the LCD driver and registers a HAL handler (halFunction) for the EHalGroupDisplay group, passing a pointer to the LCD driver for context when the handler is invoked.

When a client makes a HAL request, halFunction is invoked, which is implemented as follows (most HAL functions omitted for clarity):

LOCAL_C TInt halFunction(TAny* aPtr, TInt aFunction, TAny* a1, TAny* a2) 
{
DLcdPowerHandler* pH=(DLcdPowerHandler*)aPtr;
return pH->HalFunction(aFunction,a1,a2);
}
 
TInt DLcdPowerHandler::HalFunction(TInt aFunction, TAny* a1, TAny* a2)
{
TInt r=KErrNone;
switch(aFunction)
{
case EDisplayHalScreenInfo:
{
TPckgBuf<TScreenInfoV01> vPckg;
ScreenInfo(vPckg());
Kern::InfoCopy(*(TDes8*)a1,vPckg);
break;
}
case EDisplayHalSecure:
kumemput32(a1, &iSecureDisplay, sizeof(TBool));
break;
case EDisplayHalSetState:
{
if(!Kern::CurrentThreadHasCapability(ECapabilityPowerMgmt, NULL)) return KErrPermissionDenied;
if ((TBool)a1) WsSwitchOnScreen(); else WsSwitchOffScreen();
}
default:
r=KErrNotSupported;
break;
}
return r;
}

Note in particular how this example performs API policing and safely accesses user-side memory using the APIs described in Sections 12.1.7 and 12.1.8. These are absolutely essential when implementing HAL handlers, device drivers or any other service that responds to requests from user-side code.

Device drivers

In this section of the chapter I'll be discussing device drivers in a little more depth. I'll talk about the execution model, and about how to create a device driver. I'll also discuss how user code interacts with a device driver. To make this concrete, I'll be walking through the creation of my own device driver, a simple comms driver. But first, let's look at the device driver counterpart to the extension's entry point macros.

Device driver entry point macros

DECLARE_STANDARD_LDD and DECLARE_STANDARD_PDD The following macros are provided to support device driver LDDs and PDDs:

#define DECLARE_STANDARD_LDD() \ TInt KernelModuleEntry(TInt) \ 
{ return KErrNone; } \
EXPORT_C DLogicalDevice* CreateLogicalDevice() �DECLAREEXTENSIONLDDand DECLAREEXTENSIONPDDEXTENSIONLDDand DECLAREEXTENSIONPDDDEVICE DRIVERS #define DECLARE_STANDARD_PDD() \
TInt KernelModuleEntry(TInt) \
{ return KErrNone; } \
EXPORT_C DPhysicalDevice* CreatePhysicalDevice()

This would be implemented in an LDD as follows:

DECLARE_STANDARD_LDD() { return new DSimpleSerialLDD; }

Notice that KernelModuleEntry does not provide any initialization hooks. As we shall see later, LDDs and PDDs are polymorphic DLLs which are dynamically loaded after the kernel has booted. Instead, this macro defines the first export to represent the DLL factory function.

DECLARE_EXTENSION_LDD and DECLARE_EXTENSION_PDD

Although device drivers are dynamically loadable DLLs, there may be some instances where a device driver must perform some one-off initialization at boot time. For example, media drivers register themselves with the local media sub-system at boot time to provide information about the number of supported drives, partitions and drive numbers prior to the driver being loaded (this is described in detail in Chapter 13, Peripheral Support).

To support this, you should use the DECLARE_STANDARD_EXTENSION macro previously described, in conjunction with the following macros to export the required factory function:

#define DECLARE_EXTENSION_LDD() \ 
EXPORT_C DLogicalDevice* CreateLogicalDevice() #define DECLARE_EXTENSION_PDD() \
EXPORT_C DPhysicalDevice* CreatePhysicalDevice()

Device driver classes

Throughout this chapter I shall be referring to the various classes that make up the EKA2 device driver framework. Figure 12.3 gives an overview of these classes which you can refer back to while you are reading this chapter.

In Figure 12.3, the white boxes represent classes provided by the EKA2 device driver framework. The shaded boxes indicate classes that must be implemented by the device driver.

Two components make up the logical device driver (LDD) - the LDD factory (derived from DLogicalDevice) and the logical channel (derived from DLogicalChannelBase). The LDD factory is responsible for creating an instance of the logical channel, which, as I described in the overview in Section 12.1.2.1, contains functionality that is common to a specific class of devices (such as communications devices). A user-side application communicates with the logical channel via a handle to the logical channel (RBusLogicalChannel).

Figure 12.3 The EKA2 device driver classes

Similarly, two components make up the physical device driver (PDD) - the PDD factory (derived from DPhysicalDevice) and the physical channel (derived from DBase). As I also described in Section 12.1.2.2, the physical channel is responsible for communicating with the underlying hardware on behalf of the more generic logical channel. The physical channel exists purely to provide functionality to the logical channel, so is not directly accessible from the user side.

Note: In Figure 12.3, two shaded boxes appear in the physical channel (DDriver1 and DDriver1Device). These represent a further abstraction known as the platform-independent and platform-specific layers (PIL/PSL). The PIL (DDriver1) contains functionality that, although not generic enough to live in the logical channel, is applicable to all hardware that your driver may be implemented on. The PSL (DDriver1Device) contains functionality that is too specific to belong in the LDD or PSL, such as the reading and writing of hardware-specific registers. Such layering is often beneficial when porting your device driver to a new platform, but is optional - so it is up to you when designing your device driver to determine if such layering is appropriate.

The execution model

When a device driver is loaded and a channel is opened to it, it is ready to handle requests. EKA2 provides two device driver models, which are distinguished by the execution context used to process requests from user-side clients. In the first model, requests from user-side clients are executed in the context of these clients, in privileged mode. This functionality is provided by the DLogicalChannelBase class, as shown in Figure 12.4.

Figure 12.4 Requests handled in user thread context

Alternatively, the DLogicalChannel class provides a framework that allows user-side requests to be executed in the context of a kernel thread, as shown in Figure 12.5.

Figure 12.5 Requests handled in kernel thread context

In the latter model, a call to a device driver goes through the following steps:

  1. The user-side client uses an executive call to request a service from a driver
  2. The kernel blocks the client thread and sends a kernel-side message to the kernel thread handling requests for this driver
  3. When the kernel thread is scheduled to run, it processes the request and sends back a result
  4. The kernel unblocks the client thread when the result is received.

This model makes device-driver programming easier because the same kernel thread can be used to process requests from many user-side clients and DFCs, thus serializing access to the device driver and eliminating thread-related issues. Several drivers can use the same request/DFC kernel thread to reduce resource usage.

There are two kinds of request: synchronous and asynchronous.

Synchronous requests

You would typically use a synchronous request to set or retrieve some state information. Such a request may access the hardware itself, but usually completes relatively quickly. Synchronous requests are initiated by a call to RBusLogicalChannel::DoControl(), which does not return until the request has fully completed.

Asynchronous requests

An asynchronous request is one which you would typically use to perform a potentially long running operation - for example, one that transmits or receives a block of data from the hardware when it becomes available. The time taken for such a request to complete depends on the operation performed, and during this time the client user-side thread may be able to continue with some other processing. Asynchronous requests are initiated by a call to RBusLogicalChannel::DoRequest(), which takes a TRequestStatus object as an argument and normally returns control to the user as soon as the request has been issued. Typically, hardware will indicate completion of an operation by generating an interrupt, which is handled by an interrupt service routine (ISR) provided by the driver. This in turn schedules a DFC, which runs at some later time in the context of a kernel-side thread and signals the client user-side thread, marking the asynchronous request as complete.

More than one asynchronous request can be outstanding at the same time, each one associated with its own TRequestStatus object, and each identified by a specific request number. The device driver framework puts no explicit limit on the number of concurrent outstanding asynchronous requests; any limit must be enforced by the driver itself. However, the API to cancel a request uses a TUint32 bit mask to specify the operations to be cancelled, which implicitly prevents you from uniquely identifying more than 32 concurrent request types.

User-side access to device drivers

To put the previous introduction into context, let's take a look at how a user-side application would typically go about initializing and using a simple device driver. The following example shows how a user application might access a serial port. This application simply echoes KBufSize characters from the serial port - of course, a real application would be more complex than this, and would be likely to make use of the active object framework to handle the transmission and reception of data.

TInt TestSerialPort() 
{
// Load the physical device
TInt err = User::LoadPhysicalDevice(_L(16550.PDD));
if (err != KErrNone && err != KErrAlreadyExists) return err;
// Load the logical device
err = User::LoadLogicalDevice(_L(“SERIAL.LDD));
if (err != KErrNone && err != KErrAlreadyExists) return err;
// Open a channel to the first serial port (COM0)
RSimpleSerialChannel serialPort;
err = serialPort.Open(KUnit0);
if (err != KErrNone) return err;
// Read the default comms settings
TCommConfig cBuf;
TCommConfigV01& c=cBuf();
serialPort.Config(cBuf);
c.iRate = EBps57600; // 57600 baud
c.iDataBits = EData8; // 8 data bits
c.iParity = EParityNone; // No parity
c.iStopBits = EStop1; // 1 stop bit
// Write the new comms settings
err = theSerialPort.SetConfig(cBuf);
if(err == KErrNone)
{
TRequestStatus readStat, writeStat; TUint8 dataBuf[KBufSize];
TPtr8 dataDes(&dataBuf[0],KBufSize,KBufSize);
// Read some data from the port
serialPort.Read(readStat, dataDes);
User::WaitForRequest(readStat);
if((err = readStat.Int()) == KErrNone)
{
// Write the same data back to the port
serialPort.Write(writeStat, dataDes);
User::WaitForRequest(writeStat);
err = writeStat.Int();
}
}
serialPort.Close();
return(err);
}

This example demonstrates some of the following fundamental device driver concepts:

  • Loading of a logical and physical device
    User::LoadLogicalDevice
    User::LoadPhysicalDevice
  • Opening a channel to the device driver
    RSimpleSerialChannel::Open
  • Performing a synchronous operation
    RSimpleSerialChannel::Config
    RSimpleSerialChannel::SetConfig
  • Performing an asynchronous operation
    RSimpleSerialChannel::Read
    RSimpleSerialChannel::Write
  • Closing the channel to the device driver
    RSimpleSerialChannel::Close

In the following sections I'll be discussing the underlying principles behind each of these concepts, both from a user-side perspective and how these operations are translated and implemented by the kernel and device driver.

Loading the driver from user-side code

As I have previously mentioned, device drivers are kernel DLLs that are initialized by the loader in the same manner as any other DLL; this contrasts with the extensions I mentioned previously, which are XIP modules initialized explicitly by the kernel without the use of the loader. Before a client can use a device driver, its DLLs must be loaded using a combination of the following APIs:

TInt User::LoadLogicalDevice(const TDesC &aFileName) 
TInt User::LoadPhysicalDevice(const TDesC &aFileName)

These functions ask the loader to search the system path for the required LDD or PDD. If you don't supply a filename extension, then the required extension (.LDD or .PDD) will be added to the filename. If the file is found, its UID values are verified to make sure the DLL is a valid LDD or PDD before the image is loaded. Once loaded, the kernel proceeds to call the DLL entry point as described in Section 12.1.6, and this constructs any global objects in the DLL.

After loading the DLL, the loader calls its first export immediately. LDDs and PDDs are polymorphic DLLs, and the first export is defined as the factory function required to create an object of a class derived from either DLogicalDevice for an LDD, or DPhysicalDevice for a PDD. (I'll describe these classes in detail in the next section.)

As I described in Section 12.2.2, the kernel defines two macros, DECLARE_STANDARD_LDD and DECLARE_STANDARD_PDD, which are used by the device driver to define both the kernel module entry point and the exported factory function, as shown in the following example:

DECLARE_STANDARD_LDD() 
{
return new DSimpleSerialLDD;
}

If the device driver needs to perform some one-off initialization at system boot time, you should ensure that it is also an extension. In this case, you would use the DECLARE_STANDARD_EXTENSION macro discussed in Section 12.2.2 to define the custom kernel module entry point, and use the alternative DECLARE_EXTENSION_LDD andDECLARE_EXTENSION_PDD macros to export the factory function.

Note: If you are using this feature to allocate resources early in the boot process, consider carefully whether such initialization would be better off being deferred to some later point in the process (such as when the driver is actually loaded or a channel is created). Any resources allocated at boot time will remain allocated until the system is rebooted, which may not be the behavior that you are looking for.

Once the factory object is created, the kernel calls its second phase constructor, Install(). You must register an appropriate name for the newly created LDD or PDD factory object with this function (see the following example), as well as performing any other driver-specific initialization as required. If this function is successful, the kernel will add the named object to its kernel object container. The kernel reserves two object containers specifically to maintain the list of currently loaded LDD and PDD factory objects:

Object Container Name
DLogicalDevice ELogicalDevice <ldd>
DPhysicalDevice EphysicalDevice <ldd>.<pdd>

All future references to the name of the device driver should now refer to the object name, rather than the LDD/PDD filename.

The object name is also used to associate a PDD with a specific class of LDD - I'll talk about this more later in the chapter.

Note: Some device drivers deviate from the standard <ldd>.<pdd> naming convention and define a different prefix for PDD names than the LDD name. These are usually drivers that don't rely on the kernel's automatic PDD matching framework, and I'll talk about this later.

So, in our simple serial driver, the Install() functions would look something like this:

// Simple Serial LDD 
_LIT(KLddName,"Serial");
TInt DSimpleSerialLDD::Install()
{
return(SetName(&KLddName));
}
 
// Simple 16550 Uart PDD
_LIT(KPddName,"Serial.16550");
TInt DSimple16550PDD::Install()
{
return(SetName(&KPddName));
}

I've already mentioned that the Install() function may also be used to perform any initialization that should take place when the driver is first loaded. For example, the driver may create a new kernel thread, allocate shared memory or check for the presence of a required kernel extension or hardware resource.

Verifying that devices are loaded

Symbian OS provides iterator classes to enable applications to identify which kernel objects are currently present in the system. In particular, TFindPhysicalDevice and TFindLogicalDevice may be used to identify which device drivers are currently loaded:

class TFindPhysicalDevice : public TFindHandleBase 
{
public:
inline TFindPhysicalDevice();
inline TFindPhysicalDevice(const TDesC& aMatch);
IMPORT_C TInt Next(TFullName& aResult);
};

These are derived from TFindHandleBase, a base class which performs wildcard name matching on kernel objects contained within object containers, from which a number of classes are derived to find specific types of kernel objects:

class TFindHandleBase 
{
public:
DEVICE DRIVERS IMPORT_C TFindHandleBase();
IMPORT_C TFindHandleBase(const TDesC& aMatch);
IMPORT_C void Find(const TDesC& aMatch);
inline TInt Handle() const;
protected:
TInt NextObject(TFullName& aResult,TInt aObjectType);
protected:
/** The find-handle number. */
TInt iFindHandle;
/** The full name of the last kernel-side object found. */
TFullName iMatch;
};

Each iterator class provides its own implementation of Next, which calls the protected NextObject method providing the ID of the container to be searched:

EXPORT_C TInt TFindPhysicalDevice::Next(TFullName &aResult) 
{
return NextObject(aResult,EPhysicalDevice);
}

For example, to find all physical devices present in the system, we would use TFindPhysicalDevice as follows:

TFindPhysicalDevice findHb; 
findHb.Find(_L(*));
TFullName name;
while (findHb.Next(name)==KErrNone)
RDebug::Print(name);

This is precisely the mechanism used by the text window server's PS command, which produces the output shown in Figure 12.6.

Figure 12.6 Using the text shell's command

Unloading the driver

A user-side application can unload an LDD or PDD using one of the following APIs:

TInt User::FreeLogicalDevice(const TDesC &aDeviceName) 
TInt User::FreePhysicalDevice(const TDesC &aDeviceName)

Note that an object name (aDeviceName) is used in this instance, rather than the file name that was used when loading the device. These functions enter the kernel via the executive call:

TInt ExecHandler::DeviceFree(const TDesC8& aName, TInt aDeviceType)

The aDeviceType parameter identifies the necessary object container (EPhysicalDevice or ELogicalDevice) within which the object is located by name. If the kernel finds the object, it closes it, which will result in deletion of the object and its code segment, if it is not in use by another thread:

DLogicalDevice::~DLogicalDevice() 
{
if (iCodeSeg)
{
__DEBUG_EVENT(EEventUnloadLdd, iCodeSeg);
iCodeSeg->ScheduleKernelCleanup(EFalse);
}
}

The function call ScheduleKernelCleanup(EFalse) unloads the associated DLL with module reason KModuleEntryReasonProcessDetach, ensuring that the any static data initialized at DLL load time is destroyed. The EFalse parameter indicates that the code segment is not to be immediately destroyed (since we are still using the code to run the destructor), but is to be added to the garbage list and scheduled for deletion when the null thread next runs.

Opening the device driver

In the previous section, I discussed how device drivers are loaded, creating LDD and PDD factory objects. The next step in using a device driver is to open a channel through which requests can be made. User-side code does this by making a call to RBusLogicalChannel::DoCreate(). (In reality, a client cannot call this method directly, since it is protected. It is called indirectly, via a driver-specific wrapper function, usually named Open(), although this doesn't affect our current discussion.)

inline TInt DoCreate(const TDesC& aDevice, const TVersion& aVer, TInt aUnit, const TDesC* aDriver, 
const TDesC8* anInfo, TOwnerType aType=EOwnerProcess, TBool aProtected=EFalse);

The client provides the name of the LDD (again, giving the object name that uniquely identifies the LDD factory), the supported version number, the unit number, an optional PDD name and an optional extra information block. For example:

DoCreate(_L("Serial"), version, KUnit0, NULL, NULL); DoCreate(_L("Serial"), version, KUnit0, _L(16550), NULL);

These examples demonstrate the two different mechanisms that the kernel device driver framework provides for opening channels to device drivers:

  1. Automatic search for a suitable physical device (no PDD name is specified)
  2. User-specified physical device (a PDD name is provided).

I will discuss both of these methods in the following sections, in which I will show how channels are created using LDD and PDD factory objects.

Creating the logical channel - the LDD factory

When you call RBusLogicalChannel::DoCreate(), it performs an executive call to create the kernel-side instance of a logical channel (DLogicalChannelBase) before initializing the client-side handle:

EXPORT_C TInt RBusLogicalChannel::DoCreate( const TDesC& aLogicalDevice, const TVersion& aVer, TInt aUnit, const TDesC* aPhysicalDevice, const TDesC8* anInfo, TInt aType) 
{
TInt r = User::ValidateName(aLogicalDevice);
if(KErrNone!=r) return r;
TBuf8<KMaxKernelName> name8;
name8.Copy(aLogicalDevice);
TBuf8<KMaxKernelName> physicalDeviceName;
TChannelCreateInfo8 info;
info.iVersion=aVer;
info.iUnit=aUnit;
if(aPhysicalDevice)
{
physicalDeviceName.Copy(*aPhysicalDevice);
info.iPhysicalDevice = &physicalDeviceName;
}
else info.iPhysicalDevice = NULL;
info.iInfo=anInfo;
return SetReturnedHandle(Exec::ChannelCreate(name8, info, aType),*this);
}

The info parameter is of type TChannelCreateInfo, which encapsulates the user-supplied version, unit number and optional information block:

class TChannelCreateInfo 
{
public:
TVersion iVersion;
TInt iUnit;
const TDesC* iPhysicalDevice;
const TDesC8* iInfo;
};

The channel creation mechanism in Exec::ChannelCreate makes use of the DLogicalDevice and DPhysicalDevice factory objects that the kernel created when it loaded the device drivers. The logical device is defined in kernel.h as follows:

class DLogicalDevice : public DObject 
{
public:
IMPORT_C virtual ~DLogicalDevice();
IMPORT_C virtual TBool QueryVersionSupported( const TVersion& aVer) const;
IMPORT_C virtual TBool IsAvailable(TInt aUnit, const TDesC* aDriver, const TDesC8* aInfo) const;
TInt ChannelCreate(DLogicalChannelBase*& pC, TChannelCreateInfo& aInfo);
TInt FindPhysicalDevice(DLogicalChannelBase* aChannel, TChannelCreateInfo& aInfo);
virtual TInt Install()=0;
virtual void GetCaps(TDes8& aDes) const =0;
virtual TInt Create(DLogicalChannelBase*&aChannel)=0;
public:
TVersion iVersion;
TUint iParseMask;
TUint iUnitsMask;
DCodeSeg* iCodeSeg;
TInt iOpenChannels;
};

This is an abstract base class - a device driver must provide an implementation of the GetCaps(), Create() and Install() methods, as these are used by the framework when creating the channel.

To create a channel, the kernel-side executive handler, ExecHandler::ChannelCreate, first uses the supplied LDD name to search the ELogicalDevice container for an associated DLogicalDevice factory object. If it finds one, it increments the factory's instance count before validating the supplied unit number against the value of the KDeviceAllowUnit flag in iParseMask, using the following rules:

  1. If the device supports unit numbers, the unit number must be within the range of 0 to KMaxUnits(32).
  2. If the device does not support unit numbers, the aUnit parameter must be KNullUnit.

You need to initialize iVersion and iParseMask in the constructor of your DlogicalDevice-derived LDD factory to determine how your device driver is loaded. For example, if my serial driver needs the client to specify a unit number and the relevant PDD to be present in the system, I would code the constructor like this:

DSimpleSerialLDD::DSimpleSerialLDD() 
{
iParseMask = KDeviceAllowPhysicalDevice | KDeviceAllowUnit;
iVersion = TVersion(KCommsMajorVersionNumber, KCommsMinorVersionNumber, KCommsBuildVersionNumber);
}

The following table summarizes the usage of iVersion and iParseMask

iVersion The interface version supported by this LDD. This is used to check that an LDD and PDD are compatible, so you should increment it if the interface changes. The version checking API, Kern::QueryVersionSupported(), assumes that clients requesting old versions will work with a newer version, but clients requesting new versions will not accept an older version.

iParseMask This is a bit mask indicating a combination of:

  • KDeviceAllowPhysicalDevice: The LDD requires an accompanying PDD
  • KDeviceAllowUnit: The LDD accepts a unit number at channel creation time
  • KDeviceAllowInfo: The LDD accepts additional device-specific info at channel creation time
  • KDeviceAllowAll: A combination of all of these.

iUnitsMask No longer used by the LDD; present for legacy reasons.

Once the factory object has been identified and its capabilities validated, the kernel calls the driver's channel factory function, DLogicalDevice::Create(). This function is responsible for creating an instance of the logical channel (derived from DLogicalChannelBase) through which all subsequent requests to the driver will be routed:

TInt DSimpleSerialLDD::Create(DLogicalChannelBase*& aChannel) 
{
aChannel = new DSimpleSerialChannel;
return aChannel ? KErrNone : KErrNoMemory;
}

The kernel returns a pointer to the newly created channel to the framework via a reference parameter, returning an error if it is unable to create the channel. The framework stores a pointer to the DLogicalDevice that created the channel in the channel's iDevice field (so that its reference count may be decremented when the channel is eventually closed), and increments iOpenChannels.

If the logical device specifies that it needs a PDD (indicated by the KDeviceAllowPhysicalDevice flag in iParseMask), then the kernel locates a suitable PDD factory, which is used to create the device-specific physical channel - I will cover this in more detail in the next section. The kernel stores a pointer to the newly created physical channel in the iPdd member of the logical channel.

The kernel framework will now initialize the newly created DlogicalChannelBase-derived object by calling DLogicalChannelBase::DoCreate(), passing in the information contained in the TChannelCreateInfo package supplied by the user. This is the logical channel's opportunity to validate the supplied parameters, allocate additional resources and prepare the hardware for use.

If initialization is successful, the kernel adds the newly created logical channel into the ELogicalChannel object container, and creates a handle, which it returns to the user-side client. If it is not, the kernel closes the logical channel and any associated physical device and returns a suitable error.

Note: Being a handle to a kernel object, the client side RBusLogicalChannel handle inherits the standard handle functionality described in Chapter 5, Kernel Services. By default, the kernel creates an RBusLogicalChannel handle with ELocal and EOwnerProcess attributes, thus restricting usage to the process that opened the channel. Protection may be promoted to EProtected by specifying aProtected = ETrue in RBusLogicalChannel::DoCreate. This will allow the handle to be shared with other processes using the IPC mechanisms available for handle sharing. The handle may never be promoted to an EGlobal object.

Creating the physical device - the PDD factory

Some LDDs don't require a physical device to be present (two examples being the local media sub-system which takes responsibility for loading its own media drivers and the USB controller which communicates directly with a kernel extension). But the majority of LDDs do need a PDD, since most device drivers rely on hardware with more than one possible variant.

A physical channel is nothing more than a simple DBase-derived object, and as such has an interface that is determined only by the LDD with which it is associated. (Contrast this with the logical channel, which must be derived from DLogicalChannelBase and conforms to a globally defined interface). It is the responsibility of the DPhysicalDevice-derived PDD factory to validate and create the physical channel:

class DPhysicalDevice : public DObject 
{
public:
enum TInfoFunction { EPriority=0, };
public:
IMPORT_C virtual ~DPhysicalDevice();
IMPORT_C virtual TBool QueryVersionSupported(const TVersion& aVer) const;
IMPORT_C virtual TBool IsAvailable(TInt aUnit, const TDesC8* aInfo) const;
virtual TInt Install() =0;
virtual void GetCaps(TDes8& aDes) const =0;
virtual TInt Create(DBase*& aChannel, TInt aUnit, const TDesC8* aInfo, const TVersion& aVer) =0;
virtual TInt Validate(TInt aUnit, const TDesC8* aInfo, const TVersion& aVer) =0;
IMPORT_C virtual TInt Info(TInt aFunction, TAny* a1);
public:
TVersion iVersion;
TUint iUnitsMask;
DCodeSeg* iCodeSeg;
};

Notice that this looks very similar to DLogicalDevice - not surprising since they perform an almost identical task. However, there are a few differences in the physical device:

  • iParseMask does not exist
  • A Validate() method must be provided to support the logical device in searching for suitable PDDs. (I'll show an example implementation of this later)
  • An optional Info() method may be provided to provide additional device-specific information about the driver. This is currently only used by media drivers (as you can see in Chapter 13, Peripheral Support).

Now, let's look at PDD loading in a little more detail.

User-specified PDD If a PDD name was supplied in the call to RBusLogicalChannel::DoCreate(), the kernel first validates the name to ensure that it is a match for the logical channel (that is, it compares the supplied <ldd>.<pdd> name with the wildcard string <ldd>.*). If the name is valid, the kernel uses it to locate the corresponding DPhysicalDevice object in the EPhysicalDevice container. It then calls the Validate() method on this object, passing the unit number, optional extra information block and version number. This is the PDD's opportunity to verify that the version number matches that of the requesting logical channel, and that the requested unit number is supported:

TInt DSimpleSerialPDD::Validate(TInt aUnit, const TDesC8* /*anInfo*/, const TVersion& aVer) 
{
if(!Kern::QueryVersionSupported(iVersion,aVer) return KErrNotSupported;
if (aUnit<0 || aUnit>=KNum16550Uarts) return KErrNotSupported;
return KErrNone;
}

Automatic search for PDD Alternatively, if the user-side does not provide a PDD name, but the logical device requires a PDD to be present, then the kernel makes a wildcard search for all DPhysicalDevice objects with the name <ldd>.*. For each such object, it calls the Validate() function, and the first one which returns KErrNone is taken to be the matching PDD. Note that the order of DPhysicalDevice objects within the container is influenced only by the order in which the PDDs were originally loaded. Note: This mechanism is useful when there are many PDDs supporting a single LDD, and it is not known in advance which of these PDDs support a given unit number. Once a suitable physical device has been identified, the kernel opens it (incrementing its reference count) and places the pointer to the DPhysicalDevice in the logical channel's iPhysicalDevice, so that its reference count may be decremented if an error occurs or the channel is closed. Finally, the kernel calls DPhysicalDevice::Create() on the matching PDD, again passing the unit number, optional extra information block and version number. The device driver must provide this method; it is responsible for creating and initializing the actual DBase derived physical channel:

TInt DSimpleSerialPDD::Create(DBase*& aChannel, TInt aUnit, const TDesC8* anInfo, const TVersion& aVer) 
{
DComm16550* pD=new DComm16550;
aChannel=pD; TInt r=KErrNoMemory;
if (pD) r=pD->DoCreate(aUnit,anInfo);
return r;
}

Again, the newly created physical channel is returned by reference, and the kernel places a pointer to it in the logical channel's iPdd field for later use.

Advanced LDD/PDD factory concepts

In the previous section, I discussed the basic mechanism by which the LDD and PDD factory classes are used to create a physical channel. Most device drivers follow this simple model, but the framework also provides additional functionality that may be useful for more complex implementations.

Obtaining device capabilities from user-side code Both DLogicalDevice and DPhysicalDevice define three virtual functions that I haven't yet explained: QueryVersionSupported, IsAvailable and GetCaps. You can implement these in a device driver if you want to provide device capability information to user-side code before it opens a channel. The functions are accessible via the RDeviceclass, which is the user-side handle representing the kernel-side LDD factory object. You can obtain this by opening the handle by name, or using the TFindLogicalDevice class described in Section 12.4.4.2:

class RDevice : public RHandleBase 
{
public:
inline TInt Open(const TFindLogicalDevice& aFind, TOwnerType aType=EOwnerProcess);
IMPORT_C TInt Open(const TDesC& aName, TOwnerType aType=EOwnerProcess);
IMPORT_C void GetCaps(TDes8& aDes) const;
IMPORT_C TBool QueryVersionSupported( const TVersion& aVer) const;
IMPORT_C TBool IsAvailable(TInt aUnit, const TDesC* aPhysicalDevice, const TDesC8* anInfo) const;
 
#ifndef __SECURE_API__
IMPORT_C TBool IsAvailable(TInt aUnit, const TDesC* aPhysicalDevice, const TDesC16* anInfo) const;
#endif
};

The implementation of these APIs is driver dependent. For example, our simple serial port may report its version number in the following manner:

class TSimpleSerialCaps 
{
public:
TVersion iVersion;
};
void DSimpleSerialLDD::GetCaps(TDes8& aDes) const
{
TPckgBuf<TSimpleSerialCaps> b;
b().iVersion=TVersion(KCommsMajorVersionNumber, KCommsMinorVersionNumber, KCommsBuildVersionNumber); Kern::InfoCopy(aDes,b);
}

And the user application might obtain this information in this way:

RDevice theLDD; 
TInt err = theLDD.Open(_L("Serial"));
if(err == KErrNone)
{
TPckgBuf<TSimpleSerialCaps> c;
theLDD.GetCaps(c);
TVersionName aName = c().version.Name();
RDebug::Print(_L("Serial Ver = %S\n"), &aName);
theDevice.Close();
}

Advanced PDD identification I have described how a logical device may use the KDeviceAllowPhysicalDevice flag to enable the framework to either find a PDD by name, or by wildcard search. If an LDD does not specify this parameter, it is free to perform its own search for a suitable physical device. In fact, this is precisely the mechanism used by the local media sub-system. The kernel provides the following kernel-side iterator, which is similar in concept to the user-side TFindPhysicalDevice:

struct SPhysicalDeviceEntry 
{
TInt iPriority;
DPhysicalDevice* iPhysicalDevice;
};
 
class RPhysicalDeviceArray : public RArray<SPhysicalDeviceEntry>
{
public:
IMPORT_C RPhysicalDeviceArray();
IMPORT_C void Close();
IMPORT_C TInt GetDriverList(const TDesC& aMatch, TInt aUnit, const TDesC8* aInfo, const TVersion& aVersion);
};

This class gives the same results as the automatic PDD search provided by the kernel, and it allows a logical channel to identify suitable physical devices according to its own rules. If using this scheme, it is the responsibility of the channel to maintain the list of channels that it has opened, and to define its own identification mechanism. For example, the local media sub-system defines the aUnit parameter to represent the media type of a media driver. For more advanced mechanisms, the aInfo parameter may be used to specify device-specific information when Validate() is called.

Interacting with a device driver

In previous sections, I have explained how device drivers are loaded and channels are opened using the device driver framework. The next stage is to service requests issued from user-side code. There are three main classes involved:

  1. RBusLogicalChannel - the user-side channel handle
  2. DlogicalChannelBase - the kernel-side channel (receives requests in the context of the client thread)
  3. DLogicalChannel - the kernel-side channel (receives requests in the context of a separate kernel thread).

Note: In fact, there are four classes if you include the physical channel, DPhysicalChannel, but since this is a device specific interface, I won't discuss its use until we start looking at our serial driver in more detail. I have already touched on these classes when discussing how device drivers are loaded and the channel is opened. Now I shall discuss how these are actually used in the context of a real device driver.

12.4.6.1 RBusLogicalChannel - the user-side channel handle

The RBusLogicalChannelclass is a user-side handle to a kernel-side logical channel (DLogicalChannelBase), and provides the functions required to open a channel to a device driver and to make requests:

class RBusLogicalChannel : public RHandleBase 
{
public:
IMPORT_C TInt Open(RMessagePtr2 aMessage, TInt aParam, TOwnerType aType=EOwnerProcess);
IMPORT_C TInt Open(TInt aArgumentIndex, TOwnerType aType=EOwnerProcess);
protected:
inline TInt DoCreate(const TDesC& aDevice, const TVersion& aVer, TInt aUnit, const TDesC* aDriver, const TDesC8* anInfo, TOwnerType aType=EOwnerProcess, TBool aProtected=EFalse);
IMPORT_C void DoCancel(TUint aReqMask);
IMPORT_C void DoRequest(TInt aReqNo, TRequestStatus& aStatus);
IMPORT_C void DoRequest(TInt aReqNo, TRequestStatus& aStatus, TAny* a1);
IMPORT_C void DoRequest(TInt aReqNo, TRequestStatus& aStatus, TAny* a1,TAny* a2);
IMPORT_C TInt DoControl(TInt aFunction); IMPORT_C TInt DoControl(TInt aFunction, TAny* a1);
IMPORT_C TInt DoControl(TInt aFunction, TAny* a1,TAny* a2);
private:
IMPORT_C TInt DoCreate(const TDesC& aDevice, const TVersion& aVer, TInt aUnit, const TDesC* aDriver, const TDesC8* aInfo, TInt aType);
};

Note: If you have access to the EKA2 source code, you will find that the real class is slightly more complex than the version given here. The extra methods and data are mainly provided to maintain binary compatibility with the EKA1 kernel, since this is the user-side interface to the device driver. See Section 12.5 for more on the differences between the EKA1 and EKA2 device driver framework.

RBusLogicalChannel provides the following functionality:

  • Creation of the logical channel (discussed in the previous section)
  • DoRequest - performs an asynchronous operation
  • DoControl - perform a synchronous operation
  • DoCancel - cancel an outstanding asynchronous request.

See Figure 12.7. All but two of the methods provided by RBusLogicalChannel are protected, so the client can do nothing useful with this class directly; it needs a derived interface, specific to the implementation of the device driver. The usual way to do this is to provide a header file to define the class and an inline file to provide the implementation, and include both in the client-side code at build time. As an example, let's look at how I would provide an interface to my example serial driver:

class RSimpleSerialChannel : public RBusLogicalChannel 
{
public:
enum TVer
{
EMajorVersionNumber=1,
EMinorVersionNumber=0,
EBuildVersionNumber=KE32BuildVersionNumber
};
enum TRequest
{
ERequestRead=0x0,
ERequestReadCancel=0x1,
ERequestWrite=0x1,
ERequestWriteCancel=0x2,
};
enum TControl
{
EControlConfig,
EControlSetConfig
};
public:
#ifndef __KERNEL_MODE__
inline TInt Open(TInt aUnit);
inline TVersion VersionRequired() const;
inline void Read(TRequestStatus& aStatus, TDes8& aDes);
inline void ReadCancel();
inline void Write(TRequestStatus& aStatus, const TDesC8& aDes);
inline void WriteCancel();
inline void Config(TDes8& aConfig);
inline TInt SetConfig(const TDesC8& aConfig);
#endif
};
 
#include <simpleserial.inl>

The shaded box represents a class implemented by the example driver RDriver1

Figure 12.7 Mapping the user-side API to RBusLogicalChannel

The implementation is in the corresponding inline file:

#ifndef __KERNEL_MODE__ 
_LIT(KDeviceName,"Serial");
inline TInt RSimpleSerialChannel::Open(TInt aUnit)
{
return(DoCreate(KDeviceName,VersionRequired(), aUnit,NULL,NULL));
}
inline TVersion RSimpleSerialChannel::VersionRequired() const
{
return(TVersion(EMajorVersionNumber, EMinorVersionNumber, EBuildVersionNumber));
}
inline void RSimpleSerialChannel::Read(TRequestStatus&aStatus, TDes8& aDes)
{
TInt len=aDes.MaxLength();
DoRequest(ERequestRead,aStatus,&aDes,&len);
}
inline void RSimpleSerialChannel::ReadCancel()
DoCancel(ERequestReadCancel);
inline void RSimpleSerialChannel::Write(TRequestStatus& aStatus, const TDesC8& aDes)
{
TInt len=aDes.Length();
DoRequest(ERequestWrite,aStatus,(TAny *)&aDes,&len);
}
inline void RSimpleSerialChannel::Config(TDes8& aConfig) DoControl(EControlConfig,&aConfig);
inline TInt RSimpleSerialChannel::SetConfig(const TDesC8& aCfg) return(DoControl(EControlSetConfig, (TAny *)&aCfg));
#endif

Note: These headers are also included in the kernel-side implementation of the device driver, so that it can pick up the version number and request number enumerations. This is why #ifndef _KERNEL_MODE_ is used around the user-side specific methods.

Next I will look at how the kernel handles communication from the user-side application, using the DLogicalChannelBase object.

DLogicalChannelBase - the kernel-side channel

DLogicalChannelBase is the kernel-side representation of the user-side RBusLogicalChannel. It is an abstract base class, the implementation of which is provided by the device driver:

class DLogicalChannelBase : public DObject 
{
public:
IMPORT_C virtual ~DLogicalChannelBase();
public:
virtual TInt Request(TInt aReqNo, TAny* a1, TAny* a2)=0;
IMPORT_C virtual TInt DoCreate(TInt aUnit, const TDesC8* aInfo, const TVersion& aVer);
public:
DLogicalDevice* iDevice; DPhysicalDevice* iPhysicalDevice; DBase* iPdd;
};

I have already discussed the meaning of several members of this class when explaining how a logical channel is created:

  • iDevice - a pointer to the LDD factory object that created this logical channel. The framework uses it to close the LDD factory object when the channel is closed
  • iPhysicalDevice - a pointer to the PDD factory object that created the physical channel. The framework uses it to close the PDD factory object when the channel is closed
  • iPdd - a pointer to the physical channel associated with this logical channel. It is used by the logical channel itself when communicating with the hardware and by the framework to delete the physical channel when the logical channel is closed.

iPhysicalDevice and iPdd are only provided by the framework if the logical device has specified that a physical channel is required by specifying the KDeviceAllowPhysicalDevice flag in DLogicalDevice::ParseMask.

Creating the logical channel

The virtual DoCreate() method is the logical channel's opportunity to perform driver-specific initialization at creation time (see Section 12.4.5.1). The device driver framework calls it after creating the logical channel:

TInt DoCreate(TInt aUnit, const TDesC8* aInfo, const TVersion& aVer);

Typically, a device driver would use this function to perform the following actions:

  • Validate that the user-side code has sufficient capabilities to use this device driver
  • Validate the version of the driver that the user-side code requires
  • Initialize any DFC queues needed by the device driver. (Interrupt handlers would rarely be initialized here - this is usually the responsibility of the physical channel)
  • Create and initialize its power handler.

This is how my simple serial driver does it:

TInt DSimpleSerialChannel::DoCreate(TInt aUnit, const TDesC8* /*anInfo*/, const TVersion &aVer) 
{
if(!Kern::CurrentThreadHasCapability(ECapabilityCommDD,_ _PLATSEC_DIAGNOSTIC_STRING("Checked by SERIAL.LDD (Simple Serial Driver)"))) return KErrPermissionDenied;
if (!Kern::QueryVersionSupported(TVersion( KCommsMajorVersionNumber, KCommsMinorVersionNumber, KCommsBuildVersionNumber),aVer)) return KErrNotSupported;
// initialise the TX buffer
iTxBufSize=KTxBufferSize;
iTxBuffer=(TUint8*)Kern::Alloc(iTxBufSize);
if (!iTxBuffer) return KErrNoMemory;
iTxFillThreshold=iTxBufSize>>1;
// initialise the RX buffer
iRxBufSize=KDefaultRxBufferSize;
iRxCharBuf=(TUint8*)Kern::Alloc(iRxBufSize<<1);
if (!iRxCharBuf)
{
Kern::Free(iTxBuffer);
return KErrNoMemory;
}
iRxDrainThreshold=iRxBufSize>>1;
return KErrNone;
}

Performing the capability check This should be the very first thing that you do in your DoCreate() method. You need to check that the client has sufficient capabilities to use this driver (see Section 12.1.8 and Chapter 8, Platform Security, for more information on the EKA2 security model).

Check the version My DoCreate() method also verifies that the version of the device driver is compatible with that expected by the client. As I described in Section 12.4.6.1, both the user and the kernel side share a common header file at build time, and this provides the version number of the API. The kernel provides the Kern::QueryVersionSupported() method to enforce a consistent set of rules to all APIs, and this returns ETrue if the following conditions are satisfied:

  • The major version of the client is less than the major version of the driver
  • The major version of the client is equal to the major version of the driver and the minor version of the client is less than or equal to the minor version of the driver.

The request gateway function

virtual TInt Request(TInt aReqNo, TAny* a1, TAny* a2)=0;
Figure 12.8 The request gateway function

The request gateway function takes a request number and two undefined parameters, which are mapped from the RBusLogicalChannel as follows:

[--hamishwillee : section below needs layout - check book itself]

RBusLogicalChannel DLogicalChannelBase DoControl(aFunction) Request(aFunction, 0, 0) DoControl(aFunction, a1) Request(aFunction, a1, 0) DoControl(aFunction, a1, a2) Request(aFunction, a1, a2) DoRequest(aReqNo, aStatus) Request(~aReqNo, &aStatus, &A[0, 0]) DoRequest(aReqNo, aStatus, a1) Request(~aReqNo, &aStatus, &A[a1, 0]) DoRequest(aReqNo, aStatus, a1, a2) Request(~aReqNo, &aStatus, &A[a1, a2]) DoCancel(aReqMask) Request(0x7FFFFFFF, aReqMask, 0)

The following code shows how the RBusLogicalChannel::DoControl() method is implemented:

EXPORT_C TInt RBusLogicalChannel::DoControl(TInt aFunction, TAny *a1,TAny *a2) 
{
return Exec::ChannelRequest(iHandle,aFunction,a1,a2);
}

And here we show how an RBusLogicalChannel::DoRequest method is implemented. You can see that the stack is used to pass parameters a1 and a2. (This stack usage is represented in the previous table as \&A[a1,a2].)

EXPORT_C void RBusLogicalChannel::DoRequest(TInt aReqNo, TRequestStatus &aStatus, TAny *a1, TAny *a2) 
{
TAny *a[2];
a[0]=a1;
a[1]=a2;
aStatus=KRequestPending;
Exec::ChannelRequest(iHandle,~aReqNo,&aStatus,&a[0]);
}

There is in fact little difference between DoControl() and DoRequest() as far as the kernel is concerned, so DoControl() could theoretically perform asynchronous operations by passing the address of a TRequestStatus as one of the user parameters - this is a valid optimization to make under some circumstances. However, you should be aware that, although the DLogicalChannelBase framework does not make any assumptions as to the format of the parameters supplied, they are crucial to the operation of the DLogicalChannel class - so any deviation from this pattern should be performed with care.

When understanding the operation of device drivers, it is crucial to understand the context of requests coming from user-side. These requests arrive at the gateway function via the following executive call:

__EXECDECL__ TInt Exec::ChannelRequest(TInt, TInt, TAny*, TAny*) 
{
SLOW_EXEC4(EExecChannelRequest);
}

This is a slow executive call which takes four parameters. It runs innthe context of the calling thread, with interrupts enabled and the kernel unlocked, so it can be preempted at any point of its execution. The following parameters are defined for this handler:

DECLARE_FLAGS_FUNC(0|EF_C|EF_P|(ELogicalChannel+1), ExecHandler::ChannelRequest)

Referring back to Chapter 5, Kernel Services, this means that the call is be preprocessed (as indicated by the EF_P[KExecFlagPreProcess] flag) to obtain the kernel-side object from the supplied user handler using the ELogicalChannel container, which in turn implies that it must hold the system lock (as indicated by the EF_C[KExecFlagClaim] flag). After preprocessing, the kernel invokes this executive handler:

TInt ExecHandler::ChannelRequest(DLogicalChannelBase* aChannel, TInt aFunction, TAny* a1, TAny* a2)
{
DThread& t=*TheCurrentThread;
if (aChannel->Open()==KErrNone)
{
t.iTempObj=aChannel; NKern::UnlockSystem();
TInt r=aChannel->Request(aFunction,a1,a2);
NKern::ThreadEnterCS();
t.iTempObj=NULL;
aChannel->Close(NULL);
NKern::ThreadLeaveCS();
return r;
}
K::PanicCurrentThread(EBadHandle);
return 0;
}

The executive handler calls DLogicalChannelBase::Request(), so this runs in the context of the calling (client) thread. Before calling the request function, the kernel (a) increments the channel's reference count, (b) stores the pointer to the channel in the iTempObj member of the current thread and (c) releases the system lock. Since the request function may be preempted at any time, these measures ensure that:

  • If the current thread exits, the channel is safely closed after servicing the request
  • Another thread cannot close the channel until the request function completes.

12.4.6.5 Using DLogicalChannelBase::Request

In the previous section we discussed the context within which DLogicalChannelBase::Request() is called. This has implications for the design of a device driver using this function. Consider the following points:

  1. A device driver is likely to use hardware interrupts that queue a DFC on a kernel thread for further processing. This kernel thread can preempt the Request() method at any time
  2. Several threads (in the process that opened the channel) may use a single instance of the channel concurrently
  3. Several concurrent operations may require access to the same hardware and registers.

So, in all but the simplest device drivers, it is unlikely that the driver will get away with supporting only one asynchronous operation at a time. Depending on the requirements of the driver, it may be acceptable for the client to issue a second request without having to wait for the first to complete - and the same applies to issuing a synchronous request while an asynchronous request is outstanding. Since the completion of an asynchronous operation occurs in a kernel-side DFC thread, and may preempt any processing that the device driver does in user context, you must take great care to avoid synchronization issues - the kernel does not do this for you.

Request synchronization Most device drivers are either written to perform all processing within the calling thread context, or to perform all processing within the kernel thread. The former is not generally acceptable for long running tasks, as this effectively blocks the client thread from running. A simple way to provide synchronization of requests in the latter case is to provide a single DFC per request (all posted to the same DFC queue). This scheme provides serialization of requests and guarantees that one operation is not preempted by another operation. However, this may not always be a practical solution as it takes time to queue a DFC and time for it to be scheduled to run. If you are performing a simple request (such as a read from a fast hardware register), this delay may be unacceptable. Device drivers can support a combination of fast synchronous requests (handled in the context of the client thread) and long running asynchronous requests (handled in the context of one or more kernel threads), each of which may require access to the same resources. The kernel provides a set of primitives that should be used when addressing such synchronization issues. First, the kernel provides a set of primitives to allow you to safely perform operations such as increment, decrement, swap or read/modify/write:

TInt NKern::LockedInc(TInt& aCount) 
TInt NKern::LockedDec(TInt& aCount)
TInt NKern::LockedAdd(TInt& aDest, TInt aSrc)
TUint32 NKern::LockedSetClear(TUint32& aDest, TUint32 aClearMask, �TUint32 aSetMask)
TUint8 NKern::LockedSetClear8(TUint8& aDest, TUint8 aClearMask, TUint8 aSetMask)
TInt NKern::SafeInc(TInt& aCount) TInt NKern::SafeDec(TInt& aCount)
TAny* NKern::SafeSwap(TAny* aNewValue, TAny*& aPtr)
TUint8 NKern::SafeSwap8(TUint8 aNewValue, TUint8& aPtr)

And of course, for synchronization, the nanokernel's fast semaphores and mutexes are available:

void NKern::FSWait(NFastSemaphore* aSem); 
void NKern::FSSignal(NFastSemaphore* aSem);
void NKern::FMWait(NFastMutex* aMutex);
void NKern::FMSignal(NFastMutex* aMutex);

However, as the number of request types increases, and we add new DFCs and synchronization objects, the complexity of the driver rises dramatically. What we would like to do is to serialize our requests using a message queue, which is the very scheme employed by the DLogicalChannel framework and which I will explain in the next section.

DLogicalChannel

The DLogicalChannel class is provided to address the synchronization issues I mentioned previously. It supplies a framework within which user-side requests are executed in the context of a single kernel-side thread. It derives from DLogicalChannelBase and makes use of a kernel-side message queue and its associated DFC queue. Additionally, DLogicalChannel overrides the Close() method so that close events are handled safely within the same DFC queue.

Here's how DLogicalChannel is defined in kernel.h:

class DLogicalChannel : public DLogicalChannelBase 
{
public:
enum
{
EMinRequestId=0xc0000000,
ECloseMsg=0x80000000
};
public:
IMPORT_C DLogicalChannel();
IMPORT_C virtual ~DLogicalChannel();
IMPORT_C virtual TInt Close(TAny*);
IMPORT_C virtual TInt Request(TInt aReqNo, TAny* a1, TAny* a2);
IMPORT_C virtual void HandleMsg(TMessageBase* aMsg)=0;
IMPORT_C void SetDfcQ(TDfcQue* aDfcQ);
public:
static void MsgQFunc(TAny* aPtr);
public:
TDfcQue* iDfcQ;
TMessageQue iMsgQ;
};

iDfcQ is a pointer to a DFC queue used to handle client requests.

iMsgQ is a message queue used to handle client requests.

The DFC queue

A driver based on DLogicalChannel provides a DFC queue to the framework at channel creation time. DLogicalChannel uses this queue to dispatch messages in response to user-side requests received by the Request() function. The driver may use one of the standard kernel queues or provide its own, and to avoid synchronization issues, this queue will usually be the same queue that it uses for DFCs related to hardware interrupts and other asynchronous events. For example, I would initialize my serial driver to use a standard kernel queue like this:

TInt DSimpleSerialChannel::DoCreate(TInt aUnit, const TDesC8* anInfo, const TVersion &aVer)
{
// Security and version control code as shown in the
// previous example are omitted for clarity.
SetDfcQ(Kern::DfcQue0());
iMsgQ.Receive();
return KErrNone;
}

The pattern I have just shown puts the selection of the DFC queue into the LDD, which is useful if all the physical channels are likely to talk to the same hardware registers or memory, because it ensures that all requests for all channels will be serviced in the same kernel thread.

The kernel provides two standard DFC queues (running on two dedicated kernel threads, DfcThread0 and DfcThread1). These are available for use by device drivers and are obtained using Kern::DfcQue0() and Kern::DfcQue1(). DfcThread0 is a general-purpose low priority thread, which is currently used by serial comms, sound, Ethernet, keyboard and digitizer device drivers, and is suitable for simple device drivers without very stringent real-time requirements. DfcThread1 is a higher priority thread and should be used with care, since inappropriate use of this thread may adversely affect the accuracy of nanokernel timers.

Some device drivers create and use their own thread. This may be specified by the logical channel as I have described previously, or by the physical device itself. The local media sub-system uses the latter method.

The LDD provides a generic interface suitable for all types of media driver, and to allow multiple media drivers to operate concurrently, each media driver is responsible for specifying a DFC queue within which to service requests.

The caution I gave earlier about using DfcThread1 is even more appropriate if your driver creates a thread with higher priority than DfcThread1. In both cases, you must take care not to delay the nanokernel timer DFC by more than 16 ticks.

The following code shows how my logical channel queries the physical device to obtain a device-specific DFC queue:

TInt DSimpleSerialChannel::DoCreate(TInt aUnit, const TDesC8* anInfo, const TVersion &aVer) 
{
// Security and version control code as shown in the
// previous example are omitted for clarity.
SetDfcQ(((DComm*)iPdd)->DfcQ(aUnit));
iMsgQ.Receive();
return KErrNone; }

The physical channel can creates its DFC queue using the following kernel API:

TInt r=Kern::DfcQInit(&iDfcQ, iPriority, iName);

Alternatively, the physical channel can obtain the DFC queue from some other source, such as a kernel extension. For example, consider the peripheral bus controller which services removable media devices such as PC Card and SDIO devices. Such a controller would typically use a dedicated DFC thread to synchronize its requests, which would be made available for use by device drivers.

DFC queue usage tips

  • Tip 1: Since several drivers may share the same DFC queue, then the minimum latency of a DFC is the sum of the execution times of all other DFCs executed beforehand. Therefore, DFCs should be kept as short as possible, especially those on shared queues.
  • Tip 2: Consider your allocation of DFC priorities carefully when designing your device driver. If some events are more critical than others, you can use DFC priorities to ensure that these are serviced before lower priority events. For example, a cable disconnect or media removal notification may be given a higher priority than data transfer events to allow your driver to take emergency action and recover gracefully. However, if you are using a shared DFC queue then choose your priorities carefully to avoid affecting other drivers.
  • Tip 3: If the DFC queue is obtained from a kernel extension, the queue will be available throughout the life of the system. However, if you create the queue (when the PDD or LDD is opened) then you must destroy it (when the PDD or LDD is unloaded) to avoid resource leaks. There is no kernel API to destroy a DFC queue from outside the DFC queue's own thread, so the standard way to do this is to post a cleanup DFC to the queue.

The message queue

I described message queues in Chapter 4, Inter-thread Communication. In this section, I shall talk about how device drivers make use of message queues. DLogicalChannel uses a message queue (iMsgQ) to allow multiple requests from several client-side threads to be queued and handled sequentially by a single kernel-side DFC. This is illustrated in Figure 12.9.

Figure 12.9 The driver's message queue framework

The message queue consists of a DFC and a doubly linked list of received messages (TThreadMessage objects). Each user-side thread owns its own TThreadMessage object as shown in Figure 12.10.

Figure 12.10 The message queue and user threads

When issuing a request to a device driver, the driver framework populates the thread's TThreadMessage object with the supplied request parameters before adding the message to the message queue using SendReceive():

EXPORT_C TInt TMessageBase::SendReceive(TMessageQue* aQ) 
{
Send(aQ);
NKern::FSWait(&iSem);
return iValue;
}

The calling thread is blocked until the message is delivered and the driver signals that it has received the message by completing it, using TMessageBase::Complete(). This does not necessarily mean that the requested operation is done - merely that the message has been received and the driver is ready to accept a new one. If the driver thread is asked to perform a long running task, it will later use a TRequestStatus object to signal completion to the client. Since all messages are delivered to the same DFC queue, this has the effect of serializing requests from multiple threads.

The message queue is constructed during the construction of the DLogicalChannel, and it is specified as a parameter to that construction in this way:

EXPORT_C DLogicalChannel::DLogicalChannel() : iMsgQ(MsgQFunc,this,NULL,1) { }

The NULL parameter is the DFC queue, which in this example is initialized in the DoCreate() function:

SetDfcQ(Kern::DfcQue0()); 
iMsgQ.Receive();

SetDfcQ is inherited from DLogicalChannel, and is responsible for storing the DFC queue and passing it on to the message queue:

 EXPORT_C void DLogicalChannel::SetDfcQ(TDfcQue* aDfcQ) 
{
iDfcQ=aDfcQ;
iMsgQ.SetDfcQ(aDfcQ);
}

Note that in DoCreate(), I called iMsgQ.Receive() immediately after setting up the message queue with the appropriate DFC queue. This marks the queue as ready, so that the first message to be received will be accepted immediately. If a message is already available, iMsgQ. Receive() immediately queues a DFC to service this message within the context of the device driver's chosen thread. DLogicalChannel defines its message queue function like this:

void DLogicalChannel::MsgQFunc(TAny* aPtr) 
{
DLogicalChannel* pC=(DLogicalChannel*)aPtr;
pC->HandleMsg(pC->iMsgQ.iMessage);
}

HandleMsg() is the pure virtual function implemented by the device driver to handle the incoming requests, and I'll explain this in detail shortly. First, I'll explain how messages are added to the queue.

DLogicalChannel provides its own implementation of the Request() gateway function:

EXPORT_C TInt DLogicalChannel::Request(TInt aReqNo, TAny* a1, TAny* a2) 
{
if (aReqNo<(TInt)EMinRequestId) K::PanicKernExec(ERequestNoInvalid);
TThreadMessage& m=Kern::Message();
m.iValue=aReqNo;
m.iArg[0]=a1;
if (aReqNo<0)
{
kumemget32(&m.iArg[1],a2,2*sizeof(TAny*));
}
else
m.iArg[1]=a2;
return m.SendReceive(&iMsgQ);
}

Remember that this is running in the context of the user-side client thread. This means that Kern::Message() obtains the client thread's message object and uses this as a placeholder for the request arguments supplied.

EXPORT_C TThreadMessage& Kern::Message() 
{
return TheCurrentThread->iKernMsg;
}

Now, you may remember that when I discussed the user-side APIs representing the logical channel (Section 12.4.6.4), I described how synchronous requests pass a request number and up to two optional request arguments, while asynchronous requests pass the compliment of the request number, a pointer to a TRequestStatus object and a user-side structure containing two function arguments:

[--hamishwillee : Need to check book to see layout of code text below]

BusLogicalChannel DLogicalChannelBase DoControl(aFunction) Request(aFunction, 0, 0) DoControl(aFunction, a1) Request(aFunction, a1, 0) DoControl(aFunction, a1, a2) Request(aFunction, a1, a2) DoRequest(aReqNo, aStatus) Request(~aReqNo, &aStatus, &A[0, 0]) DoRequest(aReqNo, aStatus, a1) Request(~aReqNo, &aStatus, &A[a1, 0]) DoRequest(aReqNo, aStatus, a1, a2) Request(~aReqNo, &aStatus, &A[a1, a2]) DoCancel(aReqMask) Request(0x7FFFFFFF, aReqMask, 0)

A negative request number indicates that an asynchronous DoRequest() operation is being made, so the DLogicalChannel knows to extract the arguments in the appropriate manner (that is, the second argument is allocated on the client stack and represents two arguments which must both be copied and stored before the calling function goes out of scope). A positive number indicates that the operation does not require any special treatment other than a direct copy of the message arguments. Once the arguments have been copied from the user-side thread into the request message, the message is delivered to the appropriate thread using SendReceive(&iMsgQ).

Closing the logical channel

DLogicalChannelBase does not provide its own Close() method, but instead relies on the default implementation of the reference counting DObject::Close() method to destroy the channel. In contrast, a DLogicalChannel-based driver makes use of the message queue to destroy the channel. This is necessary because there may be an outstanding DFC or asynchronous request that must be cancelled or completed before we are able to close the channel completely.

A special message, ECloseMsg, is reserved for this purpose, and is issued by DLogicalChannel::Close() as shown:

EXPORT_C TInt DLogicalChannel::Close(TAny*) 
{
if (Dec()==1)
{
NKern::LockSystem();
NKern::UnlockSystem();
if (iDfcQ)
{
TThreadMessage& m=Kern::Message();
m.iValue=ECloseMsg;
m.SendReceive(&iMsgQ);
}
DBase::Delete(this);
return EObjectDeleted;
}
return 0;
}

As you can see, this close message is treated exactly the same as any other message, and is delivered to the driver's thread to give it the opportunity to cancel or complete any outstanding DFCs or requests before the channel is finally destroyed. The calls to NKern::LockSystem() followed by NKern::UnlockSystem() may look odd, but this ensures that nobody else is using the object while the close message is sent and the channel subsequently deleted.

Handling messages in the device driver

Messages are handled in a DLogicalChannel-derived device driver using the HandleMsg() function. Here is my serial driver's implementation:

void DSimpleSerialChannel::HandleMsg(TMessageBase* aMsg) 
{
TThreadMessage& m=*(TThreadMessage*)aMsg;
TInt id=m.iValue;
if (id==(TInt)ECloseMsg)
{
Shutdown();
m.Complete(KErrNone, EFalse);
return;
}
else if (id==KMaxTInt)
{
// DoCancel
DoCancel(m.Int0());
m.Complete(KErrNone,ETrue);
return;
}
if (id<0)
{
// DoRequest
TRequestStatus* pS=(TRequestStatus*)m.Ptr0();
TInt r=DoRequest(~id,pS,m.Ptr1(),m.Ptr2());
if (r!=KErrNone) Kern::RequestComplete(iClient,pS,r);
m.Complete(KErrNone,ETrue);
}
else
{
// DoControl
TInt r=DoControl(id,m.Ptr0(),m.Ptr1());
m.Complete(r,ETrue);
}
}

Most device drivers follow this pattern, as it draws a clean separation between the different types of request. I use the function ID (obtained from m.iValue) to determine if the message is a synchronous or asynchronous request, or a cancel request or a close message. After invoking the necessary handler function, I complete the message, which unblocks the client thread and allows further messages to be processed by the DLogicalChannel message-handling framework.

You should also notice also how errors are reported. A synchronous request reports its return value directly to the client, passing the error code via in the call to m.Complete().(RBusLogicalChannel::DoControl() returns a TInt.) An asynchronous request always completes its message with KErrNone, and reports errors back to the client using the client's TRequestStatus object. (RBusLogicalChannel::DoRequest() has a void return type). This ensures that if the client is using the active object framework, then the framework's CActive::RunError() method will be invoked if an error occurs when handling an asynchronous request.

Handling synchronous requests

Handling a synchronous request is simple - here's my DoControl method for the simple serial port channel:

TInt DSimpleSerialChannel::DoControl(TInt aFunction, TAny* a1, TAny* a2) 
{
TCommConfigV01 c;
TInt r=KErrNone;
switch (aFunction)
{
case RSimpleSerialChannel::EControlConfig:
{
TPtrC8 cfg((const TUint8*)&iConfig, sizeof(iConfig));
r=Kern::ThreadDesWrite(iClient,a1,cfg, 0,KTruncateToMaxLength,iClient);
break;
}
case RSimpleSerialChannel::EControlSetConfig:
{
memclr(&c, sizeof(c));
TPtr8 cfg((TUint8*)&c,0,sizeof(c));
r=Kern::ThreadDesRead(iClient,a1,cfg,0,0);
if(r==KErrNone) r=SetConfig(c);
break;
}
default:
r=KErrNotSupported;
}
return(r);
}

The RSimpleSerialChannel::EControlConfig case corresponds to the synchronous user-side API RSimpleSerialChannel::Config() which was defined in Section 12.4.6.1, RBusLogicalChannel - the user-side channel handle. To handle this request, a constant pointer descriptor is created to point to the configuration data to be returned to the client, and it is safely written back to user space using the Kern::ThreadDesWrite() API. (Remember, when using DLogicalChannel, all requests are handled within the context of a kernel thread so the appropriate API must be used when writing to user space as described in Section 12.1.7.2.) Note the use of KTruncateToMaxLength- this ensures that the length of data copied back to user space does not exceed the length of the user-side descriptor, and is good practice when passing structures that may change size in future releases of the driver.

RSimpleSerialChannel::EControlSetConfig corresponds to the synchronous user-side API RSimpleSerialChannel::SetConfig(). This is handled in a similar manner to the previous case, this time using the Kern::ThreadDesRead()API to read the configuration data from user space.

Handling asynchronous requests

Similarly, asynchronous requests are usually handled by a DoRequest() method which is responsible for setting up the hardware to create an event that will complete the request at some point in the future.

Depending on the nature of the device driver, it may be possible to handle several outstanding asynchronous requests simultaneously. Consider my serial port example for a moment - a duplex link allows simultaneous transmission and reception of data, so I want to allow both a read and a write request to be outstanding simultaneously. However, I want to prevent a client from requesting two simultaneous operations of the same type. The serial port driver handles this by maintaining a copy of the outstanding read/write request status objects (iRxStatus and iTxStatus), and panicking the client if it receives a second request of the same type as an outstanding one. (Panicking is the right thing to do here, as the client's behavior indicates that it has a serious bug.) Other device drivers, such as the local media sub-system, do allow simultaneous requests of the same type to be issued, since a single instance of a media driver may be servicing several file systems which access different partitions on the disk. Such scenarios are handled by forwarding the requests to an internal queue, from which any deferred requests are handled when it is convenient to do so.

Here's how asynchronous requests are handled in my example serial driver:

TInt DSimpleSerialChannel::DoRequest(TInt aReqNo, TRequestStatus* aStatus, TAny* a1, TAny* a2) 
{
if(iStatus==EOpen)
Start();
else
return(KErrNotReady)
TInt r=KErrNone;
TInt len=0;
switch (aReqNo)
{
case RSimpleSerialChannel::ERequestRead:
{
if(iRxStatus)
{
Kern::ThreadKill(iClient,EExitPanic, ERequestAlreadyPending,KLitKernExec);
return(KErrNotSupported);
}
if(a2) r=Kern::ThreadRawRead(iClient,a2, &len,sizeof(len));
if(r==KErrNone)
{
iRxStatus=aStatus;
InitiateRead(a1,len);
}
break;
}
case RSimpleSerialChannel::ERequestWrite:
{
if(iTxStatus)
{
Kern::ThreadKill(iClient,EExitPanic, ERequestAlreadyPending,KLitKernExec);
return(KErrNotSupported);
}
if(!a1) a1=(TAny*)1;
r=Kern::ThreadRawRead(iClient,a2,&len,sizeof(len));
if(r==KErrNone)
{
iTxStatus=aStatus;
InitiateWrite(a1,len);
}
break;
}
default:
return KErrNotSupported;
}
return r;
}

Both ERequestRead and ERequestWrite requests follow the same basic pattern. First, the status of the device is checked to determine if the channel is currently open, and if so the hardware is prepared for data transfer by calling ::Start():

void DSimpleSerialChannel::Start() 
{
if (iStatus!=EClosed)
{
PddConfigure(iConfig);
PddStart();
iStatus=EActive;
}
}

Since the configuration of a port is specific to the underlying hardware, a call is made to the PDD to set up the required configuration:

void DComm16550::Configure(TCommConfigV01 &aConfig) 
{
// wait for uart to stop transmitting
Kern::PollingWait(FinishedTransmitting,this,3,100);
// Select the UART, clear bottom two bits
iUart->SelectUart();
TUint lcr=0;
switch (aConfig.iDataBits)
{
case EData8:
lcr = T16550UartIfc::K16550LCR_Data8;
break;
// ... etc
}
switch (aConfig.iStopBits)
{
case EStop1:
break;
case EStop2:
lcr |= T16550UartIfc::K16550LCR_Stop2;
break;
}
switch (aConfig.iParity)
{
case EParityEven:
lcr |= T16550UartIfc::K16550LCR_ParityEnable | T16550UartIfc::K16550LCR_ParityEven;
break;
// ... etc
}
iUart->SetLCR(lcr|K16550LCR_DLAB);
iUart->SetBaudRateDivisor(BaudRateDivisor[(TInt)aConfig.iRate]);
iUart->SetLCR(lcr);
iUart->SetFCR(T16550UartIfc::K16550FCR_Enable | T16550UartIfc::K16550FCR_RxReset | T16550UartIfc::K16550FCR_TxReset | T16550UartIfc::K16550FCR_TxRxRdy | T16550UartIfc::K16550FCR_RxTrig8);
}

Notice the use of the Kern::PollingWait() API. I don't want to change the port configuration while the UART is transmitting, as this may lead to lost or badly formed data. Since there can be at most 16 bytes of data outstanding (the size of my TX FIFO), then I may simply poll the FIFO until it is fully drained. But rather than waste power and CPU cycles doing this in a code loop, I would prefer that the current thread be put to sleep for a while before checking the status again. The Kern::PollingWait() API allows me to do this. It first checks the supplied polling function (FinishedTransmitting()) before sleeping the current thread for the specified poll interval (100 mS). This process is repeated until the poll period expires or the polling function returns ETrue. Be aware that if you are using this API (or any other method of polling which sleeps the current thread) then all the other drivers sharing the DFC thread will also be blocked until the poll is complete. You should take care to ensure that you don't inadvertently affect the operation of other drivers in the system - particularly if you are running within any of the standard kernel threads.

Similarly, it is the responsibility of the PDD to set up and enable any interrupt-specific configuration:

TInt DComm16550::Start() 
{
// if EnableTransmit() called before Start()
iTransmitting=EFalse;
iUart->SetIER(T16550UartIfc::K16550IER_RDAI | T16550UartIfc::K16550IER_RLSI | T16550UartIfc::K16550IER_MSI);
Interrupt::Enable(iInterruptId);
return KErrNone;
}

Once the hardware is successfully configured, DSimpleSerialChannel::DoRequest() determines the actual request to be handled (ERequestRead or ERequestWrite) and reads the corresponding request parameters from user space in exactly the same way as I described when I looked at the handling of synchronous requests in the previous section.

If you refer back to my original definition of the RSimpleSerialChannel API in Section 12.4.6.1, you can see that the message parameters consist of a TRequestStatus object, a 32-bit length (allocated on the user-side stack), and a pointer to a user-side descriptor. These are represented by the aStatus, a1 and a2 parameters respectively. Before starting the request, I must first obtain the length parameter from the user-side stack using the Kern::ThreadRawRead() API, because the stack will go out of scope once the request has been issued (see Section 12.4.6.1 for the implementation of SimpleSerialChannel::DoRead()). The client-side descriptor (passed in the a2 parameter) is extracted in a similar manner inside the call to InitiateRead()or InitiateWrite(). The implementation of these functions is not shown here, but the same principle applies - the pointer to the user-side data must be safely obtained before the request completes since the user-side stack may go out of scope once the request is issued (the observant reader will notice that in our example user-side function shown in Section 12.4.4 this will not happen because a call to User::WaitForRequest() is made immediately after issuing the request. However, this is a simplified use case - a real application would make use of the active object framework, so you must ensure that your device driver is designed to handle asynchronous requests without making any assumptions as to the behavior of the user-side application).

After the request parameters have been successfully read, the address of the internal request status is stored (in iRxStatus or iTxStatus) so the client can be signaled when the operation is complete. Finally, a call to InitiateRead(a1,len) orInitiateWrite(a1,len) which will start the asynchronous operation requested, eventually signaling the client from a DFC when it has completed.

Cancelling asynchronous requests

If the client cancels the asynchronous operation that I described in the previous section, the framework will issue a request with the special value of KMaxTInt(0x7FFFFFFF).

Looking back at Section 12.4.7.4 you can see how the request gateway function intercepted this request:

... else if (id==KMaxTInt) 
{
// DoCancel
DoCancel(m.Int0());
m.Complete(KErrNone,ETrue);
return;
}

The implementation of my DoCancel method is responsible for:

  • Determining which operation is to be cancelled (specified in the first message parameter)
  • Tidying up resources specific to the request being cancelled, and cancelling any outstanding DFCs or timers
  • Signaling the client that the operation has been cancelled.
void DSimpleSerialChannel::DoCancel(TInt aMask) 
{
TInt irq;
if(aMask&RSimpleSerialChannel::ERequestReadCancel)
{
iRxOutstanding=EFalse;
iNotifyData=EFalse;
iRxDesPtr=NULL;
iRxDesPos=0;
iRxLength=0;
iRxError=KErrNone;
iRxOneOrMore=0;
iRxCompleteDfc.Cancel();
iRxDrainDfc.Cancel();
iTimer.Cancel();
iTimerDfc.Cancel();
Complete(ERx,KErrCancel);
}
if(aMask&RSimpleSerialChannel::ERequestWriteCancel)
{
irq=DisableIrqs();
iTxPutIndex=0;
iTxGetIndex=0;
iTxOutstanding=EFalse;
iTxDesPtr=NULL;
iTxDesPos=0;
iTxDesLength=0;
iTxError=KErrNone;
RestoreIrqs(irq);
iTxCompleteDfc.Cancel();
iTxFillDfc.Cancel();
Complete(ETx,KErrCancel);
} }

Closing the device driver

When the client has finished with the device driver, an ECloseMsg will be dispatched to our message handler to allow the driver to tidy free up resources and perform other hardware-specific operations before the channel is finally destroyed (see Section 12.4.7.3):

void DSimpleSerialChannel::Shutdown() 
{
if (iStatus == EActive) Stop();
Complete(EAll, KErrAbort);
iRxDrainDfc.Cancel();
iRxCompleteDfc.Cancel();
iTxFillDfc.Cancel();
iTxCompleteDfc.Cancel();
iTimer.Cancel();
iTimerDfc.Cancel();
}

This is similar to the behavior I described in the previous section, when I talked about the cancellation of an asynchronous request. However, there are some fundamental differences:

  • The hardware interface is shut down, using ::Stop()
  • All outstanding requests are completed with KErrAbort.

Note also that I do not worry about clearing down any of my member data, since this will be the last message to be received before the channel is finally deleted.

Summary

In this section I have discussed the DLogicalChannel framework using a simple serial device driver as an example. I'll now conclude by discussing some of the differences between the EKA1 and EKA2 device driver frameworks.

Differences between EKA1 and EKA2

Before concluding this chapter, I will discuss the fundamental differences between the EKA1 and EKA2 device driver models. I do not intend to explain how to port an existing device driver from EKA1 to EKA2 - please refer to the Device Driver Kit (DDK) documentation, which is available to Symbian Partners, for a detailed explanation of how to do this.

The device driver model in EKA2 has not changed that much from EKA1. EKA1's model was based on the LDD and the PDD and shares such concepts as the LDD factory, the logical channel, the PDD factory and the physical channel with EKA2. The differences are in the detail of how the model is implemented.

The main change is in the way user-side requests are routed and handled kernel side. As you've seen, in EKA2 requests from user-side clients can be executed in the context of a DFC running in a separate kernel-side thread. This means that code running kernel-side can now block - in EKA1 this would have halted the system.

Changes to the user-side API

Both EKA1 and EKA2 use the RBusLogicalChannel class to provide the client-side API to a device driver. On EKA1, this is defined as follows:

class RBusLogicalChannel : public RHandleBase, public MBusDev
{
protected:
IMPORT_C TInt DoCreate(const TDesC& aDevice,const TVersion& aVer, const TDesC* aChan,TInt aUnit,const TDesC* aDriver, const TDesC8* anInfo,TOwnerType aType=EOwnerProcess);
IMPORT_C void DoCancel(TUint aReqMask);
IMPORT_C void DoRequest(TInt aReqNo,TRequestStatus& aStatus);
IMPORT_C void DoRequest(TInt aReqNo,TRequestStatus& aStatus,TAny* a1);
IMPORT_C void DoRequest(TInt aReqNo,TRequestStatus& aStatus,TAny* a1,TAny* a2);
IMPORT_C TInt DoControl(TInt aFunction); IMPORT_C TInt DoControl(TInt aFunction,TAny* a1);
IMPORT_C TInt DoControl(TInt aFunction,TAny* a1,TAny* a2);
IMPORT_C TInt DoSvControl(TInt aFunction);
IMPORT_C TInt DoSvControl(TInt aFunction,TAny* a1);
IMPORT_C TInt DoSvControl(TInt aFunction,TAny* a1,TAny* a2);
private:
TInt CheckBusStatus();
TInt DoCheckBusStatus(TInt aSocket);
};

This defines four separate types of device driver call:

  • RBusLogicalChannel::DoControl - perform a synchronous device driver function
  • RBusLogicalChannel::DoSvControl - perform a synchronous device driver function in the kernel server context
  • RBusLogicalChannel::DoRequest - initiate an asynchronous device driver operation
  • RBusLogicalChannel::DoCancel - prematurely terminate an asynchronous device driver operation.

As I described in Section 12.4.6.4, there is little distinction between these operations in EKA2, since all calls are routed to a single gateway function. However, to reduce the effort involved in porting a device driver from EKA1 to EKA2, we have maintained the binary compatibility of this interface (but not the source compatibility). Here's the EKA2 version of the RBusLogicalChannel class in full:

class RBusLogicalChannel : public RHandleBase 
{
public:
IMPORT_C TInt Open(RMessagePtr2 aMessage,TInt aParam,TOwnerType aType=EOwnerProcess);
IMPORT_C TInt Open(TInt aArgumentIndex, TOwnerType aType=EOwnerProcess);
protected:
inline TInt DoCreate(const TDesC& aDevice, const TVersion& aVer, TInt aUnit, const TDesC* aDriver, const TDesC8* anInfo, TOwnerType aType=EOwnerProcess, TBool aProtected=EFalse);
#ifndef __SECURE_API__
IMPORT_C TInt DoCreate(const TDesC& aDevice,const TVersion& aVer, const TDesC* aChan,TInt aUnit,const TDesC* aDriver, const TDesC8* anInfo,TOwnerType aType=EOwnerProcess);
#endif
IMPORT_C void DoCancel(TUint aReqMask);
IMPORT_C void DoRequest(TInt aReqNo,TRequestStatus& aStatus);
IMPORT_C void DoRequest(TInt aReqNo,TRequestStatus& aStatus,TAny* a1);
IMPORT_C void DoRequest(TInt aReqNo,TRequestStatus& aStatus,TAny* a1,TAny* a2);
IMPORT_C TInt DoControl(TInt aFunction); IMPORT_C TInt DoControl(TInt aFunction,TAny* a1);
IMPORT_C TInt DoControl(TInt aFunction,TAny* a1,TAny* a2);
IMPORT_C TInt DoSvControl(TInt aFunction);
IMPORT_C TInt DoSvControl(TInt aFunction,TAny* a1); IMPORT_C TInt DoSvControl(TInt aFunction,TAny* a1,TAny* a2); private:
IMPORT_C TInt DoCreate(const TDesC& aDevice, const TVersion& aVer, TInt aUnit, const TDesC* aDriver, const TDesC8* aInfo, TInt aType);
private:
// Padding for Binary Compatibility purposes
TInt iPadding1;
TInt iPadding2;
};

Note in particular that, on EKA1 any allocation or freeing of memory on the kernel heap must occur within the kernel server, hence the use of the DoSvControl() API. On EKA2, any device driver can now allocate and free memory on the kernel heap providing that either it runs in the context of a kernel-side thread, or, if it runs in the context of a user-side thread it enters a critical section before performing the heap operation by calling NKern::ThreadEnterCS(). However, to make porting a device driver from EKA1 to EKA2 easier we have maintained this API.

The EKA2 version of RBusLogicalChannel contains eight padding bytes to maintain binary compatibility with existing EKA1 clients. These bytes correspond to the member data contained within the now deprecated MBusDevClass from which the EKA1 version of RBusLogicalChannel was derived.

In summary, here's how these operations translate into kernel-side calls on EKA1 and EKA2: [--hamishwillee : Code below needs to be formatted in line with book]

User-side API – RBusLogicalChannel DoControl(aFn) DoControl(aFn, a1) DoControl(aFn, a1, a2) DoSvControl(aFn) DoSvControl(aFn,a1) DoSvControl(aFn,a1, a2) DoRequest(aFn, aStat) DoRequest(aFn, aStat, a1) DoRequest(aFn, aStat, a1, a2) DoCancel(aReqMask) EKA1–Kernel-side DLogicalChannel DoControl(aFn, 0, 0) DoControl(aFn, a1, 0) DoControl(aFn, a1, a2) DoControl(aFn, 0, 0) DoControl(aFn, a1, 0) DoControl(aFn, a1, a2) DoRequest(aFn, 0, 0) DoRequest(aFn, a1, 0) DoRequest(aFn, a1, a2) DoCancel(aReqMask) EKA2–Kernel-side DLogicalChannelBase Request(aFn, 0, 0) Request(aFn, a1, 0) Request(aFn, a1, a2) Request(aFn, 0, 0) Request(aFn, a1, 0) Request(aFn, a1, a2) Request(~aFn, &aStat, &A[0, 0]) Request(~aFn, &aStat,&A[a1, 0]) Request(~aFn, &aStat, &A[a1, a2]) Request(0x7FFFFFFF, aReqMask, 0)

This highlights a fundamental difference between the EKA1 and EKA2 device driver models - the kernel no longer stores the TRequestStatus pointers (aStat) on your behalf for pending asynchronous operations. There is no limit to the number of outstanding requests that can be handled by a device driver on EKA2, so it is your responsibility as a device driver writer to maintain a list of outstanding requests should you need this functionality.

Figure 12.11 Comparison of the EKA1 and EKA2 device driver models

Choice of device driver model

In EKA1, the functions DoControl(), DoRequest() and DoCancel() that you implemented in your logical channel class all ran in the context of the user-side thread. On EKA2, you have the choice of implementing a device driver based on DLogicalChannelBase (in which case requests run in the context of the client thread) or DLogicalChannel (in which case requests run in a DFC in the context of a kernel-side thread as shown in Figure 12.11).

When porting a device driver from EKA1 to EKA2, you may find it easier to use the functionality provided by the DLogicalChannel class, as this helps to avoid synchronization issues by handling all requests in the context of a single thread. If you follow the pattern I described in Section 12.4.7.4, you can simplify migration to the new device driver model by implementing your HandleMsg() to determine the type of request, and then call your existing DoControl(), DoCancel() and DoRequest() functions. Of course, you can now rename these, giving them any name you wish, as the names are no longer mandated by the device driver framework.

In EKA1, you could access user memory directly from your kernel-side code. Although this is exceedingly bad practice, it does work. In EKA2, you are unlikely to get away with it as the kernel may (depending on memory model) panic the system with KERN-EXEC 3). You must ensure that you follow the rules set out in Section 12.1.5.

More detailed API changes

In previous sections I have described the fundamental differences between the EKA1 and EKA2 device driver models, but this is in no way a definitive list. For a more detailed discussion of the changes between EKA1 and EKA2 device driver models, please refer to the DDK (available to Symbian Partners).

Summary

In this chapter I have introduced kernel extensions and device drivers. I have shown how these are managed by the kernel framework and might be implemented by a device driver provider using a simple serial driver as an example. Next I will take a look at the support that Symbian OS provides for peripherals such as MultiMediaCard and USB.

Licence icon cc-by-sa 3.0-88x31.png© 2010 Symbian Foundation Limited. This document is licensed under the Creative Commons Attribution-Share Alike 2.0 license. See http://creativecommons.org/licenses/by-sa/2.0/legalcode for the full terms of the license.
Note that this content was originally hosted on the Symbian Foundation developer wiki.

This page was last modified on 30 May 2013, at 04:40.
110 page views in the last 30 days.

Was this page helpful?

Your feedback about this content is important. Let us know what you think.

 

Thank you!

We appreciate your feedback.

×