Vehicle accessories – Rotary encoder illuminated button

Having a rotary encoder with RGB illuminated push button, gives a nice feature to the project. The speed of the car can be controlled, and a start/stop state can be set and viewed with the illuminated push button.

Autonomous vehicles, HC-SR04 sensors

Step 1: Detecting objects with HC-SR04
Step 2: Multiple HC-SR04 with Arduino using interrupts
Step 2b: Multiple HC-SR04 with Arduino using only 1 interrupt pin
Step 3: Choosing the right vehicle
Step 4: Automatic steer correction with HC-SR04
Step 5a: Vehicle accessories – SSD1306 status display
Step 5b: Vehicle accessories – Rotary encoder illuminated button
Step 6: Autonomous vehicles, putting it all together
Alternative 1: Steer correction with HC-SR04: Single sensor, stop and scan
Alternative 2: Steer correction with HC-SR04: Servo controlled car

Rotary encoder with RGB illuminated button

The rotary encoder with RGB illuminated push button, can be purchased as a complete set, or a separate switch, breakout board and optionally transparant knob.

Having a rotary encoder with RGB illuminated push button, gives a nice feature to the project.

  • Turning clockwise (CW) or counter clockwise (CCW) can be used to increase or decrease the speed of the car
  • The push button can serve as a start/stop mode
  • The RGB led can be used as feedback of the robot state

Complete set

Since the rotary encoder switch is not breadboard friendly, a breakout board is a good solution. The pre-soldered version can be bought from Sparkfun, but it uses I2C (or optionally a Qwiic Twist connector), and is a bit more expensive. It will not be covered here, but you can find it at the Sparkfun website here.

Self assembled

Sparkfun also provides loose switches and breakout boards, and are available in 2 versions, so make sure you buy the right ones

  • Rotary encoder with Red/Green (RG) LED illuminated button
    Find it on the Sparkfun website here
  • Rotary encoder with Red/Green/Blue (RGB) LED illuminated button
    Find it on the Sparkfun website here

For the breakout board, there is only one version. The top side is for the RGB version. Flip the board to the bottom side for the RG version.
The breakout board can be found here.

Encoder switch with breakout boards
Encoder switch with breakout boards

Optionally, a transparant knob can be added to the button.
It can also be found on the Sparkfun website here.

Connecting the encoder button

If all functions of the encoder button are used, 6 pins are needed, and off-course the GND and 5V pins will be needed as well.

  • The encoder will be read using 1 interrupt pin, and 1 regular pin
  • The push button will be read on a regular pin
  • The RGB leds will be connected to regular pins (no PWM control)

Having one signal (A) of the encoder connected to an interrupt pin, allows for simple and accurate read (no missing triggers). The second signal (B) will just be used to determine the rotation direction. The common pin (pin C) will be connected to ground.

The push button has a common connection with the anode side of the LEDs. Therefore a pull-down resistor (1K – 10K) needs to be used in order to have a stable input value.

Rotary encoder switch schematic
Rotary encoder switch schematic

Download the schematic for connecting the rotary encoder to an Arduino Uno below

Coding the encoder button

For the autonomous vehicle, all functions will be used

  • Turning clockwise (CW) or counter clockwise (CCW) will increase or decrease the speed of the car
  • The push button will set the car in auto or stopped mode
  • The RGB led will give feedback on the state of the car

Push button

The push button will be used with the previous written class “class_button.h”. This class will take care of de-bouncing the button, and detecting rising and falling triggers.

Encoder

Since this rotary encoder can detect the rotation direction, it will be used on an interrupt pin watching for a changing signal (signal A). By looking at signal B, the rotation direction can be determined, and a counter is increased or decreased by 1.

Timing diagram CW vs CCW
Timing diagram CW vs CCW

RGB LEDs

In this project, the PWM signals are used to control the DC motors, so not all LEDs will be able to be controlled by PWM signals to set the intensity. Since in this project only red and green are interesting (add other colours to your own liking) and the intensity is not important (unless you want different), all LEDs will be controlled by regular pins.

/****************************************************************
      To be used as a local class
****************************************************************/
class BUTTON
{
  private:
    unsigned long storeMillis = millis();
    unsigned long tmrTON = millis();
    unsigned long tmrTOF = millis();
    bool REpassed = false;  // Remember Rising Edge
    bool FEpassed = false;  // Remember Falling Edge

