Embedded Software Architecture Design – Message Interaction

Click the blue text above to learn more about practical skills in embedded programming.If you find this article helpful, feel free to like and follow.

Introduction

After familiarizing yourself with task scheduling, program layering, and modular programming regarding software architecture, layering, and module design, you will encounter how to achieve message interaction between modules of the same layer, typically between application layers.

For example, a device designed with an architecture that includes a human-machine interaction application layer module (which generally calls functions for buttons and displays) and a communication application layer module (which generally calls functions for serial ports, CAN, and network ESP8266), if two modules at the same layer need to exchange data, they usually call the interfaces provided in their respective header files (modules should avoid using global variables to prevent other modules from modifying them arbitrarily), resulting in coupling.

Design Approach

The above situation can also be addressed by implementing a callback function to decouple the modules, but this requires introducing new content, namely a common module (which includes third-party function libraries).Embedded Software Architecture Design - Message Interaction

The common module mainly includes type definitions, structure definitions, general functions, or commonly used macro definitions that all modules need to use (these usually belong to basic functionality and are not affected by functional requirements or different platforms).

Based on the common module, to solve data interaction between modules, basic functionality can be implemented through the common module to achieve decoupling of each application layer module.

By referencing a message queue approach, a producer/consumer functional module can be implemented (this can be termed the observer pattern, where there areobservers andobserved objects), meaning that when a certain module updates data, other modules can be notified immediately to update (using a callback function approach).

See the diagram:

Embedded Software Architecture Design - Message Interaction

Callback is an array of pointer variables, where each array member is a function pointer variable. The function addresses of the application layer code functions OnSaveParam(...) and OnUpdateParam(...) are obtained through the function Notify_Attach. The human-machine interaction module then calls Notify_EventNotify, which in turn calls Callback. The calling method is slightly different from directly calling OnFunction(...) because it is an array, so it requires [] to obtain the function address. To ensure the system runs safely, it is necessary to ensure Callback[i] is not NULL before calling; otherwise, it will cause program exceptions.

From the above, some may feel that this approach complicates things; isn’t it simpler to call directly? (The aforementioned human-machine interaction module is the observed object, while the parameters and other modules are observers.)

There are several benefits:

  1. Avoid mutual calls between modules, achieving decoupling.

  2. Even if one observer module is removed, the observed object or other observer code does not need to be modified, ensuring system stability.

  3. Adding a new observer module does not require modifying the observed object code, ensuring system stability.

Of course, this method also has drawbacks:

  1. If there are too many callback functions, or if the execution time of a callback function for a certain observer is very long, it will certainly affect the notification time of other observer modules and may even impact the normal operation of the observed object module.

  2. If there is a circular dependency between observers and observed objects, it will lead to circular calls and cause the system to crash.

Avoidance methods:

  1. Ensure that the execution time in the callback function is short; it should not have long execution time functions or delays (generally, only short execution time functions like data updates should be handled in callbacks; time-consuming processing after data updates can be executed in the main loop).

  2. In observer callback functions, avoid executing other observer callback functions to prevent circular calls.

Example Code

Below is a simple implementation where the human-machine interaction module needs to save parameters under certain conditions, and the specific method of saving parameters is implemented by the parameter module. The human-machine interaction module notifies the parameter module through the event notification module that it needs to save data.

At first glance, it may seem that an extra step has been added, making implementation cumbersome; however, from the perspective of future functional expansion and decoupling, this is very necessary.

Event Notification Module

Header file definition

#ifndef _NOTIFY_H_
#define _NOTIFY_H_


#include <stdint.h>


/**
  * @brief Application module ID enumeration definition
  *
  */
typedef enum
{
    NOTIFY_ID_HMI = 0,   // Human-machine interaction module
    NOTIFY_ID_SYS_PARAM, // Parameter management module

    NOTIFY_ID_TOTAL
} NotifyId_e;

/**
  * @brief Event type enumeration definition
  *
  */
typedef enum
{
    NOTIFY_EVENT_PARAM_UPDATE,     // Parameter update event, corresponds to structure PrramUpdateInfo_t

    NOTIFY_EVENT_TOTAL
} NotifyEvent_e;


typedef struct
{
    uint16_t addr;
    uint32_t param;
}PrramUpdateInfo_t;


