linux下USB驱动

USB拓扑结构不是以总线方式的,而是一棵由几个点对点的连接构成的树,连接线由4根电缆组成(电源,地线,两个数据线)。

USB主控制器负责询问每一个USB设备是否有数据需要发送,也就是说一个USB设备在没有主控制器要求的情况下是不能发送数据的。

USB端点(endpoint):只能往一个方向传送数据。

控制端点:用于配置设备获取设备信息,发送命令到设备或获取设备的状态报告

中断端点:每当USB主控制器要求设备传输数据时,中断端点就以一个固定的速率来传送少量的数据(中断端点和中断不同,一是它无法向USB主控制器发送数据,二是需要等待USB主控制器轮询)

批量端点:用来传输大批量的数据

等时端点:用来传输大批量数据,但数据是否能够到达,则无法保证

每个端点被捆绑为一个USB接口(Interface),一个接口只对应一个逻辑连接

每个USB diver只能处理一个USB接口,所以一个设备也许会对应多个driver,所以USB core在处理USB设备插入时,会针对不同的接口唤醒它认为合适的driver。

一个或多个USB接口被捆绑为配置。一个USB设备可以有多个配置,并且可以在多个配置之间切换。

linux kernel中的USB代码通过urb(USB请求块)与所有的USB设备通信。

HCD(host control driver)提供主控制器驱动的硬件抽象,它只对USB core一个负责,USB core将用户的请求映射到相关的HCD,用户不能直接访问HCD。换句话说USB core就是HCD与USB设备唯一的桥梁。

USB设备驱动只有一个,即usb_generic_driver这个对象,所有USB设备都要绑定到usb_generic_driver上,它的使命可以概括为:为USB选择合适的配置,让设备进入configured状态,Hub集线器用来连接更多USB设备,硬件上实现了USB设备总线枚举过程,软件上实现了USB设备与接口在USB总线上的匹配。

usb_new_device();这时USB设备已经进入了configure状态,调用device_add()在USB总线上寻找驱动,若匹配成功,则加载对应的驱动程序。

USB主机在USB设备和USB主机之间发生传输过程,称为事务。每次事务以2到3个数据包形式进行USB总线传输。数据包:PID,数据/控制信息,CRC校验码。

当USB设备连接到集线器,集线器状态将发生相应变化,并将状态变化信息传递给USB主机。一旦hub集线器的状态发生变化就会产生相应中断,主机端控制器就会执行相应的中断处理函数。

USB主机和设备之间必须支持控制传输,通过端点0进行数据传输。

控制传输分为令牌数据传输和握手阶段。

USB数据传输都以URB请求,URB生成,URB递交,URB释放为主线。


接下来,来看一下linux源码:

