Tutorial Proposal: Using Physical Inputs (Buttons, Encoders, Joysticks, Switches, Potentiometers)

In most projects, the goal is usually to 'get my idea working'™ rather than messing around in the weeds with low level stuff. This is where libraries like Thomas Fredericks' Bounce2 and Paul Stoffregen's Encoder have helped (me) enormously over the years. People far cleverer than I have written code that does its job very reliably.

Working with physical inputs involves 'listening' to one or more pins and acting on changes to the state of those. For inputs like buttons and encoders, state changes over a time also play a role, so things can get quite messy, quite quickly. If you have multiple inputs, that can soon become Spaghetti code.

This tutorial will show how using libraries (well, a library) can free you to concentrate on the code that matters to you. This library is called InputEvents and is an evolution of a few libraries I wrote to get me out of the corners I coded myself into when using Bounce2 and Encoder on their own. If you are new to programming, it will also introduce the Arduino-style 'event programming' concept.

As noted in the comments below, there are many ways (and other libraries) that can be used to read physical inputs but using event is probably the cleanest way to deal with input from users & pins. The InputEvents library provides a consistent approach to all the different input types it supports.

While your primary project goal is to get your idea working, the overriding aim of good coding should be maintainability. You want to be able to add features to your code without breaking existing features. You want to be able to find and fix bugs without introducing new ones (aka regressions). To this end, we will follow the Unix mantra: "Do one thing but do it well".

This means extracting everything possible from your loop() function and putting it into one or (likely) more functions that each do one thing well. The functions are then called by loop() or by another function. In the Arduino world, separating things into functions was often less common because of CPU/memory restrictions on early microcontrollers but as we now usually have much more available RAM and flash storage, we have the opportunity to create more maintainable code for our projects.

Note: Throughout this tutorial, you can substitute 'function' for 'class method' but that is a whole other subject. 'method' is just the name for a 'function' within a class.

Here is a really basic example of extracting code from loop() into a function:

void doMyThing() {
    Serial.println("Doing my thing");
}

void loop() {
    doMyThing();
}

Admittedly, it is a little silly but if you have a bug when 'Doing my thing', you know exactly where to look and any changes you make will only affect that functionality.

Now imagine you have a few physical inputs on your board. We want to 'encapsulate' what functionality each input performs in a function. Nice. Clean. Code.

Here is a function that might do some things if a button 'event' occurs (an event could be 'pressed', 'released', 'clicked' and a few more).

void onButtonEvent(InputEventType et, EventButton& eb) {
    // The logic/code for your button events goes here
    Serial.println("A button event happened");
}

The function now takes two parameters (which together with its return value void, defines the function 'signature'). The first parameter named et is the event type and the second ebis a reference to the EventButton that is 'connected' to the pin. From the et parameter we can decide what to do and from the eb parameter we can read the state of the button - such as how many clicks were counted. I have used the name eb here but it is a reference to (ie the same as) the myButton created below.

Note: the term 'parameter' and 'argument' are often used interchangeably - a parameter is part of what defines a function signature, an argument is what is passed to to a function parameter. See Difference Between Parameters and Arguments - GeeksforGeeks for clarification.

The function name and parameter names can be anything you like (descriptive is best) so long as the function has the correct signature. The prefix on... is just a convention. Often the suffix ...Handler is used instead, ie myButtonEventHandler. et and eb are not great examples of descriptive naming but it is generally accepted, the smaller the scope, the smaller the name can be!

We need to 'connect' the EventButton to a pin, so we must create an 'instance' of EventButton:

EventButton myButton(4);

The constructor's parameter receives the number of the pin your button is physically attached to (in this example, 4). A constructor is just a special method (a class 'function' with the same name as the class) that is 'called' when an instance of that class is created.

In our setup() function we must initialise the button wit begin() and tell the button to use our onButtonEvent() function:

