Site icon techtutorialsx

ESP32 Arduino: Passing a variable as argument of a FreeRTOS task

The objective of this post is to explain how to pass a variable as argument of a function implementing a FreeRTOS task.


Introduction

The objective of this post is to explain how to pass a variable as argument of a function implementing a FreeRTOS task. In order to understand this code, we will analyse first the concept of variables scope.

Although not mandatory, a prior knowledge to C pointers helps understanding the code and its working principle. We assume a previous installation of the ESP32 support for the Arduino IDE.


Variables scope

To understand the code we are going to create, it’s important to have knowledge about the concept of variables scope.

So, a global variable is a variable that can be seen and accessed by any function of our program [1]. In regular Arduino code, these are the variables we declare outside the setup, loop and custom functions we create in our code.  Global variables scope extends to the whole execution of the program, no matter if we use them inside functions or not.

In contrast, local variables are variables declared inside a function and can only be seen and accessed inside that function [2]. So, unless we specifically allocate a memory location for a variable (with malloc), local variables are lost after the execution of the function, since they are allocated in the stack.

This means that if we declare a variable on the Arduino setup function, once the function ends, the variable is no longer valid. So, if we try to access the memory position of that variable outside the scope of the setup function, the behavior will be undefined, meaning that we cannot predict its value. Maybe that memory position still has the original value or maybe another value was written on it, but the important is that we don’t know and we shouldn’t use it that way.

So, we need to be careful when passing parameters to tasks since we will basically passing a pointer to the memory position of a variable. Hence, we need to make sure that the memory position is still valid when we access the variable inside the FreeRTOS task.

So, for example, if we declare a variable in the setup function with value 5, launch a task and pass it a pointer to that memory position and then the setup function ends, we will be accessing an invalid memory position inside our task. Again, we may be lucky and that memory position still holds the correct value, but the behavior is not predictable and we should never do this.

So, we need to be careful when passing parameters to our tasks, to avoid these kind of problems, which are very hard to debug since the code compiles fine and doesn’t crash.

Naturally, if we guarantee that the variable is valid during the execution of the task, then there is no problem.

If we use, for example, a global variable, then it should also work fine since, as said before, its scope extends to the execution of the whole program.


The setup code

The first thing we are going to do is declaring a global variable, of type int, and assign it a value. We will assign the value 5. Since this is a global variable, its scope extends to the whole program execution.

int globalIntVar = 5;

Then, on the setup function, we will start a serial connection. This will allow us to output the results of our program to the Arduino IDE serial console using familiar functions, which is a great advantage of using both FreeRTOS and the Arduino environment.

Serial.begin(112500);
delay(1000);

After that, we will create a task. Please check this previous post for all the details and parameters needed for creating a task with FreeRTOS. We will write the code for the implementing function latter. For now, we will focus on passing to the task a parameter, which will be our previously declared global variable.

So, as specified here, we need to pass a pointer to void, which corresponds do (void*). Note that a pointer is a variable that has the address of other variable [3]. So, since our task receives a pointer to void, we will not pass it the value of globalIntVar (which is 5) but rather its memory position.

So, to get the memory position (or address) of a variable, we use the & operator [4]. To get the address of our var, the correct code is &globalIntVar. Before that, we need the cast to (void*).

(void*)&globalIntVar;

Check bellow how to create a task, passing as parameter our global var address.

xTaskCreate(
                    globalIntTask,             /* Task function. */
                    "globalIntTask",           /* String with name of task. */
                    10000,                     /* Stack size in words. */
                    (void*)&globalIntVar,      /* Parameter passed as input of the task */
                    1,                         /* Priority of the task. */
                    NULL);                     /* Task handle. */

As we will see in the implementation of the function, we will then have a mechanism to convert a generic void pointer to a integer pointer and access the value of its memory position.

Now, just to illustrate the problem of passing a variable with local scope to the task, we will declare a variable in the setup function and create a different task, passing its address as parameter. The pointer procedure mentioned before will be the same, as will be seen bellow.

