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.

ESP32 Passing variables to FreeRTOS tasks

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

30 thoughts on “ESP32 Arduino: Passing a variable as argument of a FreeRTOS task”

  1. Hi! Thanks for the feedback 🙂 I’m glad you found out how to do it. It makes sense and you are basically saying that the value of the memory position pointed by parameter is now 42.

    It’s a basic operation when using pointers and thus it makes perfect sense to use it.

    Note that pointers may seem complex at first, but learning how to use them correctly will open a lot of possibilities when developing Arduino / C / C++ code.

    Best regards,
    Nuno Santos

  2. Hi! Thanks for the feedback 🙂 I’m glad you found out how to do it. It makes sense and you are basically saying that the value of the memory position pointed by parameter is now 42.
    It’s a basic operation when using pointers and thus it makes perfect sense to use it.
    Note that pointers may seem complex at first, but learning how to use them correctly will open a lot of possibilities when developing Arduino / C / C++ code.
    Best regards,
    Nuno Santos

  3. What is the point of passing a parameter to a task if you declare it global in the first place? You don’t need the fancy pointer representations and typecastings.

    The below code works as well by setting the parameter in xtaskcreate(), NULL
    Serial.print(“globalIntTask: “);
    Serial.println(globalIntVar);

    New to freeRTOS just asking out of curiosity.

    1. Hi!

      The objective of the code was to illustrate the problem of passing a pointer to a local variable of a function that will be used inside a task.

      If you use that local variable after the function returns, then you will get an undefined result since you are not sure what will be on that memory position.

      This may lead to run time errors really hard to debug.

      As stated in the final notes, at the end of the post, the case when we pass the pointer to the global var is just to illustrate that the variable is still “valid” even after the setup function returns.

      So, as you mentioned, it’s much easier to access the global variable directly and this was just some demonstration code.

      Nonetheless, there may be use cases where you need to pass some parameter to a task that, by some reason, cannot be declared global, and thus it’s important to not get caught on this kind of problems.

      Hope this clarifies 🙂

      Best regards,
      Nuno Santos

  4. What is the point of passing a parameter to a task if you declare it global in the first place? You don’t need the fancy pointer representations and typecastings.
    The below code works as well by setting the parameter in xtaskcreate(), NULL
    Serial.print(“globalIntTask: “);
    Serial.println(globalIntVar);
    New to freeRTOS just asking out of curiosity.

    1. Hi!
      The objective of the code was to illustrate the problem of passing a pointer to a local variable of a function that will be used inside a task.
      If you use that local variable after the function returns, then you will get an undefined result since you are not sure what will be on that memory position.
      This may lead to run time errors really hard to debug.
      As stated in the final notes, at the end of the post, the case when we pass the pointer to the global var is just to illustrate that the variable is still “valid” even after the setup function returns.
      So, as you mentioned, it’s much easier to access the global variable directly and this was just some demonstration code.
      Nonetheless, there may be use cases where you need to pass some parameter to a task that, by some reason, cannot be declared global, and thus it’s important to not get caught on this kind of problems.
      Hope this clarifies 🙂
      Best regards,
      Nuno Santos

  5. I have a small nit to pick with the statement “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*).” While this is syntactically correct and does no harm, the information as presented is not entirely correct. The cast to void* is not required by C/C++. All pointers implicitly cast to void*. You do, however, need to cast back to int* before obtaining the number stored as the actual parameter.

  6. I’m sure if your localIntTask is big and need time to run, at the end the access to localIntVar will be lost (invalid pointer) as the setup() will finish. Now is working just because localIntTask access localIntVar so fast. not not garneted.

Leave a Reply to paccerCancel reply

Discover more from techtutorialsx

Subscribe now to keep reading and get access to the full archive.

Continue reading