void setup() {
    myButton.begin();
    myButton.setCallback(onButtonEvent);

and just like magic, your onButtonEvent function has become a 'callback' or a 'handler'. These are just terms to describe a function that is (usually) called when an event happens.

In larger systems (PCs, servers etc) that event is often 'fired' from a separate process or thread but most microcontrollers only have one process/thread so we fake it in our loop() function:

void loop() {
    myButton.update();
}

The EventButton's update() method will check the state of the button pin and decide if it is appropriate to 'fire' an event. If it is, then your onButtonEvent function will be called (back) and the code within your function will be executed.

With a function and four lines of code, you no longer have to worry about what the details of reading inputs, freeing you to concentrate on what you want to do if they are pressed, turned, moved etc.

This is a core tenet of good programming - separating concerns.

An example sketch for toggling an LED on an Arduino is here: ButtonToggle and can be seen in action on Wokwi here.

The EventInputs library can be used with Buttons, Encoders, Encoder Buttons (my favourite :slight_smile:), Potentiometers (Analog inputs), Joysticks and Switches. The inputs don't have to be physical but that is the primary expected use case. It has been tested on all the Arduinos I own, plus ESP8266, ESP32 and Teensy 4.1.

These actually are not interchangeable, they each mean something.

Use the one that is appropriate in the context.

a7

Nice to have more choice :+1:

You should not call any function that accesses hardware (e.g. pinMode) in a constructor. The constructor is called before the hardware is initialised and the init functions can undo the configuration.

It's OK (but bad practice for above reason) on AVR based boards; there is at least one board where your approach will fail (but I can't remember which one).

Add a begin() function to your classes to prevent that problem.

2 Likes

Thank you @sterretje. I have become aware of the issue accessing hardware in the constructor (PJRCs Encoder has issues with ESP32 for the very same reason). So far the only input that has been affected is the joystick/analog but only if the input is declared at a global level (like all examples!) but those inputs also have logic applied before the first update(). I may revisit and add begin() if it becomes essential.

See e.g. pinMode() in class constructor? seem not to work - #23 by oqibidipo.

1 Like

Hi @alto777, thank you, I'm aware of the difference but they are frequently used interchangeably, especially in the Arduino world. I'll clarify and add a link to an external source.

Edit: updated the tutorial.

Ordinarily I wouldn't bother, but a tutorial is an opportunity to be correct at all times. In this case to perhaps bend the arc towards ppl being less sloppy.

Something about the way you originally worded it made me think you were cheerfully suggesting that using the terms interchangeably is fine, just another ship that has sailed as words lose there their meaning in this age of computers.

is an argument.

If we need to blame anyone, it would be my Moms, who I remember alla time I read books she owned and see her using more than her share of red pencil lead.

a7

Oh, I'm with you all the way. Probably why the tutorial veered slightly towards 'best' practice.

Mmmm - I looked at that and thought: "Am I referring to the function's parameter or am I referring to the const value '4'?" I chose the former...

I'll update :slight_smile:
Edit: Nearly re-wrote as "to which your button is physically attached" out of fear of the red pencil lead, but I just couldn't.

1 Like

That depends on the code you write. I have been demonstrating an alternative here for 10+ years but in general only some members get past the first part, making the code non-blocking, aka "millis code".

Once your functions in void loop() do not block, ie hog cycles, and interleaves execution with other non-blocking functions, you can run tasks that don't need deep indenting to work by sharing data, and triggers/flags through global variables. I know this because I've been doing it since the 80's.

Can it be overloaded? Sure, anyone can wreck a good thing while not everyone can organize a massive endeavor. My demos aren't big because I don't want to bury one or two basic ideas in a haystack of details, and special conditions are not for general lessons.

Using cooperative tasking allows keeping a toolbox of functions that can be used as is or modified where necessary. Pre-Covid I had started such a toolbox with Input functions for differing hardware and protocols but to be honest, I would need to keep a private site like Nick Gammon did to not have tutorials/examples get buried in posts where few if any will see them. Go look at the Introductory Tutorials section of Using Arduino on this forum for the evidence.

