Summary: In this installment of "Some Assembly Required", Scott Hanselman teams up with the makers of the Microbric Viper Robot and the guys at Iguanaworks to create a custom IR serial port board to control the Viper Robot using .NET!
The Microbric Viper is a robot construction kit based on a new solderless construction technique. The Viper uses a Basic Atom microcontroller that you program, shockingly, using BASIC. Modules like an Infrared Receiver, motors and switches can be fairly easily controlled as you just screw them directly into the mainboard and address them by number. Once I got my hands on a Microbric, I started trying to talk to the robot using my laptop's Infrared Port. It turns out after a number of failed attempts that Windows doesn't expose standard laptop IR ports as Serial Ports or in any way other than via the IRDA protocol. IRDA was not only overkill for this kind of communication but also a hassle to program to. What I really needed was an IR transmitter that I could talk to using an interface that was clear and clean like the Serial Port interface exposed by System.IO.Ports.
I approached the guys at Iguanaworks as they make an infrared transceiver that is addressable via a standard Serial Port. The Viper comes with a Sony Remote Control so I set off to figure out how to get the Iguanaworks IR transmitter to speak to the Viper using .NET.
Programming the Microbric Viper Robot
The IR receiver module is screwed into the Microbric Viper, in my case, into the addressable P14 port on the side of the main body. The Viper includes a main chip called the Basic Atom that is flashed using BASIC programs written in an included BASIC IDE. In my BASIC file a constant is used to address the IR "Data Pin," in this case, P14.
The Basic Atom includes a number of BASIC helper methods that the microprocessor uses to talk to the external world. The "pulsin" method is used to talk to the IR Data Pin and detect a pulse. Infrared receivers use high-speed pulses at a certain frequency (how fast it flashes) and duty cycle (a percentage of how long the LED is on versus off). These pulses of light continue for a set amount of time to indicate ones, zeros or the header.
How the Sony Infrared Protocol works
I'm trying to pulse the IR LED to emulate the Sony Remote Control, and started by looking for some information on how the Sony Infrared Protocol (SIRC) works. I found two excellent writeups.
IR Protocols are fairly sensitive to timing. The pulses need to happen at specific intervals with the correct frequency. Because the timing is so fine-grained, I'm using the new System.Diagnostic.Stopwatch class in .NET (a wrapper for the Win32 QueryPerformanceCounter API) that uses Ticks for its unit of measurement. The Frequency is 2992540000 ticks per second, so I figure that'd be enough resolution.
The Sony remote I'm trying to emulate uses a 40kHz frequency, so it wants to flash the LED one cycle, once, every 1/40000 of a second. That means every 74814 ticks or every 25µs (microseconds are 1/1000000 of a second.)
I'm trying to send a header pulse of 2.4ms in length and I need to cycle the LED once every 25µs. I turn it on for 8µs and turn if off for 17µs. That means it will cycle 96 (2400µs) times for the header, 24 (1200µs) times for a space or zero, and 48 (600µs)times for a one. An image from San Bergmans illustrates:
The Iguanaworks IR serial port board uses DTR (Data Terminal Ready) to turn on the IR LED. When DTR is high, the LED lights up, when it's off, the LED turns off. Using the Stopwatch and some really tight loops I figure I can flash (pulse) the LED fast enough.
Now at this point, you may, dear reader, have already had the thought that perhaps trying to flash the LED via software, rather than hardware, is an astonishingly bad idea. Well, I could have used your insight earlier, my friend. But, on with my tale, as it is the telling that is so enjoyable, right?.
We'll take a look at more details of the internal implementation of the Sony Protocol in a moment.
INTERESTING REMINDER: Remember, you can't see IR (it's infrared, therefore not in our visible spectrum) but you can see it if you point it at a Webcam or digital camera, which is what I've been doing to sanity check the hardware. The picture at left is an image of the LED pointed at my Logitech Webcam.
Infrared Serial Port via Software
Of course, I've started with managed code, because I'm a managed kind of a guy. I started using System.IO.Ports to address the Iguanaworks IR transmitter that is connected to a serial point on my PC. They make both a Serial Port and a USB version, but there are not yet drivers for the USB version so I got a three-foot long serial extension cord and went to work.
ASIDE: Where does the Power come from?
The Iguanaworks IR is a uniquely high-power transmitter that charges up a capacitor in order to provide a range of up to 10-meters. Your mileage may vary. However, it requires a few minutes to charge up the capacitor. Once it is charged up, however, even if you are using it constantly, it'll find a comfortable middle place where the output matches the input. If you use it intermittently, which is more typical, you'll likely get very good range and bright output. In my initial testing, though, while I had no trouble getting output from it using Winlirc (the only officially supported Open Source software package for this transmitter) but when I used my application, the transmitter would peter out and eventually go dim. What the heck was going on?
I fought with it for a while, then decided to RTFS (where "S" is "Schematic). The board layout is here. Notice that the RTS (Serial Port Ready-To-Send) Pin 7 goes straight to VoltageIn. Duh! <slaps forehead>. They are sipping power off the Ready To Send pin and I'm not setting that pin hot via RtsEnable.
1: port = new SerialPort(portString);
2: port.RtsEnable = true; //needed for power!
3: port.BaudRate = 115200;
4: port.StopBits = StopBits.One;
5: port.Parity = Parity.None;
6: port.DataBits = 7;
7: port.Handshake = Handshake.None;
So, if you ever find yourself using the High-Power LIRC Transmitter/Receiver in an unsupported way writing your own program, remember to set RTS high or you won't get any power. Heh!
Back to the narrative. I suspected I might be writing more than one serial port class, or possibly using different pieces of hardware, so I defined an IIRSerialPort interface with some basics like On, Open, Off, etc. Since the Iguanaworks IR uses DTR to turn on the LED, it was as easy as setting the port's DtrEnable property to true to get a solid LED output.
Here's part of the first Managed IR Serial Port class.
1: public class ManagedIRSerialPort : IIRSerialPort
3: SerialPort port = null;
5: public ManagedIRSerialPort(string portString)
7: port = new SerialPort(portString);
8: port.RtsEnable = true; //needed for power!
9: port.BaudRate = 115200;
10: port.StopBits = StopBits.One;
11: port.Parity = Parity.None;
12: port.DataBits = 7;
13: port.Handshake = Handshake.None;
16: public void Open()
21: public void On()
23: port.DtrEnable = true;
26: public void Off()
28: port.DtrEnable = false;
31: public void Close()
This class is just the beginning, as it only turns the LED on and off. Remember I need to "pulse" the LED, turning it on and off 96 times all in the space of 2400µs. I wrote a SendPulse that spun very tightly and used the StopWatch class to manage timing.
1: public unsafe void SendPulse(long microSecs)
3: long end = LastTimeInTicks + (microSecs * (STOPWATCHFREQ / MILLION));
4: int i = 0;
15: while (LastTimeInTicks < end);
17: System.Diagnostics.Debug.WriteLine(i.ToString() + " pulses in " + timer.ElapsedTicks/(double) req*MILLION+"us");
Even with lots of optimization, I just couldn't get it to cycle fast enough. Remember, I need the header to take 2400µs total. In this screenshot, you can see it's taking an average of 30000µs! That sucks - it's 10 times slower than I need it to be.
So I futzed with this for a while, and then Reflector'd around. I noticed the implementation of set_dtrEnable inside of System.IO.Ports.SerialStream was WAY more complicated than it needed to be for my purposes.
1: //Reflector'd Microsoft code from inside System.IO.Ports.Port
2: internal bool DtrEnable
6: int num1 = this.GetDcbFlag(4);
7: return (num1 == 1);
11: int num1 = this.GetDcbFlag(4);
12: this.SetDcbFlag(4, value ? 1 : 0);
13: if (!UnsafeNativeMethods.SetCommState(this._handle, ref this.dcb))
15: this.SetDcbFlag(4, num1);
18: if (!UnsafeNativeMethods.EscapeCommFunction(this._handle, value ? 5 : 6))
All I figured I needed to do was call the Win32 API EscapeCommFunction to set the DTR pin high. One thing I learned quickly was that calling EscapeCommFunction was 4 times faster than calling SetCommState for the purposes of raising DTR, so in this code sample those lines are commented out.
I then wrote another implementation of IIRSerialPort, still in managed code, but this one talking to the COM Port using the underlying Win32 APIs.
1: public class UnmanagedIRSerialPort : IIRSerialPort
3: IntPtr portHandle;
4: DCB dcb = new DCB();
5: string port = String.Empty;
6: public UnmanagedIRSerialPort(string portString)
8: port = portString;
11: public void Open()
13: portHandle = CreateFile("COM1",
14: EFileAccess.GenericRead | EFileAccess.GenericWrite,
18: EFileAttributes.Overlapped, IntPtr.Zero);
19: GetCommState(portHandle, ref dcb);
20: dcb.RtsControl = RtsControl.Enable;
21: dcb.DtrControl = DtrControl.Disable;
22: dcb.BaudRate = 115200;
23: SetCommState(portHandle, ref dcb);
26: public void On()
28: EscapeCommFunction(portHandle, SETDTR);
29: //dcb.DtrControl = DtrControl.Enable;
30: //SetCommState(portHandle, ref dcb);
33: public void Off()
35: EscapeCommFunction(portHandle, CLRDTR);
36: //dcb.DtrControl = DtrControl.Disable;
37: //SetCommState(portHandle, ref dcb);
40: public void Close()
45: #region Interop Serial Port Stuff
47: static extern bool GetCommState(IntPtr hFile, ref DCB lpDCB);
50: static extern bool SetCommState(IntPtr hFile, [In] ref DCB lpDCB);
52: [DllImport("kernel32.dll", SetLastError = true)]
53: public static extern bool CloseHandle(IntPtr handle);
55: [DllImport("kernel32.dll", SetLastError = true)]
56: static extern bool EscapeCommFunction(IntPtr hFile, int dwFunc);
57: //Snipped so you don't go blind...
As you can see I've got it abstracted away with a common interface so I can switch between managed serial and unmanaged serial quickly. I ran the same tests again, this time with MY serial port stuff:
Sweet, almost 10x faster and darn near where I need it to be. However, it's not consistent enough. I need numbers like 2400, 600, 1200. I'm having to boost the process and thread priority just to get here...
1: previousThreadPriority = System.Threading.Thread.CurrentThread.Priority;
2: System.Threading.Thread.CurrentThread.Priority = System.Threading.ThreadPriority.Highest;
3: System.Diagnostics.Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.RealTime;
...and undo it with...
1: System.Threading.Thread.CurrentThread.Priority = previousThreadPriority;
2: System.Diagnostics.Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.Normal;
...and that's just naughty and not getting the job done.
At this point, it's close, but I'm wondering if it's even possible to flash this thing fast enough. I'm at the limit of my understanding of serial ports (Is DTR affected by Baud Rate? Is 115200 the fastest? Would this be faster in C++ (probably not), or is there a faster way to PInvoke?)
More Details of the SIRC Protocol
The Sony IR Protocol uses a header that consists of a pulse 2400µs long, followed by 12 ones or zeros (twelve bits) where a "1" is a pulse 1.2ms long followed by an off state for 600µs long and a "0" is a pulse 600µs long followed by an off state for 600µs long. The whole packet is finalized with an off state 600µs long.
According to the SIRC spec, these are the "1s and 0s" we will be using to control the robot. They are the buttons used for the cursor on Sony remotes. This next code snippet is from the BASIC program that controls the robot, so this is what the receiver on the robot needs to see to take action.
2: High LED_Pin ;Set LED_Pin high
3: if irdata = %0000100100000000 then forward
4: if irdata = %1000100100000000 then backward
5: if irdata = %0100100100000000 then twistright
6: if irdata = %1100100100000000 then twistleft
7: if irdata = %0010100100000000 then still
The protocol indicates that it wants the Most Significant Bit on the RIGHT, which is the opposite of how it's done on Intel chips. Note that this doesn't refer to "Endianness," which is byte-by-byte. We're talking about reversing or flipping the entire bit stream.
Here's the constants in C# that correspond (in reverse) to the values the BASIC file. These values are straight from the spec.
1: const int FORWARD = 144;
2: const int BACKWARD = 2192;
3: const int RIGHT = 1168;
4: const int LEFT = 3216;
5: const int STILL = 656;
I could have stored them already reversed, but in the interest of making the code more reusable for other IR-related projects, I added a reverse function with help from Nicolas Allen and Wesner Moise that takes a long and the number of bits to reverse.
1: public long Reverse(long i, int bits)
3: i = ((i >> 1) & 0x5555555555555555) | ((i & 0x5555555555555555) << 1);
4: i = ((i >> 2) & 0x3333333333333333) | ((i & 0x3333333333333333) << 2);
5: i = ((i >> 4) & 0x0F0F0F0F0F0F0F0F) | ((i & 0x0F0F0F0F0F0F0F0F) << 4);
6: i = ((i >> 8) & 0x00FF00FF00FF00FF) | ((i & 0x00FF00FF00FF00FF) << 8);
7: i = ((i >> 16) & 0x0000FFFF0000FFFF) | ((i & 0x0000FFFF0000FFFF) << 16);
8: i = (i >> 32) | (i << 32);
9: return i >> (64 - bits);
This makes the SendData() method nice and clean and allows me to send codes straight from the SIRC spec:
1: public unsafe void SendData(long data, int bits)
3: int i;
5: data = Reverse(data,bits);
8: if((data & 1) == 1)
This method takes each bit at a time and flashes the IR port. But, we've still got the problem of speed. It's just not happening fast enough. The basic design is there, but at this point I'm starting to suspect I need different hardware.
Infrared Serial Port via Custom Hardware
At this point, I went back to the folks at Iguanaworks and explained the problem. They agreed it was time to put the "carrier" onto the hardware. We'd change the original serial port board that turned the LED on and off using the Serial DTR signal so that now it would oscillate when DTR was set hot.
The graphic above shows the original serial port adapter on the right, the prototype in the middle with the "daughter board" attached. We tried a number of resistor values before we got it right. And the completed first-run prototype on the far left. There's ways to make it smaller, but this way gives the user more choices. The frequency can be swapped to emulate different remote controls by changing resistors yourself in the final design, or turning the carrier off completely.
My IR controlling code wouldn't change significantly, it would actually shrink, as it was silly for me to try and do the work that was better handled by hardware. Here's the updated SendPulse() method along with SendSpace().
1: public unsafe void SendPulse(long microSecs)
7: public unsafe void SendSpace(long length)
It's worth noting also that the guys from Iguanaworks and the guys from Microbric worked very hard, collaborating between Colorado and Australia, along with me chiming in occasionally, to get this to work. To the right you can see an image of the IR pulse on an oscilloscope. You can see the header on the far right, of 2400 microseconds, followed by 0,0,0,0,1,0,0,1,0,0,0,0.
Controlling the Robot from the Console
The next step is hooking it all up to a console app to try moving the robot around with the keyboard. At this point, the hard work is done and we can setup a simple key-checking loop that runs until you press Ctrl-C
1: class Program
3: const int FORWARD = 144;
4: const int BACKWARD = 2192;
5: const int RIGHT = 1168;
6: const int LEFT = 3216;
7: const int STILL = 656;
8: const int REPEAT = 4;
10: static void Main(string args)
12: using (IR ir = new IR("COM1"))
14: while (true)
16: ConsoleKeyInfo key = Console.ReadKey(true);
17: switch (key.Key)
19: case ConsoleKey.UpArrow:
20: ir.Send(FORWARD, REPEAT);
22: case ConsoleKey.DownArrow:
23: ir.Send(BACKWARD, REPEAT);
25: case ConsoleKey.RightArrow:
26: ir.Send(RIGHT, REPEAT);
28: case ConsoleKey.LeftArrow:
29: ir.Send(LEFT, REPEAT);
In the next article, we'll attach the Viper and IR classes to PowerShell and script enable them like the LOGO Turtle of years past.
The Microbric Viper can be ordered online in North America, check out www.microbric.com for North American distributors. It's only US$89 at Saelig and CAD$99 at RobotShop. They have a number of educational robots that can be assembled by kids of all ages and skill levels. They're great for the classroom, and include projects like Sumo Robots, and a line-following bot, as well as a Spiderbot that climbs rope - all from the same kit.
You can order the IR Transmitter/Receiver from IguanaWorks. The serial version works on Windows or Linux, and there's a Linux USB version. It's not just a Transmitter, but also a learning receiver that works with WinLIRC and turns your computer into a learning remote control.
As with all my projects, there's things that could be extended, added, and improved on with this project. Here are some ideas to get you started.
- Extend the IR program running inside the Viper to support other commands.
- Integrate the Viper with the Microsoft Robotics Toolkit.
- Use the IR class to control your home theater and television.
- Write a paint program that controls the Viper and draws with a pen what you draw in paint!
Have fun and have no fear when faced with the words - Some Assembly Required!
Scott Hanselman is the Chief Architect at the Corillian Corporation, an eFinance enabler. He has thirteen years experience developing software in C, C++, VB, COM, and most recently in VB.NET and C#. Scott is proud to be both a Microsoft RD and Architecture MVP. He is co-author of Professional ASP.NET 2.0 with Bill Evjen, available on BookPool.com and Amazon. His thoughts on the Zen of .NET, Programming and Web Services can be found on his blog at http://www.computerzen.com. He thanks his wife and Zenzo for indulging him in these hobbies!