Google esh:面向 ARM 与 RISC-V 的轻量级嵌入式 Shell
Google 开源的 esh(Embedded-Shell) 是一个面向资源受限嵌入式系统设计的轻量级 UART 命令行 Shell。
根据项目说明,esh 的内存占用低于 4KB,因此可以部署在 Flash 和 RAM 都十分有限的微控制器系统中,同时为固件调试、硬件验证以及新开发板快速 Bring-up 提供交互式命令接口。
项目支持 C、C++ 和 Assembly,覆盖 ARM 与 RISC-V 架构。其轻量级命令注册机制还允许开发者直接将固件函数暴露为交互式 CLI 命令,而无需引入完整的大型 Shell 框架。
对于从事裸机固件、早期硬件 Bring-up 或底层调试的开发者而言,esh 可以作为固件与开发者之间的一层轻量级交互控制接口。
项目采用 Apache-2.0 开源许可证。
项目地址: Google esh GitHub 仓库
项目状态: 该仓库目前为公开归档项目,不应将其视为 Google 当前正式支持的产品。
🧩 核心特性与架构 #
esh 的设计理念非常直接:在提供交互式 Shell 能力的同时,将运行时开销控制在尽可能低的水平。
超低内存占用 #
根据项目文档,Shell 的内存占用低于 4KB。
这使其适用于 Flash 和 RAM 都非常有限的系统,也避免了传统交互式 Shell 框架可能带来的额外开销。
对于新开发板 Bring-up 而言,一个小型 Shell 往往已经足够满足读取寄存器、测试外设、修改硬件配置以及触发诊断程序等需求。
简化命令注册 #
esh 使用 ADD_CMD() 宏注册自定义命令。
开发者无需针对每个固件功能重新实现命令解析器,只需要编写一个符合标准 C 函数形式的处理函数,然后将其注册到 Shell 中即可。
命令处理函数采用熟悉的接口:
int function_name(int argc, char** argv);
这种方式与普通 C 程序的参数处理机制保持一致,同时允许函数接收命令行参数。
多语言支持 #
esh 原生支持:
- C
- C++
- Assembly
这对于裸机项目尤其有用,因为启动代码、异常入口和部分底层硬件相关代码经常采用 Assembly 实现,而驱动和应用逻辑则主要使用 C 或 C++。
自动发现源文件 #
项目构建系统可以自动发现多种源文件,包括:
.c.cpp.S
同时,它能够自动处理头文件搜索路径。
因此,当开发者向项目中加入新的源文件时,通常不需要手动修改 Makefile。
对于小型固件实验和开发板验证项目而言,这可以明显缩短编辑、编译和运行之间的迭代周期。
🖥️ ARM 与 RISC-V 平台支持 #
虽然 esh 本身非常小,但项目提供了覆盖多个 ARM 处理器系列以及 32 位、64 位 RISC-V 环境的示例。
| 架构 | 处理器 | 目标平台 / 开发板 |
|---|---|---|
| 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 |
其中,ARM Cortex-M 示例主要针对微控制器级裸机环境,而 Cortex-A 和 RISC-V 示例则覆盖能力更强的 32 位和 64 位系统。
QEMU 模拟目标尤其适合开发者快速验证 Shell 和调试流程,因为无需实体开发板即可运行相关实验。
🚀 使用 QEMU 快速运行 RISC-V 64 示例 #
评估 esh 最简单的方法之一,是直接使用项目提供的 RISC-V 64 QEMU 模拟示例。
克隆项目 #
首先获取源码:
git clone https://github.com/google/esh.git
cd esh
仓库包含 Shell 实现、构建系统、示例代码以及不同平台的配置。
配置开发环境 #
项目提供了用于自动化依赖安装的管理脚本:
./manage -s
在基于 Debian 或 Ubuntu 的系统中,也可以手动安装所需的交叉编译、模拟和调试工具:
sudo apt install -y \
binutils \
make \
binutils-riscv64-linux-gnu \
gcc-riscv64-linux-gnu \
g++-riscv64-linux-gnu \
qemu-system-riscv64 \
gdb-multiarch \
openocd
如果需要使用项目提供的 GDB 增强配置,还可以执行:
wget -P ~ https://git.io/.gdbinit
pip3 install pygments
完成后,开发环境将具备运行示例所需的 RISC-V 交叉编译器、QEMU、GDB 和 OpenOCD 工具链。
编译并启动 Shell #
进入 RISC-V 64 模拟示例目录:
cd examples/emulation/riscv-64
执行编译:
make
然后启动 QEMU:
make run
固件启动后,终端将显示交互式 esh 提示符。
开发者可以直接执行内置命令,也可以使用已经注册的应用程序命令。
🐞 使用双终端进行 GDB 调试 #
esh 的另一个实用特性,是可以与 GDB 结合使用 QEMU 进行底层固件调试。
基本工作流需要两个终端。
终端 A:启动调试目标 #
首先让 QEMU 进入调试模式:
make debug
此时模拟固件会等待调试器连接。
终端 B:连接 GDB #
在第二个终端中执行:
make gdb
这样可以将 Shell 交互和源码级调试分离到两个终端。
一个终端负责执行 esh 命令,另一个终端则负责控制程序执行,并使用断点、单步执行、寄存器检查等 GDB 功能。
这种模式特别适合调试:
- Shell 命令处理函数
- 外设访问代码
- 中断行为
- 内存访问
- 底层硬件初始化
- 裸机固件执行路径
整个开发循环可以理解为:
交互式 Shell
│
▼
执行固件命令
│
▼
触发断点
│
▼
检查寄存器 / 内存
│
▼
单步执行底层代码
│
▼
恢复 Shell 交互
对于早期固件开发而言,这种组合非常实用,因为开发者可以直接触发特定代码路径,而不必每次都通过临时测试代码重新编译整个固件。
🛠️ 使用 ADD_CMD 添加自定义命令 #
esh 最主要的扩展机制就是 ADD_CMD() 宏。
一个 Shell 命令由一个普通的 C/C++ 函数和对应的注册信息组成。
命令示例 #
#include "shell.h"
/*
* 标准 Shell 命令函数原型:
* 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;
}
// 注册命令
ADD_CMD(
hello,
"Echoes the commandline\n\tusage: hello <any string>",
hello
);
命令处理函数接收与普通 C 程序类似的参数:
argc:命令行参数数量argv:保存各个参数的字符串数组
因此,开发者可以继续使用熟悉的 C 参数解析方式,而无需学习一套特殊的回调接口。
ADD_CMD 参数 #
注册宏主要关联三类信息:
| 参数 | 作用 |
|---|---|
hello |
用户在 Shell 中输入的命令名称 |
| Help 字符串 | help 命令显示的说明和使用方法 |
hello |
实际执行的 C/C++ 函数 |
例如:
ADD_CMD(command_name, help_text, function_name)
第一个参数定义 Shell 中显示的命令,最后一个参数指定实际执行该命令的函数。
自动构建集成 #
将包含命令实现的源文件放入项目对应目录后,esh 构建系统可以自动发现支持的源文件。
按照项目现有的自动源文件发现机制,开发者通常无需额外修改 Makefile。
重新编译并启动 Shell 后,可以执行:
help
查看已经注册的命令。
随后即可直接调用:
hello Hello embedded world
Shell 会将命令行参数传递给对应的 C 函数。
🔍 为什么裸机开发需要轻量级 Shell #
对于最终只执行固定功能的固件而言,引入 Shell 看起来可能并非必要。
但在开发板 Bring-up 和早期固件开发阶段,交互式 CLI 可以显著提高开发效率。
开发者经常需要执行以下操作:
- 读取硬件寄存器
- 写入配置值
- 测试 GPIO
- 验证通信外设
- 启动诊断程序
- 检查内存
- 触发固件测试
- 验证时钟配置
- 测试中断路径
如果没有交互式 Shell,每一次实验都可能需要修改测试代码、重新编译并重新烧录固件。
轻量级 CLI 提供了另一种方式:
开发者
│
▼
UART
│
▼
esh 命令解析器
│
▼
注册的固件函数
│
▼
硬件 / 外设
对于尚未运行完整操作系统、RTOS Shell 或大型诊断框架的系统,这种方式尤其有价值。
⚙️ esh 在嵌入式开发流程中的定位 #
esh 更适合作为一个开发和诊断接口,而不是完整操作系统 Shell 的替代品。
它的优势恰恰来自其有限的设计范围:
- 极低内存开销
- 简单的命令注册机制
- UART 交互
- 裸机兼容
- ARM 与 RISC-V 支持
- QEMU 示例
- GDB 调试集成
- 简洁的构建系统
这种组合适合希望在资源受限环境中获得交互式调试接口的固件开发者,而不需要在固件镜像中引入大型命令行框架。
在实际开发中,它还可以作为从早期硬件验证到正式产品固件之间的临时调试层。
例如,开发者可以在 Bring-up 阶段暴露底层诊断命令,在进入生产配置后再移除或限制这些接口。
🚀 面向裸机系统的轻量级 CLI #
Google 的 esh 展示了一个非常简单但实用的思路:嵌入式固件并不需要大型基础设施,也可以获得交互式 Shell 能力。
在低于 4KB的内存占用下,其核心价值并不在于提供多少预置命令,而在于提供了一套非常低成本的开发模型:
编写固件函数 → 使用 ADD_CMD() 注册 → 自动构建 → 通过 UART 交互执行。
再结合 ARM Cortex-M、Cortex-A 和 RISC-V 平台支持,以及 QEMU 和 GDB 调试流程,esh 对底层固件开发、开发板 Bring-up 和硬件验证具有较高的实用价值。
对于资源受限的微控制器系统而言,一个小型嵌入式 Shell 可以成为非常有效的调试层,在不引入完整操作系统环境复杂度的情况下,为开发者提供直接控制和观察裸机固件的能力。