You are on page 1of 22

Contents

Radio Stack for Microsoft Flight Simulator X version 1 .................................................................................... 2


Radio Stack for Microsoft Flight Simulator X version 2, part 1......................................................................... 3
Introduction ..................................................................................................................................................... 3
Panel Layout .................................................................................................................................................... 4
Breadboard prototype ..................................................................................................................................... 5
Arduino Code: Rotary Encoders and 7-Segment Displays ............................................................................... 6
Arduino Code: Numeric Keypad .................................................................................................................... 10
Arduino Code: Latching Buttons.................................................................................................................... 12
Radio Stack for Microsoft Flight Simulator X version 2, part 2....................................................................... 14
Front Panel .................................................................................................................................................... 14
COM Radio Display Modules ......................................................................................................................... 14
Buttons and Switches .................................................................................................................................... 17
Keypad and Transponder Module ................................................................................................................. 18
LEDs ............................................................................................................................................................... 19
Auto Pilot Module ......................................................................................................................................... 20
Putting it all together .................................................................................................................................... 21

Radio Stack version 2.0 David J. Lewis, 2015

Page: 1

Radio Stack for Microsoft Flight Simulator X version 1


Every now and then I get the urge to take to the sky (in a virtual sense) in a Cessna using Microsofts Flight
Simulator X (FSX). A few years ago, having faced some difficulty trying to tune the COM radios using a
mouse during a bout of turbulence, I decided to build a unit to control setting the radio frequencies,
transponder code and auto pilot functions. The project used a universal joystick controller (BU0836X)
obtained from Leo Bodnar (www.leobodnar.com). Here is a photograph of the unit attached to the top of
my Saitek switch panel which in turn is fixed to the Saitek Yoke.

The switches on the upper left replicate the functions of the top row of switches on the FSX radio stack. The
individual COM/NAV radios are selected using the upper rotary switch, which also has a position to select
the transponder. A dual rotary encoder (centre also obtained from Leo Bodnar) is used to change
frequencies of the COM/NAV radios and also changes the individual auto-pilot parameters (selected by the
lower rotary switch). The upper row of push buttons control auto-pilot operation, whilst the bottom row of
buttons set the transponder code.
The unit is built into a 6 x 4 plastic box and interfaces with FSX using FSUIPC over USB. It works quite well,
but as the BU0836X controller only has inputs, it still relies on the on-screen radio stack display to provide
visual feedback. It occurred to me then that one day I would revisit the task and update the unit to not only
control the inputs but also to include the displays, thereby negating the need to bring up the radio stack on
the screen. That day has now arrived.

Radio Stack version 2.0 David J. Lewis, 2015

Page: 2

Radio Stack for Microsoft Flight Simulator X version 2, part 1


Introduction
This latest project began when I decided to buy an Arduino Mega 2560 off a well-known online shopping
site. I used to do a lot of programming in C, pascal, et-al., many years ago (more years than I care to
remember!) and this micro-processor board gave me the opportunity to re-visit the radio stack project (as
well as putting some of my old grey-cells back through their paces now that I am retired). It is ongoing and
as such this blog will be updated accordingly.
Having never used an Arduino before, I was on a learning curve. I started to breadboard my ideas and
determine the options available for the revised radio stack design. I had a basic idea of what I wanted to
include and initially invested in some 0.36 inch 7-segment displays (from www.squirrel-labs.net). This I felt
was the largest useable display size to keep the overall footprint down. I soon discovered the MAX7219/21
LED driver chips and this appears to be the best way to not only interface the 7-segment displays but they
can also drive the other LEDs in the project. The original incarnation used momentary buttons to program
the squawk code into the transponder. I wanted to replace the buttons with a 3x4 numeric keypad (which
I already had) in this version. The keypad will be connected via a single analogue input pin on the Arduino
using a resistor matrix. This technique is well documented on numerous websites.
For the switches, although most are latching, I decided to go with momentary push buttons and perform the
latching operation in software. Each switch will have a LED to indicate its current latch state. The main
reason for this is that some of the buttons are dependent on the state of others (e.g. if COM2 is pressed
when COM1 is operational, then COM2 becomes operational and COM1 needs to automatically un-latch.
Also, most of the auto-pilot buttons have similar dependencies.). Talking of the auto-pilot, I am going to use
a 2x16 LCD display for this as, a) it keeps the size down and b) the auto pilot display uses characters (e.g.
NAV, HDG, APR, etc.) that could not be displayed using standard 7-segment displays. The transponder will
use a 4-digit 7-sement display.
Finally there are a number of other controls that can be difficult to adjust with a mouse on the PC screen,
such as, heading bug, barometric pressure adjustment, etc. I want to incorporate these into the project
with rotary encoders. Frequency adjustment of the COM/NAV radios will use the same dual rotary encoder
used in the original design. With that in mind, I bought another one with the idea that each radio pair (i.e.
COM and NAV) would be controlled by its own encoder.
However, this increases the overall size
significantly so I have opted to use a single dual rotary encoder and a button to cycle through the four radios
(with a LED showing which radio is currently under control of the encoder). The built-in switch on the dual
rotary encoder is used to swap the active/standby frequencies.
Whilst the original project used FSUIPC as the interface between the BU0836X and FSX, it is intended that
this version will use a program written by Jim NZ called Link2fs Multi for FSX. Information and download
links for this program are available here: http://www.jimspage.co.nz/intro.htm

