Trouble reading joystick buttons with SPI

Status
Not open for further replies.

doveman

Member
I bought myself a secondhand F-16 FLCS stick http://thrustmaster.vanree.net/flcs.html and a Teensy 3.1, so that I could convert it into a USB stick. Someone has already worked out what each of the five wires from the stick are for, so following that I've connected Brown to Vin, Green to GND, Orange to SCK (Pin 13), Red to SS (Pin 10) and Yellow to DIN/MISO (12) and I'm using this code:

Code:
/* USB FLCS Grip
   You must select Joystick from the "Tools > USB Type" menu
*/

// Buttons are muxed into shift registers, use the SPI protocol to read them
#include <SPI.h>

const int slaveSelectPin = 10;

unsigned int buttonInputs1;   // data read from SPI
unsigned int buttonInputs2;
unsigned int buttonInputs3;

// Use some macros to clean things up
#define S3   !(buttonInputs1 & 0x80)    /* Pinky Switch */
#define TG1  !(buttonInputs1 & 0x40)    /* Trigger 1 */
#define TG2  !(buttonInputs1 & 0x20)    /* Trigger 2 */
#define S1   !(buttonInputs1 & 0x10)    /* Nose Wheel Steering */
#define S4   !(buttonInputs1 & 0x08)    /* Paddle Switch */
#define S2   !(buttonInputs1 & 0x04)    /* Pickle */

#define H1D  !(buttonInputs2 & 0x80)    /* Trim */
#define H1R  !(buttonInputs2 & 0x40)
#define H1U  !(buttonInputs2 & 0x20)
#define H1L  !(buttonInputs2 & 0x10)
#define H4U  !(buttonInputs2 & 0x08)    /* CMS */
#define H4L  !(buttonInputs2 & 0x04)
#define H4D  !(buttonInputs2 & 0x02)
#define H4R  !(buttonInputs2 & 0x01)

#define H3D  !(buttonInputs3 & 0x80)    /* DMS */
#define H3R  !(buttonInputs3 & 0x40)
#define H3U  !(buttonInputs3 & 0x20)
#define H3L  !(buttonInputs3 & 0x10)
#define H2D  !(buttonInputs3 & 0x08)    /* TMS */
#define H2R  !(buttonInputs3 & 0x04)
#define H2U  !(buttonInputs3 & 0x02)
#define H2L  !(buttonInputs3 & 0x01)

// setup() runs once on boot
void setup() {
  // set the slaveSelectPin as an output:
  pinMode (slaveSelectPin, OUTPUT);
  // start the SPI library:
  SPI.begin();
  // configure the joystick to manual send mode.  This gives precise
  // control over when the computer receives updates, but it does
  // require you to manually call Joystick.send_now().
  Joystick.useManualSend(true);
}


// loop() runs for as long as power is applied
void loop() {
  // take the SS pin low to select the chip
  digitalWrite(slaveSelectPin,LOW);
  // send a value of 0 to read the SPI bytes
  buttonInputs1 = SPI.transfer(0x00);
  buttonInputs2 = SPI.transfer(0x00);
  buttonInputs3 = SPI.transfer(0x00);
  // take the SS pin high to de-select the chip:
  digitalWrite(slaveSelectPin,HIGH); 

  // Write to joystick buttons
  Joystick.button(1,  TG1);
  Joystick.button(2,  S2);
  Joystick.button(3,  S3);
  Joystick.button(4,  S4);
  Joystick.button(5,  S1);
  Joystick.button(6,  TG2);
  Joystick.button(7,  H2U);
  Joystick.button(8,  H2R);
  Joystick.button(9,  H2D);
  Joystick.button(10, H2L);
  Joystick.button(11, H3U);
  Joystick.button(12, H3R);
  Joystick.button(13, H3D);
  Joystick.button(14, H3L);
  Joystick.button(15, H4U);
  Joystick.button(16, H4R);
  Joystick.button(17, H4D);
  Joystick.button(18, H4L);
  //Joystick.button(19, H1U);
  //Joystick.button(20, H1R);
  //Joystick.button(21, H1D);
  //Joystick.button(22, H1L);
  
  // Determine Joystick Hat Position
  int angle = -1;

  if (H1U) {
    if (H1R) {
      angle = 45;
    } else if (H1L) {
      angle = 315;
    } else {
      angle = 0;
    }
  } else if (H1D) {
    if (H1R) {
      angle = 135;
    } else if (H1L) {
      angle = 225;
    } else {
      angle = 180;
    }
  } else if (H1R) {
    angle = 90;
  } else if (H1L) {
    angle = 270;
  }
  Joystick.hat(angle);
  
  // Because setup configured the Joystick manual send,
  // the computer does not see any of the changes yet.
  // This send_now() transmits everything all at once.
  Joystick.send_now();
}

