Developing software for embedded devices like microcontrollers usually requires a different mindset than developing for desktop or web applications. For embedded systems, there is a higher concern on latency, determinism, power usage, and reliability than in other domains. It can be particularly important that an embedded software developer has some experience in electrical circuit design since they code they are developing is so intimately related to the circuit it is manipulating. On the other hand, embedded software is often simpler and more self-contained than the complex, multi-computing environment of web & cloud.
Process
Our process and mindset for software development follows the V-Model in most cases when the requirements of the system can be fairly well defined. The exception is in cases where the client is unsure of how the final product should behave. In this case, a shorter-cycle Agile-like process can be more applicable. The typical V-Model applied to embedded software development is outlined below (blue links show verification steps):
In the Software architecture design, we develop the high-level requirements of what the product should do. If we were also part of the PCB design, then the software requirements may be implicit in the overall system requirements. Once high-level requirements are agreed upon, we develop a high-level software architecture which will meet the system requirements while also employing the tenants of modularity, maintainability, and extensibility. We organize the required code into a hierarchical block diagram and each block has a certain ownership of functionality. Additionally, required third-party libraries are identified that will be needed by the project as well as any low-level drivers or utilities that need to be developed.
Using the software architecture, we can begin Header file & API development. Essentially, in C or C++, a documented header file provides a comprehensive Application Programmer Interface (API) which defines how other code blocks should interact with that module. In languages like Python, there are no header files, but we can still start with a documented file with stubbed-out functions. The three aspects that need to be described for each module are Inputs, Outputs, and Behavior. If it is difficult to describe these three things concisely, then it indicates the need to refactor the architecture and modularize the design better.
Once the interfaces are designed, we can begin implementation of functionality for each function and code module. Essentially, this is the code needed to implement the behavior aspect according to the documented specification. In C or C++ this step amounts to writing the .c or .cpp file for a module.
The lowest level of verification is unit testing where the behavior of a code module is tested to ensure that it behaves according the header file or API specification. Unit testing is a test at the lowest level of abstraction and most granular functionality. An example is testing that a file system class can open, write to, and close a file. A typical unit test would stimulate a piece of code with an input, then check the code’s output against some expected value. We typically like to test a few typical inputs as well as any corner cases we can think of. The unit testing framework is ideal for isolating bugs to small areas of code so they are easier to find, however these tests will not catch bugs that arise from multiple interacting modules.
The next level of verification is integration testing which tests multiple code modules interacting with each other. For instance, an integration test could be connecting to a server, uploading a file, then checking it was received on the other end. The purpose of these tests are to catch errors in the API specification itself and issues that arise in an integrated system, like timing and multithreading issues.
The final testing step is system testing which verifies the entire system against the high-level requirements. This is an important last step to ensure that the system has the specified performance level expected by the client. However, system tests give the least clarity on the source of any bugs or issues that arise since the whole codebase is being tested at once. If a bug does arise in system testing that was not caught in previous rounds of testing, it means we need to have greater coverage with the earlier testing rounds.
Developing software can quickly become disorganized and unmaintainable if not designed and executed in a proper manner. The V-model described is one way to ensure code always ties back to high-level requirements and that bugs or inconsistencies are quickly identified. If you would like our help in implementing or advising on your next embedded software solution please Contact Us.