Google esh: Lightweight Embedded Shell for ARM and RISC-V
Google’s open-source esh (Embedded-Shell) is a compact UART-based command-line shell designed for resource-constrained embedded systems.
With a reported memory footprint of under 4KB, esh is small enough to fit naturally into microcontroller-class firmware while still providing an interactive command interface for debugging, hardware validation, and rapid development-board bring-up.
The project supports C, C++, and Assembly source files across ARM and RISC-V targets. Its lightweight command-registration model also makes it possible to expose firmware functions through an interactive CLI without building a large shell framework.
For developers working on bare-metal firmware, early board bring-up, or low-level debugging, this makes esh particularly useful as a temporary interactive control layer between the firmware and the developer.
The project is available under the Apache-2.0 license.
Repository: Google esh on GitHub
Project status: The repository is publicly archived and should not be interpreted as an actively supported Google product.
🧩 Core Features and Architecture #
The design philosophy behind esh is straightforward: provide an interactive shell while imposing as little overhead as possible on the embedded firmware.
Ultra-Low Memory Footprint #
The shell occupies less than 4KB of memory, according to the project’s documentation.
This makes it suitable for systems where Flash and RAM are tightly constrained and where a conventional interactive shell would introduce unnecessary overhead.
A small shell can be especially valuable during board bring-up because developers often need only a handful of commands to inspect registers, exercise peripherals, configure hardware, or trigger test routines.
Simple Command Registration #
Custom commands are exposed through the ADD_CMD() macro.
Instead of implementing a separate command parser for every firmware function, developers provide a conventional C-style function and register it with the shell.
The command handler follows a familiar interface:
int function_name(int argc, char** argv);
This keeps command implementations close to ordinary application functions while allowing them to receive command-line arguments.
Multi-Language Support #
esh supports source files written in:
- C
- C++
- Assembly
This is particularly relevant to bare-metal projects, where low-level startup code and hardware-specific routines are often implemented in Assembly while drivers and application logic use C or C++.
Automatic Source Discovery #
The build system automatically discovers supported source files, including:
.c.cpp.S
It also handles header search paths automatically, reducing the amount of manual Makefile maintenance required when adding source files.
For small firmware experiments and board-validation projects, this can significantly shorten the edit-build-run cycle.
🖥️ Supported ARM and RISC-V Platforms #
Although the shell itself is small, the repository provides examples covering several generations of ARM processors as well as both 32-bit and 64-bit RISC-V environments.
| Architecture | Processor | Target Platform / Board |
|---|---|---|
| ARMv7-M | Cortex-M3 / M4 | QEMU, Nucleo-F401RE, TIVA-C |
| ARMv8-M | Cortex-M33 | QEMU mps3-an524 |
| ARMv7-A / ARMv8-A | Cortex-A7 / A53 / A72 | QEMU Raspberry Pi 2/3, virt |
| RISC-V | RV32IMAC / RV64G | HiFive1-RevB, QEMU virt |
The ARM Cortex-M examples target microcontroller-style bare-metal environments, while the Cortex-A and RISC-V examples demonstrate operation on more capable 32-bit and 64-bit systems.
The QEMU targets are particularly useful because developers can experiment with the shell and debugging workflow without requiring physical hardware.
🚀 Quick Start with RISC-V 64 and QEMU #
One of the simplest ways to evaluate esh is to use the RISC-V 64 emulation example.
Clone the Repository #
Start by obtaining the source tree:
git clone https://github.com/google/esh.git
cd esh
The repository contains the shell implementation, build infrastructure, examples, and platform-specific configurations.
Set Up the Development Environment #
The project includes a management script intended to simplify dependency setup:
./manage -s
On Debian- or Ubuntu-based systems, the required toolchain and debugging utilities can also be installed manually:
sudo apt install -y \
binutils \
make \
binutils-riscv64-linux-gnu \
gcc-riscv64-linux-gnu \
g++-riscv64-linux-gnu \
qemu-system-riscv64 \
gdb-multiarch \
openocd
For the project’s GDB enhancements, the setup can additionally include:
wget -P ~ https://git.io/.gdbinit
pip3 install pygments
The resulting environment provides the cross-compiler, QEMU system emulator, GDB, and OpenOCD tooling needed for the examples and hardware-oriented debugging workflows.
Build and Launch the Shell #
Move to the RISC-V 64 emulation example:
cd examples/emulation/riscv-64
Build the firmware:
make
Then launch it under QEMU:
make run
Once the emulated firmware starts, the terminal provides an interactive esh prompt.
Built-in commands can be executed directly, while application-specific commands become available after they are registered with the shell.
🐞 Dual-Terminal GDB Debugging #
One of the more useful aspects of the QEMU workflow is that esh can be combined with GDB for low-level firmware debugging.
The basic workflow uses two terminals.
Terminal A: Start the Debug Target #
Launch QEMU in debugging mode:
make debug
The emulated firmware waits for a debugger connection.
Terminal B: Attach GDB #
In a second terminal, run:
make gdb
This creates a workflow in which the developer can interact with the shell in one terminal while controlling program execution from GDB in another.
This is particularly useful for debugging command handlers, peripheral access routines, interrupt behavior, and other bare-metal code paths.
A typical development cycle becomes:
Interactive shell
│
▼
Execute firmware command
│
▼
Hit breakpoint
│
▼
Inspect registers / memory
│
▼
Step through low-level code
│
▼
Resume shell interaction
The combination of an interactive CLI and source-level debugging is valuable during early firmware development because developers can trigger specific code paths without repeatedly rebuilding the application around temporary test logic.
🛠️ Adding Custom Commands with ADD_CMD #
The primary extension mechanism in esh is the ADD_CMD() macro.
A command consists of a conventional C or C++ function plus a registration entry that provides the command name and help information.
Example Command #
#include "shell.h"
/*
* Standard shell command function prototype:
* int function_name(int argc, char** argv)
*/
int hello(int argc, char** argv) {
for (int i = 0; i < argc; i++) {
printf(argv[i]);
printf(" ");
}
printf("\nPress ctrl + a, x to exit !\n");
return 0;
}
// Register the command
ADD_CMD(
hello,
"Echoes the commandline\n\tusage: hello <any string>",
hello
);
The command handler receives the same fundamental arguments used by a conventional C program:
argc— number of command-line argumentsargv— array containing the individual arguments
This allows command implementations to use familiar argument-parsing patterns without requiring developers to learn a proprietary callback interface.
ADD_CMD Parameters #
The registration macro associates three pieces of information:
| Parameter | Purpose |
|---|---|
hello |
Command name entered at the shell prompt |
| Help string | Description and usage information shown by help |
hello |
C/C++ function invoked when the command executes |
For example:
ADD_CMD(command_name, help_text, function_name)
The first argument defines the shell-visible command, while the final argument identifies the implementation function.
Automatic Build Integration #
Once the source file is placed within the appropriate project directory, the esh build system automatically discovers the supported source file.
No additional manual Makefile entry is required for the normal source-discovery workflow.
After rebuilding and launching the shell, running:
help
displays the registered command alongside the existing commands.
The developer can then invoke it directly:
hello Hello embedded world
The shell passes the command-line arguments to the registered C function.
🔍 Why a Tiny Shell Matters in Bare-Metal Development #
A command shell may appear excessive for firmware that ultimately performs a fixed set of functions, but an interactive CLI can dramatically simplify the early stages of embedded development.
During board bring-up, developers frequently need to perform operations such as:
- Reading hardware registers
- Writing configuration values
- Testing GPIOs
- Exercising communication peripherals
- Starting diagnostic routines
- Inspecting memory
- Triggering firmware tests
- Validating clock configuration
- Testing interrupt paths
Without an interactive shell, each experiment may require recompiling firmware with temporary test code.
A lightweight command interface provides a more efficient alternative:
Developer
│
▼
UART
│
▼
esh command parser
│
▼
Registered firmware function
│
▼
Hardware / peripheral
The approach is especially attractive when a full operating system, RTOS shell, or large diagnostic framework is unavailable or undesirable.
⚙️ esh in the Embedded Development Workflow #
esh is best viewed as a development and diagnostic interface, rather than a replacement for a full operating-system shell.
Its strengths come directly from its small scope:
- Very low memory overhead
- Simple command registration
- UART-based interaction
- Bare-metal compatibility
- ARM and RISC-V support
- QEMU examples
- GDB integration
- Minimal build-system overhead
That combination makes it useful for firmware engineers who need an interactive interface without pulling a substantial command-line framework into a constrained embedded image.
It can also serve as a convenient bridge between early hardware validation and later production firmware. Developers can initially expose low-level diagnostic commands and then remove or restrict those interfaces when moving toward a production configuration.
🚀 A Practical Lightweight CLI for Embedded Systems #
Google’s esh demonstrates how little infrastructure is required to create a useful interactive shell for bare-metal firmware.
At under 4KB, its primary value is not the number of commands it provides out of the box, but the development model it enables: expose a firmware function, register it with ADD_CMD(), build automatically, and interact with the target through UART.
Its support for ARM Cortex-M, Cortex-A, and RISC-V platforms, combined with QEMU and GDB workflows, makes the project particularly relevant for low-level firmware development and hardware bring-up.
For developers working with constrained microcontrollers or early-stage board validation, a small embedded shell can provide a surprisingly powerful debugging layer without imposing the footprint or complexity of a full operating-system environment.