Today I was able to pass my other milestone, which was getting this library to make a working interrupt handler that could be called from the firmware side. I really like the way it turned out. I have attached a simple demo below. I now have a credible way to both read and write the I/O pins.
The reason this is interesting is because it is much harder than the synchronous case. In the synchronous case, the whole .NET framework screeches to a halt while you're running your precious opcodes. But in the asynchronous case, you need to install a little interrupt handler and find a way to pin its memory down (both the code and whatever buffers it might be using) so that the garbage collector can't move it. For a while I thought I might have to dynamically allocate or reserve a block of memory on the firmware side; I didn't like this idea very much so I was hoping to find another way. But then I had a little brainwave: start a new C# thread and have it create a fixed() block for each buffer it needs to pin. This thread isn't expected to do any work other than sleep forever inside the fixed() block. This technique allows me to allocate whatever storage I need on the C# side (code, buffers, whatever) and keep them around as long as I need to.
The attached files contain:
- the new firmware
- a program that demonstrates this, called Demo\InterruptDemoOne\InterruptDemoOne.sln
I've also added a feature to my "language" to support static variables--i.e. variables that persist between calls to the compiled code rather than being stack-allocated and therefore transient. Among other things, this enables a poor man's version of shared memory communication, allowing the C# program to inspect or modify the state of the interrupt handler.
My demo program behaves in the following way. It compiles and installs an interrupt handler which will be triggered by interrupts on the onboard button. When an interrupt happens, it will write the button state to the onboard LED. To prove that the C# side is still humming along, the main thread computes prime numbers and prints them out to the debug console (with a 1 second delay so as not to overwhelm the debug window). The thread which is hanging on to the fixed block wakes up once a second and checks the shared variable to see if 30 interrupts have happened (which would correspond to 15 button presses and releases) Once this has happened, it disposes its InputPort (which will disable the interrupt) and then it leaves its little fixed block, unpinning the memory.
Here is a movie, which isn't really that interesting as demos go, but it does prove that the whole thing works.
http://www.youtube.com/watch?v=es17G_u8FZk
For convenience I show Program.cs here. For the whole program you will need to download the attachment. I realize that some of the code here won't make intuitive sense without more detailed documentation, but hopefully you can get the gist of it.
Next: If I find time I may try to see if I can get some higher-quality readings from my PING))) sensor.
using System.Collections; using System.Threading; using FluentInterop.CodeGeneration; using FluentInterop.Expressions; using FluentInterop.Fluent; using Kosak.SimpleInterop; using Microsoft.SPOT; using Microsoft.SPOT.Hardware; using SecretLabs.NETMF.Hardware.Netduino; namespace InterruptDemoOne { public class Program { /// <summary> /// stick this somewhere so the thread is not prematurely GC'ed /// </summary> private static Thread thread; public static void Main() { thread=LaunchISR(); GeneratePrimes(); } /// <summary> /// The purpose of this is to prove that the C# side is still humming away, while my installed firmware code /// is showing the button state on the LED /// </summary> private static void GeneratePrimes() { var primes=new ArrayList {2}; Debug.Print("2 is prime"); var candidate=3; while(true) { var isPrime=true; foreach(int prime in primes) { if(prime*prime>candidate) { break; } if((candidate%prime)==0) { isPrime=false; break; } } if(isPrime) { Debug.Print(candidate+" is prime"); primes.Add(candidate); Thread.Sleep(1000); } candidate+=2; } } private static Thread LaunchISR() { var isrCode=MakeInterruptServiceRoutine(); var installerCode=MakeInterruptInstaller(); //run once in order to initialize its internally-stashed method dispatch table (see comments) isrCode.Invoke(); //The purpose of this C# thread is twofold: //1. By staying alive, inside the fixed() block, it keeps my isrCode array from being relocated // by the garbage collector (this would be bad, because the code in isrCode has been installed // as a firmware interrupt handler) //2. It periodically wakes up and checks the "count" variable inside isrCode (this is a form of // poor man's shared memory communication). When that count reaches 30 (usually // fifteen button presses and releases), the loop will exit. When the InputPort is disposed, // this will uninstall the ISR (ISR=Interrupt Service Routine) // //It should be stressed that the thread is NOT monitoring the button state. That is being //done by hyper-awesome dynamically compiled code. var thread=new Thread(() => { unsafe { fixed(short* isrp=isrCode) { //we do this so we can let the system dispose the InputPort, and hopefully uninstall the ISR, //when we exit the loop using(new InputPort(Pins.ONBOARD_SW1, true, ResistorModes.Disabled)) { installerCode.Invoke(sa0 : isrCode); var oldCount=-1; while(true) { var count=isrCode[isrCode.Length-2]; if(count!=oldCount) { oldCount=count; Debug.Print("count is "+count); if(count>=30) { break; } } Thread.Sleep(1000); } } } } }); thread.Start(); return thread; } /// <summary> /// By convention, the 17th argument to any of my fluent routines is the "firmware" argument, which I have arranged /// to point to a vector of useful firmware routines. /// /// When you are called by my framework, this argument is set properly. However, when you are called as an interrupt /// service routine, the argument is not set, because the caller (in this case, the firmware) knows nothing about it /// /// The way we get around this is to keep a static variable inside this routine. On the first-ever call to this /// routine, it stashes the firmware in its own static variable. This is so subsequent calls can access that /// variable. /// /// This is why we call the ISR once from C#: to properly initialize this static variable. /// /// Because my type system isn't complete, there's a lot of casting in this code. Sorry about that /// </summary> private static short[] MakeInterruptServiceRoutine() { var isrCode=CodeGenerator.Compile((g, buttonPin, pinState, param, ច, ឆ, ជ, ឋ, ឌ, ព, ផ, ត, ថ, ទ, ធ, ម, វ, firmware) => { //Our first static variable keeps a count of how many times we are called. //The C# side uses this to end the program after the user presses the button a certain number of times. //Because it is the first static variable declared, it will be stored at the end of the array. //(And since it is an int, it will take up two slots in the short array) //Therefore it is guaranteed to live at isrCode[isrCode.Length-2] and isrCode[isrCode.Length-1] var clickCount=g.AllocateStaticInt("clickCount"); //Our second variable stashes a pointer to the firmware method dispatch table. //Since it is the second static variable declared, it will be stored just before the first one. //(It is our convention to store our statics in the reverse order that they are declared) //Therefore it is guaranteed to live at isrCode[isrCode.Length-4] and isrCode[isrCode.Length-3] var stashedFirmwareTable=g.AllocateStaticInt("stashedFirmwareTable"); g.If(stashedFirmwareTable==0, () => { stashedFirmwareTable.AssignFrom(firmware.AsInt()); g.Return(0); }); stashedFirmwareTable.AsFirmwareMethodDispatchTable().SetPinState((int)Pins.ONBOARD_LED, 1-pinState); clickCount.AssignFrom(clickCount+1); }); return isrCode; } private static short[] MakeInterruptInstaller() { var installerCode=CodeGenerator.Compile((g, ក, គ, ង, ច, ឆ, ជ, ឋ, ឌ, isr, ផ, ត, ថ, ទ, ធ, ម, វ, firmware) => { var result=new IntVariable("result"); firmware.EnableInputPin2(result, (int)Pins.ONBOARD_SW1, 1, isr.AsFuncPointer(), 0, (int)Port.InterruptMode.InterruptEdgeBoth, (int)ResistorModes.Disabled); g.Return(result); }); return installerCode; } } }