Projects Research & Design Hub

Build a Joystick Interface for Flight Sims

Written by Jeff Bachiochi

Using Trusted RC Hardware

Flight simulator programs are a great way to get the feel of flying, but using mouse or keyboard keys lessens the reality of the experience. In this project article, Jeff builds a joystick interface device that can wirelessly control aircraft on a flight simulation program. He uses trusted model airplane radio control (RC) electronics.

  • How to build a joystick interface device that can wirelessly control aircraft on a flight simulation program

  • How to understand the advantages of USB

  • How to install the joystick library

  • How to use pulse width modulation (PWM) for RC servo control

  • How to design the circuitry

  • How to adjust the data for the application

  • How to design the enclosure

  • Arduino Joystick Library

  • HelmPCB’s Serial Port Monitor

  • SparkFun’s Pro Micro development board

  • Flysky FS-A8S RC receiver

After months of investigating the 9/11 terrorist attacks, it was announced that the planes were commandeered by radicals who had never flown an actual plane in their lives. They had used a flight simulator app to learn how to fly. Simulators are a popular way to hone flight skills before ever taking your first flight with an instructor. In the model aircraft world, people often spend many hours building a model from balsa wood and special tissue paper.

As an aspiring pilot, I dreaded learning to use an RC (radio control) system during a live flight test of the model—a model that I’d just spent a good part of my life building from scratch. Bruce Artwick (subLOGIC Corp.) began the development of Flight Simulator in 1977 for multiple platforms. In 1981, it was released by Microsoft. I flew my first model aircraft in the 70s, when flight simulators were still a dream. So, for me, it was learn on the job. This meant numerous crashes, followed by long periods of no flight time until hours were expended resurrecting the damage to more than just my ego.

Today, we have simulators for all kinds of vehicles—aircraft, war machines, muscle cars—you name it. Newbies can learn to operate a vehicle before ever having to put their skills on the line with the actual model. That’s highly desirable if you want to prevent crashing a model airplane, but not so important if all you crash is a model car. For those RC buffs who want to relive the excitement of flying indoors, or just to keep skills honed, using a flight simulator is a great tool. A host of interfacing hardware is available.

While still offering the ability to fly using just a keyboard, today you can use real hardware. By that I mean external controllers. You can purchase yokes and foot pedals that emulate the hardware of a real plane. RCers can use their actual RC transmitters by purchasing a module that translates the RC signals into human interface device (HID)-compliant game controller movements. There is nothing like learning on the equipment you will also use to fly for real.

A HID-compliant “game system” controller is required for use on a PC. With a compliant interface, the PC won’t know that the controller being used is actually an RC system. This translation process is two-fold—one to look like a HID-compliant game controller and the second to interpret the RC system signal. Let’s take a look at the HID-compliant part first. You probably use a USB device every day. For us experimenters, we are always plugging in some USB microcontroller (MCU), like an Arduino, to get a serial connection to a PC for development purposes. This creates a virtual serial port and assigns it a COM port number.

Although you get a cute “bong” when you plug in a USB serial device, there is no identification of the COM port number. If you wish to connect to it, you have to know to which port number it has been assigned. The Device Manager gives you a list, but that’s just a list of available COM ports. If you watch the list as you remove the USB plug and replace it, you can see what gets removed and added to this list. What a pain! Download the “serial Port Monitor” tool from HelmPCB [1]. This little gem pops up a message with the COM port number, each time you plug something in!


USB is more than just a replacement for our older DB9/25 serial ports. They can take on special functions as long as you provide the USB drivers for your special needs. There are many standard drivers available to handle different classes of input—audio, communication, human interface devices, image, printer, mass storage and so on. We are interested in HIDs, like the mouse and keyboard. The game controller comes under the HID umbrella. So, on the PC side, no code is necessary, if the device we plug in conforms to that class. Our device, however, must have some very complicated code bundled with the application to look like a compliant HID device.

If you want to get deep into USB get Jan Axelson’s “USB Complete” [2]. While I have a second edition copy, a fifth edition is available that includes USB3.1. You’re probably familiar with FTDI’s USB-to-serial interface dongles. Many use them to go between the PC (USB) and an MCU (TTL serial). The Arduino UNO for instance, has this on board. The problem is that it doesn’t look like a HID device, because it’s for serial CDC (communications device class) only. But there are other versions of the Arduino that do not use this fixed interface. Arduinos that use the Microchip Technology (formerly Atmel) ATmega32U4 processor do not use this interface chip. Instead, they contain a USB interface, and it must be programmed to look like a USB CDC interface or other device—like a HID device. While the HID specification was originally meant for mouse and/or keyboard use, it has been expanded to include gamepad and joystick support. For the flight simulator, I want it to look like dual joysticks.

