Embedded Command Line Interfaces and why you need them

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.

Requirements

Relying on our experience with closed source and internally developed CLIs, below are our prioritized recommendations for some key features.

Must Haves

Must Haves

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.

Communication Interface

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.

Bare Metal

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).

Desired

Must Haves

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

Language

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.

Function Pointers

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.

Unit Testing

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.

Static Allocation

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.

Minimal Files

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.

Built-in Help

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

Must Haves

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?”

Parameter Validation

Keep a user from providing bad arguments to the command line.

Multiple Instances

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.

Recently Updated

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.

Candidates

After researching online I found a handful of candidates to evaluate:

  1. Embedded-cli
    1. https://github.com/FARLY7/embedded-cli
  2. Memfault firmware shell
    1. https://github.com/memfault/interrupt/tree/master/example/firmware-shell/complex/shell
    2. https://interrupt.memfault.com/blog/firmware-shell
  3. Anchor shell
    1. https://github.com/rideskip/anchor/tree/master/console
  4. LwSHELL
    1. https://github.com/MaJerle/lwshell
    2. https://docs.majerle.eu/projects/lwshell/en/latest/
  5. embedded-cli
    1. https://github.com/funbiscuit/embedded-cli

Data

Each candidate shell was evaluated against the list of requirements. The data can be found in the table below.

Command Line Interface
NameEmbedded-cliMemfault firmware shellAnchor ShellLwShellEmbedded-cli
AuthorFARLY7MemfaultrideskipMaJerlefunbiscuit
LinkGithubMemfault.comGithubGithubGithub
FeaturesMust HavesOpen SourceYesYesYesYesYes
Permissive LicenseYesYesYesYesYes
Abstracted InterfaceYesYesYesYesYes
DesiredBackspaceYesYesYesYesYes
Static Allocation YesYesYesYesYes
C or C++CCCCC
Number of Files24442
Built-In HelpNoYesYesYesYes
Function PointersYesYesYesYesYes
Echo InputYesYesYesYesYes
Unit TestingNoNoYesNoYes
Nice to haveAutocompleteNoNoYesNoYes
Parameter Validation NoNoYesNoNo
HistoryNoNoYesNoYes
Recently UpdatedYesNoNoYesYes
Multiple InstancesYesYesNoNoYes
ProsVery SimpleVery configurable
Unit Testing
STM32 is a target platformCMAKE
ConsPossible bug where README and code do not agreeDoes not seem to be a standalone release, shell in blog post is part of a larger repoMost recent update was 2 years ago

Conclusion

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.

arduino-demo (1)

The embedded-cli by Funbiscuit ticks every box except for parameter validation but that was only a “nice to have” feature. We even discovered some additional features while integrating this into a template project. There is a hook function pointer that can be set up to be called whenever a command is about to be executed by the CLI library. It lets you print out the command and its arguments. What a great feature for debugging and logging!

Below is a table comparing the size of a project built before and after adding the CLI.

filenametextdatabssdechex
with_cli.elf40000664525209318416c00
without_cli.elf36048664509688768015680

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.

gif2_optimized

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!