Thursday, January 26, 2012

DIY Arduino Racing Wheel! (with Direct Port Manipulation)

The title says it all: I'm building a racing wheel for my computer. It will use the HDD encoders from a few posts back to measure how far the pedals have been pressed and the angle of the steering wheel, six buttons for a six speed shifter(with another button for toggling speed shift vs real shift, how this works depends on the game being played), and a few auxiliary buttons.

Since that i am now using multiple encoders and interrupts, i am now using direct port manipulation to read the pin states as it is many times faster than regular digitalReads.

I found a way better way to write the Arduino values to the virtual joystick; PPJoyCom. It is a add-on type program for the PPJoy virtual joystick driver. PPJoyCom waits for a initialization byte over the serial port from the Arduino(print a byte value 240+number of channels). then the encoder values range from 0 to 255(max value of a byte) and are sent to PPJoyCom and written to the joystick. way easier than a C# app. :) basic sample PPJoyCom arduino sketch below, along with encoder code.

basic flow:
1. arduino reads HDD encoders for position, button states etc. sends the data to the PPJoyCom program.
3. PPJoyCom program "writes" the values from the Arduino to the virtual joystick.
3. racing game reads the virtual joystick; X axis is turning, Y is throttle, Z is brake, buttons for shifting, menu keys...



#define Xpin  2
#define Ypin 3
#define Btn1Pin 11
#define Btn2Pin 12

byte Xaxis;
byte Yaxis;
byte Btn1 = 0;
byte Btn2 = 0;

void setup()
{
  Serial.begin(9600);
}

void loop()
{
  Btn1 = digitalRead(Btn1Pin);
  Btn2 = digitalRead(Btn2Pin);

  Xaxis = analogRead(Xpin) / 4;
  Yaxis = analogRead(Ypin) / 4;

  Serial.print(244,BYTE); // 240 + # of channels, 2 Buttons and 2 axis
  Serial.write(0);
  Serial.print(Btn1,BYTE);
  Serial.print(Btn2,BYTE); 
  Serial.print(x,BYTE);
  Serial.print(y,BYTE);
  delay(25);
}


#include <PinChangeInt.h>
#include <PinChangeIntConfig.h>

#define WheelPinA 2
#define WheelPinB 3

#define GasPinA 4
#define GasPinB 5

#define BrakePinA 6
#define BrakePinB 7

#define ClutchPinA 8
#define ClutchPinB 9

#define Gear1pin 10
#define Gear2pin 11
#define Gear3pin 12
#define Gear4pin 13
#define Gear5pin 14 //A0
#define Gear6pin 15 //A1

#define ShiftModepin 16 //A2, real shifting or speed shift
#define Aux1pin 17 //A3
#define Aux2pin 18 //A4
#define Aux3pin 19 //A5

volatile unsigned int Encoders[3] = {0}; //wheel, gas, brake, clutch

volatile boolean ButtonStates[3] = {0}; //current gear, shift mode, aux: 1, 2, or 3

void setup()
{
  //         76543210
  //(PIND & B00001000) check only the pin '1' (in this case digital 3) in this line for low or high, & means dont check any others
  //would be the same as: PIND & (1 << 3);

  //1111111 checking for true would be 128+64+32+16+8+4+2+1
  //(PIND & B00001000) checking for true would be 8

  //goes from left to right
  //       76543210
  DDRD =  B00000000; //sets 2 through 7 as inputs
  PORTD &= B11111100; //enable pullups on all but tx/rx

  //       ..13...8
  DDRB &= B11000000; //sets  through 13 as inputs excpet for crytsl pins
  PORTB &= B00111111; //enable pullups on all but crystal pins

   //     ..543210
  DDRC = B00000000; //sets analog 0 through 5 as inputs
  PORTC &= B00111111; //enable pullups on all but analog 6 an 7 (6 and 7 arent avaliable on atmega 328)

  PCattachInterrupt(WheelPinA, WheelTrigA, CHANGE);
  PCattachInterrupt(WheelPinB, WheelTrigB, CHANGE);

  PCattachInterrupt(GasPinA, GasTrigA, CHANGE);
  PCattachInterrupt(GasPinB, GasTrigB, CHANGE);

  PCattachInterrupt(BrakePinA, BrakeTrigA, CHANGE);
  PCattachInterrupt(BrakePinB, BrakeTrigB, CHANGE);

  PCattachInterrupt(ClutchPinA, ClutchTrigA, CHANGE);
  PCattachInterrupt(ClutchPinB, ClutchTrigB, CHANGE);

  PCattachInterrupt(Gear1pin, GearItrig, CHANGE);
  PCattachInterrupt(Gear2pin, GearIItrig, CHANGE);
  PCattachInterrupt(Gear3pin, GearIIItrig, CHANGE);
  PCattachInterrupt(Gear4pin, GearIVtrig, CHANGE);
  PCattachInterrupt(Gear5pin, GearVtrig, CHANGE);
  PCattachInterrupt(Gear6pin, GearVItrig, CHANGE);
  PCattachInterrupt(Aux1pin, AuxItrig, CHANGE);
  PCattachInterrupt(Aux2pin, AuxIItrig, CHANGE);
  PCattachInterrupt(Aux3pin, AuxIIItrig, CHANGE);
  PCattachInterrupt(ShiftModepin, ShiftTrig, CHANGE);

  Serial.begin (115200);
}

