Release 2017.4.20 / Update 2017.10.22

Using rotary encoder part 2 : High-Res with “non-click”

Previously, I wrote about efficient Sketch how to use click type Rotary encoder. But, it wasn’t for non-click type encoder.

So I will try write new Sketch for it, and more, it makes worth to High-Resolution. No need extra electronic parts, just a Sketch.

Of course, it is not a perfect method. If you want it, you should get more expensive parts like “absolute-type” encoder. But I think my Sketch will works pretty well.

New sketch is available on Part 3 (2017/10/22)

Wiring

Necessary electric parts is non-click Rotary Encoder, and LED if you have. Digital pins for Rotary Encoder is pull up by inside Arduino.

Rotary Encoder

Generally, rotary encoder judges “forward” or “backward” by comparing 2 digital pins of the current combination and the last. And “click-type” is consist of 4 different combination as one “click”. So if you have “20 click” encoder, there are 80 tangent point for each rotation on it.

If we can pick up all the change, we could get more high-resolution control by the encoder. That’s the what I want.

You can buy “non-click-type” rotary encoder on shops.

But most of those are fewer tangent point, and it hard to find one which have many tangent point. So I find “click-type” encoder which has more tangent point, and remodel to “non-click-type” physically.

Remodeling Rotary Encoder

Do take your responsibility for your own if you will try this. I will never take for yours.

This idea was coming from this article.(Sorry, this page is in Japanese only.)

The mechanism of “click” is generated by protulutions meshing with the rotating table inside. And it becomes “non-click” if you remove it.

In this article, I use this Rotary Encoder which has 24 clicks.

It will have 96 resolution, in my theory.

Remove caulking that connects the foundation.
You can see mechanism of tangent point.
When further separated from the knob, you can see what makes “click” mechanism. So bend the protrusion not to touch ditch.

Mechanism for “click” differs from each type. So this way is reference.

Sketch

I noticed a miss in the Sketch and modified it.(Ver1.1) So it may improved encoder-read. (16/10/2017)
New sketch is available on Part 3. (22/10/2017)

This method uses “MsTimer2” library. So if you haven’t installed it yet, get from “Library Manager”.

Download this project file,

Sketch:High Res Rotary Encoder

or copy code of down below.

If you fail to compile, read here.

// Rotary Encoder Reading with watch dog timer and Dual Gauge
// HighResEnc Version 1.1 by jumbler

#include <MsTimer2.h>
#define TIM MsTimer2

//pins
#define ENCA 2
#define ENCB 3
#define LED  4

//encoder constants
#define ENC_JUDGE 30
#define ECUR B00000011  // enc current position
#define EPRE B00001100  // enc previous position
#define EHOM B00110000  // enc home position
#define ETRG B01000000  // enc has been triggered
#define ECHG B10000000  // encoder count has been charged

const    byte  enc_pins[2] = {ENCA, ENCB};
volatile byte  enc_status;
volatile short enc_count = 0;


void setup() {
  pinMode(LED, OUTPUT);
  Serial.begin(38400);

  for (byte i = 0 ; i < 2 ; i++)
  {
    pinMode(enc_pins[i], INPUT_PULLUP);
    digitalWrite(LED, 1 - i);
    delay(1000);
  }

  TIM::set(1, ENC_GAUGE);
  TIM::start();
  enc_status = ENC_PIN_READ() << SBIT(EHOM);
}


void loop() {
  static int val;
  static unsigned long time_led;

  if (SBVAL(enc_status, ECHG))
  {
    digitalWrite(LED, HIGH);
    val = ENC_COUNT(val);
    Serial.println(val);
    time_led = millis() + 10;
  } else {
    if (millis() > time_led) digitalWrite(LED, LOW);
  }
}



short ENC_COUNT(int val) {
  static int enc_old;
  if (enc_old != enc_count)
  {
    val += enc_count - enc_old;
    enc_old = enc_count;
    enc_status = enc_status & ~ECHG;
  }
  return val;
}



byte SBIT(byte layer) {
  for (byte i = 0 ; i < 8; i++) if ((layer >> i)&B00000001) return i;
}
byte SBVAL(byte val, byte layer) {
  byte tmp = (val & layer) >> SBIT(layer);
  return tmp;
}



byte ENC_PIN_READ() {
  // Read encoder pins status
  byte enc_cur = (digitalRead(ENCB) << 1) + digitalRead(ENCA);
  // Modify position order
  if (enc_cur < 2) enc_cur = 1 + (enc_cur * -1);

  if (!SBVAL(enc_status, ETRG))
  {
    if (SBVAL(enc_status, EHOM) != enc_cur) enc_status = enc_status | ETRG;
  }

  // apply update to enc_status
  enc_status = (enc_status & B11110000) + ((enc_status & ECUR) << 2) + enc_cur;

  return enc_cur;
}



