1. USB Gadget 框架概述设计出发点1.1 文件功能和作用1.2 功能(Function)目录2. 软件架构分析2.1 核心组件2.2 USB Gadget 驱动框架功能层(Functions)Gadget核心层UDC (USB Device Controller) 驱动层3. 从硬件软件角度理解Gadget框架3.1 底层硬件操作_UDC驱动3.1.1 主要数据结构3.1.2 数据结构关系3.1.3 初始化流程3.2 上层软件操作4. 从构造描述符的角度理解Gadget框架4.1 描述符层次结构4.2 Gadget框架中的描述符构造4.3 ConfigFS接口下的描述符构造4.4 zero.c 文件分析5. 从获取描述符的角度理解Gadget框架5.1 IMX6ULL的核心函数5.2 STM32MP157的核心函数5.3 如何处理控制传输6. 从数据传输的角度理解Gadget框架6.1 使用流程6.2 USB Endpoint 传输流程6.2.1 通过endpoint描述符表明需要的endpoint类型6.2.2 bind函数根据endpoint描述符向底层申请分配endpoint6.2.3 功能驱动使能endpoint6.2.4 给endpoint分配buffer、设置usb_request、提交usb_request6.2.5 完整流程示例:ACM通知端点的使用6.3 回调函数6.3.1 IMX6ULL6.3.2 STM32MP1576.4 loopback分析6.4.1 Gadget接收数据6.4.2 Gadget回环数据6.5 f_sourcesink分析6.5.1 Host与Gadget通信6.5.2 Host读Gadget6.6 不同传输类型的处理7. 总结
参考资料:
- Linux下USB gadget设备详解:https://www.docin.com/p-1852320293.html
- Linux usb gadget框架概述:https://blog.csdn.net/daocaokafei/article/details/114114824
- USB设备驱动程序-USB Gadget Driver(二):https://blog.csdn.net/chenzhen1080/article/details/53742924
- usb gadge驱动设计之我是zero:https://www.bbsmax.com/A/Ae5RvbwM5Q/
- Linux-USB驱动笔记(四)--USB整体框架: https://qingmu.blog.csdn.net/article/details/119979199
- USB gadget设备驱动解析: https://www.likecs.com/show-843861.html
- 调试软件:USB view 、bushound 、及一些硬件USB信号分析仪
- 可以用wireshark+usbmon捕捉usb协议数据包。
1. USB Gadget 框架概述
USB Gadget框架是Linux内核中支持设备端USB功能的软件架构,它使Linux设备能够作为USB外设(如U盘、网卡、串口等)被主机识别和使用。与USB主机模式不同,Gadget模式下Linux设备充当USB从设备角色,连接到USB主机(如PC)上。
USB Gadget框架的发展经历了多个版本的演进:
- 早期的Gadget驱动是单一功能的,每个驱动只能实现一种USB设备功能
- 随后引入了复合设备支持,允许一个物理设备呈现为多个逻辑USB设备
- 在Linux 2.6.30引入ConfigFS接口后,Gadget框架实现了更灵活的运行时配置能力
- 现代Linux内核中的USB Gadget已支持高速(2.0)和超高速(3.0/3.1)USB协议
Gadget框架广泛应用于嵌入式系统、移动设备和开发板,如安卓手机的ADB模式、各种开发板的虚拟网卡/串口功能等。
设计出发点
USB Gadget框架的核心设计理念是提供一个灵活、模块化的架构,解决以下关键问题:
- 硬件抽象:屏蔽不同USB控制器硬件的差异,提供统一的软件接口
- 功能重用:允许功能模块(如存储、网络、串口等)被不同的Gadget配置复用
- 动态配置:支持运行时配置USB设备的各种属性和功能
- 协议兼容:确保生成的USB描述符和通信过程符合USB标准规范
Gadget框架采用了分层设计模式,将USB控制器驱动、协议栈和功能实现分离,使开发者能专注于特定层次的开发而无需理解整个系统。
1.1 文件功能和作用
文件名 | 功能描述 |
composite.c | 实现复合设备功能,允许多个功能组合成一个 USB 设备 |
config.c | 处理 USB 配置相关功能 |
configfs.c | 提供基于 ConfigFS 的 USB Gadget 配置接口 |
epautoconf.c | 自动配置端点(Endpoint)功能 |
functions.c | 管理 USB 功能(Function)的注册和注销 |
usbstring.c | 处理 USB 字符串描述符 |
u_f.c/u_f.h | 提供 USB 功能(Function)的通用工具函数 |
1.2 功能(Function)目录
function/ 目录包含各种 USB 功能的实现:
文件名 | 功能描述 |
f_acm.c | 实现 USB CDC ACM(串口通信)功能 |
f_ecm.c | 实现 USB CDC ECM(以太网控制模型)功能 |
f_eem.c | 实现 USB CDC EEM(以太网仿真模型)功能 |
f_fs.c | 实现 USB 功能文件系统,允许用户空间配置 USB 功能 |
f_mass_storage.c | 实现 USB 大容量存储设备功能 |
f_rndis.c | 实现 Microsoft RNDIS(远程网络驱动接口规范)功能 |
f_uac1.c/f_uac2.c | 实现 USB 音频类功能(1.0/2.0 版本) |
f_uvc.c | 实现 USB 视频类功能 |
2. 软件架构分析
2.1 核心组件
- UDC (USB Device Controller): 硬件抽象层,与具体的 USB 控制器硬件交互
- Composite Framework: 管理多个功能的组合
- Functions: 实现具体的 USB 功能(如存储、网络、串口等)
- ConfigFS 接口: 提供用户空间配置 USB 设备的接口
2.2 USB Gadget 驱动框架
USB Gadget 驱动框架是 Linux 内核中用于实现 USB 设备功能的子系统。在这个框架中,主要包含三层结构:
功能层(Functions)
功能层实现具体的USB设备功能,如:
- mass_storage.c - USB存储设备功能
- f_acm.c - USB串口功能
- f_ecm.c - USB以太网功能
典型功能驱动结构:
Linux内核中常见的UDC驱动包括:
- dwc3_gadget.c - Synopsys DesignWare USB3控制器驱动
- musb_gadget.c - Mentor USB控制器驱动
- pxa27x_udc.c - PXA处理器系列UDC驱动
Gadget核心层
Gadget核心层(drivers/usb/gadget/udc/udc-core.c)负责:
- 管理UDC驱动的注册与注销
- 提供标准USB请求的处理框架
- 管理USB设备状态(如已连接、已配置等)
核心代码结构:
UDC (USB Device Controller) 驱动层
UDC驱动负责抽象底层USB控制器硬件,主要包括:
下面从四个不同视角,看看它如何在软硬件层次、描述符构造、描述符获取和数据传输方面提供了完整的解决方案。
这些视角相互关联,共同构成了一个完整的工作流程:
- 硬件软件层次构建了基础架构
- 描述符构造实现了设备身份定义
- 描述符获取处理了与主机的初始交互
- 数据传输实现了实际功能
3. 从硬件软件角度理解Gadget框架
USB传输的核心是 endpoint,使用 endpoint 可以收发数据。在 endpoint 之上,就可以模拟USB串口、USB触碰屏、USB摄像头。基于这个角度,Gadget框架可以分为两层:
- 底层endpoint操作
- 上层模拟各类USB设备
3.1 底层硬件操作_UDC驱动
对于底层endpoint的代码,需要从UDC驱动开始分析:
下面以IMX6ULL的代码:
Linux-4.9.88\drivers\usb\chipidea\ci_hdrc_imx.c
为例,ChipIdea (CI) 控制器是一种常见的 USB 控制器实现,在多种 SoC 中被广泛使用,包括 Freescale/NXP 的 i.MX 系列处理器。3.1.1 主要数据结构
struct ci_hdrc
- 主控制器结构体这是ChipIdea控制器的核心数据结构,包含了控制器的所有状态信息、寄存器映射、端点信息等。从源码中可以看出,该结构体包含以下重要成员:
struct ci_hw_ep
- 硬件端点结构体表示控制器中的物理端点,包含端点状态、DMA缓冲区、请求队列等信息:
struct ci_hw_req
- 硬件请求结构体表示一个USB传输请求,包含请求状态、数据缓冲区等信息:
ChipIdea控制器驱动通过以下方式与USB Gadget框架集成:
- 结构体嵌入:
struct ci_hdrc
中嵌入了struct usb_gadget gadget
struct ci_hw_ep
中嵌入了struct usb_ep ep
struct ci_hw_req
中嵌入了struct usb_request req
- 接口实现:
- 控制器驱动实现了
usb_gadget_ops
接口,提供了如get_frame
、wakeup
等功能 - 端点实现了
usb_ep_ops
接口,提供了如queue
、dequeue
、set_halt
等功能
- 数据流:
- Gadget驱动通过
usb_ep_queue
提交请求 - 控制器驱动将请求转换为硬件特定的格式并提交给硬件
- 当传输完成时,控制器驱动通过中断处理程序处理完成事件并通知Gadget驱动
- 状态管理:
- 控制器驱动负责管理USB设备状态(如配置、接口等)
- 当收到USB主机的请求时,控制器驱动会调用Gadget驱动的回调函数
3.1.2 数据结构关系
- udclist:全局 UDC 列表,维护系统中所有的 USB 设备控制器
- udc:USB 设备控制器,代表物理硬件控制器
- 包含 gadget 设备抽象
- 负责与硬件交互
- gadget:USB 设备抽象
- 包含端点列表 (ep_list)
- 实现 usb_gadget 接口
- 通过 gadget_ops 提供操作函数
- ep_list:端点列表
- 包含多个 endpoint 结构
- 管理设备的所有端点
- endpoint:USB 端点
- 通过 ep_ops 提供端点操作函数
- 处理数据传输请求
3.1.3 初始化流程
函数调用栈说明
- ci_hdrc_imx_probe
- IMX平台特定的探测函数,负责初始化IMX平台上的ChipIdea控制器
- 分配和初始化平台资源(中断、时钟、GPIO等)
- ci_hdrc_add_device
- 创建并注册一个新的平台设备
- 使用 platform_device_alloc("ci_hdrc", id) 分配平台设备
- platform_device_alloc("ci_hdrc", id)
- 分配一个新的平台设备结构体,名称为"ci_hdrc"
- 这将触发 ci_hdrc_driver 的probe函数
- ci_hdrc_probe
- 控制器的主要初始化函数
- 分配和初始化 ci_hdrc 结构体
- 映射寄存器空间
- 初始化硬件
- 调用 hw_device_init 初始化设备硬件
- 调用 ci_hdrc_gadget_init 初始化gadget驱动
- ci_hdrc_gadget_init
- 初始化gadget相关的数据结构
- 设置端点操作函数
- 注册gadget设备到USB子系统
- 最终调用 udc_start 启动UDC
- udc_start
- 启动USB设备控制器
- 连接到USB总线,准备好处理USB请求

