4. 仪器数据采集驱动开发
仪器边缘采集的第一道关口,是把物理接口变成可读写的软件通道。串口、网口、USB 三类接口在操作系统中的抽象方式不同,但驱动开发要回答的问题高度一致:设备如何被打开、参数如何配置、数据如何读写、超时如何界定、错误如何分类处理。本节以一台典型仪器为例,把这三条路径拆开讲清楚,并给出可直接参考的实现骨架。
一、打开:从设备标识到可用句柄
打开阶段的核心是“找到设备并建立独占或共享的访问通道”。串口在 Linux 下通常表现为 /dev/ttyUSB0、/dev/ttyS0,在 Windows 下为 COM3;网口仪器一般以 TCP 服务形式暴露,例如 192.168.1.50:5025(SCPI 常用端口);USB 仪器则分两类,一类是 CDC/虚拟串口,打开方式与串口一致,另一类是 USBTMC,需要按 USB 设备类协议打开端点。
打开时不能只依赖固定设备名。串口建议通过 udev 规则或 VID/PID 匹配,把设备稳定映射为 /dev/instrument_rs232;网口建议先做一次连接探测,确认端口可达再进入配置;USBTMC 则要枚举接口、确认 bInterfaceClass=0xFE、bInterfaceSubClass=0x03,再打开 Bulk-OUT 与 Bulk-IN 端点。打开失败要区分“设备不存在”“权限不足”“被占用”“协议不匹配”四类,分别给出不同恢复动作,而不是统一报“打开失败”。
二、配置:参数必须与仪器端一致
串口配置最容易出错。以 9600 波特率、8 数据位、无校验、1 停止位、无流控为例,对应 8N1。若仪器端是 7 数据位、偶校验,而驱动端仍按 8N1 打开,读到的数据会出现高位错乱或帧错误。配置项包括波特率、数据位、校验位、停止位、流控、读写超时。Linux 下用 termios 设置,Windows 下用 DCB 结构体设置,两者都要在打开后、首次读写前完成。
网口配置的重点是连接超时、读写超时和 TCP_NODELAY。仪器响应通常短小,若启用 Nagle 算法,小包可能被延迟合并,导致“命令已发、响应迟迟不到”。建议对 SCPI 类连接设置 TCP_NODELAY,并把连接超时设为 1~3 秒,读超时按最长测量时间加余量设置,例如 5 秒。USB 配置则要设置端点超时和传输块大小,USBTMC 的 Bulk-IN 端点常见最大包长为 64 或 512 字节,读缓冲应不小于该值。
三、读写:命令与响应的边界
仪器通信大多遵循“一问一答”模型:驱动写入一条命令,仪器返回一段响应。串口没有天然的消息边界,必须靠终止符或长度字段分帧。常见做法是约定以 \n 或 \r\n 结尾,读循环中逐字节累积,遇到终止符才认为一帧完整。若仪器返回二进制块,则要先读头部长度字段,再按长度读满剩余字节。
网口基于 TCP 字节流,同样没有消息边界,不能假设一次 recv 就是一条完整响应。正确做法是维护接收缓冲区,按终止符或长度切分,剩余部分留给下一次。USB 的 Bulk 传输以端点包为单位,USBTMC 还带有 12 字节头部,包含传输方向和长度,解析时要先读头部再读数据体。
写操作要处理“部分写”。串口和 USB 都可能一次只写入部分字节,必须循环写直到全部发出。网口 send 返回值小于请求长度时,也要继续发送剩余部分。读写之间建议加一个短延时或等待仪器就绪信号,避免命令尚未处理完就发下一条。
四、超时:区分“慢”与“断”
超时不是单一数值,而是分层设置。连接超时用于建链,通常 1~3 秒;写超时用于发送命令,通常 500~1000 毫秒;读超时用于等待响应,应按仪器最长测量时间加 20%~50% 余量。例如某仪器单次测量最长 2 秒,读超时可设 3 秒。若读超时后立即重试,可能把仍在处理的仪器压垮,建议先发一次查询状态命令,确认仪器空闲再重发。
超时与断连要区分:读超时后连接仍可能有效,只需重发或延长等待;若连续多次超时且心跳探测也失败,才判定为断连,进入重连流程。重连要有退避,例如 1 秒、2 秒、4 秒、8 秒,避免频繁冲击仪器。
五、错误处理:分类、恢复与可观测
错误处理的目标不是“不报错”,而是“报得准、恢复得了、查得到”。建议把错误分为四类:参数类(配置不合法)、通信类(超时、断连、校验错)、协议类(响应格式不符、状态字异常)、设备类(仪器返回错误码)。每类对应不同动作:参数类直接拒绝启动;通信类重试后重连;协议类丢弃当前帧并记录原始字节;设备类解析仪器错误队列并上报。
可观测性方面,至少记录时间戳、接口类型、设备标识、命令、原始响应、错误码、重试次数。下面给出一段串口读写的参考骨架,展示超时与错误分支如何落地:
int read_frame(int fd, char *buf, int maxlen, int timeout_ms) {
int n = 0;
struct pollfd pfd = { .fd = fd, .events = POLLIN };
while (n < maxlen - 1) {
int pr = poll(&pfd, 1, timeout_ms);
if (pr == 0) return -ETIMEDOUT; // 读超时
if (pr < 0) return -errno; // 系统错误
int r = read(fd, buf + n, maxlen - 1 - n);
if (r < 0) {
if (errno == EAGAIN || errno == EINTR) continue;
return -errno; // EIO 等
}
if (r == 0) return -ECONNRESET; // 对端关闭
n += r;
if (buf[n-1] == '\n') break; // 帧结束
}
buf[n] = '\0';
return n;
}
这段代码把“超时”“系统错误”“对端关闭”“正常成帧”四种结果分开返回,上层就能据此决定重试、重连还是丢弃。实际工程中还应加上写循环、命令队列和缓存落盘,保证在仪器短暂不可用时数据不丢。
把打开、配置、读写、超时、错误处理五件事分别做扎实,驱动才能在长时间运行中保持稳定。下一阶段将在此基础上引入缓存与断点续传,解决网络抖动和仪器离线时的数据保全问题。