Showing posts with label arduino. Show all posts
Showing posts with label arduino. Show all posts

Tuesday, January 24, 2023

I made a joystick. i.e. Why pay $X for something you can spend $(X+Y) building ¯\_(ツ)_/¯

I Made A Joystick.

    I was playing Microsoft Flight Simulator - gorgeous graphics! - and amidst the splendor of... someplace I've never been, I found myself wanting more physical controls. I have a nice joystick - A VKB Gladiator. It even has a little throttle. That's the only analog control other than the stick though. It's far from bad, but I just felt like trying more...


    The first place I checked was amazon. The Thrustmaster TCA "Airbus" controller caught my eye. It was expandable to 4 levers, and there was even a sale. So I bought it...
    Then I thought about checking Reddit. it's not a bad controller, but there was a lot of love for the pricier fare. Of course, I then stumble onto threads of homemade joysticks. "Yeah... I can do that..." I thought - and off I dove into Amazon again, canceling my order, and getting a project box, various switches, wires, and an Arduino Leonardo.


Are you board?

    It should be noted that not every Arduino board can work in a joystick project (at least, not normally to my knowledge). The Leonardo can, and there are a few libraries that will work with it. 

    That takes care of me telling the computer what to do - "button 5 is pressed", or "set that much throttle". Now I need to figure out how to put together the physical switches, potentiometers etc and read them in.


Building!

    I started with the project box - cutting holes for the switches - and a couple radio-control replacement sticks. These are modular joysticks for use in radio-control transmitters - like the type you'd control a model airplane with.

    This simplified the project - I didn't have to develop a gimbal. This has it all built in. It's small - a thumb stick - but this is fine for at least version 1 right?
    NB: looks like there are nicer stick modules about too. "FrSky" has an all-aluminum hall sensor model. Version 2? 😀



    I used sticker paper with a cutout to get the holes right before applying it to the inside of the project box, and using my scroll saw to cut the larger holes and slider-hole. The smaller holes were simply drilled. The texture of the box is the same as my last headset - Rhino-liner.


    
    Measure twice, and cut once. With the holes cut, everything fit quite nicely. Rhinoliner did get pretty scratched while bolting in the buttons so I make another pass with a touch of bronze.



    The underside of the panel looked like it provided plenty of room - Piece of cake right? (The cake is a lie!)
    Meanwhile, I'm messing with the Arduino code, trying to see what it will take to read the values correctly.



    I've got 16 buttons (6 up-down toggle + 4 regular) - I don't think I had enough pins - so the alternative, is to use 8 pins in a 4x4 grid. Loop through one row of pins sending a signal, then check each pin in the column for a response. If it's there, the button is on. Throw in a diode, and you can press multiple buttons without issue.


I ain't 'fraid of no ghosting...





    Turns out you cannot assume an input pin is going to be high or low - it should be pulled to that point - or else you can expect some garbage. Arduino has an input mode for this - "INPUT_PULLUP".
    
    From the grid above, pins 1-4 are read. Pins A-D are scanned.

    So I'm taking "HIGH" as normal (not pressed). If I press a button that closes a switch - say at "C-2" in the diagram, then as I scan the pins - set "C" to low, then read 1-4 - pin 2 will register low, button press. When done with scanning for "C", set "C" again to high.

    The diode setup prevents ghosting - you can smush a bunch of buttons and only those buttons show up as pressed. Without the diode, singular button presses are fine - but if I were to press A1, A2 and B1 - then B2 will show up as pressed as well.


So... many... wires...

Gaaahhh!!


    
    The most annoying bit of this was soldering all the wires in place. The board needed several wires to act as rows and columns. 4 wires for reading, 4 for the scan, and 32 for 16 buttons. Wires tend to come joined - Joined wires appear to put a lot of stress on the shorter wire. I had to resolder a few that pulled away from the board.


    Soldering the wires to the buttons after that was no picnic either!



    
    I thought I had enough space - really. 



    I had to get creative, bending the headers on the Arduino board a bit. Heatshrink was getting in the way, so I went with masking tape and tucked those wires above one of the stick modules. Not the best, but it will do.



    Yay - we have a pulse!