3.2 上层软件操作
模拟各类USB设备时,软件怎么分层?以访问设备、获取描述符为例:
- Host要分配地址、把地址发送给设备:不管要模拟什么设备,Gadget都必须接收地址,这部分由usb_gadget(硬件相关的驱动程序)实现
- Host要读取各类描述符,这些描述符是由上层的驱动程序提供的
- 怎么把上层的描述符通过底层硬件usb_gadget传回给Host?因此在USB Function Driver 还需要一个协议处理层 (usb_gadget_driver)。
所以,从获取描述符的角度看看,上层软件至少分为2层:
- usb_gadget_driver:实现标准USB协议的通用操作,处理标准请求,如描述符获取,作为上层应用和底层硬件之间的桥梁
- 在这上面提供各类描述符,实际上,描述符的提供还可以分为两层:
- 提供设备描述符和配置描述符:由程序员决定,由复合设备层 (usb_composite_driver)提供
- 接口描述符、endpoint描述符:由内核事先实现的、通用的存储、网络、串口等功能由驱动层 (function driver)提供
软件层次可以进一步细化,如下图:

这涉及2个结构体:
usb_composite_dev
:它里面汇集有各类描述符、有一个usb_funciton
链表(实现数据传输)
usb_udc
:UDC的本意是"usb device controller
",usb_udc结构体里面有usb_gadget
(表示UDC本身)、usb_gadget_driver
(表示UDC driver)
4. 从构造描述符的角度理解Gadget框架
假设你要模拟一个USB设备,USB设备需要通过描述符向主机表明自己的身份和功能
- 这个USB设备含有厂家信息:它记录在设备描述符里,所以设备描述符应该由开发者提供
- 这个芯片可能有多种配置,这也是由你决定,所以配置描述符应该由开发者提供
- 某个配置下多个接口,接口就是功能,Linux内核里事先提供了很多功能的驱动程序,所以:接口描述符是内核提供的
- 某个接口下需要什么端点,也是内核里各类功能的驱动程序提供的
4.1 描述符层次结构
4.2 Gadget框架中的描述符构造
Gadget框架提供了一套完整的描述符构造机制,在Gadget框架中,描述符构造遵循从底向上的组装过程:
- 功能层定义接口和端点:每个功能模块定义自己需要的接口和端点
- Composite框架组装配置描述符:将多个功能的接口和端点组合成配置描述符
- Gadget核心提供设备描述符:设备级描述符由Gadget核心根据注册信息生成
4.3 ConfigFS接口下的描述符构造
在现代Linux系统中,更常用的是通过ConfigFS接口动态构造描述符:
- 用户空间挂载ConfigFS并创建Gadget配置
- 内核将用户空间配置转换为内部描述符结构
4.4 zero.c 文件分析
以zero.c为例:
- 配置1:loopback,Host写数据给它,就可以读出原样的数据
- 配置2:sourcesink,Host写数据给它(它只是记录下数据),Host还可以读数据(读到的都是0)
从下到上涉及这些文件:
函数调用过程中主要的函数如下,重点关注"xxx_bind"函数,可以认为bind就是初始化的意思:
- usb_composite_probe
- composite_bind
- zero_bind
- sourcesink_bind/loopback_bind

