Using Inheritance and the .NET Compact Framework for Extensible Stream Interface Driver Access

.NET Compact Framework 1.0
 

Chris Tacke
Co-Founder
OpenNETCF.org

March 2004

Download Sample.


Summary: Learn to create a generic framework that can be used to access hardware specific drivers.

Contents

Introduction
Stream Interface Drivers
A Generic Managed Shim
Inheriting the StreamInterfaceDriver Class
Using the Derived Class
A Concrete Example
Conclusion
P/Invoke Resources

Introduction

The .NET Compact Framework, when coupled with a managed development language like VB.NET or C#, is an amazingly powerful enabler for rapid application development. Software engineers can quickly architect, code and deploy sophisticated embedded applications faster than ever before and since they run under the .NET Common Language Runtime (CLR) they are more stable.

The embedded development world has a very large amount of available hardware that can be interfaced with but, unfortunately, relatively few of these devices have a managed code interface. No matter how robust or mature the .NET Compact Framework gets, there will always be hardware that a solution needs to use that cannot be directly used from managed code.

To handle this need for flexibility, a generic framework can be used to reduce code redundancy, improve application quality and increase code reusability.

Stream Interface Drivers

A very common driver architecture used in Windows CE (and the Windows desktop OSes for that matter) is known as a stream interface driver. Stream interface drivers are extremely versatile. They can be used for most any type of device that can be a data source, a data sink or both. They can be loaded at boot, or dynamically at a later time, and they are accessed through the system's device manager using a simple set of File APIs, which we will look at in more detail later.

Figure 1 shows how a stream interface driver generally fits into the overall architecture of a platform.

Figure 1 - Stream Interface Driver Architecture

In the upper left of the figure is a block described as a "Driver Consumer" which is part of a managed application. It is this block that we're most interested in, and on which we'll focus.

We will discuss the architecture of stream interface drivers only as it relates to how it affects our managed assemblies. For more information on the nuts and bolts of stream interface drivers, how to develop them and how to deploy and activate them, I direct you to MSDN and the samples installed with Platform Builder.

There are some general commonalities among all stream interface drivers. First, they all have a three-character prefix and an index which can be used to identify and use the driver. One of the most common stream interface driver prefixes is for serial communication drivers: COM. Add to that the index and you get the familiar COM1:, COM2: and COM3 "port" names. The prefixes have few restrictions other than they must be 3 characters, none of which can be special characters, and the first cannot be a numeral.

Stream interface drivers are requires to publicly export a set of functions (think of a C# or VB class interface). The interface is a "contract" that guarantees that an implementation has certain entry points. Aside from the obvious DllMain entry point, stream interface drivers must export the following: XXX_Init, XXX_Open, XXX_Read, XXX_Write, XXX_DeviceIoControl, XXX_Seek, XXX_Close, XXX_Deinit, XXX_PowerUp and XXX_PowerDown where "XXX" in the function name is the three-character prefix.

The index number(s) for the driver are assigned when the driver loads, and for our purposes that's really all we need to know.

A Generic Managed Shim

As mentioned earlier, the exported functions of a stream interface driver are all called by the OS's device manager. Some of the functions, like XXX_Init and XXX_Deinit are called automatically by the device manager. All of the others are accessed through unmanaged File APIs.

Since there are no direct managed methods that map to the File APIs, all of them must be accessed through Platform Invocation, or P/Invoke.

P/Invoking is not always straightforward, and it's beyond the scope of this article to delve into how it all works, but to really be able to make use of a stream interface driver "shim" or wrapper class, you will need to have a very solid understanding of data marshaling across the managed/unmanaged boundary through P/Invoke. For more information on the details of P/Invoking, I highly recommend reading the articles referenced at the end of this article.

To abstract the P/Invoke calls and do some API-level error checking, we need to generate a managed class that wraps the P/Invokes. At this point we need to make a development decision: do we A) create a class that wraps the P/Invoke calls for the driver we're using or B) create a more generic class that can't actually be used directly, but that allows us to quickly generate derived classes that have a common function set.