So good luck and remember that spaghetti code arises from a writing style, it is not inevitable.

@GoForSmoke, thank you for you comments - I think we have very a similar approach. When I look at the first link in you profile, the blink example is exactly the kind of 'method extraction' I mean. The two functions toggleGreenLED() and toggleRedLED() are called, with a little liming logic, from loop(). If the contents of those two functions were embedded within loop() the code would be more difficult to understand, debug and maintain.

I'm not quite as 'purist' as Robert C Martin on the size of functions, but really inspired by the *nix approach of small 'units' doing one thing well - perhaps an allegory to your 'toolbox of functions'. For me, that toolbox is a curated set of good quality libraries.

I come from a background of terrabyte databases, so C/C++ on microcontrollers is neither my first language nor my natural habitat, but over the years have had far too much exposure to near unmaintainable code that grew unwieldy most likely because it started with incredibly poor writing style. Despite all the highly trained professional programmers, it seems to be extraordinarily common across all industries and sectors.

This from @alto777, I think is a noble ambition:

Maybe not at all times, but certainly to strive for better.

The traffic on the forum is quite high, so I might replicate the tutorial within the library docs if it gets buried (or not approved).

You would write them as functions and the compiler would inline them.

With non-blocking compatibility we have the ability to debug each piece in a smaller sketch, assemble the parts and do debug on coordination as needed.

I dunno about you but I've run across mediocre coders with master's degrees and some PhD's who would have problems with such code. It doesn't make the code less. They can plow through a lot but don't do anything "new" like get out of modal.

Arduino at the low end is small but cheap. AVR's have a pretty wide niche, AMD's make them look like pushcarts.

You might seek to explore what's available and set up more than one program.

This example shows the difference between blocking and non-blocking code with the movement of one jumper. Let me know if it's not easy to understand. I put 2 or 3 hours into it and tried to keep it at experienced-beginner level

// DualActionDelayMillis v1.1 by GoForSmoke 11/18/24 -- made for Uno R3
// expect: ground pin 7 to run delay mode, not grounded runs millis mode
// expect: enter key in Serial Monitor to pause action, unpause action

// note that Arduino millis is +/-1 and that printing takes time as well!

const byte blinkPin = 13; // Uno board LED pin13

const byte jumperPin = 7; // a jumper from pin 7 to GND or pin 7 unterminated

byte jumperStateNow, jumperStatePrev; // to compare what is to what was
const word debounce = 20; // ms delay while the contacts settle, dirty signal

byte blinkState;  // led13 0=OFF, not-0=ON

const unsigned long interval1 = 3000;
unsigned long start1;
byte started1 = 0;
const unsigned long interval2 = 700;
unsigned long start2;
byte started2 = 0;

void usage()
{
  Serial.println( F( "\n    Dual Action Delay Millis \n" ));
  Serial.println( F( "    ground pin 7 to run delay mode, not grounded runs millis mode" ));
  Serial.println( F( "    Send enter key in Serial Monitor to pause action, unpause action \n\n" ));
}

void setup()
{
  Serial.begin( 115200 ); // run serial fast to clear the output buffer fast
  // set Serial Monitor to match

  pinMode( blinkPin, OUTPUT ); // LOW by default
  // blinkState is 0 by default
  pinMode( jumperPin, INPUT_PULLUP );
  jumperStateNow = jumperStatePrev = digitalRead( jumperPin );
}

