Embedded software development is a challenging task. Sometimes, dues to legacy code, it becomes very difficult to maintain it and it often breaks in some unexpected ways. Many times, changes done in one part of code causes problem in some another part. And as we move on, we find that the biggest challenges is dealing with same set of problems repeatedly.
Often, the root cause of these type of problems is the lack of clear architectural guidelines and not using any specific design patterns. It is also observed that if we follow suitable design pattern and enforce the coding guideline to the developers then the code that gets produced is much clean, easy to maintain and easy to test.
Coding guidelines is easy to follow but understanding the design pattern takes time and effort. But using proper Design pattern we can solve the recurring problem in firmware development. They are not prescriptive but rather they provide a template for solving any particular problem.
There are many powerful design patterns for embedded firmware programming that can help developers write cleaner, more maintainable code. Let’s us discuss some of them.
Creational patterns are a type of design patterns that mainly focus on creating objects in efficient and flexible manner. The creational patterns focus on encapsulation by hiding how the instances of classes is created. So, in simple words we can say that these patterns help developers create objects in a way that is decoupled from the specific classes that are being instantiated in such a way that it always more flexibility and scalability. By using the creational patterns, we can increase the flexibility of the embedded system in terms of what, who, how, and when an object should be created. Some of the most common creational patterns are:
- Singleton Pattern ensures that only one object of a certain type is created. This is useful for cases where only one instance of an object is needed. Global configuration is best example of this.
- Factory Method Pattern allows you to create objects without specifying their class. This provides greater flexibility in the creation process because the exact class of an object can be determined at runtime.
- Abstract Factory Method is similar to the Factory Method Pattern, but it provides an interface for creating families of related objects. This allows for greater flexibility while creating object hierarchies. This pattern is useful while creating a complex system.
- Builder Pattern is a way to create complex objects step by step. So, instead of creating an object all at one time. The Builder Pattern breaks the creation of the process into smaller and more manageable steps. This type of pattern is useful for complex objects that will have many different configurations.
- Object Pool Pattern is a way to reuse objects that are costly to create. So, instead of creating a new object each time when it is needed, the Object Pool Pattern stores a group of previously created objects. These created objects are reused when needed. This results into improved performance & reduced cost of creation of objects.
- Prototype Pattern is a way to create new objects by copying existing objects. This can be useful when creating objects that are similar in structure but have different attribute values. So, instead of creating a new object from scratch, this Pattern allows you to create a new object by copying an existing object. Then this newly created object is modified with its own attributes whenever needed.
Structural pattern is a type of design patterns that helps you in organizing the code in a way that is easier to understand and maintain. They provide solutions to common problems that developers face when working with complex software systems. For example, some structural patterns help manage relationships between objects, while others help define hierarchies of objects. By using these patterns, developers can make their code more organized, easier to understand, and easier to modify in the future.
- Adapter Pattern: Imagine a situation where you have two devices that work differently, but you need them to work together. In such a case, the adapter pattern helps you solve it by creating an intermediary object which can translate between these two devices by allowing them to communicate and work together.
- Bridge Pattern: This pattern helps you to separate the abstraction of something from its implementation. This can help you to make your code more flexible.
- Composite Pattern: The composite pattern allows you to treat groups of objects as a single object. This can be useful when you have a collection of objects that you want to manipulate as if they were a single object.
- Decorator Pattern allows you to add new functionality to any object without changing its structure. This is useful when you want to add new features to an object, but you don’t want to modify its existing functionality.
- Facade pattern provides a simplified interface to a complex system. This can be useful when you have a system that is difficult to use or understand but you want to make it easier for the users who will interact with it.
- Flyweight pattern is used to reduce memory usage by sharing objects that are used frequently. This can help improve performance and reduce the overall memory footprint of your system.
- Proxy pattern provides you a way to control the access to an object. This can be useful when you want to limit who can access an object or when you want to add additional functionality to an object without changing its core functionality.
Behavioral patterns are design patterns that help developers manage communication and collaboration between different objects. They provide solutions to common problems that arises when objects need to work together to achieve any specific goal. For example, it helps you to define how objects communicate with one another, how to manage complex sequences of interactions etc.
- Observer Pattern is also known as “Sub/Pub Pattern”. It is a design pattern that allows objects to observe changes in other objects and take action based on those changes. This is useful when you want to notify one object about the changes in another object.
- Null Object Pattern is a design pattern that provides a default object which can be used in place of null. This is useful when you want to avoid null pointer exceptions in your code.
- Return Value Pattern is a design pattern that involves returning a value from a method or function. This is valuable in C/C++ because return values in C/C++ are the primary way of signal status of an operation.
- State pattern is a design pattern that allows an object to change its behavior based on its internal state. This is useful when you want to create objects that behave differently based on their current state.
- Visitor pattern is a design pattern that allows you to separate an algorithm from the objects that it operates on. This is useful when you want to perform operations on a collection of objects without modifying the objects themselves.
- Command pattern is a design pattern that allows you to encapsulate a request as an object. This is useful when you want to queue up requests or undo/redo requests in your code.
- Mediator Pattern is a design pattern that helps to manage the communication between objects in a system. This is useful when you have a complex system with many objects that need to communicate with one another.
Concurrency patterns are design patterns that help developers manage code that is executed simultaneously by multiple threads. These patterns provide solutions to common problems that arise when multiple threads need to access the same memory/structure/resources.
- Spinlock: A spinlock is a concurrency pattern that repeatedly polls a shared resource until it becomes available. This is useful when you have multiple threads that need to access a shared resource, but you want to avoid the overhead of context switching.
- Semaphore: A semaphore is a concurrency pattern that limits the number of threads that can access a shared resource at any given time. This is useful when you have a limited number of resources and you want to avoid race conditions.
- Mutual Exclusion: Mutual exclusion is a concurrency pattern that ensures that only one thread can access a shared resource at a time. This is useful when you want to avoid race conditions and ensure that multiple threads don’t access the same resource simultaneously.
- Conditional Variable: A conditional variable is a concurrency pattern that allows threads to wait for a specific condition to occur before continuing. This is useful when you have multiple threads that need to coordinate their activities.
- Monitor Pattern: The monitor pattern is a concurrency pattern that combines mutual exclusion and conditional variables to manage shared resources. This is useful when you want to ensure that only one thread can access a shared resource at a time, and that other threads wait until the resource is available.
- Readers-Writer Locks: Readers-writer locks are a concurrency pattern that allows multiple threads to read a shared resource simultaneously, but only one thread to write to the resource at a time. This is useful when you have a large amount of data that needs to be read by multiple threads.
- Thread-Local Storage: Thread-local storage is a concurrency pattern that allows each thread to have its own copy of a shared resource. This is useful when you have data that needs to be accessed by multiple threads, but you want to avoid race conditions.
- Active Objects: Active objects are a concurrency pattern that allows objects to encapsulate their own thread of execution. This is useful when you want to create objects that can execute in the background without blocking the main thread.
- Thread Pools: Thread pools are a concurrency pattern that manages a pool of threads that can be used to execute tasks in parallel. This is useful when you have a large number of tasks that need to be executed, but you don’t want to create a new thread for each task.
Architectural patterns are high-level design patterns that provide a framework for organizing and designing software systems. It can be thought of as blueprints or templates that describe the fundamental organization of a system. They define the components and their interactions, as well as the relationships between the components. These patterns help architects and developers create systems that are modular, flexible, and easy to understand.
- MVC (Model-View-Controller): A design pattern that separates the code of an application into three interconnected components: the model (for data and logic), the view (for the user interface), and the controller (for managing the flow between model and view).
- Layered Pattern: A design pattern that divides an application into layers. Each layer performs a specific function or set of functions. The layers are organized in a hierarchy, with each layer building upon the layer below it. This pattern helps developers create applications that are easy to understand, maintain, and modify.
- Microservices: A design pattern where an application is built as a collection of small, independent services that communicate with each other through APIs. Each service performs a specific function and can be developed and deployed independently. This pattern helps developers create scalable, flexible, and easily maintainable applications. Microservices are mainly used for web-based applications but in recent times it is getting accelerating in embedded systems also.
- Server-Client: A design pattern where an application is split into two parts: the server (which provides resources and services) and the client (which requests and uses those resources and services). The server and client communicate with each other over a network.
- Event-Driven: A design pattern where the flow of an application is controlled by events. When an event occurs, the appropriate code is executed. This pattern is often used in user interfaces, where events (such as button clicks or key presses) trigger actions (such as displaying a message or performing a calculation).
Embedded system specific patterns
Embedded System Specific Patterns are design patterns that are specific to embedded systems, which are specialized computer systems designed to perform a single or a limited set of functions. Some of them are explained below:
- Memory Management Patterns are used to optimize the usage of memory in an application. Memory Pool creates a pre-allocated pool of memory that can be used by an application to store and retrieve data, Dynamic Memory Allocation allows for memory allocation at runtime, and Linked List is a data structure that can be used to store and manage data.
- Input/Output Management Patterns help manage the inputs and outputs of an application. Serial Communication is a way for two devices to exchange data over a serial port, GPIO Interfacing is a way to connect a device to the GPIO pins of a microcontroller, and ADC/DAC Sampling is a way to convert analog signals to digital signals.
- Timing and Scheduling Patterns help to manage the timing and scheduling of tasks in an application. Round Robin Scheduling allows for each task to be executed in a round-robin fashion, Timer Interrupts are used to trigger an interrupt when a certain time has elapsed, and Time-Triggered Systems schedule tasks to be executed at specific times.
- Interrupt Management Patterns are used to manage interrupts in an application. Interrupt Service Routine is a function that is executed when an interrupt occurs, Interrupt Latency Reduction is a technique to reduce the time between when an interrupt occurs and when the interrupt service routine starts, and Interrupt-Driven Event Loop is a way to handle interrupts by using an event loop.
- Power Management Patterns are used to manage the power usage of an application. Power Modes are different states in which an application can be put to conserve power, Power-Aware Scheduling is a technique used to schedule tasks to run during low power modes, and Low-Power Design Techniques are used to minimize power consumption in an application.
- Communication Patterns help manage the communication between different parts of an application. Message Queue is a way to pass messages between different parts of an application, Publish-Subscribe Pattern is used to notify interested parties when an event occurs, and Remote Procedure Call is a way for different parts of an application to call functions on another part of the application.
- State Management Patterns help manage the state of an application. State Machines are used to model the behavior of an application, State-Event Tables are used to specify the transitions between states in a state machine, and State Transition Diagrams are used to visualize the transitions between states in a state machine.
- Singleton Design Pattern
- Builder Design Pattern
- Factory & Abstract Factory Method Design Pattern