Throughout the development process, from hardware prototyping to manufacturing, the need to test code repeatedly arises. This is crucial for verifying functionality and performing system-level testing.
However, relying on debuggers or restarting the device to trigger events can lead to delays during boot-up and initialization. Moreover, it assumes that manufacturers and non-developers possess the necessary knowledge and tools to navigate these complexities. Thus, it becomes essential for embedded systems to include a straightforward method for executing commands repeatedly through an accessible interface during development and manufacturing.
Enter the solution: a command-line interface (CLI). This tool empowers users to exercise various features and functions without requiring expertise in debugging or software intricacies. Testing can be efficiently conducted via the CLI using the most suitable communication interface for the scenario, such as UART, USB, RTT, or Bluetooth.
While custom CLI implementations are certainly an option, a wealth of open-source solutions is available that can often meet your needs with minimal modification. Before embarking on a project, it’s worth reviewing available open-source CLI solutions to find one that fits your requirements. Here, we outline the key features to consider when choosing or implementing a CLI for embedded systems.

Requirements
Based on experience with closed-source and internally developed CLIs, here are the key features to prioritize.
Must Haves
| Feature | Description |
| Open Source with Permissive License | The solution should be easily redistributable, ideally under the MIT license. |
| Communication Interface Agnostic | The CLI should abstract input/output, supporting protocols such as UART, USB, and Bluetooth. |
| Bare Metal Support | The CLI should not rely on any RTOS or other dependencies. |
| Basic Shell Functionality | Includes echoing input, backspace support, and help messages. |
| C or C++ Implementation | Preferably a small, lightweight library in C or C++, with minimal files. |
| Static Memory Allocation | Avoids dynamic allocation, a best practice for embedded systems. |
Nice to Have
| Feature | Description |
| Autocomplete and History | Allows easier interaction through tab completion and command history recall. |
| Parameter Validation | Ensures correct command arguments, improving usability. |
| Multiple Instances Support | Enables serving multiple CLIs over different interfaces or protocols. |
| Built-in Help | Native support for printing help messages and available commands. |
| Runtime Command Registration | Commands can be registered at runtime, enabling dynamic updates. |
Additional Considerations
Security Considerations
Securing embedded CLIs is crucial, especially in production environments. Best practices include implementing user authentication, encrypting communication interfaces, and restricting access to sensitive commands. For example, you can secure CLI access via password protection or encryption keys, ensuring only authorized users can interact with the system. A CLI secured with a password protects the system from unauthorized tampering during manufacturing or field use.
Performance Impact
When integrating a CLI into resource-constrained embedded systems, large command sets and strings, for instance, can increase the footprint and slow down system response times. Reducing string sizes by using a more compact and manual format can help mitigate this. Keeping the CLI lightweight and trimming unnecessary features will also minimize the impact on system resources. Another approach is to have separate applications for manufacturing and production. The manufacturing application has CLI, while the production application does not have access to the CLI.
Tip: Slimming down shell commands and minimizing string processing overhead will help keep your system responsive.
Interchangeable CLI Output Formats
A flexible CLI should support both human-readable and machine-readable formats, making it suitable for automated testing and interaction. This feature lets you toggle between output formats depending on the situation. For example, configuring the CLI to output structured data such as JSON or XML for machines while retaining plain text for human operators.
USB-CDC Support Example
A practical example would be adding USB-CDC support to your system, enabling two separate USB-CDC classes for different CLI instances: one for human operators and one for automated systems. This setup allows different users or processes to interact with the CLI simultaneously over the same hardware interface.
Hardware-Enabled CLI
In some systems, you can control when the CLI is available. For instance, external hardware could be used to determine whether the CLI is enabled at boot. If the hardware (such as a specific jumper) is absent, the CLI module would never load. This method ensures that the CLI isn’t exposed unnecessarily during operation, adding another layer of security.
Binary Protocols as an Alternative
In some cases, the full functionality of a CLI may not be necessary, or the nature of the deployment might not permit its use. A binary protocol could be a viable alternative for applications that only need basic interaction with the device (e.g., for simple commands or status updates). This reduces the code size and execution overhead while still allowing communication.
Reduced Functionality CLI
Sometimes, a lightweight, reduced functionality CLI is preferable. For example, a CLI that only exposes essential commands and omits advanced features (such as autocomplete and history) might be appropriate for a system with strict resource constraints.
Multiple CLI Instances
Some applications may require multiple CLIs or separate instances of a CLI. For example, you may need one CLI for handling RX and TX over different protocols, or need to create a distinction between userland and kernel interactions. In such cases, a system with support for multiple shells or CLI instances, each managing its state, would be beneficial.
By considering these additional aspects, you can create a flexible and powerful CLI that suits both your development needs and deployment scenarios, ensuring that testing, manufacturing, and troubleshooting are smooth and efficient.
Open-Source CLI Library Example
Based on the analysis above, our team found an open-source CLI library called embedded-cli. Check out the embedded-cli GitHub repository. It ticks every box except for parameter validation, but that was only a “nice to have” feature. We even discovered some additional features while integrating it 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.
| filename | text | data | bss | dec | hex |
| with_cli.elf | 40000 | 664 | 52520 | 93184 | 16c00 |
| without_cli.elf | 36048 | 664 | 50968 | 87680 | 15680 |
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 of the binary size is worth the benefits, and if the CLI is not needed in production, it can be easily removed. The Funbiscuit’s embedded-cli project includes everything we were looking for and should make a great addition to our projects.
Why Use a CLI?
Initial Development
In the initial phase of an embedded project, it often starts with driver development. Developers can verify the implementation of the drivers by interacting with the hardware using the CLI.
Board Bring-up
There will be multiple revisions of boards throughout an embedded project. The firmware team can provide the hardware team with a set of CLIs to run through for verifying the functionality of a new board. By following this procedure, the hardware team can have higher confidence before distributing a new board to other departments for development.
Testing on Manufacturing Line
The same set of CLIs can also be used on the manufacturing line to verify new boards and capture board defects as early as possible. The testers on the manufacturing line can test various features and functions without using debuggers or deep software knowledge.
Automated Testing
By integrating an embedded project with Continuous Integration (CI) pipelines, the CLIs can be scripted and run automatically whenever submitting code changes to the source control platform. This makes sure that the new changes, such as updating calibration parameters, do not violate the software requirements.
Board Configuration
If a key-value store (KVS) exists in an embedded application, the end-of-line tester can use the CLI to interact with the KVS and configure new boards before shipping them to the customers. Developers can also use it to configure a board for developing certain features or reproducing a bug that is only observed in certain configurations.
Conclusion
Due to the precious flash space and security concerns, there is a debate about adding a CLI to an embedded application. With the benefits that it brings to facilitate the development and deployment of an embedded application, it is worth the effort to add a CLI.
DojoFive brings modern tools, techniques, and best practices from web and mobile development environments, paired with leading-edge innovations in firmware, to our customers for building successful products. We have talented engineers on hand ready to help you with all aspects of your EmbedOps journey. Bring us your interesting firmware problems that need solving – we are always happy to help out.


