Processing problem?

Hi. I've built this button box:


I'm using a knock off board that although it has the layout of a Pro Micro, it's detected by the computer as a Leonardo. It does have a 32U4 chip, so it's properly detected as a game device by windows.
I couldn't think of a better way to wire it up than using an analog pin for the encoder pushbuttons, with different resistors, and the same for the bottom pushbuttons.
The rotary encoder function, switches and leds are independently wired up to the rest of the available pins (including TX and RX).

This is the schematic (sorry for being so shabby):

And this is the code:

#include <Joystick.h>

#define ENABLE_PULLUPS
#define NUMROTARIES 4

int led1 = 15;
int led2 = 14;
int led3 = 16;
int led4 = 10;
int switch1 = A0;
int switch2 = A1;
int switch3 = 9;
int switch4 = 8;
int encoders = A2;
int buttons = A3;

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

rotariesdef rotaries[NUMROTARIES] {
  {0, 1, 0, 1, 0},
  {3, 2, 3, 4, 0},
  {5, 4, 6, 7, 0},
  {7, 6, 9, 10, 0},
};

#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

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();
  pinMode(led1, OUTPUT);
  pinMode(led2, OUTPUT);
  pinMode(led3, OUTPUT);
  pinMode(led4, OUTPUT);
  pinMode(switch1, INPUT_PULLUP);
  pinMode(switch2, INPUT_PULLUP);
  pinMode(switch3, INPUT_PULLUP);
  pinMode(switch4, INPUT_PULLUP);

}

void loop() {

  CheckAllEncoders();
  CheckAllSwitches();
  CheckAllEncoderButtons();
  CheckAllButtons();

}

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 CheckAllEncoderButtons(void) {

  if ((analogRead(encoders) < 950) && (analogRead(encoders) > 900)) {
    Joystick.setButton(2, 1);
  }
  else {
    Joystick.setButton(2, 0);
  }
  if ((analogRead(encoders) < 870) && (analogRead(encoders) > 830)) {
    Joystick.setButton(5, 1);
  }
  else {
    Joystick.setButton(5, 0);
  }
  if ((analogRead(encoders) < 700) && (analogRead(encoders) > 650)) {
    Joystick.setButton(8, 1);
  }
  else {
    Joystick.setButton(8, 0);
  }
  if ((analogRead(encoders) < 530) && (analogRead(encoders) > 490)) {
    Joystick.setButton(11, 1);
  }
  else {
    Joystick.setButton(11, 0);
  }

}

void CheckAllButtons(void) {

  if ((analogRead(buttons) < 600) && (analogRead(buttons) > 450)) {
    Joystick.setButton(16, 1);
  }
  else {
    Joystick.setButton(16, 0);
  }
  if ((analogRead(buttons) < 700) && (analogRead(buttons) > 600)) {
    Joystick.setButton(17, 1);
  }
  else {
    Joystick.setButton(17, 0);
  }
  if ((analogRead(buttons) < 900) && (analogRead(buttons) > 800)) {
    Joystick.setButton(18, 1);
  }
  else {
    Joystick.setButton(18, 0);
  }
  if ((analogRead(buttons) < 950) && (analogRead(buttons) > 900)) {
    Joystick.setButton(19, 1);
  }
  else {
    Joystick.setButton(19, 0);
  }
  if ((analogRead(buttons) < 1000) && (analogRead(buttons) > 950)) {
    Joystick.setButton(20, 1);
  }
  else {
    Joystick.setButton(20, 0);
  }
  if (analogRead(buttons) > 1000) {
    Joystick.setButton(21, 1);
  }
  else {
    Joystick.setButton(21, 0);
  }

}

void CheckAllSwitches(void) {

  if (digitalRead(switch1) == 0) {
    digitalWrite(led1, 1);
    Joystick.setButton(12, 1);
  } else {
    digitalWrite(led1, 0);
    Joystick.setButton(12, 0);
  }

  if (digitalRead(switch2) == 0) {
    digitalWrite(led2, 1);
    Joystick.setButton(13, 1);
  } else {
    digitalWrite(led2, 0);
    Joystick.setButton(13, 0);
  }

  if (digitalRead(switch3) == 0) {
    digitalWrite(led3, 1);
    Joystick.setButton(14, 1);
  } else {
    digitalWrite(led3, 0);
    Joystick.setButton(14, 0);
  }

  if (digitalRead(switch4) == 0) {
    digitalWrite(led4, 1);
    Joystick.setButton(15, 1);
  } else {
    digitalWrite(led4, 0);
    Joystick.setButton(15, 0);
  }

}

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);
    };
  }
}

The functions in the loop control each "section", and work as expected independently. However, if I leave all the functions on, the rotary pulses are not detected. If then comment one of the functions (obviously not the rotary one), then it detects the pulses, but not very smoothly. If I comment two of the functions, they work pretty much ok.

I think the issue might be in the way the rotary encoders are read (I copied this function from another project), or the processing power of the chip, one of the two.
Can you think of another/better way to read input from the rotary encoders?

