Doing several things at the same time with functions, objects or interrupts?

First of all, thank you for taking the time to read this. I am a newbie, so apologies (done examples on arduino website, basic C++).

I'm trying to get an idea of what the best way to structure a sketch is, which needs to do several things at the same time. For example, reading an IMU sensor and using it to control the mouse, reading a button for on/off and let's say flashing an LED. I have repetitive injury so this is the project I'm trying to complete, but the principle should be scalable for more complex projects.

I've come across some great resources and my understanding is you can use:
functions (e.g. blink without delay)
objects (cooperative multitasking)
interrupts
something like a RTOS

Where my limits lie is understanding the benefits/limitations of these options and picking which one to focus on for my project. For example, if using functions is sufficient, why do people bother with interrupts and installing a RTOS?

I would appreciate any guidance for what an effective approach should look like.

Thanks again.

It depends, or it may simply be personal preference.

Arduinos are surprisingly fast and so a millis based approach (BWOD) is often enough to give the illusion of doing many things at the same time. In some cases, you can get library assistance to do things asynchronously so that your code isn't delayed by reading a DS18B20 or doing an analogRead.

Sometimes though, you need to be sure to catch every pulse from external hardware and you can't poll fast enough to guarantee that. An encoder on a motor is an example. That's when interrupts come to the fore. There are some minor pitfalls associated with their use, so they're best avoided unless you really need them. Invaluable when you actually do though.

I haven't used cooperative multi-tasking per se, but the example you gave appears to be the same old BWOD with objects. There's no magic and you're still reliant on each piece doing its thing efficiently and letting other objects have some CPU. It's about using OOP to organize your code, which is probably a good thing but in little Arduino programs, beginners don't necessarily want to learn to write classes. It's worth noting that Paul Murray is a professional programmer so an approach that's trivial to him may be baffling to newcomers.

Haven't used an RTOS on an Arduino either - never had the need, but some people like the ability to have a bunch of cooperative threads so that they can organize their code into tidy pieces and let something else organize when they are called. The example you linked actually appears to do preemptive multitasking so that threads get a time slice and you can get multiple things happening automagically. Which is great. Until you have a bug relating to the interaction between threads and are trying to debug. On an Arduino where your primary debugging tool is Serial.print.

I can't see me using an RTOS on an Arduino ever, but some folks really like them. YMMV.

With a ESP32 there is no installing a RTOS because freeRTOS is already the OS.

And an RTOS is not hard to troubleshoot.

zheygrudov:
For example, if using functions is sufficient, why do people bother with interrupts and installing a RTOS?

In my opinion interrupts are not at all appropriate. You would almost certainly get yourself completely tangled up with something that is almost impossible to debug. Just use interrupts when it is essential to capture a very brief event or where the required timing is too fast for millis() or micros().

An operating system will use up a considerable amount of CPU cycles but if you have the memory and computing power then it can be very helpful by looking after a lot of housekeeping and protecting you from some foolishnesses.

With the limited memory and processor speed of an Uno or Mega I reckon an RTOS would be a complete waste of resources and you would get better overall performance just by using the techniques in Several Things at a Time

...R

wildbill:
I haven't used cooperative multi-tasking per se, but the example you gave appears to be the same old BWOD with objects. There's no magic and you're still reliant on each piece doing its thing efficiently and letting other objects have some CPU.

Haven't used an RTOS on an Arduino either ...

I can't see me using an RTOS on an Arduino ever, but some folks really like them. YMMV.

Thanks for the long answer. Thank you as well Robin. Sorry if this is a bit basic, I just got confused reading about all the options.

The consensus seems to be RTOS would be an overkill. You both advise to only use interrupts for very brief actions. What about reading a sensor and using a button to turn something (e.g. the LED) on or off. Would the press of the button qualify as a very brief action, or would it still be reasonable to use millis() based approach?

Also, if I understood you correctly whether I encapsulate a piece of code into a function or object would make no difference from a multitasking perspective? So if I'm not using inheritance there's no benefit of using objects?

