rotary quadrature encoder: let's make a digital safe... © CC BY-SA

Build a treasure chest or use an existing storage box, attach a push-button quadrature rotary encoder (endless spinning dial) and lets go!

  • 4,733 views
  • 3 comments
  • 9 respects

Components and supplies

13907 01
SparkFun ESP32 Thing
Optional, may be any ESP32 device. *Need one of either of these
×1
Ard nano
Arduino Nano R3
Optional, any Arduino should work. *Need one of either of these
×1
Bourns pec11r 4215f s0024 image 75px
Rotary Encoder with Push-Button
https://www.amazon.co.uk/gp/product/B0085I4D5C/
×1
Knob
6mm shaft, and any knob would do, 3d print yourself something more like a Safe Dial by adapting this: https://www.thingiverse.com/thing:169291
×1
12002 04
Breadboard (generic)
×1
11026 02
Jumper wires (generic)
×1
Kemet c320c104k5r5ta image
Capacitor 100 nF
×13

Necessary tools and machines

Common Sense
Basic electrical knowledge, health and safety of you and others, ask an old person...

Apps and online services

About this project

Standing on the shoulders of giants (i.e. learning from great inspirational people who have gone before you) allows us to create things in a fraction of the time compared to past generations.

I need to understand the ESP32, rotary Quadrature encoders and practise binary manipulation....

If you don't feel comfortable with binary, head over to this curriculum learning resource from SparkFun.

A couple of years ago I bought some push-button style spinning inputs (which are rotary quad encoders) for very cheap, but they require a micro-controller to understand the timing of two pulsing data lines (basically oppositely phased, or out of sync, signals indicating increase or decrease in a direction). I never got around to needing them enough, but now plan to use them in a heating system input device (think thermostat control).

To make my experience useful for everyone else I present the more interesting example of a treasure chest with a digital spinning padlock...

Build a fantastic treasure chest according to this guide, or locate a suitable storage box and drill a hole in the front for the rotating shaft of the switch (rotary quadrature encoder but 'switch' from here on). We can then easily stick the switch through from the back and then lock it in place with the included washer and nut.

I've stuck two on a piece of prototype board with headers attached. Using a purple (channel A), yellow (common) and green (channel B) jumper wire.

The first one on the protoboard (which we will use in this article) is nothing more than a rotary switch with headers, where as the second has additional filtering circuitry to de-bounce the signals, as is recommended for electro-mechanical switches. These additions are currently unnecessary, however with a more complicated, noisy circuit, interrupt-based, or faster spinning of the shaft they may become essential, if you're interested then look up the chip 74HC14.

To ensure the quad encoders work, I'm following a guide for arduino, sadly the guide I found was no longer available (404 error), so using the internet's library (archive.org) I looked at a past version of the page from 2014.

The setup is quick, only 3 wires plus the usb cable and including software I was done in less than 5minutes. Now it's your turn!

Looking from above on the 3 pin side, assuming it's in a breadboard, using the datasheet (an identical products datasheet) we can see that the first pin is channel A, then common (think ground), then B.

Our example guide says pin AnalogInput Zero is best for A and AnalogInput 1 for B, but it also mentions using Analog 8 + 9 without code change. It's using the loop to read the hardware registers and access the pin values instead of being interrupt based which is required and really matters for timing critical applications

I am using a cheap arduino nano (revision3) clone, which has usb to serial so I just plug it in and upload the code, then starting serial monitor it instantly works with changing values scrolling correctly.

I modified the code to ensure the correct pin numbers, removing the 2 top lines defining the pins as 14 and 15, and manually replacing them with A0 and A1 which are predefined for each board and therefore correct for the nano.

Included below is the actual code I used:

