当前位置:首页 > 芯闻号 > 充电吧
[导读]http://www.shangshuwu.cn/index.php/Linux%E5%86%85%E6%A0%B8USB%E4%B8%BB%E8%AE%BE%E5%A4%87%E9%A9%B1%E5

http://www.shangshuwu.cn/index.php/Linux%E5%86%85%E6%A0%B8USB%E4%B8%BB%E8%AE%BE%E5%A4%87%E9%A9%B1%E5%8A%A8%E7%A8%8B%E5%BA%8F
目录 [隐藏] 1 ehci-hcd控制器 1.1 EHCI构架介绍1.2 EHCI驱动程序分析2 Mass Storage主机驱动程序 2.1 Mass Storage规范介绍2.2 Bulk-Only传输协议介绍2.3 SCSI命令描述块结构2.4 Mass Storage设备对象结构2.5 Mass Storage设备初始化2.6 探测函数storage_probe分析 ehci-hcd控制器 EHCI构架介绍

USB主控器规范包括USB1.1主控器规范和USB2.0主控器规范。USB1.1主控器规范有包括UHCI(Universal Host Controller Interface)和OHCI(Open Host Controller Interface Specification);USB2.0主控器规范为EHCI(Enhanced Host Controller Interface Specification)。UHCI和OHCI在硬件实现以及对底层软件访问上都有所不同,但二者又都完全USB 1.1中对主控制器的要求。

USB主控制器驱动程序的整个系统框架图如图4所示,从图中可以看出USB驱动程序包括客户驱动、通用总线驱动程序、EHCI驱动程序等组 成。其中,客户驱动程序是特定USB设备的驱动程序,提供了USB设备的功能操作及特定子类协议封装;USB驱动程序(USBD)是特定操作系统上抽象出 的主机控制器驱动程序共有特性,对应于Linux USB驱动程序的HCD层;EHCI控制器驱动程序(EHCD)是依赖于特定硬件寄存器接口定义的主控制器驱动程序。USB设备是执行终端用户功能硬件设 备。


图4 USB驱动程序系统框架图

EHCI通用构架如图5所示。每个EHCI接口定义了三个接口空间,该个接口空间说明如下:

PCI配置空间包括PCI寄存器,它们用来系统部件枚举和PCI电源管理。 寄存器空间,通常称为I/O空间。它必须被用作内存映射I/O空间。它包括特定应用参数寄存器和能力寄存器、加上可选的控制和状态寄存器。 调度接口空间是特殊分配的内存并且被EHCI驱动程序管理用来周期性或异步调度。


图 EHCI通用构架图

EHCI支持两种类型传输:异步类型和周期类型。周期类型包括同步传输和中断传输,异步类型包括控制传输和批量传输。EHCI调度接口给两种类型提 供了分开的调度。周期调度基于时间发起的帧链表,它代表主机控制器工作条目的滑动窗口。所有的同步和中断传输都通过周期调度来进行。

异步调度是简单的调度工作条目的循环链表,它给所有异步传输提供了循环调度服务。

EHCI使用一个简单的buffer队列数据结构来管理所有的中断、批量和控制传输类型。排队的数据结构提供了自动的、排序的数据传输流。软件能异步地加数据buffer到一个队列并维护数据流。USB定义的短包语法在没有软件干预下完全支持所有的边界条件处理。

USB总线的主机控制器要求应用根集线器,主机控制器模拟了根集线器,它在操作寄存器空间装有端口寄存器,寄存器含有在USB规范中需要管 理每个端口的最小硬件状态和控制。事务通过根端口被广播下流的USB设备,端口寄存器提供给系统软件对端口的管理和端口的状态信息,包括:设备的连接与断 开、执行设备复位、处理端口功率和端口电源管理。

EHCI控制器提供了两套软件可访问的寄存器:内存映射的主机控制器寄存器和可选的PCI配置寄存器。PCI配置寄存器仅是用到主机控制器的PCI设备需要的。

主机控制器能力寄存器定义了限制、主机控制器使用的能力,如:下行端口数、主机控制器的接口版本号、同步调度门限等。在代码中使用结构ehci_caps来描述。

主机控制器操作寄存器位于能力寄存器之后,是双字对齐读写寄存器。这些寄存器分为两套,第一套从地址00到3Fh,在主控制器核心电源好的 情况下使用,包括USB控制命令、状态、中断使能、帧序号寄存器。第二套寄存器从40h到可使用的寄存器空间结尾,在外围辅助电源好的情况下使用,包括每 个端口的状态与控制寄存器。在代码中使用结构ehci_regs来描述。

接口数据结构在hcd软件和ehci控制器硬件之间用于通信控制、数据和状态。接口由周期调度、周期帧链表、异步调度、同步事务描述子 (iTD)、分离事务同步传输描述子(siTD)、队列头(QH)和队列元素传输描述子(qTD)组成。在代码中,qTD用结构ehci_qtd描 述,QH用结构ehci_qh描述。iTD用结构ehci_itd描述,siTD用结构ehci_sitd描述。

EHCI主机控制器带有一个模拟操作的根集线器,通过寄存器可完成对根集线器的各个端口的状态及连接控制,因此,它不会调用到USB核心层中有关HUB的操作函数。

EHCI主机控制器对于URB的提交排队及传输、调度以及控制器的各种状态转移提供了控制。特别是寄存器级的控制函数与EHCI控制器本身结构相关,牵涉到对众多寄存器值的理解,因而这里只说明了ehci控制器的上层功能函数。

EHCI驱动程序分析

EHCI驱动程序的编写思路是:EHCI驱动程序是一个结构hc_driver实例,它应该实现结构hc_driver中的函数,另外,从硬件上层 来看,EHCI主控制从PCI总线桥接,应是一个PCI驱动程序实例,因此,应实现结构pci_driver中的函数,并用PCI注册函数 pci_register_driver注册此实例。

函数__init init注册了&ehci_pci_driver控制器驱动程序,由于ehci-hcd是通过PCI总线与CPU相连,因而,它被注册成一个新的PCI驱动程序。

函数__init init分析如下(在drivers/usb/host/ehci-hcd.c中):

