前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >USB Gadget 驱动程序框架

USB Gadget 驱动程序框架

作者头像
Jasonangel
发布2023-08-22 13:57:34
3.3K0
发布2023-08-22 13:57:34
举报
文章被收录于专栏:嵌入式Linux系统开发

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 设备驱动程序时,主要是:

  • 读取设备的各类描述符,比如 endpoint 描述符,得到端点号
  • 使用底层 USB Host Controller 驱动程序提供的 API 函数,从 endpoint 上读写数据

基于 Gadget 驱动框架模拟一个 USB 设备时,endpoint 的数据传输能力是底层的 USB Device Controller 驱动提供的,我们要做的就是:

  • 提供各类设备描述符
  • 使用底层 USB Device Controller 驱动程序提供的 API 函数,从 endpoint 得到数据、反馈数据

Gadget 的含义是"小器件",在 Linux 的 USB 系统中,它表示"usb device"。Gadget 驱动程序,就是用来模拟 USB Device。对于真实的 USB Device,它有两大要素:

  • 怎么表示自己?
    • 每个 USB Device 都有 1 个设备描述符
    • 都 1 个或多个配置描述符
    • 每个配置描述符里面有 1个 或多个接口描述符
    • 每个接口描述符里面有 0 个多个端点描述符
  • 怎么进行数据传输?
    • 通过端点进行传输
    • 有端点的操作函数

在学习过程中,记住这几个要点非常有帮助:

  • 各类描述符的构造
  • USB Host 获得 Gadget 各类描述符的过程
  • 数据传输的流程

2. 从硬件软件角度理解 Gadget 框架

USB 传输的核心是 endpoint,使用 endpoint 可以收发数据。在 endpoint 之上,就可以模拟 USB 串口、USB 触碰屏、USB 摄像头。基于这个角度,Gadget 框架可以分为两层:

  • 底层 endpoint 操作
  • 上层模拟各类 USB 设备

2.1 底层硬件操作_UDC 驱动

不同平台采用的 USB 控制器型号不同,确认型号方法是从 dtb 反编译,找到包含 otg 字符的节点,在 Linux code 中搜索 dts 节点的 compatible,可以找到对应的 usb 从机控制器驱动。

对于底层 endpoint 的代码,需要从 UDC 驱动开始分析:

  • IMX6ULL 的代码:Linux-4.9.88\drivers\usb\chipidea\ci_hdrc_imx.c
代码语言:javascript
复制
  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      
  • STM32MP157 的代码:Linux-5.4\drivers\usb\dwc2\platform.c
代码语言:javascript
复制
  dwc2_driver_probe
      retval = dwc2_gadget_init(hsotg);    

2.2 上层软件操作

模拟各类 USB 设备时,软件怎么分层?以访问设备、获取描述符为例:

  • Host 要分配地址、把地址发送给设备:不管要模拟什么设备,Gadget 都必须接收地址,这部分由 usb_gadget (硬件相关的驱动程序)实现
  • Host 要读取各类描述符,这些描述符是由上层的驱动程序提供的
  • 怎么把上层的描述符通过底层的 usb_gadget 传回给 Host?还需要一个中间层。Host 获取描述符时,方法是固定、通用的,这些方法可以由内核统一提供,这就是:usb_gadget_driver。

所以,从获取描述符的角度看看,上层软件至少分为 2 层:

  • usb_gadget_driver:实现一些通用的 USB 访问方法,比如 Host 访问描述符时,由 usb_gadget_driver 提供
  • 在这上面提供各类描述符,实际上,描述符的提供还可以分为两层:
    • 设备描述符、配置描述符:由程序员决定,由 usb_composite_driver 提供
    • 接口描述符、endpoint 描述符:由内核事先实现的、常用的 function driver 提供

软件层次可以进一步细化,如下图:

这涉及 2 个结构体:

  • usb_composite_dev:它里面汇集有各类描述符、有一个 usb_funciton 链表(实现数据传输)
代码语言:javascript
复制
  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;
  };
  • usb_udc:UDC 的本意是"usb device controller",usb_udc 结构体里面有 usb_gadget (表示 UDC 本身)、usb_gadget_driver()
代码语言:javascript
复制
  struct usb_udc {
   struct usb_gadget_driver *driver;
   struct usb_gadget  *gadget;
   struct device   dev;
   struct list_head  list;
   bool    vbus;
  };

3. 从构造描述符的角度理解 Gadget 框架

假设你要【模拟】一个 USB 设备:

  • 这个 USB 设备含有厂家信息:它记录在设备描述符里,所以设备描述符应该由你提供
  • 这个芯片可能有多种配置,这也是由你决定,所以配置描述符应该由你提供
  • 某个配置下多个接口,接口就是功能,Linux 内核里事先提供了很多功能的驱动程序,所以:接口描述符是内核提供的
  • 某个接口下需要什么端点,也是内核里各类功能的驱动程序提供的

以 zero.c 为例:

  • 配置 1:loopback,Host 写数据给它,就可以读出原样的数据
  • 配置 2:sourcesink,Host 写数据给它(它只是记录下数据),Host 还可以读数据(读到的都是0)

从下到上涉及这些文件:

阅读源码时,入口函数是usb_composite_probe(&zero_driver)

函数调用过程中主要的函数如下,重点关注"xxx_bind"函数,bind 就是初始化的意思:

  • usb_composite_probe
  • composite_bind
  • zero_bind
  • sourcesink_bind/loopback_bind

