Rust in Everything
基于 embassy-usb 0.6.0

Descriptor buffer full / control buffer too small

主线位置:USB 设备栈的建立和枚举,中断主线上「驱动登记 waker、打开中断」之前。Builder 的四块缓冲区、各种上限和它们的实测边界见设备栈。

flowchart TB
  b["Builder::new 四块缓冲区"] --> cls["每个类写描述符"]
  cls --> q1{"config 缓冲够吗"}
  q1 -->|不够| p1["建设备时 panic"]
  q1 -->|够| run["UsbDevice::run"]
  run --> host["主机 GET_DESCRIPTOR STRING"]
  host --> q2{"control_buf 够吗"}
  q2 -->|不够| p2["枚举时 panic"]
  q2 -->|够| ok["枚举完成"]

实验 crate exp-embassy/pitb-usb(复制自 usb-std:std 上的假 USB 驱动加一个假主机,走真实的控制传输),bin p26_buffers(platform-std,release);M2 Max、macOS 27、rustc 1.99.0。

现象

一个 CDC-ACM 串口设备,config_descriptor_buf 开了 96 字节,一直正常(CDC-ACM 的配置描述符 70 字节)。后来加了一个 HID 键盘(p26_buffers small):

let mut builder = Builder::new(
  FakeDriver::new(&WIRE),
  config,
  leak(vec![0u8; conf_len]).as_mut_slice(), // 96
  leak([0u8; 256]),                          // bos
  &mut [],                                   // msos
  leak([0u8; 64]),                           // control_buf
);
let _acm = CdcAcmClass::new(&mut builder, leak(cdc_acm::State::new()), 64);
let _kbd = HidWriter::<_, 8>::new(&mut builder, leak(hid::State::new()), hid_cfg);
product string: 31 characters
CdcAcmClass::new
HidWriter::new

thread 'main' (5051640) panicked at ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/embassy-usb-0.6.0/src/descriptor.rs:109:9:
Descriptor buffer full

把 config_descriptor_buf 加到 256(p26_buffers string),设备建起来了,主机读配置描述符、读厂商字符串都正常,一读产品字符串就 panic:

built: UsbBufferReport { config_descriptor_used: 103, bos_descriptor_used: 12, msos_descriptor_used: 0, control_buffer_size: 64 }
host: bus reset
...
host: configuration descriptor 103 B
host: GET_DESCRIPTOR(STRING 1)
...
    [  0] 34 B  22 03 72 00 75 00 73 00 74 STRING "rustineverything"
host: GET_DESCRIPTOR(STRING 2)
host: SETUP [80 06 02 03 09 04 ff 00]  GET_DESCRIPTOR(STRING)

thread 'main' (5051644) panicked at ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/embassy-usb-0.6.0/src/lib.rs:758:29:
control buffer too small

产品名是 "rustineverything USB serial+kbd",31 个字符。第二种更难发现:上电不出错,要等主机枚举到读产品字符串那一步才 panic。

出处

Builder 不分配内存,描述符直接写进调用方的切片。DescriptorWriter::write 每写一个描述符检查一次:

        assert!(
            (self.position + 2 + total_length) <= self.buf.len() && (total_length + 2) <= 255,
            "Descriptor buffer full"
        );

CDC-ACM 写完是 70 字节,HID 再加 33 字节(IAD 8、接口 9、HID 描述符 9、端点 7),103 > 96,在 HidWriter::new 里 panic。

字符串描述符不在建设备时生成,而是主机请求时在 handle_get_descriptor 里现编码进 control_buf:

                        let mut pos = 2;
                        for c in s.encode_utf16() {
                            assert!(pos + 2 < buf.len(), "control buffer too small");

                            buf[pos..pos + 2].copy_from_slice(&c.to_le_bytes());
                            pos += 2;
                        }

31 个字符要 2 + 62 = 64 字节,正好等于 64 字节的 control_buf,但断言是 < 而不是 <=:写最后一个字符时 62 + 2 < 64 不成立,panic。30 个字符(62 字节)没事。这是一个差一字节的上游问题,main 上这行没有改(文件已搬到 embassy-usb/src/device/mod.rs,断言原样)。字符串超过 126 个字符时 bLength 还会被截断,见设备栈。

修法

先给足,再收紧。buffer_usage() 报告实际用量:

built: UsbBufferReport { config_descriptor_used: 103, bos_descriptor_used: 12, msos_descriptor_used: 0, control_buffer_size: 64 }
  • config_descriptor_buf:至少 config_descriptor_used,每加一个类重新看一次。

  • control_buf:至少「最长字符串的 UTF-16 字节数 + 2 + 1」(多出的 1 是为了绕开上面那个 <),也不能小于各类的要求(CDC-ACM 要 7,cdc_acm.rs 里有断言,见设备栈的表)。31 个字符的产品名、control_buf 改成 128(p26_buffers bigctrl):

    built: UsbBufferReport { config_descriptor_used: 103, bos_descriptor_used: 12, msos_descriptor_used: 0, control_buffer_size: 128 }
    ...
        [  0] 64 B  40 03 72 00 75 00 73 00 74 STRING "rustineverything USB serial+kbd"
    
  • 或者缩短字符串:"RIE serial + keyboard"(21 个字符,44 字节)在 64 字节里正常返回(p26_buffers fix)。

怎么确认没中招

  • 上电后打印一次 usb.buffer_usage(),和你给的缓冲区大小比;改了类或描述符就再看一次。
  • 字符串只在主机请求时编码:在 std 上用假驱动跑一遍枚举(或者在真机上插一次电脑),确认每个字符串都被读过一次。
  • 数 manufacturer、product、serial_number 以及各类自定义字符串的字符数,2 × 字符数 + 2 必须小于 control_buf 的长度。

小结

  • Builder 把描述符写进调用方给的定长缓冲区,放不下就 panic「Descriptor buffer full」,在建设备时暴露。
  • 字符串描述符在主机请求时才编码进 control_buf,放不下的 panic「control buffer too small」要到枚举时才出现;断言差一字节,正好填满也会 panic(main 未改)。
  • 先给大缓冲区,用 buffer_usage() 看实际用量再收紧;control_buf 要严格大于最长字符串的描述符长度。

💬 关联讨论

发起讨论
加载中...