void loop()
{
  for(int x=0; x<4; x++)
  {
    Serial.print(Encoders[x]);
    Serial.print(","); //delimiter
  }
  for(int x=0; x<4; x++)
  {
    Serial.print(ButtonStates[x]);
    Serial.print(","); //delimiter
  }
  Serial.println();
}

void GearItrig()
{
  ButtonStates[0]=1;
}

void GearIItrig()
{
  ButtonStates[0]=2;
}

void GearIIItrig()
{
  ButtonStates[0]=3;
}

void GearIVtrig()
{
  ButtonStates[0]=4;
}

void GearVtrig()
{
  ButtonStates[0]=5;
}

void GearVItrig()
{
  ButtonStates[0]=6;
}

void ShiftTrig()
{
  ButtonStates[1]=~ButtonStates[1];
}

void AuxItrig()
{
  ButtonStates[2]=1;
}

void AuxIItrig()
{
  ButtonStates[2]=2;
}

void AuxIIItrig()
{
  ButtonStates[3]=3;
}

void WheelTrigA()
{
  // look for a low-to-high on pin A
  if ((PIND & B00000100) == 4)
  {
    // check pin B to see which way encoder is turning
    if ((PIND & B00001000) == 0)
    {
      Encoders[0]  -= 1;         // CW
    }
    else
    {
      Encoders[0]  += 1;         // CCW
    }
  }
  else   // must be a high-to-low edge on pin A                                    
  {
    // check pin B to see which way encoder is turning
    if ((PIND & B00001000) == 8)
    {
      Encoders[0]  -= 1;          // CW
    }
    else
    {
      Encoders[0]  += 1;          // CCW
    }
  }
  //Serial.println (Encoders[0], DEC);        
  // use for debugging - remember to comment out
}
void WheelTrigB()
{
  // look for a low-to-high on pin B
  if ((PIND & B00001000) == 8)
  {
    // check pin A to see which way encoder is turning
    if ((PIND & B00000100) == 4)
    {
      Encoders[0]  -= 1;         // CW
    }
    else
    {
      Encoders[0]  += 1;         // CCW
    }
  }
  // Look for a high-to-low on pin B
  else
  {
    // check pin B to see which way encoder is turning
    if ((PIND & B00000100) == 0)
    {
      Encoders[0]  -= 1;          // CW
    }
    else
    {
      Encoders[0]  += 1;          // CCW
    }
  }
}

void GasTrigA()
{
  // look for a low-to-high on pin A
  if ((PIND & B00010000) == 16)
  {
    // check pin B to see which way encoder is turning
    if ((PIND & B00100000) == 0)
    {
      Encoders[1]  -= 1;         // CW
    }
    else
    {
      Encoders[1]  += 1;         // CCW
    }
  }
  else   // must be a high-to-low edge on pin A                                    
  {
    // check pin B to see which way encoder is turning
    if ((PIND & B00100000) == 32)
    {
      Encoders[1]  -= 1;          // CW
    }
    else
    {
      Encoders[1]  += 1;          // CCW
    }
  }
  //Serial.println (Encoders[1], DEC);        
  // use for debugging - remember to comment out
}
void GasTrigB()
{
  // look for a low-to-high on pin B
  if ((PIND & B00100000) == 32)
  {
    // check pin A to see which way encoder is turning
    if ((PIND & B00010000) == 16)
    {
      Encoders[1]  -= 1;         // CW
    }
    else
    {
      Encoders[1]  += 1;         // CCW
    }
  }
  // Look for a high-to-low on pin B
  else
  {
    // check pin B to see which way encoder is turning
    if ((PIND & B00010000) == 0)
    {
      Encoders[1]  -= 1;          // CW
    }
    else
    {
      Encoders[1]  += 1;          // CCW
    }
  }
}