Unfortunately, that doesn't work. The only button that does anything is the red pinky button at the base of the stick, which triggers on all buttons 1-18 and upper-right diagonal on the hat as well!

Has anyone got any ideas where the problem might be?

I note the guides all seem to say to select "USB -> Joystick" in Arduino but I don't have that, only "Keyboard + Mouse + Joystick" or "Serial + Keyboard + Mouse + Joystick", so will either of those work just as well? I've been using the former.
 
Same issue

Did you ever figure this out? I am trying to do the same thing with the exact same issue!
 
Have you added some Serial.prints for your buttonInputs1 2 and 3 to see what they look like? Since one button works I'm suspecting the encoding is different so first step is to see what those three bytes actually are for each button.
 
I got it working. On a Teensy 2.0. I had the MISO (Pin 3) on the 2.0, on the wrong Pin. For the 2.0 it is Pins 0, 1, 3. I put it on the third Pin, not "Pin 3". After I moved it it worked perfect. I'm wondering if you don't have your wires on the wrong Pins, I say this only because the error you described was exactly the same as mine, even the Hat switch pointing the same way...

Good luck.
 
I got it working. On a Teensy 2.0. I had the MISO (Pin 3) on the 2.0, on the wrong Pin. For the 2.0 it is Pins 0, 1, 3. I put it on the third Pin, not "Pin 3". After I moved it it worked perfect. I'm wondering if you don't have your wires on the wrong Pins, I say this only because the error you described was exactly the same as mine, even the Hat switch pointing the same way...

Good luck.

I really don't know. From the diagram https://www.pjrc.com/teensy/teensy31.html I'm not sure what else could be MISO apart from pin 12. Perhaps someone knows for sure?
 
Have you added some Serial.prints for your buttonInputs1 2 and 3 to see what they look like? Since one button works I'm suspecting the encoding is different so first step is to see what those three bytes actually are for each button.

That might be an idea but probably a waste of time until I know for sure which pins I should be using.
 
If it works on a Teesny 2 but not a 3 then then it's probably a voltage related thing since Teensy 2 is 5V logic where the Teensy 3 is 3.3V so clock may not be reaching quite high enough to reliably trigger the joystick logic. Level converters exist:

https://www.adafruit.com/product/757

or you can roll your own given this is SPI and thus single direction so tricks like tieing Chip Select low and using some sort of Opamp to step up the Clock would do the job. Depends on what hardware you have on hand.

Simple option may be to use a 5V micro controller but there are a range of options depending on your patience and resources. Also SPI should be doing it for you but may be worth adding pinMode(12,INPUT) and pinMode(13,OUTPUT); just in it leaves the pins inactive.
 
Last edited:
If it works on a Teesny 2 but not a 3 then then it's probably a voltage related thing since Teensy 2 is 5V logic where the Teensy 3 is 3.3V so clock may not be reaching quite high enough to reliably trigger the joystick logic. Level converters exist:

https://www.adafruit.com/product/757

or you can roll your own given this is SPI and thus single direction so tricks like tieing Chip Select low and using some sort of Opamp to step up the Clock would do the job. Depends on what hardware you have on hand.

Simple option may be to use a 5V micro controller but there are a range of options depending on your patience and resources. Also SPI should be doing it for you but may be worth adding pinMode(12,INPUT) and pinMode(13,OUTPUT); just in it leaves the pins inactive.

That'll be annoying if I made life complicated just buying the Teensy 3 instead of the 2!

I'm not sure how I'd wire up that level convertor but it looks like I'd need to connect 3.3v to one side and 5v to the other and there's no 5v available from the stick as it's only power comes from the Teensy.

