Using FSIO Panel Cards with Arduino.

As I have proved with the MCP project, FSIO panel hardware cards can be connected to Arduino and used for PMDG Data and Events.

There are several FSIO panel cards I publish about, among them General IO Card and General Input card.

The General IOCard offers 32 inputs and 16 7-segment displays or 128 LED outputs (any combination of the displays and LEDs. The General INPUT card allows 72 inputs (switches, buttons etc) and rotary encoders, where rotary encoders need 2 inputs.

FSIO cards can be chained in any order you like.

It will help if you read or have read about FSIO panel hardware.

Making your own Arduino sketch for FSIO panel cards

To start download the FSIOMC8.ino. You can do this by registering at my forum.

Go to the section about PMDG-NGX-MCP for Arduino. Download the sketch FSIOMC8. Although this sketch is specifically made for the MCP, you can easily change the code to program other functions and cards. I will use the sketch to go into some detail to explain below.

All the code that is needed to handle the communication between PMDGDataEventServer and your Arduino is present and basically you do not need to touch that.

All the code that is needed for handling the panel hardware is in the ino and also this code you do not need to touch.

I did not make a Arduino library, just copy the code, make yourself familiar with it and start adapting to your specific needs.

Some values that you will have to adapt.


#define NUMBER_OF_MAXIMS 3               // number of display chips MAX
#define BUTTON_GROUP_SIZE 5              // number of 74LS165 chips in chain

As FSIO panel cards can be chained, you should specify the number of input and display chips in the total chain.

Let’s assume you chain a general IOCard and a general input card, you have 2 maxim chips and 13 74LS165 chips in the chain. Your Arduino can now use 128 LEDs and 104 inputs.

So you will change above to:

#define NUMBER_OF_MAXIMS 2             // number of display chips MAX
#define BUTTON_GROUP_SIZE 13           // number of 74LS165 chips in chain

Look at:

#define DEBOUNCE_BUFFER_DEPTH 4         // Strength of software de-bounce between 1 and 4

The code that handles the input of buttons takes care of de-bouncing, that is why you do not need some capacitor in every input circuit. The code is written in a way that it needs to read the same value of in input during 4 consecutive scans before change in input is recognise. Obvious this routine will take processor power and memory. I allow to set the 4 deep buffer to any value between 1 and 4. Any value > 2 will do software de-bouncing a value of 1 means no de-bouncing.

The rest of the FSIO.h do not touch, unless you want to change the charactes that are displayed on the 7 segment displays. I do not use the bcd to  7 segment character decode from the MAXim chip.


Most of the variables defined and assigned in the beginning of the INO speak for themselves.

Blink settings is yours to expand or delete. This is a byte indicator that I use to set if I want some characters of LED’s to blink. It is used by the “DoBlinks()” routine that I will explain below.

The rest of the variables, you do not need to touch (unless you know exactly what you are doing).

Programming your own panel aware code.

As in the comments of the code indicated there are basically 4 routines you need to program yourself.

Register variables.

This routine will be called by the FSIO code when the serial connection is established. The code will send:

The ID of this Ardiono panel code.

Serial.print("ID:MCP Rev ");

You can change this to your own characters e.g. “ID:MyPanel”. Make sure that the length of the combines ID + revision (see first variable) is maximum 12 characters.

Next you will have to send registration request for any variable that you want the DataEventServer to send to you (when that value changes). Send multiple lines as the buffer line is 64 characters.

Example from the code.

sprintf(line, "Reg:%d:%d*:%d:%d:%d:%d:%d:%d:%d",
MCP_Course_0 ,
MCP_IASBlank ,
MCP_IASOverspeedFlash ,
MCP_IASUnderspeedFlash ,
MCP_Heading ,
MCP_VertSpeed );

The literals for the values are taken from PMDGData.h. You need a ‘%d’ in the line for every variable you request. Make sure they match.

You see a ‘*’ character used for the 2nd variable IASMach. When PMDGDataEventServer sees this * it will send the IASMach value multiplied by 100. Why? In PMDG IASMach value can be a non-integer number (e.g 0.61). In this case we would need some means to receive non-integer numbers. This will complicate the code and therefor PMDGDataEventServer will only send integer numbers. By receiving the IASMach value x100, our code will have to divide by 100 unless the value is <10000. In that case the IASMach value is <100 this means MACH and we do not need to divide but set the decimal point in the display. See below.

There are not many non-integer values in PMDG, but by requesting multiplied by 100 the accuracy is 1%.

Once you have send the registration request PMDGDataEventServer will start sending the requested values if non zero and when they change.

Handle variables received.

When PMDGDataEventServer sends data HandleVariablesReceived will be called. This function is a simple switch()-case tree that you need to adapt to your needs.

When you use the literals it is easy to program. Basically you will have one case: for every variable you requested in the registration request. If you are not programming for the MCP you can delete the tree and build your own.

I will explain a few things here.

case MCP_Course_0:
ToDisplay(16, 3, true);        // Send Cvalue to display start 5 lenght 3

When ‘MCP_Course_0’ is received you want them to be displayed on some digits. I will explain about digit position below. In the case above, the course is displayed on digits 18,17,16. Be aware that you specify the least significant digit (16 in this case). The number of digits is 3 and you want the value displayed with leading 0’s.

case MCP_IASMach:
    if (lvalue >= 10000) // We request IASMach *100 to display Mach value
    valuelength -= 2; // If lvalue >= 100*100 then we do not use lower digits
ToDisplay(19, 3, false);
    if (lvalue < 10000)    SetLed(175, 1); else SetLed(175, 0); // Set MACH decimal point

A few things to explain here.

I already explained about the IASMACH value being requested multiplied by 100. This is the code that evaluates this. Look at the ToDisplay(19,3,false). IASMACH is dispayed with leading blanks on digits 21,20,19.
When the received value is <10000 so the original value is <100 we know we the display is Mach, so we have to set the decimal point. Any segment of the displays can also be treated as an LED. As you have read the FSIO Panel hardware explanation you know. SetLED is simple, use 1 t light the LED and 0 to extinguish it.

One more thing.

case MCP_IASUnderspeedFlash:
BlinkUnderspeed = lvalue;
    if (lvalue == 0)
    cvalue[0] = cBlank;    // Make sure to switch it off
    ToDisplay(22, 1, false);

In the beginning of this INO we defined ‘BlinkUnderSpeed =0’. In the above case if MCP_IASUnderspeedFlash = 1 it will set this value to 1. This will signal to the blink routine to blink the most significant digit of the IASMACH display. The rest of the code is to blank the display when the indicator is 0.

I will explain about the blink function below.

This is in fact all there is to know about handling Data Received.


This routine is called when the code detects any change in hardware button or switches. Also this is a big switch-case structure.

I will walk you through a few specifics.

case 1:
SendEvent(EVT_MCP_FD_SWITCH_L, !ONOF);  // Not the swich value is reversed (!)

For the FD switch you want to send a 0 or 1 to PMDGDataEventServer.
Important note: in PMDG sending a 0 value to a switch means UP position. Whether this means on or off does not matter. In the case above the up position of my hardware gave a 1 so I need to reverse that by sending ‘!ONOF’ (NOT ONOF).

case 3:
if (ONOF == 1) SendEvent(EVT_MCP_N1_SWITCH, -1);

The MCP N1 is a button. Typically a button will send a 1 when pressed and a 0 when released. We are only interested in the pressed actions, so if pressed we send -1 (mouse click) to the Event in PMDG. Read about Events and Mouse Clicks in PMDGDataEventServer.

Rotary encoder is a special case that I will explain about separately.

There are a few other elements like switches with multiple positions. This is no different. These switches are connected to multiple inputs so you need some case: routine for that. Note that in PMDG these switches can be set by a value, where the most counter clock position is represented by 0

This is it for buttons and switches.

How buttons and switches are numbered.

2017-12-01 16_03_43-Nieuw - Microsoft Word-document.docx - Microsoft Word

In the above I picture 2 input chips from the chain. The first chip, is the one closes to the Arduino, and hold number 0. This carries on for the number input chips in you chain.

Rotary encoders.

The FSIO code can handle rotary encoders. But the code needs to know on what inputs a rotary encoder is connected to. I assume that you have read about rotary encoders here.

Rotary encoders can be connected to any 2 consecutive pins of the same input chip. Let’s assume that we connect 2 rotary encoders. One to inputs 6-7 and the other to 12-13.

This is what we know now:

The rotary encode will be seen as Button 6, 7 or Button 12,13 in the ButtonPressed function.

We need to specify encoder position in the array ‘Encoderconfig’.

Look at this line:

byte EncoderConfig[BUTTON_GROUP_SIZE] = {…, …, …, ..,..}; // 2 Bit positions 1 if encoder connected to this input

We have to set ONE-bit for every input that is used for encoders. So for the above example we have to set bits 6 and 7 of byte 0 and bits 5 and 4 of byte 1. You can specify bit values any way you like in your code. In the FSIOMC8 I used hexadecimal representation. But you could easily use binary like so:

byte EncoderConfig[BUTTON_GROUP_SIZE] = {B11000000, B00110000, 0, 0,0};


byte EncoderConfig[BUTTON_GROUP_SIZE] = {0xC0, 0x30, 0, 0,0};

This is the first step. Now you need to specify the EncoderType. We are going to use the same bits in a second array:

byte EncoderType[BUTTON_GROUP_SIZE] =

Now you need to use bits to represent 1,2 or 3 (01 10 or 11) depending on the encode type you use.

A ‘1’ represents a Q cycle encoder, ‘2’ a half cycle encoder and ‘3’ a full cycle encoder. Let’s assume you have full cycle encoder. We would need 11 in bits 7-6 and 11 in bits 13-12. Just about the same as the config bits.

If you have Q cycle encoder you would need to set only the bits 6 and 12. Like:

byte EncoderConfig[BUTTON_GROUP_SIZE] = {B01000000, B00010000, 0, 0,0};

Going back to the input handler ButtonPressed, When you turn the rotary encoder you will receive +1 of -1 byte. From out rotary encoder on input 7-6 we receive 6 as number and 1 or 255=-1,

case 6:
if (ONOF == 1) SendEvent (EVT_MCP_SPEED_SELECTOR, -1);
if (ONOF == 255) SendEvent (EVT_MCP_SPEED_SELECTOR, -3);

Based in the direction we send a right of left mouse event to the MCP speed selector knob.


The function DoBlinks() is called periodically. See ‘const long blinkInterval’. In this function I look at the blink indicators the other part of the code has set. If set I will blink some LEDs or displays.

if (BlinkOverspeed != 0)
if (blinkLed == 0) DisplayBuffer[22] = 0xFF; else DisplayBuffer[22] = 0x00;
DisplayChanged = true;

Display buffer [22] represents digit 22 of the displays, I explain now.

Digit numbering

Display digits are numbered starting 0, where digit 0 is digits 0 of the MAX7219 chip closest to the Arduino.

Below a partial table for LED numbers.

2017-12-01 16_05_50-Nieuw - Microsoft Word-document.docx - Microsoft Word

Any segment of the displays can be addressed as LED using the SetLed() function. LEDs are also numbered 0 and up starting from the MAXim closest to the Arduino. If you position displays on you hardware, the rightmost digit is the lowest number. When you want to address displays in the SetDisplay() routine you will specify the least significant digit, being the right most display.

In the table below you see the relation between Segment, Pin number on the GIO connector and LED number address.

Segment GIO Pin LED NumberFirst Digit Next digits have LED’s
G 7 0 08 – 16 – 24 – 32 – 40 – 48 – 56
F 6 1 09 – 17 – 25 – 33 – 41 – 49 – 57
E 5 2 10 – 18 – 26 – 34 – 42 – 50 – 58
D 4 3 11 – 19 – 27 – 35 – 43 – 51 – 59
C 3 4 12 – 20 – 28 – 36 – 44 – 52 – 60
B 2 5 13 – 21 – 29 – 37 – 45 – 53 – 61
A 1 6 14 – 22 – 30 – 38 – 46 – 54 – 62
DP 8 7 15 – 23 – 31 – 39 – 47 – 55 – 63

This means that a LED connected to pin 7 of the GIO Board connector is LED 0

The LED number represent Segment A-F and DP of the display chips. So LED 7 is the decimal point of the rightmost digit 0.

In my MCP designs I use the DP output to drive LEDs and did not wire the DP in the displays except for the IAS/MACH display. By doing so I could avoid having to use an extra display chip.

Testing input

When you ar ready to connect your switches, encoders etc, you do not have to painstakingly note down and pay attention to what pin you connect the switches and buttons. Just connect to any input. We use a small change in the ino to find out the switch number.

In the INO I put in the directory after adding this text you should enable 2 lines of code.

void ButtonPressed(byte Button, byte ONOF)
// Enabling the 2 statements below alows you to test and find your switch and button position using the serial monitor from the arduino IDE.
// If enanbled the serial monitor will display:
// nn=1 when you make a contact and nn=0 when you break it. Now you know the number of the button, switch or rotary encoder.
// for the Rotary encoder check if you get 2 consequetive numbers!!
 SendEvent(Button, ONOF); 

Recompile and load the ino with these lines enabled and connect the Serial Monitor to the Arduino.

You will be able to see the REG: message.

Type ‘R’eady ( no ‘ ) and see the REG: message again.

When you push a button, or flip a switch the serial monitor will list that switch as: nn=1 when you make the contact and nn=0 when you break it. Now you make a note of the switch number and its function.



The EncoderConfig when already set, will be considered here too. When you are not sure it is better to disable this line and replace with all 00 line.

When you have checked the ecoders pins and constructed the encode config you can test the encoders working correct.

Be sure to disable the lines again by //

// SendEvent(Button, ONOF); 
// return;

Needless to say, recompile and reload the INO.

Testting LED

I made a test facility to Set individual segments by there number.

You type ‘Tnn=1″ to turn on a segment, “Tnn=0” to turn it off.

If you type “T12=1” it will turn on LED #12. See above: This LED is on:

  • The first MAX7219 in the chain
  • Second character
  • Segment C
  • PIN 3 of GIO cards, second connecter from the MAXIM.

Segment C of the second digit of the first MAX7219, On general IOCards this is pin 3 of the second connector. This way you can identiefy and test LED settings.

The code to allow you to do this is always enabled.