void loop()
{
  //  ============================ change mode with jumper =======
  jumperStateNow = digitalRead( jumperPin );    // check for mode change
  if ( jumperStateNow != jumperStatePrev )      // if jumperPin changes state, stop and debounce then re-init
  {
    while ( jumperStateNow != jumperStatePrev )
    {
      jumperStatePrev = jumperStateNow;
      delay( debounce );
      jumperStateNow = digitalRead( jumperPin );
    } // finished debounce

    started1 = started2 = 0; // init for millis mode
  }
  //  ============================ end change mode with jumper =======

  //  ============================ millis mode ===================
  if ( jumperStateNow > 0 ) // run millis mode
  {
    if ( started1 == 0 )
    {
      started1 = 1;
      Serial.print( F( "Millis Wait " ));
      Serial.print( interval1 );
      Serial.print( F( " time " ));
      Serial.println( millis());
      start1 = millis();
    }

    if ( started2 == 0 )
    {
      started2 = 1;
      Serial.print( F( "Millis Wait " ));
      Serial.print( interval2 );
      Serial.print( F( " time " ));
      Serial.println( millis());
      start2 = millis();
    }

    if ( millis() - start1 >= interval1 )
    {
      started1 = 0;
      Serial.print( F( "Finished Millis Wait " ));
      Serial.print( interval1 );
      Serial.print( F( " time " ));
      Serial.println( millis());
    }

    if ( millis() - start2 >= interval2 )
    {
      started2 = 0;
      Serial.print( F( "Finished Millis Wait " ));
      Serial.print( interval2 );
      Serial.print( F( " time " ));
      Serial.println( millis());
    }
  }
  //  ============================ end millis mode ===================

  //  ============================ delay mode ===================
  else // run delay mode
  {
    Serial.print( F( "Delay " ));
    Serial.print( interval1 );
    Serial.print( F( " time " ));
    Serial.println( millis());
    delay( interval1 );
    Serial.print( F( "Finished Delay " ));
    Serial.print( interval1 );
    Serial.print( F( " time " ));
    Serial.println( millis());

    Serial.print( F( "Delay " ));
    Serial.print( interval2 );
    Serial.print( F( " time " ));
    Serial.println( millis());
    delay( interval2 );
    Serial.print( F( "Finished Delay " ));
    Serial.print( interval2 );
    Serial.print( F( " time " ));
    Serial.println( millis());
  }
  //  ============================ end delay mode ===================

  //  ============================ pause from serial monitor=====
  if ( Serial.available())  // enter serial to pause
  {
    usage();

    while ( Serial.available())
    {
      Serial.read(); // empty the buffer
    }

    while ( !Serial.available());  // wait for unpause

    while ( Serial.available())
    {
      Serial.read(); // empty the buffer
    }

    started1 = started2 = 0; // re-init millis mode
  }
  //  ============================ end pause from serial monitor=====
}

My preferred switch handling uses 1 byte to maintain a state history, 1 bit per read, read when millis changes (every 1024 micros). The state history provides up to 8 bits of debounce but as few as 4 bits can be used, so

if ( history = 0b10000000 ) { button press and debounce detected }

or

if ( (history & 0b1111 ) = 0b1000 ) { button press and debounce detected }

are up to the function reading the history byte that's updated 960x/sec by an independent task that uses very few cycles. My view is that inputs should always be checked and the IPO code running them should be asynchronous.

Thank you @GoForSmoke for your reply and putting in the time to write the sketch.

I may be wrong (surely not!), but I believe the majority of people who come to Arduino do so because they want to do something with a project. With (often) no programming experience, having to learn the details of how to 'properly' write code for a microcontroller is overwhelming - show them bitwise operations and watch their eyes glaze over!

There are, of course, those who want to learn the nitty gritty and that is one of the great things about the Arduino world. Your sketch is educational for both demographics but more likely to be read and understood by this one..

My target demographic is firmly the former but with caviats:

I do think new users should always be guided towards understanding that, in the absence of multi-cores and (hyper) threading, MCU code is always blocking. We 'fake' async by using state machines but the reality is, every line of code is blocking the next. Even interrupts pause the main process.

The trick is to 'get in' and 'get out' as fast as possible. This is where extracting code into functions comes in handy because it is easier to explain to new users the 'get in, set a state, get out fast' concept.