I don't think I've got any Opamps on hand, I'll have to double-check what the odd chips I've got lying around are though, just in case.

I'll try adding the pinmode lines you suggested first though, thanks.

I took a picture of the inside of the stick and it shows the chips inside are HCF4021BE.


I found a datasheet for it here but I'm not able to interpret what it means in terms of using my Teensy 3 with the stick. http://www.bucek.name/pdf/4021.pdf
 
For what it's worth there is 5V on a Teensy if you are running from USB on the Vin pin. The Shift registers linked are capable of operating down to 3V so it may be possible to convert the entire stick to 3.3V operation and be done with it. This may have undesirable effects on the analog functions but pins should read fine and confirm functions there.

The simple method to convert is to move that brown Vin wire to the 3.3V out on the Teensy, though there may be issues if the joystick wants more than 100ma. Have you already removed whatever the original interface circuitry was?
 
For what it's worth there is 5V on a Teensy if you are running from USB on the Vin pin. The Shift registers linked are capable of operating down to 3V so it may be possible to convert the entire stick to 3.3V operation and be done with it. This may have undesirable effects on the analog functions but pins should read fine and confirm functions there.

The simple method to convert is to move that brown Vin wire to the 3.3V out on the Teensy, though there may be issues if the joystick wants more than 100ma. Have you already removed whatever the original interface circuitry was?

Ah yes, of course I could use the 5v from the USB input. What voltage is the Vin pin outputting then, obviously not 3.3v if moving the brown wire to 3.3v would make a difference?

Yeah, I've removed the original circuitry, so the Teensy is directly connected to the circuitry in the stick as shown in the photo. I do still have the board, so there might be some opamps on there I could use if necessary. The analog pots I'll just wire to the Teensy's analog inputs, so I'll have to add some code for those.
 
The Vin is tied to the USB power so in your case you can use it for 5V power, though it's sounding like just running things off the 3.3v output will be a way ahead. I had thought the analog circuitry was still in there and was worried that it would complicate things but just the buttons shouldn't be a problem.

When wiring the analog inputs use the 3.3V not 5V level to drive the pots since that is the range of the ADC channels.
 
The Vin is tied to the USB power so in your case you can use it for 5V power, though it's sounding like just running things off the 3.3v output will be a way ahead. I had thought the analog circuitry was still in there and was worried that it would complicate things but just the buttons shouldn't be a problem.

When wiring the analog inputs use the 3.3V not 5V level to drive the pots since that is the range of the ADC channels.

Ah thanks, that makes sense that everything needs to be running at the same level and not a mix of 3.3v and 5v. I'll give that a go ASAP.

I guess the only problem might be if the stick circuitry and the pots combined need more than the 100ma that the 3.3v pin can supply, in which case I'll have to work out a way to create a 3.3v rail from the USB 5v, instead of taking it from the Teensy.
 
OK, well changing the brown wire to 3.3v hasn't got it working but it's changed things a bit.

Now, pressing the pinky button triggers buttons 1,3,5 & 6; the trigger does 2 & 4 and NE on the hat; the button on the right-edge does 7,8,9 & 10 and the rest of the buttons and hats do nothing.

I tried adding those pinmode lines, so I have:

// set the slaveSelectPin as an output:
pinMode (slaveSelectPin, OUTPUT);
pinMode (12, INPUT);
pinMode (13, OUTPUT);

but that hasn't changed the results at all.

If I add serialprints at the end, like so:

Serial.print(buttonInputs1);
Serial.print(buttonInputs2);
Serial.print(buttonInputs3);
Joystick.send_now();

I see what looks like a stream of 255 in the Serial Monitor until I press one of the buttons mentioned above that does anything, which cause the monitor to show for the Pinky 152 (and the monitor stream pauses), Trigger 24015 (and it pauses), Right button 240 (but the stream doesn't pause so it's hard to be sure). For all three these digits are mixed with 255, so for the Pinky it reads 152255152255152.....

I'll see if I have any more luck with this sketch https://github.com/gerryk/USBJoystick
 