It doesn't look much different, but it's got a brain now! I've tried it in MS Flight Simulator, and Superflight. I still need to try it in Kerbal Space Program 😀.


Code:

#include "Joystick.h"

Joystick_ Joystick(JOYSTICK_DEFAULT_REPORT_ID, JOYSTICK_TYPE_JOYSTICK, 16, 0,
   true,  true,  true, false, false, false,  true, true, false, false, false);
//    X,     Y,     Z,    RX,    RY,    RZ,  rudr, throt, accel, brake, steer);


#define JOYAX1_PIN  A1 // Arduino pin connected to VRX pin
#define JOYAX2_PIN  A2 // Arduino pin connected to VRY pin
#define JOYAX3_PIN  A4 // Arduino pin connected to VRY pin
#define JOYAX4_PIN  A3 // Arduino pin connected to VRY pin
#define JOYAX5_PIN  A0 // Arduino pin connected to VRY pin
#define HIGHPIN 13
#define CHKPIN 12

#define buttchanA 2
#define buttchanB 3
#define buttchanC 4
#define buttchanD 5

#define buttreadA 8
#define buttreadB 9
#define buttreadC 7
#define buttreadD 6

int ButtonState[16] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0};

int reRange(int inval, int inmin, int inmax, int neg){
  int range=inmax-inmin;
  float oldval=inval;
  int newval=(((oldval-inmin)*1023)/range);
  newval=min(newval,1023);
  newval=max(newval,0);
  if (neg<0) {
    newval=1023-newval;
  }
  return int(newval);
}

void setup() {
  //Serial.begin(9600);
  pinMode(JOYAX1_PIN, INPUT_PULLUP);
  pinMode(JOYAX2_PIN, INPUT_PULLUP);
  pinMode(JOYAX3_PIN, INPUT_PULLUP);
  pinMode(JOYAX4_PIN, INPUT_PULLUP);
  pinMode(JOYAX5_PIN, INPUT_PULLUP);
 
  pinMode(HIGHPIN, OUTPUT);
  digitalWrite(HIGHPIN, HIGH);
  pinMode(buttchanA, OUTPUT);
  pinMode(buttchanB, OUTPUT);
  pinMode(buttchanC, OUTPUT);
  pinMode(buttchanD, OUTPUT);
  digitalWrite(buttchanA, HIGH);
  digitalWrite(buttchanB, HIGH);
  digitalWrite(buttchanC, HIGH);
  digitalWrite(buttchanD, HIGH);
  pinMode(buttreadA, INPUT_PULLUP);
  pinMode(buttreadB, INPUT_PULLUP);
  pinMode(buttreadC, INPUT_PULLUP);
  pinMode(buttreadD, INPUT_PULLUP);
  pinMode(CHKPIN, INPUT);
  Joystick.begin();
}