深入解读描述符的构造过程,可以得到下面的图:
- 构造出一个usb_composite_dev结构体
- 它把各层串联起来,里面构造有设备描述符、配置描述符、接口描述符、端点描述符

5. 从获取描述符的角度理解Gadget框架
安装好gadget驱动程序后(比如modprobe g_zero),它只是构造好了各类描述符。在设备的枚举过程会读取描述符,枚举过程要做的事情可以参考。
使用OTG线连接电脑和开发板时,电脑软件会执行如下操作:
- 使用控制传输,读取设备信息(设备描述符):第一次读取时,它只需要得到8字节数据,因为第8个数据表示端点0能传输的最大数据长度。
- Host分配地址给设备,然后把新地址发给设备
- 使用新地址,重新读取设备描述符,设备描述符长度是18
- 读取配置描述符:它传入的长度是255,想一次性把当前配置描述符、它下面的接口描述符、端点描述符全部读出来
- 读取字符描述符
上述过程里,设备方都是接收到
Host
发给endpoint 0
的数据,然后做出回应。不同的Gadget
设备,在返回描述符给主机时,这些操作都是一样的,只是回应的数据不同而已。源码分析的起点都是某个中断函数:- IMX6ULL:ci_irq(drivers/usb/chipidea/core.c)
- STM32MP157: dwc2_hsotg_irq(drivers/usb/dwc2/gadget.c)
5.1 IMX6ULL的核心函数
IMX6ULL芯片中USB控制器型号是chipidea,在
Linux-4.9.88\drivers\usb\chipidea\core.c
中注册了中断函数:发生中断后,对于
endpoint 0
的数据处理流程如下:函数
isr_setup_packet_handler
就是处理endpoint 0
接收到的控制传输的关键。5.2 STM32MP157的核心函数
STM32MP157芯片中USB控制器型号是dwc2,在
Linux-5.4\drivers\usb\dwc2\gadget.c
中注册了中断函数:发生中断后,函数
dwc2_hsotg_irq
被调用,它处理endpoint中断有两种方法:- 使用DMA时:调用
dwc2_hsotg_epint
来处理
- 不使用DMA时:调用
dwc2_hsotg_handle_rx
来处理
以
dwc2_hsotg_epint
为例进行分析,对于endpoint 0的数据处理流程如下:函数
dwc2_hsotg_epint
中,对于endpoint 0的处理如下:函数
dwc2_hsotg_enqueue_setup
被调用时,Gadget
设备已经收到了SETUP
令牌包,但是还没收到DATA0
令牌包。dwc2_hsotg_enqueue_setup
的作用是设置、启动一个request
,核心在于设置了request
的complete
函数(当SETTUP
事务完成后这个函数被调用):
当控制传输的"setup事务"完成时,函数
dwc2_hsotg_complete_setup
被调用。5.3 如何处理控制传输
无论是IMX6ULL的函数
isr_setup_packet_handler
,还是STM32M157的函数dwc2_hsotg_complete_setup
,它们都是在Gadget设备收到"SETUP事务"后才被调用。接收完"SETUP事务"后,就可以从里面知道这个控制传输想做什么(req.bRequest是什么),然后就可以处理它了。怎么处理呢?可以分为3层:

- UDC驱动程序:类似"设置地址"的控制传输,在底层的UDC驱动程序里就可以处理,
- 这类请求有:
- 驱动程序位置
- gadget driver:涉及描述符的操作
- 这类请求有:
- 驱动程序位置
- usb_configuration或usb_function的处理:这是二选一的。大部分设备使用控制传输实现标准的USB请求,但是也可以用控制传输来进行实现相关的请求,对于这些非标准的请求,就需要上层驱动来处理。
6. 从数据传输的角度理解Gadget框架
USB设备枚举完成后,主要功能是进行数据传输。Gadget框架如何处理各种传输类型?
6.1 使用流程
在USB协议中,永远是Host主动发起传输。作为一个Gadget驱动程序,它永远都是这样:
- 想接收数据:
- 先构造好usb_request:分配buffer、设置回调函数
- 把usb_request放入队列
- UDC和Host完成USB传输,在usb_request中填充数据,并触发中断调用usb_request的回调函数
- 想发送数据:
- 先构造好usb_request:分配buffer、在buffer里填充数据、设置回调函数
- 把usb_request放入队列
- UDC和Host完成USB传输,把usb_request的数据发给Host,并触发中断调用usb_request的回调函数
6.2 USB Endpoint 传输流程
基于Linux内核源码中的
f_acm.c
文件,我将详细分析USB传输中endpoint的使用流程,并提供完整的函数调用栈及其含义说明。USB传输的对象是endpoint,使用流程主要包括以下四个步骤:
- 功能驱动通过endpoint描述符表明需要的endpoint类型
- 功能驱动的bind函数根据endpoint描述符向底层申请分配endpoint
- 功能驱动使能endpoint
- 功能驱动给endpoint分配buffer、设置usb_request、提交usb_request
6.2.1 通过endpoint描述符表明需要的endpoint类型
函数调用栈:
含义说明:
- 功能驱动首先定义endpoint描述符,指定endpoint的属性
bEndpointAddress
字段指定端点地址和方向(IN/OUT)
bmAttributes
字段指定传输类型(批量、中断、等时等)
wMaxPacketSize
字段指定最大包大小
bInterval
字段指定轮询间隔(对中断和等时传输)
- 这些描述符会在USB枚举过程中发送给主机,告诉主机该设备需要什么样的endpoint
6.2.2 bind函数根据endpoint描述符向底层申请分配endpoint
函数调用栈:
关键代码:
含义说明:
acm_bind()
函数在设备连接到主机时被调用
usb_interface_id()
分配接口ID,用于区分不同的接口
usb_ep_autoconfig()
根据描述符要求自动配置物理endpoint- 该函数会查找符合要求(方向、类型等)的物理endpoint
- 配置成功后,endpoint地址会被填入描述符中
gs_alloc_req()
为通知端点分配请求结构- 通过
usb_ep_alloc_request()
分配USB请求结构 - 设置请求完成回调函数
6.2.3 功能驱动使能endpoint
函数调用栈:
关键代码:
含义说明:
acm_set_alt()
在主机选择接口的备用设置时被调用
config_ep_by_speed()
根据当前速度(全速/高速/超速)配置端点- 选择合适的端点描述符(全速/高速/超速)
- 将描述符与端点关联
usb_ep_enable()
使能物理endpoint,准备数据传输- 调用底层控制器的
enable
操作 - 初始化endpoint的硬件资源
gserial_connect()
连接串行端口,准备数据传输
6.2.4 给endpoint分配buffer、设置usb_request、提交usb_request
函数调用栈:
关键代码:
含义说明:
acm_cdc_notify()
用于发送CDC通知(如串行状态变化)- 使用预先分配的请求结构
- 设置请求的数据缓冲区和长度
- 通过
usb_ep_queue()
提交请求
gs_start_io()
启动I/O操作gs_alloc_req()
分配请求结构gs_alloc_buf()
分配数据缓冲区- 设置请求的完成回调函数
- 通过
usb_ep_queue()
提交请求
- 完成回调函数(
gs_recv_complete
、gs_write_complete
) - 处理传输完成的数据
- 重新提交请求,实现连续传输
6.2.5 完整流程示例:ACM通知端点的使用
- 描述符定义:
- 端点分配(在
acm_bind
中):
- 端点使能(在
acm_set_alt
中):
- 请求提交(在
acm_cdc_notify
中):
6.3 回调函数
功能驱动里构造的usb_request,可以是接收Host发来的数据,也可以是向Host发送数据。当传输完成,usb_request的回调函数被调用。在回调函数里,可以再次提交usb_request。
怎么调用到回调函数?源头是UDC的中断函数。参考IMX6ULL 和 STM32MP157函数调用栈进行分析。
6.3.1 IMX6ULL
调用关系如下:
6.3.2 STM32MP157
调用关系如下:
6.4 loopback分析
6.4.1 Gadget接收数据
loopback功能的核心是将从USB主机接收到的数据原样发送回主机。从源码分析,数据接收流程如下:
- 初始化阶段:
- 在
loopback_bind
函数中,通过usb_ep_autoconfig
配置输入和输出端点 - 在
alloc_requests
函数中,为每个端点分配请求缓冲区
- 数据接收过程:
- 当USB主机发送数据到设备时,数据到达OUT端点
loopback_complete
回调函数处理接收到的数据- 关键代码片段:
6.4.2 Gadget回环数据
loopback功能的回环机制实现如下:
- 请求关联:
- 在
alloc_requests
函数中,IN和OUT请求被关联起来:
- 数据回环过程:
- 当OUT端点接收到数据后,
loopback_complete
回调将数据长度设置到IN请求中 - 然后将IN请求提交到IN端点,将数据发送回主机
- 当IN请求完成后,再次提交OUT请求等待新的数据