typedef int (*EventNotifyCB)(NotifyId_e id, NotifyEvent_e eEvent, const void *pData, uint32_t length);


extern void Notify_Init(void);

extern int Notify_Attach(NotifyId_e id, NotifyEvent_e eEvent, EventNotifyCB pfnCallback);
extern int Notify_Detach(NotifyId_e id, NotifyEvent_e eEvent);
extern int Notify_EventNotify(NotifyId_e id, NotifyEvent_e eEvent, const void *pData, uint32_t length);

#endif /* _NOTIFY_H_ */

Source file implementation

#include "notify.h"
#include <string.h>

static EventNotifyCB sg_pfnCallback[NOTIFY_ID_TOTAL][NOTIFY_EVENT_TOTAL];

/**
  * @brief      Event initialization
  *
  */
void Notify_Init(void)
{
    memset(sg_pfnCallback, 0, sizeof(sg_pfnCallback));
}

/**
  * @brief      Add event listener notification
  *
  * @param[in]  id          Application module ID
  * @param[in]  eEvent      Event
  * @param[in]  pfnCallback Callback function
  * @return     0, success; -1, failure
  */
int Notify_Attach(NotifyId_e id, NotifyEvent_e eEvent, EventNotifyCB pfnCallback)
{
    if (id >= 0 && id < NOTIFY_ID_TOTAL && eEvent < NOTIFY_EVENT_TOTAL)
    {
        sg_pfnCallback[id][eEvent] = pfnCallback;
        return 0;
    }

    return -1;
}

/**
  * @brief      Remove event listener notification
  *
  * @param[in]  id          Application module ID
  * @param[in]  eEvent      Event
  * @return     0, success; -1, failure
  */
int Notify_Detach(NotifyId_e id, NotifyEvent_e eEvent)
{
    if (id >= 0 && id < NOTIFY_ID_TOTAL && eEvent < NOTIFY_EVENT_TOTAL)
    {
        sg_pfnCallback[id][eEvent] = 0;
        return 0;
    }

    return -1;
}

/**
  * @brief      Event notification
  *
  * @param[in]  id          Application module ID
  * @param[in]  eEvent      Event type
  * @param[in]  pData       Message content
  * @param[in]  length      Message length
  * @return     0, success; -1, failure
  */
int Notify_EventNotify(NotifyId_e id, NotifyEvent_e eEvent, const void *pData, uint32_t length)
{
    int i;

    if (eEvent < NOTIFY_EVENT_TOTAL)
    {
        for (i = 0; i < NOTIFY_ID_TOTAL; i++)
        {
            if (sg_pfnCallback[i][eEvent] != 0)
            {
                sg_pfnCallback[i][eEvent](id, eEvent, pData, length);
            }
        }

        return 0;
    }

    return -1;
}

Parameter Application Layer Module

Example communication, acting as an observer to listen to messages for parameter saving.

#include "notify.h"

static int Param_OnNotifyProc(NotifyId_e id, NotifyEvent_e eEvent, const void *pData, uint32_t length);

void Param_Init(void)
{
    Notify_Attach(NOTIFY_ID_SYS_PARAM, NOTIFY_EVENT_PARAM_UPDATE, Param_OnNotifyProc);
}

// Event callback handling
int Param_OnNotifyProc(NotifyId_e id, NotifyEvent_e eEvent, const void *pData, uint32_t length)
{
    switch (eEvent)
    {
    case NOTIFY_EVENT_PARAM_UPDATE:
        {
            PrramUpdateInfo_t *pInfo = (PrramUpdateInfo_t *)pData;
            SaveParam(pInfo->addr, pInfo->param); // Save parameter
        }
        break;
    default:
        break;
    }

    return 0;
}

Human-Machine Interaction Application Layer Module

Example communication, acting as the observed object to notify/send messages for parameter saving.

#include "notify.h"

void Hmi_Init(void)
{

}

// Need to save parameters
int Hmi_SaveProc(void)
{
    ParamUpdateInfo_t info;

    info.addr = 5;
    info.param = 20;

    Notify_EventNotify(NOTIFY_ID_HMI, NOTIFY_EVENT_HMI_UPDATE, &info, sizeof(ParamUpdateInfo_t));
}

Leave a Comment

×