This creates a bit of a dilemma for the Arduino using the ATmega32U4 processor. To be programmed and look like a CDC device, the bootloader has to contain the code for programming the internal USB port. If the device has been programmed to be a HID device, it will initially program the USB for that function every time the device is powered up. In this mode, it can’t be programmed by the Arduino IDE. The board requires a physical double reset to initially have the bootloader program the USB as a CDC device for about 8 seconds, before jumping to its programmed application. While this process is also initiated via IDE, it can get out of sync, lose its COM port number and be assigned a different one.


We begin by installing the joystick library [3]. This library has plenty of examples that demonstrate how the library can be used for different input devices. We are interested in the “Flight Controller” example. The example will demonstrate the process without using any physical Arduino inputs, by simulating all inputs, so you can get feedback immediately. For this project, I chose the “Pro Micro” from SparkFun, since it’s only 0.7″ × 1.3″ (Figure 1). Just plug its USB into your PC, and you’re ready to program it up. You’ll notice that it initially comes up in your port list as “COMx (Arduino Leonardo).”

FIGURE 1 – The Pro Micro uses an ATmega32U4 processor on a 0.7” × 1.3” dual in-line module. Its small size makes it easy to embed into projects. It looks like an Arduino Leonardo to the Arduino IDE. Because the processor has built-in USB support, it can directly be programmed to “look” like different kinds of USB devices. This project creates an HID joystick controller for your PC.

After programming the example FlightControllerTest.ico, the COM port goes away, and that’s it. Since this is now a HID device, “serial Port Monitor” will announce that COMx has been removed. To find it, you’ll need to go to the Device Manager on your PC and look under Human Interface Devices for a “USB Input Device.” If all is well, you will find one of the devices has a “Bus reported device Descriptor” as “SparkFun Pro Micro” (Figure 2). In looking through the FlightControllerTest code, your two essentials—the library and the definition of the device—should look as shown in Listing 1.

FIGURE 2 – When the project is plugged into a USB port, its USB descriptor is read by the port, and Windows installs the proper driver that will allow it to be used as an HID joystick controller. It will not show up as a serial port, but as a Human Interface Device. You can verify this via the Computer Manager. Look for “SparkFun Pro Micro” in the “Input Device” property “HID reported device description” of one of the HID devices in the HID list.

#include "Joystick.h"

true, true, false, false, false, false,
true, true, false, false, false);

The joystick parameters set the ID and type, and set a maximum of 32 buttons with no hatswitches. NOTE: A hatswitch is a bit of a misnomer, because these knobs are analog. The following 11 parameters indicate which of the analog functions are enabled. They are:


The code indicates we are using XAxis (ailerons, wing flaps), YAxis (elevator, rear flap), RudderThrottle, by the Boolean true. This joystick function is enabled in the setup() function in one of two ways:

Joystick.begin(); // implied Joystick.begin(true);



If the parameter is true, then a message is sent every time any value changes. If the parameter is false, then a message is sent only when the user calls this function:

void sendState();

Now, here are the remaining functions available for this device library.

For the 11 analog devices:

void set?Range(int minimum, int maximum);
void set?(int value);


void setHatSwitch(byte hatSwitch, int value);

For each digital device:

void setButton(byte button, byte value);
void pressButton(byte button);
void releaseButton(byte button);

With this knowledge, we have what we need to connect with the HID interface. Now we need to collect information from the RC controller. Once we have that, we can use the functions already described to set each device with the appropriate values received from the RC Controller.

When we control an RC servo, we use pulse width modulation (PWM). The pulse width (ON time) determines the servo position. For a 180-degree servo, an ON time of 1,000µs produces 0 degrees (fully counter clockwise (CCW)), 1,500µs equals 90 degrees (centered) and 2,000µs produces 180 degrees (fully clockwise (CW)). The repetition rate isn’t required to be constant for a servo. To keep things orderly, most servo control uses a repetition rate of 20ms. This means that, with a PWM frequency of 50Hz, the pulse width varies between 5% ON time (minimum) and 10% ON time (maximum).

You’ll note that much time is wasted. We could eliminate much of the OFF time, and the servo would still work the same. In an RC system, we want to send the position of more than one servo through the radio and keep the transmission to a minimum, so each servo’s position is sent in succession, all packed together. Eight servos’ pulses (in their maximum positions) all packed together would be (500µs [OFF] + 2,000µs [ON]) × 8 = 20ms.

