Linux下的硬件驱动——USB设备

前言

USB是英文"Universal Serial Bus"的缩写,意为"通用串行总线"。是由Compaq(康柏)、DEC、IBM、Intel、NEC、微软以及Northern Telecom(北方电讯)等公司于1994年11月共同提出的,主要目的就是为了解决接口标准太多的弊端。USB使用一个4针插头作为标准插头,并通过这个标准接头,采用菊花瓣形式把所有外设连接起来,它采用串行方式传输数据,目前最大数据传输率为12Mbps, 支持多数据流和多个设备并行操作,允许外设热插拔。

目前USB接口虽然只发展了2代(USB1.0/1.1,USB2.0),但是USB综合了一个多平台标准的所有优点 -- 包括降低成本,增加兼容性,可连接大量的外部设备,融合先进的功能和品质。使其逐步成为PC接口标准,进入了高速发展期。

那么对于使用Linux系统,正确支持和配置常见的USB设备,就是其使用必不可少的关键一步。

相关技术基础

模块(驱动程序)

模块(module)是在内核空间运行的程序,实际上是一种目标对象文件,没有链接,不能独立运行,但是可以装载到系统中作为内核的一部分运行,从而可以动态扩充内核的功能。模块最主要的用处就是用来实现设备驱动程序。

Linux下对于一个硬件的驱动,可以有两种方式:直接加载到内核代码中,启动内核时就会驱动此硬件设备。另一种就是以模块方式,编译生成一个.o文件。当应用程序需要时再加载进内核空间运行。所以我们所说的一个硬件的驱动程序,通常指的就是一个驱动模块。

设备文件

对于一个设备,它可以在/dev下面存在一个对应的逻辑设备节点,这个节点以文件的形式存在,但它不是普通意义上的文件,它是设备文件,更确切的说,它是设备节点。这个节点是通过mknod命令建立的,其中指定了主设备号和次设备号。主设备号表明了某一类设备,一般对应着确定的驱动程序;次设备号一般是区分不同属性,例如不同的使用方法,不同的位置,不同的操作。这个设备号是从/proc/devices文件中获得的,所以一般是先有驱动程序在内核中,才有设备节点在目录中。这个设备号(特指主设备号)的主要作用,就是声明设备所使用的驱动程序。驱动程序和设备号是一一对应的,当你打开一个设备文件时,操作系统就已经知道这个设备所对应的驱动程序。

SCSI 设备

SCSI是有别于IDE的一个计算机标准接口。现在大部分平板式扫描仪、CD-R刻录机、MO光磁盘机等渐渐趋向使用SCSI接口,加之SCSI又能提供一个高速传送通道,所以,接触到SCSI设备的用户会越来越多。Linux支持很多种的SCSI设备,例如:SCSI硬盘、SCSI光驱、SCSI磁带机。更重要的是,Linux提供了IDE设备对SCSI的模拟(ide-scsi.o模块),我们通常会就把IDE光驱模拟为SCSI光驱进行访问。因为在Linux中很多软件都只能操作SCSI光驱。例如大多数刻录软件、一些媒体播放软件。通常我们的USB存储设备,也模拟为SCSI硬盘而进行访问。

Linux硬件驱动架构

对于一个硬件,Linux是这样来进行驱动的:首先,我们必须提供一个.o的驱动模块文件(这里我们只说明模块方式,其实内核方式是类似的)。我们要使用这个驱动程序,首先要加载运行它(insmod *.o)。这样驱动就会根据自己的类型(字符设备类型或块设备类型,例如鼠标就是字符设备而硬盘就是块设备)向系统注册,注册成功系统会反馈一个主设备号,这个主设备号就是系统对它的唯一标识(例如硬盘块设备在/proc/devices中显示的主设备号为3 ,我们用ls -l /dev/had看到的主设备就肯定是3)。驱动就是根据此主设备号来创建一个一般放置在/dev目录下的设备文件(mknod命令用来创建它,它必须用主设备号这个参数)。在我们要访问此硬件时,就可以对设备文件通过open、read、write等命令进行。而驱动就会接收到相应的read、write操作而根据自己的模块中的相应函数进行了。

其中还有几个比较有关系的东西:一个是/lib/modules/2.4.XX目录,它下面就是针对当前内核版本的模块。只要你的模块依赖关系正确(可以通过depmod设置),你就可以通过modprobe 命令加载而不需要知道具体模块文件位置。 另一个是/etc/modules.conf文件,它定义了一些常用设备的别名。系统就可以在需要此设备支持时,正确寻找驱动模块。例如alias eth0 e100,就代表第一块网卡的驱动模块为e100.o。他们的关系图如下:


 

配置USB设备

内核中配置.

要启用 Linux USB 支持,首先进入"USB support"节并启用"Support for USB"选项(对应模块为usbcore.o)。尽管这个步骤相当直观明了,但接下来的 Linux USB 设置步骤则会让人感到糊涂。特别地,现在需要选择用于系统的正确 USB 主控制器驱动程序。选项是"EHCI" (对应模块为ehci-hcd.o)、"UHCI" (对应模块为usb-uhci.o)、"UHCI (alternate driver)"和"OHCI" (对应模块为usb-ohci.o)。这是许多人对 Linux 的 USB 开始感到困惑的地方。

要理解"EHCI"及其同类是什么,首先要知道每块支持插入 USB 设备的主板或 PCI 卡都需要有 USB 主控制器芯片组。这个特别的芯片组与插入系统的 USB 设备进行相互操作,并负责处理允许 USB 设备与系统其它部分通信所必需的所有低层次细节。

Linux USB 驱动程序有三种不同的 USB 主控制器选项是因为在主板和 PCI 卡上有三种不同类型的 USB 芯片。"EHCI"驱动程序设计成为实现新的高速 USB 2.0 协议的芯片提供支持。"OHCI"驱动程序用来为非 PC 系统上的(以及带有 SiS 和 ALi 芯片组的 PC 主板上的)USB 芯片提供支持。"UHCI"驱动程序用来为大多数其它 PC 主板(包括 Intel 和 Via)上的 USB 实现提供支持。只需选择与希望启用的 USB 支持的类型对应的"?HCI"驱动程序即可。如有疑惑,为保险起见,可以启用"EHCI"、"UHCI" (两者中任选一种,它们之间没有明显的区别)和"OHCI"。( 赵明注:根据文档,EHCI已经包含了UHCI和OHCI,但目前就我个人的测试,单独加EHCI是不行的,通常我的做法是根据主板类型加载UHCI或OHCI后,再加载EHCI这样才可以支持USB2.0设备)。