Radio Stack version 2.0 David J. Lewis, 2015

Page: 3

Panel Layout
Here is a concept drawing of the panel layout of the proposed radio stack.

FSX users will almost certainly recognise the various elements incorporated in this design.
I intend to use a 10 x 8 x 3mm sheet of Perspex with a paper artwork sheet behind it. The displays will
mount behind the Perspex but remain visible through cutouts in the artwork sheet. Although the buttons
have a square front bezel, they mount through circular holes which can be easily drilled. The only tricky part
will be accurately cutting out the area for the keypad. Each LED will be inserted into some small black tubing
(probably heat-shrink, as I already have that) with a small hole in the artwork to provide a small dot of light
when the associated button is latched, i.e. operated. The artwork, which provides the black background and
all the text labels, will be printed onto premium quality photographic paper, similar to how I did for the
original version.

Radio Stack version 2.0 David J. Lewis, 2015

Page: 4

Breadboard prototype

I know, it looks like a real rats nest but it has evolved overtime. I have added some labels to the image to
help identify the various elements. As I added a new functionality to the breadboard, I wrote and tested the
necessary code on the Arduino to process it. It initially began with just the 7-segment displays driven by two
MAX7221 LED drivers (ultimately, six MAX7221 chips will be required in the final unit). This was based on an
article on the Arduino site at http://playground.arduino.cc/Main/MAX72XXHardware. Then came the dual
rotary encoder used to set the frequency of the standby (rightmost 6-digit display). The rotary encoders are
interrupt-driven using a finite state machine to determine direction of rotation. The idea for this method
was prompted by an article by Oleg Mazurov at http://www.circuitsathome.com/mcu/reading-rotaryencoder-on-arduino. However, my implementation differs considerably from his. The displays are swapped
when the rotary encoder button is pressed.
The keypad and its attendant potential-divider resistors came next. The keypad is simply a 3x4 matrix, with
a button at each inter-section, which when pressed presents a unique voltage to the analogue input pin to
which the resistor network is connected. This voltage on the analogue pin is polled by the software to
determine if a key has been pressed and if so, which one. The keypad wiring was based on an article at
http://www.instructables.com/id/Arduino-3-wire-Matrix-Keypad/step2/Wiring-up-the-resistors.

Radio Stack version 2.0 David J. Lewis, 2015

Page: 5

Next on the list were the momentary push buttons (currently 15 in all) which are wired to a series resistor
network. The far end of the resistor network is connected to +5v, with 2.2Kohm resistors between that and
each button. This eventually connects to another analogue pin. The buttons connect to ground as shown
here.

It is assumed only one button will be pressed at a time and I initially waited (in the code) for the button to be
released before continuing. This was modified when I added the UP/DOWN buttons (the two round red
ones on the lower right of the breadboard) which change the Altitude and Vertical Speed of the Auto-Pilot.
When these are held pressed, the values start to change more rapidly. So a time interval was introduced in
the button process routine. You can just make out the LEDs above each button on the breadboard image
which indicates the latch state of the associated button. The LEDs are currently controlled via two 8-bit shift
registers (SN74HC959N), but I am planning to control these with the MAX7221 devices in the final unit.
I am currently using a 2 (rows) by 16 (columns) LCD shield for the auto-pilot display, which can be seen
connected to the Arduino on the left hand side of the breadboard image. The final design will contain a
separate 2x16 LCD. Although I tend to limit the use of 3rd-party libraries in my code, I made an exception
here for expediency.
See http://arduino.cc/en/Reference/LiquidCrystal?from=Tutorial.LCDLibrary for
details of the LCD library.
Finally I fitted another rotary encoder to the breadboard (just left of the 7-segment displays) which I
programmed to control the heading bug on the heading indicator (one of the six primary flight indicators).
The heading bug value is also output to the auto-pilot display as it is used by the auto-pilot to maintain
direction of flight. I also wanted to make sure the interrupt driven code worked just as well with a cheap
rotary encoder. It did, so I now have ten more of these on their way from China!

Arduino Code: Rotary Encoders and 7-Segment Displays


The code is likely to change as the project progresses, but what is shown here are code snippets used for the
breadboard version. Hopefully these fragments will be useful to others.
I created a header file (RadioStack.h) to define structures for the 7-segment displays and rotary encoders.
Header File Structures 7-segment displays and rotary encoders
// 7-segment display structure
#define MAX_DISP
6
typedef struct {
byte digits;
// holds the number of digits of the display (1-MAX-DISP)
byte dp;
// position of decimal point (1-MAX_DISP), 0 = no decimal point
char value[MAX_DISP+1]; // holds character ('0'-'9') for each digit as a NULL terminated string
byte chip[MAX_DISP];
// used to select the required MAX72xx chip (currently 0 or 1)
byte reg[MAX_DISP];
// used to select digit register on chip (1-8)
char fsx[4];
// holds the string used by link2fs to update FSX, e.g. "A05"
unsigned long minimum;
// minimum value allowed for the display
unsigned long maximum;
// maximum value allowed for the display
} disp7seg_t;

Radio Stack version 2.0 David J. Lewis, 2015

Page: 6