  public:
    bool state = false;     // State of the button to check
    int TON_delay = 1000;   // Debounce/Delay time for ON/Rising Trigger detection
    int TOF_delay = 1000;   // Debounce/Delay time for OFF/Falling Trigger detection
    bool Q = false;         // Debounce/Delay time passed and button still high
    bool RE = false;        // Rising Edge detected (remains high for only 1 cycle)
    int FE = false;         // Falling Edge detected (remains high for only 1 cycle)

    // Call this every cycle
    void refresh()
    {
      // First check the timers
      if (!this->state)
      {  this->tmrTON = millis(); }
      else  // (this->state)
      {  this->tmrTOF = millis(); }

      // How long is the button PRESSED ?
      if (this->state && (this->tmrTON + this->TON_delay < millis()))
      {
        // Button was continuously pressed for TON_delay time
        this->Q = true;           // Set Q to HIGH state
        this->FE = false;         // Falling Edge low (small change it's still high)
        this->FEpassed = false;   // Falling Edge detection help var low

        // Rising Edge detected ?
        if(!this->REpassed)
        {
          this->RE = true;        // Not yet -> Set Rising Edge detected HIGH
          this->REpassed = true;  // Help var for Rising Edge
        }
        else
        {
          this->RE = false;       // Already detected -> Set Rising Edge detected LOW
        }
      }

      // How long is the button RELEASED ?
      if (!this->state && (this->tmrTOF + this->TOF_delay < millis()))
      {
        // Button was continuously released for TOF_delay time
        this->Q = false;          // Set Q to LOW state
        this->RE = false;         // Rising Edge low (small change it's still high)
        this->REpassed = false;   // Rising Edge detection help var low

        // Falling Edge detected ?
        if(!this->FEpassed)
        {
          this->FE = true;        // Not yet -> Set Falling Edge detected HIGH
          this->FEpassed = true;  // Help var for Falling Edge
        }
        else
        {
          this->FE = false;       // Already detected -> Set Falling Edge detected LOW
        }
      }
    };
};
// Include button class
#include "class_button.h";
BUTTON btnShortPress;
BUTTON btnLongPress;

/****************************************************************
      Configuration
****************************************************************/
// Encoder
#define pinEncA 3          // Encoder A signal (must be interrupt pin)
#define pinEncB 4          // Encoder B signal (any pin is fine)
#define pinEncButton 7     // Encoder push button
#define pinEncLED_R 8      // Encoder RGB LED, Red
#define pinEncLED_G 9      // Encoder RGB LED, Green
#define pinEncLED_B 12     // Encoder RGB LED, Blue

// Sensor delay
#define debugDelay 200     // Debug output every x ms
/*****   END OF CONFIGUTATION    *****/

volatile int encoderCounter;     // Encoder counter value
byte byteCounter = 0;

// Poll timers
unsigned long lastDebugMillis;

/****************************************************************
      SETUP
****************************************************************/
void setup()
{
  Serial.begin(115200);
  while (!Serial) {
    ; // Wait for Serial
  }
  Serial.println("--- Serial monitor started ---");

  // Set pins as input
  pinMode(pinEncA, INPUT_PULLUP);  // Encoder signal A (C is GND !)
  pinMode(pinEncB, INPUT_PULLUP);  // Encoder signal B (C is GND !)
  pinMode(pinEncButton, INPUT);    // Encoder push button (common anode, no pull-up !)

  // Set pins as output
  pinMode(pinEncLED_R, OUTPUT);  // Encoder RGB led, Red
  pinMode(pinEncLED_G, OUTPUT);  // Encoder RGB led, Green
  pinMode(pinEncLED_B, OUTPUT);  // Encoder RGB led, Blue

  // Set interrupt
  attachInterrupt(1, ISR_ENC, CHANGE);      // Set interrupt call for encoder

  lastDebugMillis = millis();
}

/****************************************************************
      LOOP
****************************************************************/
void loop()
{
  // Switch has common anode with LEDs (pull-down resistor)
  bool buttonState = digitalRead(pinEncButton);  // Read button state
  btnShortPress.state = buttonState;
  btnLongPress.state = buttonState;
  btnShortPress.refresh();             // update button
  btnLongPress.refresh();              // update button

  // For debugging button class en testing timings
  // detectAllEdges();

  // RGB LED control
  controlRGB();

  // Interval call
  if (millis() - lastDebugMillis >= debugDelay)
  {
    showEncoderValue();
    lastDebugMillis = millis();
  }
}