启用了"USB support"和适当的"?HCI"USB 主控制器驱动程序后,使 USB 启动并运行只需再进行几个步骤。应该启用"Preliminary USB device filesystem",然后确保启用所有特定于将与 Linux 一起使用的实际 USB 外围设备的驱动程序。例如,为了启用对 USB 游戏控制器的支持,我启用了"USB Human Interface Device (full HID) support"。我还启用了主"Input core support" 节下的"Input core support"和"Joystick support"。

一旦用新的已启用 USB 的内核重新引导后,若/proc/bus/usb下没有相应USB设备信息,应输入以下命令将 USB 设备文件系统手动挂装到 /proc/bus/usb:

# mount -t usbdevfs none /proc/bus/usb 

为了在系统引导时自动挂装 USB 设备文件系统,请将下面一行添加到 /etc/fstab 中的 /proc 挂装行之后:

none /proc/bus/usb usbdevfs defaults 0 0 

模块的配置方法.

在很多时候,我们的USB设备驱动并不包含在内核中。其实我们只要根据它所需要使用的模块,逐一加载。就可以使它启作用。

首先要确保在内核编译时以模块方式选择了相应支持。这样我们就应该可以在/lib/modules/2.4.XX目录看到相应.o文件。在加载模块时,我们只需要运行modprobe xxx.o就可以了(modprobe主要加载系统已经通过depmod登记过的模块,insmod一般是针对具体.o文件进行加载)

对应USB设备下面一些模块是关键的。

usbcore.o 要支持usb所需要的最基础模块
usb-uhci.o (已经提过)
usb-ohci.o (已经提过)
uhci.o 另一个uhci驱动程序,我也不知道有什么用,一般不要加载,会死机的
ehci-hcd.o (已经提过 usb2.0)
hid.o USB人机界面设备,像鼠标呀、键盘呀都需要
usb-storage.o USB存储设备,U盘等用到

相关模块

ide-disk.o IDE硬盘
ide-scsi.o 把IDE设备模拟SCSI接口
scsi_mod.o SCSI支持

注意kernel config其中一项:

	Probe all LUNs on each SCSI device

最好选上,要不某些同时支持多个口的读卡器只能显示一个。若模块方式就要带参数安装或提前在/etc/modules.conf中加入以下项,来支持多个LUN。

	add options scsi_mod max_scsi_luns=9  

sd_mod.o SCSI硬盘
sr_mod.o SCSI光盘
sg.o SCSI通用支持(在某些探测U盘、SCSI探测中会用到)

常见USB设备及其配置

在Linux 2.4的内核中已经支持不下20种设备。它支持几乎所有的通用设备如键盘、鼠标、modem、打印机等,并不断地添加厂商新的设备象数码相机、MP3、网卡等。下面就是几个最常见设备的介绍和使用方法:

USB鼠标:

键盘和鼠标属于低速的输入设备,对于已经为用户认可的PS/2接口,USB键盘和USB鼠标似乎并没有太多更优越的地方。现在的大部分鼠标采用了PS/2接口,不过USB接口的鼠标也越来越多,两者相比,各有优势:一般来说,USB的鼠标接口的带宽大于PS/2鼠标,也就是说在同样的时间内,USB鼠标扫描次数就要多于PS/2鼠标,这样在定位上USB鼠标就更为精确;同时USB接口鼠标的默认采样率也比较高,达到125HZ,而PS/2接口的鼠标仅有40HZ(Windows 9x/Me)或是60HZ(Windows NT/2000)。

对于USB设备你当然必须先插入相应的USB控制器模块:usb-uhci.o或usb-ohci.o

	modprobe usb-uhci

USB鼠标为了使其正常工作,您必须先插入模块usbmouse.o和mousedev.o

	modprobe usbmouse
	modprobe mousedev

若你把HID input layer支持和input core 支持也作为模块方式安装,那么启动hid模块和input模块也是必要的。

	modprobe hid
	modprobe input

USB键盘:

一般的,我们现在使用的键盘大多是PS/2的,USB键盘还比较少见,但是下来的发展,键盘将向USB接口靠拢。使用USB键盘基本上没有太多的要求,只需在主板的BIOS设定对USB键盘的支持,就可以在各系统中完全无障碍的使用,而且更可以真正做到在即插即用和热插拔使用,并能提供两个USB连接埠:让您可以轻易地直接将具有USB接头的装置接在您的键盘上,而非计算机的后面。

同样你当然必须先插入相应的USB控制器模块:usb-uhci.o或usb-ohci.o

	modprobe usb-uhci

然后您还必须插入键盘模块usbkbd.o,以及keybdev.o,这样usb键盘才能够正常工作。此时,运行的系统命令:

modprobe usbkbd
modprobe keybdev

同样若你把HID input layer支持和input core 支持也作为模块方式安装,那么启动hid模块和input模块也是必要的。

U盘和USB读卡器:

数码存储设备现在对我们来说已经是相当普遍的了。CF卡、SD卡、Memory Stick等存储卡已经遍及我们的身边,通常,他们的读卡器都是USB接口的。另外,很多MP3、数码相机也都是USB接口和计算机进行数据传递。更我们的U盘、USB硬盘,作为移动存储设备,已经成为我们的必须装备。

在Linux下这些设备通常都是以一种叫做usb-storage的方式进行驱动。要使用他们必须加载此模块

	modprobe usb-storage

当然,usbcore.o 和usb-uhci.o或usb-ohci也肯定是不可缺少的。另外,若你系统中SCSI支持也是模块方式,那么下面的模块也要加载

	modprobe scsi_mod
	modprobe sd_mod

在加载完这些模块后,我们插入U盘或存储卡,就会发现系统中多了一个SCSI硬盘,通过正确地mount它,就可以使用了(SCSI硬盘一般为/dev/sd?,可参照文章后面的常见问题解答)。

	mount /dev/sda1 /mnt

Linux支持的其他USB设备。

MODEM--(比较常见)


网络设备


摄像头--(比较常见)例如ov511.o


联机线--可以让你的两台电脑用USB线实现网络功能。usbnet.o