// rotary encoder structure


#define ENC_STATES
0x0F
#define ENC_FIRED
0x10
typedef struct {
byte pinA;
byte pinB;
byte interrupt;
void (*isr)(void);
int counter;
int stepvalue;
union {
byte flags;
struct {
unsigned spare:3;
unsigned isrFired:1;
unsigned states:4;
0=new B)
};
};
} encoder_t;

// used to access bits 0-3, i.e. states, of the flags structure


// used to access bit 4 (bits 5-7 are spare) of the flags structure
//
//
//
//
//
//

this pin must be associated with an interrupt


this is the anti-clockwise pin (non-interrupt)
interrupt number associated with pin A
name of interrupt service routine
used to store accumulated encoder movement between processing
defines the step value for each increment/decrement

// use ( enc.flags & ENC_xxxxx ) to access elements


// reserved for future use
// signals that associated interrupt has been called
// 4 bits hold current/previous states (bit 3=old A, 2=old B, 1=new A,
// end of struct
// end of union

Global Variables section (i.e. prior to setup() routine)


The 7-segment display and rotary encoder structures are initialised thus
/*-------------------------------------------------------------------------------------------------7-SEGMENT DISPLAYS...
dCOM1a
... COM1 active display
dCOM1s
... COM1 standby display
dXPNDR
... Transponder display
==================================================================================================*/
disp7seg_t dCOM1a = { 6, 3, "118750",
// number of digits, DP position and default value
{ 0, 0, 0,
0, 0, 0 }, // which chip serves each digit (0-1)
{ 1, 2, 3,
4, 5, 6 }, // which register serves each digit (1-8)
"A05",
// FSX update code (SimConnect Input from link2fs)
118000, 135950 };
// minimum and maximum values
disp7seg_t dCOM1s = { 6, 3, "118750",
// number of digits, DP position and default value
{ 0, 0, 1,
1, 1, 1 }, // which chip serves each digit (0-1)
{ 7, 8, 1,
2, 3, 4 }, // which register serves each digit (1-8)
"A62",
// FSX update code (SimConnect Input from link2fs)
118000, 135950 };
// minimum and maximum values
disp7seg_t dXPNDR = { 4, 0, "1200",
// number of digits, DP position and default value
{ 1, 1, 1, 1 },
// which chip serves each digit (0-1)
{ 5, 6, 7, 8 },
// which register serves each digit (1-8)
"A42",
// FSX update code (SimConnect Input from link2fs)
0000, 7777 };
// minimum and maximum values
/*==================================================================================================
ROTARY ENCODERS...
eCOM1a
... COM1 frequency (Mhz) control
eCOM1b
... COM1 frequency (kHz) control
eHDG
... Heading bug control
==================================================================================================*/
encoder_t eCOM1a = { 20, 30, 3, isr_3, 0, 1000, 0 }; // A, B, int, isr, counter, step value, flags
encoder_t eCOM1b = { 21, 31, 2, isr_2, 0,
25, 0 };
encoder_t eHDG
= { 19, 33, 4, isr_4, 0,
1, 0 };

Radio Stack version 2.0 David J. Lewis, 2015

Page: 7

Rotary Encoder Explanation of the Finite State Machine operation


/*=================================================================================================
A 4-bit state machine (B0000-B1111) is used to interpret the rotational direction of encoders:
i.e. anti-clockwise (CCW) or clockwise (CW)
Uses grey code encoders which give the following output. Note only pin A triggers the interrupt.
____
____
A ___|
|____|
|____|
____
____
B
____|
|____|
|____|
:
:
:
:
:
<-- detent positions
^
^
^
^
^
<-- interrupt fire points
AB
10
01
10
01
10 ----------> Clockwise sequence
AB
00
11
00
11
00
<--------- Anti-clockwise sequence
normal valid states: 0011 (3) and 1100 (12) indicating CCW movement
and 0110 (6) and 1001 (9) indicating CW movement. However...
after initialising (i.e. states == 0000), transition to 0010 (2) or 0001 (1) indicates CW movement
and 0000 (0) or 0011 (3) indicates CCW movement
when direction changes these states occur: 0111 (7) or 1000 (8) when direction changes from CW to CCW
0010 (2) or 1101 (13)when direction changes from CCW to CW
Remaining states are invalid: 0100 (4), 0101 (5), 1010 (10), 1011 (11), 1110 (14) and 1111 (15)
Hence...
0
1
2
3
4
5
6
7
8
9 10 11 12 13 14 15
===================================================================================================*/
const int offsets[] = { -1, 1, 1, -1, 0, 0, 1, -1, -1, 1, 0, 0, -1, 1, 0, 0 };

Rotary Encoder - Interrupt Service Routine (example)


/*===================================================================================================
Interrupt Service Routine for int 3, i.e. pin 20 (1st encoder)
Both the isFired flag and counter should be reset by calling program ISR handling routine
===================================================================================================*/
void isr_3 () {
byte A = digitalRead(eCOM1a.pinA);
// Read pins
byte B = digitalRead(eCOM1a.pinB);
eCOM1a.flags |= ENC_FIRED;
// set interrupt fired flag
byte states = eCOM1a.flags & ENC_STATES;
// get current states
states <<= 1;
// shift old AB into bits 1 & 2
states |= (A & 0x01);
// add current state for Pin A into bit 0
states <<= 1;
// shift pin A into bit 1 and old AB into bits 2 & 3
states |= (B & 0x01);
// add current state for Pin B into bit 0
eCOM1a.flags &= ~ENC_STATES;
// clear existing encoder states
eCOM1a.flags |= states;
// save current states into encoder
eCOM1a.counter += offsets[(eCOM1a.flags & ENC_STATES)]; // increment/decrement counter
} // end of isr_3()