#define ENC_PORT PINC 
void setup() 
{ 
 /* Setup encoder pins as inputs */ 
 pinMode(A0, INPUT); 
 digitalWrite(A0, HIGH); 
 pinMode(A1, INPUT); 
 digitalWrite(A1, HIGH); 
 Serial.begin (115200); 
 Serial.println("Start"); 
} 
void loop() 
{ 
static uint8_t counter = 0;      //this variable will be changed by encoder input 
int8_t tmpdata; 
/**/ 
 tmpdata = read_encoder(); 
 if( tmpdata ) { 
   Serial.print("Counter value: "); 
   Serial.println(counter, DEC); 
   counter += tmpdata; 
 } 
} 
/* returns change in encoder state (-1,0,1) */ 
int8_t read_encoder() 
{ 
 static int8_t enc_states[] = {0,-1,1,0,1,0,0,-1,-1,0,0,1,0,1,-1,0}; 
 static uint8_t old_AB = 0; 
 /**/ 
 old_AB <<= 2;                   //remember previous state 
 old_AB |= ( ENC_PORT & 0x03 );  //add current state 
 return ( enc_states[( old_AB & 0x0f )]); 
} 

=============== Finished ? =================

For some that will be enough, the counter variable contains a number that spins from 0 to 255 and from that it would be trivial to add code to trigger an output pin attached to a solenoid based on a series of predefined values (like a safe combination would).

If I was to do this I would probably implement a buffer of 4 numbers, and lets say on each change of direction, push the number into the buffer, and test the result. You could also require the user to pass 0 before each subsequent number is stored like a safe does.

Alternatively, instead of on change of direction, it could use the other 2 pins of the rotary switch which are the push button pins, bridging when the shaft is depressed (pushed in), at which point storing the counter value.

================ ESP32 =================

Now the challenging part for me, replacing the arduino Nano with the ESP32. A dirt cheap wifi/bluetooth micro-controller, which at this time is still having drivers written to bring full support to the arduino IDE (integrated development environment), but it progresses every day! Mine is a sparkfun ESP32 thing (has onboard lipo charger), but any ESP32 dev board will work.

All I need is a simple pair of interrupts and a serial output, but the common issue I seem to run into is a limited selection of pins available for use as serial or interrupts, and the use of hardware registers (fixed addressed locations in memory usually for controlling pins) like the first line and last few lines of the previous example.

The ESP32 complicates matters more by allowing the pins to be assigned dynamically, meaning not necessarily consistently. There are fortunately some predefined rules, like serial (UART) support only being on certain pins, and again interrupts have their own rules as well as there being multiple types.

I'll crack into that over the weekend after refreshing my mind on the documentation of the hardware registers and interrupts...

================ ESP32 Arduino Core ===============

Having spoken to Kolban on IRC in the #esp32 channel I got a pointer in the right direction to get access to the pin registers. The function gpio_input_get() returns the first 32pins, and there is a _high() version for the later pins (above 32).

Having modified my code accordingly it wouldn't compile until I found and included the header file for esp32 gpio function. (grep to the rescue )

#in arduino hardware folder (where you install esp32 arduino core)
# enter the following:
grep -rnw -e "gpio_input_get"
#Which located my function in ~/Arduino/hardware/espressif/esp32/rom/gpio.h

'So the esp32 code follows and compiles.

