Skip to main content

Backend Communication

In most applications, the UI needs to be connected to the rest of your system somehow, and send and receive data. This could be interfacing with hardware peripherals (sensor data, A/D conversions, serial communication, ...) or interfacing with other software modules.

This article describes the recommended solutions for implementing this connection.

The first method is a "quick-and-dirty" approach, primarily intended for prototyping, whereas the second method is an architecturally sound way of properly connecting the UI with the remaining components in a real world application.

In the end of this article, we link to examples of using both methods.

The Model Class

All TouchGFX applications have a Model class, which apart from storing UI state information is also intended to function as the interface to your surrounding system. By this we are referring to both hardware peripherals but also communicating with other OS tasks in your system. It is normally not a good design to access other software modules or hardware in the individual View classes.

Further reading
To learn more about the Model: MVP pattern

The Model class is well suited for placing any such interface code because:

  1. The Model class has a tick() function which is automatically called every frame and can be implemented to look for and react to events from other sub-modules.
  2. The Model class has a pointer to your currently active Presenter, in order to be able to notify the UI of incoming events.

System Interfacing

There are two ways of interfacing with the surrounding system, either by sampling directly from the GUI Task, or by sampling from a Secondary Task

Sampling from GUI Task

The best way of interfacing with surrounding system depends on how frequently you need to sample, how time consuming it is and how time critical it is.

If your requirements in those regards are lenient, the simplest approach is to just sample the surrounding system directly in the Model::tick function.

If the sampling occurs less frequently than your frame rate (typically around 60Hz), you can just add a counter and only do the sampling every Nth tick. When done this way, your sampling operation must be somewhat fast (typically 1ms or less), otherwise your frame rate will begin to suffer, since the sampling is done in the context of the GUI task and will delay drawing the frame.

Sampling from Secondary Task

Alternatively, if it is not desirable to place the interaction with the surrounding system directly within the context of the GUI task, you can create a new OS task responsible for doing the sampling.

You can configure this task to run at the exact time intervals you need for your specific scenario. Also depending on your needs this new task can have a lower or higher priority than the GUI task.

If it has a higher priority, then you are guaranteed that it runs at exactly the times you have specified, regardless of what the GUI task is doing. This has the drawback that if it is a CPU consuming process it might impact the frame rate of the UI.

If on the other hand the sampling is not time critical, you can assign the task a lower priority than the GUI task, such that the UI frame rate is never affected by the sampling of the surrounding system. The GUI task will sleep a lot while rendering (e.g. when waiting for a DMA-based pixel transfer to complete) so lower priority tasks will be allowed to run quite frequently and this is therefore sufficient for the vast majority of applications.

If you use the secondary task approach, we recommend that you take advantage of the inter-task messaging system that is provided by your RTOS. Most, if not all, RTOSes have a queue/mail mechanism which allows you to send data (typically user-defined C structs, byte arrays or simple integers) from one task to another. In order to communicate new data to the GUI task, set up a mailbox or message queue for the UI task, and send the data to the GUI task using this messaging system. You can then Model::tick poll the mailbox of the GUI task to check if any new data has arrived. In case, read the data and update the UI accordingly.

Propagating Data to UI

Regardless of whether you use Sampling from GUI Task or Sampling from Secondary Task, the Model::tick function is the place where the GUI Task becomes aware of the new data to be shown in the UI. Apart from acting as an interface to your surrounding system, recall from earlier that the Model class is also responsible for holding state data, so there might be some state variables that need to be updated too.

Let us consider a simple example where a temperature sensor is attached to the system, and that the current temperature is to be shown in the UI. In preparation, we will augment the Model class to support this:

Model.hpp
class Model
{
public:
// Function that allow your Presenters to read current temperature.
int getCurrentTemperature() const { return currentTemperature; }

// Called automatically by framework every tick.
void tick();
...
private:
// Variable storing last received temperature;
int currentTemperature;
...
};

With the above, your Presenters are able to ask the model about the current temperature, allowing a Presenter to set this value in the UI (the View) when entering a screen that displays the temperature. What we need to do now is to be able to update the UI again when new temperature information is received. To do this we take advantage of the fact that the Model has a pointer to your currently active Presenter. The type of this pointer is an interface (ModelListener) which you can modify to reflect the application-specific events that are appropriate:

ModelListener.hpp
class ModelListener
{
public:
// Call this function to notify that temperature has changed.
// Per default, use an empty implementation so that only those
// Presenters interested in this specific event need to
// override this function.
virtual void notifyTemperatureChanged(int newTemperature) {}
};

Now that we have this interface hooked up, the remaining this is to do the actual sampling of incoming "new temperature" events Model::tick