Thank you

Your CheckAllEncoders function has 200mS of delay in it. You will need to find a different way to use your joystick buttons, likely one using millis if you want your box to be responsive.

That doesn't make much sense. The toggle switches don't do anything in this schematic, correct?
Also correct that you can detect if any of the rotary buttons is pushed, but you don't know which one it is? (same for the push buttons)

I'm not surprised:

That doesn't sound conducive to good responsiveness.

Yeah, poll them without any delays. Debouncing is already handled by the nifty state machine code that you copied into your sketch.

Sorry about the toggle switches. One of the remaining legs of each one is hooked up to pins 8, 9, 10 and 16. I forgot to draw those lines.

I've also tried removing the delays, but that doesn't work either. I think they are there because rotary movement is supposed to be like a "pulse", and it needs to turn the button of the controller on and off. If you remove it, the "pulse" is not even detected, I think, though it could be just the Microsoft game devices tested. I will test it with an actual game.

You likely do need the button pulse to be of a certain minimum length for it to be picked up. What you don't need is to use delay to achieve it.

I think the easiest solution would be to remove the leds and just plug the buttons and switches as a matrix. It's a shame though. The leds on the switches gave it a really nice touch.
This is the project it's inspired by: MAKE THIS BUTTON BOX | 32 FUNCTION w ENCODERS - YouTube
Maybe the analog readings, or the way I'm capturing the button presses are the problem, not the encoders code.

Is there a way to control the 4 leds independently with only one or two pins? maybe some IC?
The main problem is that I cannot use the matrix solution for the pushbuttons/switches because I'm 2 pins short.
I've just learned of shift registers, but the very little I've seen for now, they require 3 pins + 5V and GND, so I would still be 1 pin short anyway.

I'm also reading about GPIO expanders. So much to learn...

GPIO expanders might come in handy.
For reading buttons you could use something like the TCA8418, but it's a QFN chip and I don't know if anyone has already made a handy breakout module with it.

The easy way out of course would just to use an Arduino board (or clone) with some more pins. Is there any fundamental objection against this easy solution?

Btw, your schematic still does not make sense to me. You have the buttons (both the tactile switches and the rotary keys) wired in a way that doesn't allow for distinguishing which button was pushed. Does this part actually work? If so, I'm perplexed that it does, because it really shouldn't. You essentially have A3 and A2 floating, and only pressing a button will pull them up. When doing so the respective resistor value doesn't matter since there's no apparent resistor divider. Are you sure this is the schematic as you have built it in its entirety?

Exactly.
Which, btw, is an entirely different issue than the number of pins, which popped up all of a sudden.

You're right. It does work because the schematics are once again wrong. I forgot to add (in the schematics, not in the circuit) that both pins are grounded with a 10k resistor each. I believe I've used 1k, 2k, 5k1 and 10k resistors.

Ok, that makes sense and should work, apart from the spurious readings you'll get if two or more buttons are pressed simultaneously, but that's inherent to this approach.

Anyway, with a simple MCP23017 expander you could move all those buttons to digital pins which you can poll frequently. That would free up most of your GPIO's. You will still have to eliminate the use of delays to time the output signals. You can use something based on millis() for it. There are examples of this online and on this forum in particular as well.

I think it might be possible without any extra chips...

What's on the back of your rotary encoders? Is there a small PCB? Or just the bare encoder?

If there are PCBs, I don't think you will have separate access to the 2 pins for the push button built into the encoder, so you can't make them part of your button matrix. If so, continue with your resistors idea.

Encoders work better if an external interrupt is used for each encoder. Fortunately, Pro Micro has 5 of these (compared to only 2 on Uno/Pro Mini). They are pins 0, 1, 2, 3 & 7. So maybe connect the "A" or "CLK" pins of the 4 encoders to Arduino pins 0, 1, 2, 3 and the "B" or "DATA" pins of the encoders to Arduino pins 4, 5, 6, 7.

You have 10 other buttons/switches. Because some of them are SPDT switches, you will need to attach small diodes to every switch and button, such as 1n4148, to prevent the "ghost button press" problem. But at least you can get remove the other 6 resistors.

You will, however, need 4 series resistors for your LEDs, which you forgot in your diagram.

Using 8 available pins, make a 4x4 matrix containing your 10 buttons/switches in columns 0 to 2 and the LEDs in column 3. There would even be space in the matrix for 2 more buttons/switches!

I'm really bad at drawing schematics, because I did use resistors with the leds :sweat_smile:. In fact, I used a higher value than needed because they are very bright and for this purpose they don't need to be.
The encoders I'm using are the bare ones, but if you say I can make it better with the board ones, I'll try that. They are not very expensive.

I didn't get the part where you integrate the leds in the matrix though.

I'm not saying that. With bare encoders, I think they will have 5 pins. 3 pins for the encoder ("A", "B" and "COM") and 2 separate pins for the push/click button. You could therefore include the 4 pushbuttons into your matrix, which means you would not need the 4 resistors, but you would need 4 more diodes. You can use the analog pin which is now free as an extra digital pin to extend the matrix to 5x4 to include the extra buttons.

