5 steps to design an embedded software architecture, step 4

This article explores the fourth step in the design of an embedded software architecture: the definition of task components and their interfaces.

The last three articles have explored the five stages of designing an embedded software architecture. So far, we have separated our software architecture into hardware-dependent and hardware-independent architectures. Additionally, we explored how identifying our data assets early can help us improve device security and support natural architectural decomposition.

The five stages of designing an embedded software architecture that we examine in this series include:

This article explores the fourth step in designing an embedded software architecture: interface and component design.

You are here… A brief overview

In the last article, we explored how to break down a system whose data assets we had identified into domains and tasks. You may recall that the previous diagram we created separated our application into secure and insecure application domains, hardware-independent and hardware-dependent domains (separated by the abstraction layer), and then finally into tasks. The breakdown of our system looked like Figure 1.

Click for full size image

Figure 1: An example of on-board motor control system task decomposition. (Source: Embedded Beningo Group)

Due to the space and time constraints of the blog format, we note that there are additional steps in the architecture process that describe how to make the tasks interact. We want to explore how we can take one of our tasks and design our interfaces and components. Let’s see how to define the components and their interfaces for the motor task.

Step #4 – Interface and Component Design

Component and interface design are closely related activities. In fact, if we look at the standard definition of a component, we will find the following description:

A software component is a unit of composition with contractually specified interfaces and explicit context dependencies.

You can’t even define a component without talking about interfaces! Interfaces are what software developers use to interact with a component. Therefore, it makes sense that we start breaking down our motor task into components, and then we can define the interfaces for each component.

Definition of software components

Embedded software developers can use many different methods and techniques to identify which components will stack together to perform the desired behavior provided by a task. My preference for almost any task is to break the system down into a layered software diagram that starts at low-level drivers and works its way up to application code. Figure 2 shows an example of what such a diagram might look like for the motor task.

An example of component stacking
Figure 2: Example of component stacking. (Source: Beningo Embedded Group)

The purpose of each of these components is probably self-explanatory, but just in case, let me define what each does:

pwm_drv – hardware device driver for pulse width modulation available on the microcontroller.

Abstraction layer – breaks the dependency between the engine driver and the hardware. This allows us to use a PWM driver to drive the motor or replace the pwm_drv with a driver for an external IC.

motor_drv – driver designed to control the motor. It has no hardware dependencies. Its only dependency is the abstraction layer.

engine_sm – state machine that tracks the current state of the engine and the desired state.

motor_app – application-specific support functions. These can include features such as telemetry collection, fault finding, etc.

task_engine – the component that contains the actual motor task code. The component has dependencies on other engine components. The motor task can include RTOS application interactions such as semaphores, queues, mutexes, and event logic.

Together, these components have all the behaviors necessary to receive a command that modifies the state machine that controls the motor, and then operates it. The advantage of this breakdown is that everything is self-contained if aspects of the motor control chain change, such as a new driver chip, new application requirement, etc. With the proper interfaces, minimal change is required.

Definition of interfaces

For the motor task, there are two categories of interfaces that must be defined. First, there is the task interface which would receive commands from other application components telling it what the motor should do, such as MOTOR_ON or MOTOR_OFF. Second, there are interfaces for each component. Let’s look at these one at a time.

Motor task interface design

When defining the communication interface with the motor task, it is useful to go back to the data assets that we illustrated in Figure 1. We can see that the controller task interacts with the motor task. For the controller task to tell the motor task what the motor should do, several pieces of information are needed:

  • Engine condition
  • motor direction
  • Motor speed

We could even include an engine ID if we want the system to be scalable to handle multiple engines. For example, there may be applications where a state machine controls the engine through messages, but a user can override the state with a message. In these situations, we might even want to include a task ID so that the motor task knows which task is requesting motor control.

From the data interface point of view, I can define a structure for the data that will be used to interface the control and motor tasks. I could code this in C to be something like:

typedef struct
    MotorID_t              ID;
    MotorState_t         State;
    MotorDirection_t  Direction;
    MotorSpeed_t       Speed
} MotorMessage_t;

The MotorMessage_t structure defines the data interface needed to command the motor task to perform practical work. How we convey this information to the motor task varies depending on the needs of the project. For example, I can use a message queue, a data buffer, or some other mechanism. These are the details that the programmer must decide, not the software architect.

Define component interfaces

There are several ways to design the interface of our components. First, we can take a sticky note and list the functions we think we need. Then we could use a UML diagramming tool and take advantage of the class diagram, even if we’re not writing object-oriented code. Finally, we could jump into the code and start writing tests that would require us to develop the interface for the component’s behavior. In all honesty, I often use a combination of the three. I start with a simple napkin list, write it in UML, then when I’m ready to develop the component, I use the UML diagram to guide my testing, evolving my interface design.

Class diagrams are great because they allow us to specify attributes, operations, and the relationship between classes (modules and components can also work). For example, see Figure 3, which shows an initial design of the motor task components. There are several things we should notice. First, Motor Task uses the Motor Application, Motor State Machine, and Motor Drivers. There are no inheritance relationships here. We can see that the engine driver has an engine interface. The engine application uses a MotorError_t enumeration, but other than that the components are decoupled and don’t interact.

Then we defined what we think are the operations and functions or methods that our components need. For example, it can be seen that the motor driver has two functions: motor initialization and motor control. Motor control takes a motor message type with all the information to control the motor, which is done through the motor interface (abstraction layer).

Finally, we can see the “extras” that support these interfaces. I included a MotorError_t enum used by the motor application. In all honesty, there are several enumerations that I should define, but I left them out to prevent the diagram from becoming too complex.

Click for full size image
Example of first-pass interface design using class diagrams

Figure 3: Example of first-pass interface design using class diagrams. (Source: Beningo Embedded Group)

When we look at the class diagram, we can see that the motor task interacts with the lower level components and determines the ultimate behavior of the motor. First, information enters the motor task via the MotorQ; then the motor task executes the motor state machine. Finally, the motor task calls the motor application for errors and uses the result with status to control the motor through the motor driver. That’s a lot to glean from a class diagram that probably isn’t obvious. So, is there something missing from our architecture?

What is missing from our design?

We probably have enough to start implementing engine application code; however, more diagrams and more time could significantly improve clarity. For example, I might consider additional UML diagrams, like a sequence diagram, to show the timing requirements for the control task to interact with the motor task. I could also create a sequence diagram so the developers understand how the motor task interacts with the other components. For example, should the motor task command the motor at the end of the task, or is it performed first to minimize jitter? What is the logic to manage in case of error? Should the engine continue to run or be stopped?

You’ll typically find that you need at least three to four UML diagrams to fully specify a task so developers can take it and code it. We’ve only looked at a few, which means there’s still a lot to do. Don’t get me wrong, with what we have; we could start developing tests and use test-driven development to further evolve our interfaces and understanding of the system. This may be the way to go right now. But, we know enough to get started, and as we implement the details, questions will arise that will require changes to the initial architecture.

Software Architecture Design Step #4 Conclusions

As seen in this article, we can take advantage of UML diagrams and component stacking diagrams to identify components and define their interfaces. Additionally, class diagrams can be a great tool for designers when designing interfaces. We must remember that although we follow a simple five-step process to develop our architectures, these steps are only a guide and not the whole story. As architects, we often have to dig deeper and explore the peripheries of what we have discussed so far to fill in the gaps and details.

In the next article, we’ll explore the last step: simulate, iterate, and scale.

Related Content:

For more embedded, subscribe to Embedded’s weekly newsletter.