You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

ALSA:如何可靠启动USB原始MIDI的接收(Rx)功能?

解决Linux下USB MIDI设备SysEx消息读取丢失问题

问题背景

我需要从Linux电脑向带USB MIDI接口的设备发送MIDI SysEx消息,流程是发送消息后接收响应,示例代码如下:

handle = open("/dev/midiXxx", O_RDWR);
write(f, "\xF0...\xF7", LENGTH_OF_MESSAGE_1);
read(f, response1, sizeof(response1));
write(f, "\xF0...\xF7", LENGTH_OF_MESSAGE_1);
read(f, response2, sizeof(response2));

但首次调用read()有时会挂起,且发现MIDI驱动会丢弃首次read()调用前收到的所有字节——如果设备在程序调用read()前就发送了响应(比如首次write()和read()之间发生任务切换),响应会直接丢失。

该问题在以下场景均会出现:

  • 使用/dev/midiXxx设备文件
  • 直接打开/dev/snd/midiCXxxDxxx并调用read()/write()
  • 使用ALSA的snd_rawmidi_Xxx() API替代原生open()/read()/write()(内部仍访问/dev/snd/midiCXxxDxxx)

核心问题:确保MIDI数据被正确读取的可靠方法是什么?

我试过两种可行方法:

  • 先以O_NONBLOCK模式打开设备,调用read()(返回EWOULDBLOCK)后,再用fcntl(F_SETFL)取消非阻塞模式(程序其余部分不需要该模式)
  • 在write()前调用带POLLIN参数的poll()

但不确定这些方法在Linux版本更新后是否仍能可靠工作。


可靠解决方案及分析

推荐方案:全程非阻塞模式+事件监听

你尝试的两种方法均有合理性,但更可靠的做法是全程使用非阻塞模式配合poll()/select(),而非临时切换模式,可避免模式切换带来的竞态问题:

  1. 打开设备时直接指定O_RDWR | O_NONBLOCK
  2. 发送SysEx消息后,用poll()监听设备的POLLIN事件,直到有数据可读或超时
  3. 有数据时调用read()读取,循环读取直到获取完整响应(注意SysEx消息以0xF7结尾,需判断消息完整性)

示例代码片段:

#include <poll.h>
#include <fcntl.h>

// 打开设备
int handle = open("/dev/snd/midiC0D0", O_RDWR | O_NONBLOCK);
if (handle < 0) {
    // 错误处理逻辑
}

// 发送SysEx消息
write(handle, "\xF0...\xF7", LENGTH_OF_MESSAGE);

// 等待响应
struct pollfd pfd = {
    .fd = handle,
    .events = POLLIN
};
// 超时时间设为1秒,可根据设备响应速度调整
int ret = poll(&pfd, 1, 1000); 
if (ret > 0 && (pfd.revents & POLLIN)) {
    unsigned char buf[256];
    ssize_t bytes_read;
    // 循环读取直到获取完整SysEx响应
    while ((bytes_read = read(handle, buf, sizeof(buf))) > 0) {
        // 处理读取到的数据,检查是否以0xF7结尾
        // ...
    }
} else if (ret == 0) {
    // 超时处理逻辑
} else {
    // poll调用错误处理逻辑
}

临时切换非阻塞模式的隐患

临时用fcntl切换模式的做法存在竞态窗口:在取消O_NONBLOCK后,如果设备刚好此时发送数据,而程序还未调用read(),数据仍可能被丢弃(取决于驱动缓冲机制)。全程非阻塞+事件监听可避免该问题。

write()前调用poll()的注意事项

在write()前调用poll(POLLIN)的目的是清空驱动缓冲区中的残留旧数据,逻辑合理,但需注意:

  • 调用poll()后若有数据,必须将所有残留数据读完,否则旧数据会干扰后续响应读取
  • 该步骤仅需在第一次发送消息前执行一次,无需每次发送前重复操作

驱动层面的本质原因

Linux ALSA的rawmidi驱动默认不会缓冲read()调用前收到的数据,只有当程序调用read()(或通过poll()注册监听)后,驱动才会开始缓存设备发来的数据。这就是提前到达的响应会被丢弃的核心原因——此时程序尚未通知驱动要接收数据。

因此核心逻辑是:必须在发送请求前,让驱动知晓你要接收数据,要么通过提前注册poll()监听,要么通过打开非阻塞模式并持续尝试读取(poll()的效率更高)。


内容的提问来源于stack exchange,提问作者Martin Rosenau

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.27 15:16:19