The
.NET Micro Framework is a sleek and robust way of bringing high-level functionality, quickly and easily, to low-specification platforms. A developer can enable a standard suite of features on a device using the
Micro Framework Porting Kit. But what do you do when the functionality you want
isn't supported in the Porting Kit?
One of the coolest new features of Micro Framework version 3.0 is
Interop - the ability to extend the Micro Framework's functionality, adding features with C# interfaces in a fairly straightforward way. As the .NET MF is still young, this is extremely useful for filling in the blanks in the Micro Framework specification. At Adeneo we've already used interop to implement real-time timekeeping, power management, audio playback, and other features not natively supported by the MF.
In the following piece, I'll walk through the process of adding an interop feature to an existing Micro Framework port, step by step - although Microsoft
recommends a method using their SolutionWizard tool, we've found this to be pretty flimsy. I'll use the example of a GPIO-driven LED feature, which is fairly simple from the driver perspective, but should be suitable for demonstrating the vast possibilities that interop gives us.
Some of what I describe below is by convention, and not strictly necessary, so if you're wondering about a particular step - feel free to experiment.
Step 0: The Starting PointAs is hopefully obvious, this walkthrough assumes you've already got a piece of MF-compatible hardware, as well as
Visual Studio 2008 SP1 and the
Micro Framework SDK 3.0 (the SDK is freely available
from Microsoft) for creating Micro Framework projects.
The walkthrough additionally assumes that you have access to the .NET Micro Framework
Porting Kit, version 3.0, as well as the ability to
build ports within the kit. (I will not describe the build process, as the kit's included documentation does a sufficient job of this.) It also assumes, for the sake of example, an LED driver which already exists in C++ code, and can be compiled/linked into your port. I'll assume that this driver has the following example functions:
// enable the selected pin for LED control
BOOL LED_Initialize(GPIO_PIN PinLED);
// return the pin to general availability
BOOL LED_Uninitialize(GPIO_PIN PinLED);
// turn the LED on (when passed TRUE) or off (when passed FALSE)
void LED_SetState(GPIO_PIN PinLED, BOOL TurnOn);
// return TRUE if the LED is on, or FALSE if the LED is off
BOOL LED_GetState(GPIO_PIN PinLED);
For brevity I will not go into detail on how to implement a GPIO-driven LED driver, the mechanics of which should be pretty straightforward.
Step 1: Create Your C# Interface (Library)The process of implementing interop starts at the top: designing the C# class you'll use as the high-level interface. Open Visual Studio, and create a new project of type
Micro Framework ->
Class Library. For organizational purposes, I suggest that you create this project in your
PortingKit\Solutions\[Your Solution]\ManagedCode\ folder, although you
can put it wherever you want.
Within this library, name and describe the C# functions you'll use to access your driver. Note well that a later step compiles a
checksum of this library into your project, and so you should try to finalize this interface first, else you'll have to update
more files later.
Here's an example for C# control of an LED, starting from a Library project named
LEDInterop:
using System;
using Microsoft.SPOT;
using Microsoft.SPOT.Hardware; // for Cpu.Pin type
using System.Runtime.CompilerServices;
namespace LEDInterop
{
public class LED
{
public LED(Cpu.Pin PinLED)
{
_pin = (UInt32)PinLED;
LED_Enable(_pin);
}
~LED()
{
LED_Disable(_pin);
}
public bool IsOn
{
get
{
if (LED_Get(_pin) == 1)
return true;
else
return false;
}
set
{
if (value)
LED_Set(_pin, 1);
else
LED_Set(_pin, 0);
}
}
//--//
// this is the LED object's associated pin
private UInt32 _pin;
[MethodImpl(MethodImplOptions.InternalCall)]
private extern void LED_Enable(UInt32 PinLED);
[MethodImpl(MethodImplOptions.InternalCall)]
private extern void LED_Disable(UInt32 PinLED);
[MethodImpl(MethodImplOptions.InternalCall)]
private extern Byte LED_Get(UInt32 PinLED);
[MethodImpl(MethodImplOptions.InternalCall)]
private extern void LED_Set(UInt32 PinLED, Byte State);
}
}
There are a few things to note about this C# code sample:
• This example uses the
Cpu.Pin type, which requires the
Microsoft.SPOT.Hardware reference. Right-click on
References in the Solution Explorer and select
Add Reference..., then click the
.NET tab and choose
Microsoft.SPOT.Hardware - without this, the LEDInterop project will not build!
• "Interop" functions - that is, functions which are defined in C++, but accessible in C# - are marked with this attribute:
[MethodImpl(MethodImplOptions.InternalCall)]
These properties are inherited from
System.Runtime.CompilerServices and, together with
extern, indicate that the function is implemented in C++.
• The above sample has some forms of artificial abstraction (
bool to
Byte,
Cpu.Pin to
UInt32) that may seem unintuitive. Interop functions can only accept parameters of, and return values in, a limited number of types. Per Microsoft's documentation, here is a table of the allowed types and how they link up, e.g. a
Byte in the CLR (C#) is the same as a
UINT8 in the Porting Kit (C++):
C# Type | C++ Type | C++ Array Type |
System.Byte | UINT8, BYTE | CLR_RT_TypedArray_UINT8 |
System.UInt16 | UINT16 | CLR_RT_TypedArray_UINT16 |
System.UInt32 | UINT32 | CLR_RT_TypedArray_UINT32 |
System.UInt64 | UINT64 | CLR_RT_TypedArray_UINT64 |
System.SByte | INT8 | CLR_RT_TypedArray_INT8 |
System.Int16 | INT16 | CLR_RT_TypedArray_INT16 |
System.Int32 | INT32 | CLR_RT_TypedArray_INT32 |
System.Int64 | INT64 | CLR_RT_TypedArray_INT64 |
System.Single | float | CLR_RT_TypedArray_float |
System.Double | double | CLR_RT_TypedArray_double |
System.String | LPCSTR | Not Supported |
The C++ array types can be accessed with subscripts (
Array[i]), but otherwise do not behave quite like normal C arrays. See
PortingKit\CLR\Include\TinyCLR_Interop.h for more information on these custom types.
• You can build this project in Debug or in Release mode; either is fine. The rest of this example will assume you've remained in Debug mode.
Step 2: Insert Stubbed Functionality Into Your PortOnce your C# interface is well designed, you're ready to move back down to C++. Open your project's
Properties (by right-clicking on the project, or double-clicking
Properties in the Solution Explorer panel) and click the
.NET Micro Framework tab. Below your deployment options - which don't matter at all for your Library - is a checkbox labeled
Generate native stubs for internal methods. Check this box, and Build your project; this will construct stubbed-out C++ files for your interop interface, in the directory named below the checkbox - by default, in a
Stubs sub-folder within your project folder. It will also, of course, generate the binary form of your C# class for a reference assembly.
First, copy the compiled binary files (your project's
bin folder) into your Porting Kit Solution. I suggest putting them in the base project folder, e.g.
PortingKit\Solutions\[Your Solution]\ManagedCode\LEDInterop\. These files are required for building your port, and for Micro Framework applications to use your new functionality.
Open the stubbed driver folder, and you should see several generated C++, H, and Project files. Following the example above,
•
dotNetMF.proj — project file for the generated code
•
LEDInterop.cpp — properties of your class
•
LEDInterop.featureproj — project file for the C# feature
•
LEDInterop.h — methods of your class
•
LEDInterop_LEDInterop_LED.cpp — implementation of the C#/C++ link
•
LEDInterop_LEDInterop_LED.h — definitions for this implementation
•
LEDInterop_LEDInterop_LED_mshl.cpp — CLR wrappers for your interop functions
Take this folder and copy its contents into your Solution as well - I'd suggest
PortingKit\Solutions\[Your Solution]\DeviceCode\LED\. You'll need to edit two files to add the driver to your port.
•
LEDInterop.featureproj - You'll need to change two paths in this file to accomodate your port. The tag that refers to an
MMP_DAT_CreateDatabase path should point to the
.pe file from your Library project:
<MMP_DAT_CreateDatabase Include="$(SPOCLIENT)\Solutions\[Your Solution]\ManagedCode\LEDInterop\bin\Debug\LEDInterop.pe" />
You also want to point the
RequiredProjects path to your stubbed files:
<RequiredProjects Include="$(SPOCLIENT)\Solutions\[Your Solution]\DeviceCode\LED\dotNetMF.proj" />
•
TinyCLR.proj - You also need to add this library and feature to your CLR project, in
PortingKit\Solutions\[Your Solution]\TinyCLR\TinyCLR.proj. Open up this file, and near the top should be a list of
.featureproj Import references: add yours in here.
<Import Project="$(SPOCLIENT)\Solutions\[Your Solution]\DeviceCode\LED\LEDInterop.featureproj" />
Note that this Import must be
above the Import for
$(SPOCLIENT)\tools\targets\Microsoft.SPOT.System.Interop.Settings - otherwise, your project will not build!
You must also add an inclusion statement for the project that builds your new interop library, and the library itself. (The name of your library is tagged as
AssemblyName in the generated
dotNetMF.proj file.) Somewhere in the list of included libraries for TinyCLR, add the project and driver library:
<ItemGroup>
<RequiredProjects Include="$(SPOCLIENT)\Solutions\[Your Solution]\DeviceCode\LED\dotNetMF.proj" />
<DriverLibs Include="LEDInterop.$(LIB_EXT)" />
</ItemGroup>
That's it! Now if you rebuild and flash your port, it should include your new interop feature. You can verify this by checking the output log at CLR startup - an assembly reference of your feature's name (in our case,
LEDInterop) should appear in the list of included assemblies.
Step 3: Implement Your FeatureNow that the pieces are in place, it's time to link them together. One of the C++ files you generated previously (for the example,
LEDInterop_LEDInterop_LED.cpp) serves as a bridge between your C# interface and your C++ codebase. This file contains skeleton functionality for the C# functions you defined as
InternalCall, and which you will now fill in.
Following the example, here's how you might fill in this file:
#include "LEDInterop.h"
#include "LEDInterop_LEDInterop_LED.h"
using namespace LEDInterop;
void LED::LED_Enable( CLR_RT_HeapBlock* pMngObj, UINT32 param0, HRESULT &hr )
{
// param0 == PinLED
if(LED_Initialize((GPIO_PIN)param0) == FALSE)
hr = CLR_E_INVALID_ARGUMENT;
}
void LED::LED_Disable( CLR_RT_HeapBlock* pMngObj, UINT32 param0, HRESULT &hr )
{
// param0 == PinLED
if(LED_Uninitialize((GPIO_PIN)param0) == FALSE)
hr = CLR_E_INVALID_ARGUMENT;
}
UINT8 LED::LED_Get( CLR_RT_HeapBlock* pMngObj, UINT32 param0, HRESULT &hr )
{
// param0 == PinLED
if(LED_GetState((GPIO_PIN)param0) == TRUE)
return 1;
else
return 0;
}
void LED::LED_Set( CLR_RT_HeapBlock* pMngObj, UINT32 param0, UINT8 param1, HRESULT &hr )
{
// param0 == PinLED
// param1 == TurnOn
if(param1 == 1)
LED_SetState((GPIO_PIN)param0, TRUE);
else
LED_SetState((GPIO_PIN)param0, FALSE);
}
The trickiest thing about this file is the function parameters. Note that the C# parameters are renamed when they come down to this level, in the form
param0,
param1, and so on. For this reason it's wise to put some documentation in this file explaining what each of the parameters means.
With this new implementation, build your port, and flash it again. Now everything is assembled.
Step 4: Use It!To use your interop feature in a C# application, you'll need to include the library reference. Open a new or existing Micro Framework application project. Right-click on
References in the Solution Explorer and select
Add Reference..., then click the
Browse tab and browse for the output of your interop library - for the example,
PortingKit\Solutions\[Your Solution]\ManagedCode\LEDInterop\bin\Debug\LEDInterop.dll - and add this Reference to your project. Now, you have access to the interop class you defined in Step 1.
Here's an example Micro Framework console application to control two LEDs:
using System;
using Microsoft.SPOT;
using Microsoft.SPOT.Hardware;
// NOTE: you'll also need a reference to your
// hardware assembly, to use its pins!
using LEDInterop;
namespace MFConsoleApplication
{
public class Program
{
public static void Main()
{
LED led1 = new LED(Pins.GPIO_PORT_A_00);
LED led2 = new LED(Pins.GPIO_PORT_B_01);
led1.IsOn = true;
if(led2.IsOn)
Debug.Print("led2 is on!");
else
Debug.Print("led2 is off!");
}
}
}
Controlling hardware through intuitive C# interfaces is what the .NET Micro Framework is all about, and with a little practice, it's fairly easy to use interop features to push your platform beyond the standard Micro Framework: CODEC control, PWM, and arbitrary GPIO peripherals are only a few examples. Interop opens the door to a whole world of new Micro Framework-driven applications.
-
TS
NOTES• Properties of your C# class - such as
_pin in our
LEDInterop example - will be made accessible to your C++ code in the stub-generation process. If you create a property of a type that isn't compatible with C++ (per the table in Step 1), your C++ project will not build; but if you aren't using this property in C++, you can comment out the declaration for it in the
.h file.
• As remarked in Step 2, the order in which you Import
.featureproj references matters. Make sure you place them correctly!
• Changing the layout of your interop library class will require that you update all of your generated C++/H files, as they contain, among other things, a checksum of the class's signature. Be sure you update all of the generated files (and the binary output!) every time you change your interop library.
• The
.pe binary file will only contain the aforementioned checksum value if the
Generate native stubs for internal methods box is checked. If you build your class library without this checked, your port will be unable to accept deployed applications!