By dave | November 15, 2018

Using SwitchInput in your sketches

SwitchInput is a button and rotary encoder management library that treats button presses and encoder adjustments as events, similar to web and UI applications. Supporting as many switches and rotary encoders as you need, in a completely event driven way, working along with TaskManagerIO to keep your code clean.

Key points:

  • Buttons using either pull down or pull up logic connected directly or via any supported IO expander
  • De-bouncing of events on all buttons and encoders to significantly reduce duplicate callbacks.
  • Callbacks when a button gets pressed, released or both.
  • Indication of a long press (held state) with configurable support for repeat keys.
  • Rotary encoder needs no additional components in most cases.
  • Works with Arduino, mbed 5/6, ESP on pins, PCF8574, MCP23017 or shift-registers.

You can see SwitchInput in the reference docs.

To setup SwitchInput

To initialise switches during setup we call the initialise or initialiseInterrupt method, indicating if the buttons are on arduino pins, a shift register or i2c. You do this by providing an IoAbstractionRef documented here. The second parameter is true for PULL UP button logic, and false for PULL DOWN button logic.

// inside setup
void setup() {
    // we default to using IO over arduino pins, see above to change to i2c or shift registers.
    switches.initialise(ioUsingArduino(), usePullUpLogic);
}

Or you can initialiseInterrupt if you prefer that each button causes an interrupt that the library monitors. In this case it will register an interrupt for each button with taskManager. As above, second parameter defines if the buttons use PULL_UP or PULL_DOWN logic.

// inside setup
void setup() {
    // we default to using IO over arduino pins, see above to change to i2c or shift registers.
    switches.initialiseInterrupt(ioFrom8574(0x20), usePullUpLogic);
}

The usePullUpLogic parameter refers to if the switch is active low (pull-up) or active high (pull-down). Pull-up is the most common because no external components are needed in most cases. When using a switch the input must always be pulled in one direction, because the input pin is very high impedance and will otherwise float between low and high.

Below the diagram shows all three possibilities:

Pull down & pull up examples

Example wiring of a pull-down & pull-up button to an Arduino

  • PU1 - here we use INPUT_PULLUP as the input mode, it enables the internal pull-up resistor, it holds the input HIGH until the switch is pressed, when it pulls the input LOW.
  • PU2 - here as above, but we provide an external pull up resistor, needed when either the pin is not pull-up capable or for longer wire runs.
  • PD1 - here we use INPUT as the mode, and provide a resistor to GND that holds the input LOW. When the switch is pressed, the input goes HIGH.

To add a button

To add a button there are three possible methods, you can call addSwitch, addSwitchListener or onRelease. Which ever you use, the pin requested will be monitored by switches and appropriate callbacks triggered as events occur. If an interrupt needs to be registered, switches will do it as part of initialisation.

Firstly, lets look at addSwitch, this allows us to add a function to be called back when the user presses the button:

// this function is the callback, name the function as you wish.
void onPressed(uint8_t pin, bool heldDown) {
    // your code for when switch is pressed here.
}

// then in setup to add a button that doesn't repeat
// in this case you get one extra call back when held down
void addSwitch(buttonPin, onPressed);

// or a repeating button simply add the extra parameter
// in this case you are repeatedly called back when held
// invertedLogic allows you to invert the pull-up/down behaviour
void addSwitch(buttonPin, onPressed, repeatInterval, invertedLogic);

You can also register to receive onRelease callbacks, it uses the same listener callback function signature as above. If addSwitch has not previously been called, then the pin is initialised first:

// and if you want to be notified when the button is released..
// the callback is exactly the same signature as for onPressed.
void onRelease(buttonPin, onReleased);

Where:

  • buttonPin : the chosen IO port that the button is connected to.
  • onPressed : a callback function to receive the notification, specify any function without the brackets.
  • repeat: defaults to NO_REPEAT or provide a multiple of twenty milliseconds up to 254 (~5 seconds between events).
  • invertLogic : invert the pull-up/down behaviour just for this pin.

You can also add switches using a listener object instead of a callback. The listener must implement the SwitchListener interface which provides methods for handling key presses and releases.

First we create a class that implements the interface and declare a global instance of it, either globally or by using new:

class MyKeyListener : public SwitchListener {
public:    
    void onPressed(uint8_t /*pin*/, bool held) override {
        // Called on key press
    }

    void onReleased(uint8_t /*pin*/, bool held) override {
        // Called on key release
    }
} keyListener;

Then during setup we add a switch as follows (other parameters as per addSwitch):

switches.addSwitchListener(buttonPin, &keyListener);
switches.addSwitchListener(buttonPin, &keyListener, repeatInterval);

For both callback and listener forms of addSwitch you can also invert the logic by providing a 4th bool parameter that indicates if the logic should be inverted. To use this form you must always pass the repeat interval:

switches.addSwitch(buttonPin, callbackOrListener, repeatInterval, isInverted);

Using a rotary encoder