#define ENC_A 11 
#define ENC_B 21 
// initially was going to hand pull the required bits from esp32 gpio via 
// the 32bit functions gpio_input_get() and gpio_input_get_high() and test 
// something like gpio_input_get() & (1<<11 | 1<< 21) == (1<<11 | 1<<21) 
// but looking for the docs on the functions I found this line in gpio.h 
//#define GPIO_INPUT_GET(gpio_no)     ((gpio_no < 32)? ((gpio_input_get()>>gpio_no)&BIT0) : ((gpio_input_get_high()>>(gpio_no - 32))&BIT0)) 
// this will involve two calls to the function which is not okay really, 
// as the pins may have changed value between calls potentially. One should 
// suffice, maybe at the expense of a second variable or additional shifts. 
#include <rom/gpio.h> 
void setup() 
{ 
 /* Setup encoder pins as inputs */ 
 pinMode(ENC_A, INPUT); 
 digitalWrite(ENC_A, HIGH); 
 pinMode(ENC_B, INPUT); 
 digitalWrite(ENC_B, HIGH); 
 Serial.begin (115200); 
 Serial.println("Start"); 
} 
void loop() 
{ 
static uint8_t counter = 0;      //this variable will be changed by encoder input 
int8_t tmpdata; 
/**/ 
 tmpdata = read_encoder(); 
 if( tmpdata ) { 
   Serial.print("Counter value: "); 
   Serial.println(counter, DEC); 
   counter += tmpdata; 
 } 
} 
/* returns change in encoder state (-1,0,1) */ 
int8_t read_encoder() 
{ 
 static int8_t enc_states[] = {0,-1,1,0,1,0,0,-1,-1,0,0,1,0,1,-1,0}; 
 static uint8_t old_AB = 0; 
 static uint32_t curval = 0; 
 /**/ 
 old_AB <<= 2;                   //remember previous state 
 //bit shift old_AB two positions to the left and store. 
curval = gpio_input_get(); 
 // returns gpio pin status of pins - SEE DEFINE or gpio.h  
 //note to self: these curval bits are probably backwards... 
old_AB |= ( ( (curval & 1<< ENC_A ) >> ENC_A  | (curval & 1<< ENC_B ) >> (ENC_B - 1) ) & 0x03 );  
 //add current state and hopefully truncate to 8bit  
return ( enc_states[( old_AB & 0x0f )]); 
 // return the array item that matches the known possible encoder states 
 // Thanks to kolban in the esp32 channel, who has many great books on iot, 
 // for his initial help at my panic on the esp32 gpio access.
 // long live IRC :) 
}  

Just got to actually test it now, but I've got distracted looking at LCD screen datasheets, as I have an old JDH162A (2line 16pin) that I equally should have hooked up at least once by now..

========= TESTING AND FIXUPS ==========

Having tested it there were unexpected results.

  • Firstly I could not get serial output for love nor money, eventually resolved by having to reboot the esp32 after each upload, which I didn't need to do with the example wifi sketch.
  • Secondly: Pullups...

having added delays and separated each element of the method, passing each term to serial.println, I couldn't change the result and being none the wiser I sat there thinking about pull-down resisters for a while. Eventually I realised I needed pull up resisters or to enable the internal ones because the "switch" is connecting common(ground) to pin_A and pin_B which must therefore be pulled high.