void BrakeTrigA()
{
  // look for a low-to-high on pin A
  if ((PIND & B01000000) == 64)
  {
    // check pin B to see which way encoder is turning
    if ((PIND & B10000000) == 0)
    {
      Encoders[2]  -= 1;         // CW
    }
    else
    {
      Encoders[2]  += 1;         // CCW
    }
  }
  else   // must be a high-to-low edge on pin A                                    
  {
    // check pin B to see which way encoder is turning
    if ((PIND & B10000000) == 128)
    {
      Encoders[2]  -= 1;          // CW
    }
    else
    {
      Encoders[2]  += 1;          // CCW
    }
  }
  //Serial.println (Encoders[2], DEC);        
  // use for debugging - remember to comment out
}

void BrakeTrigB()
{
  // look for a low-to-high on pin B
  if ((PIND & B10000000) == 128)
  {
    // check pin A to see which way encoder is turning
    if ((PIND & B01000000) == 64)
    {
      Encoders[2]  -= 1;         // CW
    }
    else
    {
      Encoders[2]  += 1;         // CCW
    }
  }
  // Look for a high-to-low on pin B
  else
  {
    // check pin B to see which way encoder is turning
    if ((PIND & B01000000) == 0)
    {
      Encoders[2]  -= 1;          // CW
    }
    else
    {
      Encoders[2]  += 1;          // CCW
    }
  }
}

void ClutchTrigA()
{
  // look for a low-to-high on pin A
  if ((PINB & B00000001) == 1)
  {
    // check pin B to see which way encoder is turning
    if ((PINB & B00000010) == 0)
    {
      Encoders[3]  -= 1;         // CW
    }
    else
    {
      Encoders[3]  += 1;         // CCW
    }
  }
  else   // must be a high-to-low edge on pin A                                    
  {
    // check pin B to see which way encoder is turning
    if ((PINB & B00000010) == 2)
    {
      Encoders[3]  -= 1;          // CW
    }
    else
    {
      Encoders[3]  += 1;          // CCW
    }
  }
  //Serial.println (Encoders[3], DEC);        
  // use for debugging - remember to comment out
}
void ClutchTrigB()
{
  // look for a low-to-high on pin B
  if ((PINB & B00000010) == 2)
  {
    // check pin A to see which way encoder is turning
    if ((PINB & B00000001) == 1)
    {
      Encoders[3]  -= 1;         // CW
    }
    else
    {
      Encoders[3]  += 1;         // CCW
    }
  }
  // Look for a high-to-low on pin B
  else
  {
    // check pin B to see which way encoder is turning
    if ((PINB & B00000001) == 0)
    {
      Encoders[3]  -= 1;          // CW
    }
    else
    {
      Encoders[3]  += 1;          // CCW
    }
  }
}

