Following the previous post on how to structure data in serial communication, this one describes an application specific approach, based on the concept of commands and built on top of the previous structure.
A typical architecture for embedded systems comprises a microcontroller being capable of executing multiple actions, such as reading values from sensors or performing some type of actuation. So, we can define those actions as a set of possible commands that other communication entity (for example, a computer) can send. Naturally, each commands needs an answer, sent by the microcontroller. To simplify the notation, we will call the microcontroller the slave and the other communication entity the master. Figure 1 illustrates this architecture.
In order for the microcontroller to know which task to execute, every command should have a unique identifier (ID). Assuming that no more than 256 commands are available, this ID can be represented in the message as a single byte.
Following the ID, the message needs to include any data needed for the execution of the command, which are the arguments for that command. For example, if we are giving a command to turn on a DC motor, one argument could be the desired speed. In some cases, there are no arguments, so the message can include only the ID byte. For example, if the master sends a command to get a measurement from a temperature sensor, the command probably doesn’t need any argument.
Integrating this idea in the structure described in the previous post, we get the structure illustrated in figure 2.
The grey bytes are part of the generic structure that can be applied for many different applications and are used just to build a generic message. So, they don’t contain any information needed for the execution of the commands and they don’t need to be stored or passed to the functions handling the execution of commands.
To keep things organized, the generic message building algorithm and the handling of commands should be clearly separated. Once the message is built and validated, using the generic algorithm, the data needed for the execution of the command (ID and arguments) is passed to a different function. This function will then check if the number of arguments is correct for the specific command ID and forward the data to the specific function that will handle the command. This separation of duties is illustrated in figure 3.
As can be seen by the diagram, this approach is very modular and escalates well. If we need to add a new command, we just create the handling function and add an entry point to the forwarding function (along with the new rule for validating the number of arguments).
After sending a message, the master needs to know if its execution was successful or failed and, if it applies, receive the data requested. Since a command message can be lost due to multiple factors (disconnection of the slave, errors in the communication channel, etc …), the slave needs to always return an answer. So, an absence of answer should not be used by the slave to indicate that it failed to execute the command, but rather include that information in the answer message.
So we can use the same message structure as before for the answer, allowing for a similar implementation in the master. We maintain the ID, in order to identify to which command the answer belongs, and include the data needed for the answer in the following bytes. This structure is illustrated in figure 4.
As stated before, the answer can have actual data, such as temperature readings from a sensor, or just an information stating if the command was executed. In the latter case, typical just one byte is needed. For example, if the command is successfully executed, the byte has the value 0. If the command fails, the byte has a value that indicates the type of error.
Of course this is just a basic structure that can be refined accordingly to the needs of the application. For example, the message can include a sequence number or timestamp, to guarantee the synchronization in the messages exchanged, or some kind of slave ID, if there are multiple slaves, amongst many other options.
It’s also important to take into account that the separation between message building and command processing functions may not be so clear. For example, if the checksum fails, a message should be sent to the master. Since this verification happens in the message building function, it may already need to have in consideration the application specific structure of the answer, to return the corresponding error. Other option may be shifting this verification to the command processing function and perform the validation when the number of arguments is verified.