How to create a global structure (struct), to store all the data printed to the serial port?

Hello,
...It is me again with another programming question. This one is about structures. I've created a simple program, using freeRTOS, to perform 4 tasks:

  • Task1 randomly generates a 0 or 1
  • Task2 randomly generates an integer between 500 and 1000
  • Task3 randomly generates an integer between 0 and 100 and averages the last 4 values
  • Task4 Log the values in tasks 1, 2 and 3 to the serial port.
    This is the code:
// Use only core 1 to implement everything on the single core
#if CONFIG_FREERTOS_UNICORE
static const BaseType_t app_cpu = 0;
#else
static const BaseType_t app_cpu = 1;
#endif

int state; //Task1's variable
int frequency;//Task2's variable
unsigned short int filter_analg_val;//Task3's variable

void Task1(void *argp) {

  while (1) {
    state = rand() % 2; //generate a random int between 0 and 1 (i.e. 0 or 1)

    vTaskDelay(200); //execute this task every 200ms
  }
}

void Task2(void *argp) {
  while (1) {
    frequency = 500 + (rand() % (1000 - 500 + 1)); //randomly set frequency to an int between 500 to 1000

    vTaskDelay(1000); //execute this periodic task every second
  }
}

void Task3(void *argp) {
  //filtered analogue value, by averaging the last 4 readings.
  while (1) {
    int new_analg_val = 0;
    for (int i = 0; i <= 3; i++) { //take 4 readings
     int analg_val = 0 + (rand() % (100 - 0 + 1)); //randomly set analg_val to an int between 0 to 100
      new_analg_val = new_analg_val + analg_val;
    }
    filter_analg_val = new_analg_val / 4; //average of the last 4 readings

    vTaskDelay(40); //execute this task every 40ms
  }
}

//Task4 - print values to the serial port ONLY when state=1;
void Task4(void *argp) {
  while (1)
  {
    if (state == 1) {
      Serial.print(state); Serial.print(","); Serial.print(frequency); Serial.print(","); Serial.println(filter_analg_val);
    }
    else {
      Serial.println("state = 0");
    }
    vTaskDelay(5000); //The system must executes this task every 5 seconds
  }
}


void setup() {
  Serial.begin(9600);


  // Task1 to run forever
  xTaskCreatePinnedToCore(
    Task1,        // Function to be called
    "Task1",      // Name of task
    1024,         // Stack size (bytes in ESP32, words in FreeRTOS)
    NULL,         // Parameter to pass to function
    1,            // Task priority (0 to configMAX_PRIORITIES - 1)
    NULL,         // Task handle
    app_cpu);     // Run on one core

  // Task2 to run forever
  xTaskCreatePinnedToCore(
    Task2,        // Function to be called
    "Task2",      // Name of task
    1024,         // Stack size (bytes in ESP32, words in FreeRTOS)
    NULL,         // Parameter to pass to function
    1,            // Task priority (0 to configMAX_PRIORITIES - 1)
    NULL,         // Task handle
    app_cpu);     // Run on one core

  // Task3 to run forever
  xTaskCreatePinnedToCore(
    Task3,        // Function to be called
    "Task3",      // Name of task
    1024,         // Stack size (bytes in ESP32, words in FreeRTOS)
    NULL,         // Parameter to pass to function
    1,            // Task priority (0 to configMAX_PRIORITIES - 1)
    NULL,         // Task handle
    app_cpu);     // Run on one core

  // Task4 to run forever
  xTaskCreatePinnedToCore(
    Task4,        // Function to be called
    "Task4",      // Name of task
    1024,         // Stack size (bytes in ESP32, words in FreeRTOS)
    NULL,         // Parameter to pass to function
    1,            // Task priority (0 to configMAX_PRIORITIES - 1)
    NULL,         // Task handle
    app_cpu);     // Run on one core

}
  void loop() {
  }

Even though this is a very simple program, I am using freeRTOS because I need it to be consistent with the main project I am doing. I just created this simple program to help me understand structures.

I am using ESP32. The program above complies and gives me the anticipated outputs. Great. BUT now my question is how can I create a global structure (struct), to store all the data printed to the serial port in task 4?

My progress so far:
I have done research into structs and tried to relate them to my problem. Here are my findings:


From that, the only solution I could think of is to create a new variable of type 'logged_info', every time task4 gets executed i.e. every 5 seconds. And apparently, according to this post c++ - change variable name with a loop - Stack Overflow it is not possible
to create variables dynamically. So what should I do?

Thanks again for your help :slight_smile:

could you havean array of the structure? e.g.

struct Data {
  int state;
  float frequency;
  float analogue;
} data[100];
  
void setup() {
  data[0].frequency=200.0f;

how much task 4 data do you wish to store?

A structure is a way to group together attributes that are related.
creating a variable of that structure type is just 1 instance of those attributes
if you need to record many instances, you need an array of those structs
the size of the array is going to define the max number of instances you can use.

There are alternatives using dynamic memory allocation but they are not highly recommended on small microcontrollers where memory is limited;

// say we want to remember which process generated which value and when 
struct t_processData { // create a new type named t_processData which is the grouping of the following attributes
  byte processID;
  int valueGenerated;
  uint32_t timeOfGeneration;
};

// define the max number of records we want to keep in memory
const size_t maxRecords = 100;

// define an array of records
t_processData records[maxRecords];

// defne a variable to keep track of the first empty record you can use
size_t currentRecord = 0;

this array would be used by the 3 tasks, so the variable currentRecord needs to be dealt with in a critical section so that two tasks won't grab the same record position

Your program cannot work correctly because it has data races. You're not allowed to read and write to the same variable from different threads/tasks (e.g. the variable state) unless you provide the necessary synchronization, for example by using by atomics or mutexes.

The rand() function may not be thread-safe or reentrant either. Use thread-local C++ standard library random number generators instead, see the notes and links on std::rand - cppreference.com.

yeh I know what you mean. I was planning to use semaphores to control their read/ write access. That was going to be the next stage. I first wanted to program the structs

Let's say I want to save 100 records. So how do I actually assign the variables in the array inside task4?

Aslo, I don't undertsnad this line.

Is that just an example of frequency getting set to 200?

You cannot share structs between threads either. You always need synchronization, there's no way around that, even for simple tests.

What do you want to do with the data? Is it only ever accessed in Task 4?

as each task 4 data arrives you would copy its values into an array element,
e.g. assume index (a global or static int initialised to 0 at program startup) points at the next element

    if (state == 1) {
      Serial.print(state); Serial.print(","); Serial.print(frequency); Serial.print(","); Serial.println(filter_analg_val);
      data[index].state=state;    //  copy data into structure   
      data[index].frequency=frequency;
      data[index].analogue=filter_analg_val;
      index++;     // increment for next data set
    }

you would need to check index to make sure you don't get array overflow

You can, but what's the purpose of such an object? What if the memory is full?

I'd use a logging program on the connected PC that stores enough text in a disk file. Then you also can perform post-mortem dump analysis.

what machine are you running on? an ESP32?
if so you could use WiFi or BLE to send data to a PC for processing as @DrDiettrich suggested

@horace and everyone else, I apologise, I believe I have miscommunicated what I meant. I want to have a global struct to store the values that are to be printed, rather than storing them after they are printed. So I suppose I wouldn't need to worry about the number of records.
@DrDiettrich, the purpose is so that access to this structure must be adequately protected, using FreeRTOS semaphore(s).

I am using ESP32.

https://www.dfrobot.com/blog-935.html

FreeRTOS queues allow for using more complex data structures, such as structs.

If you use a semaphore / mute for the index to ensure no two threads are accessing the same struct in the array, you should be fine - don’t you ?

That's what every driver does, even the Serial device on an 8 bit AVR. If FreeRTOS does not allow to access that buffer then it's for good reason.

I'm not too sure what you mean. Do you mean it is unnecessary to use Semaphores as access to the buffer will be given to different tasks regardless?

unsigned int logSize = 100;
unsigned int logIndex;

struct Data {
  unsigned char task1RNG;
  unsigned int task2RNG;
  unsigned char task3RNG;
} extern log[logSize];

Data log[logSize];

Serial.print(log[logIndex].task1RNG);

No, you can't dynamically create variables after compile, but you can organize your variables in a struct and then have an array of structs, so that your log has a maximum size. Then you index through the array using and unsigned variable large enough to index the entire array from 0 to (logSize - 1).

I wrote this code, it is giving expected results in the output. @PieterP I don't actually don't see why I would need to use semaphores or mutexes here. There doesn't seem to be an access problem from different threads. I mean I understand that theoretically I should include them but why is it working fine without it?

// Use only core 1 to implement everything on the single core
#if CONFIG_FREERTOS_UNICORE
static const BaseType_t app_cpu = 0;
#else
static const BaseType_t app_cpu = 1;
#endif

int buttom_state;
int anlg_freq;

struct Struct_Data {
  int state; //Task1's variable
  int frequency;//Task2's variable
  unsigned short int avg_val;//Task3's variable
} ;

Struct_Data logged_info;

void Task1(void *argp) {

  while (1) {
    buttom_state = rand() % 2; //generate a random int between 0 and 1 (i.e. 0 or 1)
    logged_info.state = buttom_state;
    vTaskDelay(200); //execute this task every 200ms
  }
}

void Task2(void *argp) {
  while (1) {
    anlg_freq = 500 + (rand() % (1000 - 500 + 1)); //randomly set frequency to an int between 500 to 1000
    logged_info.frequency = anlg_freq;
    vTaskDelay(1000); //execute this periodic task every second
  }
}

void Task3(void *argp) {
  //filtered analogue value, by averaging the last 4 readings.
  while (1) {
    int filter_analg_val;
    int new_analg_val = 0;
    for (int i = 0; i <= 3; i++) { //take 4 readings
      int analg_val = 0 + (rand() % (100 - 0 + 1)); //randomly set analg_val to an int between 0 to 100
      new_analg_val = new_analg_val + analg_val;
    }
    filter_analg_val = new_analg_val / 4; //average of the last 4 readings
    logged_info.avg_val = filter_analg_val;
    vTaskDelay(40); //execute this task every 40ms
  }
}

//Task4 - print values to the serial port ONLY when state=1;
void Task4(void *argp) {
  while (1)
  {
    if (logged_info.state == 1) {
      //Serial.print(state); Serial.print(","); Serial.print(frequency); Serial.print(","); Serial.println(filter_analg_val);
      Serial.print(logged_info.state);Serial.print(",");Serial.print(logged_info.frequency);Serial.print(",");Serial.println(logged_info.avg_val);
            
    }
    else {
      Serial.println("state = 0");
    }
vTaskDelay(5000); //execute this task every 5 seconds
  }
}


void setup() {
  Serial.begin(9600);


  // Task1 to run forever
  xTaskCreatePinnedToCore(
    Task1,        // Function to be called
    "Task1",      // Name of task
    1024,         // Stack size (bytes in ESP32, words in FreeRTOS)
    NULL,         // Parameter to pass to function
    1,            // Task priority (0 to configMAX_PRIORITIES - 1)
    NULL,         // Task handle
    app_cpu);     // Run on one core

  // Task2 to run forever
  xTaskCreatePinnedToCore(
    Task2,        // Function to be called
    "Task2",      // Name of task
    1024,         // Stack size (bytes in ESP32, words in FreeRTOS)
    NULL,         // Parameter to pass to function
    1,            // Task priority (0 to configMAX_PRIORITIES - 1)
    NULL,         // Task handle
    app_cpu);     // Run on one core

  // Task3 to run forever
  xTaskCreatePinnedToCore(
    Task3,        // Function to be called
    "Task3",      // Name of task
    1024,         // Stack size (bytes in ESP32, words in FreeRTOS)
    NULL,         // Parameter to pass to function
    1,            // Task priority (0 to configMAX_PRIORITIES - 1)
    NULL,         // Task handle
    app_cpu);     // Run on one core

  // Task4 to run forever
  xTaskCreatePinnedToCore(
    Task4,        // Function to be called
    "Task4",      // Name of task
    1024,         // Stack size (bytes in ESP32, words in FreeRTOS)
    NULL,         // Parameter to pass to function
    1,            // Task priority (0 to configMAX_PRIORITIES - 1)
    NULL,         // Task handle
    app_cpu);     // Run on one core

}
void loop() {

}

One thing stands out right away:

    if (logged_info.state == 1) {
      //Serial.print(state); Serial.print(","); Serial.print(frequency); Serial.print(","); Serial.println(filter_analg_val);
      Serial.print(logged_info.state);Serial.print(",");Serial.print(logged_info.frequency);Serial.print(",");Serial.println(logged_info.avg_val);        
    }

What happens if there's a context switch after the 'if' statement evaluates to true but before the print() statement(s) executes and one or more of the struct's members changes value?

Also, Task4 runs so infrequently that it will miss multiple value changes of the struct.

Does the the following line differ semantically with the above line?

A structure is a way to group together data items/variables of different types.

if that works for you. It's a high level explanation of what they are

if you want to be more precise you'll say this is a new type, may be something like

A structure is a user-defined data type allowing to combine data items of various data types (the structure's members) under a single typename.

the spec says A struct is a type consisting of a sequence of members whose storage is allocated in an ordered sequence

and of course in C++ you can also define functions as part of the members (they are the same thing as classes — the difference being the default accessibility of member variables and functions . In a struct they are public whereas in a class they are private).