26、USB数据采集卡(DAQ)开发:USB DAQ设备协议(NI DAQ、LabJack)、Android端USB DAQ驱动开发、模拟量/数字量采集与控制、实战:通过USB DAQ采集温度/电压数据

各位同学,今天我们来啃一块硬骨头——USB DAQ开发。说实话,这个主题在Android开发圈子里聊的人不多,但做工业物联网、现场测试、便携仪器的小伙伴,迟早得面对它。

我最早接触DAQ是在一个车载电池测试项目里。客户要求用平板实时采集32路电压和4路热电偶温度。当时我天真地想:“不就是读个USB吗?”结果折腾了整整两周才把第一帧数据稳定读上来。嗯,今天我把这些经验揉碎了讲给你听。

本章核心目标:理解USB DAQ设备的通信协议,掌握Android端驱动开发方法,最终实现模拟量/数字量的采集与控制。

26.1 USB DAQ设备协议:NI DAQ与LabJack

市面上主流的USB DAQ设备,协议上大致分两类:一类是厂商自定义的批量传输协议(如NI DAQmx),另一类是HID类设备协议(如LabJack U3)。

我个人习惯把DAQ协议拆成三层来看:

  • 物理层:USB Bulk传输或HID中断传输。NI DAQ用Bulk,LabJack用HID。
  • 命令层:厂商定义的控制字。比如“启动采集”、“设置通道”、“读取数据”。
  • 数据层:采样数据的封装格式。通常是固定长度的字节流,包含通道ID、时间戳、原始值。

拿NI DAQ举例,它的协议其实不公开,但通过USB抓包可以逆向出基本结构。我曾在项目中用Wireshark配合usbmon抓过NI USB-6009的通信包,发现它的命令包是64字节固定长度,前4字节是命令码,后面是参数。

LabJack就友好多了。它公开了HID报告描述符,你直接通过HID API就能读写。它的协议更简单:发送一个输出报告(Output Report)作为命令,然后读取输入报告(Input Report)获取数据。

特性 NI DAQ (USB-6009) LabJack (U3)
USB类 Vendor Specific (Bulk) HID
命令包大小 64字节 8字节 (Output Report)
数据包大小 512字节 (Bulk In) 64字节 (Input Report)
采样率 最高10 kS/s 最高50 kS/s
Android驱动难度 高(需实现Bulk传输) 中(HID API较成熟)

我的建议:如果你刚开始做Android DAQ开发,先从LabJack这类HID设备入手。HID协议在Android上支持最稳定,不需要root权限,也不容易遇到驱动冲突。

26.2 Android端USB DAQ驱动开发

Android端开发USB DAQ驱动,核心就两个API:UsbManagerUsbDeviceConnection。但真正写起来,坑比想象的多。

我先把整体流程画出来,你心里有个谱:

Android USB DAQ驱动开发流程 1. 枚举USB设备 2. 获取USB权限 3. 打开设备 + 声明接口 4. 发送命令包 / 读取数据包(循环) 5. 解析数据 + 回调

你看,流程不复杂。但实际编码时,有几个关键点必须注意。

26.2.1 枚举与权限

Android的USB Host API要求你在AndroidManifest.xml中声明设备过滤,或者动态获取权限。我建议用动态方式,因为DAQ设备往往有多个固件版本,PID/VID可能变化。

// 动态获取USB权限
UsbManager usbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
HashMap<String, UsbDevice> deviceList = usbManager.getDeviceList();

for (UsbDevice device : deviceList.values()) {
    if (device.getVendorId() == 0x1234 && device.getProductId() == 0x5678) {
        PendingIntent permissionIntent = PendingIntent.getBroadcast(
            this, 0, new Intent(ACTION_USB_PERMISSION), PendingIntent.FLAG_IMMUTABLE);
        usbManager.requestPermission(device, permissionIntent);
    }
}