/****************************************************************
      RGB LED handling
****************************************************************/
void controlRGB() {
  /*
      Red: 1.88V    (3V/20mA = 150E)
      Green: 2.98V  (2V/20mA = 100E)
      Blue: 2.98V   (2V/20mA = 100E)
  */

  // Button settings
  btnShortPress.TON_delay = 25;     // de-bounce
  btnShortPress.TOF_delay = 25;     // de-bounce
  btnLongPress.TON_delay = 1000;    // de-bounce + delay
  btnLongPress.TOF_delay = 25;      // de-bounce

  // Long press set green to 50% (leave rest as is)
  // Short press toggles to next color
  if (btnLongPress.FE) {
    Serial.println("This time: LONG press");
    digitalWrite(pinEncLED_R, not(byteCounter >> 2 & 1));
    analogWrite(pinEncLED_G, 127);
    digitalWrite(pinEncLED_B, not(byteCounter >> 0 & 1));
  }
  else if (btnShortPress.FE) {
    byteCounter++;
    Serial.println("This time: SHORT press");
    digitalWrite(pinEncLED_R, not(byteCounter >> 2 & 1));
    digitalWrite(pinEncLED_G, not(byteCounter >> 1 & 1));
    digitalWrite(pinEncLED_B, not(byteCounter >> 0 & 1));
  }
}

/****************************************************************
      Encoder value
****************************************************************/
void showEncoderValue() {
  // Keep encoder values -100..100 (%)
  encoderCounter = constrain(encoderCounter, -100, 100);

  // Show current value
  Serial.print("Encoder value: ");
  Serial.println(encoderCounter);
}

/****************************************************************
      Debug function
****************************************************************/
void detectAllEdges() {
  // Button settings
  btnShortPress.TON_delay = 25;     // de-bounce
  btnShortPress.TOF_delay = 25;     // de-bounce
  btnLongPress.TON_delay = 1000;    // de-bounce + delay
  btnLongPress.TOF_delay = 1000;    // de-bounce + deay

  // Just for the class_button.h DEBUG
  if (btnShortPress.RE) {
    Serial.println("Button Rising Edge Short Press");
  }
  if (btnShortPress.FE) {
    Serial.println("Button Falling Edge Short Press");
  }
  if (btnLongPress.RE) {
    Serial.println("Button Rising Edge Long Press");
  }
  if (btnLongPress.FE) {
    Serial.println("Button Falling Edge Long Press");
  }
}

/****************************************************************
      INTERRUPT handling encoder
****************************************************************/
// Encoder pin
void ISR_ENC()
{
  // static unsigned byte encoder_state = 0;     // Why not working ?
  static unsigned char encoder_state = 0;     // Only exists in this funtion and init once
  encoder_state = encoder_state << 2;         // Shift 2 bit to the left

  //  encoder_state = encoder_state | digitalRead(pinEncA);           // New value A on bit 0
  //  encoder_state = encoder_state | (digitalRead(pinEncB) << 1));   // New value B on bit 1

  // For ATmega328(P) chips, INT1 = pin 3 = PD3 , pin 4 = PD4
  byte pinRead = PIND;
  encoder_state = encoder_state | (PIND >> 3 & 1);    // New value A on bit 0 [xxxxxxxA]
  encoder_state = encoder_state | (PIND >> 3 & 10);   // New value B on bit 1 [xxxxxxBx]

  encoder_state = encoder_state & B00001111;          // Mask with 0x0F
  // Format is now 0000 <prev B> <prev A> <cur B> <cur A>

  // Decide counter direction (comment / uncomment for best stability)
  switch (encoder_state) {
    case B1001: // B high to low ; A low to high
    // case B0110: // B low to high ; A high to low
      encoderCounter++;
      break;
    // case B0011: // A and B low to high
    case B1100: // A and B high to low
      encoderCounter--;
      break;
    default:
      ; // unsure state
      break;
  }
}
//END

Conclusion

Having the option to quickly adjust the speed without having to dive into the code and tweak a few parameters is a nice feature. Being able to visually see the status (started / stopped / something else) of the robot, is a must. With this rotary encoder switch, both can be achieved easily.


References
Arduino Boards: Arduino Uno R3 – Arduino Nano V3 – Arduino Micro
Arduino Software: Arduino IDE
Arduino Playground: Library NewPing
Arduino forum: Arduino Micro Interrupts 6
Arduino Reference: Port RegistersattachInterrupt()interrupt()noInterrrupt()pulseIn()pulseInLong()Servo library
Random Nerd Tutorials: Complete Guide for Ultrasonic Sensor HC-SR04 with Arduin
Circuit Digest: Arduino Interrupts Tutorial
Electro Noobs: Arduino register control, timers and interruptions
Wikipedia: Speed of sound – Echolocation