显示器--(我没见过)


游戏杆


电视盒--(比较常见)


手写板--(比较常见)


扫描仪--(比较常见)


刻录机--(比较常见)


打印机--(比较常见)


注意:上面所说的每个驱动模块,并不是都要手动加载,有很多系统会在启动或你的应用需要时自动加载的,写明这些模块,是便于你在不能够使用USB设备时,可以自行检查。只要用lsmod确保以上模块已经被系统加载,你的设备就应该可以正常工作了。当然注意有些模块已经以内核方式在kernel启动时存在了(这些模块文件在/lib/modules/2.4.XX中是找不到的)。

最常遇见的USB问题

  1. 有USB设备的系统安装完redhat 7.3启动死机问题

    有USB设备,当你刚装完redhat 7.3第一次启动时,总会死掉。主要原因是Linux在安装时探测到有usb-uhci和ehci-hcd两个控制器,但在启动时,加载完usb-uhci再加载ehci-hcd就会有冲突。分析认为redhat7.3系统内核在支持USB2.0标准上存在问题。在其他版本的Linux中均不存在此问题。

    解决办法:在lilo或grub启动时用命令行传递参数init=/sbin/init。这样在启动后就不运行其他服务而直接启动shell。然后运行
    mount -o remount,rw / 使/ 可写,init直接启动的系统默认只mount /为只读 
    然后vi /etc/modules.config文件 
    删除alias usb-controller1 ehci-hcd一行。或前面加#注释掉 
    然后mount -o remount,ro / 使/ 只读,避免直接关机破坏文件系统 
    然后就可以按Ctrl-Alt-Delete直接重启了 
    或许,你有更简单的办法:换USB键盘和鼠标为PS2接口,启动后修改/etc/modules.config文件。

  2. 我们已经知道U盘在Linux中会模拟为SCSI设备去访问,可怎么知道它对应那个SCSI设备呢?

    方法1:推测。通常你第一次插入一个SCSI设备,它就是sda,第二个就是sdb以此类推。你启动Linux插入一个U盘,就试试sda,换了一个就可能是sdb。这里注意两个特例:1) 你用的是联想U盘,它可能存在两个设备区(一个用于加密或启动电脑),这样就可能一次用掉两个sda、sdb,换个U盘就是sdc、sdd。2) 联想数码电脑中,可能已经有了六合一读卡器。它同样也是USB存储设备。它会占掉一个或两个SCSI设备号。

    方法2:看信息。其实,只要你提前把usb-storage.o、scsi_mod.o、sd_mod.o模块加载(直接在kernel中也可以)了,在你插入和拔出U盘时,系统会自动打出信息如下:

    SCSI device sda: 60928 512-byte hdwr sectors ( 31 MB )
    sda: Write Protect is on
    

    根据此信息,你就知道它在sda上了。当然,可能你的系统信息级别比较高,上述信息可能没有打出,这时候你只要tail /var/log/messages就可以看到了。

    方法3:同样,cat /proc/partitions也可以看到分区信息,其中sd?就是U盘所对应的了。若根本没有sd设备,就要检查你的SCSI模块和usb-storage模块是否正确加载了。

  3. 在使用U盘或存储卡时,我该mount /dev/sda还是/dev/sda1呢?

    这是一个历史遗留问题。存储卡最初尺寸很小,很多厂商在使用时,就直接使用存储,不含有分区表信息。而随着存储卡尺寸的不断扩大,它也就引入了类似硬盘分区的概念。例如/dev/hda你可以分成主分区hda1、hda2扩展分区hda3,然后把扩展分区hda3又分为逻辑分区hda5、hda6、hda7等。这样,通常的U盘就被分成一个分区sda1,类似把硬盘整个分区分成一个主分区hda1。实际上,我们完全可以通过fdisk /dev/sda对存储卡进行完全类似硬盘的分区方式分成sda1、sda2甚至逻辑分区sda5、sda6。实际上,对USB硬盘目前你的确需要这样,因为它通常都是多少G的容量。而且通常,它里面就是笔记本硬盘。

    一个好玩的问题。你在Linux下用fdisk /dev/sda 对U盘进行了多分区,这时候到windows下,你会发现怎么找,怎么格式化,U盘都只能找到第一个分区大小尺寸,而且使用看不出任何问题。这主要是windows驱动对U盘都只支持一个分区的缘故。你是不是可以利用它来进行一些文件的隐藏和保护?你是不是可以和某些人没玩过Linux的人开些玩笑:你的U盘容量变小了J。

    现在较多的数码设备也和windows一样,是把所有U盘容量分为一个,所以在对待U盘的时候,通常你mount的是sda1。但对于某些特殊的数码设备格式化的U盘或存储卡(目前我发现的是一款联想的支持模拟USB软盘的U盘和我的一个数码相机),你就要mount /dev/sda。因为它根本就没分区表(若mount /dev/sda1通常的效果是死掉)。其实,这些信息,只要你注意了/proc/partitions文件,都应该注意到的。

  4. 每次插入U盘,都要寻找对应设备文件名,都要手动mount,我能不能做到象windows那样插入就可以使用呢。

    当然可以,不过你需要做一些工作。我这里只提供一些信息帮助你去尝试完成设置:Linux内核提供了一种叫hotplug支持的东西,它可以让你系统在PCI设备、USB等设备插拔时做一些事情。而automount 功能可以使你的软驱、光盘等设备的分区自动挂载和自动卸载。你甚至可以在KDE桌面中创建相应的图标,方便你操作。具体设置方法就要你自己去尝试了。反正我使用Linux已经麻木了,不就是敲一行命令嘛。

    USB驱动开发

    在掌握了USB设备的配置后,对于程序员,我们就可以尝试进行一些简单的USB驱动的修改和开发了。这一段落,我们会讲解一个最基础USB框架的基础上,做两个小的USB驱动的例子。

    USB骨架

    在Linux kernel源码目录中driver/usb/usb-skeleton.c为我们提供了一个最基础的USB驱动程序。我们称为USB骨架。通过它我们仅需要修改极少的部分,就可以完成一个USB设备的驱动。我们的USB驱动开发也是从她开始的。

    那些linux下不支持的USB设备几乎都是生产厂商特定的产品。如果生产厂商在他们的产品中使用自己定义的协议,他们就需要为此设备创建特定的驱动程序。当然我们知道,有些生产厂商公开他们的USB协议,并帮助Linux驱动程序的开发,然而有些生产厂商却根本不公开他们的USB协议。因为每一个不同的协议都会产生一个新的驱动程序,所以就有了这个通用的USB驱动骨架程序, 它是以pci 骨架为模板的。

    如果你准备写一个linux驱动程序,首先要熟悉USB协议规范。USB主页上有它的帮助。一些比较典型的驱动可以在上面发现,同时还介绍了USB urbs的概念,而这个是usb驱动程序中最基本的。

    Linux USB 驱动程序需要做的第一件事情就是在Linux USB 子系统里注册,并提供一些相关信息,例如这个驱动程序支持那种设备,当被支持的设备从系统插入或拔出时,会有哪些动作。所有这些信息都传送到USB 子系统中,在usb骨架驱动程序中是这样来表示的:

    static struct usb_driver skel_driver = {
         name:        "skeleton",
         probe:       skel_probe,
         disconnect:  skel_disconnect,
         fops:        &skel_fops,
         minor:       USB_SKEL_MINOR_BASE,
         id_table:    skel_table,
    };
    

    变量name是一个字符串,它对驱动程序进行描述。probe 和disconnect 是函数指针,当设备与在id_table 中变量信息匹配时,此函数被调用。

    fops和minor变量是可选的。大多usb驱动程序钩住另外一个驱动系统,例如SCSI,网络或者tty子系统。这些驱动程序在其他驱动系统中注册,同时任何用户空间的交互操作通过那些接口提供,比如我们把SCSI设备驱动作为我们USB驱动所钩住的另外一个驱动系统,那么我们此USB设备的read、write等操作,就相应按SCSI设备的read、write函数进行访问。但是对于扫描仪等驱动程序来说,并没有一个匹配的驱动系统可以使用,那我们就要自己处理与用户空间的read、write等交互函数。Usb子系统提供一种方法去注册一个次设备号和file_operations函数指针,这样就可以与用户空间实现方便地交互。

    USB骨架程序的关键几点如下:

    1. USB驱动的注册和注销

      Usb驱动程序在注册时会发送一个命令给usb_register,通常在驱动程序的初始化函数里。

      当要从系统卸载驱动程序时,需要注销usb子系统。即需要usb_unregister 函数处理:

      static void __exit usb_skel_exit(void)
      {
         /* deregister this driver with the USB subsystem */
         usb_deregister(&skel_driver);
      }
      module_exit(usb_skel_exit);
      

      当usb设备插入时,为了使linux-hotplug(Linux中PCI、USB等设备热插拔支持)系统自动装载驱动程序,你需要创建一个MODULE_DEVICE_TABLE。代码如下(这个模块仅支持某一特定设备):

      /* table of devices that work with this driver */
      static struct usb_device_id skel_table [] = {
          { USB_DEVICE(USB_SKEL_VENDOR_ID,
            USB_SKEL_PRODUCT_ID) },
          { }                      /* Terminating entry */
      };
      MODULE_DEVICE_TABLE (usb, skel_table);
      

      USB_DEVICE宏利用厂商ID和产品ID为我们提供了一个设备的唯一标识。当系统插入一个ID匹配的USB设备到USB总线时,驱动会在USB core中注册。驱动程序中probe 函数也就会被调用。usb_device 结构指针、接口号和接口ID都会被传递到函数中。

      static void * skel_probe(struct usb_device *dev,
      unsigned int ifnum, const struct usb_device_id *id)
      

      驱动程序需要确认插入的设备是否可以被接受,如果不接受,或者在初始化的过程中发生任何错误,probe函数返回一个NULL值。否则返回一个含有设备驱动程序状态的指针。通过这个指针,就可以访问所有结构中的回调函数。

      在骨架驱动程序里,最后一点是我们要注册devfs。我们创建一个缓冲用来保存那些被发送给usb设备的数据和那些从设备上接受的数据,同时USB urb 被初始化,并且我们在devfs子系统中注册设备,允许devfs用户访问我们的设备。注册过程如下:

      /* initialize the devfs node for this device
         and register it */
      sprintf(name, "skel%d", skel->minor);
      skel->devfs = devfs_register 
                    (usb_devfs_handle, name,
                     DEVFS_FL_DEFAULT, USB_MAJOR,
                     USB_SKEL_MINOR_BASE + skel->minor,
                     S_IFCHR | S_IRUSR | S_IWUSR |
                     S_IRGRP | S_IWGRP | S_IROTH,
                     &skel_fops, NULL);
      

      如果devfs_register函数失败,不用担心,devfs子系统会将此情况报告给用户。

      当然最后,如果设备从usb总线拔掉,设备指针会调用disconnect 函数。驱动程序就需要清除那些被分配了的所有私有数据、关闭urbs,并且从devfs上注销调自己。

      /* remove our devfs node */
      devfs_unregister(skel->devfs);
      

      现在,skeleton驱动就已经和设备绑定上了,任何用户态程序要操作此设备都可以通过file_operations结构所定义的函数进行了。首先,我们要open此设备。在open函数中MODULE_INC_USE_COUNT 宏是一个关键,它的作用是起到一个计数的作用,有一个用户态程序打开一个设备,计数器就加一,例如,我们以模块方式加入一个驱动,若计数器不为零,就说明仍然有用户程序在使用此驱动,这时候,你就不能通过rmmod命令卸载驱动模块了。

      /* increment our usage count for the module */
      MOD_INC_USE_COUNT;
      ++skel->open_count;
      /* save our object in the file's private structure */
      file->private_data = skel;
      

      当open完设备后,read、write函数就可以收、发数据了。

    2. skel的write、和read函数

      他们是完成驱动对读写等操作的响应。

      在skel_write中,一个FILL_BULK_URB函数,就完成了urb 系统callbak和我们自己的skel_write_bulk_callback之间的联系。注意skel_write_bulk_callback是中断方式,所以要注意时间不能太久,本程序中它就只是报告一些urb的状态等。

      read 函数与write 函数稍有不同在于:程序并没有用urb 将数据从设备传送到驱动程序,而是我们用usb_bulk_msg 函数代替,这个函数能够不需要创建urbs 和操作urb函数的情况下,来发送数据给设备,或者从设备来接收数据。我们调用usb_bulk_msg函数并传提一个存储空间,用来缓冲和放置驱动收到的数据,若没有收到数据,就失败并返回一个错误信息。

    3. usb_bulk_msg函数

      当对usb设备进行一次读或者写时,usb_bulk_msg 函数是非常有用的; 然而, 当你需要连续地对设备进行读/写时,建议你建立一个自己的urbs,同时将urbs 提交给usb子系统。

    4. skel_disconnect函数

      当我们释放设备文件句柄时,这个函数会被调用。MOD_DEC_USE_COUNT宏会被用到(和MOD_INC_USE_COUNT刚好对应,它减少一个计数器),首先确认当前是否有其它的程序正在访问这个设备,如果是最后一个用户在使用,我们可以关闭任何正在发生的写,操作如下:

      /* decrement our usage count for the device */
      --skel->open_count;
      if (skel->open_count <= 0) {
         /* shutdown any bulk writes that might be
            going on */
         usb_unlink_urb (skel->write_urb);
         skel->open_count = 0;
      }
      /* decrement our usage count for the module */
      MOD_DEC_USE_COUNT;
      

      最困难的是,usb 设备可以在任何时间点从系统中取走,即使程序目前正在访问它。usb驱动程序必须要能够很好地处理解决此问题,它需要能够切断任何当前的读写,同时通知用户空间程序:usb设备已经被取走。

      如果程序有一个打开的设备句柄,在当前结构里,我们只要把它赋值为空,就像它已经消失了。对于每一次设备读写等其它函数操作,我们都要检查usb_device结构是否存在。如果不存在,就表明设备已经消失,并返回一个-ENODEV错误给用户程序。当最终我们调用release 函数时,在没有文件打开这个设备时,无论usb_device结构是否存在、它都会清空skel_disconnect函数所作工作。

      Usb 骨架驱动程序,提供足够的例子来帮助初始人员在最短的时间里开发一个驱动程序。更多信息你可以到linux usb开发新闻组去寻找。

    U盘、USB读卡器、MP3、数码相机驱动

    对于一款windows下用的很爽的U盘、USB读卡器、MP3或数码相机,可能Linux下却不能支持。怎么办?其实不用伤心,也许经过一点点的工作,你就可以很方便地使用它了。通常是此U盘、USB读卡器、MP3或数码相机在WindowsXP中不需要厂商专门的驱动就可以识别为移动存储设备,这样的设备才能保证成功,其他的就看你的运气了。

    USB存储设备,他们的read、write等操作都是通过上章节中提到的钩子,把自己的操作钩到SCSI设备上去的。我们就不需要对其进行具体的数据读写处理了。

    第一步:我们通过cat /proc/bus/usb/devices得到当前系统探测到的USB总线上的设备信息。它包括Vendor、ProdID、Product等。下面是我买的一款杂牌CF卡读卡器插入后的信息片断:

    T: Bus=01 Lev=01 Prnt=01 Port=01 Cnt=02 Dev#= 5 Spd=12 MxCh= 0 
    D: Ver= 1.10 Cls=00(>ifc ) Sub=00 Prot=00 MxPS=8 #Cfgs= 1 
    P: Vendor=07c4 ProdID=a400 Rev= 1.13 
    S: Manufacturer=USB 
    S: Product=Mass Storage 
    C:* #Ifs= 1 Cfg#= 1 Atr=80 MxPwr=70mA 
    I: If#= 0 Alt= 0 #EPs= 2 Cls=08(vend.) Sub=06 Prot=50 Driver=usb-storage 
    E: Ad=81(I) Atr=02(Bulk) MxPS= 64 Ivl= 0ms
    E: Ad=02(O) Atr=02(Bulk) MxPS= 64 Ivl= 0ms
    

    其中,我们最关心的是Vendor=07c4 ProdID=a400和Manufacturer=USB(果然是杂牌,厂商名都看不到)Product= Mass Storage。

    对于这些移动存储设备,我们知道Linux下都是通过usb-storage.o驱动模拟成scsi设备去支持的,之所以不支持,通常是usb-storage驱动未包括此厂商识别和产品识别信息(在类似skel_probe的USB最初探测时被屏蔽了)。对于USB存储设备的硬件访问部分,通常是一致的。所以我们要支持它,仅需要修改usb-storage中关于厂商识别和产品识别列表部分。

    第二部,打开drivers/usb/storage/unusual_devs.h文件,我们可以看到所有已知的产品登记表,都是以UNUSUAL_DEV(idVendor, idProduct, bcdDeviceMin, bcdDeviceMax, vendor_name, product_name, use_protocol, use_transport, init_function, Flags)方式登记的。其中相应的涵义,你就可以根据命名来判断了。所以只要我们如下填入我们自己的注册,就可以让usb-storage驱动去认识和发现它。

    UNUSUAL_DEV(07c4, a400, 0x0000, 0xffff, 
    " USB ", " Mass Storage ", 
    US_SC_SCSI, US_PR_BULK, NULL, 
    US_FL_FIX_INQUIRY | US_FL_START_STOP |US_FL_MODE_XLATE )
    

    注意:添加以上几句的位置,一定要正确。比较发现,usb-storage驱动对所有注册都是按idVendor, idProduct数值从小到大排列的。我们也要放在相应位置。

    最后,填入以上信息,我们就可以重新编译生成内核或usb-storage.o模块。这时候插入我们的设备就可以跟其他U盘一样作为SCSI设备去访问了。

    键盘飞梭支持

    目前很多键盘都有飞梭和手写板,下面我们就尝试为一款键盘飞梭加入一个驱动。在通常情况,当我们插入USB接口键盘时,在/proc/bus/usb/devices会看到多个USB设备。比如:你的USB键盘上的飞梭会是一个,你的手写板会是一个,若是你的USB键盘有USB扩展连接埠,也会看到。

    下面是具体看到的信息

    T:  Bus=02 Lev=00 Prnt=00 Port=00 Cnt=00 Dev#=  1 Spd=12  MxCh= 2
    B:  Alloc= 11/900 us ( 1%), #Int=  1, #Iso=  0
    D:  Ver= 1.00 Cls=09(hub  ) Sub=00 Prot=00 MxPS= 8 #Cfgs=  1
    P:  Vendor=0000 ProdID=0000 Rev= 0.00
    S:  Product=USB UHCI Root Hub
    S:  SerialNumber=d800
    C:* #Ifs= 1 Cfg#= 1 Atr=40 MxPwr=  0mA
    I:  If#= 0 Alt= 0 #EPs= 1 Cls=09(hub  ) Sub=00 Prot=00 Driver=hub
    E:  Ad=81(I) Atr=03(Int.) MxPS=   8 Ivl=255ms
    T:  Bus=02 Lev=01 Prnt=01 Port=01 Cnt=01 Dev#=  3 Spd=12  MxCh= 3
    D:  Ver= 1.10 Cls=09(hub  ) Sub=00 Prot=00 MxPS= 8 #Cfgs=  1
    P:  Vendor=07e4 ProdID=9473 Rev= 0.02
    S:  Manufacturer=ALCOR
    S:  Product=Movado USB Keyboard
    C:* #Ifs= 1 Cfg#= 1 Atr=e0 MxPwr=100mA
    I:  If#= 0 Alt= 0 #EPs= 1 Cls=09(hub  ) Sub=00 Prot=00 Driver=hub
    E:  Ad=81(I) Atr=03(Int.) MxPS=   1 Ivl=255ms
    

    找到相应的信息后就可开始工作了。实际上,飞梭的定义和键盘键码通常是一样的,所以我们参照drivers/usb/usbkbd..c代码进行一些改动就可以了。因为没能拿到相应的硬件USB协议,我无从知道飞梭在按下时通讯协议众到底发什么,我只能把它的信息打出来进行分析。幸好,它比较简单,在下面代码的usb_kbd_irq函数中if(kbd->new[0] == (char)0x01)和if(((kbd->new[1]>>4)&0x0f)!=0x7)就是判断飞梭左旋。usb_kbd_irq函数就是键盘中断响应函数。他的挂接,就是在usb_kbd_probe函数中

    FILL_INT_URB(&kbd->irq, dev, pipe, kbd->new, maxp > 8 ? 8 : maxp,
    		usb_kbd_irq, kbd, endpoint->bInterval);
    

    一句中实现。

    从usb骨架中我们知道,usb_kbd_probe函数就是在USB设备被系统发现是运行的。其他部分就都不是关键了。你可以根据具体的探测值(Vendor=07e4 ProdID=9473等)进行一些修改就可以了。值得一提的是,在键盘中断中,我们的做法是收到USB飞梭消息后,把它模拟成左方向键和右方向键,在这里,就看你想怎么去响应它了。当然你也可以响应模拟成F14、F15等扩展键码。

    在了解了此基本的驱动后,对于一个你已经拿到通讯协议的键盘所带手写板,你就应该能进行相应驱动的开发了吧。

    程序见附录1: 键盘飞梭驱动。

    使用此驱动要注意的问题:在加载此驱动时你必须先把hid设备卸载,加载完usbhkey.o模块后再加载hid.o。因为若hid存在,它的probe会屏蔽系统去利用我们的驱动发现我们的设备。其实,飞梭本来就是一个hid设备,正确的方法,或许你应该修改hid的probe函数,然后把我们的驱动融入其中。

    参考资料

    1. 《LINUX设备驱动程序》 
      ALESSANDRO RUBINI著 
      LISOLEG 译 
    2. 《Linux系统分析与高级编程技术》 
      周巍松 编著 
    3. Linux Kernel-2.4.20源码和文档说明 

    附录1:键盘飞梭驱动

    #include <linux/kernel.h>
    #include <linux/slab.h>
    #include <linux/module.h>
    #include <linux/input.h>
    #include <linux/init.h>
    #include <linux/usb.h>
    #include <linux/kbd_ll.h>
    /*
     * Version Information
     */
    #define DRIVER_VERSION ""
    #define DRIVER_AUTHOR "TGE HOTKEY "
    #define DRIVER_DESC "USB HID Tge hotkey driver"
    #define USB_HOTKEY_VENDOR_ID 0x07e4
    #define USB_HOTKEY_PRODUCT_ID 0x9473
    //厂商和产品ID信息就是/proc/bus/usb/devices中看到的值
    MODULE_AUTHOR( DRIVER_AUTHOR );
    MODULE_DESCRIPTION( DRIVER_DESC );
    struct usb_kbd {
      struct input_dev dev;
      struct usb_device *usbdev;
      unsigned char new[8];
      unsigned char old[8];
      struct urb irq, led;
    //  devrequest dr;     
    //这一行和下一行的区别在于kernel2.4.20版本对usb_kbd键盘结构定义发生了变化
          struct usb_ctrlrequest dr;
      unsigned char leds, newleds;
      char name[128];
      int open;
    };
    //此结构来自内核中drivers/usb/usbkbd..c
    static void usb_kbd_irq(struct urb *urb)
    {
      struct usb_kbd *kbd = urb->context;
            int *new;
            new = (int *) kbd->new;
      if(kbd->new[0] == (char)0x01)
      {
        if(((kbd->new[1]>>4)&0x0f)!=0x7)
        {
    handle_scancode(0xe0,1);
    handle_scancode(0x4b,1);
                    handle_scancode(0xe0,0);
                    handle_scancode(0x4b,0);
        }
        else
        {
            handle_scancode(0xe0,1);
                    handle_scancode(0x4d,1);
                    handle_scancode(0xe0,0);
                    handle_scancode(0x4d,0);
        }
      }
      
      
      printk("new=%x %x %x %x %x %x %x %x",
        kbd->new[0],kbd->new[1],kbd->new[2],kbd->new[3],
        kbd->new[4],kbd->new[5],kbd->new[6],kbd->new[7]); 
        
    }
    static void *usb_kbd_probe(struct usb_device *dev, unsigned int ifnum,
                               const struct usb_device_id *id)
    {
      struct usb_interface *iface;
            struct usb_interface_descriptor *interface;
      struct usb_endpoint_descriptor *endpoint;
            struct usb_kbd *kbd;
            int  pipe, maxp;
      iface = &dev->actconfig->interface[ifnum];
            interface = &iface->altsetting[iface->act_altsetting];
      if ((dev->descriptor.idVendor != USB_HOTKEY_VENDOR_ID) ||
        (dev->descriptor.idProduct != USB_HOTKEY_PRODUCT_ID) ||
        (ifnum != 1))
      {
        return NULL;
      }
      if (dev->actconfig->bNumInterfaces != 2)
      {
        return NULL;  
      }
      if (interface->bNumEndpoints != 1) return NULL;
            endpoint = interface->endpoint + 0;
            pipe = usb_rcvintpipe(dev, endpoint->bEndpointAddress);
            maxp = usb_maxpacket(dev, pipe, usb_pipeout(pipe));
            usb_set_protocol(dev, interface->bInterfaceNumber, 0);
            usb_set_idle(dev, interface->bInterfaceNumber, 0, 0);
      printk(KERN_INFO "GUO: Vid = %.4x, Pid = %.4x, Device = %.2x,
                         ifnum = %.2x, bufCount = %.8x\\n",
      dev->descriptor.idVendor,dev->descriptor.idProduct,
                    dev->descriptor.bcdDevice, ifnum, maxp);
            if (!(kbd = kmalloc(sizeof(struct usb_kbd), GFP_KERNEL))) return NULL;
            memset(kbd, 0, sizeof(struct usb_kbd));
            kbd->usbdev = dev;
            FILL_INT_URB(&kbd->irq, dev, pipe, kbd->new, maxp > 8 ? 8 : maxp,
        usb_kbd_irq, kbd, endpoint->bInterval);
      kbd->irq.dev = kbd->usbdev;
      if (dev->descriptor.iManufacturer)
                    usb_string(dev, dev->descriptor.iManufacturer, kbd->name, 63);
      if (usb_submit_urb(&kbd->irq)) {
                    kfree(kbd);
                    return NULL;
            }
      
      printk(KERN_INFO "input%d: %s on usb%d:%d.%d\\n",
                     kbd->dev.number, kbd->name, dev->bus->busnum, 
                                 dev->devnum, ifnum);
            return kbd;
    }
    static void usb_kbd_disconnect(struct usb_device *dev, void *ptr)
    {
      struct usb_kbd *kbd = ptr;
            usb_unlink_urb(&kbd->irq);
            kfree(kbd);
    }
    static struct usb_device_id usb_kbd_id_table [] = {
      { USB_DEVICE(USB_HOTKEY_VENDOR_ID, USB_HOTKEY_PRODUCT_ID) },
      { }            /* Terminating entry */
    };
    MODULE_DEVICE_TABLE (usb, usb_kbd_id_table);
    static struct usb_driver usb_kbd_driver = {
      name:    "Hotkey",
      probe:    usb_kbd_probe,
      disconnect:  usb_kbd_disconnect,
      id_table:  usb_kbd_id_table,
      NULL,
    };
    static int __init usb_kbd_init(void)
    {
      usb_register(&usb_kbd_driver);
      info(DRIVER_VERSION ":" DRIVER_DESC);
      return 0;
    }
    static void __exit usb_kbd_exit(void)
    {
      usb_deregister(&usb_kbd_driver);
    }
    module_init(usb_kbd_init);
    module_exit(usb_kbd_exit);
    



  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