我曾经踩过的坑:有些DAQ设备在枚举时,接口描述符里会包含多个Alternate Setting。如果你不遍历所有设置,直接claimInterface可能会失败。记得用 device.getInterface(0).getAlternateSetting(0) 确认一下。

26.2.2 批量传输与HID传输

对于NI DAQ这类Bulk设备,你需要用UsbDeviceConnection.bulkTransfer()。这里有个性能要点:缓冲区大小最好和设备端点描述符的wMaxPacketSize对齐,否则会出现数据错位。

// 批量传输示例(NI DAQ风格)
UsbEndpoint endpointIn = interface.getEndpoint(0); // Bulk IN
UsbEndpoint endpointOut = interface.getEndpoint(1); // Bulk OUT

byte[] command = new byte[64];
command[0] = 0x01; // 启动采集命令
command[1] = 0x00; // 通道0

// 发送命令
connection.bulkTransfer(endpointOut, command, command.length, 1000);

// 读取数据
byte[] buffer = new byte[512];
int bytesRead = connection.bulkTransfer(endpointIn, buffer, buffer.length, 1000);

对于LabJack这类HID设备,Android没有直接的HID API,但你可以通过UsbDeviceConnection.controlTransfer()模拟HID报告传输。说白了,就是用控制传输来发送Output Report和接收Input Report。

// HID控制传输示例(LabJack风格)
// 发送Output Report (命令)
byte[] report = new byte[8];
report[0] = 0x00; // Report ID
report[1] = 0x01; // 命令码:读取AI0
connection.controlTransfer(
    0x21, // 主机到设备,类请求
    0x09, // SET_REPORT
    0x0200, // Output Report
    0, // 接口号
    report,
    report.length,
    1000);

// 读取Input Report (数据)
byte[] input = new byte[64];
connection.controlTransfer(
    0xA1, // 设备到主机,类请求
    0x01, // GET_REPORT
    0x0100, // Input Report
    0,
    input,
    input.length,
    1000);

性能提示:Bulk传输的吞吐量远高于HID控制传输。如果你需要高速采集(比如每通道1kS/s以上),尽量选Bulk设备。HID设备适合低速监控场景。

26.3 模拟量/数字量采集与控制

DAQ的核心功能就两个:采集(读)和控制(写)。模拟量是连续值,数字量是0/1。

26.3.1 模拟量采集

模拟量采集的关键是量程转换。设备返回的原始值通常是ADC的二进制码,你需要根据参考电压和分辨率换算成实际物理量。

// 模拟量转换示例
public double convertAnalog(int rawValue, double referenceVoltage, int resolutionBits) {
    // 假设设备返回的是无符号整数
    double voltage = (rawValue / Math.pow(2, resolutionBits)) * referenceVoltage;
    return voltage;
}

// 如果是热电偶,还需要查表或多项式拟合
public double convertThermocouple(double voltageMv, ThermocoupleType type) {
    // 这里简化处理,实际需要查NIST标准表
    return voltageMv * 0.025; // 粗略估算
}

我在项目中遇到过一个问题:有些DAQ设备的ADC是差分输入,返回的是有符号值。如果你按无符号处理,负电压会变成很大的正数。记得检查设备手册的编码格式。

26.3.2 数字量采集与控制

数字量就简单多了。通常一个字节的每一位对应一个数字通道。读的时候按位与,写的时候按位或。

// 数字量读取
byte digitalStatus = readDigitalPort();
boolean channel0 = (digitalStatus & 0x01) != 0;
boolean channel1 = (digitalStatus & 0x02) != 0;

// 数字量写入(控制继电器、LED等)
byte outputValue = 0x00;
outputValue |= 0x01; // 打开通道0
outputValue |= 0x04; // 打开通道2
writeDigitalPort(outputValue);

重要提醒:数字量输出时,建议先读取当前端口状态,只修改你要控制的位,再写回。否则会意外改变其他通道的状态。我见过有人直接写死0xFF,结果把整个板子的继电器全吸合了……

26.4 实战:通过USB DAQ采集温度/电压数据

