(Update: just posted a new chapter of my “journey” inside .NET MF, this time about interrupt handling. You can find it here)
If you’re like me, since the first time you saw a .NET Micro Framework application running turning on and off an LED you wondered about how this “magic” interaction between managed code, produced by C#/VB.NET compiler, and native code, directly run by MCU, actually happened. I am not saying that inner working couldn’t be guessed at all, probably without being very wrong, but we have to be honest we can’t actually tell we “know” how things really work without first taking a look under the hood, following step-by-step application stream flowing. Indeed, Visual Studio step-by-step debugging hides the most interesting stuff, since stepping-into (F11’ing) while being on following line we just see LED turning on (or off), skipping over the exact point where port switching actually happens:
OutputPort led = new OutputPort(Pins.GPIO_PIN_D_12, false);
led.Write(true);
In this post I’ll try to show you with greater detail how we can follow application flow related to this code snippet, going from IL/CIL inside .NET assembly generated by C# compilation to actual set/reset of the bit inside memory location that is mapped to GPIO peripheral the LED is connected to. From now on I’ll refer to .NET Micro Framework porting targeted to STM32F4 microcontroller that Oberon staff just published on Codeplex (http://netmf4stm32.codeplex.com/). It is worth to mention that in these days such MCU model is under the spotlight of whole .NET MF world, since both GHI and Secret Labs embedded it on their new product lines (FEZ-Cerberus, FEZ-Cerb40 and FEZ-Cerbuino by GHI and Netduino Go by Secret Labs). Even more, Oberon and CSA just announced their brand new Mountaneer platform based on same porting we’re talking about (http://www.mountaineer.org/netmf-for-stm32/).
Tools needed
Besides Visual Studio 2010 and .NET Micro Framework SDK 4.2 RTM QFE1 for managed application building, I’ll show you how to use following tools to go deeper inside .NET MF:
- .NET Micro Framework Porting Kit 4.2 RTM QFE1
- Visual C++ 2010 (i.e. Visual C++ plugin for Visual Studio 2010), needed to build all Porting Kit sources targeted to Windows and not to microcontroller
- Oberon porting for STM32F4, available on Codeplexsite (beta 1 version, at this time) as a merging update for stock Porting Kit file structure
- USB drivers for Oberon porting, needed for debugging/deployment features
- C/C++ Toolchain for ARM. At this time only Keil MDKand RVDS are supported but GCC support will probably follow soon
- An IDE for native ARM debugger (for example GDB from GNU toolchain). Technically an IDE is not mandatory, but will make debugging experience much, much easier. My favorite IDE is Crossworks by Rowley Associates: it is smart, fast, flexible, good-looking and quite inexpensive for personal use
- A developer board based on STM32F4 MCU. My favorite one is Discovery4, by ST Microelectronics itself. Besides a powerful STM32F407VGT6 (Z revision, despite first boards had an “A” revision built-in) with 1MB flash and 192Kb RAM, it features an ST-LINK/V2 SWD debugger (with 32/64 bit certified Windows driver) that you can use to debug external boards too, USB OTG/Device port, 3-axis accelerometer, audio sensor with omni-directional mic, audio DAC with built-in class “D” amplifier, 8 LEDs (4 of which usable as user LEDs), user push-button, reset button, 2-side headers already soldered on-board for all I/Os and...sold at only €12.77 on Mouser!!!
- ST-LINK utility application for firmware (i.e. binary .hex files generated by porting building) downloading (beware: rev Z MCU will need at least a patched 2.2.6 version, when official released version at this time is 2.2)
Building porting solution and flashing output binary files
Let’s have a look at how to prepare all you need to begin your “explorative” debug session:
- Install .NET MF Porting Kit and merge all files coming with Oberon porting (downloaded from Codeplex site) into Porting Kit folders structure, overwriting all preexistent files. As alternative, you can use an Hg client (such as TortoiseHG) to clone Codeplex project store and manage source updates through that source control system
- Open an administrative command prompt inside .NET MF Porting Kit main folder (e.g. “C:\MicroframeworkPK_4_2”) and set environment variables using setenv_XXX batch file. For example “setenv_MDK4.12.cmd c:\Keil\ARM”)
- Start a debug build of “Discovery4” solution, invoking msbuild tool as follows: “msbuild /p:flavor=DEBUG;memory=FLASH Solutions\Discovery4\dotNetMF.proj”. Once finished, you should have 3 binary image files into “X:\MicroFrameworkPK_v4_2\BuildOutput\THUMB2FP\MDK4.12\le\FLASH\debug\Discovery4\bin” folder: TinyBooter.hex, ER_CONFIG and ER_FLASH (latest 2 are actually inside “tinyclr.hex” subfolder)
- Using ST-LINK utility (after having connected USB cable to SWD/ST-LINK debugger board port, the one on the opposite side of audio jack, just to be clear), open the connection to target board (be careful: with Z rev. MCUs you’ll need a patched 2.2.6 version of ST-LINK utility), run an “erase chip” command and soon after download TinyBooter.hex file intto MCU flash. Such image file will be placed at 0x8000000 address, .NET Micro Framework entry-point for Oberon STM32F4 porting
- Reset board (either with reset push-button or power-cycling) and observe TinyBooter running: if everything went fine, Windows will detect a new USB device and ask for drivers. Install the ones you downloaded from Codeplex project site and double-check all is working by launching MFDeploy tool and running a “Ping” command to detected USB .NET MF Device (VID 0483 and PID A08F, with “STM32F4 Test” device name). TinyBooter should reply with a special data packet that MFDeploy will “intercept” by appending “TinyBooter” to log window
- Clicking on “Browse” button on MFDeploy, select both ER_FLASH and ER_CONFIG files inside “X:\MicroFrameworkPK_v4_2\BuildOutput\THUMB2FP\MDK4.12\le\FLASH\debug\Discovery4\bin\tinyclr.hex” folder, then clicjk on “Deploy” button. Flashing procedure should take not longer than 10-15 seconds, rebooting automatically once finished
- If everything went well, running a new “Ping” request in MFDepoly we should have back an amazing “TinyCLR” from device: we’re ready to go!
Sample managed application
We’ll use a simple managed application as an entry-point into .NET MF core. Such application will switch on and then off each of 4 user on-board LEDs, waiting 250ms before every toggling. Such LEDs are connected to pins 12, 13, 14, 15 of MCU Port D. Mapping between symbolic names (Pins.GPIO_PIN_D_X) and underlying memory register address offset can be enforced by including CPU.cs source file, located in “X:\MicroFrameworkPK_v4_2\DeviceCode\Targets\Native\STM32F4\ManagedCode\Hardware”. There we’ll find a bunch of declarations similar to “public const Cpu.Pin GPIO_PIN_D_12 = (Cpu.Pin)60;”.
Sample application complete code listing is:
using System;
using Microsoft.SPOT;
using Microsoft.SPOT.Hardware;
using Microsoft.SPOT.Hardware.STM32F2;
using System.Threading;
namespace TestDiscovery4Board
{
public static class Program
{
static OutputPort _led1 = new OutputPort(Pins.GPIO_PIN_D_12, false);
static OutputPort _led2 = new OutputPort(Pins.GPIO_PIN_D_13, false);
static OutputPort _led3 = new OutputPort(Pins.GPIO_PIN_D_14, false);
static OutputPort _led4 = new OutputPort(Pins.GPIO_PIN_D_15, false);
public static void Main()
{
Debug.Print("Started!");
while (true)
{
_led1.Write(!_led1.Read());
Thread.Sleep(250);
_led2.Write(!_led2.Read());
Thread.Sleep(250);
_led3.Write(!_led3.Read());
Thread.Sleep(250);
_led4.Write(!_led4.Read());
Thread.Sleep(250);
}
}
}
}
After having built our sample project, take a look at decompiled IL/CIL byte-code, using Reflector or ILDASM tool (built-in in standard .NET Framework). Here it is Main() method byte-code:
.method public hidebysig static void Main() cil managed
{
.entrypoint
// Code size 166 (0xa6)
.maxstack 3
.locals init ([0] bool CS$4$0000)
IL_0000: nop
IL_0001: ldstr "Started!"
IL_0006: call void [Microsoft.SPOT.Native]Microsoft.SPOT.Debug::Print(string)
IL_000b: nop
IL_000c: br IL_009f
IL_0011: nop
IL_0012: ldsfld class [Microsoft.SPOT.Hardware]Microsoft.SPOT.Hardware.OutputPort TestDiscovery4Board.Program::_led1
IL_0017: ldsfld class [Microsoft.SPOT.Hardware]Microsoft.SPOT.Hardware.OutputPort TestDiscovery4Board.Program::_led1
IL_001c: callvirt instance bool [Microsoft.SPOT.Hardware]Microsoft.SPOT.Hardware.Port::Read()
IL_0021: ldc.i4.0
IL_0022: ceq
IL_0024: callvirt instance void [Microsoft.SPOT.Hardware]Microsoft.SPOT.Hardware.OutputPort::Write(bool)
IL_0029: nop
IL_002a: ldc.i4 0xfa
IL_002f: call void [mscorlib]System.Threading.Thread::Sleep(int32)
IL_0034: nop
IL_0035: ldsfld class [Microsoft.SPOT.Hardware]Microsoft.SPOT.Hardware.OutputPort TestDiscovery4Board.Program::_led2
IL_003a: ldsfld class [Microsoft.SPOT.Hardware]Microsoft.SPOT.Hardware.OutputPort TestDiscovery4Board.Program::_led2
IL_003f: callvirt instance bool [Microsoft.SPOT.Hardware]Microsoft.SPOT.Hardware.Port::Read()
IL_0044: ldc.i4.0
IL_0045: ceq
IL_0047: callvirt instance void [Microsoft.SPOT.Hardware]Microsoft.SPOT.Hardware.OutputPort::Write(bool)
IL_004c: nop
IL_004d: ldc.i4 0xfa
IL_0052: call void [mscorlib]System.Threading.Thread::Sleep(int32)
IL_0057: nop
IL_0058: ldsfld class [Microsoft.SPOT.Hardware]Microsoft.SPOT.Hardware.OutputPort TestDiscovery4Board.Program::_led3
IL_005d: ldsfld class [Microsoft.SPOT.Hardware]Microsoft.SPOT.Hardware.OutputPort TestDiscovery4Board.Program::_led3
IL_0062: callvirt instance bool [Microsoft.SPOT.Hardware]Microsoft.SPOT.Hardware.Port::Read()
IL_0067: ldc.i4.0
IL_0068: ceq
IL_006a: callvirt instance void [Microsoft.SPOT.Hardware]Microsoft.SPOT.Hardware.OutputPort::Write(bool)
IL_006f: nop
IL_0070: ldc.i4 0xfa
IL_0075: call void [mscorlib]System.Threading.Thread::Sleep(int32)
IL_007a: nop
IL_007b: ldsfld class [Microsoft.SPOT.Hardware]Microsoft.SPOT.Hardware.OutputPort TestDiscovery4Board.Program::_led4
IL_0080: ldsfld class [Microsoft.SPOT.Hardware]Microsoft.SPOT.Hardware.OutputPort TestDiscovery4Board.Program::_led4
IL_0085: callvirt instance bool [Microsoft.SPOT.Hardware]Microsoft.SPOT.Hardware.Port::Read()
IL_008a: ldc.i4.0
IL_008b: ceq
IL_008d: callvirt instance void [Microsoft.SPOT.Hardware]Microsoft.SPOT.Hardware.OutputPort::Write(bool)
IL_0092: nop
IL_0093: ldc.i4 0xfa
IL_0098: call void [mscorlib]System.Threading.Thread::Sleep(int32)
IL_009d: nop
IL_009e: nop
IL_009f: ldc.i4.1
IL_00a0: stloc.0
IL_00a1: br IL_0011
} // end of method Program::Main
Let’s create a new native debugging project on Crossworks
Crossworks IDE manages creation of specific projects/solutions related to debugging of externally built executables. Such projects actually do not provide building configuration, indeed they don’t contain any source file, since their only purpose is to coordinate reset/downloading/debugging features related to a pre-built binary image file containing symbolic debug info. Crossworks is able to work with several image file formats (such as .elf), so we’ll use tinyclr.axf binary file produced by MDK porting solution building. Such file contains references to all linked modules and corresponding source file paths, the memory offset of every code line in compiled image, parameters info and in general all you need to do symbolic debugging of running native firmware. It’s worth to mention that you’ll need also “STM32 CPU Support Package”, available as a separate download on Rowley website or as alternative installable through built-in package manager.

