Incremental encoder and "speed" of DigitalWrite

Hello everybody,
I hope I am in the good sub, sorry if not.

I'm using an incremental encoder to generate a command. When I'm turning it in the clockwise direction, the Arduino generates a LOW state on the digital pin 8. When I'm turning it in the counterclockwise direction, the Arduino generates a LOW state on the digital pin 9.

These two pins are wired to the GPIO pins of my device (BrightSign HD224 player, datasheet here : https://support.brightsign.biz/hc/en-us/articles/218065937-GPIO-Which-pins-correspond-to-which-buttons-)

Then, according to the direction, the device will scroll in a image list (next or previous image).
The player needs a change of state on the GPIO pins in order to generate an event. This is why I do a digitalWrite(LOW), the rest state being HIGH.

Everything works well, except when I'm turning the encoder too fast (just for you to understand my needs, I want to create a kind of flipbook / folioscope).
I think the problem is that there is a continous LOW state in this case, so the player is not able to detect the event.

Here is my code, taken on a website and slightly adapted to my needs :

/*     Stepper Motor using a Rotary Encoder
 *      
 *  by Dejan Nedelkovski, www.HowToMechatronics.com
 *  
 */
 
// defines pins numbers
// - enc_a is ENC Signal A line (Arduino digital pin 2)
// - enc_b is ENC Signal B line (Arduino digital pin 3)
 #define ENC_A 2
 #define ENC_B 3
 #define CW_cmd 8 
 #define CCW_cmd 9

// Main loop refresh period.
#define REFRESH_MS  10

// Main serial data connection to computer.
#define BAUD_RATE   9600

 // Encoder signal line states
volatile boolean state_a = 0;
volatile boolean state_b = 0;

// Rotation direction states
 volatile boolean cwState = 0;
 volatile boolean ccwState = 0;
 
// Encoder position
volatile int enc_pos = 0;
int enc_pos_prev = 0;

 
void setup() {

  pinMode(ENC_A, INPUT);
  pinMode(ENC_B, INPUT); 
  pinMode(CW_cmd,OUTPUT); 
  pinMode(CCW_cmd,OUTPUT);

  state_a = (boolean) digitalRead(ENC_A);
  state_b = (boolean) digitalRead(ENC_B);

    attachInterrupt(0, interrupt_enc_a, CHANGE);
    attachInterrupt(1, interrupt_enc_b, CHANGE); 

     Serial.begin(BAUD_RATE);
  
  }
  
void loop() {
  

  ccwState = 1;
  cwState = 1;
 //digitalWrite(CW_cmd, HIGH) ;
// digitalWrite(CCW_cmd, HIGH) ;
  
  
  if (enc_pos > enc_pos_prev){     
       cwState = 0;
       digitalWrite(CW_cmd, LOW) ; 
        //digitalWrite(CW_cmd, HIGH) ;
     }

     if (enc_pos < enc_pos_prev){     
       ccwState = 0;
       digitalWrite(CCW_cmd, LOW) ; 
       //digitalWrite(CCW_cmd, HIGH) ;
  
     }
  

   enc_pos_prev = enc_pos;


// Emit data
    Serial.print(state_a);
    Serial.print("\t");
    Serial.print(state_b);
    Serial.print("\t");
    Serial.print(cwState);
    Serial.print("\t");
    Serial.print(ccwState);
    Serial.print("\t");
    Serial.print(enc_pos);
    Serial.print("\t");
    Serial.print(enc_pos_prev);
    Serial.print("\t");
    Serial.print("\n");
    

  delay(REFRESH_MS);
}


// Detect pulses from depth encoder.

void interrupt_enc_a()
{
    if (!state_a) {
        state_b ? enc_pos++: enc_pos--;         
    }
    state_a = !state_a;
}

void interrupt_enc_b()  
{
    state_b = !state_b;
}

I tried to decrease the refresh time, or to add some digitalWrite(HIGH) in the loop but nothing works ...

Do you know if there is a way to create a kind of pulse signal ? Or any other solution to solve my problem ?

Many thanks :slight_smile:

That is a bit simplistic, it omits to consider enough the other pin.

Not sure I get this, what are you writing to here? Writing to a pin that is already set as an input will toggle the internal pull up resistor.

Things would be faster if you increased the baud rate, as writing out all that text takes time and slows down the repetition rate you can get from the read.

Basically you are polling the encoders pins to see if they have changed.

The proper way to do a responsive rotary encoder is generate an interrupt on a pin change and let the ISR perform a state machine. That way the switch bounce will be automatically compensated for.

Best use a library that does this. The one by Paul is very good.

See this link from Paul for more information of how to do it correctly
https://www.pjrc.com/teensy/td_libs_Encoder.html

It seems that What you are saying is that the target device needs to see a clean pulse (HIGH LOW HIGH) to take one step. What’s the shortest timing acceptable for this LOW state so that it’s taken into account and how long is one scroll? Is the scrolling code blocking and if it is, for how long?

If it’s less than a ms may be you can get away by having a delay in your code to create a clean pulse when you detect the encoder change

digitalWrite(CW_cmd, LOW) ;   // create the pulse 
delayMicroseconds(50);        // hold it long enough
digitalWrite(CW_cmd, HIGH) ;  // go back to rest

and it will kinda work OK if you turn not too fast

If it’s longer than a ms (or a few) then you need a different strategy but the scrolling might not be able to keep up with the encoder rotation and UI will lag.

Yes so to be precise, the encoder outputs A and B are wired on the Arduino's digital pins 2 and 3 respectively. Also the encoder is directly supplied by the Arduino (GND / 5V pins). To finish the Arduino's digital outputs pins 8 and 9, where the command is created, simply goes to the GPIO pins 3 and 4 (button 0 and 1) of my player.

Yes and from what I understand this is what I need to do to create an event on the player. The GPIO input needs a pulse, like the one create by a push button (HIGH/LOW/HIGH) .

Ok I will try this, thanks.

Thanks, I will take a look at this.

On the player, in the image list parameters, I can change the transition duration, which maybe be I can consider as the minimum time between two inputs received on the GPIO inputs. But maybe I'm wrong, and I don't know the minimum acceptable value. I will look for.

Thanks , i will try it !

Hi @akilou23 ,

this was a fun exercise for me to write the code for this application.
My code makes use of the NewEncoder-library from user @gfvalvo
All the details to detect encoder-pin-changes are done inside the library

to install this library follow this tutorial

This is void loop() of the code

void loop() {
  DetectAndQueClicks();
  CreateForwardPulses();
  CreateBackwardPulses();
}

Pretty short. Because the code is well organised in functions.
To qoute the comment about loop()
void loop() iterates at high speed, because the timing is NON-blocking
this enables to increment a counter-variable for each encoder-click
this can be seen as some kind of queing up the incoming encoder-clicks

creating pulses is done independent from this queing up
you could say the clicks are dammed up
each time an encoder-click is detected the counter increments by 1 "++"
each time an output-pulse has finished the counter is decremented by 1 "--"

If you would turn the encoder into one direction for a long time the code would create pulses for a even longer time as the pulses must have a minimum-length

To avoid creating forward/backward-pulses in a mess inbetween each other
if you change encoder-rotationdirection qued up clicks of the opposite direction where decremented first until they are zero.

for demonstration purposes the constant pulseLength is set to 1000 milliseconds
reduce this pulseLength to the value you would like to have.

#include "Arduino.h"
#include "NewEncoder.h"

const byte EncChA_Pin = 2;
const byte EncChB_Pin = 3;
const int minVal = -20;
const int maxVal =  20;
const int startVal = 0;


const byte forwardPulsePin  = 8;
const byte backwardPulsePin = 9;

const unsigned long pulseLength = 1000;

// Pins 2 and 3 should work for many processors, including Uno. See README for meaning of constructor arguments.
// Use FULL_PULSE for encoders that produce one complete quadrature pulse per detnet, such as: https://www.adafruit.com/product/377
// Use HALF_PULSE for endoders that produce one complete quadrature pulse for every two detents, such as: https://www.mouser.com/ProductDetail/alps/ec11e15244g1/?qs=YMSFtX0bdJDiV4LBO61anw==&countrycode=US&currencycode=USD

NewEncoder myEncoderObject(EncChA_Pin, EncChB_Pin, minVal, maxVal, startVal, FULL_PULSE);

NewEncoder::EncoderState myCurrentEncoderState;

int16_t currentValue;
int16_t prevEncoderValue;

int forwardCount  = 0;
unsigned long forwartPulseStarted = 0;

int backwardCount = 0;
unsigned long backwardPulseStarted = 0;

// easy to use helper-function for non-blocking timing
boolean TimePeriodIsOver (unsigned long &startOfPeriod, unsigned long TimePeriod) {
  unsigned long currentMillis  = millis();
  if ( currentMillis - startOfPeriod >= TimePeriod ) {
    // more time than TimePeriod has elapsed since last time if-condition was true
    startOfPeriod = currentMillis; // a new period starts right here so set new starttime
    return true;
  }
  else return false;            // actual TimePeriod is NOT yet over
}

void setup() {
  digitalWrite(forwardPulsePin, HIGH);
  pinMode(forwardPulsePin, OUTPUT);

  digitalWrite(backwardPulsePin, HIGH);
  pinMode(backwardPulsePin, OUTPUT);
  // myEncState is a variable of type EncoderState
  // EncoderState is a structured variable that has two "simple" variables
  // .currentValue which is type int16_t
  // (16 bit signed integer valuerange -36767 to 36767)
  // currentValue counts up / down with each pulse created through rotating the encoder
  // and
  // .currentClick which is of type "EncoderClick"
  // the variable type "EncoderClick" can have just 3 values
  // NoClick, DownClick, UpClick where "click" means a "pulse" created through rotating the encoder
  NewEncoder::EncoderState myEncState;

  Serial.begin(115200);
  Serial.println( F("Setup-Start") );

  if (!myEncoderObject.begin()) {
    Serial.println("Encoder Failed to Start. Check pin assignments and available interrupts. Aborting.");
    while (1) {
      yield();
    }
  } else {
    // store values of currentValue and EncoderClick into variable myEncState
    myEncoderObject.getState(myEncState);
    Serial.print("Encoder Successfully Started at value = ");
    prevEncoderValue = myEncState.currentValue;
    Serial.println(prevEncoderValue);
  }
}

void DetectAndQueClicks() {
  myEncoderObject.getState(myCurrentEncoderState);

  switch (myCurrentEncoderState.currentClick) {

    case NewEncoder::UpClick: // UpClick is reset to NoClick on reading it
      Serial.println("UpClick");
      if (backwardCount == 0) { // if no backward-clicks are queued up
        forwardCount++;         // whenever an UpClick occures increment
      }
      else { // if backward-clicks are queued up decrement backwardCount
        if (backwardCount > 0) {
          backwardCount--;
          Serial.println("backwardCount--");
        }
      }
      break;

    case NewEncoder::DownClick: // DownClick is reset to NoClick on reading it
      Serial.println("DownClick");
      if (forwardCount == 0) {
        backwardCount++; // whenever an DownClick occures increment
      }
      else {
        if (forwardCount > 0) {
          forwardCount--;
          Serial.println("forwardCount--");
        }
      }
      break;
  }
}

void CreateForwardPulses() {
  static boolean CreatePulse;

  if (!CreatePulse && forwardCount > 0) { // as long as forward clicks have been occurred
    forwartPulseStarted = millis();  // store actual timestamp
    CreatePulse = true;              // set flag-variable
    digitalWrite(forwardPulsePin, LOW); // start low-pulse
  }

  if (CreatePulse) {
    if ( TimePeriodIsOver(forwartPulseStarted, pulseLength) ) { // check if the number of milliseconds stored in pulseLength have passed by
      // if the number of milliseconds stored in pulseLength have passed by
      forwardCount--;
      CreatePulse = false;    // reset flagvariable to false
      digitalWrite(forwardPulsePin, HIGH); // end low-pulse
      Serial.println( F("forward pulse finished") );
    }
  }
}

void CreateBackwardPulses() {
  static boolean CreatePulse;

  if (!CreatePulse && backwardCount > 0) { // as long as forward clicks have been occurred
    backwardPulseStarted = millis();  // store actual timestamp
    CreatePulse = true;              // set flag-variable
    digitalWrite(backwardPulsePin, LOW); // start low-pulse
  }

  if (CreatePulse) {
    if ( TimePeriodIsOver(backwardPulseStarted, pulseLength) ) { // check if the number of milliseconds stored in pulseLength have passed by
      // if the number of milliseconds stored in pulseLength have passed by
      backwardCount--;
      CreatePulse = false;    // reset flagvariable to false
      digitalWrite(backwardPulsePin, HIGH); // end low-pulse
      Serial.println( F("backward pulse finished") );
    }
  }
}

// void loop() iterates at high speed
//because the timing is NON-blocking
// this enables to increment a counter-variable for each encoder-click
// this can be seen as some kind of queing up the incoming encoder-clicks
// creating pulses is done independent from this queing up
// you could say the clicks are dammed up
// each time an encoder-click is detected the counter increments by 1
// each time an output-pulse has finished the counter is decremented by 1
void loop() {
  DetectAndQueClicks();
  CreateForwardPulses();
  CreateBackwardPulses();
}

best regards Stefan

If I get your code right, you'll cancel some of the requested moves if the UI is lagging?

ie: I move up 10, down 7, if the animation is late, I end up only flipping through the first 3 images rather than seeing 10 forward and then 7 backward

PS: just for easier reading of your code, careful on the spelling of queue
Que ➜ Queue
are cued up ➜ are queued up

good catch. I corrected this

Yes it works just three pictures forward
without this it would be even worse flipping a picture forward inbetween flip one backward
flip one forward / flip one backward etc.

OP described what he wanted to do as a flipbook

so my understanding would be that all the movements should be recorded and played back.

Also with your code, if there is no lag and I move up 10 then down 7, you get this exact animation. But if there is some lag the actual result will vary, might be 6 up and 3 down or 3 up and nothing depending on the speed of the rotary and the duration of a sequence.
➜ this will result in inconsistent behaviour that will confuse the user

ideally of course there would be no lag at all but if there is the queue should record "10 up, 7 down, etc" and play this FIFO

Imagine somebody is turning the encoder very fast and repeatedly forward for a picture almost at the end of the sequence then recognising oh ! the picture I want to see is more at the beginning.
Depending on the maximum inputfrequency the receiving device can handle executing all flipping forward and after that flipping backwards might result in a long time until the wanted picture is shown.

Instead of waiting for all flips to be executed the flipping is made shorter.

Still an interesting variant to perform all forward / backward flipping.

I'm curious what behaviour the TO prefers
best regards Stefan

I had thought OP wanted to get some sort of animation driven by the rotary encoder - That's what a flipbook is about
Unknown

so going back and forth rather than a picture searching tool

Indeed in this case storing the exact order of each forward-click and backward-click makes sense.

So for realising this functionality my first idea is to use an array. Depending on how many pictures the flipbook does have maybe the whole RAM of an arduino uno (2kB) is not enough to hold all clicks. So one way to store more information would be to use compression:
count forward / backward up to 127 down to -127 and store the value if a direction-change is detected or if max-mumber +127 / -127 is reached
a different method would be to store direction in two bits and then do a lot of bitshifting for storing and re-playing the encoder-clicks.

Both interesting approaches and an intriguing application for using datacompression.

Going one step further: including storing the speed at which the encoder is rotated

so the sign of the number determines the direction

Yep - that could be one way to deal with it in a producer consumer design pattern, a bit like the Serial port works (fill in a circular buffer and read from the buffer to extract the oldest requirement)

yes although if you have to use the buffer it means you lagged anyway so it might not bring extra precision / user value

Hi guys,

Thanks for your messages, you're the best !!

I'm currently on the go for the work, so I don't have the setup with me to test, but as soon I go back home I will try everything and I will let you know

But to answer the discussion about alternatives solutions, I want to do as a flipbook as mentioned by JML. So for instance, if I move up 10, then down 7, I want this exact command to be executed.

However, it is not a problem for me to have a limited speed. I mean the speed of change between the images can be limited so even if the user increase the speed of rotation on the encoder, the images don't scroll faster when a certain speed is reached. I think this could bypass hardware limitations (arduino or player), if they really exist.

Obviously the faster possible - or unlimited (ie : 100% proportional to encoder speed) - will be the best. Currently nothing happen when I turn quickly so everything will be a good improvement haha

Once again thanks for your help ! Amazing community !

Seemed like an interesting rotary encoder application. Here's how I'd do it using NewEncoder. It only needs 1 bit to buffer each encoder click in a FIFO. I don't have any "flipbook" hardware, so I simulated that using delays in the loop() function. They limit the speed that the encoder clicks can be processed into page flips and force them to be buffered. You can see the buffering action by turning the encoder back and forth quickly several times and then stopping. The "flips" will take a while to catch up.

Main .ino:

#include "FlipBookController.h"

FlipBookController<256> controller(2, 3, FULL_PULSE); // Reserve 256 bytes (2048 steps) in the FIFO

void setup() {
  Serial.begin(115200);
  delay(1000);
  Serial.println("Starting");

  if (!controller.begin()) {
    Serial.println("FlipBookController Did Not Start");
    while (1) {
      yield();
    }
  }
}

void loop() {
  NewEncoder::EncoderClick nextFlip = controller.getNextFlip();

  switch (nextFlip) {
    case NewEncoder::NoClick:
      break;

    case NewEncoder::UpClick:
      Serial.println("Flip Forward");
      delay(500);
      break;

    case NewEncoder::DownClick:
      Serial.println("Flip Backward");
      delay(500);
      break;
  }
}

FlipBookController.h

#ifndef FLIPBOOK_CONTROLLER
#define FLIPBOOK_CONTROLLER
#include <Arduino.h>
#include "NewEncoder.h"

template <uint32_t Nbytes>
class FlipBookController : public NewEncoder {
  public:
    FlipBookController(uint8_t aPin, uint8_t bPin, uint8_t type) :
      NewEncoder(aPin, bPin, -5, 5, 0, type), numBits(Nbytes << 3) {
    }

    NewEncoder::EncoderClick getNextFlip() {
      if (fifoEmpty) {
        return NewEncoder::NoClick;
      }

      // Pull next bit from FIFO and map to click direction
      NewEncoder::EncoderClick returnValue;
      uint32_t fifoByte = readPtr >> 3;
      uint8_t fifoBit = readPtr & 0x07;

      noInterrupts();
      fifoFull = false;
      if ((fifo[fifoByte] & (1 << fifoBit)) > 0) {
        returnValue = NewEncoder::UpClick;
      } else {
        returnValue = NewEncoder::DownClick;
      }

      readPtr++;
      if (readPtr >= numBits) {
        readPtr = 0;
      }
      if (writePtr == readPtr) {
        fifoEmpty = true;
      }
      interrupts();
      return returnValue;
    }

  private:
    uint8_t fifo[Nbytes];
    const uint32_t numBits;
    uint32_t readPtr = 0;
    uint32_t writePtr = 0;
    bool fifoEmpty = true;
    bool fifoFull = false;

    virtual void ESP_ISR updateValue(uint8_t updatedState) {
      if (fifoFull) {
        return;
      }

      // Encode click direction as 1 bit and put in FIFO;
      fifoEmpty = false;
      uint32_t fifoByte = writePtr >> 3;
      uint8_t bitMask = 1 << (writePtr & 0x07);
      uint8_t delta = updatedState & DELTA_MASK;

      if (delta == INCREMENT_DELTA) {
        fifo[fifoByte] |= bitMask;
      } else {
        fifo[fifoByte] &= ~bitMask;
      }

      writePtr++;
      if (writePtr >= numBits) {
        writePtr = 0;
      }
      if (writePtr == readPtr) {
        fifoFull = true;
      }
    }
};
#endif

@StefanL38 , i've tried your code. Unfortunetaly, it works in a very chaotic way, flips are random when i move the encoder, even slowly and in a unique sens ... I don't know why

@gfvalvo , i wanted to try your code also but i don't understand where i'm supposed to declare the pins ? I tried defining them in the main, and replacing "delay" by digitalWrite but nothing happen.

For both, serial monitor displays strange things (ie: ⸮⸮:⸮⸮⸮⸮⸮⸮ʚ⸮⸮⸮⸮⸮y⸮y⸮⸮6y⸮Ji⸮⸮⸮⸮J⸮⸮⸮⸮⸮y?⸮?⸮J⸮⸮⸮ۛ&⸮⸮/)

Also, i was wondering if i should wire Arduino GND pin to player GND pin, to have a common ground and be sure the voltage level are correct for the player or something like this ? (I've tried but i didn't notice a change)

Thanks for your help !

they are the first 2 parameters of the FlipBookController instance, here 2 and 3

as the code shows these 2 (aPin and bPin) are used to call the NewEncoder constructor

class FlipBookController : public NewEncoder {
  public:
    FlipBookController(uint8_t aPin, uint8_t bPin, uint8_t type) :
      NewEncoder(aPin, bPin, -5, 5, 0, type), numBits(Nbytes << 3) {
    }

I guess this is the reason:

to what pulse-length did you set
const unsigned long pulseLength = 1000;
?

adapt the baudrate in the serial monitor window to 115200
This is in the bottom right of the window
image

Yes you should connect ground of the Arduino with ground of your device

best regards Stefan

Ok thanks, and then for the output commands (pin 8 and 9), I have to declare them in the main and replace your delay by digitalWrite with the associated pins, right ? Anything else ?

Ok thanks, I will try to play with the pulse length

Ok i will change the baudrate also, it is kind of convenient to see what happen on the serial monitor

Ok for the ground, thanks.