void ENC_GAUGE() {
  static unsigned short gauge[2];
  byte curr = ENC_PIN_READ();

  // if encoder change has been triggerd
  if (SBVAL(enc_status, ETRG))
  {
    byte prev, dist;
    dist = SBVAL(enc_status, EHOM);

    for (byte i = 0 ; i < (ENC_JUDGE * 1.5) ; i++)
    {
      curr = ENC_PIN_READ();
      prev = SBVAL(enc_status, EPRE);

      // each gauge for "moved" or "not moved"
      bool bias = (curr != dist) ? 1 : 0;
      gauge [bias]++;
      int goal = gauge[1] - gauge[0];

      if (abs(gauge[1] - gauge[0]) > ENC_JUDGE)
      {
        // encoder moved!
        if (goal > 0)
        {
          // increase or decrease
          bool dir = ((curr - dist) > 0) ? 1 : 0;
          if (curr == 0 && dist == 3) dir = 1;
          else if (curr == 3 && dist == 0) dir = 0;

          // add count by the direction
          if (dir) enc_count++;
          else enc_count--;

          // update home position
          enc_status = (enc_status & ~EHOM) + (curr << SBIT(EHOM));
          enc_status = enc_status | ECHG;
        }

        for (byte ii = 0 ; ii < 2 ; ii++) gauge[ii] = 0;
        enc_status = enc_status & ~ETRG;
        break;
      }
    }
  }
}

I recommend you to use “project file”. Because it is tabbed.

How to use is very simple. Set value to “ENC_COUNT()”. And it returns applied value.

static int val;

val = ENC_COUNT(val);
Serial.println(val);

In sample code, you can see ‘val’ is printed to “serial monitor”.

Also, you can check the behavior by Arduino “serial plotter”.(Ctrl+Shift+M)

Although it can’t catch fast change, it won’t be extreme bullshit value.

It becomes jagged if it failed.

I haven’t used this code with other routine so far. If you like this sketch, let me know your comment.

Incidentally, I explain about how it works as reference.

On my theory

First of all, we have to recognize how many “chattering” occurred.

Watch the “Chattering”

By using attachinterrupt to record and display pin changes. There is a problem use attachinterrupt and Serial.print use same time in a function. So it keeps the changes in arrays and prints all after intervals. (Because this sketch function slightly wrong, data of first and last goes wrong. Never minds)

// watch chattering

#define ENCA 2
#define ENCB 3
#define LED  4
const    byte  enc_pins[2] = {ENCA, ENCB};

void setup() {
  pinMode(LED, OUTPUT);
  Serial.begin(38400);
  
  for (byte i = 0 ; i < 2 ; i++)
  {
    pinMode(enc_pins[i], INPUT_PULLUP);
    attachInterrupt(i, ENC_HIT, CHANGE);
    digitalWrite(LED, 1 - i);
    delay(1000);
  }
}

#define MEM 200

volatile unsigned long time_enc_read[MEM];
volatile byte          enc_stat[MEM];
volatile unsigned int  read_count;


void loop() {
  if (read_count > 0 && (micros() - time_enc_read[read_count]) > 1000000)
  {
    for (byte i = 0 ; i <= read_count ; i++)
    {
      if (i < 100) Serial.print("0");
      if (i < 10) Serial.print("0");
      Serial.print(i);
      Serial.print(" ");
      Serial.print((char)('A' + enc_stat[i]));
      Serial.print(" ");
      Serial.print(time_enc_read[i] - time_enc_read[max(0, i - 1)]);
      Serial.println();
    }
    Serial.println();
    read_count = 0;
  }
}


byte ENC_PIN_READ() {
  // Read encoder pins
  byte enc_cur = (digitalRead(ENCB) << 1) + digitalRead(ENCA);
  // Modify position order
  if (enc_cur < 2) enc_cur = 1 + (enc_cur * -1);
  return enc_cur;
}

void ENC_HIT() {
  digitalWrite(LED, HIGH);
  enc_stat[read_count] = ENC_PIN_READ();
  time_enc_read[read_count] = micros();
  read_count++;
  digitalWrite(LED, LOW);
}

There are so many “chattering” on. “A” to “D” means pattern of encoders 2 pin. So you can see the patterns go back and forth repeatedly.

Sometimes it repeats over 100 time, on the other hands, it never appears. On the fast move of encoder, you can’t tell which pattern is correct.

Counterplan

On this situation, attachinterrupt may not helps counting.

So I thought to use “gauging” which I invented and “timer” as flash update.

  • confirmed position is “Home”
  • latest position is “Current”

I judge a position by comparing those two elements.