首先因为u盘插入的时机是随机的,所以需要开启一个内核线程来检测(内核线程叫做hub_thread,U盘的插入和拔出,USB有四条线路,一条接地,一条接电源电压,一条为D+,一条为D-,这两条差分线分别接下拉电阻,在插入的一瞬间,其中一条差分线会被拉高,就被检测到。

其中核心的函数是hub_events,该函数是一个while循环,其中检测状态的函数为hub_port_status();

该函数的实现:

static int hub_port_status(struct usb_hub *hub, int port1,
          u16 *status, u16 *change)                                                                                                         
  { 
      int ret;
    
      mutex_lock(&hub->status_mutex);
      ret = get_port_status(hub->hdev, port1, &hub->status->port);                                                                          
      if (ret < 4) {       
          dev_err(hub->intfdev,
              "%s failed (err = %d)\n", __func__, ret);                                                                                     
          if (ret >= 0)    
              ret = -EIO;  
      } else {
          *status = le16_to_cpu(hub->status->port.wPortStatus);
          *change = le16_to_cpu(hub->status->port.wPortChange);
  
          ret = 0;
      }
      mutex_unlock(&hub->status_mutex);
      return ret;
  }
改变的状态值存放在change位中,一开始在内核源码中寻找的相应的结构体成员wPortChange,发现所有涉及该结构体成员,都没有最后编译到内核镜像中,后来继续跟发现是不是把该位显式的赋值的,而是在driver/host/下具体的usb控制器去给它赋值的。所以就需要来看一看get_port_status函数:

static int get_port_status(struct usb_device *hdev, int port1,
          struct usb_port_status *data)
  {
      int i, status = -ETIMEDOUT;
  
      for (i = 0; i < USB_STS_RETRIES &&
              (status == -ETIMEDOUT || status == -EPIPE); i++) {
          status = usb_control_msg(hdev, usb_rcvctrlpipe(hdev, 0),
              USB_REQ_GET_STATUS, USB_DIR_IN | USB_RT_PORT, 0, port1,
              data, sizeof(*data), USB_STS_TIMEOUT);                                                                                        
      }
      return status;       
  } 
而usb_control_msg是linux内核中实现的自己的一套机制,通过发送不同的命令比如说上面的USB_REQ_GET_STATUS,最终会调用相应的底层usb控制器的驱动注册的回调函数。

当usb连接的状态位usb_change被置位了,会执行相应的函数,该函数为

static void hub_port_connect_change(struct usb_hub *hub, int port1, u16 portstatus, u16 portchange)

算是最核心的函数了。

其中我认为最重要的是三个函数分别是

struct usb_device *usb_alloc_dev(struct usb_device *parent, struct usb_bus *bus, unsigned port1)

该函数主要是分配了相应的usb_device结构体,初始化device中的设备类型,总线类型等等

static int hub_port_init (struct usb_hub *hub, struct usb_device *udev, int port1, int retry_counter)

该函数主要实现的是u盘能够正常工作前的一系列流程:复位设备,分配地址,获取设备描述符。

int usb_new_device(struct usb_device *udev)

该函数主要是进行usb的枚举,枚举就是要获得u盘中的一些信息,比如说分区信息lun数等等,枚举完成之后就需要创建设备节点了。

其实在这之后还有一个比较重要的转折点,如果是U盘的,并不是创建设备节点那么简单了。

因为u盘是一个存储设备,也就是一种块设备,所以会涉及scsi的接口,也就是说u盘涉及了两个linux子系统,usb子系统和scsi子系统。

之后会进入driver/usb/storage/usb.c的storage_probe函数,

static int storage_probe(struct usb_interface *intf,
              const struct usb_device_id *id)
 {
     struct us_data *us;
     int result;

     /*
      * If libusual is configured, let it decide whether a standard
      * device should be handled by usb-storage or by ub.
      * If the device isn't standard (is handled by a subdriver
      * module) then don't accept it.
      */
     if (usb_usual_check_type(id, USB_US_TYPE_STOR) ||
             usb_usual_ignore_device(intf))
         return -ENXIO;
 
     /*
      * Call the general probe procedures.
      *
      * The unusual_dev_list array is parallel to the usb_storage_usb_ids
      * table, so we use the index of the id entry to find the
      * corresponding unusual_devs entry.
      */
     result = usb_stor_probe1(&us, intf, id,
             (id - usb_storage_usb_ids) + us_unusual_dev_list);
     if (result)
         return result;
 
     /* No special transport or protocol settings in the main module */
 
     result = usb_stor_probe2(us);
     return result;
 }
其中usb_stor_probe1主要是初始化scsi设备,而真正的创建块设备节点是在usb_stor_probe2函数中,

static inline int __must_check scsi_add_host(struct Scsi_Host *host, struct device *dev);

该函数主要创建了名为do_scan_async的线程,该线程会根据lun的数目创建相应的块设备节点。

该函数一层一层调用下去之后,一个关键的函数

static void __scsi_scan_target(struct device *parent, unsigned int channel,
         unsigned int id, unsigned int lun, int rescan)
 {
     struct Scsi_Host *shost = dev_to_shost(parent);
     int bflags = 0;
     int res;
     struct scsi_target *starget;
 
     if (shost->this_id == id)
         /*
          * Don't scan the host adapter
          */
         return;
 
     starget = scsi_alloc_target(parent, channel, id);
     if (!starget)
         return;
     scsi_autopm_get_target(starget);
 
     if (lun != SCAN_WILD_CARD) {
         /*
          * Scan for a specific host/chan/id/lun.
          */
         scsi_probe_and_add_lun(starget, lun, NULL, NULL, rescan, NULL);
         goto out_reap;
     }
 
     /*
      * Scan LUN 0, if there is some response, scan further. Ideally, we
      * would not configure LUN 0 until all LUNs are scanned.
      */
     res = scsi_probe_and_add_lun(starget, 0, &bflags, NULL, rescan, NULL);
     if (res == SCSI_SCAN_LUN_PRESENT || res == SCSI_SCAN_TARGET_PRESENT) {
         if (scsi_report_lun_scan(starget, bflags, rescan) != 0)
             /*
              * The REPORT LUN did not scan the target,
              * do a sequential scan.
              */

             scsi_sequential_lun_scan(starget, bflags,
                          starget->scsi_level, rescan);
     }
 
  out_reap:
     scsi_autopm_put_target(starget);
     /* now determine if the target has any children at all
      * and if not, nuke it */
     scsi_target_reap(starget);
 
     put_device(&starget->dev);
 }
其实不论怎么样,程序走到这里都会去创建一个块设备节点,在创建完一次之后,根据之前获得max_lun数目之后再去选在是不是要再创建块设备节点号。

真正添加块设备节点的动作的函数是scsi_add_lun函数中的scsi_sysfs_add_sdev函数。

一旦添加完块设备节点之后,drivers/scsi/sd.c中的sd_probe函数就会被调用。

 static int sd_probe(struct device *dev)
 {
     struct scsi_device *sdp = to_scsi_device(dev);
     struct scsi_disk *sdkp;
     struct gendisk *gd;
     int index;
     int error;
 
     error = -ENODEV;
     if (sdp->type != TYPE_DISK && sdp->type != TYPE_MOD && sdp->type != TYPE_RBC)
         goto out;
 
     SCSI_LOG_HLQUEUE(3, sdev_printk(KERN_INFO, sdp,
                     "sd_attach\n"));
 
     error = -ENOMEM;
     sdkp = kzalloc(sizeof(*sdkp), GFP_KERNEL);
     if (!sdkp)
         goto out;
 
     gd = alloc_disk(SD_MINORS);
     if (!gd)
         goto out_free;
 
     do {
         if (!ida_pre_get(&sd_index_ida, GFP_KERNEL))
             goto out_put;
 
         spin_lock(&sd_index_lock);
         error = ida_get_new(&sd_index_ida, &index);
         spin_unlock(&sd_index_lock);
     } while (error == -EAGAIN);
 
     if (error)
         goto out_put;
 
     if (index >= SD_MAX_DISKS) {
         error = -ENODEV;
         sdev_printk(KERN_WARNING, sdp, "SCSI disk (sd) name space exhausted.\n");
         goto out_free_index;
     }
 
     error = sd_format_disk_name("sd", index, gd->disk_name, DISK_NAME_LEN);
     if (error)
         goto out_free_index;
 
     sdkp->device = sdp;
     sdkp->driver = &sd_template;
     sdkp->disk = gd;
     sdkp->index = index;
     atomic_set(&sdkp->openers, 0);
 
     if (!sdp->request_queue->rq_timeout) {
         if (sdp->type != TYPE_MOD)
             blk_queue_rq_timeout(sdp->request_queue, SD_TIMEOUT);
         else
             blk_queue_rq_timeout(sdp->request_queue,
                          SD_MOD_TIMEOUT);
     }
 
     device_initialize(&sdkp->dev);
     sdkp->dev.parent = dev;
     sdkp->dev.class = &sd_disk_class;
     dev_set_name(&sdkp->dev, dev_name(dev));
 
     if (device_add(&sdkp->dev))
         goto out_free_index;
 
     get_device(dev);
     dev_set_drvdata(dev, sdkp);
 
     get_device(&sdkp->dev); /* prevent release before async_schedule */
     async_schedule(sd_probe_async, sdkp);
 
     return 0;
 
  out_free_index:
     spin_lock(&sd_index_lock);
     ida_remove(&sd_index_ida, index);
     spin_unlock(&sd_index_lock);
  out_put:
     put_disk(gd);
  out_free:
     kfree(sdkp);
  out:
     return error;
 }
该函数是实质上创建块设备的函数,包括创建初始化struct gendisk结构体等等。

现在还有两个疑问,1刚刚的过程是u盘中有两个真实的物理分区的,所以max_lun会被设置为1,但是如果只有一个真实的物理分区,但是确认磁盘工具分配为两个虚拟的逻辑分区它的过程是怎样的,还需要继续做实验去跟踪代码。2现在对scsi有了疑问,在板子是没有scsi控制器或者总线的,为什么代码中还要用到scsi接口,scsi到底是什么,是物理的接口还是只是逻辑上被虚拟出来的和platform平台总线一样的?




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值