So you do not gain much this way. I leave the decision to you!

It's good that the LEDs are bright, because when they are part of the 4x4 or 5x4 matrix, they will only be 25% as bright because they will only be shining for 25% of the time, as the 4 rows of the matrix are scanned.

A Pro micro and Leonardo are - or should be - electrically identical/ indistinguishable. It is just the form factor - the Pro Micro is more useful. :sunglasses:

Sorry I didn't reply earlier, I had a busy weekend.
The diodes is fine, I've built other button boxes that didn't have the problem with the encoders, and I know how to include the SPDT switches with diodes to them. The part I don't understand is how to wire and control the leds inside the matrix. I thought the matrix of buttons was an input only system.
Bear in mind that each led needs to turn on only when the switch below is used. Also, do you recommend that I use lower value resistors due to the 25% thing then?

This is my idea:

Arduino pins connected to R1-R4 would be made OUTPUT and LOW in turn for ~2ms each, HIGH or INPUT at other times. Pins connected to C1-C4 would be INPUT_PULLUP. Pin connected to C5 would be set to OUTPUT and HIGH or LOW to light the LEDs. The series resistor on C5 could be as low as 100R.

Thanks! I'll see how can I make that work.
How would that translate in the code? when I'm using a matrix, I don't set the matrix pins as outputs or inputs (I believe the library Keypad.h already does that). Here's an example of another button box I did with only 1 encoder and 23 buttons in a matrix, and 5 other switches and buttons controlled independently with individual pins (radio, bateria, arranque, naranja and blando):

#include <Keypad.h>
#include <Joystick.h>

#define ENABLE_PULLUPS
#define NUMROTARIES 1
#define NUMBUTTONS 23
#define NUMROWS 5
#define NUMCOLS 5


byte buttons[NUMROWS][NUMCOLS] = {
  {5, 4, 26, 24, 25},
  {14, 13, 12, 11, 10},
  {20, 19, 18, 17, 16},
  {9, 15, 21, 23},
  {28, 27, 6, 22},
};

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

rotariesdef rotaries[NUMROTARIES] {
  {0, 1, 7, 8, 0},
};

#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

byte rowPins[NUMROWS] = {15, 18, 19, 20, 21};
byte colPins[NUMCOLS] = {14, 16, 10, 9, 8};

int radio = 2;
int bateria = 4;
int arranque = 5;
int naranja = 6;
int blanco = 7;

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();
  pinMode(radio, INPUT_PULLUP);
  pinMode(bateria, INPUT_PULLUP);
  pinMode(arranque, INPUT_PULLUP);
  pinMode(naranja, INPUT);
  pinMode(blanco, INPUT);
}

void loop() {

  CheckAllEncoders();

  CheckAllButtons();

  CheckIndependentButtons();

}

void CheckIndependentButtons(void) {

    if(digitalRead(radio) == 0) {
    Joystick.setButton(29, 1);
  }
  else {
    Joystick.setButton(29, 0);
  }

      if(digitalRead(bateria) == 0) {
    Joystick.setButton(0, 1);
  }
  else {
    Joystick.setButton(0, 0);
  }

    if(digitalRead(arranque) == 0) {
    Joystick.setButton(1, 1);
  }
  else {
    Joystick.setButton(1, 0);
  }

    if(digitalRead(naranja) == 1) {
    Joystick.setButton(2, 1);
  }
  else {
    Joystick.setButton(2, 0);
  }

    if(digitalRead(blanco) == 1) {
    Joystick.setButton(3, 1);
  }
  else {
    Joystick.setButton(3, 0);
  }
  
}

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);
    };
  }
}

Yes, I'm familiar with the keypad library. I don't know for sure if it will be possible to use that, but it might be.

One option is not to use keypad library and scan the matrix in your code.

Another option could be to use keypad library, but "borrow" the row pins when the library is not using them. I've never tried this but I think it might work.

This is code from keypad.cpp

void Keypad::scanKeys() {
	// Re-intialize the row pins. Allows sharing these pins with other hardware.
	for (byte r=0; r<sizeKpd.rows; r++) {
		pin_mode(rowPins[r],INPUT_PULLUP);
	}
        // bitMap stores ALL the keys that are being pressed.
	for (byte c=0; c<sizeKpd.columns; c++) {
		pin_mode(columnPins[c],OUTPUT);
		pin_write(columnPins[c], LOW);	// Begin column pulse output.
		for (byte r=0; r<sizeKpd.rows; r++) {
			bitWrite(bitMap[r], c, !pin_read(rowPins[r]));  // keypress is active low so invert to high.
		}
		// Set pin to high impedance input. Effectively ends column pulse.
		pin_write(columnPins[c],HIGH);
		pin_mode(columnPins[c],INPUT);
	}
}

So the library scans the columns and reads the rows. My idea above was to scan the rows and read the columns. But either way can work, so just flip columns to rows in my schematic, and flip the direction of all the diodes.