Model.cpp
void Model::tick()
{
// Pseudo-code for sampling data
if (OS_Poll(GuiTaskMBox))
{
// Here we assume that you have defined a "Message" struct containing type and data,
// along with some event definitions.
struct Message msg = OS_Read(GuiTaskMBox);
if (msg.eventType == EVT_TEMP_CHANGED)
{
// We received information that temperature has changed.
// First, update Model state variable
currentTemperature = msg.data;

// Second, notify the currently active Presenter that temperature has changed.
// The modelListener pointer points to the currently active Presenter.
if (modelListener != 0)
{
modelListener->notifyTemperatureChanged(currentTemperature);
}
}
}
}

The approach above ensures two things:

  1. The currentTemperature variable is always up-to-date so that your Presenter can at any time obtain the current temperature.
  2. The Presenter is immediately notified of temperature changes and can take appropriate action.

One advantage of the MVP pattern is that you achieve a separated handling of notifications depending on what screen you are currently on. Assume for instance that a temperature changed event occurs while displaying some kind of settings menu (e.g. MainMenuPresenter/MainMenuView is active) where the current temperature is of no relevance.

Since the notifyTemperatureChanged function has a default empty implementation, this notification is simply disregarded by the MainMenuPresenter. On the other hand, if you have a TemperatureControlPresenter you can in this Presenter override the notifyTemperatureChanged function and inform the View that it should display an updated temperature:

TemperatureControlPresenter.hpp
class TemperatureControlPresenter : public ModelListener
{
public:
// override the empty function.
virtual void notifyTemperatureChanged(int newTemperature) {
view.setTemp(newTemperature);
}
};

The View class TemperatureControlView, must of course implement the setTemp method.

Transmitting Data from UI to Surrounding System

The reverse direction where data/events are transferred from the UI to the surrounding system, is done through the Model in much the same way. Continuing the example from before if we need to add the ability to configure a new target temperature, we would add the following to the Model:

Model.hpp
void setNewTargetTemperature(int newTargetTemp)
{
// Pseudo-code for sending an event to a task responsible for controlling temperature.
struct Message msg;
msg.eventType = EVT_SET_TARGET_TEMP;
msg.data = newTargetTemp;
OS_Send(SystemTaskMBox, &msg);
}

In case the user sets a new target temperature in the UI, the View can inform the Presenter which holds a pointer to the Model object and is therefore able to call the setNewTargetTemperature function.

Examples

The following examples are Board Specific Demos (BSD), however much of the code demonstrated can be reused for other demo boards and custom hardware. For these examples we create the Tasks and Queues in STM32CubeMX. We then populate the generated Tasks and implements example user code in main_user.c. The examples use STM32CubeMX BSP librarie to control the LEDs, user buttons and other peripherals on the STM32 evaliation kits.

From GUI Task

An example application, a BSD, can be found in the latest version of TouchGFX Designer under Demos -> Board Specific Demo -> STM32F46G Discovery kit Control LED from GUI.

The application demonstrates how to sample a button and control a LED. The Model class samples a button and updates the LED to match the state of the application.

From Other Task

An example application, a BSD, can be found in the latest version of TouchGFX Designer under Demos -> Board Specific Demo -> STM32H7B3I Evaluation Board Analog Sampler Task.

The application demonstrates how to sample an analog input in separate thread. The example uses the MVP architecture to transfer the analog value to the View.

An example application, a BSD, can be found in the latest version of TouchGFX Designer under Demos -> Board Specific Demo -> STM32F46G Discovery kit Intertask Communication.

The application demonstrates intertask communication and propagation to and from the UI. Use this as inspiration for your own setup. The example communicates between the backend system implemented in C code and the C++ TouchGFX GUI. The example runs on the STM32F746G-DISCO board on top of FreeRTOS.

From Multiple tasks

An example application, a BSD, can be found in the latest version of TouchGFX Designer under Demos -> Board Specific Demo -> STM32F769I Discovery Multitast Communication Demo.

The application samples the state of the button, passes a message through the GUI message queue if the button is pressed down. This allows us to advance the animation in the application by keeping the button pressed.

The application uses three FreeRTOS tasks. One for the GUI, one for each peripheral (LED and USER Button).

From Task and External Interrupt Line

An example application, a BSD, can be found in the latest version of TouchGFX Designer under Demos -> Board Specific Demo -> STM32F769I Discovery External Interrup Line Demo.

The application was designed for the STM32F769I-DISCO board and interacts with an LED and the USER BUTTON to show how to integrate both C code and hardware peripherals into your TouchGFX application.

This application configures the button in EXTI mode (external interrupt line 0). Behavior is to receive an interrupt when the button is pressed down after which the interrupt is cleared. This does not allow the same behavior as in GPIO, but instead we'll be single stepping the animation because a message is only sent through the gui message queue whenever an interrupt is received.

The application uses two FreeRTOS tasks. One for the GUI, one for the LED. (The Button task from Multiple tasks demo remains active in this application to exemplify that the peripheral interaction code has been moved into an interrupt handler).