大家好,我是杂烩君。之前的文章有朋友留言推荐了 CherrySH,今天我们就来一起看下。
1. CherrySH简介
| 项目信息 | 详情 |
|---|---|
| 项目名称 | CherrySH |
| 定位 | 嵌入式交互式 Shell |
| 开发语言 | C |
| 主仓库 | https://github.com/cherry-embedded/CherrySH |
| 许可证 | Apache-2.0 |
1.1 功能
支持 TAB 键补全,包括命令和路径补全支持历史记录,通过 ↑↓ 按键支持环境变量,需使用 $作为前缀,例如$PATH支持设定用户名、主机名、路径支持非阻塞模式,支持裸机和 RTOS支持光标左右移动,支持 HOME、END 切换光标支持组合按键,包括 Ctrl + <key>Alt + <key> ``F1-F12` 等支持信号处理,捕获和处理不同的信号,
例如Ctrl+CSIGINT和Ctrl+ZSIGTSTP,可中断当前执行的 shell 任务支持用户登录,需要实现hash函数,默认strcmp支持环境变量添加、修改、删除、读出支持文件系统,FatFS,FileX,LittleFS,RomFS等(TODO)支持 exit 函数实现终止命令执行以及现场返回并调用设定的handler,利用setjmp实现(裸机)(TODO)支持作业控制,可以在前台或后台运行命令,并使用相关的控制命令(如fg、bg、jobs)来管理和操作作业(TODO)支持多用户命令权限(TODO)
一句话说清楚:CherrySH 就是让你在 MCU 上敲命令,还能有方向键、历史、TAB 补全这些体验,而且不用 malloc。
它怎么做到的?两招:
命令注册:编译时用宏把命令塞进链接器 section,运行时直接遍历这块内存
行编辑:单独的 readline 模块处理键盘输入,全静态 buffer
2. 核心原理
2.1 整体架构
说明:
CherryReadLine:负责"怎么把一行字编辑好"(方向键、删除、历史、补全)
CherrySH:负责"拿到一行后怎么执行"(解析参数、查命令、调函数)
命令表:编译时自动收集,运行时直接遍历
2.2 关键代码
注册一个命令:
staticintmy_cmd(int argc, char **argv)
{
chry_shell_t *csh = (void *)argv[argc + 1];
csh_printf(csh, "====== 嵌入式大杂烩 ======rn");
return0;
}
CSH_CMD_EXPORT(my_cmd, );
这个宏展开后,会在 FSymTab 段里放一个结构体:
// csh.h 里的定义
typedefstruct {
constchar *path; // 命令路径,如 "/bin"
constchar *name; // 命令名
constchar *usage; // 简短说明
constchar *help; // 详细帮助
int (*func)(int argc, char **argv); // 函数指针
} chry_syscall_t;
链接器把所有这种结构体收集到一起,形成一个"命令表"。
运行时怎么找命令?
看 chry_shell_task_repl() 核心逻辑:
// 读一行
char *line = chry_readline(&csh->rl, buffer, size, &linesize);
// 解析成 argc/argv(原地切割,空格变 )
argc = chry_shell_parse(line, linesize, argv, MAX_ARG);
// 遍历命令表找匹配
for (call = csh->cmd_tbl_beg; call < csh->cmd_tbl_end; call++) {
if (路径+名字匹配) {
call->func(argc, argv); // 找到就调用
break;
}
}
用流程图表示:
2.3 目录结构
CherrySH/
├── chry_shell.c/h # Shell 核心(解析、执行、用户管理)
├── csh.h # 配置 + 导出宏
├── cherryrl/ # ReadLine 模块(行编辑、历史、补全)
├── builtin/ # 内置命令示例(help、clear 等)
└── samples/ # 移植示例(HPM、STM32)
3. 移植步骤
以先楫半导体hpm5301evklite为例。
- 命令查找采用的是 gcc 的 section 功能,因此,我们需要先修改 linkerscript 文件,增加相关 section,举例 gcc ld 文件:
.text : {
.....
. = ALIGN(4);
__fsymtab_start = .;
KEEP(*(FSymTab))
__fsymtab_end = .;
. = ALIGN(4);
__vsymtab_start = .;
KEEP(*(VSymTab))
__vsymtab_end = .;
. = ALIGN(4);
}
- 实现字符输入输出函数,接收推荐用中断 + ringbuf的形式
#include"csh.h"
staticchry_shell_t csh;
staticuint16_tcsh_sput_cb(chry_readline_t *rl, constvoid *data, uint16_t size)
{
uint16_t i;
(void)rl;
for (i = 0; i < size; i++) {
if (status_success != uart_send_byte(HPM_UART0, ((uint8_t *)data)[i])) {
break;
}
}
return i;
}
staticuint16_tcsh_sget_cb(chry_readline_t *rl, void *data, uint16_t size)
{
uint16_t i;
(void)rl;
for (i = 0; i < size; i++) {
if (status_success != uart_receive_byte(HPM_UART0, (uint8_t *)data + i)) {
break;
}
}
return i;
}
-
- 初始化 shell,参考 samples 中实现调用
chry_shell_task_exec 和 chry_shell_task_repl,参考 samples 中实现配置系统环境变量
#define __ENV_PATH "/sbin:/bin"
constchar ENV_PATH[] = __ENV_PATH;
CSH_RVAR_EXPORT(ENV_PATH, PATH, sizeof(__ENV_PATH));
#define __ENV_ZERO ""
constchar ENV_ZERO[] = __ENV_ZERO;
CSH_RVAR_EXPORT(ENV_ZERO, ZERO, sizeof(__ENV_ZERO));
-
- 使用
CSH_CMD_EXPORT 导出命令
staticintwrite_led(int argc, char **argv)
{
if (argc < 2) {
printf("usage: write_led <status>rnrn");
printf(" status 0 or 1rnrn");
return-1;
}
board_led_write(atoi(argv[1]) == 0);
return0;
}
CSH_CMD_EXPORT(write_led, );
4. 总结
4.1 CherrySH 优缺点
优点:readline 独立,行编辑体验好(方向键、Home/End、Ctrl 组合键都支持)支持多线程执行模型,长耗时命令可被 Ctrl+C 打断命令路径机制(/bin、/sbin),适合命令多的场景无堆内存,资源占用可预测
缺点:必须改链接脚本,对新手不友好history buffer 必须是 2 的幂,容易踩坑文档较少,主要靠看 samples 学习
如果觉得文章有帮助,麻烦帮忙转发,谢谢!
28