XCOM:一个 4 MB 的串口调试工具
XCOM 是 Windows 上的一个串口调试工具,压缩包 3.93 MB。
核心特性概览
| 你关心的 | XCOM 的答案 |
|---|---|
| 多小 | 压缩包 3.93 MB,绿色版解压即用 |
| 内存占用 | 固定大小,不随数据量增长(接收池恒为 512 KB) |
| 会不会丢数据 | 只要有日志在,字节流完整;真丢了会亮横幅告诉你,不隐瞒 |
| 改功能要编译吗 | 不用。界面和逻辑都是 Lua,改脚本保存即生效 |
| 能扩展吗 | scripts/下放一个.lua文件就是插件,几行代码自动应答设备 |
| 支持什么系统 | Windows 10/11 x64 |
下面按这个顺序展开:先看它的组成,再看两个最核心的设计——不丢数据和用脚本扩展。
系统组成:三层进程结构
XCOM 启动后有三个部分在同一个进程里协作,各自只做一件事:
三块的分工很干净:
- 启动器(
xcom.exe,只有 49 KB):唯一作用是隐藏那个黑框控制台,让程序看起来像个正常的 Windows 程序。 - Lua 层(
luvjit.exe+ 两个 DLL):界面上的一切——按钮、菜单、配置、脚本——都是 Lua 写的。 - C++ 核心(
xcom_core.dll):跟硬件打交道的部分,包括串口的打开、读写、超时、断线重连,全部在这里。
技术选型:Lua 界面与 C++ 核心
一句话:改起来快,但不牺牲性能。
串口工具真正的痛点是改动成本。时间戳想换个格式、某种报文想自动解析、某类数据想标红——在传统纯 C++ 工具里,这些都意味着改代码、重新编译、重新发给同事。XCOM 把这些压缩成"改一个文本文件,保存,立刻生效"。
但"用脚本写"通常让人担心变慢。这正是 XCOM 选LuaJIT而不是普通 Lua 的原因,也是它敢把整个界面放在脚本层的底气:
- Lua 调用 C++ 不经过层层接口封装,而是直接调用,开销和 C 调用同级。
- 收数据时整批字节一次性交给渲染层,不会在 Lua 里逐字节拼接。
- 整个界面只有一个线程,所有状态天然顺序,不需要加锁——也就没有锁带来的开销和卡顿。
代价也清楚:界面和主循环同线程,Lua 里写了慢代码,界面就会卡。所以重活全部推给 C++。下面这条接收链路就是最典型的例子。
接收完整性设计:零拷贝共享内存池
这是整个工具最花力气的地方。先澄清一个常见误解:"不丢数据"不是靠开一个很大的缓冲区硬扛。缓冲区总有满的时候,内存开得越大占用就越高,和"轻量"矛盾。
XCOM 的做法是一次拷贝,两个使用者共享。
固定大小的接收内存池
数据从串口读进来时,只在读线程这一处拷贝一次,放进固定大小的内存池:一共 512 KB,分成 128 块,每块 4 KB。显示通道和存文件通道各自持有同一块内存的引用——不是各复制一份,谁最后用完谁还回池子。
好处有两个:
- 内存占用固定,收再多数据也不膨胀。
- 两个使用者共享同一块内存,不存在"复制给显示成功、复制给文件失败"这种丢数据的可能。
池满时的双通道背压策略
两条通道的策略不一样,这是关键的取舍。
存文件的通道,宁可等,也不丢。池子一时没有空闲块时,读线程会停下来等磁盘写完,而不是把数据丢掉。文件是拿来做分析的,缺一个字节都可能让分析出错。等待期间界面会提示"存储跟不上",用户看得见。
显示通道则可以慢一点。它拿不到块时界面显示滞后,但这不是数据丢失——完整的字节流已经安全写在文件里。滞后的只是画面,不是数据。
为了不让显示拖累文件,池子里给文件通道留了96 块(384 KB)的保底:只有池子剩余多于 96 块时显示通道才能拿块,所以显示再怎么积压,也吃不掉文件的保底份额。
按最高支持速率算,一块 4 KB 约装 44 毫秒的数据,96 块保底相当于4 秒多的余量,显示侧另有 1 秒多。以 10 毫秒一轮的处理节奏,偶尔被系统调度一下,完全来得及。
丢失可观测性:如实计数与报告
有,而且如实标出来:
有一种丢失 XCOM 阻止不了:操作系统串口驱动自己的缓冲区溢出。这是驱动层的限制,任何用户态程序都拦不住,只能检测并计数。
所以承诺归结为两条:
- 只要有文件日志在,接收到的字节流就是完整的。池子满最多让显示慢一拍、读线程等一等磁盘,不会让字节凭空消失。
- 万一真发生丢失(包括驱动溢出),都会记入账本,界面亮出 “DATA LOSS” 横幅,而不是悄悄瞒过去。账本还记录缺口在数据流中的位置,方便定位。
不隐瞒,是这套设计里和"不丢"同等重要的一半。
脚本扩展机制:热重载与故障隔离
界面和逻辑既然都是 Lua,把能力开放给脚本就是顺理成章的事。scripts/目录下每个.lua文件都是一个插件,写几行就能用:
on.receive(function(text)iftext:match("^AT\r?\n?$")thensys.timer_start(10,function()uart.send("OK\r\n")end)endreturntextend)这段脚本的意思是:收到以AT结尾的一行时,延迟 10 毫秒回复OK。这是一个典型的设备自动应答场景——用传统工具往往要写一个上位机程序,这里只要几行。
脚本接口分类
接口按职责分成几类:
| 接口 | 用途 |
|---|---|
uart | 收发串口 |
on | 注册"收到数据"、"发送数据"这类钩子 |
filter | 决定哪些数据要显示、哪些不要 |
log | 往日志面板写字 |
wave | 把数据推给波形图 |
sys | 定时器、文件操作 |
热重载与故障隔离机制
改完即生效。保存脚本文件后不需要重启程序,会自动重新加载。新版本写错了,程序保留原来能用的版本,不会因为一个语法错误就打不开。
写错不拖垮程序。每个插件单独隔离,连续出错 3 次自动停用,并在日志面板提示。一个插件出问题,不影响其他插件,也不影响主程序。