Last edited:
Nope! I edited the f22analyse sketch a bit to stop it serialprinting the analogue axis and hat directions constantly, leaving this:

Code:
/* USB Thrustmaster F22 Conversion 
   for Teensy 3.1 MCU
   You must select Joystick from the "Tools > USB Type" menu

   Buttons are muxed into shift registers, use the SPI protocol to read them
   F22 Shift-reg are 3 x HCF4021BE
   Wiring from Handle to SPI as follows
   Brown:  +5v    
   Green:  GND
   Orange: SCLK (pin13)
   Red:    SS (pin10)
   Yellow: MISO (pin12)
*/

#include <SPI.h>
const int ss = 10;

unsigned int buttonInputs1;   // data read from SPI
unsigned int buttonInputs2;
unsigned int buttonInputs3;

#define PINKY  !(buttonInputs1 & 0x80)    /* Pinky Switch */
#define TG1    !(buttonInputs1 & 0x40)    /* Trigger 1 */
#define TG2    !(buttonInputs1 & 0x20)    /* Trigger 2 */
#define S1     !(buttonInputs1 & 0x10)    /* Nose Wheel Steering */
#define PADDLE !(buttonInputs1 & 0x08)    /* Paddle Switch */
#define THUMB  !(buttonInputs1 & 0x04)    /* Pickle */

#define H1D  !(buttonInputs2 & 0x80)    /* HAT */
#define H1R  !(buttonInputs2 & 0x40)
#define H1U  !(buttonInputs2 & 0x20)
#define H1L  !(buttonInputs2 & 0x10)
#define H4U  !(buttonInputs2 & 0x08)    /* Castle */
#define H4L  !(buttonInputs2 & 0x04)
#define H4D  !(buttonInputs2 & 0x02)
#define H4R  !(buttonInputs2 & 0x01)
#define H3D  !(buttonInputs3 & 0x80)    /* Weap */
#define H3R  !(buttonInputs3 & 0x40)
#define H3U  !(buttonInputs3 & 0x20)
#define H3L  !(buttonInputs3 & 0x10)
#define H2D  !(buttonInputs3 & 0x08)    /* Target */
#define H2R  !(buttonInputs3 & 0x04)
#define H2U  !(buttonInputs3 & 0x02)
#define H2L  !(buttonInputs3 & 0x01)

// setup() runs once on boot
void setup() {
  Serial.begin(9600);
  Serial.println("USB Joystick analyser");
  // set the slaveSelectPin as an output:
  pinMode (ss, OUTPUT);
  
  // start the SPI library:
  SPI.begin();
  // configure the joystick to manual send mode.  This gives precise
  // control over when the computer receives updates, but it does
  // require you to manually call Joystick.send_now().
  Joystick.useManualSend(true);
}


// loop() runs for as long as power is applied
void loop() {
  // take the SS pin low to select the chip
  digitalWrite(ss,LOW);
  // send a value of 0 to read the SPI bytes
  buttonInputs1 = SPI.transfer(0x00);
  buttonInputs2 = SPI.transfer(0x00);
  buttonInputs3 = SPI.transfer(0x00);

  // throttle = analogRead(3);
//  Serial.println(buttonInputs1,BIN);
//  Serial.println(buttonInputs2,BIN);
//  Serial.println(buttonInputs3,BIN);
  // take the SS pin high to de-select the chip:
  digitalWrite(ss,HIGH); 
  if(TG1) Serial.println("Trigger 1");
  if(TG2) Serial.println("Trigger 2");
  if(PINKY) Serial.println("Pinky");
  if(THUMB) Serial.println("Thumb Fire");
  if(S1) Serial.println("S1");
  if(PADDLE) Serial.println("Paddle");
  if(H1U) Serial.println("Hat 1 Up");
  if(H1D) Serial.println("Hat 1 Down");
  if(H1L) Serial.println("Hat 1 Left");
  if(H1R) Serial.println("Hat 1 Right");
  if(H2U) Serial.println("Hat 2 Up");
  if(H2D) Serial.println("Hat 2 Down");
  if(H2L) Serial.println("Hat 2 Left");
  if(H2R) Serial.println("Hat 2 Right");
  if(H3U) Serial.println("Hat 3 Up");
  if(H3D) Serial.println("Hat 3 Down");
  if(H3L) Serial.println("Hat 3 Left");
  if(H3R) Serial.println("Hat 3 Right");
  if(H4U) Serial.println("Hat 4 Up");
  if(H4D) Serial.println("Hat 4 Down");
  if(H4L) Serial.println("Hat 4 Left");
  if(H4R) Serial.println("Hat 4 Right");

  // Determine Joystick Hat Position
  int angle = -1;

  if (H1U) {
    if (H1R) {
      angle = 45;
    } else if (H1L) {
      angle = 315;
    } else {
      angle = 0;
    }
  } else if (H1D) {
    if (H1R) {
      angle = 135;
    } else if (H1L) {
      angle = 225;
    } else {
      angle = 180;
    }
  } else if (H1R) {
    angle = 90;
  } else if (H1L) {
    angle = 270;
  }
//  Serial.print("Hat ");
//  Serial.println(angle); 
//  Joystick.hat(angle);
  
  // Because setup configured the Joystick manual send,
  // the computer does not see any of the changes yet.
  // This send_now() transmits everything all at once.
  Joystick.send_now();
}