#ifdef CONFIG_PCI #include "ehci-pci.c" #define PCI_DRIVER  ehci_pci_driver #endif static int __init ehci_hcd_init(void) {  int retval = 0;    ……   #ifdef PCI_DRIVER     //注册驱动程序,初始化ehci_pci_driver并加到内核对象体系中去  retval = pci_register_driver(&PCI_DRIVER);  if (retval < 0)   goto clean1; #endif …… clean1: #endif #ifdef PLATFORM_DRIVER  platform_driver_unregister(&PLATFORM_DRIVER); …… #endif  return retval; }


PCI驱动程序结构实例ehci_pci_driver的一些函数定义如下:

static const char hcd_name [] = "ehci_hcd"; /* pci driver glue; this is a "new style" PCI driver module */ static struct pci_driver ehci_pci_driver = {  .name =  (char *) hcd_name,  .id_table = pci_ids,    .probe = usb_hcd_pci_probe, //探测函数  .remove = usb_hcd_pci_remove, //移去设备时的清除函数   #ifdef CONFIG_PM  .suspend = usb_hcd_pci_suspend,  .resume = usb_hcd_pci_resume, #endif };


pci_ids 是PCI驱动程序选择元数据,PCI热插拔使用到它,通过它来选择驱动程序ehci_driver。pci_ids列出如下:

static const struct pci_device_id pci_ids [] = { {  //处理任何USB 2.0 EHCI控制器  PCI_DEVICE_CLASS(((PCI_CLASS_SERIAL_USB << 8) | 0x20), ~0),  .driver_data = (unsigned long) &ehci_driver,  },  { /* end: all zeroes */ } };


ehci_driver是主机控制器结构hc_driver实例,它描述了EHCI控制器信息及各种操作函数,每个主机控制器都有一个这样的结构。ehci_driver列出如下:

static const struct hc_driver ehci_driver = {  .description =  hcd_name,  .product_desc =  "EHCI Host Controller",  .hcd_priv_size = sizeof(struct ehci_hcd),    /*   * 通用与硬件相关联的成员   */  .irq =   ehci_irq,  //中断处理函数  .flags =  HCD_MEMORY | HCD_USB2, //控制器寄存器使用内存|usb2.0    /*   * 基本的生命周期操作   */   //初始化HCD和root hub  .reset =  ehci_hc_reset, //HCD复位到停止状态。     //开始运行,初始化ECHI设备的各种寄存器进入运行状态,   //调用函数hcd_register_root注册根集线器驱动程序。  .start =  ehci_start, #ifdef CONFIG_PM  .suspend =  ehci_suspend, //在所有的设备挂起后调用  .resume =  ehci_resume, //在所有的设备恢复之前调用 #endif  .stop =   ehci_stop, //HCD停止写内存和I/O操作    /*   * 管理i/o请求和相关的设备资源   */  .urb_enqueue =  ehci_urb_enqueue, //提交URB的具体处理函数  .urb_dequeue =  ehci_urb_dequeue,  .endpoint_disable = ehci_endpoint_disable,     /*   * 调度支持   */  .get_frame_number = ehci_get_frame, //得到当前的帧序号   /* root hub支持,EHCI主机控制器使用了它自己的模拟根集线器,这个集线器通过对寄存器的设置提供了简单的端口状态及连接控制功能。*/  .hub_status_data = ehci_hub_status_data,//端口状态发生变化时,直接控制端口  .hub_control =  ehci_hub_control, //hub控制的状态机  .hub_suspend =  ehci_hub_suspend,  .hub_resume =  ehci_hub_resume,//电源恢复,初始化根集线器 };


下面分析只分析结构实例ehci_pci_driver中的探测函数usb_hcd_pci_probe:

函数 usb_hcd_pci_probe初始化基于PCI的HCD(主机控制器驱动程序),参数dev是被探测的USB主机控制器,参数id是连接控制器到HCD构架的pci热插拔设备ID。这个函数不能从中断上下文中调用。

函数 usb_hcd_pci_probe作为probe()存在HCD的pci_driver结构中,它分配基本的PCI资源给这个USB控制器:它分配一个 PCI资源区域、进行I/O映射、创建usb_hcd结构实例并赋上设备操作函数集&usb_hcd_operations,给hcd创建DMA 缓冲池,申请中断,注册总线。通过与HCD相关的hotplug条目.driver_data为HCD触发start()方法。

函数 usb_hcd_pci_probe分析如下(在drivers/usb/core/hcd-pci.c中):

int usb_hcd_pci_probe (struct pci_dev *dev, const struct pci_device_id *id) {  struct hc_driver *driver;  unsigned long  resource, len;  void __iomem  *base;  struct usb_hcd  *hcd;  int   retval, region;  char   buf [8], *bufp = buf;    if (usb_disabled()) //如果没有USB设备   return -ENODEV; //结构pci_device_id中存有供应商和设备ID, //其成员driver_data指向具体设备驱动程序结构。  if (!id || !(driver = (struct hc_driver *) id->driver_data))   return -EINVAL;     //使设备的I/O和设备内存区有效,唤醒设备,在被驱动程序使用前初始化设备  if (pci_enable_device (dev) < 0)   return -ENODEV;  dev->current_state = 0;  dev->dev.power.power_state = 0;           if (!dev->irq) {//没有中断          dev_err (&dev->dev,    "Found HC with no IRQ.  Check BIOS/PCI %s setup!n",    pci_name(dev));             retval = -ENODEV;   goto done;         }       //HC寄存器使用内存  if (driver->flags & HCD_MEMORY) { // EHCI, OHCI   region = 0;   resource = pci_resource_start (dev, 0); //得到PCI设备的0号区域资源   len = pci_resource_len (dev, 0);     //申请名字为driver->description的I/O内存区域,     //从resource开始,长度为len   if (!request_mem_region (resource, len, driver->description)) {    dev_dbg (&dev->dev, "controller already in usen");    retval = -EBUSY;    goto done;   }     //映射resource开始的物理地址到CPU的虚拟地址base   base = ioremap_nocache (resource, len);   if (base == NULL) {//映射失败    dev_dbg (&dev->dev, "error mapping memoryn");    retval = -EFAULT; clean_1:    release_mem_region (resource, len); //释放资源    dev_err (&dev->dev, "init %s fail, %dn",     pci_name(dev), retval);    goto done;   }    } else {     // UHCI   resource = len = 0;     //标准PCI配置6个region(或说6个bar)   for (region = 0; region < PCI_ROM_RESOURCE; region++) {            //如果不是IO资源    if (!(pci_resource_flags (dev, region) & IORESOURCE_IO))     continue;      resource = pci_resource_start (dev, region);    len = pci_resource_len (dev, region);             //申请名字为driver->description的资源    if (request_region (resource, len, driver->description))     break;   }   if (region == PCI_ROM_RESOURCE) {//如果是rom,则说明无资源可用    dev_dbg (&dev->dev, "no i/o regions availablen");    retval = -EBUSY;    goto done;   }   base = (void __iomem *) resource;  }    //创建并初始化结构hcd  hcd = usb_create_hcd (driver);  ……  // hcd zeroed everything  hcd->regs = base;  hcd->region = region;       //将hcd驱动程序结构赋给pci设备结构,即dev ->dev->driver_data = hcd  pci_set_drvdata (dev, hcd);  hcd->self.bus_name = pci_name(dev); #ifdef CONFIG_PCI_NAMES  hcd->product_desc = dev->pretty_name; #endif  hcd->self.controller = &dev->dev;      //创建4个DMA池  if ((retval = hcd_buffer_create (hcd)) != 0) { clean_3:   pci_set_drvdata (dev, NULL);   usb_put_hcd (hcd);   goto clean_2;  }   //打印信息  dev_info (hcd->self.controller, "%sn", hcd->product_desc);    //到现在为止,HC已在一个不确定状态,调用驱动程序的reset函数复位  if (driver->reset && (retval = driver->reset (hcd)) < 0) {   dev_err (hcd->self.controller, "can't resetn");   goto clean_3;  }      //使能设备上的bus-mastering总线  pci_set_master (dev);     …   //申请共享中断号,中断处理函数是usb_hcd_irq,设备名为description  retval = request_irq (dev->irq, usb_hcd_irq, SA_SHIRQ,     hcd->driver->description, hcd);  …  hcd->irq = dev->irq;    //注册总线到sysfs和/proc文件系统  usb_register_bus (&hcd->self);     ……  return retval; }


函数 usb_create_hcd创建并初始化一个结构usb_hcd实例,参数driver是此HCD使用的HCD驱动程序。如果内存不可用,返回NULL。

函数 usb_create_hcd列出如下(在drivers/usb/core/hcd.c中):

struct usb_hcd *usb_create_hcd (const struct hc_driver *driver) {  struct usb_hcd *hcd;    hcd = kcalloc(1, sizeof(*hcd) + driver->hcd_priv_size, GFP_KERNEL);  if (!hcd)   return NULL;    usb_bus_init(&hcd->self);//初始化usb_bus结构  hcd->self.op = &usb_hcd_operations;  hcd->self.hcpriv = hcd;  hcd->self.release = &hcd_release;    init_timer(&hcd->rh_timer);    hcd->driver = driver;  hcd->product_desc = (driver->product_desc) ? driver->product_desc :    "USB Host Controller";  hcd->state = USB_STATE_HALT;    return hcd; }


Mass Storage主机驱动程序 Mass Storage规范介绍

USB大存储(USB Mass Storage)工作组(CWG Class Working Group)规范包括:

USB Mass Storage Class Control/Bulk/Interrupt(CBI) Transport 即USB大存储类控制/批量/中断传输协议。 USB Mass Storage Class Bulk-Only Transport 即USB大存储类批量传输协议。 USB Mass Storage Class UFI Command Specification 即USB大存储类UFI命令规范。 USB Mass Storage Class Bootability Specfication 即USB大存储类系统启动规范。 USB Mass Storage Class Compliance Test Specification 即USB大存储类遵从测试规范。

其中,CBI传输规范仅用于全速软盘驱动器,不能用于高速设备或其它非软盘设备。

USB大存储类使用几种命令集规范,这些命令集的命令块放在符合USB协议的USB包裹器中,USB大存储类规范定义了下面几种命令集:

软驱、光驱和磁带驱动器使用的ATAPI规范(Advanced Technology Attachment Packet Interface)。 精简块命令(Reduced Block Commands(RBC)) 多媒体命令集2(Multi-Media Command Set 2 (MMC-2)) SCSI主命令(SCSI Primary Commands-2(SPC-2)) USB规范(Universal Serial Bus Specification)

USB大存储类设备的接口描述子包含了一个bInterfaceSubClass和bInterfacePortocal的 域,bInterfaceSubClass描述了USB大存储类支持的命令块规范,如:它为06h时表示支持的是SCSI传输命令集。 bInterfacePortocal描述了USB大存储类支持的接口传输协议,如:它为50h时表示支持的是Bulk-Only传输协议。

大存储设备(Mass Storage)包括U盘、读卡器及USB接口的光驱等其它块存储设备,它们看作是SCSI接口设备,当用户从设备上读写数据时,文件系统将读写操作传送 到SCSI协议层,SCSI协议层的读写请求封装成USB请求块(URB)通过USB接口传递给设备,USB设备从URB中解析出SCSI协议命令后再操 作块设备。USB接口大存储设备的操作流程图如图6所示。


图6 USB接口大存储设备的操作流程图

USB接口大存储设备驱动程序的设计思路是:设计一个控制线程,这个线程被注册为虚拟SCSI控制器,这个线程在设备插入/移去时一直作为SCSI 节点存在的。这样,被移去的设备能在再插上时被给以与以前/dev中同一节点。当一个设备被插上时,控制线程从SCSI中间层代码得到命令。控制线程接收 命令,在检查后送命令到协议处理函数。这些处理函数负责再写命令(如果必要)到设备得接受的形式。例如:ATAPI设备不能支持6byte命令,这样,它 们必须被再写成10byte变量。一旦协议处理函数已再写了命令,它们被送到传输处理函数。传输处理函数负责送命令到设备、交换数据、并接着得到设备的状 态。在协议处理函数和传输处理函数之间有一小段代码,来决定REQUEST_SENSE命令是否应该发出。在命令被处理后,scsi_done()被调用 来发信号给SCSI层命令已完成。我们准备接收下一条命令。

作为具有操作系统的智能嵌入设备,它使用了SCSI命令块集与Bulk-only传输协议。它既能作为主机来操作其它USB大存储设备,称 为大存储设备主机。同时,也能作为USB大存储设备被其它主机控制。下面对具有linux操作系统的嵌入设备分别就两种模式分别进行分析。

Bulk-Only传输协议介绍

Bulk-Only传输协议是USB大容量存贮器类中的USB批量数据传输协议,它定义了仅通过批量端点传输的命令、数据和状态。它使用命令块数据 包裹器(CBW)发送命令,使用命令状态数据包裹器(CSW)接收返回的状态。命令块数据包裹器(CBW)是一个包含命令块和相关信息的数据包。 命令状态数据包(CSW)裹器是一个包含命令块状态的数据包。命令块数据包裹器(CBW)的格式如表1所示。

表1 命令块数据包裹器(CBW)格式表
Byte Bit 7 6 5 4 3 2 1 0 0-3 dCBWSignature 4-7 dCBWTag 8-11
(08h-0Bh) dCBWDataTransferLength 12
(0Ch) bmCBWFlags 13
(0Dh) Reserved(0) bCBWLUN 14
(0Eh) Reserved(0) bCBWCBLength 15-30
(0Fh-1Eh) CBWCB

命令块数据包裹器(CBW)用下述数据结构描述(在drivers/usb/storage/transport.h中):

struct bulk_cb_wrap {  __le32 Signature;  //签名'USBC'  __u32 Tag;   //每个命令唯一的ID  __le32 DataTransferLength; //数据大小  __u8 Flags;   //在bit 0中表示方向  __u8 Lun;   //表示LUN(SCSI逻辑单元)正常为0  __u8 Length;   //数据传输长度  __u8 CDB[16];  //传输的命令字节 };


命令状态数据包裹器(CSW)的格式如表2所示。

表2 命令状态数据包(CSW)的格式表
Byte Bit 7 6 5 4 3 2 1 0 0-3 dCSWSignature 4-7 dCSWTag 8-11(Bh) dCSWDataResidue 12(Ch) dCSWStatus

命令状态数据包裹器(CSW)用下述数据结构描述(在drivers/usb/storage/transport.h中):

/* 命令状态包裹器*/ struct bulk_cs_wrap {  __le32 Signature;  //签名 'USBS'  __u32 Tag;   //与CBW中Tag一样  __le32 Residue;  //没有传输完的数据量  __u8 Status;   //操作状态标识,如:成功、失败等  __u8 Filler[18]; };


 传输过程是:当传输方向是从设备到主机时,则当CBW发送成功后,设备从设备的In端点读取CBW中规定长度的数据CBWCB;当传输方向是从主机到设 备时,则当CBW发送成功后,向设备的Out端点发送CBW中规定长度的数据CBWCB。CBWCB是命令块数据,是遵循某一规范的命令集, 如:SCSI-2命令集,最长16字节。

 当主机与设备之间的数据传送完毕后,主机还需从设备的In端点读取传送状态,主机根据接收的CSW数据包即可判断出通信是否正常。若返回的结果有错误,还须进行相应的出错处理。

样例:从设备读取数据的传输过程

下面是一个从设备读取数据的传输过程的例子,主机先向端点1发出CBW命令,设备解析CBW解析命令后,从主机指定的端点2将数据传回给主 机。在传送成功后,主机又读取端点2的状态CSW。主机从设备读到数据的流程图如下图。从图中可看出,第0到第2包是发送CBW的过程,第3到第5包是读 取数据的过程,下面接着的第0到第1包是读取CSW的过程。令牌包和握手包是由控制管道(对应ep0)来发送接收的。


图 主机从设备读到数据的流程图

第0到第5包的数据格式图列出如图7所示:

图7 第0到第5包的数据格式图

在第1包中,CBW传输了31(1FH)个字节的数据。内容含义是:55 53 42 43 是CBW后面固有的特征码;28 E8 31 FE 是由主机产生的CBWTag;00 02 00 00 是CBW数据传输长度,在此情况下是0000,0200H=512字节;80 是后面固有的标志码;00 是后面固有的CBWLUN;0A 是CBWCB长度,意味着命令描述块(CDB)长度是10字节,其中。28表示对应SCSI协议28h读命令。对于命令块,看下节的SCSI命令描述块的 结构。

SCSI协议28h读命令是Read(10),在这个CBW中,要求读取0柱0道1扇区共512字节的MBR数据,前446字节为主引导记录,接着的64字节为DPT(Disk Partition Table盘分区表),最后的2字节"55 AA"为有效结束标志。

在第4包中传输了512字节的数据。

CSW包的数据格式图列出如图8所示:

图8 CSW包的数据格式图

CSW数据包传输13(0DH)个字节的数据。内容含义是:55 52 42 53是CSW后面固有的特征码;28 E8 31 FF是主机产生的CSWTag;00 00 00 00是CSW的数据冗余;00 指示在此情况下CSW的状态,此例中为OK。

SCSI命令描述块结构

各种SCSI命令描述块具有相似的结构,SCSI命令描述块的结构如表8所示。

表8一个典型的SCSI命令描述块结构
  7 6 5 4 3 2 1 0 0 操作码 1

命令的指定参数 … … n-1 n 控制字节

  SCSI命令描述块的结构的各项说明如下:

操作码(Opcode)

  每个命令的0号字节就是操作码,它定义了命令的类型和长度。它的高3位代表了命令所属的命令组,低5位表示命令本身。每个命令组都有一个命令长度。因而,对命令的第一个字节进行解码以后,目标器就知道这个命令还剩下多少字节。操作码在不同设备上含义是不同的。

SCSI常用命令块有查询、读请求、测试单元准备、禁止媒介删除、读缓冲、写缓冲等。

命令组

代表命令组的高3位可以有8个不同的组合,所以可以代表8个命令组,当制造商实现自己的标准的时候,就必须使用6号组或者7号组,实际上,使用6号组或者7号组的情况很少发生。命令组的说明如表9所示。

表9 SCSI命令组说明
组 操作码 说明 0 00h~1Fh 6字节命令 1 20h~3Fh 10字节命令 2 40h~5Fh 10字节命令 3 60h~7Fh 保留 4 80h~9Fh 16字节命令 5 A0h~BFh 12字节命令 6 C0h~DFh 厂商自定 7 E0h~FFh 厂商自定


控制字节

控制字节的格式如表10所示。SCSI-2中,控制字节仅仅包含了在标准中定义的两位,它们是连接位(Link bit)和标志位(flag bit),而且这两位都是可选的。连接位使你可以将几个命令连接成一个命令链,命令链中的每一个命令被称为连接的命令。从而这些连接的命令就形成了一个连 接的I/O过程。这就可以阻止其他I/O过程的命令插入这个已形成命令链的I/O过程,这就是在目标器内的优化方法。举个例子,当一个逻辑数据块需要被读 取一修改一写回时,这个做法就变得十分有用。而且,连接的命令允许使用逻辑数据块的相对地址。

表10 控制字节的格式
位数 7 6 5 4 3 2 1 0   厂商自定 保留 ACA 状态 连接

标志位必须和连接命令一起使用。这引起在连接的命令执行结束之后发送服务响应LINKED COMMAND COMPLETE(WITH FLAG)(0BH),而不是发送服务响应LINKED COMMAND COMPLETE(OAH)。这样,你就可以在一个命令链中标出一个特定的命令。

在SCSI-3中出现了新的标志位:ACA位。ACA是偶然事件自动通信(auto contingent allegiance)的缩写,它是在命令执行过程中万一发生错误时LUN所采取的一种措施。如果ACA位没有被置"1",那么只要下一个命令从同一个启 动器中发出时,该错误状态就被取消。如果ACA位被置"1",它就会阻止取消错误状态的行动并保持这种状态。

Mass Storage设备对象结构

每个大存储设备用一个对象结构us_data来描述它的设备、管道、SCSI接口、传输、协议等各方面的信息及处理函数。

结构us_data列出如下(在drivers/usb/storage/usb.h中):

/*我们提供了一个DMA映射I/O buffer给小USB传输使用。CB[I]需要12字节buffer,Bulk-only需要31字节buffer,但Freecom需要64字节buffer,因此,我们分配了64字节的buffer。*/ #define US_IOBUF_SIZE  64    typedef int (*trans_cmnd)(struct scsi_cmnd *, struct us_data*); typedef int (*trans_reset)(struct us_data*); typedef void (*proto_cmnd)(struct scsi_cmnd*, struct us_data*); typedef void (*extra_data_destructor)(void *);  //格外的数据析构函数       struct us_data {  //工作设备、接口结构及各种管道  struct semaphore dev_semaphore;  //保护pusb_dev  struct usb_device *pusb_dev;    //从类usb_device继承  struct usb_interface *pusb_intf;  //从类usb_interface继承  struct us_unusual_dev   *unusual_dev;  //常用的设备链表定义  unsigned long  flags;   /* 最初来自过滤器的标识*/  unsigned int  send_bulk_pipe;  /* 缓存的管道值*/  unsigned int  recv_bulk_pipe;  unsigned int  send_ctrl_pipe;  unsigned int  recv_ctrl_pipe;  unsigned int  recv_intr_pipe;    //设备的信息  char   vendor[USB_STOR_STRING_LEN]; //供应商信息  char   product[USB_STOR_STRING_LEN]; //产品信息  char   serial[USB_STOR_STRING_LEN]; //产品序列号  char   *transport_name; //传输协议名  char   *protocol_name; //协议名  u8   subclass;  //子类  u8   protocol;  u8   max_lun; //最大的逻辑单元    u8   ifnum;   //接口数  u8   ep_bInterval;  //中断传输间隔     //设备的函数指针  trans_cmnd  transport;    //传输函数  trans_reset  transport_reset; //传输设备复位  proto_cmnd  proto_handler;  //协议处理函数    //SCSI接口  struct Scsi_Host *host;   //虚拟SCSI主机数据结构  struct scsi_cmnd *srb;   //当前SCSI命令描述块    //线程信息  int   pid;   //控制线程    //控制和批量通信数据  struct urb  *current_urb;  //USB请求  struct usb_ctrlrequest *cr;   //USB控制请求的setup数据  struct usb_sg_request current_sg;  //碎片-收集请求  unsigned char  *iobuf;   //I/O buffer  dma_addr_t  cr_dma;   //控制请求数据buffer的DMA地址  dma_addr_t  iobuf_dma; // I/O buffer的DMA地址    //互斥保护和同步结构  struct semaphore sema;   /* to sleep thread on   */  struct completion notify;   //线程开始/结束时发通知出去  wait_queue_head_t dev_reset_wait;  //在复位期间等待  wait_queue_head_t scsi_scan_wait;  //在SCSI扫描前等待   struct completion scsi_scan_done;  //SCSI扫描线程结束时通知处理函数     //子驱动程序信息  void   *extra;   //任何格外的数据  extra_data_destructor extra_destructor;//格外的数据析构函数  };

Mass Storage设备初始化

函数usb_stor_init注册和初始化大存储驱动程序。函数usb_stor_init列出如下(在drivers/usb/storage/usb.c中):

static int __init usb_stor_init(void) {  int retval;  printk(KERN_INFO "Initializing USB Mass Storage driver...n");    //注册驱动程序,如果操作失败,返回负值的错误代码   retval = usb_register(&usb_storage_driver);  if (retval == 0)   printk(KERN_INFO "USB Mass Storage support registered.n");    return retval; }

大存储设备驱动程序结构实例usb_storage_driver列出如下:

struct usb_driver usb_storage_driver = {  .owner = THIS_MODULE,  .name =  "usb-storage",  .probe = storage_probe, //探测并初始化设备  .disconnect = storage_disconnect,//断开连接处理函数  .id_table = storage_usb_ids, };

在usb_device_id结构类型数组中storage_usb_ids定义了设备类、子类及命令块集的协议类型。部分列出如下:

static struct usb_device_id storage_usb_ids [] = { ……  /* Bulk-only transport for all SubClass values */  { USB_INTERFACE_INFO(USB_CLASS_MASS_STORAGE, US_SC_RBC, US_PR_BULK) },  { USB_INTERFACE_INFO(USB_CLASS_MASS_STORAGE, US_SC_8020, US_PR_BULK) },  { USB_INTERFACE_INFO(USB_CLASS_MASS_STORAGE, US_SC_QIC, US_PR_BULK) },  { USB_INTERFACE_INFO(USB_CLASS_MASS_STORAGE, US_SC_UFI, US_PR_BULK) },  { USB_INTERFACE_INFO(USB_CLASS_MASS_STORAGE, US_SC_8070, US_PR_BULK) }, #if !defined(CONFIG_BLK_DEV_UB) && !defined(CONFIG_BLK_DEV_UB_MODULE)  { USB_INTERFACE_INFO(USB_CLASS_MASS_STORAGE, US_SC_SCSI, US_PR_BULK) }, #endif    /* Terminating entry */  { } };

探测函数storage_probe分析

函数storage_probe 探测看是否能驱动一个新连接的USB设备。创建了大存储设备控制线程usb_stor_control_thread和SCSI设备后期扫描线程 usb_stor_scan_thread。函数storage_probe在控制线程中通过虚拟SCSI主机控制器发送SCSI命令,经Bulk- Only协议封装后,再填充为URB包,传送给USB核心层来发送给设备。函数storage_probe调用层次图如图2所示。下面按照这个图分析函数 storage_probe。


图2 函数storage_probe调用层次图

函数storage_probe列出如下(在drivers/usb/storage/usb.c中):

static int storage_probe(struct usb_interface *intf,     const struct usb_device_id *id) {  struct us_data *us;  const int id_index = id - storage_usb_ids;   int result;    US_DEBUGP("USB Mass Storage device detectedn");    //分析us_data结构对象空间  us = (struct us_data *) kmalloc(sizeof(*us), GFP_KERNEL);  if (!us) {   printk(KERN_WARNING USB_STORAGE "Out of memoryn");   return -ENOMEM;  }  memset(us, 0, sizeof(struct us_data));  init_MUTEX(&(us->dev_semaphore));  init_MUTEX_LOCKED(&(us->sema));  init_completion(&(us->notify));  init_waitqueue_head(&us->dev_reset_wait);  init_waitqueue_head(&us->scsi_scan_wait);  init_completion(&us->scsi_scan_done);    //将USB设备与结构us_data关联起来   //设置 intf->dev ->driver_data = us,分配buffer  result = associate_dev(us, intf);  if (result)   goto BadDevice;       //得到unusual_devs条目和描述子,初始化us。     //id_index与usb_device_id表中序号匹配,找到表中对应的条目。    get_device_info(us, id_index);   #ifdef CONFIG_USB_STORAGE_SDDR09  if (us->protocol == US_PR_EUSB_SDDR09 || //SDDR-09 的SCM-SCSI桥     us->protocol == US_PR_DPCM_USB) { // CB/SDDR09混合体   //设置配置,STALL在这儿是一个可接受的反应    if (us->pusb_dev->actconfig->desc.bConfigurationValue != 1) {    US_DEBUGP("active config #%d != 1 ??n", us->pusb_dev     ->actconfig->desc.bConfigurationValue);    goto BadDevice;   }     //重置配置,重新初始化端点及接口   result = usb_reset_configuration(us->pusb_dev);   ……  } #endif    //将传输方式、协议和管道设置赋给us    result = get_transport(us);  if (result)   goto BadDevice;  result = get_protocol(us);  if (result)   goto BadDevice;  result = get_pipes(us);  if (result)   goto BadDevice;    //初始化所有需要的动态资源   result = usb_stor_acquire_resources(us);  if (result)   goto BadDevice;  result = scsi_add_host(us->host, &intf->dev);  if (result) {   printk(KERN_WARNING USB_STORAGE    "Unable to add the scsi hostn");   goto BadDevice;  }    /*线程usb_stor_scan_thread执行延迟的SCSI设备扫描工作,扫描给定的适配器us->host,扫描通道及目标,扫描探测LUN。*/   result = kernel_thread(usb_stor_scan_thread, us, CLONE_VM);  if (result < 0) {   printk(KERN_WARNING USB_STORAGE           "Unable to start the device-scanning threadn");   scsi_remove_host(us->host);   goto BadDevice;  }    return 0;  …… }

函数usb_stor_acquire_resources初始化所有的需要的动态资源,启动控制线程,函数列出如下(在drivers/usb/storage/usb.c中):

static int usb_stor_acquire_resources(struct us_data *us) {  int p;    us->current_urb = usb_alloc_urb(0, GFP_KERNEL); //分配urb结构对象空间  if (!us->current_urb) {   US_DEBUGP("URB allocation failedn");   return -ENOMEM;  }    //当我们执行下两个操作时锁住设备。   down(&us->dev_semaphore);    //仅对于批量设备,得到最大逻辑单元值,   //在SCSI协议模型中,每个逻辑单元用来操作SCSI设备。   if (us->protocol == US_PR_BULK) {   p = usb_stor_Bulk_max_lun(us);   if (p < 0) {    up(&us->dev_semaphore);    return p;   }   us->max_lun = p;  }    //如果设备需要初始化,在开始控制线程前初始化设备  if (us->unusual_dev->initFunction)   us->unusual_dev->initFunction(us);    up(&us->dev_semaphore);    //因为这是一个新设备,我们需要注册一个设备的虚拟SCSI控制器,   //用来处理SCSI层高层协议。   us->host = scsi_host_alloc(&usb_stor_host_template, sizeof(us));  if (!us->host) {   printk(KERN_WARNING USB_STORAGE    "Unable to allocate the scsi hostn");   return -EBUSY;  }    //设置为SCSI扫描准备的hostdata  us->host->hostdata[0] = (unsigned long) us;    //启动控制线程  p = kernel_thread(usb_stor_control_thread, us, CLONE_VM);  if (p < 0) {   printk(KERN_WARNING USB_STORAGE           "Unable to start control threadn");   return p;  }  us->pid = p;    //等待线程启动  wait_for_completion(&(us->notify));    return 0; }

SCSI主机模板结构被用来分配SCSI主机,结构实例usb_stor_host_template列出如下(在drivers/usb/storage/scsiglue.c中):


struct scsi_host_template usb_stor_host_template = {  //基本的用户使用的接口  .name =    "usb-storage",  .proc_name =   "usb-storage",  .proc_info =   proc_info,  .info =    host_info,    //命令接口,仅用于排队  .queuecommand =   queuecommand,    //错误及错误退出处理函数  .eh_abort_handler =  command_abort,  .eh_device_reset_handler = device_reset,  .eh_bus_reset_handler =  bus_reset,    //排队命令数,每个LUN仅一个命令。   .can_queue =   1,  .cmd_per_lun =   1,    /* unknown initiator id */  .this_id =   -1,    .slave_alloc =   slave_alloc,  .slave_configure =  slave_configure, //设置一些限制    //能被处理的碎片收集片断数  .sg_tablesize =   SG_ALL,    //一次传输的总大小限制到240扇区,即120 KB  .max_sectors =          240,    //融合命令...   .use_clustering =  1,    //模拟HBA  .emulated =   1,    //当设备或总线复位后做延迟操作。   .skip_settle_delay =  1,    //sysfs设备属性  .sdev_attrs =   sysfs_device_attr_list,    /* 用于内核模块管理 */  .module =   THIS_MODULE };

线程函数usb_stor_control_thread分析处理SCSI命令请求描述块srb后,调用协议处理函数us->proto_handler来进行封装传输。函数usb_stor_control_thread列出如下:

static int usb_stor_control_thread(void * __us) {  struct us_data *us = (struct us_data *)__us;  struct Scsi_Host *host = us->host;    lock_kernel();    //线程后台化,定向成从init进程继承,这样就去掉了不需要的进程资源  daemonize("usb-storage");    current->flags |= PF_NOFREEZE;    unlock_kernel();    //发信号表示我们已开始了这个线程  complete(&(us->notify));    for(;;) {   US_DEBUGP("*** thread sleeping.n");   if(down_interruptible(&us->sema))    break;     US_DEBUGP("*** thread awakened.n");     /* lock the device pointers */   down(&(us->dev_semaphore));     //如果us->srb是NULL, 线程被请求退出。   if (us->srb == NULL) {    US_DEBUGP("-- exit command receivedn");    up(&(us->dev_semaphore));    break;   }     //锁住SCSI主机控制器   scsi_lock(host);     //命令超时   if (test_bit(US_FLIDX_TIMED_OUT, &us->flags)) {    us->srb->result = DID_ABORT << 16;    goto SkipForAbort;   }     //如果USB总线是断开状态,就不做任何事。    if (test_bit(US_FLIDX_DISCONNECTING, &us->flags)) {    US_DEBUGP("No command during disconnectn");    goto SkipForDisconnect;   }     scsi_unlock(host);     //如果方向标识是未知的,拒绝命令。    if (us->srb->sc_data_direction == DMA_BIDIRECTIONAL) {    US_DEBUGP("UNKNOWN data directionn");    us->srb->result = DID_ERROR << 16;   }     //如果target != 0 或LUN超过最大的已知LUN数,拒绝命令。    else if (us->srb->device->id &&      !(us->flags & US_FL_SCM_MULT_TARG)) {    US_DEBUGP("Bad target number (%d:%d)n",       us->srb->device->id, us->srb->device->lun);    us->srb->result = DID_BAD_TARGET << 16;   }     else if (us->srb->device->lun > us->max_lun) {    US_DEBUGP("Bad LUN (%d:%d)n",       us->srb->device->id, us->srb->device->lun);    us->srb->result = DID_BAD_TARGET << 16;   }     //处理需要伪装它们的查询数据的设备    else if ((us->srb->cmnd[0] == INQUIRY) &&        (us->flags & US_FL_FIX_INQUIRY)) {    unsigned char data_ptr[36] = {        0x00, 0x80, 0x02, 0x02,        0x1F, 0x00, 0x00, 0x00};      US_DEBUGP("Faking INQUIRY commandn");    fill_inquiry_response(us, data_ptr, 36);    us->srb->result = SAM_STAT_GOOD;   }     //得到一个命令,按照功能设备支持的协议来转换SCSI命令   else {    US_DEBUG(usb_stor_show_command(us->srb));    us->proto_handler(us->srb, us);   }     /* 加锁 */   scsi_lock(host);     //指示命令执行完成   if (us->srb->result != DID_ABORT << 16) {    US_DEBUGP("scsi cmd done, result=0x%xn",         us->srb->result);    us->srb->scsi_done(us->srb);   } else { SkipForAbort:    US_DEBUGP("scsi command abortedn");   }     /*如果一个错误退出请求被收到,我们需要发信号表示退出完成了。应该测试TIMED_OUT标识而不是srb->result == DID_ABORT,因为timeout/abort请求可能在所有的USB处理完成后被收到的*/   if (test_bit(US_FLIDX_TIMED_OUT, &us->flags))    complete(&(us->notify));     //完成了在这个命令上的操作 SkipForDisconnect:   us->srb = NULL;   scsi_unlock(host);     /* 解锁*/   up(&(us->dev_semaphore));  } /* for (;;) */    //通知exit例程我们实际上正在退出操作。   complete_and_exit(&(us->notify), 0); }

对于支持SCSI协议的功能设备来说,us->proto_handler协议处理函数就是函数 usb_stor_transparent_scsi_command,该函数把SCSI命令发送到传输层处理。该函数列出如下(在 drivers/usb/storage/protocol.c中):

void usb_stor_transparent_scsi_command(struct scsi_cmnd *srb,            struct us_data *us) {  //发送命令到传输层  usb_stor_invoke_transport(srb, us);    if (srb->result == SAM_STAT_GOOD) {   /* Fix the READ CAPACITY result if necessary */   if (us->flags & US_FL_FIX_CAPACITY)    fix_read_capacity(srb);  } }

函数usb_stor_invoke_transport是传输例程,它触发传输和基本的错误处理/恢复方法,它被协议层用来实际发送消息到设备并接收响应。

函数usb_stor_invoke_transport列出如下(在drivers/usb/storage/transport.c中):

void usb_stor_invoke_transport(struct scsi_cmnd *srb, struct us_data *us) {  int need_auto_sense;  int result;    //发送命令到传输层  srb->resid = 0;  result = us->transport(srb, us);    ……  srb->result = SAM_STAT_GOOD;    //决定是否需要auto-sense标识  need_auto_sense = 0;   /*如果我们正在支持CB传输,它不能决定它自己的状态,我们将自动感知(auto-sense),除非操作包括在一个data-in的传输中。设备能通过安装bulk-in管道来发出大多关开data-in错误的信号。*/   if ((us->protocol == US_PR_CB || us->protocol == US_PR_DPCM_USB) &&    srb->sc_data_direction != DMA_FROM_DEVICE) {   US_DEBUGP("-- CB transport device requiring auto-sensen");   need_auto_sense = 1;  }    //如果有一个操作失败,我们将自动做REQUEST_SENSE。   //注意在传输机制中在“失败”和“错误”之间的命令是不同的。   if (result == USB_STOR_TRANSPORT_FAILED) {   US_DEBUGP("-- transport indicates command failuren");   need_auto_sense = 1;  }    ……  //做auto-sense  if (need_auto_sense) {   int temp_result;   void* old_request_buffer;   unsigned short old_sg;   unsigned old_request_bufflen;   unsigned char old_sc_data_direction;   unsigned char old_cmd_len;   unsigned char old_cmnd[MAX_COMMAND_SIZE];   unsigned long old_serial_number;   int old_resid;     US_DEBUGP("Issuing auto-REQUEST_SENSEn");     //存储旧的命令   memcpy(old_cmnd, srb->cmnd, MAX_COMMAND_SIZE);   old_cmd_len = srb->cmd_len;     //设置命令和LUN   memset(srb->cmnd, 0, MAX_COMMAND_SIZE);   srb->cmnd[0] = REQUEST_SENSE;   srb->cmnd[1] = old_cmnd[1] & 0xE0;   srb->cmnd[4] = 18;     //在这儿必须做协议转换   if (us->subclass == US_SC_RBC || us->subclass == US_SC_SCSI)    srb->cmd_len = 6;   else    srb->cmd_len = 12;     //设置传输方向   old_sc_data_direction = srb->sc_data_direction;   srb->sc_data_direction = DMA_FROM_DEVICE;     //存buffer中内容   old_request_buffer = srb->request_buffer;   srb->request_buffer = srb->sense_buffer;     //设置buffer传输长度   old_request_bufflen = srb->request_bufflen;   srb->request_bufflen = 18;     //存碎片收集链表中的碎片数    old_sg = srb->use_sg;   srb->use_sg = 0;     //改变序号 – 或非高位   old_serial_number = srb->serial_number;   srb->serial_number ^= 0x80000000;     //发生auto-sense命令   old_resid = srb->resid;   srb->resid = 0;   temp_result = us->transport(us->srb, us);     //恢复命令   srb->resid = old_resid;   srb->request_buffer = old_request_buffer;   srb->request_bufflen = old_request_bufflen;   srb->use_sg = old_sg;   srb->serial_number = old_serial_number;   srb->sc_data_direction = old_sc_data_direction;   srb->cmd_len = old_cmd_len;   memcpy(srb->cmnd, old_cmnd, MAX_COMMAND_SIZE);     ……   /*设置result,让上层得到此值*/   srb->result = SAM_STAT_CHECK_CONDITION;     //如果ok,显示它们,sense buffer清0,这样不让高层认识到我们做了一个自愿的auto-sense    if (result == USB_STOR_TRANSPORT_GOOD &&    /* Filemark 0, ignore EOM, ILI 0, no sense */     (srb->sense_buffer[2] & 0xaf) == 0 &&    /* 没有ASC或ASCQ */     srb->sense_buffer[12] == 0 &&     srb->sense_buffer[13] == 0) {    srb->result = SAM_STAT_GOOD;    srb->sense_buffer[0] = 0x0;   }  }    //我们传输小于所要求的最小数据量   if (srb->result == SAM_STAT_GOOD &&    srb->request_bufflen - srb->resid < srb->underflow)   srb->result = (DID_ERROR << 16) | (SUGGEST_RETRY << 24);    return;    //出错退出处理:bulk-only传输在一个出错退出后请求一个复位操作     Handle_Abort:  srb->result = DID_ABORT << 16;  if (us->protocol == US_PR_BULK)   us->transport_reset(us); }

对于USB Mass Storage相适应的设备来说,us->transport(srb, us)调用的是函数usb_stor_Bulk_transport,该函数列出如下(在drivers/usb/storage/transport.c中):

int usb_stor_Bulk_transport(struct scsi_cmnd *srb, struct us_data *us) {  struct bulk_cb_wrap *bcb = (struct bulk_cb_wrap *) us->iobuf;  struct bulk_cs_wrap *bcs = (struct bulk_cs_wrap *) us->iobuf;  unsigned int transfer_length = srb->request_bufflen;  unsigned int residue;  int result;  int fake_sense = 0;  unsigned int cswlen;  unsigned int cbwlen = US_BULK_CB_WRAP_LEN;    //对于BULK32设备,设置多余字节到0   if ( unlikely(us->flags & US_FL_BULK32)) {        
本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除。
换一批
延伸阅读

9月2日消息,不造车的华为或将催生出更大的独角兽公司,随着阿维塔和赛力斯的入局,华为引望愈发显得引人瞩目。

关键字: 阿维塔 塞力斯 华为

加利福尼亚州圣克拉拉县2024年8月30日 /美通社/ -- 数字化转型技术解决方案公司Trianz今天宣布,该公司与Amazon Web Services (AWS)签订了...

关键字: AWS AN BSP 数字化

伦敦2024年8月29日 /美通社/ -- 英国汽车技术公司SODA.Auto推出其旗舰产品SODA V,这是全球首款涵盖汽车工程师从创意到认证的所有需求的工具,可用于创建软件定义汽车。 SODA V工具的开发耗时1.5...

关键字: 汽车 人工智能 智能驱动 BSP

北京2024年8月28日 /美通社/ -- 越来越多用户希望企业业务能7×24不间断运行,同时企业却面临越来越多业务中断的风险,如企业系统复杂性的增加,频繁的功能更新和发布等。如何确保业务连续性,提升韧性,成...

关键字: 亚马逊 解密 控制平面 BSP

8月30日消息,据媒体报道,腾讯和网易近期正在缩减他们对日本游戏市场的投资。

关键字: 腾讯 编码器 CPU

8月28日消息,今天上午,2024中国国际大数据产业博览会开幕式在贵阳举行,华为董事、质量流程IT总裁陶景文发表了演讲。

关键字: 华为 12nm EDA 半导体

8月28日消息,在2024中国国际大数据产业博览会上,华为常务董事、华为云CEO张平安发表演讲称,数字世界的话语权最终是由生态的繁荣决定的。

关键字: 华为 12nm 手机 卫星通信

要点: 有效应对环境变化,经营业绩稳中有升 落实提质增效举措,毛利润率延续升势 战略布局成效显著,战新业务引领增长 以科技创新为引领,提升企业核心竞争力 坚持高质量发展策略,塑强核心竞争优势...

关键字: 通信 BSP 电信运营商 数字经济

北京2024年8月27日 /美通社/ -- 8月21日,由中央广播电视总台与中国电影电视技术学会联合牵头组建的NVI技术创新联盟在BIRTV2024超高清全产业链发展研讨会上宣布正式成立。 活动现场 NVI技术创新联...

关键字: VI 传输协议 音频 BSP

北京2024年8月27日 /美通社/ -- 在8月23日举办的2024年长三角生态绿色一体化发展示范区联合招商会上,软通动力信息技术(集团)股份有限公司(以下简称"软通动力")与长三角投资(上海)有限...

关键字: BSP 信息技术
关闭
关闭