I wrote a chattering-less switch library with “gauging” method.
(26.6.2017)

If “Home” and “Curr” is different, “Trigger” goes on. But it isn’t confirmed yet. By triggered, digital pins is read repeatedly.

And it watches changes of pins by two gauge. If one of them reaches value of “ENC_JUDGE”, it is comfirmed “moved” or “not moved” by the gauge’s value. If it moves, update “Home” position and count. Or abort if it is “not move”.

This is the basic task for reading pins.

Process and Constant

Patterns for 2 pins can be expressed by 2 bits. So, I set several encoder’s status to a byte in variable “edc_status”.

#define ECUR B00000011  // enc current position
#define EPRE B00001100  // enc previous position
#define EHOM B00110000  // enc home position
#define ETRG B01000000  // enc has been triggered
#define ECHG B10000000  // encoder count has been charged

Those values is manipulated by “bit-shift”. So I made some functions.

// Determine and return the number of bits
byte SBIT(byte layer) {
  for (byte i = 0 ; i < 8; i++) if ((layer >> i)&B00000001) return i;
}

// Masks only on the specified bits and returns the numeric value right-shifted to the minimum bit
byte SBVAL(byte val, byte layer) {
  byte tmp = (val & layer) >> SBIT(layer);
  return tmp;
}

Then, I create a function to read the pin state.

byte ENC_PIN_READ() {
  // Acquire current pin state with 2 bits
  byte enc_cur = (digitalRead(ENCB) << 1) + digitalRead(ENCA);
  // Since in decimal notation, the order of the pattern is 1023, I forced it to 0123.
  if (enc_cur < 2) enc_cur = 1 + (enc_cur * -1);

  // Trigger is on, if "Curr" and "Home" is different.
  if (!SBVAL(enc_status, ETRG))
  {
    if (SBVAL(enc_status, EHOM) != enc_cur) enc_status = enc_status | ETRG;
  }

  // Update "enc_Status"
  enc_status = (enc_status & B11110000) + ((enc_status & ECUR) << 2) + enc_cur;

  return enc_cur;
}

Timer works to this “ENC_GAUGE”.

void ENC_GAUGE() {
  static unsigned short gauge[2];
  // Read pins at first
  byte curr = ENC_PIN_READ();

  // Excute repeat read if it were triggered
  if (SBVAL(enc_status, ETRG))
  {
    byte prev, dist;
    dist = SBVAL(enc_status, EHOM);

    // repeat digital pins and update
    for (byte i = 0 ; i < (ENC_JUDGE * 1.5) ; i++)
    {
      curr = ENC_PIN_READ();
      prev = SBVAL(enc_status, EPRE);

      // gauging for "move" and "not move"
      bool bias = (curr != dist) ? 1 : 0;
      gauge [bias]++;
      int goal = gauge[1] - gauge[0];

      // confirmation of filled gauge
      if (abs(gauge[1] - gauge[0]) > ENC_JUDGE)
      {
        // it moved !
        if (goal > 0)
        {
          // find direction of encoder
          bool dir = ((curr - dist) > 0) ? 1 : 0;
          if (curr == 0 && dist == 3) dir = 1;
          else if (curr == 3 && dist == 0) dir = 0;

          // refect to variable "enc_count"
          if (dir) enc_count++;
          else enc_count--;

          // update "Home" position
          enc_status = (enc_status & ~EHOM) + (curr << SBIT(EHOM));
          enc_status = enc_status | ECHG;
        }

        // Reset gauge and Trigger
        for (byte ii = 0 ; ii < 2 ; ii++) gauge[ii] = 0;
        enc_status = enc_status & ~ETRG;
        break;  // escape repeat
      }
    }
  }
}

By not to update encoder change immediately, but charge to function “ENC_COUNT()”, prevent mistakes and it makes easy access to counter value.

short ENC_COUNT(int val) {
  // Previous counter value
  static int enc_old;
  // If values of counter and previous is different, add and update.
  if (enc_old != enc_count)
  {
    val += enc_count - enc_old;
    enc_old = enc_count;
    // Reset
    enc_status = enc_status & ~ECHG;
  }
  return val;
}

Adjustment

This sketch is optimized for Arduino Uno. So it may not works as well if you use another type Arduino. Several changes of constant may helps for your trouble shooting.

  • ENC_JUDGE
  • TIM::set(1, ENC_GAUGE);

ENC_JUDGE value is for “repeating times” and “judging gauge”. If wobble is terrible, changing here may resolves it.

“Timer” is set to 1ms. This is minimum value. But if you use faster one than Arduino Uno, it may not necessary to be 1ms.

By this explain, I realized some code are irrelevant. Those were abandoned on my try and error. But as it are. Sorry. Modify it yourself if you care about.

Reference Site

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.