With that, I get less output in Windows' joystick control panel (I think only the trigger sent Hat NE) whilst in Serial Monitor, Pinky outputs "Trigger 1, Trigger 2, Pinky, S1", Trigger outputs "Thumb Fire, Paddle, Hat 1 Up, Hat 1 Down, Hat 1 Left, Hat 1 Right" and the right-edge button outputs "Hat 2 Up, Hat 2 Down, Hat 2 Left, Hat 2 Right". None of the other buttons or hats generate any output. If I uncomment the three lines starting with "Serial.print("Hat ");", it just outputs a stream of "Hat -1", regardless of which direction I push any of the hats.

So only the same three buttons are creating output with both sketches. Any ideas why the other buttons/hats aren't generating any output in Serial Monitor?
 
Ok, there is something strange happening there. Unsure how much time you have spent looking at the code here but they are using the 3 bytes to encode 16*3 switch states (-2 for some reason) Then the joystick sens 1s with the switch up (hence lots of 255s and the NOT ! symbol being used) and 0 when down.

So looking at the first case byte1 encodes on it's 8 bits Pinky, TG1, TG2,S1,Paddle and thumb (they don't use bits 0x01 and 0x02). So the Pinky outputs all of the first four bits and the rest appear to do nothing.
Trigger outputs a total of 8 values across TWO bytes. While the last one outputs the last four bits of the last byte.
byte 1 byte2 byte3
1111 1111 1111 1111 1111 1111
[pik] [ trig ] [trg2]
Now a scope would be really nice at this point since I'd love to see what the pulse chain there actually looks like and confirm the stick hardware is in fact working but at the moment I'm thinking that what is happening is timing. So for whatever reason either the Teensy is reading much much faster than it should and getting ahead of the stick as it pops out bits or the stick is somehow missing clocks and running slow. Neither of those is making a lot of sense to me at the moment.

Is there any jitter in which buttons show up or is it rock solid? If it was electrical I'd expect sometimes to see one fewer or one more button.
 
It was rock solid but having said that, I've just tried changing the CPU Speed from 96Mhz (Overclock) to 72Mhz in Arduino IDE and now when I press Pinky I see "Trigger 1, Trigger 2, Pinky" but not S1, Trigger gives "Thumb Fire, S1, Paddle, Hat 1 Up, Hat 1 Down, Hat 1 Right", so that's gained S1 and lost Hat 1 Left and the right-edge button gives "Hat 2 Up, Hat 2 Down, Hat 2 Left, Hat 2 Right, Hat 3 Left", so that's gained Hat 3 Left.

I am testing under a different installation of Windows, so just to be sure I changed the CPU Speed back to 96Mhz (Overclock) and confirmed the same results as in my previous post, so it's definitely the CPU Speed that's changing the results. Still nothing from any of the other buttons/hats though, so it does make me wonder if those three that are working are wired to one of the shift registers and the rest to the other two, which are faulty.

My Dad does have a scope I could use if necessary. Which lines on the Teensy would I need to connect the probes to and what should I look for?
 