资源大于15MB分2次上传。 清晰度一般。加到11章 第12,13章没有。 第1章 嵌入式系统基础. 1.1 嵌入式系统简介 1.1.1 嵌入式系统定义 1.1.2 嵌入式系统与PC 1.1.3 嵌入式系统的特点 1.2 嵌入式系统的发展 1.2.1 嵌入式系统现状 1.2.2 嵌入式系统发展趋势 1.3 嵌入式操作系统与实时操作系统 1.3.1 Linux 1.3.2 uC/OS 1.3.3 Windows CE 1.3.4 VxWorks 1.3.5 Palm OS 1.3.6 QNX 1.4 嵌入式系统选型 第2章 基于ARM9处理器的硬件开发平台 2.1 ARM处理器简介 2.1.1 ARM公司简介 2.1.2 ARM微处理器核 .2.2 ARM9微处理器简介 2.2.1 与ARM7处理器的比较 2.2.2 三星S3C2410X处理器详解 2.3 FS2410开发平台 第3章 创建嵌入式系统开发环境 3.1 嵌入式Linux的开发环境 3.2 Cygwin 3.3 虚拟机 3.4 交叉编译的预备知识 3.4.1 Make命令和Makefile文件 3.4.2 binutils工具包 3.4.3 gcc编译器 3.4.4 Glibc库 3.4.5 GDB 3.5 交叉编译 3.5.1 创建编译环境 3.5.2 编译binutils 3.5.3 编译bootstrap_gcc 3.5.4 编译Glibc 3.5.5 编译完整的gcc 3.5.6 编译GDB 3.5.7 成果 3.5.8 其他交叉编译方法 3.6 通过二进制软件包创建交叉编译环境 3.7 开发套件 第4章 调试嵌入式系统程序 4.1 嵌入式系统调试方法 4.1.1 实时在线仿真 4.1.2 模拟调试 4.1.3 软件调试 4.1.4 BDM/JTAG调试 4.2 ARM仿真器 4.2.1 techorICE ARM仿真器 4.2.2 ARM仿真器工作原理 4.2.3 ARM仿真器的系统功能层次 4.2.4 使用仿真器和ADS Debugger调试ARM开发板 4.3 JTAG接口 4.3.1 JTAG引脚定义 4.3.2 通过JTAG烧写Flash 4.3.3 烧写Flash技术内幕 第5章 Bootloader 5.1 嵌入式系统的引导代码 5.1.1 初识Bootloader 5.1.2 Bootloader的启动流程 5.2 Bootloader之vivi 5.2.1 vivi简介 5.2.2 vivi的配置与编译 5.2.3 vivi代码导读 5.3 Bootloader之U-Boot 5.3.1 U-Boot代码结构分析 5.3.2 编译U-Boot代码 5.3.3 U-Boot代码导读 5.3.4 U-Boot命令 5.4 FS2410的Bootloader 第6章 Linux系统在ARM平台的移植 6.1 移植的概念 6.2 Linux内核结构 6.3 Linux-2.4内核向ARM平台的移植 6.3.1 根目录 6.3.2 arch目录 6.3.3 arch/arm/boot目录 6.3.4 arch/arm/def-configs目录 6.3.5 arch/arm/kernel目录 6.3.6 arch/arm/mm目录 6.3.7 arch/arm/mach-s3c2410目录 6.4 Linux-2.6内核向ARM平台的移植 6.4.1 定义平台和编译器 6.4.2 arch/arm/mach-s3c2410/devs.c 6.4.3 arch/arm/mach-s3c2410/mach-fs2410.c 6.4.4 串口输出 6.5 编译Linux内核 6.5.1 代码成熟等级选项 6.5.2 通用的一些选项 6.5.3 和模块相关的选项 6.5.4 和块相关的选项 6.5.5 和系统类型相关的选项 6.5.6 和总线相关的选项 6.5.7 和内核特性相关的选项 6.5.8 和系统启动相关的选项 6.5.9 和浮点运算相关的选项 6.5.10 用户空间使用的二进制文件格式的选项 6.5.11 和电源管理相关的选项 6.5.12 和网络协议相关的选项 6.5.13 和设备驱动程序相关的选项 6.5.14 和文件系统相关的选项 6.5.15 和程序性能分析相关的选项 6.5.16 和内核调试相关的选项 6.5.17 和安全相关的选项 6.5.18 和加密算法相关的选项 6.5.19 库选项 6.5.20 保存内核配置 第7章 Linux设备驱动程序开发 7.1 设备驱动概述 7.1.1 设备驱动和文件系统的关系 7.1.2 设备类型分类 7.1.3 内核空间和用户空间.. 7.2 设备驱动基础 7.2.1 设备驱动中关键数据结构 7.2.2 字符设备驱动开发 第8章 网络设备驱动程序开发 8.1 网络设备驱动程序简介 8.1.1 device数据结构 8.1.2 sk_buff数据结构 8.1.3 内核的驱动程序接口 8.2 以太网控制器CS8900A 8.2.1 特性 8.2.2 工作原理 8.2.3 电路连接 8.2.4 引脚 8.2.5 操作模式 8.3 网络设备驱动程序实例 8.3.1 初始化函数 8.3.2 打开函数 8.3.3 关闭函数 8.3.4 发送函数 8.3.5 接收函数 8.3.6 中断处理函数 第9章 USB驱动程序开发 9.1 USB驱动程序简介 9.1.1 USB背景知识 9.1.2 Linux内核对USB规范的支持 9.1.3 OHCI简介 9.2 LinuxUSB系统文件结点 9.3 USB主机驱动结构 9.3.1 USB数据传输时序 9.3.2 USB设备连接/断开时序 9.4 主要数据结构及接口函数 9.4.1 数据传输管道 9.4.2 统一的USB数据传输块 9.4.3 USBD数据描述 9.4.4 USBD与HCD驱动程序接口 9.4.5 USBD层的设备管理 9.4.6 设备驱动USBD接口 9.5 USBD文件系统接口 9.5.1 设备驱动程序访问 9.5.2 设备拓扑访问 9.5.3 设备信息访问 9.6 设备驱动与文件系统接口 9.7 USB HUB驱动程序 9.7.1 HUB驱动初始化 9.7.2 HUB Probe相关函数 9.8 OHCI HCD实现 9.8.1 OHCI驱动初始化 9.8.2 与USBD连接 9.8.3 OHCI根HUB 9.9 扫描仪设备驱动程序 9.9.1 USBD接口 9.9.2 文件系统接口 9.10 USB主机驱动在S3C2410X平台的实现 9.10.1 USB主机控制器简介 9.10.2 驱动程序的移植 第10章 图形用户接口 10.1 嵌入式系统中的GUI简介 10.1.1 MicroWindows 10.1.2 MiniGUI 10.1.3 Qt/Embedded 10.2 MiniGUI编程 10.2.1 MiniGUI移植 10.2.2 MiniGUI编程 10.3 初识Qt/Embedded 10.3.1 Qt介绍 10.3.2 系统要求 10.3.3 Qt的架构 10.4 Qt/Embedded嵌入式图形开发基础 10.4.1 建立Qt/Embedded 开发环境 10.4.2 认识Qt/Embedded开发环境 10.4.3 窗体 10.4.4 对话框 10.4.5 外形与感觉 10.4.6 国际化 10.5 Qt/Embedded实战演练 10.5.1 安装Qt/Embedded工具开发包 10.5.2 交叉编译Qt/Embedded库 10.5.3 Hello,World 10.5.4 发布Qt/Embedded程序到目标板 10.5.5 添加一个Qt/Embedded应用到QPE 第11章 Java虚拟机的移植 11.1 Java虚拟机概述 11.1.1 Java虚拟机的概念 11.1.2 J2ME 11.1.3 KVM 11.2 Java虚拟机的移植 11.2.1 获得源码 11.2.2 编译环境的建立 11.2.3 JDK的安装 11.2.4 KVM的移植及编译 11.2.5 KVM的测试 11.3 其他可选的虚拟机 11.4 性能优化
Linux下,编写USB设备驱动程序的过程可以分为以下几个步骤: 1. 确定驱动程序的框架:一般情况下,驱动程序需要使用Linux内核提供的USB子系统进行通信。可以选择使用USB核心驱动框架,如usbcore或者usbfs,这取决于驱动程序的需求和功能。 2. 编写设备识别和初始化函数:USB设备Linux系统中由Vendor ID(厂商ID)和Product ID(产品ID)唯一标识。在驱动程序中,需要编写设备识别函数,通过与系统中的已知设备进行匹配,确定设备的类型和特性。然后,初始化设备的状态和资源。 3. 实现设备的控制和数据传输:USB设备通常具有多个接口和端点,每个端点的功能和方向都不同。驱动程序需要实现设备的控制和数据传输功能,包括读取设备的描述符、配置设备、发送和接收数据等。 4. 处理中断和事件:某些USB设备可能会产生中断或者其他事件,这需要驱动程序对这些事件进行处理。可以注册中断处理程序或事件处理程序,并根据设备的需求进行响应。 5. 编写设备文件操作函数:Linux系统将USB设备作为设备文件进行管理。驱动程序需要编写打开、关闭、读取、写入等操作函数,将应用程序的请求传递给对应的设备。 6. 编译和加载驱动程序:完成驱动程序的编写后,需要将其编译成模块或者内核。通过insmod命令将驱动程序模块加载到内核中,然后可以通过udev等工具进行设备的管理和配置。 编写USB设备驱动程序需要对Linux内核有一定的了解,了解USB协议和接口规范,掌握Linux内核编程的基本知识。同时,还需要通过查阅文档和示例代码来学习和理解USB设备驱动程序的编写方法和技巧。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值