一、项目简介
在嵌入式系统开发中,我们常常需要通过串口、USB等通信接口与设备交互,执行调试命令、配置参数或查询状态。
本次推荐一个轻量级命令行shell库——LwSHELL(Lightweight Shell)。
https://github.com/MaJerle/lwshell
MIT license
它提供了一套简洁的API,让开发者像注册中断回调一样注册命令处理函数,库负责字符解析、参数分割和命令分发。更重要的是,LwSHELL完全零动态内存分配,所有数据结构在编译时确定大小,非常适合资源受限的嵌入式系统。
核心特性:
- 零动态内存分配,内存占用可预测支持动态命令和静态命令两种注册方式流式字符处理,逐字节输入支持引号参数和转义字符平台无关,易于移植
二、LwSHELL核心原理
2.1 整体流程
LwSHELL的设计非常精巧,整个库只有两个核心文件:lwshell.h和lwshell.c。让我们先看一下整体流程:
2.2 目录结构
项目采用清晰的分层结构:
lwshell/
├── lwshell/src/ # 核心库源码
│ ├── include/lwshell/ # 公开头文件
│ │ ├── lwshell.h # 主API接口
│ │ └── lwshell_opt.h # 配置选项
│ └── lwshell/ # 实现文件
│ └── lwshell.c # 唯一的C实现
├── dev/ # 开发测试代码
│ ├── main.c # 示例程序
│ └── lwshell_opts.h # 用户配置文件
├── examples/ # 最小示例
│ └── example_minimal.c
└── tests/ # 单元测试
└── test.c
整个库实现只有一个.c文件(约360行),所有功能通过编译时宏配置开关,避免了运行时的条件判断开销。
2.3 核心数据结构
LwSHELL的精髓在于lwshell_t结构体的设计:
说明:
零动态分配:
所有数组大小在编译时确定,通过宏LWSHELL_CFG_MAX_*配置
双命令表模式:
-
- 支持动态命令(存RAM)和静态命令(存Flash),后者对小容量MCU极其友好
argv原地解析:argv
-
- 数组不复制字符串,而是直接指向buff中的位置,节省内存
命令结构体设计同样简洁:
2.4 工作流程剖析
当用户输入addint 10 20n时,LwSHELL内部发生了什么?
流程关键点:
字符级处理: 每次调用lwshell_input可以传入1个或多个字符,库内部逐字节处理,支持流式输入(典型场景是串口中断每次收到一个字节)
回车触发解析: 只有收到r或n才触发命令解析,之前的字符都缓存在buff中
原地参数分割: 解析时直接在buff中将空格替换为,argv数组指向分割后的各段起始位置,避免内存拷贝
命令查找策略: 先查动态命令表,再查静态命令表,使用strncmp精确匹配
2.5 动态vs静态命令表的权衡
LwSHELL提供两种命令注册方式,这是针对不同应用场景的精心设计:
动态命令模式(适合开发/大容量MCU):
静态命令模式(适合量产/小容量MCU):
2.6 零依赖的编译时配置
通过lwshell_opts.h文件,可以裁剪功能:
每个配置项直接影响lwshell_t结构体的大小。
三、使用示例
3.1 最小示例
以下是一个完整的最小示例:
#include "lwshell/lwshell.h"
#include <stdio.h>
// 定义命令回调函数
int32_t cmd_led(int32_t argc, char** argv) {
if (argc < 2) {
printf("Usage: led <on|off>rn");
return-1;
}
if (strcmp(argv[1], "on") == 0) {
// HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET);
printf("LED turned ONrn");
} elseif (strcmp(argv[1], "off") == 0) {
// HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_RESET);
printf("LED turned OFFrn");
}
return0;
}
// 可选:定义输出回调(用于回显和反馈)
void shell_output(const char* str, lwshell_t* lw) {
(void)lw;
printf("%s", str);
}
int main(void) {
// 初始化Shell
lwshell_init();
// 设置输出函数
lwshell_set_output_fn(shell_output);
// 注册命令
lwshell_register_cmd("led", cmd_led, "Control LED on/off");
// 主循环处理输入
while (1) {
char c = uart_getchar(); // 从串口读一个字符
lwshell_input(&c, 1); // 喂给LwSHELL处理
}
}
3.2 串口中断集成
在实际嵌入式项目中,通常在串口接收中断中调用lwshell_input:
// UART接收中断回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
if (huart->Instance == USART1) {
// rx_char是中断接收到的字符
lwshell_input(&rx_char, 1);
// 重新启动接收
HAL_UART_Receive_IT(&huart1, &rx_char, 1);
}
}
// 或者使用DMA缓冲区
void process_uart_buffer(uint8_t* buffer, size_t len) {
lwshell_input(buffer, len);
}
四、注意事项
缓冲区溢出防护: 输入超过LWSHELL_CFG_MAX_INPUT_LEN的命令会被静默丢弃,建议在应用层加提示
线程安全:lwshell_t实例不是线程安全的,多线程环境需要外部加锁或为每个线程创建独立实例
命令名称限制: 不支持包含空格的命令名,如需复杂命令结构建议用层级形式(如system.info)
753