Scope wise you are interested in the MISO line and when it's zero. If possible you want to negative edge trigger on SS (select) and then watch MISO. I'm just looking at the code here so making some guesses but with no buttons pressed MISO should stay high. with one button pressed you should see a single low pulse a steady time from SS going low, with the time delay as shown in your code (so pinky is first H2L is longest).
Then check the clock (SCK) and make sure it's using the same time scale as the data pulses (at the same time of the scope as enough channels).

The behavior would be explained if your data pulses are four times as long as the clock pulses that are supposed to be producing them, but that would be very strange.

http://en.wikipedia.org/wiki/Serial_Peripheral_Interface_Bus

May be worth trying the Teensy at 48mhz as well since that's the default speed, but the SPI code should be 'just working' with any of them. Also make sure your SPI library is the most up to date version (aka not a bit bang version intended for a UNO) and/or try this one https://github.com/xxxajk/spi4teensy3. Old or incorrect libraries should have flagged up compiler errors though.
 
At 48Mhz it gives the same results as at 96Mhz.

Thanks for the teensy SPI library but I'm not sure how to modify the sketch to use it. If I modify #include <SPI.h> to #include <spi4teensy3.h> then it highlights the line SPI.begin(); and gives these errors:

f22analyse.ino: In function 'void setup()':
f22analyse:54: error: 'SPI' was not declared in this scope
f22analyse.ino: In function 'void loop()':
f22analyse:67: error: 'SPI' was not declared in this scope

and if I try changing that line to spi4teensy3.begin(); it gives:

f22analyse.ino: In function 'void setup()':
f22analyse:54: error: expected primary-expression before '.' token
f22analyse.ino: In function 'void loop()':
f22analyse:67: error: 'SPI' was not declared in this scope

So if you could explain what I need to modify, I'll try that first and if that doesn't work, I'll test with the scope.
 
Yes, still somewhat puzzled by this one. Fundamentally something is wrong timing wise and a scope may be the best way ahead to pin down what is happening rather than making guesses at the code.
 
Did what I probably should have done first and read
http://arduino.cc/en/Reference/SPI
If you are checking with a scope, or even before then try changing
SPI.setClockDivider()
in line with
http://arduino.cc/en/Reference/SPISetClockDivider

Try setting it to higher (slower) values and see how many buttons cross talk. A better way to do this is described in
https://www.pjrc.com/teensy/td_libs_SPI.html
but that will involve changing those three SPI calls to
Code:
  SPISettings settingsA(1000000, MSBFIRST, SPI_MODE0);

  SPI.beginTransaction(settingsA);
  digitalWrite(ss,LOW);
  // reading only, so data sent does not matter
  buttonInputs1 = SPI.transfer(0x00);
  buttonInputs2 = SPI.transfer(0x00);
  buttonInputs3 = SPI.transfer(0x00);
    digitalWrite(ss,HIGH);
  SPI.endTransaction();
 
Hurrah! Some good news at last. Just by replacing that section with the lines you posted, it works!

I tested at both 48Mhz and 96Mhz and with the f22analyse sketch, the serial monitor shows each button/switch individually when pressed. Then I tried the non-analyse version and Windows joystick control panel sees each of the buttons individually when pressed and the single hat directions as well (the other hats are actually just 4 position switches that trigger different joystick buttons in Windows).

Thanks for your help, I'm very happy to be able to use the joystick at last. I'd love to know why it was necessary to substitute those lines to make it work though, when the original sketch worked for others with the same F-16 stick as I have, as well as the F-22 stick, which also uses the same 3 x HCF4021BE shift registers.
 
Last edited:
Glad to hear you got there, and congratulations on persisting with it.

Having read through the links above my understanding is that the original Teensy SPI code emulated the slow arduino Uno performance as a default. That has now been changed to 'default to as fast as possible' which in most cases is a good thing, since the SPI code is blocking (code pauses until bytes are received) so you want them complete as fast as possible.

The code you had would have worked fine when written but with the updated SPI libraries you need to explicitly say 'run slow'. You may want to try and get the original author of the sketch to update his example since it will probably impact anybody trying to use the example with these fairly basic ICs.
 
Status
Not open for further replies.
Back
Top