好了,理论说完了,我们来个完整的实战。假设你手头有一块LabJack U3,我们要采集通道AI0的电压,同时读取数字通道FIO0的状态。

26.4.1 初始化与配置

public class LabJackU3Driver {
    private UsbDeviceConnection connection;
    private UsbInterface usbInterface;
    
    public boolean init(UsbDevice device, UsbManager manager) {
        usbInterface = device.getInterface(0);
        connection = manager.openDevice(device);
        if (connection == null) return false;
        
        // 声明接口
        if (!connection.claimInterface(usbInterface, true)) {
            connection.close();
            return false;
        }
        return true;
    }
    
    // 配置AI0为单端输入,±10V范围
    public void configureAI0() {
        byte[] cmd = new byte[8];
        cmd[0] = 0x00; // Report ID
        cmd[1] = 0x01; // 配置命令
        cmd[2] = 0x00; // 通道0
        cmd[3] = 0x01; // 单端模式
        cmd[4] = 0x00; // 增益:±10V
        sendCommand(cmd);
    }
}

26.4.2 数据采集循环

// 采集线程
public void startAcquisition() {
    new Thread(() -> {
        while (isRunning) {
            // 读取模拟量
            byte[] aiCmd = new byte[8];
            aiCmd[0] = 0x00;
            aiCmd[1] = 0x02; // 读取AI命令
            aiCmd[2] = 0x00; // 通道0
            sendCommand(aiCmd);
            
            byte[] aiData = readResponse();
            int rawValue = ((aiData[2] & 0xFF) << 8) | (aiData[1] & 0xFF);
            double voltage = convertAnalog(rawValue, 10.0, 16);
            
            // 读取数字量
            byte[] dioCmd = new byte[8];
            dioCmd[0] = 0x00;
            dioCmd[1] = 0x03; // 读取DIO命令
            sendCommand(dioCmd);
            
            byte[] dioData = readResponse();
            boolean fio0 = (dioData[1] & 0x01) != 0;
            
            // 回调到UI线程
            runOnUiThread(() -> {
                textViewVoltage.setText(String.format("%.3f V", voltage));
                textViewDigital.setText(fio0 ? "HIGH" : "LOW");
            });
            
            Thread.sleep(100); // 10Hz采样率
        }
    }).start();
}

26.4.3 数据解析与显示

采集到的数据怎么用?你可以实时显示,也可以存到SQLite数据库,或者通过MQTT上传到服务器。我个人习惯在采集线程里用HandlerLiveData把数据抛到UI层,避免ANR。

调试技巧:开发阶段,建议把原始字节流也打印到日志里。这样一旦数据异常,你可以对照设备手册逐字节排查。我曾经靠这个办法发现LabJack U3的固件版本不同,数据偏移量差了2个字节。

26.5 避坑指南与经验总结

最后,我把自己这些年踩过的坑总结一下,你遇到了直接翻这页就行。

  • USB权限弹窗不出现:检查AndroidManifest.xml中是否有<uses-feature android:name="android.hardware.usb.host" />。有些平板默认禁用了USB Host。
  • 数据传输不稳定:DAQ设备通常要求严格的时序。Android的USB调度可能延迟,建议在采集线程里设置Thread.setPriority(Thread.MAX_PRIORITY)
  • 设备断开后重连:注册UsbManager.ACTION_USB_DEVICE_DETACHED广播,在广播里清理资源并尝试重连。
  • 采样率上不去:检查你的读取循环里有没有做UI更新。UI操作是重量级的,会拖慢采集。把UI更新放到单独的Handler里,用postDelayed控制频率。

嗯,USB DAQ开发说难不难,说简单也不简单。核心就是吃透协议,管好缓冲区,处理好异步。你只要动手做一遍,把这些代码跑通,以后再遇到任何USB采集设备,心里就有底了。

记住,数据采集的终点不是拿到数值,而是让数值产生价值。不管是做环境监测、工业控制还是科研实验,这套方法都通用。


公众号:蓝海资料掘金营,微信deep3321