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要严格大于最长字符串的描述符长度。