[ It turns out I'd overlooked that the code I initially used on the arduino utilised the old technique to set pullups, which is to write 1 (high logic level) to the pin (causing pullup resister to pullup the pin to 5v) after setting the pin to input mode which initially by default pulls the pin down to logic level 0 (0volts). After a certain arduino ide version the INPUT_PULLUP functionality was implemented. ]

Changing the pinmode definition fixed everything.

void setup() 
{ 
 /* Setup encoder pins as inputs */ 
 pinMode(ENC_A, INPUT_PULLUP); 
 digitalWrite(ENC_A, HIGH); 
 pinMode(ENC_B, INPUT_PULLUP); 
 digitalWrite(ENC_B, HIGH); 

There is still a timing issue when scrolling the knob but occasionally intermittent at most, possibly all my delays and serial prints affect things, and so I'll next be trying the de-bouncing circuitry, and switching the code to use interrupts.

=============== Debouncing =================

Everyone's favourite reason to look at oscilloscopes and visualise what happens to the pin of the microcontroller...because switches are more like springs, One press results in on-off-on-off-on...

I ended up using the schmitt trigger to debounce the circuit and you can see my initial version on the same protoboard in the photos. It worked, what more can I say. There are a vast number of youtube videos and resources I've watched on or around the subject, but really you should read the recent hackaday article on the humble schmitt trigger which covers enough for most needs.

=========== Final conclusion? ================

On the ESP32 front:

  • I should improve my prgramming in C because I will need to utilise some features not yet supported in the Arduino Port for esp32, and keep an eye on the esp32 port of micropython which is very promising.
  • Debounced Interrupts are required for absolute timing and avoiding missed steps.

Generally:

  • Having written this over a few sessions I should have looked for comments in-between sessions, because as soon as I finished it I then read two comments mentioning pull-ups. Doh!

This has been good fun as far as exploring docs, lateral thinking and finally achieving what we set out to.

Custom parts and enclosures

Safe Dial
3d Print this after adjusting to accommodate your switches shaft, or use a pre-built knob.

Schematics

QuadEncoder
Fritzing file showing setup
quadencoder_eaXlbu8m2b.fzz

Code

esp32 arduino code FIRST ATTEMPTArduino
Copy and paste, adjust pin numbers, using pins under 32, or adjust function to be gpio_input_get_high() if you wish to use the top 32 gpio register. -- NEEDS PULL UP RESISTORS OR pinmode(INPUT_PULLUP)
#define ENC_A 11
#define ENC_B 21

// initially was going to hand pull the required bits from esp32 gpio via
// the 32bit functions gpio_input_get() and gpio_input_get_high() and test
// something like gpio_input_get() & (1<<11 | 1<< 21) == (1<<11 | 1<<21)
// but looking for the docs on the functions I found this line in gpio.h

//#define GPIO_INPUT_GET(gpio_no)     ((gpio_no < 32)? ((gpio_input_get()>>gpio_no)&BIT0) : ((gpio_input_get_high()>>(gpio_no - 32))&BIT0))

// this will involve two calls to the function which is not okay really,
// as the pins may have changed value between calls potentially. One should
// suffice, maybe at the expense of a second variable or additional shifts.
#include <rom/gpio.h>

 
void setup()
{
  /* Setup encoder pins as inputs */
  pinMode(ENC_A, INPUT); // OR INPUT_PULLUP
  digitalWrite(ENC_A, HIGH);
  pinMode(ENC_B, INPUT);
  digitalWrite(ENC_B, HIGH);
  Serial.begin (115200);
  Serial.println("Start");
}
 
void loop()
{
 static uint8_t counter = 0;      //this variable will be changed by encoder input
 int8_t tmpdata;
 /**/
  tmpdata = read_encoder();
  if( tmpdata ) {
    Serial.print("Counter value: ");
    Serial.println(counter, DEC);
    counter += tmpdata;
  }
}
 
/* returns change in encoder state (-1,0,1) */
int8_t read_encoder()
{
  
  static int8_t enc_states[] = {0,-1,1,0,1,0,0,-1,-1,0,0,1,0,1,-1,0};
  static uint8_t old_AB = 0;
  static uint32_t curval = 0;
  /**/
  old_AB <<= 2;                   //remember previous state
  //bit shift old_AB two positions to the left and store.

  curval = gpio_input_get();  // returns gpio pin status of pins - SEE DEFINE 

  //note to self: these curval bits are probably backwards...
 
  old_AB |= ( ( (curval & 1<< ENC_A ) >> ENC_A  | (curval & 1<< ENC_B ) >> (ENC_B - 1) ) & 0x03 ); 
  //add current state and hopefully truncate to 8bit 
 
  return ( enc_states[( old_AB & 0x0f )]);
  // return the array item that matches the known possible encoder states

  // Thanks to kolban in the esp32 channel, who has a great book on everything iot,
  // for his initial help at my panic on the esp32 gpio access. long live IRC :)
}
ESP32 WORKING CODEArduino
#define ENC_A 12
#define ENC_B 13

// initially was going to hand pull the required bits from esp32 gpio via
// the 32bit functions gpio_input_get() and gpio_input_get_high() and test
// something like gpio_input_get() & (1<<11 | 1<< 21) == (1<<11 | 1<<21)
// but looking for the docs on the functions I found this line in gpio.h

//#define GPIO_INPUT_GET(gpio_no)     ((gpio_no < 32)? ((gpio_input_get()>>gpio_no)&BIT0) : ((gpio_input_get_high()>>(gpio_no - 32))&BIT0))

// this will involve two calls to the function which is not okay really,
// as the pins may have changed value between calls potentially. One should
// suffice, maybe at the expense of a second variable or additional shifts.
#include <rom/gpio.h>

 
void setup()
{
  /* Setup encoder pins as inputs */
  pinMode(ENC_A, INPUT_PULLUP);
  digitalWrite(ENC_A, HIGH);
  pinMode(ENC_B, INPUT_PULLUP);
  digitalWrite(ENC_B, HIGH);
  Serial.begin (115200);
      delay(10);
    delay(10);

  //Serial.println("Start");
}
 
void loop()
{
 static uint8_t counter = 0;      //this variable will be changed by encoder input
 int8_t tmpdata;
 /**/
  tmpdata = read_encoder();
  if( tmpdata ) {
    Serial.print("Counter value: ");
    Serial.println(counter, DEC);
    counter += tmpdata;
  }
    else{
   //   Serial.println("No Change");
      }
  delay(10);

} 
 
/* returns change in encoder state (-1,0,1) */
int8_t read_encoder()
{
  
  static int8_t enc_states[] = {0,-1,1,0,1,0,0,-1,-1,0,0,1,0,1,-1,0};
  static uint8_t old_AB = 0;
  static uint32_t curval = 0;
  static uint32_t curtmpA = 0;
  static uint32_t curtmpB = 0;
  /**/
  old_AB <<= 2;                   //remember previous state
  //bit shift old_AB two positions to the left and store.

  curval = gpio_input_get();  // returns gpio pin status of pins - SEE DEFINE 
 // Serial.println("curval");
  //Serial.println(curval);
  //note to self: these curval bits are probably backwards...
 curtmpA = (curval & 1<< ENC_A ) >> ENC_A;
//  Serial.println("curtmp A");
  //Serial.println(curtmpA);

curtmpB=(curval & 1<< ENC_B ) >> (ENC_B - 1);
 //Serial.println(curtmpB);
  old_AB |= ( ( curtmpA | curtmpB ) & 0x03 ); 
  //add current state and hopefully truncate to 8bit 
 
  return ( enc_states[( old_AB & 0x0f )]);
  // return the array item that matches the known possible encoder states

  // Thanks to kolban in the esp32 channel, who has a great book on everything iot,
  // for his initial help at my panic on the esp32 gpio access. long live IRC :)
}
arduino sketchArduino
#define ENC_PORT PINC 
void setup() 
{ 
 /* Setup encoder pins as inputs */ 
 pinMode(A0, INPUT); 
 digitalWrite(A0, HIGH); 
 pinMode(A1, INPUT); 
 digitalWrite(A1, HIGH); 
 Serial.begin (115200); 
 Serial.println("Start"); 
} 

void loop() 
{ 
static uint8_t counter = 0;      //this variable will be changed by encoder input 
int8_t tmpdata; 
/**/ 
 tmpdata = read_encoder(); 
 if( tmpdata ) { 
   Serial.print("Counter value: "); 
   Serial.println(counter, DEC); 
   counter += tmpdata; 
 } 
} 

/* returns change in encoder state (-1,0,1) */ 
int8_t read_encoder() 
{ 
 static int8_t enc_states[] = {0,-1,1,0,1,0,0,-1,-1,0,0,1,0,1,-1,0}; 
 static uint8_t old_AB = 0; 
 /**/ 
 old_AB <<= 2;                   //remember previous state 
 old_AB |= ( ENC_PORT & 0x03 );  //add current state 
 return ( enc_states[( old_AB & 0x0f )]); 
} 

Comments

Similar projects you might like

Magic Lamp

by Nekhil ravi

  • 1,151 views
  • 3 comments
  • 12 respects

PuzzleBox

Project tutorial by Arduino

  • 424 views
  • 0 comments
  • 2 respects

Arduino MKR GSM 1400 and DTMF

by Arduino_Genuino

  • 4,218 views
  • 0 comments
  • 9 respects

Love You Pillow

Project tutorial by Arduino

  • 2,826 views
  • 0 comments
  • 5 respects

Arduino Yun Controller

Project showcase by TATCO Inc

  • 235 views
  • 0 comments
  • 3 respects
Add projectSign up / Login