- 请求队列管理:
- 系统维护一个请求队列,通过
usb_ep_queue
将请求提交到端点 - 通过
qlen
参数控制队列中的请求数量,提高吞吐量
6.5 f_sourcesink分析
前面的f_loopback也实现了两个方向的数据传输:Host到Gadget、Gadget到Host,但是它们之间是有依赖关系的,Host必须先发送数据再读数据。
f_sourcesink.c也实现了两个方向的数据传输:Host到Gadget、Gadget到Host,它们是独立的。
- Host读Gadget:驱动程序里构造好数据,Host可以读到,Gadget作为源(就是source)
- Host写Gadget:驱动程序里得到Host发来的数据,Gadget作为目的(就是sink)
source_sink功能比loopback更复杂,它不仅可以回环数据,还可以生成特定模式的数据。
6.5.1 Host与Gadget通信
- 端点配置:
- source_sink支持多种速度(全速、高速、超速)的端点配置
- 支持两种接口配置(alt0和alt1),alt1增加了同步传输端点
- 数据传输方向:
- source(IN端点):从设备向主机发送数据
- sink(OUT端点):从主机接收数据到设备
- 接口描述符:
6.5.2 Host读Gadget
- 数据源生成:
- source_sink可以根据配置生成不同模式的数据发送给主机
- 支持多种数据模式,如零填充、随机数据等
- 请求处理:
- 当主机请求读取数据时,设备通过IN端点发送数据
- 数据可以是预先生成的模式数据,也可以是从OUT端点接收到的数据
- 完成回调:
- 当数据传输完成后,通过回调函数处理后续操作
- 可以继续提交新的请求或执行其他操作
6.6 不同传输类型的处理
USB支持四种传输类型,Gadget框架对它们的支持方式如下:
- 控制传输:主要由Gadget核心的ep0处理逻辑实现
- 批量传输:最常用于高速数据传输,如存储设备
- 中断传输:用于低延迟但非周期性的小数据量传输,如HID设备
- 等时传输:用于有固定带宽要求的实时数据,如音频/视频
7. 总结
Linux USB Gadget 驱动框架采用模块化设计,通过 Composite 框架支持多功能组合,通过 ConfigFS 提供灵活的用户空间配置接口。核心组件包括 UDC 驱动、Gadget 核心、功能驱动和配置接口。该框架支持多种 USB 功能,如大容量存储、网络、串口、音频和视频等,能够满足各种嵌入式设备的 USB 设备端需求。