1. 怎样理解 Gadget 框架
USB 协议是主从结构:
左边主机,右边从机;USB 有主机控制器 UHC 和从机控制器 UDC,主机侧有 USB Device Driver,从机侧有 USB Function Driver。
意思是说,一个开发板,可以当 USB 主机,接鼠标、键盘等从机;一个开发板也可以当 U 盘,接入 PC 电脑,此时开发板是从机。因此,我们要掌握两套驱动框架。这是 Linux 下 USB 相对于 I2C、SPI 复杂的地方,I2C 等根本不会考虑主控 Soc 作为从机的情况,但 USB 需要考虑。
本文主要针对主控 Soc 作为 USB 从机的情况,Linux 为其提供了 Gadget 框架。
编写 USB 设备驱动程序时,主要是:
基于 Gadget 驱动框架模拟一个 USB 设备时,endpoint 的数据传输能力是底层的 USB Device Controller 驱动提供的,我们要做的就是:
Gadget 的含义是"小器件",在 Linux 的 USB 系统中,它表示"usb device"。Gadget 驱动程序,就是用来模拟 USB Device。对于真实的 USB Device,它有两大要素:
在学习过程中,记住这几个要点非常有帮助:
USB 传输的核心是 endpoint,使用 endpoint 可以收发数据。在 endpoint 之上,就可以模拟 USB 串口、USB 触碰屏、USB 摄像头。基于这个角度,Gadget 框架可以分为两层:
不同平台采用的 USB 控制器型号不同,确认型号方法是从 dtb 反编译,找到包含 otg 字符的节点,在 Linux code 中搜索 dts 节点的 compatible,可以找到对应的 usb 从机控制器驱动。
对于底层 endpoint 的代码,需要从 UDC 驱动开始分析:
Linux-4.9.88\drivers\usb\chipidea\ci_hdrc_imx.c
ci_hdrc_imx_probe
ci_hdrc_add_device
pdev = platform_device_alloc("ci_hdrc", id);
// Linux-4.9.88\drivers\usb\chipidea\core.c
static struct platform_driver ci_hdrc_driver = {
.probe = ci_hdrc_probe,
.remove = ci_hdrc_remove,
.driver = {
.name = "ci_hdrc",
.pm = &ci_pm_ops,
},
};
ci_hdrc_probe
ret = ci_hdrc_gadget_init(ci);
udc_start
Linux-5.4\drivers\usb\dwc2\platform.c
dwc2_driver_probe
retval = dwc2_gadget_init(hsotg);
模拟各类 USB 设备时,软件怎么分层?以访问设备、获取描述符为例:
所以,从获取描述符的角度看看,上层软件至少分为 2 层:
软件层次可以进一步细化,如下图:
这涉及 2 个结构体:
struct usb_composite_dev {
struct usb_gadget *gadget;
struct usb_request *req;
struct usb_request *os_desc_req;
struct usb_configuration *config;
/* OS String is a custom (yet popular) extension to the USB standard. */
u8 qw_sign[OS_STRING_QW_SIGN_LEN];
u8 b_vendor_code;
struct usb_configuration *os_desc_config;
unsigned int use_os_string:1;
/* private: */
/* internals */
unsigned int suspended:1;
struct usb_device_descriptor desc;
struct list_head configs;
struct list_head gstrings;
struct usb_composite_driver *driver;
u8 next_string_id;
char *def_manufacturer;
/* the gadget driver won't enable the data pullup
* while the deactivation count is nonzero.
*/
unsigned deactivations;
/* the composite driver won't complete the control transfer's
* data/status stages till delayed_status is zero.
*/
int delayed_status;
/* protects deactivations and delayed_status counts*/
spinlock_t lock;
/* public: */
unsigned int setup_pending:1;
unsigned int os_desc_pending:1;
};
struct usb_udc {
struct usb_gadget_driver *driver;
struct usb_gadget *gadget;
struct device dev;
struct list_head list;
bool vbus;
};
假设你要【模拟】一个 USB 设备:
以 zero.c 为例:
从下到上涉及这些文件:
阅读源码时,入口函数是usb_composite_probe(&zero_driver)
:
函数调用过程中主要的函数如下,重点关注"xxx_bind"函数,bind 就是初始化的意思:
深入解读描述符的构造过程,可以得到下面的图:
安装好 gadget 驱动程序后(比如 modprobe g_zero), 它只是构造好了各类描述符。在设备的枚举过程会读取描述符。
使用 OTG 线连接电脑和开发板时,电脑软件会执行如下操作:
上述过程里,设备方都是接收到 Host 发给 endpoint 0 的数据,然后做出回应。不同的 Gadget 设备,在返回描述符给主机时,这些操作都是一样的,只是回应的数据不同而已。源码分析的起点都是某个中断函数:
IMX6ULL 芯片中 USB 控制器型号是 chipidea,在Linux-4.9.88\drivers\usb\chipidea\core.c
中注册了中断函数:
ci_hdrc_probe
ret = devm_request_irq(dev, ci->irq, ci_irq, IRQF_SHARED,
ci->platdata->name, ci);
发生中断后,对于 endpoint 0 的数据处理流程如下:
// Linux-4.9.88\drivers\usb\chipidea\core.c
ci_irq
/* Handle device/host interrupt */
if (ci->role != CI_ROLE_END)
ret = ci_role(ci)->irq(ci); // udc_irq
// Linux-4.9.88\drivers\usb\chipidea\udc.c
udc_irq
if (USBi_UI & intr)
// Linux-4.9.88\drivers\usb\chipidea\udc.c
isr_tr_complete_handler(ci);
/* Only handle setup packet below */
if (i == 0 &&
hw_test_and_clear(ci, OP_ENDPTSETUPSTAT, BIT(0)))
// Linux-4.9.88\drivers\usb\chipidea\udc.c
isr_setup_packet_handler(ci);
函数isr_setup_packet_handler
就是处理 endpoint 0 接收到的控制传输的关键。
STM32MP157 芯片中 USB 控制器型号是 dwc2,在Linux-5.4\drivers\usb\dwc2\gadget.c
中注册了中断函数:
dwc2_gadget_init
ret = devm_request_irq(hsotg->dev, hsotg->irq, dwc2_hsotg_irq,
IRQF_SHARED, dev_name(hsotg->dev), hsotg);
发生中断后,函数dwc2_hsotg_irq
被调用,它处理 endpoint 中断有两种方法:
dwc2_hsotg_epint
来处理dwc2_hsotg_handle_rx
来处理以dwc2_hsotg_epint
为例进行分析,对于 endpoint 0 的数据处理流程如下:
// Linux-5.4\drivers\usb\dwc2\gadget.c
dwc2_hsotg_irq
// 处理endpoint中断
for (ep = 0; ep < hsotg->num_of_eps && daint_out; ep++, daint_out >>= 1) {
if (daint_out & 1)
dwc2_hsotg_epint(hsotg, ep, 0);
}
for (ep = 0; ep < hsotg->num_of_eps && daint_in; ep++, daint_in >>= 1) {
if (daint_in & 1)
dwc2_hsotg_epint(hsotg, ep, 1);
}
函数dwc2_hsotg_epint
中,对于 endpoint 0 的处理如下:
// Linux-5.4\drivers\usb\dwc2\gadget.c
dwc2_hsotg_epint
if (idx == 0 && !hs_ep->req)
dwc2_hsotg_enqueue_setup(hsotg);
函数dwc2_hsotg_enqueue_setup
被调用时,Gadget 设备已经收到了 SETUP 令牌包,但是还没收到 DATA0 令牌包。dwc2_hsotg_enqueue_setup
的作用是,设置、启动一个 request,核心在于设置了 request 的 complete 函数(当 SETTUP 事务完成后这个函数被调用):
当控制传输的"setup事务"完成时,函数dwc2_hsotg_complete_setup
被调用。
无论是 MX6ULL 的函数isr_setup_packet_handler
,还是 STM32M157 的函数dwc2_hsotg_complete_setup
,它们都是在 Gadget 设备收到"SETUP事务"后才被调用。接收完"SETUP事务"后,就可以从里面知道这个控制传输想做什么(req.bRequest 是什么),然后就可以处理它了。
怎么处理呢?可以分为 3 层:
UDC 驱动程序:类似"设置地址"的控制传输,在底层的 UDC 驱动程序里就可以处理,
USB_REQ_SET_ADDRESS
USB_REQ_SET_FEATURE // 有一些请求可能需要上报改 gadget driver
USB_REQ_CLEAR_FEATURE // 有一些请求可能需要上报改 gadget driver
USB_REQ_GET_STATUS // 有一些请求可能需要上报改 gadget driver
IMX6ULL: Linux-4.9.88\drivers\usb\chipidea\udc.c, 函数 isr_setup_packet_handler
STM32MP157: Linux-5.4\drivers\usb\dwc2\gadget.c, 函数 dwc2_hsotg_complete_setup
gadget driver:涉及描述符的操作
USB_REQ_GET_DESCRIPTOR
USB_REQ_SET_CONFIGURATION
USB_REQ_GET_CONFIGURATION
USB_REQ_SET_INTERFACE
USB_REQ_GET_INTERFACE
USB_REQ_GET_STATUS // 底层 UDC 驱动无法处理的话, gadget driver 来处理
USB_REQ_CLEAR_FEATURE // 底层 UDC 驱动无法处理的话, gadget driver 来处理
USB_REQ_SET_FEATURE // 底层 UDC 驱动无法处理的话, gadget driver 来处理
文件:drivers\usb\gadget\composite.c
函数:composite_setup
usb_configuration 或 usb_function 的处理:这是二选一的。大部分设备使用控制传输实现标准的 USB 请求,但是也可以用控制传输来进行实现相关的请求,对于这些非标准的请求,就需要上层驱动来处理。
在 USB 协议中,永远是 Host 主动发起传输。作为一个 Gadget 驱动程序,它永远都是这样:
USB 传输的对象是 endpoint,使用流程如下:
功能驱动里构造的 usb_request,可以是接收 Host 发来的数据,也可以是向 Host 发送数据。当传输完成,usb_request 的回调函数被调用。
在回调函数里,可以再次提交 usb_request。
怎么调用到回调函数?源头是 UDC 的中断函数。
调用关系如下:
// Linux-4.9.88\drivers\usb\chipidea\core.c
ci_irq
/* Handle device/host interrupt */
if (ci->role != CI_ROLE_END)
ret = ci_role(ci)->irq(ci); // udc_irq
udc_irq
if (USBi_UI & intr)
isr_tr_complete_handler(ci);
err = isr_tr_complete_low(hwep);
usb_gadget_giveback_request(&hweptemp->ep, &hwreq->req);
req->complete(ep, req);
调用关系如下:
// Linux-5.4\drivers\usb\dwc2\gadget.c
dwc2_hsotg_irq
// 处理endpoint中断
for (ep = 0; ep < hsotg->num_of_eps && daint_out;
ep++, daint_out >>= 1) {
if (daint_out & 1)
dwc2_hsotg_epint(hsotg, ep, 0);
dwc2_hsotg_handle_outdone(hsotg, idx);
dwc2_hsotg_complete_request(hsotg, hs_ep, hs_req, result);
usb_gadget_giveback_request(&hs_ep->ep, &hs_req->req);
req->complete(ep, req);
}
for (ep = 0; ep < hsotg->num_of_eps && daint_in;
ep++, daint_in >>= 1) {
if (daint_in & 1)
dwc2_hsotg_epint(hsotg, ep, 1);
dwc2_hsotg_complete_in(hsotg, hs_ep);
dwc2_hsotg_complete_request(hsotg, hs_ep, hs_req, 0);
usb_gadget_giveback_request(&hs_ep->ep, &hs_req->req);
req->complete(ep, req);
}
loopback 就是回环,Host 发数据给 Gadget,然后再读 Gadget 就可以得到原样的数据。
Host 选择某个配置时,默认会选择这个配置下那些接口的第 0 个设置(altsetting);
当 Host 发来 USB_REQ_SET_INTERFACE 请求时,可以选择指定的设置。
所以,我们从 f_loopback.c 的函数loopback_set_alt
开始分析。
调用关系为:
loopback_set_alt
enable_loopback
result = enable_endpoint(cdev, loop, loop->in_ep);
result = enable_endpoint(cdev, loop, loop->out_ep);
result = alloc_requests(cdev, loop);
如上图所示,先提交的是 out_req,它在等待 Host 发来数据。
假设断点 loop->out_ep 的 out_req 获得了数据,它的回调函数loopback_complete
被调用,如下:
前面的 f_loopback 也实现了两个方向的数据传输:Host 到 Gadget、Gadget 到 Host,但是它们之间是有依赖关系的,Host 必须先发送数据再读数据。
f_sourcesink.c 也实现了两个方向的数据传输:Host 到 Gadget、Gadget 到 Host,它们是独立的。
Host 选择某个配置时,默认会选择这个配置下那些接口的第 0 个设置(altsetting);
当 Host 发来 USB_REQ_SET_INTERFACE 请求时,可以选择指定的设置。
所为,我们从 f_sourcesink.c 的函数sourcesink_set_alt
开始分析。
sourcesink_set_alt
enable_source_sink(cdev, ss, alt);
作为"source",函数source_sink_start_ep
会构造数据、提交 usb_request:
当 Host 读取到数据后,usb_request 的回调函数被调用,它只是再次提交 USB 请求,给 Host 继续提供跟上次一样的数据:
仍然从 f_sourcesink.c 的函数sourcesink_set_alt
开始分析。
sourcesink_set_alt
enable_source_sink(cdev, ss, alt);
作为"sink",函数source_sink_start_ep
会故意把数据设置为 0x55(这是为了调试,当读到数据时可以看到 0x55 被覆盖)、提交 usb_request:
当 Host 发来数据,usb_request 的回调函数被调用,它检查收到的数据,再次提交 usb_request:
本文分享自 嵌入式Linux系统开发 微信公众号,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文参与 腾讯云自媒体同步曝光计划 ,欢迎热爱写作的你一起参与!