void loop() {
  int joy_ax1_val = analogRead(JOYAX1_PIN); // X axis, right stick regx
  int joy_ax2_val = analogRead(JOYAX2_PIN); // y axis, right stick regy
  int joy_ax3_val = analogRead(JOYAX3_PIN); // X axis, right stick rudder
  int joy_ax4_val = analogRead(JOYAX4_PIN); // Y axis, right stick throttle
  int joy_ax5_val = analogRead(JOYAX5_PIN); // Z axis, slider for trim              

  int pinW;
  int pinR;
  for (int idxW = 0; idxW < 4; idxW++){
    if (idxW == 0) {pinW=buttchanA;}
    if (idxW == 1) {pinW=buttchanB;}
    if (idxW == 2) {pinW=buttchanC;}
    if (idxW == 3) {pinW=buttchanD;}
    digitalWrite(pinW, LOW);
    for (int idxR = 0; idxR < 4; idxR++){
      if (idxR == 0) {pinR=buttreadA;}
      if (idxR == 1) {pinR=buttreadB;}
      if (idxR == 2) {pinR=buttreadC;}
      if (idxR == 3) {pinR=buttreadD;}
      int ChkButtonState=!digitalRead(pinR);
      int butnum=(idxW*4)+idxR;
      if (ChkButtonState != ButtonState[butnum]){
        Joystick.setButton(butnum, ChkButtonState);
        ButtonState[butnum] = ChkButtonState;
      }
    }
    digitalWrite(pinW, HIGH);
  }

  joy_ax1_val=reRange(joy_ax1_val,92,977,1);
  joy_ax2_val=reRange(joy_ax2_val,64,928,1);
  joy_ax3_val=reRange(joy_ax3_val,88,985,1);
  joy_ax4_val=reRange(joy_ax4_val,47,920,-1);
  joy_ax5_val=reRange(joy_ax5_val,15,1023,-1);
  /*Serial.println( String(joy_ax1_val)+" "+
                  String(joy_ax2_val)+" "+
                  String(joy_ax3_val)+" "+
                  String(joy_ax4_val)+" "+
                  String(joy_ax5_val)+" ");
  */
  Joystick.setXAxis(joy_ax1_val);
  Joystick.setYAxis(joy_ax2_val);
  Joystick.setRudder(joy_ax3_val);
  Joystick.setThrottle(joy_ax4_val);
  Joystick.setZAxis(joy_ax5_val);

  if (digitalRead(CHKPIN) == LOW){
    Joystick.end();
  }
  delay(5);
}


    Ignore the "HIGHPIN" and "CHKPIN" - those were there for troubleshooting. The "reRange" function may stand out as odd. The purpose of that is to calibrate the analog axis somewhat. It's not handling centering really, but it will invert an axis if needed and spread the values it does read from 0-1023. 
    e.g. A stick may read from 90-950, instead of the expected 0-1023. Taking the read value, subtracting the minimum value, then multiplying by the range I want (1023), and dividing by the range I have (950-90) gives me an effective 0-1023 analog control. 



    I'm pleased with how this turned out. It matches my headset - version 11 🎧. Two 40mm drivers in each ear, David Clark headband... The headphone builds improved with each version. Perhaps this would apply to the controller as well? 😁











Friday, September 9, 2016

Arduino is fun - great for custom Radio Control signal handling

I've been having some fun with arduino. They're quite cheap boards, with arduino nano coming in at ~$3 per board. That's $3 - THREE. That's pretty awesome for a small amount of custom processing power. Unlike a board like the Raspberry PI which has to run a full Linux kernel, this just runs whatever script you write. As such, the timings are very consistent - it's excellent for handling servo PWN signals where you need to measure pulses from ~1000-2000 microseconds accurately. From my experience the resolution was 4Ξs.

This makes it excellent for managing radiocontrol PWM signals in complex RC vehicles. 



That wrapped up red component in my latest RC vehicle (another hobby, I build RC cars...) is an arduino that reads in steering and another channel and processes it to output a new signal that lets me choose if I want the truck to have 4 wheel steering, or crabwalk (both wheels point the same way making it go mostly sideways).




Definitely one of the more interesting vehicles I've built, and a decent summer project (though I did start last year wrecking the transmission pushing myself in a chair). 

This actually started out with me receiving a non-working unit for handling quadsteer that I had purchased on amazon. When using the unit, the servos lacked power, wouldn't center, and were slow. I used an oscilloscope to diagnose the signal and saw the pulses were 40 milliseconds apart. For Radio control PWM signals, the leading edge should be 20 milliseconds apart. What was essentially happening was the server would get a signal and act on it for 20 milliseconds, and then not do anything for the next 20 milliseconds.

So I turned to arduino nano in the hopes to making my own. Below I've setup that arduino nano on the breadboard to mix signals as I'd like. Each square on the oscilloscope is 10 ms, so my working prototype is showing the proper waveform. This is what was wrapped up in electrical tape in the first picture.