It's difficult to give a definitive answer without details of a particular application, what works for one may be inappropriate for another.

As to buttons, if it's a human pressing it, you probably don't need an interrupt. You should be able to arrange your code so that you poll often enough that they won't notice a slight (milliseconds) delay. But sometimes that won't be true and you have something that you're talking to that takes a while and you want to know right now that Bob pressed the 'reject this one' button.

I would avoid an RTOS on a micro controller like the plague. But as IdahoWalker says, you're getting one whether you like it or not on an ESP32. And you've got two cores there I think, so not having a thread for each would be a waste. And if you're going two, why not three, if you know how to debug it.

Objects have benefits. Lots of them. There is no need to save them for an inheritance situation. "Prefer composition over inheritance" after all. But they demand more expertise from the programmer and many Arduino users don't want to go that far - they just want to get their project working and for them, the simpler the better.

If you are a beginner another way to think about interrupts is only use them if its not possible to do the thing any other way.

For example I use an interrupt to detect one pulse per revolution from a sensor on a small DC motor operating at 16,000 RPM.

...R

Thank you for taking the time again wildbill and Robin. I appreciate it’s a vague question, but this is very useful guidance. So I’ll start with the approach described by Robin/in BWD and only resorts to interrupts if something is not working as quickly as it should.

Bill, I will take your advice to avoid the RTOS as I’m not experienced enough to be able to properly use it.

Thanks for the pointers on objects as well. I think I have a good enough grasp of them to give it a go and see where it ends up.

Robin, sorry for being a pain but in your example why is the arduino not quick enough to sample the sensor normally? If it’s a 8 MHz/16 MHz shouldn’t it be fast enough? Just trying to understand the principle in a “learn to fish, rather than catch a single fish” kind of way.

16,000 rpm is 960,000 pulses per second. That's a rising edge every 16 clock cycles at 16MHz. I am surprised that even using interrupts would be fast enough! You might be able to get one of the atmega's hardware timers to count the pulses by using them as an external clock signal...

PaulRB:
16,000 rpm is 960,000 pulses per second.

Back to school I think. :slight_smile: :slight_smile:

...R

Robin2:
Back to school I think. :slight_smile: :slight_smile:

...R

Totally. I must say that was rather unexpected. Thanks both.

At 16K RPM, the Arduino could easily count pulses by polling, there's enough time as you observe. But if there's other stuff going on, especially if it takes a little while, you may miss pulses. If that matters (and it probably does), better to use interrupts, even though you don't actually positively have to on initial inspection.

PaulRB:
16,000 rpm is 960,000 pulses per second.

Oops! Brain fart. Sorry everyone...

It's 266 pulses per second, of course.

wildbill:
At 16K RPM, the Arduino could easily count pulses by polling, there’s enough time as you observe. But if there’s other stuff going on, especially if it takes a little while, you may miss pulses. If that matters (and it probably does), better to use interrupts, even though you don’t actually positively have to on initial inspection.

Thanks for the explanation. Curious that this is not the issue for polling sensors like IMUs. I suppose you don’t need that level of speed with them.

Hopefully you lovely forum goers are not getting tired of my questions yet.

Going through resources such as several things at the same time and with some greatly appreciated assistance on here I'm finally getting a grasp on how to multitask a microcontroller.

Most examples use the millis() approach to nonblocking programming. However, even in the link above the button state is being polled every loop (from what I understood).

So what confuses me is: is it bad practice to write functions, which poll sensor(s) and input(s) without timing constraints, i.e. at every iteration of loop?

Is this still consider nonblocking programming?

I've written some mock code to better illustrate what I mean. Thank you very much for reading through.

void loop() {
    Button();
    Sensor();
    LED();
    // ...
}

void Button() {
  // Read button(s)
  button_state = digitalRead(button_pin);
  if (button_state == LOW) {
    // ...
  };
};

