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:UsbManager 和 UsbDeviceConnection。但真正写起来,坑比想象的多。
我先把整体流程画出来,你心里有个谱:
你看,流程不复杂。但实际编码时,有几个关键点必须注意。
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上传到服务器。我个人习惯在采集线程里用Handler或LiveData把数据抛到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