Having the groundwork out of the way, the next challenge was to actually input and output those RC signals. This was a bit daunting, so I referenced the code of MultiWii as a starting point, having previously read through most of it. I started with input, as it’s a good thing to have when generating output. The general concept of reading the RC signals is that the time between pulses is recorded, and if it’s a sane value (between the expected minimum and maximum), it’s accepted as the input value. This is accomplished by setting up the AVR to fire an interrupt every time the input pins change, and then running a short burst of code for that interrupt (the “Interrupt Service Routine”, or ISR) which tracks the change in time for each pin. In order to prevent the interrupt running for pins I don’t care about and clogging up the CPU, only the Pin Change Interrupt (PCINT) for the bank of pins the RC input is on is enabled, and a mask (PCMSK) is applied to that interrupt so only the RC input pins trigger it. AVR pins are divided into ports, which are sets of bytes, so all the pins are in groups of 8, and have to be accessed in those groups. Fortunately, all the input pins were on one port and one PCINT bank, so things were a bit easier than they could have been.
When the ISR for the pin change runs, interrupts are disabled (to prevent interrupting the time-sensitive parts), the current time is recorded, the change in the RC pins is recorded (by applying a logical XOR to the current port value and the last recorded port value), and interrupts are re-enabled. Then, a for loop iterates through an index of the RC input pins, which contains their position in the port (a bitmask with the pin in question as a 1). For each pin, the previously found change is compared to the pin position with a logical AND, and if the result is boolean true, then the change in time is found for that pin by subtracting the last recorded time from the current time. If that value is within the expected limits, that is, between 900 and 2100 microseconds, it is recorded as the RC input value for that pin/channel. Otherwise, the value is discarded. Then, the current time is recorded as the new last time for that channel, and the for loop continues. After that’s done, the current port value is recorded as the new last port value, interrupt concludes, and we have our RC inputs.
The major disadvantage of these boards versus Arduino is that they have no USB interface, and the serial pins are hooked up to some trimpots. While programming is still easy enough, there isn’t any easy way to extract real-time data from the boards. So, whether or not my code was working was basically a guess, and I didn’t want to tackle output until I knew input was working. The only method of communication I had readily available on the board was the status LED. So, I set up the main loop to turn on the LED if the channel 0 input was above 1500 microseconds (the expected midpoint for RC PWM). Unsurprisingly, it didn’t work the first time. Since the program was and still is very small, I simply changed the conditions for turning on the LED and recompiled until it came on, then changed them a few more times until I knew what values my code was returning and how they corresponded to the actual output of the receiver. As it turned out, my code was actually fine the first time, I had just mistakenly set the maximum input value to 1100 instead of 2100, causing the code to throw out everything (my controller strangely didn’t have the expected full range, so it never got to 1100). Having solved input, it was time to tackle output.
The timer is configured using 3 registers TCCR1A, B, and C (Timer/Counter Control Register 1 A, B, and C). These define how fast the timer runs, what its max value is, and how the output pins are controlled. The registers are set using atomic logical ORs and ANDs, although they could just be set to a value since no other part of the program is or should be using them. I set the prescaler value to 64, meaning the timer ticked up once every 8 microseconds, and meaning that my output had a precision of 8 microseconds, which is about equivalent to 7 bits of precision for the range I was outputting in. Probably not good enough for flight control, but fine for a simple ground vehicle. I then set the Waveform Generation Mode to mode 3, 10-bit Phase Correct PWM. “Phase Correct” is important here - in short terms, is means the output waveforms are back to back, which effectively halves the frequency and doubles precision. This means I actually get about 8 bits of precision, which is what I wanted and expected. Lastly, I set both the output compare pins to mode 2 (COM1n1:0=2) so that they output the desired waveform. With the timers properly set up, the only thing left to do is set the Output Compare registers, which tell the timer when to turn the output pins on or off. The difficulty there is that my output value is in microseconds, but the timer is running at 8 microsecond intervals, and back to back. The solution seemed clear: divide my output value by four. My logic might be flawed, but what matters is that it didn’t work. The actual output was way off from what I expected, and in an odd way. I didn’t write down what the initial results were, and I’m writing this from a different computer so I don’t know numerically what exactly I did. Basically, I divided my output value by a constant, and subtracted another constant from that. Similar to testing with the LED earlier on, I just changed constants, recompiled, and repeated until I got the output I wanted. Not necessarily elegant, but effective and reliable.
I ran into a major roadblock when writing the output code: for a while, I was compiling and running code (out of desperation, at one point I just compiled the MultiWii code), and I was getting basically no output. The first few times, my waveform was sort of visible at the microvolt level and with completely wrong timing, but the correct response to the input. I spent an hour or so getting no results, then left it for a while. Then when I came back for round two, I just got electrical noise. No changes to the code could get any sort of output on those pins. I realized then that I had forgotten to set the output pins as outputs, by setting them high in the Data Direction Register. So I set them as outputs, and still got electrical noise. I put a simple blink program on it and confirmed that they were outputs, could be set high, and that my oscilloscope was working. I kept playing with it for another hour with no changes, which was very confusing, so I called it a day at that point. When I came in the next morning and turned everything on, the waveform was there on the oscilloscope as expected. I’m really not sure where it all went wrong, but it was probably tunnel vision of some sort. At that point I started changing my coefficient and offset to get the waveform I wanted, but that’s just a great example of how sometimes just taking a break and restarting can solve problems.
I went into this project knowing that it would be a challenge and knowing that there were easier ways, and it was every bit as fun as I expected. Staring at code for hours wondering why it’s not working and what could possibly be wrong is never fun, but getting to see the results when something finally works keeps me going, and the final functional product is always exciting to see, which definitely makes the whole process worth it. From a board that did nothing, to a Blink program, to an exploration of interrupts and timers, to a fully functional board that converts arcade drive to tank drive, the process was a great learning experience and goes to show that something seemingly simple like inputting and outputting a few RC signals can actually be a complex problem. Finding documentation and tutorials for my particular application was tough, so hopefully this article and the accompanying code can be of assistance to anyone trying to do something similar. Thanks for reading!
Update Jan. 14, 2016:
Here is the source code that this article discusses: