Why are local variables avoided in Arduino?

Asked

Viewed 4,153 times

20

In several code examples for the Arduino I note that there is almost no use of variables in local scope. One of the examples present in the IDE: Analog > AnalogInput:

int sensorPin = A0;
int ledPin = 13;
int sensorValue = 0;

void setup() {
  pinMode(ledPin, OUTPUT);
}

void loop() {
  sensorValue = analogRead(sensorPin);
  digitalWrite(ledPin, HIGH);
  delay(sensorValue);
  digitalWrite(ledPin, LOW);
  delay(sensorValue);
}

The variable sensorValue is global, while its use is only within the function loop. Another case is the library Moving-Average-Filter. A part of the code:

#define MAX_DATA_POINTS 20

class MovingAverageFilter {
public:
  MovingAverageFilter(unsigned int newDataPointsCount);
  float process(float in);

private:
  float values[MAX_DATA_POINTS];
  int k;
  int dataPointsCount;
  float out;
  int i;
};

Here the members out and i are used only in process, must be local:

float MovingAverageFilter::process(float in) {
  out = 0;

  values[k] = in;
  k = (k+1) % dataPointsCount;

  for (i=0; i<dataPointsCount; i++) {
    out += values[i];
  }

  return out/dataPointsCount;
}

Using variables in this way seems absurd to me. Is it purposeful? If so, why?

The only possibility I can imagine is that the address of local variables would be known at compile time, so they can be accessed without taking into account the stack registrar. It really makes a difference?

But in the case of the class, I can’t see how it could be faster to read an object through the pointer this than reading it in the stack, relative to the registered.

Another explanation might be to avoid having to allocate one stack frame for the function. But this allocation should be as simple as incrementing a registered one, I do not understand why it should be avoided. In addition functions that receive arguments will have a stack frame anyway.

  • Could having the variables this way facilitate identification in a possible memory dump? (just a kick - I’m a layman in low-level programming).

  • 2

    Actually, it’s a more philosophical issue than performance. There may be a minimum difference in access to the stack frame or the fixed memory created for the global ones, but both are so close due to the nature of the applications that the difference is not even considerable. I prefer to continue with good practice and lose a few milliseconds of Runtime to lose hours of debugging.

  • I don’t understand Uino, but what size is available for the stack? It is possible that the space in Uino for this is limited.

3 answers

17


I researched the subject and did not find any good answer about it. The reasons I was able to raise are as follows:

  • Programs written for Arduino are generally quite simple and there is little memory available. As a result, few people care a lot about modularization and encapsulation in Arduino programs.

  • Arduino does not use a single method main() to run the program. Instead, it uses two methods setup() and loop() independent. The result of this is that you can use within loop() what has been defined in setup(), you end up being required to use global variables.

  • Often the variables used in the method loop() should be remembered between one iteration and another. This causes you to end up being forced to use global variables.

  • Arduino is too simple and limited for you to use events, callbacks and messaging effectively.

  • Most examples are written for beginners who know very little about C, so everything is a little too simplified. Also, most Arduino users do not have much interest, practice or training in software programming, because their focus is on hardware.

Useful but inconclusive discussions can be found here and here.

In conclusion: I recommend following good traditional programming practices. Constants can be easily optimized by the compiler, so you can declare them in the global scope smoothly (the problem of global variables is when the value is changed, which does not occur with constants). Anything that can change value, it’s best to stay in a local scope, unless you don’t have a choice. If using the global scope, remember the modifier static to make the variable private and provide functions getters and setters if you need to export it.

  • 3

    That’s right. The first three items on your answer list define everything. Arduino is a standard for very low-level microcontroller boards, to control sensors, Leds etc. Normally supported controllers have 32KB or 256KB of memory for programs and 20 to 40 input and output ports. It is possible to use classes in C++, but usually there are not many levels of abstraction, as there is not enough memory or need to abstract complexity. When the library used has classes, usually instances are created in global scope as well.

  • As for the use of memory, the only gain would be the fact that the stack (stack) is not used to store the variables. Nothing I criticize in the examples cited, but in a recursive function, for example, it would make a lot of difference. However, another solution would be the use of static variables (Static) within the functions.

  • 1

    @Adrianop a recursive function would be precisely a problem since changing variables in one iteration would affect the others. Unless the function is called only at the end, but in this case Tail call Optimization applies and there is no allocation of more stack. I agree to use variables such as Static. At least it would be less dirty.

6

See what the Arduino site says:

"When programs start to get bigger and more complex, local variables are a useful way to ensure that only the function has access to its own variables. This avoids programming errors when a function inadvertently modifies variables used by another function. Sometimes it is convenient to declare and initialize variables within a for(...)." (http://arduino.cc/en/Reference/scope)

Indiscriminate use of global variables is not advised; they should be used only when it is necessary to access their values from different blocks. Use local constants or variables whenever possible.

2

What happens is that variables defined within a scope are allocated in the function frame (in the stack).

Globally defined variables go straight to the data segment.

The difference is that while accessing a data segment data is using direct addressing, to access data allocated in the frame are accessed by indirect indexed addressing.

For those who understand ASM Z80, is something like (AVR uses RISC, this is not exactly what happens in AVR):

Direct addressing:

LD A, (VAR_ADDRESS)

Indexed indirect addressing:

LD HL, stack_pointer
LD A, (HL),VAR_INDEX

Obviously the first way is faster, IE, consumes less processor cycles. Even if there is an AVR intro that does everything at once, it still hits the memory twice: one to get the base, the other to get the data (whose address is calculated from the base, which itself also uses processor cycles).

So if you are programming a Critical time algorithm, each processor cycle counts and you will want to generate the fastest instruction possible. Most of the time, this gain is marginal but time and again makes a difference.

An alternative to global variables is to use "Static" scope variables. They are allocated in the data segment equally, but only the scope where it was defined is able to address it:

float MovingAverageFilter::process(float in) {
  static float out = 0;

  values[k] = in;
  k = (k+1) % dataPointsCount;

  static int i;
  for (i=0; i<dataPointsCount; i++) {
    out += values[i];
  }

  return out/dataPointsCount;
}

A side effect is that instead of having only one "i" in the universe, each function that declares its Static int i for its counters will allocate an address in the data segment - 10 functions, 10 variables allocating space.

Hence if your RAM space is getting short, it is better to declare globally even to save space in the data segment.

Sometimes we need to give up good practice when dealing with limited hardware. When you only have 2K of RAM, spending 40 bytes on 10 32-bit counters can do the same service with only 4 bytes can be the difference between using a cheaper AVR, or having to use a more expensive one.

If you want to produce thousands of units of your product, a more expensive 1 USD chip will have to cost thousands of USD more to do the same service.

EDIT: Note that I have solemnly ignored the compiler optimizations. A compiler could, if the counter were declared within the scope of the function (and using for(int i = 0....) is a way to help the compiler pull this out), it could go straight to an AVR register (it has 32!), and thereby save memory and processor cycles - nothing is faster than using the recorder!

Browser other questions tagged

You are not signed in. Login or sign up in order to post.