Ready? Go!
So, on Crossworks IDE we can now activate ST-LINK/SWD connection to target board by double-clicking on “ST-LINK/V2” item inside “Targets” toolwindow. Once established the connection (item text will be rendered as bold) we can start debug session using “Debug/Go” command (i.e. F5, as Crossworks uses same Visual Studio default shortcut scheme…nice!). TinyCLR image will then be downloaded on target board and a new GDB session will be created and started. Usually debug sessions hangs on main() entry-point, but tinyclr uses a different funciotn as entry-point, so firmware will keep on running until we pause it explicitly. Before doing that, let’s turn to Visual Studio sample application and set a breakpoint on “Debug.Print()” line (just after Main() method declaration), then start a new (managed) debug session, waiting for Visual Studio to break on that code statement. Turning back to Crossworks, we’ll obviously note that firmware isn’t actually stopped at all, since tinyclr must be necessarily running to manage (among many other things) Visual Studio debugging USB communication protocol, of course. Nevertheless, one of the “parts” of tinyclr we can surely consider not running in this situation is CIL/IL interpreter.
As you certainly know, .NET MF runtime doesn’t enforce a Jitter (i.e. a “just-in-time” compiler capable to compile on-the-fly managed code so that machine code can be executed directly, forgetting IL byte-code at all, starting from second execution on) but uses an interpreter that is invoked by each managed thread for every IL instruction has to be executed. Source code of such interpreter can be found inside “X:\MicroFrameworkPK_v4_2\CLR\Core\Interpreter.cpp”, that you actually cannot find inside .map file since it is indirectly included (among many others) by “X:\MicroFrameworkPK_v4_2\CLR\Core\core_fastcompile.cpp”.
If we open Interpreter.cpp in Crossworks (yes, through standard “open” menu/toolbar command) and set a breakpoint over line 918 (inside cpp method declared as “HRESULT CLR_RT_Thread::Execute_IL( CLR_RT_StackFrame* stack )”, invoked by managed CLR to process every IL opcode, using managed thread stack, whose reference is passed as pointer parameter, to process managed parameters), things begin to be interesting. Indeed, turning back to Visual Studio and stepping over with F10 we’ll see…nothing, because in the meanwhile native breakpoint on Crossworks side will be hit! Turning to Crossworks you’ll be watching to CLR waiting to execute first IL opcode related to Debug.Print() (we set a breakpoint on): now you’ll be watching the Matrix".
HRESULT CLR_RT_Thread::Execute_IL( CLR_RT_StackFrame* stack )
{
NATIVE_PROFILE_CLR_CORE();
TINYCLR_HEADER();
CLR_RT_Thread* th = stack->m_owningThread;
CLR_RT_Assembly* assm = stack->m_call.m_assm;
CLR_RT_HeapBlock* evalPos;
CLR_PMETADATA ip;
bool fCondition;
bool fDirty = false;
READCACHE(stack,evalPos,ip,fDirty);
while(true)
{
#if defined(TINYCLR_ENABLE_SOURCELEVELDEBUGGING)
if(th->m_timeQuantumExpired == TRUE)
{
TINYCLR_SET_AND_LEAVE( CLR_S_QUANTUM_EXPIRED );
}
// [.............]
Let’s go a little deeper and take a look at current value for “op” local variable (either via tooltip or locals toolwindow): you will see that interpreter is going to process an LDSTR opcode, whose purpose is to load a string into managed stack. Following Execute_IL method we’ll arrive to a switch statement whose cases manage each opcode. The one concerning LDSTR opcode is the following:
OPDEF(CEE_LDSTR,"ldstr",Pop0,PushRef,InlineString,IObjModel,1,0xFF,0x72,NEXT)
{
FETCH_ARG_COMPRESSED_STRINGTOKEN(arg,ip);
evalPos++; CHECKSTACK(stack,evalPos);
UPDATESTACK(stack,evalPos);
TINYCLR_CHECK_HRESULT(CLR_RT_HeapBlock_String::CreateInstance( evalPos[ 0 ], arg, assm ));
break;
}
Going even deeper, we can see that string loaded into managed stack comes from string literals table, located at same memory area of running firmware and specifically at address pointed by m_header field of current Assembly object (an instance of CLR_RT_Assembly class) summed to a displacement contained in “startOfTables” field. In other words, to look at actual string memory representation, we have to monitor address 0x8080000 (m_header) + 0x1e8, obtained dereferencing offset of 0x0B of m_header->startOfTables vector since string table is located at 0x0B index, as reported on line 293 of “TinyCLR_Types.h” header file, and finally summing requested offset for that specific LDSTR instruction parameter, which is 0x9E in our case. Magically, pointing a memory dump toolwindow to 0x8080286, well see exactly what we were looking for: “Started!”:

Nice, but also quite complex, so let’s jump to exploration of tinyclr code that “transforms” _led1.Write() into actual LED toggling…
Ok, but how TinyCLR actually manages to turn on a LED?
I think it’s useless to mention that we’ll never find a point, looking at what happens at IL interpreter level, where one or more instructions manages to turn a LED: what allows .NET MF to reach interaction with physical world is a specific layer, named HAL (Hardware Abstraction Layer), whose main purpose is to provide an integration model between the two sides of an interface: at one side we have managed code, to the opposite side we have native code; in the middle there is a runtime part aimed to manage marshaling, i.e. proper settlement of data that pass through that interface in a way they adhere to both “contracts” at same time.
Specifically, following managed implementation of Write method of OutputPort class with Reflector or dotPeek, we see that such method isn’t actually implemented inside that Assembly, but it is marked by MethodImplOptions.InternalCall attribute, telling runtime not to look for method implementation inside its containing Assembly, but to point its “Instruction Pointer” (IP) elsewhere to execute it…but where?

We could follow two ways to discover where to look at: either searching into an HTML document produced by porting building (tinyclr.htm) a reference to OutputPort::Write (or its “mangled” C++ name) in terms of its called/caller chains, or taking a look at msbuild solution for Discovery4 porting, where we find a line referencing a subproject (.featureproj) named “$(SPOCLIENT)\Framework\Features\Hardware.featureproj” ($(SPOCLIENT) macro will be expanded in Porting Kit installation directory by msbuild), that in its turn references “$(SPOCLIENT)\Framework\Features\SPOT_Hardware_CLR.libcatproj” project, that in its turn includes “$(SPOCLIENT)\CLR\Libraries\SPOT_Hardware\dotNetMF.proj”, that at last includes “spot_hardware_native_Microsoft_SPOT_Hardware_OutputPort.cpp”, where we actually find OutputPort::Write method declaration.
HRESULT Library_spot_hardware_native_Microsoft_SPOT_Hardware_OutputPort::Write___VOID__BOOLEAN( CLR_RT_StackFrame& stack )
{
NATIVE_PROFILE_CLR_HARDWARE();
TINYCLR_HEADER();
bool state;
CLR_RT_HeapBlock* pThis = stack.This(); FAULT_ON_NULL(pThis);
if(pThis[ Library_spot_hardware_native_Microsoft_SPOT_Hardware_NativeEventDispatcher::FIELD__m_disposed ].NumericByRef().s1 != 0)
{
TINYCLR_SET_AND_LEAVE(CLR_E_OBJECT_DISPOSED);
}
state = stack.Arg1().NumericByRef().u1 != 0;
// Test if flag says it is output port
if (pThis[ Library_spot_hardware_native_Microsoft_SPOT_Hardware_Port::FIELD__m_flags ].NumericByRefConst().s4 & GPIO_PortParams::c_Output)
{ // Writes value to the port.
::CPU_GPIO_SetPinState( pThis[ Library_spot_hardware_native_Microsoft_SPOT_Hardware_Port::FIELD__m_portId ].NumericByRefConst().u4, state );
}
else
{
TINYCLR_SET_AND_LEAVE(CLR_E_INVALID_OPERATION);
}
TINYCLR_NOCLEANUP();
}
So, we can now open “C:\MicroFrameworkPK_v4_2\CLR\Libraries\SPOT_Hardware\spot_hardware_native_Microsoft_SPOT_Hardware_OutputPort.cpp” source file into Crossworks and set a breakpoint on line 27, where we see a call to global function ::CPU_GPIO_SetPinState(), together with its two parameters: a Pin identifier and a boolean value named, (guess how) “pinState”.
void CPU_GPIO_SetPinState( GPIO_PIN pin, BOOL pinState )
{
NATIVE_PROFILE_HAL_PROCESSOR_GPIO();
if (pin < STM32F4_Gpio_MaxPins) {
GPIO_TypeDef* port = Port(pin >> 4); // pointer to the actual port registers
UINT16 bit = 1 << (pin & 0x0F);
if (pinState) port->BSRRL = bit; // set bit
else port->BSRRH = bit; // reset bit
}
}
I think you won’t be surprised very much when you’ll see that this function begins (after profiler instrumentation macro) initializing a GPIO_TypeDef structure instance, whose type is declared inside STM32 “stock” peripheral library, so that it refers to Port D (the one LEDs are connected to), using a 4 bits right shift since there are 16 pins/bits for each port and the first one, i.e. A0 pin, is conventionally referred to zero offset, and left-shifting requested pin number (and-ed with 0xF) to obtain the actual register bitfield value to use. Indeed, setting or resetting target I/O is matter of writing such value into correct GPIO register: BSRRL to set, BSRRH to reset. Running step-by-step previous function code you will see, besides actual “pin” and “state” parameters values passed, also actually LEDs toggling while Crossworks shows at the same time MCU special function registers (related to GPIO registers group in following screenshot) being correspondingly updated.

It’s worth to mention that such global functions actually represent all the differences between two different porting solutions, since whole .NET Micro Framework has been designed to manage e distinct separation of concerns between “model” and “implementation”, so that it can abstract the many different ways needed to execute semantically equivalent operations on different hardware platforms (toggling a digital output port, in this case).
Where can we go, now?
So, I really hope that this short journey under TinyCLR hood succeeded in giving you same “good vibrations” I felt first time I discovered I could see .NET MF infrastructure code running, since until then I could only wonder how those things really worked. On coming posts, we’ll see how this debugging practice will be very useful in developing native extensions to existing porting solutions, such as drivers for previously unsupported peripherals or computationally-intensive features that require performance levels actually unreachable by managed applications.
(Update: just posted a new chapter of my “journey” inside .NET MF, this time about interrupt handling. You can find it here)