USB驱动开发:从协议描述符到URB传输的完整实战
USB是嵌入式Linux里最复杂的总线之一,协议层次深、描述符嵌套多、传输类型分四种,再加上URB异步机制,新手很容易绕晕。但USB驱动的套路其实很固定——搞清描述符层次、写对id_table匹配、管好URB生命周期,剩下的就是填空。本文基于Linux 6.8内核,从USB协议基础讲起,过一遍核心数据结构,再用完整示例把probe/disconnect/URB操作串起来,最后给调试技巧和常见坑点。
一、USB基础知识
USB拓扑结构
USB是树形拓扑,一根总线上的设备分三层角色:
Root Hub (主机控制器内置
├── Hub
├── Device A
└── Device B
└── Device C
- 主机(Host):发起所有传输,只有一个,由主机控制器(HCI)实现。- 集线器(Hub):扩展端口,级联最多7层(含Root Hub)。- **设备(Device):最终的功能设备,每个设备最多16个端点(0号端点固定为控制端点。
主机和设备之间是主从关系——设备永远不能主动发数据,只能被动响应主机的请求,中断传输和等时传输看起来是设备"主动"上报,实际是主机在周期性轮询。
四种传输类型
| 传输类型 | 方向 | 可靠性 | 带宽保证 | 典型应用 |
|---|---|---|---|---|
| 控制传输 | 双向 | 有(重试) | 无 | 枚举、配置、类请求 |
| 批量传输 | 单向 | 有(重试) | 无 | U盘、打印机 |
| 中断传输 | 单向(IN为主) | 有(重试) | 有(周期保证) | 键盘、鼠标 |
| 等时传输 | 单向 | 无(不重试) | 有(固定带宽) | 音频、视频 |
控制传输是USB的基础——设备枚举、配置切换、类特定命令全走它,批量传输追求可靠但不保证时效,适合U盘这种大数据量场景,中断传输名字有误导性,它不是硬件中断,而是主机按固定间隔轮询设备,等时传输牺牲可靠性换带宽,丢包不重传,音视频流必须用它。
USB描述符层次
USB设备通过描述符向主机报告自己的能力,描述符是严格嵌套的:
设备描述符(Device Descriptor)
└── 配置描述符(Configuration Descriptor)
├── 接口描述符(Interface Descriptor)
└── 端点描述符(Endpoint Descriptor)
└── 接口描述符(Interface Descriptor)
└── 端点描述符(Endpoint Descriptor)
一个设备可以有多个配置(实际几乎只有1个),每个配置下有多个接口(代表不同功能),每个接口下有多个端点(数据传输的通道)。
四种描述符关键字:
| 描述符 | 长度 | 关键字段 | 说明 |
|---|---|---|---|
| 设备描述符 | 18字节 | bDeviceClass、idVendor、idProduct、bNumConfigurations | 设备级分类、VID/PID、配置数量 |
| 配置描述符 | 9字节 | bConfigurationValue、bmAttributes、bMaxPower | 配置值、供电方式、最大功能 |
| 接口描述符 | 9字节 | bInterfaceClass、bInterfaceNumber、bNumEndpoints | 接口类、接口编号、端点数量 |
| 端点描述符 | 7字节 | bEndpointAddress、bmAttributes、wMaxPacketSize | 端点地址+方向、传输类型、最大包长 |
端点地址的最高位表示方向:0=OUT(主机→设备),1=IN(设备→主机)。低4位是端点号。比如0x81表示端点1、IN方向0x02表示端点2、OUT方向。
二、USB核心数据结构
USB驱动开发打交道最多的就四个结构体:usb_driver、usb_device_id、urb、usb_interface。
usb_driver:驱动注册入。
// linux/usb.h(简化)
struct usb_driver {
const char *name;
int (*probe)(struct usb_interface *intf,
const struct usb_device_id *id);
void (*disconnect)(struct usb_interface *intf);
int (*suspend)(struct usb_interface *intf, pm_message_t msg);
int (*resume)(struct usb_interface *intf);
const struct usb_device_id *id_table;
struct device_driver driver; // 内嵌device_driver。.8统一设备模型
};
name:驱动名,出现在/sys/bus/usb/drivers/。-probe:匹配成功后调用,做初始化-disconnect:设备拔出时调用,做清理id_table:匹配表,决定这个驱动管哪些设备
注册用usb_register_driver(),注销用usb_deregister()。内核提供了便捷宏:
module_usb_driver(my_usb_driver);
// 等价于 module_init + usb_register + module_exit + usb_deregister
usb_device_id:设备匹配规。
// linux/mod_devicetable.h(简化)
struct usb_device_id {
__u16 match_flags; // 哪些字段参与匹配
__u16 idVendor; // VID
__u16 idProduct; // PID
__u16 bDeviceClass; // 设备。 __u16 bDeviceSubClass; // 设备子类
__u16 bDeviceProtocol; // 设备协议
__u16 bInterfaceClass; // 接口。 __u16 bInterfaceSubClass;// 接口子类
__u16 bInterfaceProtocol;// 接口协议
kernel_ulong_t driver_info; // 驱动私有数据
};
match_flags决定匹配策略,常用标志:
| 标志 | 含义 |
|---|---|
| USB_DEVICE_ID_MATCH_VENDOR | 匹配VID |
| USB_DEVICE_ID_MATCH_PRODUCT | 匹配PID |
| USB_DEVICE_ID_MATCH_DEVICE | VID+PID同时匹配 |
| USB_DEVICE_ID_MATCH_INT_CLASS | 匹配接口类 |
实际写驱动时用宏定义匹配表更直观。
// 精确匹配VID/PID
{ USB_DEVICE(0x1234, 0x5678) }
// 匹配某类设备(如HID类)
{ USB_INTERFACE_INFO(USB_CLASS_HID, 0, 0) }
// 匹配某VID的所有设。{ USB_DEVICE_VENDOR(0x1234) }
urb:USB请求。
urb(USB Request Block)是USB传输的核心数据结构,所有USB数据传输都通过urb完成。
// linux/usb.h(简化)
struct urb {
struct usb_device *dev; // 目标设备
unsigned int pipe; // 管道(端点+方向+类型编码) void *transfer_buffer; // 传输缓冲区 u32 transfer_buffer_length; // 缓冲区长度 u32 actual_length; // 实际传输长度
usb_complete_t complete; // 完成回调
void *context; // 回调上下。 int status; // 传输状态 unsigned int transfer_flags; // 传输标志
int interval; // 轮询间隔(中断/等时) // ... 更多字段
};
urb是异步的——提交后函数立刻返回,传输完成后内核调用complete回调。这跟同步的usb_bulk_msg不同,后面会对比
usb_device / usb_interface / usb_host_endpoint
| 结构体 | 代表 | 获取方式 |
|---|---|---|
usb_device |
整个USB设备 | interface_to_usbdev(intf) |
usb_interface |
一个功能接口 | probe参数直接拿到 |
usb_host_endpoint |
一个端点 | usb_pipe_endpoint(dev, pipe) |
驱动绑定的对象是usb_interface而不是usb_device。一个多功能设备(如带摄像头的麦克风)有多个接口,每个接口由不同驱动管理。
三、USB驱动开发流程
MODULE_DEVICE_TABLE
匹配表定义后必须加这行宏,否则modprobe找不到驱动:
static struct usb_device_id my_usb_table[] = {
{ USB_DEVICE(0x1234, 0x5678) },
{ } // 末尾空项,必。};
MODULE_DEVICE_TABLE(usb, my_usb_table);
这个宏的作用是把匹配表导出到模块的__mod_usb_device_table段,depmod生成modules.alias时读取它。用户空间udev收到USB设备热插拔事件后,根据modules.alias找到对应驱动并自动加载。不加这行,驱动能编译能insmod,但插上设备不会自动probe。
四、USB驱动完整示例
下面是一个基于Linux 6.8的USB批量传输驱动,功能是向设备的OUT端点发送数据,从IN端点接收数据。
// my_usb_drv.c - Linux 6.8 USB批量传输驱动示例
#include <linux/module.h>
#include <linux/usb.h>
#include <linux/slab.h>
#define VENDOR_ID 0x1234
#define PRODUCT_ID 0x5678
// 驱动私有数据
struct my_usb_dev {
struct usb_device *udev; // USB设备
struct usb_interface *intf; // USB接口
__u8 bulk_in_addr; // IN端点地址
__u8 bulk_out_addr; // OUT端点地址
size_t bulk_in_size; // IN端点最大包。 struct urb *bulk_in_urb; // IN端点urb
void *bulk_in_buf; // IN端点缓冲。 struct mutex io_mutex; // IO互斥。};
// 匹配置static struct usb_device_id my_usb_table[] = {
{ USB_DEVICE(VENDOR_ID, PRODUCT_ID) },
{ }
};
MODULE_DEVICE_TABLE(usb, my_usb_table);
// IN端点urb完成回调
static void my_bulk_in_callback(struct urb *urb)
{
struct my_usb_dev *dev = urb->context;
switch (urb->status) {
case 0:
// 传输成功,处理数。 dev_info(&dev->intf->dev, "收到 %d 字节\n",
urb->actual_length);
break;
case -ECONNRESET: // urb被kill
case -ENOENT: // urb被unlink
case -ESHUTDOWN: // 设备已断开
// 这些是正常断开,不报错
return;
default:
dev_err(&dev->intf->dev, "urb错误: %d\n", urb->status);
break;
}
// 重新提交urb,持续接。 usb_submit_urb(dev->bulk_in_urb, GFP_ATOMIC);
}
// 查找批量端点
static int find_bulk_endpoints(struct my_usb_dev *dev,
struct usb_interface *intf)
{
struct usb_host_interface *alts;
struct usb_endpoint_descriptor *epd;
int i;
// 使用当前altsetting
alts = intf->cur_altsetting;
for (i = 0; i < alts->desc.bNumEndpoints; i++) {
epd = &alts->endpoint[i].desc;
if (!usb_endpoint_is_bulk_in(epd) &&
!usb_endpoint_is_bulk_out(epd))
continue;
if (usb_endpoint_is_bulk_in(epd)) {
dev->bulk_in_addr = epd->bEndpointAddress;
dev->bulk_in_size = usb_endpoint_maxp(epd);
} else {
dev->bulk_out_addr = epd->bEndpointAddress;
}
}
if (!dev->bulk_in_addr || !dev->bulk_out_addr) {
dev_err(&intf->dev, "找不到批量端点\n");
return -ENODEV;
}
return 0;
}
// probe回调
static int my_usb_probe(struct usb_interface *intf,
const struct usb_device_id *id)
{
struct my_usb_dev *dev;
struct usb_device *udev = interface_to_usbdev(intf);
int ret;
dev_info(&intf->dev, "probe: VID=%04x PID=%04x\n",
id->idVendor, id->idProduct);
// 分配私有数据
dev = kzalloc(sizeof(*dev), GFP_KERNEL);
if (!dev)
return -ENOMEM;
dev->udev = udev;
dev->intf = intf;
mutex_init(&dev->io_mutex);
// 查找端点
ret = find_bulk_endpoints(dev, intf);
if (ret)
goto fail_find_ep;
// 分配IN端点urb
dev->bulk_in_urb = usb_alloc_urb(0, GFP_KERNEL);
if (!dev->bulk_in_urb) {
ret = -ENOMEM;
goto fail_alloc_urb;
}
// 分配IN端点缓冲。 dev->bulk_in_buf = kzalloc(dev->bulk_in_size, GFP_KERNEL);
if (!dev->bulk_in_buf) {
ret = -ENOMEM;
goto fail_alloc_buf;
}
// 填充IN端点urb
usb_fill_bulk_urb(dev->bulk_in_urb,
udev,
usb_rcvbulkpipe(udev, dev->bulk_in_addr),
dev->bulk_in_buf,
dev->bulk_in_size,
my_bulk_in_callback,
dev);
// 提交IN端点urb,开始接。 ret = usb_submit_urb(dev->bulk_in_urb, GFP_KERNEL);
if (ret) {
dev_err(&intf->dev, "提交urb失败: %d\n", ret);
goto fail_submit;
}
// 保存私有数据到interface
usb_set_intfdata(intf, dev);
return 0;
fail_submit:
kfree(dev->bulk_in_buf);
fail_alloc_buf:
usb_free_urb(dev->bulk_in_urb);
fail_alloc_urb:
fail_find_ep:
kfree(dev);
return ret;
}
// disconnect回调
static void my_usb_disconnect(struct usb_interface *intf)
{
struct my_usb_dev *dev;
dev = usb_get_intfdata(intf);
if (!dev)
return;
// 先清空interface私有数据,防止其他地方还在用
usb_set_intfdata(intf, NULL);
// 杀掉urb,等回调执行。 usb_kill_urb(dev->bulk_in_urb);
// 释放资源
usb_free_urb(dev->bulk_in_urb);
kfree(dev->bulk_in_buf);
mutex_destroy(&dev->io_mutex);
kfree(dev);
dev_info(&intf->dev, "设备已断开\n");
}
// usb_driver定义
static struct usb_driver my_usb_driver = {
.name = "my_usb_drv",
.id_table = my_usb_table,
.probe = my_usb_probe,
.disconnect = my_usb_disconnect,
};
module_usb_driver(my_usb_driver);
MODULE_AUTHOR("linuxros");
MODULE_DESCRIPTION("USB批量传输驱动示例");
MODULE_LICENSE("GPL");
关键点说明
probe里做的事。
1. interface_to_usbdev(intf)拿到usb_device
2. 遍历intf->cur_altsetting->endpoint[]找批量端。3. usb_alloc_urb(0, GFP_KERNEL)分配urb。表示不创建iso帧描述符号4. usb_fill_bulk_urb()填充urb的管道、缓冲区、回调5. usb_submit_urb()提交urb
6. usb_set_intfdata()把私有数据挂到interface。
disconnect里做的事。
1. usb_get_intfdata()取出私有数据
2. usb_set_intfdata(intf, NULL)先清空,防止竞。3. usb_kill_urb()杀掉urb并等回调执行完(不能用usb_unlink_urb,它不等。4. 释放urb、缓冲区、私有数。
*管道编码。
| | 用途 |
|:---|:-----|
| usb_rcvbulkpipe(dev, ep) | 构造批量IN管道 |
| usb_sndbulkpipe(dev, ep) | 构造批量OUT管道 |
| usb_rcvintpipe(dev, ep) | 构造中断IN管道 |
| usb_sndintpipe(dev, ep) | 构造中断OUT管道 |
| usb_rcvisopipe(dev, ep) | 构造等时IN管道 |
| usb_sndisopipe(dev, ep) | 构造等时OUT管道 |
| usb_rcvctrlpipe(dev, ep) | 构造控制IN管道 |
| usb_sndctrlpipe(dev, ep) | 构造控制OUT管道 |
管道把设备地址、端点号、方向、传输类型编码成一个unsigned int,urb提交时内核用它找到目标端点。
五、URB操作详解
urb生命周期
urb的生命周期是:分配→填充→提交→完成回调→回收或重新提交。urb可以复用——回调里重新填充并提交就行,不用每次都alloc/free。
三种填充函数
// 批量传输
void usb_fill_bulk_urb(struct urb *urb,
struct usb_device *dev,
unsigned int pipe,
void *transfer_buffer,
int buffer_length,
usb_complete_t complete_fn,
void *context);
// 中断传输
void usb_fill_int_urb(struct urb *urb,
struct usb_device *dev,
unsigned int pipe,
void *transfer_buffer,
int buffer_length,
usb_complete_t complete_fn,
void *context,
int interval);
// 控制传输(需要手动设置setup_packet。// 没有usb_fill_control_urb,手动填充:
urb->setup_packet = (u8 *)dr; // struct usb_ctrlrequest
urb->transfer_buffer = buf;
urb->transfer_buffer_length = len;
中断传输的interval是轮询间隔,单位取决于设备速度:低速/全速是帧(1ms为单位),高速是微帧(125μs为单位)。
URB状态码
| status | 含义 | 处理方式 |
|---|---|---|
-EINPROGRESS |
urb正在传输 | 正常,回调还没被调用 |
0 |
传输成功 | 读取actual_length获取数据 |
-ECONNRESET |
urb被usb_unlink_urb取消 |
回调中直接return |
-ENOENT |
urb被usb_kill_urb取消 |
回调中直接return |
-ESHUTDOWN |
设备已断开或控制器已停止 | 回调中直接return,不要再提交urb |
-EPIPE |
端点stall | 需要发送USB_REQ_CLEAR_FEATURE清除 |
-EOVERFLOW |
数据溢出 | 检查缓冲区大小和wMaxPacketSize |
-ETIME |
超时 | 等时传输常见,检查interval设置 |
回调里判断status时,-ECONNRESET、-ENOENT、-ESHUTDOWN这三种是正常的断开/取消场景,不应该当错误处理,也不应该重新提交urb。
同步传输API
不想搞异步urb回调?内核提供了同步版本的传输函数:
// 同步批量传输
int usb_bulk_msg(struct usb_device *dev,
unsigned int pipe,
void *data,
int len,
int *actual_length,
int timeout); // 超时ms0=无限等待
// 同步控制传输
int usb_control_msg(struct usb_device *dev,
unsigned int pipe,
__u8 request,
__u8 requesttype,
__u16 value,
__u16 index,
void *data,
__u16 size,
int timeout);
同步API简单直接,但有严格限制。
| 限制 | 原因 |
|:-----|:-----|
| 不能在中断上下文调用 | 内部会睡眠等待 |
| 不能在持有自旋锁时调用 | 同上 |
| timeout=0可能永远阻塞 | 设备不响应就卡死 |
| 不支持等时传输 | 等时必须用urb |
实际项目中,probe里初始化设备用usb_control_msg很方便,运行时持续收数据用urb异步更合适配
六、USB设备信息获取
lsusb命令
# 列出所有USB设备
lsusb
# 查看设备详细描述符
lsusb -v -d 1234:5678
# 只看设备树拓扑
lsusb -t
lsusb -v输出量大,重点关注:
idVendor/idProduct:写id_table要用bInterfaceClass:接口类,决定用哪个驱动bEndpointAddress:端点地址,写驱动时要。-wMaxPacketSize:最大包长,分配缓冲区要。
/sys/bus/usb/devices/
内核通过sysfs暴露USB设备信息,比lsusb更底层:
# 查看USB设备
ls /sys/bus/usb/devices/
# 查看特定设备的VID/PID
cat /sys/bus/usb/devices/1-1/idVendor
cat /sys/bus/usb/devices/1-1/idProduct
# 查看设备配置
cat /sys/bus/usb/devices/1-1/bConfigurationValue
# 查看接口驱动绑定
cat /sys/bus/usb/devices/1-1:1.0/driver_override
ls -l /sys/bus/usb/devices/1-1:1.0/driver
sysfs路径格式:1-1:1.0表示总线1-端口1:配置1-接口0。冒号前面是设备拓扑路径,冒号后面是接口编号。
usbfs:用户空间USB驱动
不想写内核驱动?libusb提供了用户空间USB操作接口。
// 用户空间USB操作示例(libusb。#include <libusb.h>
libusb_device_handle *handle;
libusb_init(NULL);
handle = libusb_open_device_with_vid_pid(NULL, 0x1234, 0x5678);
libusb_claim_interface(handle, 0);
unsigned char buf[64];
int actual;
libusb_bulk_transfer(handle, 0x81, buf, sizeof(buf), &actual, 1000);
libusb_release_interface(handle, 0);
libusb_close(handle);
libusb_exit(NULL);
用户空间驱动的适用场景:设备原型验证、临时测试工具、不需要高性能的简单设备。生产环境还是建议写内核驱动,中断延迟和吞吐量都更好。
七、调试技。
usbmon:内核USB抓包
usbmon是内核自带的USB抓包工具,相当于USB版的tcpdump。
# 1. 挂载debugfs
mount -t debugfs none /sys/kernel/debug
# 2. 查看可用总线
cat /sys/kernel/debug/usb/usbmon/0t
# 3. 抓取总线1的数据
cat /sys/kernel/debug/usb/usbmon/1t > usb_trace.log
# 4. 也可以用二进制格式(信息更全)
cat /sys/kernel/debug/usb/usbmon/1u > usb_trace.bin
输出格式(文本模式)。
ee998280 2765.123456 S Co:001:00 s 80 06 0100 0000 0012 18
ee998280 2765.123789 C Co:001:00 0 18
S= Submit(提交urb),C= Complete(urb完成。-Co= 控制传输,Bo= 批量OUT,Bi= 批量INs 80 06 0100 0000 0012= 控制传输的setup包内存- 末尾数字 = 数据长度
Wireshark + usbmon
Wireshark可以直接解析usbmon的二进制输出,比看文本省事:
# 1. 生成二进制抓包
cat /sys/kernel/debug/usb/usbmon/1u | socat - UDP4-DATAGRAM:127.0.0.1:5555 &
# 或者直接用Wireshark。# Wireshark 4.x原生支持usbmon接口
更简单的方式——Wireshark 4.x直接选择USB捕获接口,不用手动cat。
动态调试
# 开启usbcore的所有调试输出
echo 'module usbcore +p' > /sys/kernel/debug/dynamic_debug/control
# 开启特定驱动的调试
echo 'file my_usb_drv.c +p' > /sys/kernel/debug/dynamic_debug/control
# 开启USB核心的枚举调试echo 'file hub.c +p' > /sys/kernel/debug/dynamic_debug/control
# 关闭调试
echo 'module usbcore -p' > /sys/kernel/debug/dynamic_debug/control
动态调试不需要重新编译内核,运行时开关,生产环境也能用。+p表示开启printk,-p表示关闭。
八、常见问题
Q1:probe不被调用。
最常见的原因是id_table匹配规则不对。排查步骤:
lsusb -v -d VID:PID确认设备的VID/PID- 检查
MODULE_DEVICE_TABLE宏有没有。3.cat /sys/kernel/debug/usb/devices看内核识别到的设备信息4.ls /sys/bus/usb/drivers/确认驱动已注。5. 检查是否有其他驱动先匹配上了——USB核心按注册顺序匹配,先到先得
如果设备是HID类(键盘鼠标),内核的usbhid驱动会先抢走。需要给设备添加quirks或者写driver_override。
# 强制指定驱动
echo "my_usb_drv" > /sys/bus/usb/devices/1-1:1.0/driver_override
echo "1-1:11.0" > /sys/bus/usb/drivers_probe
Q2:urb提交返回-ENODEV。
设备已经物理断开,但驱动还没收到disconnect回调。这是竞态窗口——设备拔出和urb提交之间没有同步保护。解决方法:
- disconnect里先
usb_set_intfdata(intf, NULL) - 提交urb前检查
usb_get_intfdata()是否为NULL - 用
io_mutex保护提交操作
// 安全的urb提交模式
mutex_lock(&dev->io_mutex);
if (dev->intf) // disconnect还没被调试 ret = usb_submit_urb(dev->bulk_in_urb, GFP_KERNEL);
mutex_unlock(&dev->io_mutex);
Q3:批量传输丢数据。
几个排查方向。
1. 端点方向搞反*:usb_sndbulkpipe和usb_rcvbulkpipe别用。2. 缓冲区太:必须≥wMaxPacketSize,批量传输一次最4096字节(高速)。4字节(全速)
3. 回调里没重新提交urb:urb是一次性的,回调执行完就结束了,想持续接收必须重新submit
4. GFP_ATOMIC vs GFP_KERNEL*:回调里必须用GFP_ATOMIC,因为回调可能在中断上下文执行
九、总结
USB驱动开发速查看
| 操作 | API | 注意事项 |
|---|---|---|
| 定义匹配规则 | USB_DEVICE(vid, pid) |
末尾空项{} |
| 导出匹配置 | MODULE_DEVICE_TABLE(usb, tbl) |
不加modprobe找不 |
| 注册驱动 | module_usb_driver(drv) |
替代module_init/exit |
| 获取usb_device | interface_to_usbdev(intf) |
probe参数是interface |
| 查找端点 | 遍历intf->cur_altsetting->endpoint[] |
用usb_endpoint_is_bulk_*判断 |
| 构造管理 | usb_rcvbulkpipe(dev, ep) |
IN用rcv,OUT用snd |
| 分配urb | usb_alloc_urb(0, GFP_KERNEL) |
0=非等 |
| 填充urb | usb_fill_bulk_urb(...) |
设置回调函数 |
| 提交urb | usb_submit_urb(urb, GFP_KERNEL) |
回调里用GFP_ATOMIC |
| 杀掉urb | usb_kill_urb(urb) |
等回调执行完 |
| 释放urb | usb_free_urb(urb) |
kill之后才能free |
| 同步控制传输 | usb_control_msg(...) |
不能在中断上下文 |
| 同步批量传输 | usb_bulk_msg(...) |
timeout别设0 |
| 保存私有数据 | usb_set_intfdata(intf, dev) |
probe里设 |
| 获取私有数据 | usb_get_intfdata(intf) |
disconnect里取 |
本文首发于linuxros.cn,转载请注明出处。