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).
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:
Callback
is an array of pointer variables, where each array member is a function pointer variable. The function addresses of the application layer code functionsOnSaveParam(...)
andOnUpdateParam(...)
are obtained through the functionNotify_Attach
. The human-machine interaction module then callsNotify_EventNotify
, which in turn callsCallback
. The calling method is slightly different from directly callingOnFunction(...)
because it is an array, so it requires[]
to obtain the function address. To ensure the system runs safely, it is necessary to ensureCallback[i]
is notNULL
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:
Avoid mutual calls between modules, achieving decoupling.
Even if one observer module is removed, the observed object or other observer code does not need to be modified, ensuring system stability.
Adding a new observer module does not require modifying the observed object code, ensuring system stability.
Of course, this method also has drawbacks:
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.
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:
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).
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));
}