The Arduino mandatory loop() routine detects when an interrupt has been fired and calls the relevant
interrupt handling routine
loop() {
// example

if (eCOM1a.flags & ENC_FIRED) serviceCOMInt(&eCOM1a, &dCOM1s);

// service any encoder interrupts

Rotary Encoder Handler (example)


/*==================================================================================================
called when an COM encoder interrupt has been fired.
uses a state machine to determine an offset which after multiplying by the (counter*step value)
is used to increment/decrement the associated counter. The relevant 7-segment display is updated
as is FSX via link2fs
==================================================================================================*/
void serviceCOMInt(encoder_t* enc, disp7seg_t* disp) {
long value = getDisplayValue(disp);
// get current display value
value += (enc->stepvalue * enc->counter);
// increment/decrement value by step value (delta)
enc->flags &= ~ENC_FIRED;
// reset interrupt fired flag
enc->counter = 0;
// reset interrupt counter
if (value > disp->maximum) value = disp->minimum; // wrap value between minimum/maximum limits
if (value < disp->minimum) value = disp->maximum;
setDisplayValue(disp, value);
// update displayed value (and FSX via link2fs)
} // end of serviceCOMInt()

Radio Stack version 2.0 David J. Lewis, 2015

Page: 8

Determining the new 7-segment display value


Having serviced the rotary encoder interrupt and determined the relevant displays new value (taking into
account minimum and maximum thresholds), the setDisplayValue() routine is called to update the 7segment display.
/*==================================================================================================
takes the passed <value> parameter and converts it to individual numeric values for each digit
it then stores them in the associated display structure <value> byte array
the 7-segment display is then updated using the updateMAX72xx() routine
finally the resulting value is output to FSX (via link2fs). NOTE: FSX only accepts 5 digits
===================================================================================================*/
void setDisplayValue(disp7seg_t* disp, long value) {
for (int i = disp->digits-1; i >= 0; i--) {
// for each digit segment
byte numeral = (value % 10);
// get the last digit (0-9)
disp->value[i] = numeral + 48;
// update display array
if (disp->dp == i+1)
// do we need a decimal point here?
numeral += DP;
// if yes, set decimal point (bit 7)
updateMAX72xx(disp->chip[i],
// output digit to MAX72xx chip register
disp->reg[i],
numeral);
value /= 10;
// remove last digit
} // end of for (i=0)
// continue until no further digits exist
Serial.print(disp->fsx);
// update FSX via link2fs, However...
if (disp->digits > 5) {
// FSX only accepts 5 digits for frequencies...
char val[MAX_DISP+1];
// so define a temporary string storage area
strCopy(&val[0], &disp->value[0]);
// copy display value into the temporary area
val[disp->digits-1] = NULL;
// remove last digit by overwriting it with NULL
Serial.println(&val[0]);
// output modified value to FSX via link2fs
} else
Serial.println(&disp->value[0]);
// output un-modified value to FSX via link2fs
} // end of updateDisplayDigits()

Updating the 7-segment display


/*=================================================================================================
sends a byte of <data> to the register (<reg> = 0-7) on the MAX72xx chip (<chip> = 0-1)
Note: only two chips are used at present. The final unit will require three MAX7221s so
this routine will need modification to cater for the third chip.
==================================================================================================*/
void updateMAX72xx (const byte chip, const byte reg, const byte data) {
digitalWrite (LOAD, LOW);
// enable shift operation
if (chip == 0) {
// if destined for chip 0
SPI.transfer (MAX7219_REG_NOOP);
// then output NULLs to chip 1...
SPI.transfer (MAX7219_REG_NOOP);
SPI.transfer (reg);
// ...followed by register and data for chip 0
SPI.transfer (data);
} else {
// if destined for chip 1
SPI.transfer (reg);
// output register and data...
SPI.transfer (data);
SPI.transfer (MAX7219_REG_NOOP);
// followed by NULLS to chip 0
SPI.transfer (MAX7219_REG_NOOP);
} // end of if (chip == 0)
digitalWrite (LOAD, HIGH);
// all finished, disable shift operation
} // end of updateMAX72xx()

Radio Stack version 2.0 David J. Lewis, 2015

Page: 9

Arduino Code: Numeric Keypad


Global defines and variables
/*==================================================================================================
KeyPad ADC values and mappings
==================================================================================================*/
#define KEYPAD_PIN A15
// defines the analog pin used by the keypad
#define KEY_MARGIN 15
// used as + or - margin either side of defined value
#define KEY_NONE KEY_MARGIN
// this is the idle keystate, i.e. no key pressed
// these are the analogue values (0-1023) used to determine which key was pressed
#define KEY_1
49
#define KEY_2
87
#define KEY_3
129
#define KEY_4
197
#define KEY_5
309
#define KEY_6
412
#define KEY_7
543
#define KEY_8
685
#define KEY_9
780
#define KEY_S
841
// '*'
#define KEY_0
924
#define KEY_H
959
// '#'
unsigned long timenow = millis();
// used to time intervals between key presses
unsigned long timethen = timenow;

Initialising the keypad


/*==================================================================================================
Initialises the analogue input pin for the keypad
==================================================================================================*/
void initKeypad() {
pinMode(KEYPAD_PIN, INPUT);
// initialise keypad analog input pin
digitalWrite(KEYPAD_PIN, LOW);
// disable pullup resistor
} // end of initKeypad()

Polling the keypad


loop() {

char key = getKey();


if (key != NULL) updateTransponder(&dXPNDR, key);

} // end of loop()

// poll Keypad
// update the transponder display

/*==================================================================================================
gets current keypad key if pressed and return it, otherwise return NULL.
==================================================================================================*/
char getKey() {
char key = NULL;
// NULL indicates no key pressed
unsigned int adcValue = readAnalogPin(KEYPAD_PIN, KEY_NONE, KEY_MARGIN);
if (! btnPressed(adcValue, KEY_NONE, KEY_MARGIN))
key = mapKeypad(adcValue);
return key;
// returns the key pressed or NULL if none
} // end of getKey()
/*==================================================================================================
returns true if <adcValue> is within range defined by <btnValue> + or - <margin>, otherwise, false
==================================================================================================*/
boolean btnPressed(unsigned int adcValue, unsigned int btnValue, unsigned int margin) {
boolean retval = false;
if ( (adcValue >= (btnValue-margin)) && (adcValue <= (btnValue+margin)) )
retval = true;
return retval;
} // end of btnPressed()

Radio Stack version 2.0 David J. Lewis, 2015

Page: 10

Reading values on Analogue-Digital Convertor (ADC) pins


The code delimited by /* DEBUG ADC Values */ is used to ascertain the actual values produced by each
button/key. The ADC value (0-1023) is displayed on the LCD screen for 1 second. Each key/button should
be pressed a few times and the values averaged. This value is then defined in the global section (see
previous). These represent the nominal values to which a margin (plus and minus) is applied to define a
range of values for each key/button.
/*==================================================================================================
read the ADC value on the associated analogue pin (<Pin>) and return it to the calling program.
<noBtn> holds the "no key press" nominal value. <margin> holds the +/- range used to define the
acceptable values representing key presses. The function applies debouncing and waits for
key/button to be released (or a time limit to be exceeded) before returning.
The time limit is initially set to 1 second to allow sufficient time to press and release a button
however, if this time limit is exceeded (i.e. by holding down a button), the time limit is reduced
to permit multiple reads to occur more quickly. This is useful, for example, to increment
a value rapidly by holding down the UP button. Once released, the original time limit is restored.
==================================================================================================*/
unsigned int readAnalogPin(byte Pin, unsigned int noBtn, unsigned int margin) {
static boolean oldState = false;
// holds previous state
unsigned int adcValue, value;
adcValue = analogRead(Pin);
// read the analog input value (0-1023)
/* DEBUG ADC Values
debugInt("ADC:", adcValue, 1000);
*/
boolean newState = ! btnPressed(adcValue, noBtn, margin);// see if a button has been pressed
if (newState) {
// if a key has been pressed...
long timelimit;
if (newState == oldState)
// if key state has not changed
timelimit = 100;
// accelerate time limit
else
timelimit = 1000;
delay(5);
// wait a short time to debounce
adcValue = analogRead(Pin);
// having settled, read the value again
long timethen = millis();
// get current time
do {
// wait for button to be released
value = analogRead(Pin);
// get current analog pin value
oldState = ! btnPressed(value, noBtn, margin);
// true if button is still pressed
if (! oldState) break;
// exit if button has been released
} while (millis() < timethen + timelimit );
// or wait for time limit to be exceeded
} // end of if (newState)
return adcValue;
// returns the ADC value
} // end of readAnalogPin()

Mapping the ADC value to a key


/*==================================================================================================
map 'value' to a character key and return it, otherwise return NULL if no key pressed.
Note that calling program should have already screened out the KEY_NONE condition,
==================================================================================================*/
char mapKeypad(unsigned int value) {
char key = NULL;
if
( btnPressed(value, KEY_1, KEY_MARGIN) ) {key = '1';}
else if ( btnPressed(value, KEY_2, KEY_MARGIN) ) {key = '2';}
else if ( btnPressed(value, KEY_3, KEY_MARGIN) ) {key = '3';}
else if ( btnPressed(value, KEY_4, KEY_MARGIN) ) {key = '4';}
else if ( btnPressed(value, KEY_5, KEY_MARGIN) ) {key = '5';}
else if ( btnPressed(value, KEY_6, KEY_MARGIN) ) {key = '6';}
else if ( btnPressed(value, KEY_7, KEY_MARGIN) ) {key = '7';}
else if ( btnPressed(value, KEY_8, KEY_MARGIN) ) {key = '8';}
else if ( btnPressed(value, KEY_9, KEY_MARGIN) ) {key = '9';}
else if ( btnPressed(value, KEY_S, KEY_MARGIN) ) {key = '*';}
else if ( btnPressed(value, KEY_0, KEY_MARGIN) ) {key = '0';}
else if ( btnPressed(value, KEY_H, KEY_MARGIN) ) {key = '#';}
return key;
} // end of mapKeypad()

Radio Stack version 2.0 David J. Lewis, 2015

Page: 11

Arduino Code: Latching Buttons


Global defines and variables
/*==================================================================================================
Each button operation is determined by comparing analogue values read via an analogue pin
(BUTTON_PIN) to (BTN_X BTN_MARGIN). Momentary buttons are used but a latching operation
(i.e. on or off) is performed in software. When button is latched on, its LED lights.
LED operation is currently performed using an 8-bit Shift Register (M74HC959)
==================================================================================================*/
// Shift Register used for LEDs
const byte DS
= 45;
// Pin connected to data in pin (DS - 14) of 74HC595
const byte CLOCK = 47;
// Pin connected to clock pin (SH_CP - 11) of 74HC595
const byte LATCH = 49;
// Pin connected to latch pin (ST_CP - 12) of 74HC595
#define LED_ON
HIGH
#define LED_OFF LOW
#define ON
true
#define OFF
false
#define BUTTON_PIN A14
// defines the analogue pin used by the buttons
#define BTN_MARGIN
8
#define BTN_NONE 1023 - BTN_MARGIN
// use to determine which button was
#define BTN_01
72
#define BTN_02
122
#define BTN_03
168
#define BTN_04
209
#define BTN_05
248
#define BTN_06
281
#define BTN_07
312
#define BTN_08 BTN_MARGIN
#define BTN_09
340
#define BTN_10
367
#define BTN_11
389
#define BTN_12
410
#define BTN_13
429
#define BTN_14
446
#define BTN_15
465
#define BTN_16
484

// ADC value when no buttons are pressed


pressed
// COM1
// COM2
// BOTH
// NAV1
// NAV2
// MKR
// DME
// reserved for ADF
// AP
// HDG
// NAV
// APR
// REV
// ALT
// UP
// DN

// this is where the current latch state of each button (currently max 16) is stored
unsigned int buttons = 0x0000;
// initialise switch states to all off
#define BTN_COM1 0x8000
// defines bit masks to access relevant bit in buttons variable
#define BTN_COM2 0x4000
#define BTN_BOTH 0x2000
#define BTN_NAV1 0x1000
#define BTN_NAV2 0x0800
#define BTN_MKR
0x0400
#define BTN_DME
0x0200
#define BTN_ADF
0x0100
#define BTN_AP
0x0080
#define BTN_HDG
0x0040
#define BTN_NAV
0x0020
#define BTN_APR
0x0010
#define BTN_REV
0x0008
#define BTN_ALT
0x0004
#define BTN_UP
0x0002
#define BTN_DN
0x0001

Initialise Shift Register (and button analogue input pin)


/*==================================================================================================
Initialises the shift register and the analogue input pin for the buttons
===================================================================================================*/
void initShiftRegister() {
pinMode(DS, OUTPUT);
// Initialise the Shift Register output pins
pinMode(CLOCK, OUTPUT);
pinMode(LATCH, OUTPUT);
pinMode(BUTTON_PIN, INPUT_PULLUP);
// initialise analog input pin with pullup resistors
} // end of initShiftRegister()

Radio Stack version 2.0 David J. Lewis, 2015

Page: 12

Polling Buttons
loop() {

unsigned int btn = checkButtons();


if (btn != NULL) {
boolean state = ! buttonState(btn);
processButton(btn, state);
} // end of if (btn != 0)

} // end of loop()

//
//
//
//

poll buttons to see if one has just been pressed


if a button event has occured
toggle current button latch state
and perform the required action and update FSX

/*==================================================================================================
reads the ADC value (0-1023) of the button resistive ladder
and return a mask to determine which button is pressed.
if no button is pressed, it returns 0x0000.
==================================================================================================*/
unsigned int checkButtons() {
unsigned int adcValue = readAnalogPin(BUTTON_PIN, BTN_NONE, BTN_MARGIN); // get ADC value (0-1023)
unsigned int btn = 0x0000;
// return 0x0000 if no button has been pressed
if ( ! btnPressed(adcValue, BTN_NONE, BTN_MARGIN)
// however, if a button has been pressed...
btn = mapButtons(adcValue);
// determine which button caused the event
return btn;
// and return bit mask for the button pressed
} // end of checkButtons()

Mapping ADC value to Button


/*=================================================================================================
map <value> to a button bit position and return it, otherwise return NULL if no button recognised.
Note that calling program should have already screened out the BTN_NONE condition,
==================================================================================================*/
unsigned int mapButtons(unsigned int value) {
unsigned int btn = 0x0000;
if
( btnPressed(value, BTN_01, BTN_MARGIN) ) {btn = BTN_COM1;}
else if ( btnPressed(value, BTN_02, BTN_MARGIN) ) {btn = BTN_COM2;}
else if ( btnPressed(value, BTN_03, BTN_MARGIN) ) {btn = BTN_BOTH;}
else if ( btnPressed(value, BTN_04, BTN_MARGIN) ) {btn = BTN_NAV1;}
else if ( btnPressed(value, BTN_05, BTN_MARGIN) ) {btn = BTN_NAV2;}
else if ( btnPressed(value, BTN_06, BTN_MARGIN) ) {btn = BTN_MKR; }
else if ( btnPressed(value, BTN_07, BTN_MARGIN) ) {btn = BTN_DME; }
else if ( btnPressed(value, BTN_08, BTN_MARGIN) ) {btn = BTN_ADF; }
else if ( btnPressed(value, BTN_09, BTN_MARGIN) ) {btn = BTN_AP; }
else if ( btnPressed(value, BTN_10, BTN_MARGIN) ) {btn = BTN_HDG; }
else if ( btnPressed(value, BTN_11, BTN_MARGIN) ) {btn = BTN_NAV; }
else if ( btnPressed(value, BTN_12, BTN_MARGIN) ) {btn = BTN_APR; }
else if ( btnPressed(value, BTN_13, BTN_MARGIN) ) {btn = BTN_REV; }
else if ( btnPressed(value, BTN_14, BTN_MARGIN) ) {btn = BTN_ALT; }
else if ( btnPressed(value, BTN_15, BTN_MARGIN) ) {btn = BTN_UP; }
else if ( btnPressed(value, BTN_16, BTN_MARGIN) ) {btn = BTN_DN; }
return btn;
} // end of mapButtons()

The project is now ready to move into the build stage. This will include making PCBs, installing the
components, building the unit into a suitable housing and finalising the Arduino code. This will be subject of
an update.
TO BE CONTINUED

Radio Stack version 2.0 David J. Lewis, 2015

Page: 13

Radio Stack for Microsoft Flight Simulator X version 2, part 2


After taking a brief hiatus, work on the project has re-commenced, so here is an update on progress.

Front Panel Design


I felt my original front panel design (see part 1) could be aesthetically improved and reduced in size. Here is
the final design which now measures 242mm x 175mm (as opposed to the original size of 264mm x 210mm).

COM Radio Display Modules


One of the early issues that needed resolving was how many digits should be displayed on the COM radios
which in turn determined the number of LED driver chips required. The frequencies used for FSX COM
radios range between 108.000 and 135.950MHz. However, as frequency channels are spaced either 25kHz
(for COM) or 50kHz (for NAV) apart, FSX only displays two decimal places, and the third decimal place is
inferred. For example, if the displayed frequency is 120.25 or 132.50, the inferred decimal is zero (i.e.
120.250 and 132.500 respectfully). Likewise, if the display shows 118.52 or 134.37, this actually represents
118.525 or 134.375MHz respectfully. I decided my design would also use five digits to minimise the
footprint, leaving the inferred last decimal place to be handled in software. However, having five digits per
COM radio display does not fit nicely with the outputs of the MAX7219/21 LED driver chips which each
handle eight 7-segment digits. My initial printed circuit board (PCBs) designs based on driving five digits per
display were overly complicated. Thus I eventually opted to make the most significant digit (which is always
set to 1 anyway!) hard-wired leaving the remaining four digits controlled by the driver chips. This resulted

Radio Stack version 2.0 David J. Lewis, 2015

Page: 14

in a more elegant solution as each display pair (i.e. active and standby) only required a single LED driver chip,
leading to a more modular design.
Some years ago, I built a UV LED light-box in which I expose photo-sensitive copper clad boards using (inkjet)
printed designs to make single sided PCBs. Although it is possible to use this set-up to create double sided
PCBs, obtaining exact alignment for each side does present a challenge. Thus I decided to stick to singlesided PCBS in this project. However, mapping the pins between the 7-segment displays and LED driver chips
required too many jumper wires on a single-sided PCB so I eventually adopted a piggy-back method whereby
the display units are mounted on one PCB and the drivers on another. The boards are then plugged
together via male/female connectors. This method keeps the overall size down as well as supporting design
modularity. To reduce the number of screws required to fit them to the front panel, I included two radio
units (each consisting of an active and a standby display) on each board, but as seen from the two
schematics a single radio board is easily be produced if required.

2 x display driver units (left) and


2 x radio display units (right)
printed circuit boards.

Here is the completed display unit. The


two LEDs situated between the active
and standby displays on each COM radio
indicate which unit is under control of
the rotary encoder used to change
frequency. The connectors used to
piggy-back the driver board have been
super-glued onto the copper side of the
PCB and their pins which protrude on
this side are connected using fine wire
through corresponding holes to pads
once again on the copper side. The four
mounting holes attach the complete
piggy-backed unit to the front panel.

Radio Stack version 2.0 David J. Lewis, 2015

Page: 15

2 x radio display unit schematic

2 x LED driver unit schematic

Radio Stack version 2.0 David J. Lewis, 2015

Page: 16

Buttons and Switches


I originally intended to use a resistive ladder connected to a single analogue pin on the Arduino board to
read all the switches and buttons which in all totalled 24. However, I found having so many buttons the
separation between each value was too small to provide satisfactory operation. I briefly considered splitting
the resistive ladders into three separate chains (thereby using three analogue pins) before abandoning this
idea for a digital solution using three 8-bit parallel-in/serial-out (PISO) shift registers (74HC165). Having
already made all the other PCBs for the project, it was easier to construct this relatively simple design on a
small prototype board of which I already had a number lying around.

3 x 8-bit PISO Shift Register Schematic

This is the resulting board which measures 70mm x


50mm. As can be seen, it accommodates the three
shift registers and the 24 female input connectors
perfectly. I also decided to provide a number of +5v
and ground connections pins (the two eight pin
headers) to permit a common connection point for all
the other modules. The 10k resistors along the
bottom are mainly connected to ground (i.e. pulldown) as the majority of buttons connect to +5v when
pressed. However, the four encoder buttons connect
to ground when pressed, thus the four leftmost
resistors are configured as pull-ups. The three pin
header connects to the Arduino board and is used to
clock data (i.e. read the button/switch states).

Radio Stack version 2.0 David J. Lewis, 2015

Page: 17

Keypad and Transponder Module


I wanted to avoid having to secure the transponder 4-digit display to the front panel with screws (i.e. similar
to the COM radio display units), so decided to combine it with the keypad on a common PCB. Using pin
headers I was able to keep the front of the 7-segment display unit level with the keypad. Once again I
adopted a piggy-back design for the PCB.

Keypad and Transponder Display Unit Schematic

The keypad uses a small resistive ladder connected to


an analogue pin to determine which button is pressed.
The design (shown left on the above schematic) was
obtained from http://www.instructables.com. The
right hand side of the schematic consists of the single
4-digit transponder display and its driver unit. Once
again I have used pin headers to connect the two
boards together. Here are three images of this
module. On the left, the transponder display and 7-pin
header for the keypad are shown uppermost with the
driver board below. As the transponder only uses four
digits, the remaining four digits on the driver chip can
be accessed as individual LEDs thereby providing the
ability to drive 32 LEDs. These are used to display latch
states of the buttons and connect via the 4x8 matrix of
headers shown on the left of the lower image. Below
are the two piggy-backed boards with the keypad
connected (left) and without the keypad connected
(right).

Radio Stack version 2.0 David J. Lewis, 2015

Page: 18

LEDs
Most of the buttons in this project have a latching action, i.e. each successive button press will toggle the
latch state on then off, etc. Each latching button has a corresponding LED to show its current state. The
LEDs are mounted on small strips of 3mm MDF with black heat-shrink over them to prevent light overspill.
The heat-shrink wrap is glued to the MDF strips and cut flush with the front of the strip. The image below
shows the six auto-pilot LEDs, (upper) and the eight switch panel LEDs (lower).

The cathodes of each LED on each strip are connected together in similar fashion to a single 7-segment
common cathode display unit. The LED anodes are then connected as though there were individual
segments to the driver module. The MAX7219/21 chip can operate in both decode mode (whereby the
required numeric value is sent to the chip where it is decoded to light up the correct segments) or nondecode mode whereby each individual segment can be controlled separately. Fortunately both modes can
co-exist on the same chip, thereby allowing the transponder display and LEDs to share the same chip. The
LED strips have been glued to the back of the front panel art-work and small 1.5mm holes allow their light to
be visible from the front.

Radio Stack version 2.0 David J. Lewis, 2015

Page: 19

Auto Pilot Module


The Auto Pilot uses a 2 (rows) x 16 (characters) liquid crystal display (LCD) which is screwed to the front
panel. Having soldered a female header to the LCD module this is used to make the connections to a small
piece of veroboard via the 16-pin male header shown below. The small 10k trim-pot is used to set the LCD
contrast whilst the 220 resistor (top right) limits the backlight current.

The two sets of eight 680 resistors on the bottom left are used to limit the current between the

driver chips and the LEDs. The buttons which control the auto pilot mount around the LCD and
there is also a switch which selects whether ALT (altitude) or VS (vertical speed) is controlled by the
UP/DN buttons. The UP/DN buttons initially increment/decrement the ALT/VS value by 100 feet
with each button press. However, if the button is held pressed for more than a second the ALT/VS
values change at a rate of 1000 feet for each 150 milliseconds (this period may change!) the button
remains pressed.

Radio Stack version 2.0 David J. Lewis, 2015

Page: 20

Putting it all together


Having described how the individual modules have been fabricated, the following images show the radio
stack with all the components connected together and working. The complete unit works off one Arduino
Mega256 board and because all the LEDs are multiplexed via the MAX7219/21 chips, current draw is well
within the capabilities of the USB connection of the Arduino so no separate power supply is needed.
Although pretty much all the functionality of the complete project has been coded and tested, I have yet to
add the communication functionality between the unit and FSX. This is the next stage. As previously stated,
this will use an excellent piece of software called link2fs available at http://www.jimspage.co.nz/intro.htm.

Here is the working front panel of the radio stack. Note the blue item labelled DREMEL on the lower left of
the image is not part of the unit. Rather it is a jaw of the vice used to hold the bare-bones unit during
construction and testing. As can be seen in the final image overleaf, the breadboard is no more and all the
connections exist entirely between the Arduino Mega256, front panel modules and the small shift register
board. The Arduino and shift register boards will ultimately be fixed to the inside of the enclosure.
The wiring will also be tidied up when the panel is mounted in its enclosure. This has yet to be built, but it is
anticipated it will be fabricated from MDF and painted black to match the panel.
A final update will be posted once the project is completed with hopefully a video showing it in action.

Radio Stack version 2.0 David J. Lewis, 2015

Page: 21

The wiring behind the panel as it stands today.

Radio Stack version 2.0 David J. Lewis, 2015

Page: 22

You might also like