It is increasingly common for modern electronic designs to incorporate some level of computational processing capability. A driving factor behind this trend is the expectation for products to operate intelligently, connect to the Internet, operate with mobile devices and implement sophisticated user interfaces. Even when such features are not required, it is often more cost effective and energy efficient to implement the majority of a product’s functionality in software rather than hardware.
Typically, the means by which a hardware design gains computational capability is via the inclusion of a component called a microcontroller. This is essentially a complete computer, incorporating CPU, memory and input/output interfaces on a single chip, sometimes referred to as a system-on-chip (SoC). A program that runs on a microcontroller is referred to as firmware – software tightly coupled to the hardware it runs on.
Many of the designs we have helped clients bring to market required supporting software to be developed including apps for interacting with products via iOS and Android mobile devices, backend services for Internet-of-Things (IoT) devices and Windows/Linux/Web applications for control and data visualization.
In addition to the experience and knowledge we have accrued through developing firmware and software for a wide range of industries, we have invested time in implementing a number of code libraries and systems in-house to enable us to develop code for new applications quickly and efficiently.
We develop firmware primarily in C targeted at ARM Cortex-M (STM32, nrf51/52, SAM), AVR and PIC microcontroller architectures. Typically we build firmware using GNU GCC and CMake but adapt easily to other toolchains and architectures when a project mandates doing so.
All firmware and software we develop is version controlled using Git and maintained in repositories via Atlassian BitBucket. This enables us to track changes made to source code throughout its lifecycle and assists in the management of firmware/software deployment.
Most software and firmware developments involve writing many thousands of lines of code, so it is important that the code is well structured and documented to facilitate its long term maintenance. Beyond documenting the features and behaviours required of a piece of firmware/software, at the specification stage of a project, we make extensive use of inline documentation (using tools such as DOxygen and Javadoc) to ensure every module and every function we write is accompanied by a description of what it does.
To ensure the software and firmware we develop functions as expected and continues to do so as new features are added or existing features are modified, we make use of a number of regression testing techniques. For software testing, we use various well established unit testing, and integration testing frameworks (JUnit, Mockito, XCTest, Mocha, Google Test) depending on the programming language being used for a particular project and the target platform.
It is fairly typical for us to begin developing firmware for a product before a prototype of the hardware it is to run on exists. We use abstraction and emulation frameworks, developed in-house, to allow us to run firmware tests and simulate the external stimuli to which the firmware must react without us having hardware to hand. Unit testing and early integration testing are performed using this method. Once we have prototype hardware for a project, we are able to run these same tests on the target hardware. In both cases, testing is automated using Expect and various utility programs developed in-house.
As part of our deployment strategy we use Atlassian Bamboo, integrated with BitBucket to enforce how and when a piece of firmware or software can be released for production. Whenever we commit new or modified code to a repository held by BitBucket, Bamboo automatically builds and tests the code, generating a report to indicate the success or failure of any tests. Rules are set up so that it is only possible to mark a version of code for release once all of its associated tests pass. To ensure the effectiveness of this strategy, we strive to ensure the tests we develop for a project achieve a high level functionality coverage.