Building a Button Box

make

I started my simracing hobby a year back when I decided to build my own rig using aluminium extrusions. I've always been fascinated by the button boxes you see in YouTube tutorials. A few months back, with no knowledge of electricity, components or even drilling a hole in a piece of plastic, I decided to build my own.

Layout

Since I'll mostly be driving GTE and GT3 cars this year, I based my mapping on similar setups from real life cars and button boxes I came across on the internet. The buttons that I need the most, or need fast access to will be on my steering wheel, other in-race buttons will be deferred to the box.

Wheel Layout (Fanatec CSL Elite Racing Wheel)

The items that are mapped to the wheel have to be reached quickly. I also distinguish between easy- and harder to reach, for example to flick the engine start and pit limiter I have to stretch my right hand all the way over, since I don't want to touch those by accident. Accidentaly pushing the tear off, wipers or flash won't cause big issue's during a race.

wheel layout

Button Box Layout

I have 3 types of components that will make up the button box

It's important to measure the parts to define the width to drill into the box itself. If you overshoot the width, and the component falls through, you will have to order another casing.

Drilling Template

I created a drilling template by using the button box layout above and space out the centers of each component, this way they will be aligned with each other vertically regardless of their size. I added the measurements of the center points of each circle to double check using my ruler, if I'm in doubt during drilling.

drilling template

Next I printed this and discovered that using actual sizes in the printing world can be a hassle. You have to configure the print settings to print on 100% and forcing the paper orientation (in my case this was in landscape mode), otherwise you will see that lengths on your print won't match up with the real world. It might be a good idea to draw a 1cm line (or inch, depending where you're from) to check if your print is correct.

drilling template fitted

Drilling

I put the printed layout of my drilling template onto the top of my project box and used clamps to attach it to a wooden surface. That way there's a surface underneath the plastic, which reduces the risk of breaking it while drilling.

I used a Cone Drill Bit to create the correct sizes of the holes, started by creating every hole at 2mm, and increased the size of all holes together, one size at a time.

drilling template cut

Schema

It took me a while to put this schema together since my knowledge of electric circuits is close to none, the schema is a "matrix" layout, it uses the crossroads of rows and columns to check if a certain component has been switched on, if you want to read more about keyboard matrices I found the following guide the easiest to understand at http://pcbheaven.com/wikipages/How_Key_Matrices_Works/

Using the matrix method we can use all available ports on the arduino to be able to connect as much components as possible, Using the matrix I have a 4x4 grid, so 16 usable components.

wiring schema

Components

wiring schema

Controller

There's a few types of controllers to choose from, the least favorable is a Raspberry Pi, altough I have a few lying around, converting one to a HID input requires a lot of programming. We can use an arduino, which is plug-n-play. There's a few shared repositories so it's only a matter of copying the code and uploading it to the Arduino.

Then there are the prebuilt controllers like the Bodnar or Derek Speare controllers, these will be more expensive then the Arduino, but come with software to map the buttons, and there's no coding involved. I decided to go for the Arduino, it's widely used in the button box guides and the micro boards (or knockoff brands) are very affordable.

Momentary vs Latching Switches

I quickly discovered there's two kinds of switches, a momentary switch will only stay on as long as there is pressure on the switch, like a door bell for example. A latching switch, when switched once, will stay on until it's switched again, to the off position.

This is something that I didn't think about in the original design of my button box, but a game controller will hardly ever use a latching type of component, since having multiple of these on would be like having multiple buttons on a controller pressed continuously. So even for in-game latching switches, such as ABS on or off, we will use a momentary action, since most games will have a button setting like "toggle abs", instead of "hold button to enable abs".

Rotary Encoders vs Rotary Switch

A rotary encoder measures the distance turned and the direction of the turn, the knob can turn endlessly. A rotary switch is a knob you can turn in either direction, but has a number of predefined positions, this means it has a lot more connections, rotary switches can have up to 12 connection points whereas a rotary encoder mostly has 5 connections. To lower the amount of wiring and complexity, we'll use the rotary encoder.

