技术
嵌入式shell
·11 分钟阅读
shell简易版
一、 核心数据结构:命令字典与函数指针
思路归纳: 将“用户输入的字符串”与“底层执行的代码”进行解耦。利用结构体数组建立一个“命令字典(Command Table)”,通过查表法实现指令的分发,彻底抛弃臃肿的
if-else或switch-case字符串匹配。命令字典结构体(Command Table) 用于绑定触发命令的单词、对应的函数入口以及帮助说明。
- 函数指针: 统一使用
void (*func)(int argc, char *argv[])签名,模仿 Linux C 程序的main函数入参,实现参数的动态传递。- 极致省内存: 将整个命令表用
const修饰,强制编译进 Flash(ROM)中,不占用 RAM。hljs c// 命令字典条目定义 typedef struct { const char *name; // 触发命令的字符串 (如 "led") void (*func)(int argc, char *argv[]); // 绑定的执行函数指针 const char *desc; // 帮助说明文本 } shell_cmd_st; // 静态实例化的命令表 (全 const,存入 ROM) static const shell_cmd_st cmd_table[] = { {"hello", cmd_hello, "Print hello message"}, {"led", cmd_led, "Control LED: led on/off"}, {"help", cmd_help, "Show help"}, {NULL, NULL, NULL} // 查表结束的哨兵标志 };
二、 运行时缓存区:单片机的“命令行大脑”(RAM区)
思路归纳: 终端的交互是一个持续的字符流输入过程,系统需要极少量的 RAM 来记住用户“当前敲了什么”、“光标在哪”以及“上次敲了什么”。
line_buf(行缓存): 存放用户当前正在输入的字符串。last_cmd(历史记录): 极简的历史记录机制,缓存上一次成功执行的命令,用于上下键调用。line_len&cursor_pos(长度与光标):line_len记录当前字符串总长度;cursor_pos记录真正的物理光标位置(用于支持在句子中间插入/删除字符)。esc_state(ANSI 状态机): 用于捕捉并解析键盘特殊按键(上下左右方向键)产生的ESC转义序列,防止屏幕出现乱码。hljs c#define SHELL_LINE_MAX 128U static char line_buf[SHELL_LINE_MAX]; static char last_cmd[SHELL_LINE_MAX] = {0}; static uint16_t line_len = 0U; static uint16_t cursor_pos = 0U; static uint8_t esc_state = 0U;
三、 核心神技:ANSI 转义序列与内存平移
思路归纳: 在单片机串口上实现类似 Linux 终端的“中间插入/删除”,绝对不能用清屏重绘(极度卡顿)。必须利用标准终端软件(如 Xshell、Putty)自带的 ANSI 控制码 配合 C 语言的
memmove实现零闪烁渲染。1. 局部刷新渲染器 (
shell_refresh_tail) 当用户在句子中间插入或删除字符时,只重绘光标后面的“尾巴”。
\x1B[K(清除到行尾): 这是 ANSI 神技,一秒擦除光标右侧的所有残影。- 退回光标: 打印完尾巴后,用
\b把物理光标稳稳拉回原来插入/删除的位置。hljs cstatic void shell_refresh_tail(void) { shell_send("\x1B[K"); // ANSI指令:清除光标到行尾的内容 if (cursor_pos < line_len) { uint16_t tail_len = (uint16_t)(line_len - cursor_pos); // 把被挤到后面的字符重新打印出来 HAL_UART_Transmit(&huart1, (uint8_t *)&line_buf[cursor_pos], tail_len, 10U); // 把物理光标退回到插入点 for (uint16_t i = 0U; i < tail_len; i++) { shell_send("\b"); } } }
四、 逻辑控制:一镜到底的字符流处理器
思路归纳: 串口每收到一个字符,就喂给
app_shell_process。该函数通过扁平化的if分支,将字符分流到“方向键处理”、“回车执行”、“退格删除”或“字符插入”四大逻辑块中。1. 过滤与拦截:ANSI 方向键状态机 防止按下方向键时屏幕出现
[D[C乱码,将其转化为实际的光标移动或历史命令调用。hljs cvoid app_shell_process(char ch) { // 状态 1: 捕捉起始符 ESC (0x1B) if ((uint8_t)ch == 0x1BU) { esc_state = 1U; return; } // 状态 2: 捕捉括号 [ if ((esc_state == 1U) && (ch == '[')) { esc_state = 2U; return; } // 状态 3: 解析方向键 (A上, B下, C右, D左) if (esc_state == 2U) { if ((ch == 'D') && (cursor_pos > 0U)) { cursor_pos--; shell_send("\b"); // 左键 } else if ((ch == 'C') && (cursor_pos < line_len)) { cursor_pos++; shell_send("\x1B[C"); // 右键 } else if (((ch == 'A') || (ch == 'B')) && (last_cmd[0] != '\0')) { // 上下键:调出历史命令 while (cursor_pos > 0U) { shell_send("\b"); cursor_pos--; } // 退回行首 shell_send("\x1B[K"); // 清空当前行 strcpy(line_buf, last_cmd); // 覆盖缓存 line_len = cursor_pos = (uint16_t)strlen(line_buf); shell_send(line_buf); } esc_state = 0U; return; // 吞噬结束,复位状态机 } esc_state = 0U; // 遇到杂乱字符强制复位 // ... 后续常规字符处理 ...2. 插入与删除:
memmove内存平移 利用 C 语言标准库极其高效的内存平移操作,为插入的字符腾出空间,或覆盖被删除的字符。hljs c// --- 退格删除逻辑 --- if (ch == '\b' || ch == 0x7F) { if (cursor_pos > 0U) { cursor_pos--; line_len--; // 把后面的数据整体往前拉一格,覆盖掉要删除的字符 memmove(&line_buf[cursor_pos], &line_buf[cursor_pos + 1U], (size_t)(line_len - cursor_pos)); shell_send("\b"); // 物理光标退一格 shell_refresh_tail(); // 重绘尾巴 } return; } // --- 字符插入逻辑 --- if (((uint8_t)ch >= 0x20U) && ((uint8_t)ch <= 0x7EU)) { if (line_len < (SHELL_LINE_MAX - 1U)) { // 把后面的数据整体往后推一格,腾出空位 memmove(&line_buf[cursor_pos + 1U], &line_buf[cursor_pos], (size_t)(line_len - cursor_pos)); line_buf[cursor_pos] = ch; // 填入新字符 cursor_pos++; line_len++; HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1U, 10U); // 回显 shell_refresh_tail(); // 重绘尾巴 } else { shell_send("\a"); // 触达界限,发出警告音 } } }
五、 命令解析与执行:词法切割 (
strtok)思路归纳: 当检测到回车键(
\r或\n)时,说明一条完整的指令输入完毕。利用strtok函数以空格为界限,将完整的字符串切割成单词数组(argv),并统计数量(argc),随后在“命令字典”中查表执行。hljs cstatic void shell_execute(char *cmd_line) { char *argv[SHELL_ARGV_MAX]; int argc = 0; // 1. 词法切割:将 "led on" 切割为 argv[0]="led", argv[1]="on" char *token = strtok(cmd_line, " \t"); while ((token != NULL) && (argc < SHELL_ARGV_MAX)) { argv[argc++] = token; token = strtok(NULL, " \t"); } if (argc == 0) return; // 空回车直接退出 // 2. 查表匹配:遍历 cmd_table 寻找同名命令 for (int i = 0; cmd_table[i].name != NULL; i++) { if (strcmp(argv[0], cmd_table[i].name) == 0) { // 找到命令,直接调用绑定的函数指针,把切割好的参数传进去 cmd_table[i].func(argc, argv); return; } } // 3. 兜底处理:未找到命令 my_printf(&huart1, "Unknown command: %s\r\n", argv[0]); }