Note that while all 8 servos fit within the 20ms repetition rate, there is also an issue if all servos are at maximum. There is no sync or recognizable break (greater than the maximum ON time of a servo.) We could force this by sending less than 8 servo positions, in which case there would be an extra 2,500µs (seen as a break). Alternately, we could redefine the ON time as the time between falling edges, thereby eliminating the need for OFF times. And so, with this reduction, an RC radio can send 8 channels in 16ms, leaving a recognizable sync period even if all the servos are at their maximum ON time.

The packed signal now is of the pulse position modulation (PPM) type. The PPM frame is 20ms. There are n+1 pulses, where n is the total the number of servos plus a sync or break pulse. The time between falling edges is an indication of servo position or sync. While these PPM data is sent by the RC controller, at the receiver, this needs to be broken down into the individual servo outputs, one for each servo. Figure 3 shows a PPM stream, sent via the 2.4GHz transmitter, as the top trace. The bottom trace is the PWM channel 1 output of the receiver for that stream. When the joystick position is held at its maximum 2ms, note that the timing of the cursor on channel 1 in the top trace equals the PWM pulse for the channel 1 servo.

FIGURE 3 – The RC transmitter sends PPM data to the RC receiver. The receiver must dissect the PPM data to retrieve individual PWM data for each servo. A model aircraft uses individual servos to control various functions—including throttle, elevator, rudder, flaps and other goodies—directly. In a drone, the servos do not directly control the props. Instead, the flight controller will take the PPM stream directly, and make its own decision on how to control its props in response to the channel requests. This project takes advantage of the streaming PPM data.

In an RC airplane, these servos directly control the various flaps (and engine speed), to allow flight of the craft. In a drone, servo control does not directly control each motor. Instead an on-board flight computer intercepts the servo control from the receiver, and figures out how it should control the motors based on the pilot’s wishes. Although each servo output from the receive could be individually jumpered to a flight computer, this can be simplified by supplying the PPM signal instead of each of the individual PWM servo outputs. The transmitter has the ability to tell the receiver to supply this as an alternate output on channel 1. We will take advantage of this for our project by supplying the PPM from a receiver to our project’s circuitry (Pro Micro).


This could not get much simpler. I found one of the smallest RC receivers to use in this project. Available on Amazon, it is the Flysky FS-A8S Receiver [4]. It has four connections via two 3-pin headers. The PPM signal is on one connector, while power, ground and I-BUS are on the other. I-BUS is a two way digital protocol, so your radio can send data to your aircraft receiver, and the receiver can send data back to your transmitter such as battery voltage. The Pro Micro supplies power and ground to the receiver and gets PPM input via D2. So, the whole shebang runs off the USB connection. To measure the edges of the PPM signal, I’ll be using an interrupt—and the D2 input has this capability.

This interrupt routine is where data is collected from the RC controller’s servo and switch positions via the PPM stream coming into D2 on the Pro Micro. So, let’s start with a flow chart that puts the task into perspective (Figure 4). We need to perform three basic steps (modes) to retrieve data. First, we need to see how many channels are being sent. Mode 0 looks for the sync pulse—that’s the time where the pulse is longer than any channel’s maximum time—approximately 2ms. To be safe, I use 2,200µs. Second, once the sync has been found, we move to Mode 1, where the number of active channels are counted (between sync pulses). With this information, we can move into the third and final phase of gathering data, Mode 2.

FIGURE 4 – This is the interrupt flow chart that will measure timing data from the PPM stream. It determines how many channels are being sent, and stores the channel data into an array. Each time the array has been filled, the gotData flag is set to indicate the data may be processed in the main loop() function.

As each falling edge interrupt occurs, we determine the time since the last interrupt. If its more than 2,200µs, then this is a sync pulse. And if it’s less than 2,200µs, then this is a servo pulse. We know from the PPM that following a sync time, each servo is sent in succession. We will save the pulse measurements in an array, channel[n]. When another sync comes around, we can check to see that we’ve accumulated the right number of channels, and either set goodData=true (to flag data ready) or set the mode=0 (start all over). The loop() function can operate on the data when goodData=true.


Before passing the data to the joystick interface, we have a chance to alter or adjust the data in any way we might see fit. The data collected is in the form of a number (actually microseconds) for each servo. This will be 1,000 (µs) for the minimum position, and 2,000 (µs) for the maximum position. We can easily subtract 1,000 to get values of from 0-1,000. However, the Arduino has a couple of useful math functions we can use here.

Constrain(variable, min, max) is a function that will restrict your variable to values between min and max. If by chance the servo position measurements produce numbers slightly less than 1,000 or slightly greater than 2,000, this function will keep the result between your min and max limits. There is no scaling or rounding, just a truncating of the measurement. Now you could subtract 1,000 without worrying about potential negative numbers.