Unfortunately the default Arduino 'Blink' example does no one any favours - IMVHO it should either be retired or have a huge disclaimer in the header and a pointer towards a non-blocking example.

For new/non programmers we need to use non-programming language to introduce programming concepts that are crucial to making Arduino sketches perform as expected.

'Blocking' is easy to understand because it relates to the physical world (if you're in a queue, you're blocking the person behind you). Asynchronous, threading, state machines all have allegories in the real world but they do not translate so easily. 'Every line of code blocks the next line' is easy to understand.

If I were asked by a new Arduino user how to read a pin with a switch/button attached, I would first point them to the Bounce2 library & examples (well, maybe my library too). Why? Because their goal is not to understand how the pins and debounce work but to get their project up and running. Of course, being me, I'd also have to try to gently introduce good writing practice and 'non-blocking' concepts... and maybe a link to the Bounce2 source code. :wink:

1 Like

Hi @stutchbury ,

first of all: Thanks for taking the effort to write a tutorial!

One of the most relevant parts is to define the intended audience and - based on that - find the appropriate way to lead them from known to the unknown, and from simple to complex.

Knowledge and experience of the forum members are as widely spread as between pupils in elementary school and IT professors at the university (one may argue about the latter :wink: )

The challenge is that any tutorial for beginners or slightly experienced beginners usually lacks accuracy and depth to a certain extent. They must because otherwise the step from ignorance to knowledge would overwhelm people.

As in school, the most promising way seems to be to start with small examples (as you have done!) but - unlike many existing tutorials and examples - add a section on the disadvantages of the example and a link to the next higher level solution. This way, the learner can either be satisfied with what he/she has learnt or take the next step forward.

A nicer solution for tutorials would be kind of a Wiki page where everybody can commit his/her contributions rather than to put the load on a single person. That would also allow to keep tutorials updated. Don't know if this was already discarded for administration reasons or the like...

BTW: It looks as forum members are more open to accept the online Simulator Wokwi as years before. It would - only as a supplement of course - allow to add "running" demos to the examples.

Anyway, good luck on your way!
ec2021

1 Like

Blocking is holding up execution long enough to matter.

Arduino Tutorial Examples does have Blink but also has Blink Without Delay which beginners encounter later.

That example... check the date at the top. I've been posting examples since late 2011 which is how long I've been learning what is and isn't suitable for beginners, which is why I don't use my best debounce in examples for beginners but rather something like what the one I posted above does.

But for a library or toolbox aimed at regular use, history bits would get used where they best fit! Hell, I have yet to go through the SD and SPI libs but I have used them plenty!

There's a lot to consider when making Tutorials.

AFAIC, the best ones on Arduino are here:
Nick Gammon's site section on microelectronics.

Nick was a member here long before I arrived and he has a way of explaining things very well and completely.

The subject of Inputs is very wide. How generalized can it get when Input can be binary pin state to analog value to serial content just for some examples? It can run through loads of very long threads on this forum! Just the number of ways to implement a button... have you messed with diy capacitive touch spots? Or piezos as sensors? Making better buttons is like making better mousetraps. You have tapped a wide subject indeed.

Thank you for your encouragement @ec2021. My target audience is those who do not yet use, or know about using libraries to make their application code more robust and maintainable. I hope I have written it in a way that is understandable to people who are new to coding but I will add in a note about blocking in event handlers.

I can't link to the next 'level' because I haven't written it yet! I'm also wary of having 'too much' in a single tutorial. Getting the right balance is a challenge.

10 years ago, I suspect Arduino was predominately a place where people interested in coding started their journey (that has been my experience) but now it is very much a tool for makers to make their projects do stuff. Quite a different demographic and learning desires. We also have a much larger variety of microcontrollers, most of which are no longer constrained by RAM or flash capacity.

The use of the forum for tutorials is certainly less than ideal. It would be great to see a formal/official curated 'getting started' series that provides streams to those who just want to code their application (my target demographic) and those who want learn to code 'nearer to the metal'.

