bareline: 写腻了第 N 版 if (strcmp(...)):我给 STM32 做了个 C++23 的极简串口 shell
凌晨一点,板子终于上电跑起来了。LED 亮、串口通,你想发个命令让它干点活——然后开始写第 N 版那个东西:
if (strcmp(buf, "led on") == 0) { HAL_GPIO_WritePin(...); }
else if (strcmp(buf, "led off") == 0) { ... }
else if (strcmp(buf, "help") == 0) { ... }
else { printf("unknown\n"); }
写到第三个 else if 你停了一下:上个项目这段也写过,上上个项目也是。printf 能看日志,可一旦要“发条命令让板子做点事”,就只剩临时糊一个字符串比较的循环——每次从零开始,每次都很丑,改一个命令名还得在三处改。
bareline 就是写腻了这个之后的东西。它是个 header-only 的 C++23 库,给裸机(Cortex-M 这类 MCU)一个最小的交互式命令行:行编辑、分词、命令分派都帮你做了,你只管注册命令。
先看效果。左边 STM32F103C8(Blue Pill)真机,串口 115200 敲命令;右边 PC 上直接跑:
📷 STM32 真机:
📷 host PC:
肯定会写的很复杂吧,其实并没有——
int cmd_led(std::span<std::string_view> args) { /* 点灯 */ return 0; }
using Cmds = bareline::cmd::command_list<
bareline::cmd::command<"ledon", cmd_led, "turn the LED on">,
bareline::cmd::command<"ledoff", cmd_led_off, "turn the LED off">>;
bareline::shell::shell<Backend, Cmds> shell(backend);
for (;;) shell.run_once(); // 提示符 → 读一行 → 分词 → 分派
- 命令是编译期注册的。
command<"ledon", ...>的名字是模板参数,拼错编译期就暴露;运行时按名字匹配,小列表走 fold,列表一大自动切到 constexpr 哈希再回查名字防碰撞。 - 不绑平台。后端满足一个
IOAbleconcept(有read/write即可)就行,UART、USB CDC、网络都接得上;库里带个zeros_io,两个 C 函数指针就能桥接。 - 零运行时开销的取向:不开异常、不开 RTTI、不分配堆;Cortex-M3 上完整一条 shell 管道大概 1 KB text。
stat这个内建命令不需要时能编译期完全消掉。 - 真机落地:仓库里有 STM32F103C8 的完整固件例子,带 OpenOCD 烧录脚本和
.vscode调试配置,整套带 HAL 编出来 7.3 KB flash / 512 B bss。笔者还没有开专门的体积定向的优化
写的时候纠结过一阵:shell 要不要做成非阻塞、事件驱动的?最后选了最朴素的——阻塞,run_once() 读满一行才往下走。理由不复杂:裸机上你本来就在主循环里轮询,与其再造一套事件循环,不如顺着重用。STM32 那个例子里,“shell 停在提示符等输入”的空转正好拿来 poll 按键,等于白捡一个响应。不算多优雅,但够轻、够好懂,够大多数场景。
bareline自己就是header only的!我相信知晓C++的朋友知道这意味着什么——把 include/bareline/ 拷进项目,或 CMake add_subdirectory + target_link_libraries(your_target bareline),就完事了。
因为考虑到C++只有高版本的情况下,一些编译期的表达才很方便,这就是为什么这个项目选择了 C++23。两个现成例子在仓库——PC 上 stdin/stdout、STM32 上 USART1。
v1.0.0 刚发。写它的时候没想着卷谁,就是不想再写第 N+1 个 else if 了。如果你也写腻了,可以试试;觉得哪儿别扭,欢迎来 issue 里聊。
- GitHub:
https://github.com/Awesome-Embedded-Learning-Studio/bareline - Release v1.0.0:见仓库 Releases