深入解读描述符的构造过程,可以得到下面的图:

  • 构造出一个 usb_composite_dev 结构体
  • 它把各层串联起来,里面构造有设备描述符、配置描述符、接口描述符、端点描述符

4. 从获取描述符的角度理解 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)

4.1 IMX6ULL 的核心函数

IMX6ULL 芯片中 USB 控制器型号是 chipidea,在Linux-4.9.88\drivers\usb\chipidea\core.c中注册了中断函数:

代码语言:javascript
复制
ci_hdrc_probe
 ret = devm_request_irq(dev, ci->irq, ci_irq, IRQF_SHARED,
   ci->platdata->name, ci);    

发生中断后,对于 endpoint 0 的数据处理流程如下:

代码语言:javascript
复制
// 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 接收到的控制传输的关键。

4.2 STM32MP157的核心函数

STM32MP157 芯片中 USB 控制器型号是 dwc2,在Linux-5.4\drivers\usb\dwc2\gadget.c中注册了中断函数:

代码语言:javascript
复制
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 中断有两种方法:

  • 使用 DMA 时:调用dwc2_hsotg_epint来处理
  • 不使用 DMA 时:调用dwc2_hsotg_handle_rx来处理

dwc2_hsotg_epint为例进行分析,对于 endpoint 0 的数据处理流程如下:

代码语言:javascript
复制
// 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 的处理如下:

代码语言:javascript
复制
// 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被调用。

4.3 如何处理控制传输

无论是 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 请求,但是也可以用控制传输来进行实现相关的请求,对于这些非标准的请求,就需要上层驱动来处理。

5. 从数据传输的角度理解 Gadget 框架

5.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 的回调函数

5.2 endpoint 是核心

USB 传输的对象是 endpoint,使用流程如下:

  • 功能驱动里,通过 endpoint 描述符表明需要怎样的 endpoint,比如(注意:bEndpointAddress 是表明方向,里面还没有地址,drivers\usb\gadget\function\f_loopback.c):
  • 功能驱动里,它的 bind 函数根据 endpoint 描述符向底层申请分配 endpoint,比如:
  • 功能驱动里,使能 endpoint,比如:
  • 功能驱动里,给 endpoint 分配 buffer、设置 usb_request、提交 usb_request,比如:

5.3 回调函数

功能驱动里构造的 usb_request,可以是接收 Host 发来的数据,也可以是向 Host 发送数据。当传输完成,usb_request 的回调函数被调用。

在回调函数里,可以再次提交 usb_request。

怎么调用到回调函数?源头是 UDC 的中断函数。

5.3.1 IMX6ULL

调用关系如下:

代码语言:javascript
复制
// 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);    
5.3.2 STM32MP157

调用关系如下:

代码语言:javascript
复制
// 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);
  } 

5.4 f_loopback分析

loopback 就是回环,Host 发数据给 Gadget,然后再读 Gadget 就可以得到原样的数据。

5.4.1 Gadget接收数据

Host 选择某个配置时,默认会选择这个配置下那些接口的第 0 个设置(altsetting);

当 Host 发来 USB_REQ_SET_INTERFACE 请求时,可以选择指定的设置。

所以,我们从 f_loopback.c 的函数loopback_set_alt开始分析。

调用关系为:

代码语言:javascript
复制
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被调用,如下:

5.4.2 Gadget 回环数据

5.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)
5.5.1 Host 写 Gadget

Host 选择某个配置时,默认会选择这个配置下那些接口的第 0 个设置(altsetting);

当 Host 发来 USB_REQ_SET_INTERFACE 请求时,可以选择指定的设置。

所为,我们从 f_sourcesink.c 的函数sourcesink_set_alt开始分析。

代码语言:javascript
复制
sourcesink_set_alt
 enable_source_sink(cdev, ss, alt);  

作为"source",函数source_sink_start_ep会构造数据、提交 usb_request:

当 Host 读取到数据后,usb_request 的回调函数被调用,它只是再次提交 USB 请求,给 Host 继续提供跟上次一样的数据:

5.5.2 Host 读 Gadget

仍然从 f_sourcesink.c 的函数sourcesink_set_alt开始分析。

代码语言:javascript
复制
sourcesink_set_alt
 enable_source_sink(cdev, ss, alt);  

作为"sink",函数source_sink_start_ep会故意把数据设置为 0x55(这是为了调试,当读到数据时可以看到 0x55 被覆盖)、提交 usb_request:

当 Host 发来数据,usb_request 的回调函数被调用,它检查收到的数据,再次提交 usb_request:

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2023-06-25,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 嵌入式Linux系统开发 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 2. 从硬件软件角度理解 Gadget 框架
    • 2.1 底层硬件操作_UDC 驱动
      • 2.2 上层软件操作
      • 3. 从构造描述符的角度理解 Gadget 框架
      • 4. 从获取描述符的角度理解 Gadget 框架
        • 4.1 IMX6ULL 的核心函数
          • 4.2 STM32MP157的核心函数
            • 4.3 如何处理控制传输
            • 5. 从数据传输的角度理解 Gadget 框架
              • 5.1 使用流程
                • 5.2 endpoint 是核心
                  • 5.3 回调函数
                    • 5.3.1 IMX6ULL
                    • 5.3.2 STM32MP157
                  • 5.4 f_loopback分析
                    • 5.4.1 Gadget接收数据
                    • 5.4.2 Gadget 回环数据
                  • 5.5 f_sourcesink 分析
                    • 5.5.1 Host 写 Gadget
                    • 5.5.2 Host 读 Gadget
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档