void Sensor() {
  // Read sensor
  int sensor_state = analogRead(sensor_pin);
  if (sensor_state >= 150) {
    // ...
  };
};

Although there are a lot of variables at play when determining how fast loop() executes, it’s safe to say that in a well designed program it is in the realm of microseconds.

For human inputs like switches/buttons, there’s no practical reason to read the switch every few microseconds. Ditto for something like an HMI LED (i.e. an LED used to inform humans of something); no reason to deal with these every few uS. In fact, doing so may complicate matters like debouncing a switch.

You can call a function to read a switch every loop but it doesn’t follow that the switch logic actually runs every time. You can use millis() timing so that you actually read the switch once every, say, 50mS:

void Button() 
{
    static uint32_t
        timeReadButton;
    uint32_t
        timeNow = millis();

    if( (timeNow - timeReadButton) < 50ul )
        return;

    timeReadButton = timeNow;
    
    // Read button(s)
    button_state = digitalRead(button_pin);
    if (button_state == LOW) 
    {
        // ...
    }
    
}

You could update an LED only when it changes state.

Analog sensors don’t usually change much in a few microseconds so reading them can also be delayed in time using millis() or other techniques.

You could also use more advanced techniques like an RTOS to schedule events to occur at regular intervals or, in bare-metal design, perhaps use a hardware timer to achieve something similar using timer overflows and and table of functions to be executed at regular intervals. This saves checking in every few microseconds to see if 50mS has passed between switch reads.

Doing this saves processor cycles if you have other time-sensitive stuff you need to run and can’t have a bunch of very -infrequent functions occupying even the timer needed to see if it’s time to run them…

As with your previous thread, it's not really plausible to give a definitive answer - it varies.

Usually, in a several things at the same time scenario, you're trying to ensure that the program is responsive to inputs. It's annoying for the user to have button presses ignored because of delays and it's bad news if you overrun a homing microswitch because your code didn't see it in time.

digitalRead is fast (though there are faster versions) and so you can read constantly if you feel it's appropriate. I don't recall seeing any code where anyone used millis to restrict the amount of CPU time spent polling buttons. You would have to be right at the edge of what your micro controller was capable of for that to be necessary and if so, I'd just get a faster one.

analogRead isn't so fast so maybe you don't want to read it continuously. Then again, if there's nothing else to do, why not? You can read it asynchronously anyway so if you are constrained by CPU, you can avoid the problem.

There is no holy writ that defines whether your code is non-blocking and it doesn't really matter anyway. If your code works to your satisfaction, you're good.

It depends...

If you're polling a button you really have to poll as frequently as possible. An interrupt might be more appropriate.

The processor is running full-speed all of the time (unless it's "sleeping") so extra reads & writes or extra loops don't hurt anything unless it takes processing time away from something else more important or more urgent.

Of course all "things" should terminate as soon as possible. E.g. a button check function can return immediately if the button state is unchanged.

I'd say that blocking code prevents other code from running even if better solutions exist.

The worst case (longest) time for all tasks (functions) together determines the responsiveness of a program. Okay as long as that time is acceptable for the entire project. Next the long runners can be serialized, i.e. only one of them is called during every loop iteration.

zheygrudov:
So what confuses me is: is it bad practice to write functions, which poll sensor(s) and input(s) without timing constraints, i.e. at every iteration of loop?

Every time through loop() the code will have to decide if it is now time to read the sensor. Will that code actually use more cpu cycles than simply reading the sensor.

If you keep adding functionality to any program there must come a time when the total code is more than that particular microprocessor is capable of dealing with. Then the only options are to omit some functions or use a faster microprocessor.

Using easy-to-understand code on a faster microprocessor is probably a better option than using arcane code to squeeze the last drop of performance from a slower microprocessor. Above all, code should be written so it is easy for you to maintain. Just because something can be done by a super-wizard-programmer does not mean that the technique is appropriate for you - especially if you have no wish to become a super-wizard-programmer.

...R