Note: staff-provided content does not represent an official statement from FARGOS Development, LLC. The policy on staff-authored content can be found here.
Go back to Geoff's home page.
Note: this is one of a set of three related notes on converting Thrustmaster F-22 Pro joysticks, Thrustmaster F-16 TQS and CH Pro Rudder Pedals from obsolete game port interfaces to modern USB-based connectivity. They use an Adafruit ItsyBitsy 32u4 5V board to interface to the existing game controller circuitry and publish the respective state as a USB Human Interface Device.
This set of conversions makes provision for a momentary push button used to trigger a self-calibration cycle. The accompanying software will react to such a button press by flashing the indicator LED rapidly and monitoring the perceived bounds of the various analog inputs. The user should exercise the limits of all of the respective axis associated with the device within the calibration period (typically 15 seconds). The obtained values are saved persistently to the device's EEPROM, thus enabling calibration to survive power cycle events.
The Thrustmaster F-16 Throttle Quadrant System was a decent, programmable throttle available in the 1990's that used the game port interface as well plugging into the keyboard port (either the old 5-pin DIN connector or the PS/2 port). It could be used with a Thrustmaster joystick as well as rudder pedals, which made for some complicated daisy-chained wiring harnesses. Unfortunately, Microsoft stopped supported the game port interface with the release of Microsoft Vista in 2006.
The programming capabilities of the Thrustmaster F-16 TQS and attached joystick were quite feature rich. Most of the definition language was focused on emitting characters in response to button presses or movement of the various potentiometers. None of those capabilities are reproduced by this USB conversion. Instead, all of the buttons and the axes are exposed and either a modern game allows the user to customize their control setup or one can make use of third-party software such as AntiMicroX as an intermediate device layer.
Rather than make use of black-box software such as MMJoy2, the decoding of electrical signals is handled via an Arduino sketch and exposed using the Arduino Joystick library. The Arduino sketch implementing the control logic is found at the bottom of this document.
The F-16 TQS contains three potentiometers that are used to to measure the relative position of the throttle and two dials that corresponded to the antenna and range adjustments on the F-16 throttle. There are three wires to the throttle potentiometer that carry the voltage in, ground and relative voltage out, which varies depending on the position of the throttle handle. The signals for the antenna and range rotary dials are carried within the 14-pin IDC plug discussed below.
| Color | Function | Board Pin |
|---|---|---|
| White | +5 VDC | 5V |
| Gray | X-Axis | A0 |
| Purple | Ground | GND |
The F-16 TQS handle has a hat switch, two 3-position switches, a mouse button, a push button on the range dial and 2 rotary dials. All of these signals are carried using a 14-wire cable terminated with a 2x7 IDC plug. Some of the switches can latch, while others are momentary.
| Relative | Color | Function | Board Pin |
|---|---|---|---|
| 1 | Brown | Dog Fight Left | 12 |
| 2 | Red | Speed Brake Right | 8 |
| 3 | Orange | Dog Fight Right | A5 |
| 4 | Yellow | Ground | GND |
| 5 | Green | Mouse Button | 11 |
| 6 | Blue | +5 VDC | 5V |
| 7 | Purple | Antenna | A1 |
| 8 | Brown | Range | A2 |
| 9 | White | Speed Brake Left | 10 |
| 10 | Blue | Range Knob Push | 6 |
| 11 | Brown | Hat Left | 9 |
| 12 | Red | Hat Right | 4 |
| 13 | Orange | Hat Down | 7 |
| 14 | Yellow | Hat Up | 5 |
Optional self-calibration is supported. A calibration cycle is triggered by depressing a normally open momentary push button switch. Feedback is provided via an indicator LED.
| Function | Board Pin | Notes |
|---|---|---|
| Calibrate | MISO | Other side of switch is connected to ground |
| LED | MOSI | Other side of LED is connected to ground |
| Ground | GND | One leg each of the switch and LED are connected to the common ground |
No destructive changes need be made to the handle wiring. The throttle joystick can be connected using a block of 3 pins headers with the common 2.54mm spacing. The handle wires should be connected using a 14-pin (2x7) IDC socket so that the existing cable can be plugged in without requiring modification.
The images shown below illustrate the use of 14-pin IDC socket for the TQS handle wires, a 3-pin 90-degree header for the throttle handle potentiometer and a 3-pin JST connector for the indicator LED and calibration request push button.
The source code for the F-16 TQS interface is illustrated below. The most current release can be retrieved from this F16_TQS.ino download link. The .ino files are really C++ source with an alternate file suffix to permit association with the Arduino IDE application.
The comments within the source code provide more detail that will not be repeated here.
For the purposes of programming, the Adafruit ItsyBitsy 32u4 5V is nearly identical to an Arduino Leonardo. Because we want the resulting device to appear correctly named under the Game Controllers control panel, a custom entry is added to the local boards.txt file. It is a duplicate of the leonardo board entry with changes made to the build, build.vid, build.pid and build.usb_product lines. The changes are highlighted below.
itsybitsy_tqs.name=F16 TQS Thrustmaster atmega32u4
itsybitsy_tqs.build.vid=0x2341
itsybitsy_tqs.build.pid=0x1023
itsybitsy_tqs.build.usb_product="Thrustmaster F-16 TQS"
/*! \brief Thrustmaster F-16 TQS USB conversion using * Adafruit ItsyBitsy 32u4 5V. https://www.adafruit.com/product/3677? * * \author Geoff Carpenter gcc@fargos.net http://www.fargos.net/gcc.html Requires additions to boards.txt found in: "C:\Users\${USER}\AppData\Local\Arduino15\packages\arduino\hardware\avr\1.8.8\boards.txt" Clone of leonardo board, modified name, build.vid, build.pid and build.usb_product. itsybitsy_tqs.name=F16 TQS Thrustmaster atmega32u4 itsybitsy_tqs.vid.0=0x2341 itsybitsy_tqs.pid.0=0x0036 itsybitsy_tqs.vid.1=0x2341 itsybitsy_tqs.pid.1=0x8036 itsybitsy_tqs.vid.2=0x2A03 itsybitsy_tqs.pid.2=0x0036 itsybitsy_tqs.vid.3=0x2A03 itsybitsy_tqs.pid.3=0x8036 itsybitsy_tqs.upload_port.0.vid=0x2341 itsybitsy_tqs.upload_port.0.pid=0x0036 itsybitsy_tqs.upload_port.1.vid=0x2341 itsybitsy_tqs.upload_port.1.pid=0x8036 itsybitsy_tqs.upload_port.2.vid=0x2A03 itsybitsy_tqs.upload_port.2.pid=0x0036 itsybitsy_tqs.upload_port.3.vid=0x2A03 itsybitsy_tqs.upload_port.3.pid=0x8036 itsybitsy_tqs.upload_port.4.board=leonardo itsybitsy_tqs.upload.tool=avrdude itsybitsy_tqs.upload.tool.default=avrdude itsybitsy_tqs.upload.tool.network=arduino_ota itsybitsy_tqs.upload.protocol=avr109 itsybitsy_tqs.upload.maximum_size=28672 itsybitsy_tqs.upload.maximum_data_size=2560 itsybitsy_tqs.upload.speed=57600 itsybitsy_tqs.upload.disable_flushing=true itsybitsy_tqs.upload.use_1200bps_touch=true itsybitsy_tqs.upload.wait_for_upload_port=true itsybitsy_tqs.bootloader.tool=avrdude itsybitsy_tqs.bootloader.tool.default=avrdude itsybitsy_tqs.bootloader.low_fuses=0xff itsybitsy_tqs.bootloader.high_fuses=0xd8 itsybitsy_tqs.bootloader.extended_fuses=0xcb itsybitsy_tqs.bootloader.file=caterina/Caterina-Leonardo.hex itsybitsy_tqs.bootloader.unlock_bits=0x3F itsybitsy_tqs.bootloader.lock_bits=0x2F itsybitsy_tqs.build.mcu=atmega32u4 itsybitsy_tqs.build.f_cpu=16000000L itsybitsy_tqs.build.vid=0x2341 itsybitsy_tqs.build.pid=0x1023 itsybitsy_tqs.build.usb_product="Thrustmaster F-16 TQS" itsybitsy_tqs.build.board=AVR_LEONARDO itsybitsy_tqs.build.core=arduino itsybitsy_tqs.build.variant=leonardo itsybitsy_tqs.build.extra_flags={build.usb_flags} */ /* There are three (3) potentiometers in the handle that are used to encode the * throttle position, and two (2) additional rotary controls for the antenna * and range knobs. The voltage fed to the potentiometers depends upon the * MCU in use; 3.3 volts is common, but 5 volts would be used with an * Adafruit ItsyBitsy 32u4 5V. * * Throttle Grip Layout & Controls * _ Main Throttle Axis: Linear forward-and-back analog signal. * + Antenna Elevation: large rotary control with center detent. * + Range Knob: small rotary control knob that also acts a push button. * + Dogfight / Missile Override Switch: 3-position toggle switch on the top * of the grip. Only the left and right positions are detectable; * no signal is emitted from the center position. * + Speed Brake Switch: 3-position switch, only the left and right positions * are detectable. Conceptually represents open/hold/close. * No signal is emitted from the center position. The left position is a * momentary switch and resets back to the center position when released. * The right position is held until manually changed. * + COMM Switch: 4-way hat switch. * + Cursor Controller: (NOT YET SUPPORTED) Pressure-sensitive "eraserhead" * mini-stick for mouse movement. */ /*! \brief GPIO pin to read X-axis analog signal. */ #define THROTTLE_AXIS_PIN A0 /* gray wire from potentiometer */ /*! \brief GPIO pin to read antenna knob. */ #define ANTENNA_AXIS_PIN A1 /* purple wire pin 7 */ /*! \brief GPIO pin to read range knob */ #define RANGE_AXIS_PIN A2 /* brown wire pin 8 */ #define DOG_FIGHT_LEFT 12 /* brown wire pin 1 */ #define DOG_FIGHT_RIGHT A5 /* orange wire pin 3 */ #define SPEED_BRAKE_LEFT 10 /* white wire pin 9 */ #define SPEED_BRAKE_RIGHT 8 /* red wire pin 2 */ #define MOUSE_BUTTON 11 /* green wire pin 5 */ #define RANGE_KNOB_BUTTON 6 /* blue wire pin 10 */ #define HAT_LEFT 9 /* brown wire pin 11 */ #define HAT_RIGHT 4 /* red wire pin 12 */ #define HAT_DOWN 7 /* orange wire pin 13 */ #define HAT_UP 5 /* yellow wire pin 14 */ /* yellow wire pin 4 goes to ground */ /* blue wire pin 6 goes to +5V */ /*! \brief Support internal calibration */ #define SUPPORT_CALIBRATION 1 #if SUPPORT_CALIBRATION /*! \brief GPIO pin used use for momentary push button to trigger manual calibration. The button is between ground and this GPIO pin. */ #define CALIBRATE_BUTTON_PIN MISO /*!\brief GPIO pin used to drive an indicator LED. */ #define INDICATOR_LED MOSI #endif /*! \brief Amount of time manual calibration cycle lasts */ #define CALIBRATION_DURATION_MS (15 * 1000) /*! \brief Sample rate per second. Not guaranteed to be achieved, but rate will be no faster. */ #define SAMPLE_RATE_PER_SECOND 100 /*! brief Delay between polling cycles in milliseconds. Normally derived from SAMPLE_RATE_PER_SECOND. */ #define POLL_DELAY_MS (1000 / (SAMPLE_RATE_PER_SECOND)) #define TQS_TOTAL_BUTTON_INPUTS 10 /*! \brief Set to 0 if only the primary hat switch should be exposed. The remaining are exposed as distinct buttons.*/ #define EXPOSE_ALL_HAT_SWITCHES 1 #if EXPOSE_ALL_HAT_SWITCHES == 1 #define TQS_BUTTONS (6) #define TQS_HAT_SWITCHES 1 /* hat switches are reported as 0, 45, 90, 135, 180, 225. 270, 315 degrees*/ #else #define TQS_BUTTONS (10) #define TQS_HAT_SWITCHES 0 /* hat switches are reported as 0, 45, 90, 135, 180, 225. 270, 315 degrees*/ #endif /* Unique id for device to be handed to Joystick constructor. */ #define MY_TQS_JOYSTICK_ID 5 #define LOG_ENABLED 1 /* These log interfaces are compatible with the advanced thread-safe * logging API made available by FARGOS Development, LLC. * See http://www.fargos.net/documents/FARGOSutilsLibrary.html */ #if LOG_ENABLED > 0 #include <Streaming.h> #if LOG_ENABLED > 2 #define LOG_COUT(level) Serial << F(__FILE__) << F(":") << __LINE__ << F("\t") << F(#level) << F("\t") #else #define LOG_COUT(level) Serial << F(":") << __LINE__ << F("\t") << F(#level) << F("\t") #endif #define LOG_ENDLINE endl #endif /*! \brief Console baud rate * * This value should match the baud rate selected in the Arduino IDE's * serial monitor window or a dedicated terminal program like Putty. */ #define CONSOLE_BAUD_RATE 115200 #include <Joystick.h> #include <EEPROM.h> #define MAX_ANALOG_VALUE 1023 #define ANALOG_MIDPOINT (((MAX_ANALOG_VALUE + 1) / 2) - 1) #define MAX_AXIS_VALUE (((MAX_ANALOG_VALUE + 1) / 2) - 1) #define MIN_AXIS_VALUE (-MAX_AXIS_VALUE) // Initialize the Joystick (with 8 buttons enabled) static Joystick_ Joystick(MY_TQS_JOYSTICK_ID, JOYSTICK_TYPE_JOYSTICK, TQS_BUTTONS, TQS_HAT_SWITCHES, false, false, false, false, false, false, false, true, true, true, false); static const uint8_t pinList[] = { DOG_FIGHT_LEFT, DOG_FIGHT_RIGHT, SPEED_BRAKE_LEFT, SPEED_BRAKE_RIGHT, /* Speed brake right latches */ MOUSE_BUTTON, RANGE_KNOB_BUTTON, /* keep hat switches at end */ HAT_LEFT, HAT_RIGHT, HAT_DOWN, HAT_UP }; static const char compiledOnDate[6 + 1] = { // YY year __DATE__[9], __DATE__[10], // First month letter, Oct Nov Dec = '1' otherwise '0' (__DATE__[0] == 'O' || __DATE__[0] == 'N' || __DATE__[0] == 'D') ? '1' : '0', // Second month letter (__DATE__[0] == 'J') ? ((__DATE__[1] == 'a') ? '1' : // Jan, Jun or Jul ((__DATE__[2] == 'n') ? '6' : '7')) : (__DATE__[0] == 'F') ? '2' : // Feb (__DATE__[0] == 'M') ? (__DATE__[2] == 'r') ? '3' : '5' : // Mar or May (__DATE__[0] == 'A') ? (__DATE__[1] == 'p') ? '4' : '8' : // Apr or Aug (__DATE__[0] == 'S') ? '9' : // Sep (__DATE__[0] == 'O') ? '0' : // Oct (__DATE__[0] == 'N') ? '1' : // Nov (__DATE__[0] == 'D') ? '2' : // Dec 0, // First day letter, replace space with digit __DATE__[4] == ' ' ? '0' : __DATE__[4], // Second day letter __DATE__[5], '\0' }; static unsigned long startCalibrateMode_ms; #define TOTAL_AXES 3 static const uint8_t axisPin[TOTAL_AXES] = { THROTTLE_AXIS_PIN, ANTENNA_AXIS_PIN, RANGE_AXIS_PIN }; struct AxisCalibrationData { uint16_t minCalibration; uint16_t maxCalibration; uint16_t centerCalibration; }; static AxisCalibrationData calibrationData[TOTAL_AXES] = { { 0, MAX_ANALOG_VALUE, (MAX_ANALOG_VALUE + 1) / 2 }, { 0, MAX_ANALOG_VALUE, (MAX_ANALOG_VALUE + 1) / 2 }, { 0, MAX_ANALOG_VALUE, (MAX_ANALOG_VALUE + 1) / 2 } }; static uint8_t indicatorState; static uint32_t indicatorBlinkCount; static uint32_t indicatorBlinkRate; static unsigned long indicatorBlinkUntilMillis; static void load_calibration_data(uint8_t axisId) { uint8_t *record = reinterpret_cast<uint8_t *>(calibrationData + axisId); uint8_t *base = reinterpret_cast<uint8_t *>(calibrationData); int offset = record - base; EEPROM.get(offset, calibrationData[axisId]); if (calibrationData[axisId].minCalibration == ~0) { // not set in EEPROM, assign default #if LOG_ENABLED LOG_COUT(info) << F("set minCalibration[") << axisId << F("] to 0") << LOG_ENDLINE; #endif calibrationData[axisId].minCalibration = 0; } if (calibrationData[axisId].maxCalibration == ~0) { // not set in EEPROM, assign default #if LOG_ENABLED LOG_COUT(info) << F("set maxCalibration[") << axisId << F("] to MAX_ANALOG") << LOG_ENDLINE; #endif calibrationData[axisId].maxCalibration = MAX_ANALOG_VALUE; } if (calibrationData[axisId].centerCalibration == ~0) { // not set #if LOG_ENABLED LOG_COUT(info) << F("set centerCalibration[") << axisId << F("] to midpoint") << LOG_ENDLINE; #endif calibrationData[axisId].centerCalibration = (calibrationData[axisId].minCalibration + calibrationData[axisId].maxCalibration) / 2; } #if LOG_ENABLED LOG_COUT(info) << F("loaded calibration[") << axisId << F("] min=") << calibrationData[axisId].minCalibration << F(" max=") << calibrationData[axisId].maxCalibration << F(" center=") << calibrationData[axisId].centerCalibration << LOG_ENDLINE; #endif } static void save_calibration_data(uint8_t axisId) { uint8_t *record = reinterpret_cast<uint8_t *>(calibrationData + axisId); uint8_t *base = reinterpret_cast<uint8_t *>(calibrationData); int offset = record - base; EEPROM.put(offset, calibrationData[axisId]); } static uint32_t totalReadCount; static int32_t read_joystick_pin(uint8_t pin, uint16_t minValue, uint16_t maxValue, uint16_t midpoint, bool invert = false) { totalReadCount += 1; int16_t value = analogRead(pin); if (invert) { value = maxValue - value; if (value < 0) value = 0; } if (value < minValue) value = minValue; if (value > maxValue) value = maxValue; int32_t scaled_result; if (value < midpoint) { int32_t range = (midpoint - minValue) + 1; int32_t offset = midpoint - value; if (offset >= range) offset = range - 1; scaled_result = (offset * ANALOG_MIDPOINT) / range; scaled_result = ANALOG_MIDPOINT - scaled_result; if (scaled_result < 0) scaled_result = 0; } else { int32_t range = (maxValue - midpoint) + 1; int32_t offset = value - midpoint; if (offset >= range) offset = range - 1; scaled_result = (offset * ANALOG_MIDPOINT) / range; scaled_result += ANALOG_MIDPOINT; if (scaled_result > MAX_ANALOG_VALUE) scaled_result = MAX_ANALOG_VALUE; } int32_t result = (scaled_result * (MAX_ANALOG_VALUE + 1)) / ((MAX_AXIS_VALUE - MIN_AXIS_VALUE) + 1); #if LOG_ENABLED > 3 LOG_COUT(info) << F("pin=") << pin << F(" val=") << value << F(" minVal=") << minValue << F(" maxVal=") << maxValue << F(" midPoint=") << midpoint << F(" scaled_result=") << scaled_result << LOG_ENDLINE; #endif return (result); } static void read_joystick_state() { for (uint8_t i = 0; i < TOTAL_AXES; i += 1) { int32_t discardValue = read_joystick_pin(axisPin[i], calibrationData[i].minCalibration, calibrationData[i].maxCalibration, calibrationData[i].centerCalibration); int32_t reading = read_joystick_pin(axisPin[i], calibrationData[i].minCalibration, calibrationData[i].maxCalibration, calibrationData[i].centerCalibration); int32_t scaled_value = reading + MIN_AXIS_VALUE; #if LOG_ENABLED > 2 LOG_COUT(info) << F("axis=") << i << F(" pin=") << axisPin[i] << F(" reading=") << reading << F(" scaled=") << scaled_value << LOG_ENDLINE; #endif switch (i) { case 0: Joystick.setThrottle(scaled_value); break; case 1: Joystick.setBrake(scaled_value); break; case 2: Joystick.setAccelerator(scaled_value); break; default: #if LOG_ENABLED LOG_COUT(info) << F("no support") << LOG_ENDLINE; #endif break; } // end switch } // end for } #if SUPPORT_CALIBRATION static void monitorCalibration() { for (uint8_t i = 0; i < TOTAL_AXES; i += 1) { uint8_t pin = axisPin[i]; uint16_t value = analogRead(pin); if (value > calibrationData[i].maxCalibration) calibrationData[i].maxCalibration = value; if (value < calibrationData[i].minCalibration) calibrationData[i].minCalibration = value; } } static bool checkForCalibration() { if (startCalibrateMode_ms == 0) { uint8_t val = digitalRead(CALIBRATE_BUTTON_PIN); if (val == HIGH) { return (false); } #if LOG_ENABLED > 1 LOG_COUT(info) << F("start calibration") << LOG_ENDLINE; #endif startCalibrateMode_ms = millis(); indicatorState = HIGH; digitalWrite(INDICATOR_LED, HIGH); indicatorBlinkRate = 10; for (uint8_t i = 0; i < TOTAL_AXES; i += 1) { uint8_t pin = axisPin[i]; // center computed sfter bounds detected calibrationData[i].minCalibration = MAX_ANALOG_VALUE; calibrationData[i].maxCalibration = 0; } } // in calibration mode monitorCalibration(); unsigned long now = millis(); if (now > (startCalibrateMode_ms + CALIBRATION_DURATION_MS)) { #if LOG_ENABLED > 1 LOG_COUT(info) << F("calibration ends") << LOG_ENDLINE; #endif for (uint8_t i = 0; i < TOTAL_AXES; i += 1) { calibrationData[i].centerCalibration = (calibrationData[i].minCalibration + calibrationData[i].maxCalibration) / 2; save_calibration_data(i); } startCalibrateMode_ms = 0; // turn off indicatorState = LOW; indicatorBlinkRate = 0; digitalWrite(INDICATOR_LED, LOW); } } #endif /* SUPPORT_CALIBRATION */ /* returns 0, 45, 90, 135, 180, 225, 270, 315 based on hat bits */ static int16_t decodeHatAngle(uint8_t up, uint8_t down, uint8_t right, uint8_t left) { int16_t angle = -1; if (up) { if (right) { angle = 45; } else if (left) { angle = 315; } else { angle = 0; } } else if (down) { if (right) { angle = 135; } else if (left) { angle = 225; } else { angle = 180; } } else if (right) { angle = 90; } else if (left) { angle = 270; } return (angle); } static void read_button_state() { #if LOG_ENABLED > 1 uint8_t buttonState[TQS_BUTTONS]; #endif bool somethingSet = false; for (uint8_t i = 0; i < TQS_BUTTONS; i += 1) { uint8_t pin = pinList[i]; // buttons are grounded when pushed, so inverted signal is published uint8_t state = !digitalRead(pin); #if LOG_ENABLED > 1 buttonState[i] = state; somethingSet |= state; #endif Joystick.setButton(i, state); } #if LOG_ENABLED > 1 if (somethingSet) { LOG_COUT(info) << F("buttons:"); for (uint8_t i = 0; i < TQS_BUTTONS; i += 1) { if (buttonState[i] != 0) { Serial << F(" ") << i + 1; } } Serial << endl; } #endif #if TQS_HAT_SWITCHES != 0 uint8_t leftHat = digitalRead(HAT_LEFT); uint8_t rightHat = digitalRead(HAT_RIGHT); uint8_t downHat = digitalRead(HAT_DOWN); uint8_t upHat = digitalRead(HAT_UP); // buttons are grounded when pushed, so inverted signal is published int16_t angle = decodeHatAngle(!upHat, !downHat, !rightHat, !leftHat); Joystick.setHatSwitch(0, angle); #if LOG_ENABLED > 1 if (angle != -1) { LOG_COUT(info) << F("hatAngle=") << angle << LOG_ENDLINE; } #endif #endif } void setup() { #if LOG_ENABLED // Setup hardware serial port Serial.begin(CONSOLE_BAUD_RATE); delay(500); // stabilize after power-on unsigned long start = millis(); while (!Serial) { unsigned long delayed = millis() - start; if (delayed > 5000) break; }; // wait for serial port to connect. Needed for native USB port only LOG_COUT(info) << F("Thrustmaster F-16 TQS firmware compiled on date ") << compiledOnDate << LOG_ENDLINE; LOG_COUT(info) << F("Original from Geoff Carpenter gcc@fargos.net http://www.fargos.net/gcc.html") << LOG_ENDLINE; #endif pinMode(THROTTLE_AXIS_PIN, INPUT); pinMode(ANTENNA_AXIS_PIN, INPUT); pinMode(RANGE_AXIS_PIN, INPUT); for (uint8_t i = 0; i < TQS_TOTAL_BUTTON_INPUTS; i += 1) { uint8_t pin = pinList[i]; pinMode(pin, INPUT_PULLUP); } pinMode(LED_BUILTIN, OUTPUT); #if SUPPORT_CALIBRATION pinMode(CALIBRATE_BUTTON_PIN, INPUT_PULLUP); pinMode(INDICATOR_LED, OUTPUT); digitalWrite(INDICATOR_LED, LOW); indicatorBlinkRate = 100; indicatorBlinkUntilMillis = millis() + 10000; for (uint8_t i = 0; i < TOTAL_AXES; i += 1) { load_calibration_data(i); } #endif Joystick.setThrottleRange(MIN_AXIS_VALUE, MAX_AXIS_VALUE); Joystick.setBrakeRange(MIN_AXIS_VALUE, MAX_AXIS_VALUE); Joystick.setAcceleratorRange(MIN_AXIS_VALUE, MAX_AXIS_VALUE); Joystick.begin(false); // Set auto-send to false for better performance } static bool blinkLED(unsigned long now) { static uint32_t count; static uint8_t ledState; count += 1; if (count < 100) return (false); // toggle LED state count = 0; ledState = 1 - ledState; digitalWrite(LED_BUILTIN, ledState); return (true); } #if SUPPORT_CALIBRATION static bool blinkIndicatorLED(unsigned long currentTime) { bool changed = false; if (indicatorBlinkUntilMillis != 0) { if (indicatorBlinkUntilMillis <= currentTime) { // reached end of cycle changed = indicatorState; indicatorBlinkUntilMillis = 0; // turn off indicatorBlinkRate = 0; indicatorState = LOW; digitalWrite(INDICATOR_LED, LOW); } } if (indicatorBlinkRate != 0) { indicatorBlinkCount += 1; if (indicatorBlinkCount >= indicatorBlinkRate) { indicatorBlinkCount = 0; indicatorState = 1 - indicatorState; digitalWrite(INDICATOR_LED, indicatorState); changed = true; } } return (changed); } #endif static unsigned long delayIfNeeded(unsigned long now) { static unsigned long lastTime; unsigned long nextTime = lastTime + POLL_DELAY_MS; if (now < nextTime) { delay(nextTime - now); now = millis(); } lastTime = now; return (now); } void loop() { unsigned long now = delayIfNeeded(millis()); blinkLED(now); #if SUPPORT_CALIBRATION blinkIndicatorLED(now); checkForCalibration(); #endif read_button_state(); read_joystick_state(); // Send the updated states to the PC all at once Joystick.sendState(); } /* vim: set expandtab shiftwidth=4 tabstop=4: */