Schema Test Fitting

Before soldering all the wires to my prototyping breadboard, I wanted to make sure that all my components work, and my schema is valid, so I used small breadboards I had lying around to connect all components without having to solder or make irreversable changes, I then connected the Arduino to my computer and used a Joystick Test Tool to see if all components work as expected.

prototype

Soldering

Since this is my first build, and I might make some mistakes, I'm going to finish the final build by soldering all wires to a prototype breadboard, instead of soldering straight onto the connectors of the components, this way I can desolder and make changes if an error is made.

soldering

This is the result of the soldering, Since I'm still learning to solder, I left an empty space between each slot in the rows, to practice applying just enough solder, in the end all slots in a row are connected, so it doesn't matter if solder would connect rows together.

soldering result

Code

Below is the code written by AM-Studio (https://github.com/AM-STUDIO/32-FUNCTION-BUTTON-BOX/blob/master/ARDUINO_BUTTON_BOXV2.ino). Below you can find the code to which I added comments to make it easier to understand it.

/*
    These libraries will be used to emulate a joystick, and assigning keys to the button output
    https://www.arduinolibraries.info/libraries/keypad
    https://github.com/MHeironimus/ArduinoJoystickLibrary
*/
#include <Keypad.h>
#include <Joystick.h>

/*
    We enable the onboard resistors that pull the current down to the board
    as you can see in the schema
*/
#define ENABLE_PULLUPS
#define NUMROTARIES 5
#define NUMBUTTONS 16
#define NUMROWS 4
#define NUMCOLS 4

/*
    This variable will map our 2D matrix, and assign a number for each button
    The values are just incrementing numbers
*/
byte buttons[NUMROWS][NUMCOLS] = {
  {0,1,2,3},
  {4,5,6,7},
  {8,9,10,11},
  {12,13,14,15}
};

struct rotariesdef {
  byte pin1;
  byte pin2;
  int ccwchar;
  int cwchar;
  volatile unsigned char state;
};

/*
    Here we map each of the rotary encoders into an array
    You will notice that 1 and 0 are reversed, that's because on this arduino board, tx and rx are reversed
    {
        pin1 (clk),
        pin2 (dta),
        button to press when turning backwards,
        button to press when turning forwards
        current state of button (0)
    }
*/
rotariesdef rotaries[NUMROTARIES] {
  {1,0,16,17,0},
  {2,3,18,19,0},
  {4,5,20,21,0},
  {6,7,22,23,0},
  {8,9,24,25,0}
};

/*
    This is some next level shit, don't look at it for too long
    And don't worry about it, it works.
*/
#define DIR_CCW 0x10
#define DIR_CW 0x20
#define R_START 0x0

#ifdef HALF_STEP
#define R_CCW_BEGIN 0x1
#define R_CW_BEGIN 0x2
#define R_START_M 0x3
#define R_CW_BEGIN_M 0x4
#define R_CCW_BEGIN_M 0x5
const unsigned char ttable[6][4] = {
  // R_START (00)
  {R_START_M,            R_CW_BEGIN,     R_CCW_BEGIN,  R_START},
  // R_CCW_BEGIN
  {R_START_M | DIR_CCW, R_START,        R_CCW_BEGIN,  R_START},
  // R_CW_BEGIN
  {R_START_M | DIR_CW,  R_CW_BEGIN,     R_START,      R_START},
  // R_START_M (11)
  {R_START_M,            R_CCW_BEGIN_M,  R_CW_BEGIN_M, R_START},
  // R_CW_BEGIN_M
  {R_START_M,            R_START_M,      R_CW_BEGIN_M, R_START | DIR_CW},
  // R_CCW_BEGIN_M
  {R_START_M,            R_CCW_BEGIN_M,  R_START_M,    R_START | DIR_CCW},
};
#else
#define R_CW_FINAL 0x1
#define R_CW_BEGIN 0x2
#define R_CW_NEXT 0x3
#define R_CCW_BEGIN 0x4
#define R_CCW_FINAL 0x5
#define R_CCW_NEXT 0x6

const unsigned char ttable[7][4] = {
  // R_START
  {R_START,    R_CW_BEGIN,  R_CCW_BEGIN, R_START},
  // R_CW_FINAL
  {R_CW_NEXT,  R_START,     R_CW_FINAL,  R_START | DIR_CW},
  // R_CW_BEGIN
  {R_CW_NEXT,  R_CW_BEGIN,  R_START,     R_START},
  // R_CW_NEXT
  {R_CW_NEXT,  R_CW_BEGIN,  R_CW_FINAL,  R_START},
  // R_CCW_BEGIN
  {R_CCW_NEXT, R_START,     R_CCW_BEGIN, R_START},
  // R_CCW_FINAL
  {R_CCW_NEXT, R_CCW_FINAL, R_START,     R_START | DIR_CCW},
  // R_CCW_NEXT
  {R_CCW_NEXT, R_CCW_FINAL, R_CCW_BEGIN, R_START},
};
#endif

/*
    This one is important, here we defined the number of the ports we are using for the rows and cols
    If you look at my schema, you will see that the rows (in red) and cols (in blue) match with the numbers below
*/
byte rowPins[NUMROWS] = {15,14,16,10}; 
byte colPins[NUMCOLS] = {21,20,19,18};

/*
    The following code will loop all inputs and assign them to buttons that our fake joystick can output
    The code below uses the above inputs, so you don't have to make any changes here.
*/
Keypad buttbx = Keypad( makeKeymap(buttons), rowPins, colPins, NUMROWS, NUMCOLS); 

Joystick_ Joystick(JOYSTICK_DEFAULT_REPORT_ID, 
  JOYSTICK_TYPE_JOYSTICK, 32, 0,
  false, false, false, false, false, false,
  false, false, false, false, false);

void setup() {
  Joystick.begin();
  rotary_init();
}

void loop() { 

  CheckAllEncoders();
  CheckAllButtons();

}

void CheckAllButtons(void) {
      if (buttbx.getKeys())
    {
       for (int i=0; i<LIST_MAX; i++)   
        {
           if ( buttbx.key[i].stateChanged )   
            {
            switch (buttbx.key[i].kstate) {  
                    case PRESSED:
                    case HOLD:
                              Joystick.setButton(buttbx.key[i].kchar, 1);
                              break;
                    case RELEASED:
                    case IDLE:
                              Joystick.setButton(buttbx.key[i].kchar, 0);
                              break;
            }
           }   
         }
     }
}

void rotary_init() {
  for (int i=0;i<NUMROTARIES;i++) {
    pinMode(rotaries[i].pin1, INPUT);
    pinMode(rotaries[i].pin2, INPUT);
    #ifdef ENABLE_PULLUPS
      digitalWrite(rotaries[i].pin1, HIGH);
      digitalWrite(rotaries[i].pin2, HIGH);
    #endif
  }
}

unsigned char rotary_process(int _i) {
   unsigned char pinstate = (digitalRead(rotaries[_i].pin2) << 1) | digitalRead(rotaries[_i].pin1);
  rotaries[_i].state = ttable[rotaries[_i].state & 0xf][pinstate];
  return (rotaries[_i].state & 0x30);
}

void CheckAllEncoders(void) {
  for (int i=0;i<NUMROTARIES;i++) {
    unsigned char result = rotary_process(i);
    if (result == DIR_CCW) {
      Joystick.setButton(rotaries[i].ccwchar, 1); delay(50); Joystick.setButton(rotaries[i].ccwchar, 0);
    };
    if (result == DIR_CW) {
      Joystick.setButton(rotaries[i].cwchar, 1); delay(50); Joystick.setButton(rotaries[i].cwchar, 0);
    };
  }
}

Result

It took me a while to finish this, I learned how buttons, rotary encoders en circuits in general work, and I managed to learn soldering without losing an eye. I've been using the box for a few days now and it's still working fine.

result

If you have any questions or comments you can reach me at miguel@notflip.be