UPDATE: Yay, this page got featured in Hack a Day!
I recently picked up on some PIC18Fx553s with a built-in USB transceiver. After playing around with the Microchip USB Framework I decided on a weekend project of a USB IR remote control receiver for my HTPC. My initial idea was to reverse-engineer my RC's protocol, implement it on the receiver side, and connect as a HID Keyboard on the USB side to simulate key presses.
After reading a little about protocols in use by RCs, I came to a conclusion there's a lot of fragmentation in that area, and that I didn't have a clue to what protocol my RC [Toshiba CT-90319] uses. (I did have a feeling though that it wasn't the simple and very common RC-5)
After understanding there are many IR protocols in use, I decided I wanted to keep the hardware generic, so it could be re-used if I change my RC. The idea now was to receive the raw IR signal and send it to the PC, where it would be decoded and key press will be simulated according to some configuration.
Since this was supposed to be a weekend project, I didn't have the time to reverse my RC's IR protocol. This was when I remembered the LIRC project.
LIRC is built of several layers - but we'll be interested only in one: the hardware driver. Once you get you're hardware talking the LIRC protocol, we can utilize all its facilities - namely, the IR protocol auto-detector and decoder and the LIRC API. I also learned that XBMC - the media center I was using - has a built-in LIRC integration so I wouldn't need to inject keyboard and mouse events.
All the files are available on the GitHub project page.
Since almost everything was going to be implemented in software - the hardware is simply a relay of IR data, and could be implemented even before I knew what's going to be on the PC end.
The hardware is pretty standard. Some support hardware for the MCU (bypass caps, crystal, USB connector, ICSP header) with an LED and the IR module connected to an Interrupt-on-change with a built-in pull-up pin.
The connector layout in the schematics is weird because of some PCB constraints I encountered later on while casing the project.
Everything went well, except I had one problem while prototyping: Everything was working when I powered the board with 3.3V, but when powered by 5V the USB device wouldn't enumerate on the host. After several frustrated hours I realized I didn't connect the
Vusb pin to large-enough bypass cap. This problem repeated itself in a different form later on...
I built the circuit on a breadboard, and set out to complete the integration with LIRC on the PC side.
This is the heart of the project - interfacing with LIRC. My initial idea was to look for a device in LIRC's supported device list that relays raw IR somehow, and implement the protocol in the firmware based on the driver's code.
Unfortunately, every USB hardware driver was talking to a device that already decodes the signal in firmware - exactly what I wanted to avoid (I need auto-detection because I don't know which protocol my remote use, remember?)
After reading the code for almost every USB driver in LIRC, I came to realize none will suite my needs. Though now I had a good understanding of LIRC's API to userspace, I could have implemented my own USB protocol and write an LIRC driver to it - I've written Linux drivers before.
However, I wanted to avoid that as well - it would be a lot simpler if I could get my device to work with a vanilla LIRC, or the one that comes with Debian and Ubunut for that matter - it would save me the trouble re-compiling my driver every time LIRC gets updated, or its API changes.
After some searches, I came across an Old Nabble thread with a problem similar to mine. They were trying to patch up the UDP driver to serial port. but were having some trouble with it.
A quick look on the UDP driver's code revealed that it expects two-byte packets, each represents a receiver event. The MSbit tells whether this is a pulse or a space, and the rest is the time the event lasted, in 1/16384 of a second. Since I didn't want to write a driver on my own, and UDP is session-less, I decided to implement the UDP protocol in firmware, and write a little script that would poll on the USB serial port, and send it in UDP.
I've started writing a small Python script that does just that, when I came to realize I was implementing a piece of software I find very familiar - socat! With proper integration with udev, I could avoid writing code almost entirely!
All I had to do was to write a udev rule to start socat on every serial USB device with my VID/PID that will relay the data from the tty to lircd over UDP.
I've wrapped socat with a little adapter script and a configuration file that also logs to syslog, just like a true daemon.
The firmware is fairly simple. The receiver is connected to an interrupt-on-change pin, with an internal weak pull-up resistor (receiver is active-low).
The ISR in receiver.c will calculate the time passed since the last event using a software timer, and prepare an LIRC packet to be sent. The packet is queued to a common send-queue, and the LED is blinked to acknowledge the receiver received the keypress.
The queue is a simple cyclic-buffer with no overwrite. Once the queue is full data is discarded. It is implemented in queue.c. It currently uses global variables (since there's only one queue in the system) but it could be easily packed into a struct and support multiple queues.
The program's main loop simply checks if there's something in the queue, and sends it over USB. It will also blink the LED for some feedback.
I had the prototype board sitting on my HTPC for several weeks, working perfectly, and I was ready to build the real thing.
Before I could start laying out the board, etching and soldering, I had to decide how the project's gonna be cased. This will most likely yield constraints on my PCB.
I didn't want to settle for a store-bought project-box. This is a project that's going to sit in the middle of my living room, and it has to look good.
I started looking for ideas, when I found a nice, small, aluminum pocket LED flashlight in a dollar store which was perfect.
It was aluminum (I love aluminum), it had a window in the front for the receiver, and a rubber button in the back I could get the wire out through. There was one thing that could be a problem - it was very small. (rules is in centimeters)
Before I could actually build anything, I had to make sure the project fits into the case. The first thing I needed was the board's size - how much copper can I layout the board on. I started by cutting a copper-clad board to the approximate size, and filing it down till I was able to fit it into the case. I was left with a 30x18mm board:
Once I measured the board's size, I layed-out the components with KiCAD. Since the board was so small, I had to compromise on the location of the ICSP pins, which looked like quite a mess when I connected to board for programming.
The next step was to make sure the entire setup fits in the case. Pins, capacitors and wires take space, and I needed to make sure the complete circuit fits inside the case. In order to test that, I printed the tracks on plain paper, and cut a cardboard to the size of the PCB. I then glued the tracks to the cardboard, and assembled the components on to it. It fitted perfectly.
This was the first double-sided board I was doing, after several one-sided boards. I've used the toner-transfer method successfully - so that was what I was going to use. Since etching a double-sided PCB requires perfect alignment, I decided to etch one side at the time. I transferred the toner to one side, and covered the other with plastic tape. Once that side was etched, I washed off the toner and drilled the board. With a pin, I punctured the other side's toner mask, and using the through-hole components themselves, I aligned the other toner mask to the copper.
Soldering the board followed. Aside from being my first double-sided board, it was my first surface-mount board - and a small and crowded one at that. I took my time, working through the magnifying glass almost exclusively:
All that was left was to program the device:
... and make sure that it worked.
Problems and Troubleshooting
It didn't work. At all. I couldn't get the computer to recognize the USB device - I was getting bus and enumeration errors. I checked the USB cable solders, did a connectivity test and all was well. I tried looking at the crystal signal with an oscilloscope - but everything looked good. The MCU was getting power and a good clock signal, it's USB pins were correctly connected to the USB cable, it had the exact same firmware loaded into the breadboard prototype - which was working - but the final build was a dud.
I was getting frustrated. After hours of trying to figure out what was wrong I was ready to give up and admit failure - something wasn't right I didn't had what it takes to figure it out. (I am just a hobbyist, after all)
The next day, I came back to the build thinking: this build should be identical to the prototype, that was still working. Then it hit me - because I had USB problems with the prototype, when the
Vusb capacitor wasn't large enough, I enlarged the bypass cap - that was the difference!
I tested the voltage on the
Vusb pin, which read be around 3.3V, but wasn't reading any voltage at all. Unfortunately, my next conclusion was that the bypass cap. I used may have been too big, which may have caused long high-current drawing from the on-die USB transciever during power-up - which probably destroyed the on-die USB voltage regulator! bummer...
Replacing the cap. was easy - replacing the PIC was a pain. I didn't have hot-air workstation available, and de-soldering the chip's 28 pins without destroying the board was very challenging. I ended up working with the solder and an x-acto knife, carefully bending each pin upwards.
After replacing the cap. and chip (and re-programming the device) - everything worked and was ready for casing. But before, I padded the inside of the (conducting!) aluminum casing with black plastic tape.
The project, as it's presented above, sat on my HTPC for a few days, rolling from side to side once in a while (which didn't affect it's function, but wasn't very pretty).
After looking for a bit, I found my perfect solution - an old indoor TV antenna I had from when I bought a DTV receiver. Conveniently enough, it had a small magnet on the bottom, and a nice screw that connected to the antenna:
I measured to screw's radius, and chose a drill slightly smaller - so later I could mill the screw's grooves in the hole. After drilling the initial hole, I very carefully force-screwed the base into the hole, milling the grooves for the screw with the screw itself. This took several attempts. (that almost scratched the paint!)
The final step was to hide everything that's inside the case. I simply glued a camera film over the clear protection, to get the final result:
This project started as a very ambitious R&D project which included reverse-engineering of an IR protocol, implementation of a decoder in firmware, and at some point Linux kernel development.
As the project progressed, I came up with more ways not to do anything, but re-use existing tools I learned about. This allowed me to complete a generic and working prototype in a single weekend. It was tempting to implement everything on my own - but eventually finding the right tools instead of developing them was pretty rewarding - I could focus on what was most important: the project's appearance in my living room :)
The build was challenging. I took on myself several challenges I haven't dealt with before: Working on a very small board, which made me use SMDs for the first time and forced me to do a double-sided board. It took some time, but slow, careful and planned work allowed me to get the hardware build right the first time.
It was very disappointing to discover the build I worked on so hard wasn't working. It took every bit of patience not to throw the project and keep looking for the problem - eventually, it payed off - which made me face another challenge: removing the chip from the board without destroying it.
For over 6 months this build is serving us reliably, on a daily basis.