Below is the code I wrote to handle this. The idea is to read two channels - the steering and a spare channel to know if to invert the steering between front and rear. Since I'm reading the time of the pulse, I need a little math to normalize 1000-2000Ξs to -1 to 1 and back again. This lets me multiply the channels so I can then smoothly transition between crabwalk/quadsteer. 

Should be noted we're using interrupts to gather input channel data. This is a non-blocking method for gathering the input. I just requires us to determine the time of the pulse by subtracting the time the clock had at the signal's leading edge.

Edit: 20200907 - code had some bugs, turning off/on interrupts wasn't needed and led to jerky servo output. Tidied it up a bit.


#include <Servo.h> 

volatile unsigned long leadingedge1;
volatile unsigned long leadingedge2;
volatile int pulsetime1; 
volatile int pulsetime2;


//declare servo pins
int servoin1  = 2; // pin 2 - steering
int servoin2  = 3; // pin 3 - inversion channel
int servoout1 = 9; // output read servo is pin 9, front is pin 2
Servo rearservo;


int ch1_hist[3];
int ch2_hist[3];

int execute=0;
long RServo=1500; //value to write to rear servo

void setup()
{
  pinMode(servoin1, INPUT);      // sets the digital pin 1 as input
  pinMode(servoin2, INPUT);      // sets the digital pin 1 as input
  pinMode(servoout1, OUTPUT);    // sets the digital pin 9 as output
//  pinMode(servoout2, OUTPUT);    // sets the digital pin 9 as output
  leadingedge1 = 0;
  leadingedge2 = 0;
  pulsetime1 = 1500;
  pulsetime2 = 1500;
  attachInterrupt(0, chan1, CHANGE);
  attachInterrupt(1, chan2, CHANGE);
  rearservo.attach(servoout1);
  Serial.begin(115200);        // for debugging
}

int amode(int a[]){ //mode or average of an array - use to smooth glitches
  if (a[0]==a[1]) { return a[0]; }
  if (a[0]==a[2]) { return a[0]; }
  if (a[1]==a[2]) { return a[1]; }
  return (a[0]+a[1]+a[2])/3;
}

int pusharr(int a[],int pushval) {
  a[2]=a[1];
  a[1]=a[0];
  a[0]=pushval;
  return 0;
}

void dostuff(){
  execute=0;
  RServo = 1500+((((long)pulsetime1-1500)*((long)pulsetime2-1500))/500);
  if (RServo > 2050) {
    RServo=2050;
  } else {
    if (RServo < 950) {
      RServo=950;
    }
  }
  Serial.print("P1.");
  Serial.print(pulsetime1);
  Serial.print("--P2.");
  Serial.print(pulsetime2);
  Serial.print("--SRear.");
  Serial.print(RServo);
  Serial.print("\n" );
  execute=1;
}

void chan1()
{
  if(digitalRead(servoin1) == HIGH)
  {
    leadingedge1 = micros();
  } else {
    if (leadingedge1 > 0)
    {
      pulsetime1 = ((volatile long)micros() - leadingedge1)-14;
      //14 us added from other operations? center needed normalizing to 1500
      if ((pulsetime1 > 800 and pulsetime1 < 2200)) {pusharr(ch1_hist,pulsetime1);}
      leadingedge1 = 0;
      pulsetime1= amode(ch1_hist);
    }
  }
}

void chan2()
{
  if(digitalRead(servoin2) == HIGH)
  {
    leadingedge2 = micros();
  }
  else
  {
    if(leadingedge2 > 0)
    {
      pulsetime2 = ((volatile long)micros() - leadingedge2)-14;
      if ((pulsetime2 > 800 and pulsetime2 < 2200)) {pusharr(ch2_hist,pulsetime2);}
      leadingedge2 = 0;
      pulsetime2= amode(ch2_hist);
    }
  }
}

void loop()
{  
  delay(2);  //delay is non blocking
  dostuff();  //do stuff after the receiver has sent all pulses
  if (execute==1) { rearservo.write(RServo);}
}