This is the same article that was published in the Micro Framework blog on October 9, 2007.
As one of my first projects with the .NET Micro Framework, I created a more sophisticated version of a little project I developed some time ago: a counter for swim training. Every two lengths of the pool (that Is, after swimming to the opposite end and back), you press a switch with your hand, and the device counts the total number of lengths.
For the .NET Micro Framework version of the application, I added a stopwatch controlled by two switches. One switch pauses or restarts the stopwatch. The other switch halts the stopwatch and records the entry number, the time of day, and the time to complete the two lengths, to the millisecond, using a SwimTiming class I created. The hardware platform is the Freescale i.MXS Development Kit.
Every time the stopwatch is halted, a new SwimTiming instance is added to the SwimTimingCollection. At the same time, the ToString() method is used to convert this information to a string and add the record to a history ListBox at the top of the screen. The difference between the most recent length and the previous length is calculated and displayed. A second ListBox at the bottom of the screen serves as a menu to allow the user to choose to display the last length measured, the average of the stored times, and the highest and the lowest time. Here is a better look at the screen.
The application also lets you download the stored data via serial port to a PC, allowing you, for example, to process the data with Excel in order to create a daily performance chart.
This article describes the three main sections of this application: the stopwatch implementation, the user interface, and the PC communication. I will cover only the salient portions of the source code in the article. You can download the complete source code for the application here.
The .NET Micro Framework is not a real-time operating system, so implementing the stopwatch requires care. To avoid possible delays that could influence measurement precision, I created a Chrono class that internally uses the Microsoft.SPOT.ExtendedTimer timer. The ExtendedTimer class is very similar to the System.Threading.Timer class that is also available in the .NET Micro Framework. My Chrono class provides Start(), Pause() and Stop() methods and uses the IntervalElapsed event to informs the application of stopwatch status and elapsed time.
The timer runs in a separate thread from the user interface. As in the full .NET framework, it is necessary to use BeginInvoke() to call a control’s methods from the timer thread, rather than calling the control directly, to avoid trouble with the display. BeginInvoke() is preferable to Invoke(), as BeginInvoke() returns immediately, allowing the timer thread to free up as quickly as possible without waiting for the event handler to stop executing.
1: // Chrono class event handler
2: void Chrono_IntervalElapsed(object sender, ChronoEventArgs e)
3: {
4: _DelArgs[0] = sender;
5: _DelArgs[1] = e;
6: this.Dispatcher.BeginInvoke(_ChronoDel, _DelArgs);
7: }
8:
9: // method executed in the context of the main thread
10: void Chrono_IntervalElapsed_Dispatched(object sender, ChronoEventArgs e)
11: {
12: _ClockText.TextContent = _Chrono.GetFormattedTime(
13: e.State == ChronoState.Paused || e.State == ChronoState.Stopped);
14: _ClockText.UpdateLayout();
15:
16: if(e.State == ChronoState.Stopped)
17: AddNewTime(new SwimTiming(DateTime.Now, e.Elapsed));
18: }
Button handling can add delays, too. The .NET Micro Framework calls BeginInvoke()to execute button event handlers in the main thread context. To avoid the small delay that this can cause, I modified the GPIOButtonInputProvider class to subscribe directly to the interrupt event handler.
1: InterruptPort _PlayPausePort;
2: InterruptPort _StopPort;
3: ...
4:
5: _PlayPausePort = new InterruptPort(..., false, Port.ResistorMode.PullUp,
6: Port.InterruptMode.InterruptEdgeBoth);
7: _StopPort = new InterruptPort(..., false, Port.ResistorMode.PullUp,
8: Port.InterruptMode.InterruptEdgeBoth);
9:
10: void PlayPause_OnInterrupt(Cpu.Pin port, bool state, TimeSpan time)
11: {
12: if(!state) // we are interested to the button up only
13: return;
14:
15: if(_Chrono.State == ChronoState.Running ||
16: _Chrono.State == ChronoState.Started)
17: {
18: _Chrono.Pause();
19: return;
20: }
21:
22: _Chrono.Start();
23: }
This change means we must use the lock statement in the Start, Stop, and Pause methods as well as in the timer event handler to avoid concurrency problems, which can lead to potential corruption of variables that are accessed from multiple threads.
1: public void Start()
2: {
3: lock(_Sync)
4: {
5: ...
6: }
7: }
So far, everything in this program could have been done on an 8-bit CPU in assembly language. But providing an attractive user interface on a color LCD screen is much more of a challenge. The .NET Micro Framework makes this nearly trivial with its logical object model, which mimicks the WPF (Windows Presentation Foundation).
The main UI element is the Window class, which in the .NET Micro Framework overlaps the entire LCD screen. After sizing the window with the help of the SystemMetrics class, we can create the controls.
The .NET Micro Framework presentation classes are classified into three categories: layout, data presentation controls, and drawing. Instead of using a Grid class, however, all layout in the .NET Micro Framework is accomplished using StackPanels, each oriented towards the top or the left of the screen.
The first layout element of my timer application is a vertical StackPanel. The first element that is added to its Children collection is a ListBox containing the time table. Also inside this StackPanel is a Border control whose Child is a second horizontally-ordered StackPanel. The second StackPanel contains a ListBox and a Border, which contains a Text control.
Every control can be extensively customized, even the ListBoxItems. In the upper list, each ListBoxItem contains a StackPanel that hosts three elements: a Text element within a Border, an Image, and a second Text element. (The Border is needed to give the element a fixed size so we can right-align the entry numbers.)
In building the lower ListBox, the ListBoxItems margin is set in a way that limits the size of the rectangle that highlights the selected element, as shown here.
1: ListBoxItem CreateLowerItem(Brush Background, string Content, int Width,
2: int Height, HorizontalAlignment HAlign)
3: {
4: ListBoxItem lbi = new ListBoxItem();
5: lbi.Background = Background;
6: lbi.HorizontalAlignment = HorizontalAlignment.Left;
7: lbi.SetMargin(10, 2, 10, 2);
8: lbi.Child = CreateText(Content, HAlign, VerticalAlignment.Bottom);
9: ...
10: }
11:
The ListBoxItems are hosted by a StackPanel, which is in turn hosted by a ScrollViewer that abstracts the scrolling logic. While the StackPanel has no Background, the ScrollViewer does, and the default color is white. To avoid transparency problems, then, it is necessary to set the ScrollViewer color as well.
Furthermore, the upper ListBox should never have a selected element, but it must be scrollable. The LineHeight property of each item must be set to the number of pixels to be scrolled for each LineUp and LineDown. This is the method that is used to insert a new element in the upper list.
1: void AddNewTime(SwimTiming st)
2: {
3: _SwimTimings.Add(st);
4: _HistoryListBox.Items.Insert(0,
5: CreateUpperItem(_UnselectedItemBrush, st.Number.ToString(),
6: st.ToString(), _HistoryListBox.Width, 0, HorizontalAlignment.Left));
7:
8: ScrollViewer sv = _HistoryListBox.Items[0].Parent.Parent as ScrollViewer;
9: sv.Background = _UnselectedItemBrush;
10: sv.LineHeight = _HistoryListBox.Items[
11: _HistoryListBox.Items.Count-1].ActualHeight + 4;
12:
13: sv.ScrollingStyle = ScrollingStyle.LineByLine;
14: }
The buttons are handled using the OnButtonUp handler included in the .NET Micro Framework template. We need only add code to handle actions triggered from the buttons. For example, the Up and Down buttons change the selection of the lower ListBox, while the Fast Forward and Rewind buttons scroll the upper history ListBox.
1: private void OnButtonUp(object sender, ButtonEventArgs e)
2: {
3: Debug.Print(e.Button.ToString());
4: if(e.Button == Button.Down) // _StatisticsListBox
5: {
6: if(_StatisticsListBox.SelectedIndex == _StatisticsListBox.Items.Count - 1)
7: return;
8: else
9: _StatisticsListBox.SelectedIndex++;
10: }
11:
12: if(e.Button == Button.Up) // _StatisticsListBox
13: {
14: if(_StatisticsListBox.SelectedIndex == 0)
15: return;
16: else
17: _StatisticsListBox.SelectedIndex--;
18: }
19:
20: if(e.Button == Button.FastForward) // _HistoryListBox
21: {
22: if(_HistoryListBox.Items.Count == 0)
23: return;
24: ScrollViewer sv = _HistoryListBox.Items[0].Parent.Parent as ScrollViewer;
25: sv.LineDown();
26: sv.Invalidate();
27: }
28:
29: ...
30: }
The .NET Micro Framework allows us to easily serialize the SwimTimingCollection via the Serializable attribute. My helper class TimeSpanFormatter and the reference to the owning collection should not be serialized; this is accomplished by marking them with NonSerialized attributes.
1: [Serializable]
2: public class SwimTiming : IComparable
3: {
4: int _Number;
5: DateTime _TimeStart;
6: TimeSpan _Elapsed;
7:
8: [NonSerialized]
9: SwimTimingCollection _List;
10:
11: ...
12: }
13:
14: [Serializable]
15: public class SwimTimingCollection : ArrayList
16: {
17: int Counter = 0;
18:
19: [NonSerialized]
20: TimeSpanFormatter _TimeSpanFormatter;
21:
22: ...
23: }
Transferring data is accomplished by the Upload method. For our needs, the serial port is fast enough, since the timing table is fairly small. However, it is possible to use another port if better performance is needed.
1: private void Upload()
2: {
3: SerialPort.Configuration config = new SerialPort.Configuration(
4: SerialPort.Serial.COM1, SerialPort.BaudRate.Baud19200, false);
5: SerialPort port = new SerialPort(config);
6: byte[] Blob = Reflection.Serialize(_SwimTimings,
7: typeof(SwimTimingCollection));
8: port.Write(Blob, 0, Blob.Length);
9: port.Dispose();
10: }
On the PC end, we will need to decode the .NET Micro Framework’s binary serialization format, which is designed to keep the data as small as possible. It would be not much more work to generate XML instead, if desired, by concatenating strings and generating a byte array (using the UTF8Encoding class) to be passed on to the SerialPort.Write() method.
Of course, programming is the most fun when you have a real device you can use for its intended purpose. I have a Freescale i.MXS Development Kit, one of the first to appear on the market. Other manufacturers, including EmbeddedFusion, Digi International, and SJJ Embedded Micro Solutions are now offering development boards compatible with the .NET Micro Framework.
The i.MXS kit offers ten buttons, USB and serial ports, and a color LCD, among other features. From the start, I mapped the functions of my application to buttons on the i.MXS Development Kit, as shown here.
The i.MXS Development Kit comes with a USB driver and a customized emulator. To use the emulator, open the Properties for the project, switch to the Micro Framework page, and choose Emulator as the Transport and i.MXS DevKit Emulator as the Device.
Once the driver is installed, you can also deploy the application to the i.MXS hardware right from Visual Studio. To do this, choose USB for the transport, and your i.MXS board from the Device drop-down.
Some minor changes are needed to the button mappings provided in the .NET Micro Framework Window Application template. The i.MXS board uses different GPIO pin numbers for the buttons than the standard emulator, and also has some hardware differences. These are easily addressed.
The file called CPU.cs in the Freescale sample project contains various classes useful for dealing with the I/O of the i.MXS kit. You should add CPU.cs to your Visual Studio project as an existing item, as shown here.
Then add this line to the GPIOButtonInputProvider.cs source file:
using iMXsBoard = Microsoft.SPOT.Hardware.FreescaleMXSDemo;
Remapping the buttons is a simple task. Here is the default mapping established in the template.
1: ButtonPad[] buttons = new ButtonPad[]
2: {
3: new ButtonPad(this, Button.Left , Cpu.Pin.GPIO_Pin0),
4: new ButtonPad(this, Button.Right , Cpu.Pin.GPIO_Pin1),
5: new ButtonPad(this, Button.Up , Cpu.Pin.GPIO_Pin2),
6: new ButtonPad(this, Button.Select, Cpu.Pin.GPIO_Pin3),
7: new ButtonPad(this, Button.Down , Cpu.Pin.GPIO_Pin4),
8: };
9:
The proper i.MXS mapping is as follows.
1: ButtonPad[] buttons = new ButtonPad[]
2: {
3: new ButtonPad(this, Button.Left , iMXsBoard.Pins.GPIO_PORT_B_16),
4: new ButtonPad(this, Button.Right , iMXsBoard.Pins.GPIO_PORT_B_12),
5: new ButtonPad(this, Button.Up , iMXsBoard.Pins.GPIO_PORT_B_11),
6: new ButtonPad(this, Button.Select, iMXsBoard.Pins.GPIO_PORT_B_8),
7: new ButtonPad(this, Button.Down , iMXsBoard.Pins.GPIO_PORT_B_10),
8: new ButtonPad(this, Button.Menu , iMXsBoard.Pins.GPIO_PORT_B_13),
9: new ButtonPad(this, Button.Rewind , iMXsBoard.Pins.GPIO_PORT_B_14),
10: new ButtonPad(this, Button.FastForward , iMXsBoard.Pins.GPIO_PORT_B_15),
11: };
We must also configure the button drivers. The i.MXS board uses the pull-up configuration, whereas the template code uses the pull-down configuration. Also, the anti-glitch filter is not supported by the i.MXS hardware. The ButtonPad constructor should therefore be changed as follows.
1: public ButtonPad(GPIOButtonInputProvider sink, Button button, Cpu.Pin pin)
2: {
3: this.sink = sink;
4: this.button = button;
5: port = new InterruptPort(pin,
6: false, // i.MXS doesn’t support AntiGlitch
7: Port.ResistorMode.PullUp, // Verify in the circuit diagram
8: Port.InterruptMode.InterruptEdgeBoth);
9:
10: port.OnInterrupt += new GPIOInterruptEventHandler(this.Interrupt);
11: }
The simplicity of the .NET Micro Framework lets you create applications with a polished user interface that would otherwise require an unlikely time investment. As long as your project is not subject to fierce price wars that require the absolute minimum device cost, the speed at which you can develop applications using the .NET Micro Framework is a major benefit.
Thanks to Visual Studio, you can develop and debug the application either with the emulator or directly on a development board via USB. You can easily explore scenarios in the debugger that otherwise would be difficult to reproduce with an emulator, in addition to the long list of tools and features that Visual Studio developers are accustomed to.
The .NET Micro Framework is not a real-time operating system, but for my application it proved more than accurate enough once I took a little care. For applications that require true real-time capability, you could interface a secondary processor (programmed in assembly or C) via I2C or SPI; the rapid development of the non-time-critical portion of the program in C# using the .NET Micro Framework will still provide a net benefit.
Privacy | Legal Copyright © Raffaele Rialdi 2009, Senior Software Developer, Consultant, p.iva IT01741850992, hosted by Vevy Europe Advanced Technologies Division. Site created by Raffaele Rialdi, 2009 - 2015 Hosted by: © 2008-2015 Vevy Europe S.p.A. - via Semeria, 16A - 16131 Genova - Italia - P.IVA 00269300109