Both options must start with clear guidance on how to get your code to behave as expected (blocking vs state machine), structuring code so problem parts can be extracted and debugged/posted to the forum and the concept of 'standing on the shoulders of giants' - ie effectively using trusted libraries.

Ideally, either Wokwi or something similar would be the core of the forum - almost every question has sample code... I might even add a link from the tutorial!

Edit: Hehe. I couldn't resist - added a link to Wokwi...

1 Like

4 posts were split to a new topic: "Spaghetti code" vs. wokeness

I dislike the automatic use of a Bounce library when ever you have an input to deal with. I never use a library but when contact bounce is a problem I will use measures to stop it.

However, in truth these are only some situations where it causes a problem and in most cases it is not a problem. The over use of a bounce function as a knee jerk reaction to any mechanical input teaches beginners bad habits right from the off.

I think it is important to teach people when they need to debounce function rather than every time they need an input. Once the code becomes sufficiently sophisticated the need for debounce will disappear.

1 Like

I don't see your entry post as "Tutorial Using Physical Inputs".
For me it reads like the introduction of callback functions and the given example "ButtonToggle" could be achieved very similar with several other "button libraries" (for example OneButton).
It is ok to promote your library, but for me It is not a tutorial to the topic "Using Physical Inputs".

I am not advocating its automatic use but its pragmatic use by people new to Arduino and programming who are unlikely to have the equipment to measure bounce. I would contend not using bounce compensation will likely cause more issues for new programmers than using it. "Why am I incrementing by more than one?"

Teaching new users when bounce compensation is required - absolutely. But teaching them how to use a (trusted) library to overcome their issue is a bad habit? I respectfully disagree (see below).

The first sentence - 100% but for the vast majority of people now using Arduino, their code is never going to become 'sufficiently sophisticated' - and that is absolutely fine. For these new users of Arduino, it is a tool to get the job done, not a vehicle to learn how to program.

It is a turorial on using physical inputs - I make no claim it is the only way to use them. Perhaps I should clarify that at the beginning.

It is that too. And yes, absolutely, all of the above could be achieved with alternative libraries or, as previously commented, scratch writing code.

That's good to hear and yes, I am promoting my library but as part of a bigger picture. This is my motivation:

On almost every IRL visit to the Maker Spaces in my area, questions on managing physical inputs arise. This is likely because my 'show and tell' mostly have physical inputs that have to work reliably (eg the Manualmatic Pendant). I created a series of input libraries available in the Library Manager but each input type required a different library. The InputEvents library brings all those libraries together with a (hopefully) consistent API and a 'simpler' callback.

My goal for these IRL interactions (and the tutorial) is not to 'teach programming' per se, but to provide pragmatic solutions to problems. Of course that often involves going back to basics of maintainable code structure, state machines etc, etc. But at the end of the day/meeting, I want the person to still be enthusiastic about both their project and their level of programming ability.

In social situations I often describe myself as a 'Creative Writer (in multiple languages)' because when describing myself as a 'programmer' the conversation usually falters. The same is true when supporting people who are using Arduino as a tool - the application programmers. Avoiding eyes glazing over is paramount in both situations - if they desire, new users can learn bitwise etc later but only if we keep them on board.

Writing and documenting (especially documenting...!) a library takes a fair bit of time and effort but I am happy with the return on my investment. If it helps people who are new to programming achieve their project goals, I'm counting that as a win. If it draws them further into the programming world, so much the better but that goal is secondary.

I have huge respect for all the hours, days and nights many of the previous commenters have contributed to this post and the forum (yes, I did check out all your profiles :wink:) so thank you all for your time. If my pragmatic approach is not deemed suitable or appropriate for this forum, I am quite happy to bow out gracefully.

@pert, thank you for removing the off topic.

1 Like

And what "saves" coders from learning that "saves" them from advancing themselves. Crutches and wheelchairs for the mind. Don't even try to get up let alone dance.