RobTillaart / PCF8574

Arduino library for PCF8574 - I2C IO expander
MIT License
119 stars 34 forks source link

Rotary encoder example #23

Closed thijstriemstra closed 2 years ago

thijstriemstra commented 3 years ago

Would be nice to have a rotary encoder example that uses pcf8574.

RobTillaart commented 3 years ago

Write one, and I will add it :)

Could be done, however to reliably read a rotary encoder one needs to connect the interrupt pin to the Arduino. If the PCF8574 triggers one of the 8 pins has changed, Note you can in theory monitor multiple RE's with one PCF. (never tried).

It will work only if the RE are not to fast as the I2C calls take time. (use setClock(400000) might help a bit) I assume on an UNO you cannot do an I2C call from an ISR (never tried)

SO the ISR needs to set a flag and in the main loop() you read the PCF and decide if the RE has moved in which direction.

if you have the AB lines of a RE, you only need one IRQ line and an XOR chip IRQPIN --- XOR (A, B) The interrupt should trigger on change.

The AB pins only allow the following changes
00 - 01 - 11 - 10 - 00 (circle) so the XOR will always change from 0 to 1 etc.

RobTillaart commented 3 years ago

That said for number of slow RE's it could be done. critical part is that the main loop() should be able to react fast enough ...

Under RTOS one could just write a task with one responsibility (keep track of an RE)

RobTillaart commented 3 years ago

Other library - https://www.mischianti.org/2020/03/13/pcf8574-i2c-digital-i-o-expander-rotary-encoder-part-2/ Might do your thing...

RobTillaart commented 3 years ago

some thoughts ...

a RE has two main questions: 1) has it moved? 2) if so in which direction?

To answer question 1 one needs to compare with previous state, so one needs to keep track of 2 bits Aprev and Bprev

in pseudo code

in x = 0;
...
A = readA();
B = readB();
if (A == Aprev && B == Bprev)  no movement
else 
{
  If (A == 0 and Aprev == 0 and B ==0 and Bprev ==1 ) x = ...   //  4 compares per line...
  check all 8 possible changes ==> 32 bit compares
  4 indicate step left  x--;
  4 indicate step right  x++;
  copy states to prev states.
}

suppose one makes a bitpattern of A and B => P <==> AB Pprev <==> AprevBprev that would make comparing the change easier.

int x = 0;
...
P = readA() << 1 | readB();
if (P == Pprev) no movement
else
{
  if (Prev == 00) && (P == 01) => x++;
  if (Prev == 01) && (P == 11) => x++;
  if (Prev == 11) && (P == 10) => x++;
  if (Prev == 10) && (P == 00) => x++;

  if (Prev == 00) && (P == 10) => x--;
  if (Prev == 01) && (P == 00) => x--;
  if (Prev == 11) && (P == 01) => x--;
  if (Prev == 10) && (P == 11) => x--;
  Pprev = P;
}

To reduce the number of compares we can merge Prev and P into one "change" byte

int x = 0;

P = readA() << 1 | readB();
if (P == Pprev) no movement
else
{
  change = change << 2;
  change = change | P;
  change = change & 0x0F;
  // now we have the previous state in bit 3 and bit 2  and the current state in bit 1 and bit 0

  switch (change)
  {
    case 0001:  // fall through..
    case 0111:
    case 1110:
    case 1000:
      x++;
      break;
    case 0010:
    case 0100:
    case 1101:
    case 1011:
      x--;
      break;
  }

  note from the 8 other states 4 are no movement and 4 are "double clicks"
}

another way to optimize speed is to make an array with the increments and decrements per value of change

int x = 0;
int8_t  incr[16] = { 0, 1, -1 0, .... };  // to be detailed
...

change = ((change << 2)  | readA() << 1 | readB() ) & 0x0F;
x += incr[change];

note: this latter version will always take the same amount of time to execute. note: this latter also handles no movements and double clicks correctly note: there is some sort of trade of between RAM and code

in fact if one would use a RE to monitor the speed of a motor in only in one direction, the array could contain steps of 0,1,2, and 3 ... so it could handle 'higher speeds' with less reads.

RobTillaart commented 3 years ago

Did work on interrupts of a PCF8574 in another context - https://github.com/RobTillaart/I2CKeyPad - yesterday. Worked pretty well!

thijstriemstra commented 3 years ago

nice! I'll give it a try today.

RobTillaart commented 3 years ago

Can you try this one, as I do not have an RE nearby

//
//    FILE: PCF8574_rotaryEncoder.ino
//  AUTHOR: Rob Tillaart
//    DATE: 2021-05-08
//
// PUPROSE: test PCF8574 library

//
//  RotaryEncoder    PCF8574      UNO
//  --------------------------------------
//    pin A           pin 0
//    pin B           pin 1
//    ....            ....     (up to 4 RE)
//
//                    SDA         A4
//                    SCL         A5
//                    INT         2
//