Obviously the point of this article is to advocate the latter, and for good reason: not only does it make your code more extensible, it prevents duplication and cut-and-paste coding, which leads to error propagation and a nightmare for maintenance.

So what we need is a simple abstract class (MustInherit for you VBers) that wraps the P/Invoke calls common to all stream interface drivers and then exposes the shim methods as protected. This means that all classes that are derived from our shim class will be able to access the stream interface driver through some shim classes, but creators or consumers of the derived classes will only get access to the driver that the derived class exposes.

[C#]
public abstract class StreamInterfaceDriver2
{
    protected void Write(byte[] data)
    {
        // P/Invoke driver function here
    }
    // other P/Invoke Wrappers
}

[VB]
Public MustInherit Class StreamInterfaceDriver
    Protected Sub Write(ByVal data As Byte())
        ' P/Invoke driver function here
    End Sub

    ' other P/Invoke Wrappers
End Class

While this may sound a bit confusing or convoluted at this point, let's look at some diagrams to help clarify how it will work.

Figure 2 - Wrapping a Stream

Take a look at Figure 2. On the right we have the functions exposed by the stream interface driver DLL itself. Access to these functions is through P/Invoking the File APIs in the middle of the diagram. Our base shim class, let's call it StreamInterfaceDriver, is represented by the column on the left. It will control all access to the driver.

Inheriting the StreamInterfaceDriver Class

Next, we would create a derived class, let's call it MyDriverClass that inherits from StreamInterfaceDriver like so:

[C#]
public class MyDriverClass : StreamInterfaceDriver
{
    // implement public classes that call the 
    // protected classes of StreamInterfaceDriver
    public void Send(byte[] data)
    {
        base.Write(data);
    }
}
[VB]
Public Class MyDriverClass
    Inherits StreamInterfaceDriver

    ' implement public classes that call the 
    ' protected classes of StreamInterfaceDriver
    Public Sub Send(ByVal data As Byte())
        Call MyBase.Write(data)
    End Sub

End Class

By making MyDriverClass abstract (MustInherit) and making all of its methods protected, we control all data flow to and from the driver. This allows the derived class to expose more "friendly" methods to consumers and do any data massaging, validation or serialization without the consumer needing to know about the underlying driver requirements. Figure 3 gives a general diagram of this flow.

Figure 3- Data flow is controlled from the consumer through to the driver

Using the Derived Class

Figure 4 is another graphical representation of how our derived class (MyDriverClass in our sample) acts as the "gatekeeper" between consumer applications (or class libraries) and the driver.

Figure 4 – Applications use the derived class to access the driver

In code, it would look something like this:

[C#]
public class MyApp
{
    // a class-scope driver class
    MyDriverClass m_driver = new MyDriverClass();

    public void Foo()
    {
        // create the data somehow
        byte[] myData = GetData();

        // send data through our driver
        m_driver.Send(myData);
    }
}

[VB]
Public Class MyApp
    ' a class-scope driver class
    Private m_driver As New MyDriverClass()

    ' protected classes of StreamInterfaceDriver
    Public Sub Foo()
        Dim myData As Byte()
        ' create the data somehow
        myData = GetData()

        ' send data through our driver
        m_driver.Send(myData)
    End Sub
End Class

As you can see, the consumer (MyApp in this case) creates an instance of our shim (MyDriverClass) and calls methods on it. Those methods in turn call protected methods in the base StreamInterface driver class, which in turn P/Invoke into the driver.

A Concrete Example

So now that we've got the general idea of how we can abstract away the complexities of stream interface drivers into a more friendly and extensible managed class, let's look at an example of a real-world implementation.

First, let's get some background.

In many embedded environments, especially the automotive and medical device industry, controllers need to communicate with sensors or other devices that are nearby, but not directly on the same board or in the same enclosure as the controller. Often the environments can have a lot of electromagnetic (EM) noise. To handle this, a 2-wire communication bus is often used called a Controller Area Network bus or simply CAN bus.

The details of how a CAN bus works and exactly where it is used are well beyond the scope of this article. For those readers who just have to know more, I suggest a Web search. For our purposes, all we need to know about the bus itself is that it's a way of sending and receiving data from a remote device, much like serial communication.

Without going into too much depth on the communication protocol itself, here is a summary:

CAN communication is a different than serial communication in that many devices can be attached to the bus at one time and data is passed in set length packets called Messages. Any device on the bus can send or receive Messages and a "listener" has a mechanism to filter messages so that it receives only messages it is interested in.

There is no "standard" CAN driver for Windows CE, so the API for using a CAN bus is unique to the system on which you're running. For this example I used the AGX from Applied Data Systems. It is a PXA-255 based SBC with a Philips SJA1000 CAN Controller chip.

The chip is controlled largely by using DeviceIoControl calls to the driver and passing it structs data blocks as the Messages.

To use the built-in driver I first created a MessageObject class for the Message data. The MessageObject provides a user-friendly interface for the managed code developer and an implicit operator to convert it to bytes so the P/Invoke calls can use it.

Next I created a class called ADS.IO.CAN that inherited the StreamInterfaceDriver. Figure 5 gives a general mapping of how an application can use the ADS.IO.CAN class to talk with the CAN controller.

Figure 5- Using the StreamInterfaceDriver to create a CAN driver class

The CAN class has a few simple properties for bus speed, filtering, resetting the CAN chip and reading the status register. It also contains a Write method that takes a MessageObject class and writes it to the bus and separate threads that read the bus and fires managed Events when data becomes available or an error occurs. You can download the full source code to see all of the implementation details, but let's look at the ResetChip method as an example:

[C#]
public void ResetChip()
{
    try
    {
        DeviceIoControl((int)CAN_IOCTL.ResetChip, null, null);
    }
    catch(Exception)
    {
        throw new Exception("Unable to reset SJA1000 CAN chip: " + 
          Marshal.GetLastWin32Error());
    }
}

[VB]
Public Sub ResetChip()
    Try
        DeviceIoControl(CType(CAN_IOCTL.ResetChip, Integer), _
          Nothing, Nothing)
    Catch
        Throw New Exception("Unable to reset SJA1000 CAN chip: " + _
          Marshal.GetLastWin32Error())
    End Try
End Sub

You can see that it is simply a wrapper around the protected DeviceIoControl method in the StreamInterfaceDriver class. It passes in the IOCTL code (which I implemented as an enumeration) and wraps the call with a try/catch block.

Conclusion

Stream Interface Drivers are a staple of Windows CE platforms and will remain so for the foreseeable future. Since they typically interface with optional or custom hardware, the .NET Compact Framework itself cannot possible provide classes for every possible implementation, and in the near term it's unlikely that device OEMs will always be providing managed classes for their drivers.

This fact does not mean that all access to drivers needs to be from-scratch Platform Invoke work. By taking advantage of the features of object-oriented programming, we can create a framework that allows rapid driver shim class creation and minimizes code duplication.

Using the OpenNETCF.IO.StreamInterfaceDriver class gives you a jump start on creating those shim classes and lets you concentrate your efforts where they should be – on your application.

P/Invoke Resources

An Introduction to P/Invoke and Marshalling on the Microsoft .NET Compact Framework, March 2003, Jon Box and Dan Fox, Quilogy
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnnetcomp/html/netcfintrointerp.asp

Advanced P/Invoke on the Microsoft .NET Compact Framework, March 2003, Jon Box and Dan Fox, Quilogy
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnnetcomp/html/netcfadvinterop.asp

Creating a P/Invoke Library, January 2004, Geoff Schwab, Excell Data Corporation
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnnetcomp/html/PInvokeLib.asp

Show: