Software design: mechanism to communicate with different tasks through queues

This is more a software design question.

so I have this SystemTask which contains a message queue to which other tasks write via Push method.

However Push is currently a non-static method which means there should be a SystemTask’s instance referenced in each class to invoke on.

// system.hpp
class SystemTask
{
     QueueHandle_t mSystemQueue;
     // ...
};

// system.cpp
void SystemTask::Start()
{
    mSystemQueue = xQueueCreate(queueSize, itemSize);
    if (xTaskCreate(SystemTask::Process, "Run", 300, this, 0, &taskHandle) != pdPASS)	
    {
        // ...
    } 
}

void SystemTask::Process(void* instance)
{
    auto* app = static_cast<SystemTask*>(instance);
    app->Run();
}

void SystemTask::Run()
{
    // start peripherals

    while(true)
    {
        // block on the message
        if (xQueueReceive(mSystemQueue, &msg, portMAX_DELAY) == pdPASS)      
        { // ...
        }  
    }
}

void SystemTask::Push(SystemTask::Message message) // write to queue
{
    BaseType_t xHigherPriorityTaskWoken;
    xHigherPriorityTaskWoken = pdFALSE;
    xQueueSendFromISR(mSystemQueue, &message, &xHigherPriorityTaskWoken);
}

UART IRQ invokes a user-handling callback upon receiving an end-of-byte character however
the problem is that the callback (UartApp::Callback) is static which means it can’t use systemTask instance to Push.

I am debating whether:

  • SystemTask instance is be static since there’s going to be a single instance rather than each class storing a reference to SystemTask
  • Push method is only made static

It makes sense for UartApp to not have any instance and only contain static methods.

Pretty sure there must be a legit way to handle this for the sake of communication between different tasks (well, here it’s between ISR and a task!)

// uart.cpp
void UART_IRQ()
{
     // ...
    // end of byte is received, delegate the handling of the input to callback
   if (data == '\r')
   {
	  userCallback(fifo); // resides in a higher layer i.e UartApp::Callback 
   }
}
// uart_app.cpp
void UartApp::Callback(FIFO fifo) // static method
{
   Message msg = Process(fifo);
   systemTask.Push(msg); // systemTask & Push have to be static
}
// main.cpp
Uart uart{UART0, commParams, UartApp::Callback}; // 3rd argument
SystemTask systemTask{uart};

int main()
{
    systemTask.Start();
    vTaskStartScheduler();
}

The existence of classes that are singletons is fairly common (classes with only one possible instance). You can convert the class into just a namespace to not have an object, but having a singleton object isn’t bad either.

As to your UartTask, that could well NOT be a singleton, as you could well have a system with more than one Uart in it.

And, one advantage of making the classes have instances, it says you can control the construction order of the classes by the order you create the objects, and you can have one common “constructor” file that creates the objects in the needed order. Remember, you can’t tell what order the various files are run when creating your initial objects (and you don’t want to do it in main due to the way the system stack is done).

Making Push alone static member along with a mSystemQueue static did the trick. Should I still consider making SystemTask singleton since technically that’s what I’m doing here: passing the reference of a single instance of SystemTask in different modules including Uart class.

Are you referring to static initialization fiasco which could be caused by singleton?
If you can initialize UART prior to ‘instantiating’ SystemTask, that shouldn’t be an issue. (the following snippet is incomplete as I am not sure how can I initialize _uart in here but hopefully I wouldn’t need one)

class SystemTask
{
   public
   SystemTask& Get() 
   { 
      static SystemTask instance;
      return instance;
   }

   private:
   Uart& _uart;
};

// main.cpp
Uart uart;
SystemTask::Get();

Slightly off-topic, but there are a couple of efficiency-related reasons why I generally prefer to encapsulate data in singleton classes rather than namespaces:

  • In a class you can declare private data along with public inline methods that access it. In a namespace you can’t do that: if you declare the data in the .cpp file then you can’t declare inline methods in the .h file to access it; and if you declare the data in the .h file so that the inline methods can access it, then it’s public and any .cpp file that includes the .h file can access it directly.
  • On ARM Cortex processors, accessing data in a namespace is slower and uses more flash memory space than accessing class member data. To access data in a namespace (or any other global data, including static data members of classes) an ARM Cortex MCU generally has to load the address of the data into a register using PC-relative addressing, then access the data through that register. Whereas the address of a class object is normally held in a register, so class members can be accessed directly using base register + offset addressing.

Yes, classes give you a lot more control over your data, and for machines like the ARM architecture with somewhat limited memory accessing, the fact that the object puts the pieces together helps a lot with accessing.

Also, I will sometimes use a class as a “Singleton” in the sense that only one copy exists, even if the class doesn’t enforce that restriction.