Rotary encoders are fully supported within switches. They need an interrupt in order to work properly, this is because they must be read extremely quickly after a change in order to get accurate readings. Using this library makes working with such encoders simple. You must ensure that pinA is connected to a pin that supports interrupts. Note that rotary encoders use the same IO device that you configured for switches during initialisation - (we fully test encoders on Arduino/mbed pins, PCF8574 and MCP23017).

The switches library will arrange for the interrupt callbacks internally, so all you need to do is follow the instructions below.

First initialise the encoder within setup()

Before creating any encoder objects, we need to create a callback function:

void encoderCallback(int newValue) {
    // do whatever is needed on encoder change
}

For a rotary encoder

void setupRotaryEncoderWithInterrupt(uint8_t pinA, uint8_t pinB, EncoderCallbackFn callback);

Where:

  • pinA is the A output of the encoder, this must connected to a pin that supports interrupts.
  • pinB is the B output of the encoder
  • callback is called whenever there is a change in the encoder value. The function is declared as void functionName(int change)
Example of wiring a rotary encoder to an Arduino

Example of wiring a rotary encoder to an Arduino

To make an encoder with UP and DOWN buttons instead use the following. This will use three buttons:

void setupUpDownButtonEncoder(
         uint8_t buttonUpPin,
         uint8_t buttonDownPin, 
         EncoderCallbackFn encoderCallback);

Where:

  • buttonUpPin is the pin that the up button is connected to - handled by switches (IoAbstraction)
  • buttonDownPin is the pin that the down button is connected to - handled by switches (IoAbstraction)
  • callback is called whenever there is a change in the encoder value, declared is void functionName(int change)

To change the range of values for either of the above

The first call purely initialises the encoder, we then need to change the range of values to be represented by the encoder and its current value. For example, if you set the value to 1000 then the range is 0 thru 1000. Current value sets the starting point and must be smaller than the maximum value.

    void changeEncoderPrecision(uint16_t maximumValue, uint16_t currentValue);

You can also set the encoder to track direction only by setting the range and value to 0, in this case the callback will receive -1 for down, and 1 for up.

void changeEncoderPrecision(0, 0);
// or from 1.6.5 onwards you can simply call directly on the encoder
switches.getEncoder()->setUserIntention(DIRECTION_ONLY);

Where:

  • precision is the maximum value (0 based) the encoder can represent
  • currentValue is the current position of the encoder.

Orientation for scrolling / editing

For Up/Down keyboard and Analog Joystick encoders you can also invert the direction by changing the intention. This is important because when using joystick based encoders the natural direction differs between scrolling and setting a value. These two modes have no effect on rotary encoders.

switches.getEncoder()->setUserIntention(CHANGE_VALUE);
switches.getEncoder()->setUserIntention(SCROLL_THROUGH_ITEMS);

Advanced usage of rotary encoders

You can also use more than one rotary encoder with switches. There is an array internally that stores all the encoders, and each entry is a slot. The “default slot” is 0, and any functions that don’t take a slot assume 0. Each entry in the array is basically a pointer to a RotaryEncoder.

You can only initialise encoder 0 (first encoder) using setupRotaryEncoderWithInterrupt or other setup functions described above.

To add additional rotary encoders:

HardwareRotaryEncoder* extraEncoder = new HardwareRotaryEncoder(extraPinA, extraPinB, onExtraValueChange);
switches.setRotaryEncoder(slot, extraEncoder);

Where:

  • slot is the slot in the encoder array from 0..3
  • encoder is the encoder class itself to be added.

In order to set the precision of an additional encoder:

switches.changeEncoderPrecision(slot, maximum, current);

And again you can set direction only mode by setting maximum and current to 0.

Where:

  • slot is the slot in the encoder array from 0..3
  • maximum is the largest value that can be represented
  • current is the current value to set.

Limitations when using more than one encoder at once

There are a few limitations with multiple encoders. rotary encoders share the same input device as switches, and every A pin of the encoder must be interrupt driven. This is easily achieved by putting all encoders on an i2c expander (MCP23017 or PCF8574) which is fully supported. If you need more than one expander, or a mix with device pins see MultiIo Secondly, there is a hard limit on the number defined by MAX_ROTARY_ENCODERS that can be changed by altering the file SwitchInput.h should you need more (or less) than 4.

Note that PinA of each encoder must be on an interrupt capable pin, so whichever way you are connecting it each encoder’s pinA must be capable of raising interrupts. In all cases switches will register the required interrupts on you behalf.

Common mistakes to avoid

A couple of common mistakes we’ve seen in the wild that you should avoid:

  • Do not initialise switches more than once, this can cause problems
  • Ensure that you only have one IoAbtractionRef referring to any IO device, they cache some important state.

Go back to the IoAbstraction page

Other pages within this category

comments powered by Disqus

We use cookies to analyse traffic and to personalise content. We also embed Twitter, Youtube and Disqus content on some pages, these companies have their own privacy policies.

Please see our privacy policy should you need more information or wish to adjust your settings.