int localIntVar = 9;

  xTaskCreate(
                    localIntTask,              /* Task function. */
                    "localIntTask",            /* String with name of task. */
                    10000,                     /* Stack size in words. */
                    (void*)&localIntVar,       /* Parameter passed as input of the task */
                    1,                         /* Priority of the task. */
                    NULL);                     /* Task handle. */


The tasks code

Both of the functions for the tasks will be implemented with a similar code. The only difference is that we will print different strings to know which one was accessing the local variable and the global variable. If you need more information on how the implementing function should be created, please check the previous post.

Since the code will be very simple, we will focus on the tricky part, which is the same for both functions. So, as we said, task functions receive a generic (void*) parameter. But, in our code, we now that we will need to interpret it as a pointer to int, which corresponds to (int*). So, the first thing we do is a cast to (int*).

(int *) parameter;

Now we have a pointer to the memory position of an integer. Nevertheless, we want to access the actual content of the memory position. So, we want the value of the memory position to which our pointer points. To do so, we use the deference operator, which is * [5].

So, what we need to do is using the deference operator on our converted pointer and we should be able to access its value.

*((int *) parameter);

Once we have access to it, we can simply print it using the Serial.println function, which is what we are going to do in both functions. So, the full source code can be seen bellow, with the implementation for both functions. Note that, for debugging purposes, we print different strings in the functions.

int globalIntVar = 5;

void setup() {

  Serial.begin(112500);
  delay(1000);

  xTaskCreate(
                    globalIntTask,             /* Task function. */
                    "globalIntTask",           /* String with name of task. */
                    10000,                     /* Stack size in words. */
                    (void*)&globalIntVar,      /* Parameter passed as input of the task */
                    1,                         /* Priority of the task. */
                    NULL);                     /* Task handle. */

  int localIntVar = 9;

  xTaskCreate(
                    localIntTask,              /* Task function. */
                    "localIntTask",            /* String with name of task. */
                    10000,                     /* Stack size in words. */
                    (void*)&localIntVar,       /* Parameter passed as input of the task */
                    1,                         /* Priority of the task. */
                    NULL);                     /* Task handle. */

}

void loop() {
  delay(1000);
}

void globalIntTask( void * parameter ){

    Serial.print("globalIntTask: ");
    Serial.println(*((int*)parameter));            

    vTaskDelete( NULL );

}

void localIntTask( void * parameter ){

    Serial.print("localIntTask: ");
    Serial.println(*((int*)parameter));            

    vTaskDelete( NULL );

}


Testing the code

To test the code, simply upload it and open the serial console of the Arduino IDE. You should get the prints of both tasks, as indicated in figure 1.

Figure 1 – Output of the program.

Note that the value printed by the task that receives the pointer to the global variable is correct and equal to 5.

On the other hand, the value printed by the task that receives the pointer to the local variable corresponds to 0, which is wrong since we assigned it the value 9 in the setup function. This happens because once the setup function finishes, that variable is no longer valid and other values can be written in its memory position.

Even if it had the value 9, the code was wrong since we were accessing a undefined value, which just by luck still had the value assigned in the setup function.


Final notes

In this simple code, we are passing the global variable as parameter of the function just as an example where we know that the variable is still valid. Naturally, it would be much easier to directly access it, since it is global and can be accessed inside the functions.

There are other ways of guaranteeing this consistence, such as explicitly allocating the memory for the variable or maintaining the function that creates the tasks active.


Related posts


References

[1] https://www.arduino.cc/en/reference/scope

[2] https://www.tutorialspoint.com/cprogramming/c_scope_rules.htm

[3] https://www.tutorialspoint.com/cprogramming/c_pointers.htm

[4] http://en.cppreference.com/w/c/language/operator_precedence

[5] https://www.computerhope.com/jargon/d/dereference-operator.htm

Exit mobile version