Using a common steering wheel (USB) with arduino and USB host module

Hello all,

I'm trying to figure out how to use a standard USB (pc/ps3) steering wheel with an arduino and read its inputs (steering angle, throttle, brake, and some buttons).

I'm finding tons of topics on how to DIY your own steering wheel, but practically nothing on reusing an old standard one.

I've found 1 project where someone rewired the whole thing and bypassed the usb entirely. He just connected the potentiometers to the arduino analog inputs. But I was hoping to find a way using a USB host module and save me some soldering.

I thought initially this would be a walk-in-the-park 1-hour project: Wiring up a USB host module to a nano. Downloading a library. And go. Just like I did with a ps3 controller some years ago. But for some reason I can't find anything about using a steering wheel instead of a ps3 controller.

For context: I want to build a small gocart with hoverboard motors and a pc steering wheel and pedals as input.

Thanks in advance!

See if you can hook it up using the PS2 library

PsxNewLib? I'll look into it. Thanks!
The wheel I got does indeed have 2 connectors. 1xusb and 1x some plug that reminds me of ps1 memory cards and controllers.

No, I meant the PS2 library (PS2 connector, not the video game), but you should try both as the one you landed on it a good idea too.

I've got a hello world to work using the USB Host Library 2.0 (the HID Joystick example).

It spits out a string on the serial monitor nicely on every change/event from the steering wheel. It recognizes ALL the buttons. And even all button combinations! Better than expected even and more than I need. But it opens up the possibility for easter eggs on a specific button combo. =)

Now I just have to find a way to somehow actually use that data. It spits it out in HEX format to the serial monitor (from 0C to FF) for a full steering wheel rotation. Any hints on how to convert that to a DEC value and save the values to variables?

Thanks!

EDIT:
Nevermind. I figured it out. I can simply use (evt=>Y2) to get the decimal value of the steering angle. Perfect!

Cool!
Can you please post your code to help others in the future?

1 Like

I'll definitely post the code as soon as I have it working properly. I might have celebrated too soon.

It seems the steering angle value I am getting is not absolute but relative to the initial startup position. So if I connected it with the wheel centered, all. Is fine. Full Left = 5-6. Center = 128. Full Right = 255.

But if the steering wheel is for example in 45deg Left position when connected/booted, that position gives a 128 value.

Interestingly, there also seems some self-calibration going on. Because when I (from the 45deg left position) rotate the wheel right. The maximum 255 value will already be reached around 45deg right. So the entire path from 45deg right to 90deg right, the value stays the same 255. But when I start rotating it back left, the value immediately starts dropping.

Because of this I'm finding it hard to code a calibration method without making an actual calibration procedure going full left and full right every time the wheel is connected. But I don't want to do that. I want it to work out of the box when connected.

I could solder 2 extra wires to the steering angle potentiometer and read that directly from the arduino. In that case I would read button states via USB, and steering angle via analog input. But that is my backup plan. Preferably, it should all go through the USB connection.

Any thoughts on how to code calibration for this case?

Thanks!

Tell the user to center the wheel, then set the value?

Yeah, but I'd like to avoid that. Since it will be a kids toy, it should not have any "startup procedures". I circumvented the problem my reinstalling the rubber cord I had removed that acts as "force feedback". This keeps the wheel centered when not touched. I liked the steering feeling better without it. But it will have to do.

Now my next challenge is figuring out how Arduino coding works with the 3 separate files (.ino, .cpp and .h). Never done that. There is a variable in the cpp file. Should I be able to access that variable from the ino file somehow?

Maybe it's best I create a new thread for this since it's a different question than the current topic. I'll play around a bit and if I don't figure it out, I'll post a new thread.

The 3 files from the library example:

In case someone needs this on the future: Here is the code of my proof of principle using the USB Host Library 2.0.

The only changes from the library example sketch are in the .cpp file. It detects any change in the steering wheel (button press of angle change), and prints the new states to the serial monitor.

The .ino file:

#include <usbhid.h>
#include <hiduniversal.h>
#include <usbhub.h>

// Satisfy IDE, which only needs to see the include statment in the ino.
#ifdef dobogusinclude
#include <spi4teensy3.h>
#endif
#include <SPI.h>

#include "hidjoystickrptparser.h"

USB Usb;
USBHub Hub(&Usb);
HIDUniversal Hid(&Usb);
JoystickEvents JoyEvents;
JoystickReportParser Joy(&JoyEvents);


void setup() {
        Serial.begin(115200);
#if !defined(__MIPSEL__)
        while (!Serial); // Wait for serial port to connect - used on Leonardo, Teensy and other boards with built-in USB CDC serial connection
#endif
        Serial.println("Start");

        if (Usb.Init() == -1)
                Serial.println("OSC did not start.");

        delay(200);

        if (!Hid.SetReportParser(0, &Joy))
                ErrorMessage<uint8_t > (PSTR("SetReportParser"), 1);
}