The map() function will linearly scale values. Suppose you want the result from the above example, 0 (min) – 1,000 (max), to be a value between 0-1,023 (full 10 bits). Your function’s parameters would be map(variable, 0, 1000, 0, 1023). You can even use this to get an inverted range—simply reverse the last two parameters.

As it turns out, you have a little control of this through the joystick setup parameters in some simulators (Figure 5). The defaults for each parameter are minimum=0 and maximum=1,023. However, you can change these in the setup() function by calling them out as in this example for the XAxis:

void setXAxisRange(int minimum, int maximum)

FIGURE 5 – Some simulators, such as the “Flying Model Simulator,” have a mapping and configuration function that allows the pilot to redirect and/or invert each joystick channel’s data.

I’ll be using the defaults, so I convert all data on the fly to this format. The main loop() function doesn’t do a thing until we’ve gotData (Listing 2). The first little routine waits 0.5 seconds before toggling an on-board LED, which continues to blink whenever PPM data is received and converted. This is just a visual indication that data is being received. Now, before transferring the data, the conversion is done, PPM2Analog(). Finally, the transfer is made from the first four channels to their appropriate axes, and the state of all the axes are sent through the USB connection as HID data. Alternately, we can enable auto changes by the Boolean parameter used in the setup() function, to enable the joystick function, as discussed earlier.

void loop()
unsigned long presentPulseWidth = micros();
lastDelayPulseWidth = presentPulseWidth + 500000;
gotData = false;


Advertise Here

LISTING 2 – This shows the transfer being made from the first four channels to their appropriate axes, and the state of all the axes are sent through the USB connection as HID data.


Most RC controllers have the ability to swap controls around for left- or right-handed pilots. Although you can reassign any channel to any function, most joysticks are spring-loaded to return to the center when released. The throttle is an exception. It has no spring release and remains where it’s set. My point is, you should check to make sure you’ve assigned each stick’s function (channel) to the proper joystick functions.

I wanted to put this month’s project’s prototype PCB into a small enclosure. In looking through my inventory, I found one that I had bought at RadioShack years ago. RadioShack supported experimenters by supplying components in their local stores. This was before online distributors existed. I bought my first computer, a TRS80, from the local RadioShack store. While driving 20 minutes to RadioShack still beats any shipping delivery times, sadly many of their stores have closed. When you bought an enclosure from them, you got a complete package, as shown in Figure 6. Note the price—$2.95! You can see the finished project in Figure 7.

FIGURE 6 – I’m finally getting around to using this enclosure purchased from RadioShack years ago. It not only contains the enclosure, but also a proto PCB and all mounting hardware, rubber feet and labels to really complete a project. Nice! You don’t know what you’ve got until it’s gone.
FIGURE 7 – The finished project. I made use of the hole in the enclosure top for the “operational LED.” The receiver is stuck to the PCB with a piece of double-sided foam tape. Its antenna sticks out through the enclosure, but I might just route it on the inside. I only had to make one hole for the USB connection.

Now I can enjoy a simulation experience with the same feel I’ll have when flying a model aircraft. I also don’t have to worry about long cables for the controller. The experience is even more enjoyable by casting the simulator to my HDTV! Fly on! Too much to do, so little time. 



[1] Serial Port Monitor
[2]  “USB Complete” ISBN 978-1-931448-28-4
[3] Joystick Library
[4] Flysky A8S Receiver
8CH 2.4Ghz Mini S-Bus PPM Receiver i-Bus for Flysky i4 i6 i6S i6X TM10 TM8 Transmitter
Amazon link:

Microchip Technology |
Sparkfun |


Keep up-to-date with our FREE Weekly Newsletter!

Don't miss out on upcoming issues of Circuit Cellar.

Note: We’ve made the Dec 2022 issue of Circuit Cellar available as a free sample issue. In it, you’ll find a rich variety of the kinds of articles and information that exemplify a typical issue of the current magazine.

Would you like to write for Circuit Cellar? We are always accepting articles/posts from the technical community. Get in touch with us and let's discuss your ideas.

Sponsor this Article
Website | + posts

Jeff Bachiochi (pronounced BAH-key-AH-key) has been writing for Circuit Cellar since 1988. His background includes product design and manufacturing. You can reach him at: or at:

Supporting Companies

Upcoming Events

Copyright © KCK Media Corp.
All Rights Reserved

Copyright © 2024 KCK Media Corp.

Build a Joystick Interface for Flight Sims

by Jeff Bachiochi time to read: 15 min