#include "PCF8574.h"
PCF8574 decoder(0x20);

// hold position of 4 RE + last position
uint8_t lastpos[4] = {0, 0, 0, 0};
int32_t encoder[4] = {0, 0, 0, 0};
volatile bool flag = false;

void moved()
{
  flag = true;
}

void setup()
{
  Serial.begin(115200);
  Serial.println(__FILE__);
  Serial.print("PCF8574_LIB_VERSION:\t");
  Serial.println(PCF8574_LIB_VERSION);

  pinMode(2, INPUT_PULLUP);
  attachInterrupt(0, moved, FALLING);
  flag = false;

  Wire.begin();
  Wire.setClock(100000);
  if (decoder.begin() == false)
  {
    Serial.println("\nERROR: cannot communicate to keypad.\nPlease reboot / adjust address.\n");
    while (1);
  }
}

void loop()
{
  if (flag)
  {
    updateRE();
    flag = false;
    for (int i = 0; i < 4; i++)
    {
      Serial.print("\t");
      Serial.print(encoder[i]);
    }
    Serial.println();
  }
}

// assumes 4 RE connected to one PCF8574
void updateRE()
{
  uint8_t cnt = 0;
  uint8_t val = decoder.read8();

  // check which of 4 has changed
  for (uint8_t i = 0; i < 4; i++)
  {
    uint8_t currentpos = (val & 0x03);
    if (lastpos[i] != currentpos) // moved!
    {
      cnt++;
      uint8_t change = (lastpos[i] << 2) | currentpos;
      switch (change)
      {
        case 0b0001:  // fall through..
        case 0b0111:
        case 0b1110:
        case 0b1000:
          encoder[i]++;
          break;
        case 0b0010:
        case 0b0100:
        case 0b1101:
        case 0b1011:
          encoder[i]--;
          break;
      }
      lastpos[i] = currentpos;
      val >>= 2;
    }
  }
}

// -- END OF FILE --

Think it could be wrapped in a class.

RobTillaart commented 3 years ago

a quick and dirty measurement indicates that a call to updateRE() takes less than 500 micros if there are no changes it takes ~250 micros.

With the PCF8575 one could monitor and read 8 RE at once ...

RobTillaart commented 3 years ago

BUG: There should be an initial read for the lastPositions in the code, otherwise the first clicks could be wrong...

thijstriemstra commented 3 years ago

Some stuff in there that seems unrelated:

Wire.begin();
  Wire.setClock(100000);
  if (decoder.begin() == false)
  {
    Serial.println("\nERROR: cannot communicate to keypad.\nPlease reboot / adjust address.\n");
    while (1);
  }
RobTillaart commented 3 years ago

If the decoder.begin() return false it cannot see the PCF on the I2C bus. but it is not the core of the idea

FIX the missing initialization

void initRE()
{
  uint8_t val = decoder.read8();
  for (uint8_t i = 0; i < 4; i++)
  {
    lastpos[i] = val & 0x03;
    val >>= 2;
  }
}

in setup() after the PCF.begin() call

RobTillaart commented 3 years ago

If the Wire.setClock() is set after the PCF.begin() it affects the performance of the updateRE() call

@600KHz => 75 - 150 micros() Assuming the overhead is 25 micros() and you want an interrupt load of 50% max one could handle an interrupt every 200-400 us. meaning up 2500~5000 interrupts per second (which is quite a bit e.g.

Given a car engine is max @12000 RPM = 200 RPS (assuming 4 interrupts per rotation) ==> 800 interrupts per second. A high speed drill (think Dremel or dentist) at 30.000 RPM = 500 RPS = 2000 IRQ/sec

RobTillaart commented 3 years ago

Created a class version of the code - https://github.com/RobTillaart/rotaryDecoder - check the development branch

As I do not have an RE nearby, could you test it?

Thanks, Rob


added more examples + single direction update version.

RobTillaart commented 3 years ago

Tested with one RE (got some bouncing, no caps) and worked pretty well.

RobTillaart commented 3 years ago

@thijstriemstra I close the issue as the example worked for me. If there are problems, just reopen the issue!

thijstriemstra commented 3 years ago

Thanks Rob. I didn't find time to test it yet, but eventually will, and I'll report back if there are any issues.

jephrepol commented 2 years ago

The example code has a bug which prevents 2nd, 3rd, 4th encoders from working as expected. In the updateRotaryDecoder function, the "val >>=2;" needs to be moved outside the if block. Movements from 2nd, 3rd, and 4th encoder will never execute the if block because this line is used to shift the values used in the next iteration of the for loop. Moving the line just outside the if block makes everything work great.

RobTillaart commented 2 years ago

@jephrepol Thanks for reporting, I will look into it asap

RobTillaart commented 2 years ago

@jephrepol Created a branch named develop with fix. Will be merged asap.

RobTillaart commented 2 years ago

Merged and released 0.3.6 oops redo build runs again...

RobTillaart commented 2 years ago

issue solved