void loop() {
        Usb.Task();
}

The .cpp file:

#include "hidjoystickrptparser.h"

JoystickReportParser::JoystickReportParser(JoystickEvents *evt) :
  joyEvents(evt),
  oldHat(0xDE),
  oldButtons(0) {
  for (uint8_t i = 0; i < RPT_GEMEPAD_LEN; i++)
    oldPad[i] = 0xD;
}

// Variabels.
int angletje = 0;
int Xval = 0;
int Yval = 0;
int DPADval = 0;
int Z2val = 0;
int Rzval = 0;

// Buttons //
// X-Channel
const int btn_X_base                 = 0;    // base value (no buttons pressed)
const int btn_X_ids[] = {1, 2, 4, 8, 16, 32, 64, 128};
bool btn_X_states[]          = {0, 0, 0, 0, 0, 0, 0, 0};

// Y-Channel
const int btn_Y_base            = 224;  // base value (no buttons pressed)
const int btn_Y_ids[]           = {225, 226, 228, 232, 240};
bool btn_Y_states[]          = {0, 0, 0, 0, 0};

// D-PAD Channel
const int btn_DPAD_base         = 255;  // base value
const int btn_DPAD_ids[]        = {0, 1, 2, 3, 4, 5, 6, 7};
bool btn_DPAD_states[]          = {0, 0, 0, 0, 0, 0, 0, 0};


void JoystickReportParser::Parse(USBHID *hid, bool is_rpt_id, uint8_t len, uint8_t *buf) {
  bool match = true;

  // Checking if there are changes in report since the method was last called
  for (uint8_t i = 0; i < RPT_GEMEPAD_LEN; i++)
    if (buf[i] != oldPad[i]) {
      match = false;
      break;
    }

  // Calling Game Pad event handler
  if (!match && joyEvents) {
    joyEvents->OnGamePadChanged((const GamePadEventData*)buf);

    for (uint8_t i = 0; i < RPT_GEMEPAD_LEN; i++) oldPad[i] = buf[i];
  }

  uint8_t hat = (buf[5] & 0xF);

  // Calling Hat Switch event handler
  if (hat != oldHat && joyEvents) {
    joyEvents->OnHatSwitch(hat);
    oldHat = hat;
  }

  uint16_t buttons = (0x0000 | buf[6]);
  buttons <<= 4;
  buttons |= (buf[5] >> 4);
  uint16_t changes = (buttons ^ oldButtons);

  // Calling Button Event Handler for every button changed
  if (changes) {
    for (uint8_t i = 0; i < 0x0C; i++) {
      uint16_t mask = (0x0001 << i);

      if (((mask & changes) > 0) && joyEvents) {
        if ((buttons & mask) > 0)
          joyEvents->OnButtonDn(i + 1);
        else
          joyEvents->OnButtonUp(i + 1);
      }
    }
    oldButtons = buttons;
  }
}

void JoystickEvents::OnGamePadChanged(const GamePadEventData *evt) {
  // Save raw values to variables.
  Xval = evt->X;
  Yval = evt->Y;
  DPADval = evt->Z1;
  Z2val = evt->Z2;
  Rzval = evt->Rz;

  // Check steering angle.
  angletje = map(evt->Z2, 0, 255, -90, 90);
  Serial.println("ANGLE:" + (String)angletje);
}

void JoystickEvents::OnHatSwitch(uint8_t hat) {
  Serial.print("Hat Switch: ");
  Serial.print(hat);
  Serial.println("");
}

void JoystickEvents::OnButtonUp(uint8_t but_id) {
  Serial.print("Up: ");
  Serial.println(but_id, DEC);
}

void JoystickEvents::OnButtonDn(uint8_t but_id) {
  Serial.print("Dn: ");
  Serial.println(but_id, DEC);
}

The .h file:

#if !defined(__HIDJOYSTICKRPTPARSER_H__)
#define __HIDJOYSTICKRPTPARSER_H__

#include <usbhid.h>

struct GamePadEventData {
        uint8_t X, Y, Z1, Z2, Rz;
};

class JoystickEvents {
public:
        virtual void OnGamePadChanged(const GamePadEventData *evt);
        virtual void OnHatSwitch(uint8_t hat);
        virtual void OnButtonUp(uint8_t but_id);
        virtual void OnButtonDn(uint8_t but_id);
};

#define RPT_GEMEPAD_LEN		5

class JoystickReportParser : public HIDReportParser {
        JoystickEvents *joyEvents;

        uint8_t oldPad[RPT_GEMEPAD_LEN];
        uint8_t oldHat;
        uint16_t oldButtons;

public:
        JoystickReportParser(JoystickEvents *evt);

        virtual void Parse(USBHID *hid, bool is_rpt_id, uint8_t len, uint8_t *buf);
};

#endif // __HIDJOYSTICKRPTPARSER_H__
2 Likes

Thanks for posting the followup

1 Like

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.