During the course of development – from prototyping hardware through manufacturing – it becomes necessary to run test code over and over to verify functionality or perform system-level testing. This can often be done by stepping through code in the debugger or by repeatedly restarting the device to cause something to happen. This is a downside of incurring possible boot/initialization delays, requiring the use of a debugger, and the assumption that non-developers (i.e. manufacturers) will have the necessary know-how and tools. Embedded systems need a simple way to run commands repeatedly via a simple interface available during development and manufacture.
The solution is a command-line interface (CLI) that can provide the functionality to exercise various features and functions, without the need for debuggers or software know-how. Testing can be run from the CLI using the communication interface most convenient to the situation, such as UART, USB, RTT, or Bluetooth.
While it’s possible to write a custom CLI, there are several open-source solutions available. We decided to first look at open source solutions to see if something could fit our needs exactly or with minimal modifications before embarking on an internal project. The first step in implementing this solution is to review the available open-source CLI solutions and compare their features. We can then settle on one standard solution to implement in our template projects for wide-scale use.
Relying on our experience with closed source and internally developed CLIs, below are our prioritized recommendations for some key features.
Open Source with a permissive license
For our use case of including this code in internal and customer projects, we needed something we could easily redistribute, meaning no licensing fees, which rules out closed-source CLIs. So we looked for open-source projects with a permissive license, i.e. an MIT License. For some information about why this can be an issue, see this helpful Embedded.fm blog post about how arduino code is licensed.
The CLI must abstract away the input and output functions so that they are agnostic to the underlying transmission protocol/medium. Our customers use a variety of USB, ethernet, Bluetooth, UART, Segger RTT, and many other protocols to communicate with their products. Ideally, we would have a CLI we could make available on any interface that can transfer bytes bidirectionally. It might even give you the ability to do something where inputs and outputs come from and go to completely different interfaces.
The CLI should be abstracted away from any RTOS or other dependencies. While this may require more work to integrate into the system (e.g. adding a processing thread), not all of our projects have an RTOS. This primarily rules out using the FreeRTOS or Zephyr built-in command-line interfaces; as nice as the Zephyr CLI may be, we are not running Zephyr everywhere (yet).
There are many features I desire in a CLI, and working in zsh every day makes it hard to give up my fancy autocomplete, history, and fuzzy find. That is probably asking too much of an embedded system anyhow, so we’ll have to make some compromises.
Basic Shell Functionality
Echo - anything you type in is printed back to the screen so you can see what you’re doing.
Backspace - Echo lets you see the mistake but supporting backspace lets you fix it.
Help messages - a quick reference on what is available and how it works
We are developing for embedded systems and mostly target C, so a C (or maybe C++) implementation is preferred. Additionally, a small library, adding only one or two files to the project, is ideal so we’ll look for implementations that are one c source and one header file.
While being transport layer agnostic is a must-have the implementation of that as passing function pointers to the library module is just a desired feature. There are many ways to allow a developer to provide data to your library but function pointers enforce an agreed upon interface, enforce one entry/exit point for data, and allow the user of the library great flexibility in implementing that function. For that reason, we’ll look for libraries that use function pointers.
Here at Dojo Five, we’re always trying to do more and better unit testing, so finding a CLI that has already implemented its own unit tests would save us the time of starting from scratch and give some confidence in the stability of the library.
Generally, a best practice in an embedded system is to avoid dynamic memory allocation, so we’ll look for libraries that use static memory allocation.
Some people can do ingenious (and crazy) things with a single header file, but no CLIs that I found were header-only implementations; but that’s ok, a two file implementation is just as good. Two files simplify inclusion into projects.
Treating help information as an integral part of the CLI makes it easier for those unfamiliar with the system to start interacting with it. Native support for help messages and printing available functions is a plus.
Runtime Command Registration
Ideally, your CLI implementation wouldn’t need to include every header file to gain access to the functions to call, but with compile-time command registration, this is often required. Runtime command registration is where the CLI is made aware of each command at runtime via function calls to set up the command. This enables individual modules to add commands to the CLI during their own initialization by calling the appropriate function from the CLI library. Instead of the CLI including all header files, each module that wants to add commands to the CLI includes the CLI header. This also allows runtime reconfigurable CLIs for more advanced use cases.
Say you have an advanced or manufacturing mode for the CLI – you can dynamically add those functions at runtime in response to a variety of inputs. Perhaps pogo pins connect a net to ground or maybe a password is required to add additional functionality. CLI libraries with runtime command registration make this feature possible without rebuilding the code and having extensive compile time #IFDEFS around which commands are available.
Nice to Have
Advanced Shell Functionality
Autocomplete - Really more of an extravagance on an embedded system, but sometimes it is nice to be able to type a few characters followed by a tab to see what is available.
History - Easily rerun commands that you’ve already typed out without needing to type them again – or even just see what you’ve run without scrolling up. “Did I assert or deassert that pin?”
Keep a user from providing bad arguments to the command line.
There could be a use case where you might want to serve a CLI over multiple interfaces. If you want to make them independent you would need to have multiple instances of the CLI, which requires that the CLI was written without using global state or allocations that prevent multiple instances of the CLI.
Preferably we will use a project with recent activity so that we can either share improvements back to the project or get help ourselves with any issues. While this isn’t a property of the quality of the CLI itself, not using an abandoned project has benefits.
After researching online I found a handful of candidates to evaluate:
- Memfault firmware shell
- Anchor shell
Each candidate shell was evaluated against the list of requirements. The data can be found in the table below.
|Command Line Interface|
|Name||Embedded-cli||Memfault firmware shell||Anchor Shell||LwShell||Embedded-cli|
|Features||Must Haves||Open Source||Yes||Yes||Yes||Yes||Yes|
|C or C++||C||C||C||C||C|
|Number of Files||2||4||4||4||2|
|Nice to have||Autocomplete||No||No||Yes||No||Yes|
|Pros||Very Simple||Very configurable|
|STM32 is a target platform||CMAKE|
|Cons||Possible bug where README and code do not agree||Does not seem to be a standalone release, shell in blog post is part of a larger repo||Most recent update was 2 years ago|
Based on the analysis we plan to use Funbiscuit’s embedded-cli, the gif included in the repo speaks volumes so we include it here.
Some of that will be the FreeRTOS task that was added to interface with the CLI library. Some optimization could be done to trim buffers and the task’s stack size, but ultimately the increase is worth the benefits – and if the CLI is not needed in production, it can be easily removed.
The Funbiscuit embedded-cli project includes everything we were looking for, and should make a great addition to our projects.
DojoFive brings modern tools, techniques, and best practices from the web and mobile development environments, paired with leading-edge innovations in firmware to our customers to help them build successful products and successful clients. We have talented engineers on hand ready to help you with all aspects of your EmbedOps journey. Bring your interesting problems that need solving - we are always happy to help out. You can reach out at any time on LinkedIn or through email!