20 comments:

  1. I've modified your code to work with a single linear pot and it the analog signal always comes up as a digital one when I check it with the windows game controller.

    Any help would be awesome.

    [code]
    #define Xpin 0
    byte Xaxis;
    void setup(){
    Serial.begin(9600);
    }

    void loop()
    {

    Xaxis = analogRead(Xpin) / 4;


    Serial.write(241); // 240 + # of channels, 2 Buttons and 2 axis
    Serial.write(Xaxis);

    delay(25);
    }
    [/code]

    ReplyDelete
  2. Oh, just thought of something! in PPjoy you have to setup the buttons/analog inputs correctly. like if you have:

    Serial.write(241);
    Serial.write(Xaxis);

    then a analog input would need to be set as the first input in PPjoy, not a digital. http://www.etheli.com/simulator/simulator.html look at first pic here.

    ReplyDelete
    Replies
    1. Thanks alot!

      That solved the issue.

      Been searching for long time for a tutorial for PPjoy with Ardiuno.

      Delete
  3. I need some help with something regarding arduino or ppjoy... Iva used your code exactly as it is and axes work great but buttons keeps flashing in game controller properties.
    Do you have a clue what could be?? I tried every code ive found but axes work and buttons dont
    Ive set up ppjoy to read all 16 buttons, however button 1 keeps flashing and button 2 doesnt exist.
    I hope ive made some sense here. English is not my first lenguaje.
    (using windows 7 64)
    Tranks!!

    ReplyDelete
  4. strange, so do the other buttons work? are you using actual buttons on the arduino, or are you just testing the code? if you are using actual buttons, try taking out the button read and make the arduino always send one state(high or low) to ppjoy and see if ppjoy lines up with it.

    ReplyDelete
  5. Hey, I am having an issue getting the ppjoycom to recognize the serial signal from the arduino. The serial monitor on the arduino program shows that a signal is coming through. However ppjoycom does not even register a initiation. It just says "Error 2 opening port" Any ideas?

    ReplyDelete
  6. that sounds more like something else is using the COM port at the same time. you say "arduino program shows that a signal is coming through" which im guessing mean you have the serial window open in the arduino IDE... close that serial window, then try.

    ReplyDelete
  7. where do i put the code? should i use both codes or what ?

    ReplyDelete
    Replies
    1. first is an example, second is actual project

      Delete
  8. im having a problem, ppjoy recognice the first button, but not the second one, im using actual buttons.

    im ussing pullups because i was haven missreadings on open buttons

    [code]
    #define B4Pin 10
    #define B3Pin 11
    #define B2Pin 12
    #define B1Pin 13
    #define Xpin A5
    #define acelPin A0
    #define frenoPin A2

    byte Xaxis;
    byte acel;
    byte freno;
    byte Bot1;
    byte Bot2;
    byte Bot3;
    byte Bot4;

    void setup()
    {
    Serial.begin(9600);
    pinMode(B4Pin,INPUT_PULLUP);
    pinMode(B3Pin,INPUT_PULLUP);
    pinMode(B2Pin,INPUT_PULLUP);
    pinMode(B1Pin,INPUT_PULLUP);
    }

    void loop()
    {

    Xaxis = analogRead(Xpin)/8;
    acel= analogRead(acelPin)/4;
    freno= analogRead(frenoPin)/4;
    Bot4=digitalRead(B4Pin);
    Bot3= digitalRead(B3Pin);
    Bot2= digitalRead(B2Pin);
    Bot1=digitalRead(B1Pin);

    byte Ib1= !Bot1;
    byte Ib2= !Bot2;
    byte Ib3= !Bot3;
    byte Ib4= !Bot4;


    Serial.write(245); // 240 + # of channels, 2 Buttons and 2 axis
    //Serial.write(Ib1);
    //Serial.write(Ib2);
    Serial.write(Ib3);
    Serial.write(Ib4);
    Serial.write(Xaxis);
    Serial.write(acel);
    Serial.write(freno);



    delay(25);
    }


    [/code]

    ReplyDelete
    Replies
    1. in serial write, that should be 247, not 245

      Delete
    2. yeah i was alittle desperate, i tried everything, but 2 buttons are comented

      Delete
    3. setup yours just like mine and see if it works, then add stuff

      Delete
    4. you mean 2 axis 2 buttons?

      Delete
    5. sorry for taking me so long to answer, now buttons dont respond, no problem with pedals.

      Serial.write(244); // 240 + # of channels, 2 Buttons and 2 axis

      Serial.write(Ib1);
      Serial.write(Ib2);
      //Serial.write(Ib3);
      //Serial.write(Ib4);
      //Serial.write(Xaxis);
      Serial.write(acel);
      Serial.write(freno);

      Delete
  9. Just send a Serial.write(0); after the initialization byte (240 + # of channels). It is needed (but not documented). After that all my axis and buttons worked.

    Serial.write(244);
    Serial.write(0);
    Serial.write(Ib1);
    Serial.write(Ib2);
    //Serial.write(Ib3);
    //Serial.write(Ib4);
    //Serial.write(Xaxis);
    Serial.write(acel);
    Serial.write(freno);

    ReplyDelete
    Replies
    1. Of course, the tuorial doesnt have it, but the project code does, DOH! haha anyways, its fixed now.

      Delete
  10. byte Btn1;
    byte Btn2;
    byte Xaxis;
    void setup(){
    Serial.begin(9600);
    }
    void loop(){
    Btn1 = digitalRead(28);
    Btn2 = digitalRead(26);
    Xaxis = analogRead(A0) / 4;

    Serial.write(byte(243));
    Serial.write(byte(0));
    Serial.write(byte(Xaxis));
    Serial.write(byte(Btn1));
    Serial.write(byte(Btn2));


    delay(25);
    }

    This code is not recognizing the two buttons, i have changed it a lot of times, but still id doesent work, any help please?

    ReplyDelete