目录
USB协议简介
USB ,是英文 Universal Serial BUS(通用串行总线)的缩写,而其中文简称为“通串线“,是一个外部总线标准,用于规范电脑与外部设备的连接和通讯。是应用在 PC 领域的接口技术。 USB 接口支持设备的即插即用和热插拔功能。 USB 是在 1994 年底由英特尔、康柏、 IBM、 Microsoft 等多家公司联合提出的。
USB 发展到现在已经有 USB1.0/1.1/2.0/3.0/3.1 等多个版本。目前用的最多的就是USB1.1 和 USB2.0, USB3.0 目前已经开始普及。 STM32F407 自带的 USB 符合USB2.0 规范。标准 USB 共四根线组成,除 VCC/GND 外,另外为 D+和 D-,这两根数据线采用的是差分电压的方式进行数据传输的。在 USB 主机上, D-和 D+都是接了 15K 的电阻到地的,所以在没有设备接入的时候, D+、 D-均是低电平。而在USB 设备中,如果是高速设备,则会在 D+上接一个 1.5K 的电阻到 VCC,而如果是低速设备,则会在 D-上接一个 1.5K 的电阻到 VCC。这样当设备接入主机的时候,主机就可以判断是否有设备接入,并能判断设备是高速设备还是低速设备。
USB 通信的详细过程是很复杂的,本书篇幅有限,不可能在这里详细介绍,有兴趣的朋友可以去看看电脑圈圈的《圈圈教你玩 USB》这本书 。
USB 设备基础概念
-
在终端用户看来, USB 设备为主机提供了多种多样的附加功能,如文件传输,声音播放等,但对 USB 主机来说,它与所有 USB 设备的接口都是一致的。一个 USB设备由 3 个功能模块组成: USB 总线接口、 USB 逻辑设备和功能单元: 这里的 USB 总线接口指的是 USB 设备中的串行接口引擎( SIE);
-
USB 逻辑设备被 USB 系统软件看作是一个端点的集合;
-
功能单元被客户软件看作是一个接口的集合。 SIE、端点和接口都是 USB 设备的组成单元;
为了更好地描述 USB 设备的特征, USB 提出了设备架构的概念。从这个角度来看,可以认为 USB 设备是由一些配置、接口和端点组成,即一个 USB 设备可以含有一个或多个配置,在每个配置中可含有一个或多个接口,在每个接口中可含有若干个端点。其中,配置和接口是对 USB 设备功能的抽象,实际的数据传输由端点来完成。在使用 USB 设备前,必须指明其采用的配置和接口。这个步骤一般是在设备接入主机时设备进行枚举时完成的,这些单元之间的关系如下:
配置通常有一个或多个接口;接口通常有一个或多个端点。 这样的概念太抽象了,可以这样看:有一个设备,如支持视频和音频的一个播放器。那么,对于上面提到的 4 个描述符,对它们设置的时候,它们分别对于哪一个描述符呢?
USB 设备使用各种描述符来说明其设备架构,包括设备描述符、配置描述符、接口描述符、端点描述符和字符串描述符,他们通常被保存在 USB 设备的固件程序中。
一个设备对应一个设备描述符,支持视频的功能对应一个接口描述符,支持音频功能的对应一个接口描述符。 为了支持视频,在下层有多个端口同时工作为提供视频数据传输的支持,所以有多个端点描述符。
USB 设备使用各种描述符来说明其设备架构,包括设备描述符、配置描述符、接口描述符、端点描述符和字符串描述符,他们通常被保存在 USB 设备的固件程序中。
设备描述符
设备代表一个 USB 设备,它由一个或多个配置组成。设备描述符用于说明设备的总体信息,并指明其所含的配置的个数。一个 USB 设备只能有一个设备描述符。
struct usb_device_descriptor
{
_ _u8 bLength; //描述符长度
_ _u8 bDescriptorType; //描述符类型编号
_ _le16 bcdUSB; //USB 版本号
_ _u8 bDeviceClass; //USB 分配的设备类 code
_ _u8 bDeviceSubClass;// USB 分配的子类 code
_ _u8 bDeviceProtocol; //USB 分配的协议 code
_ _u8 bMaxPacketSize0; //endpoint0 最大包大小
_ _le16 idVendor; //厂商编号
_ _le16 idProduct; //产品编号
_ _le16 bcdDevice; //设备出厂编号
_ _u8 iManufacturer; //描述厂商字符串的索引
_ _u8 iProduct; //描述产品字符串的索引
_ _u8 iSerialNumber; //描述设备序列号字符串的索引
_ _u8 bNumConfigurations; //可能的配置数量
} _ _attribute_ _ ((packed));
配置描述符
一个 USB 设备可以包含一个或多个配置,如 USB 设备的低功耗模式和高功耗模式可分别对应一个配置。在使用 USB 设备前,必须为其选择一个合适的配置。配置描述符用于说明 USB 设备中各个配置的特性,如配置所含接口的个数等。 USB 设备的每一个配置都必须有一个配置描述符。
struct usb_config_descriptor
{
_ _u8 bLength; //描述符长度
_ _u8 bDescriptorType; //描述符类型编号
_ _le16 wTotalLength; //配置所返回的所有数据的大小
_ _u8 bNumInterfaces; // 配置所支持的接口数
_ _u8 bConfigurationValue; //Set_Configuration 命令需要的参数值
_ _u8 iConfiguration; //描述该配置的字符串的索引值
_ _u8 bmAttributes; //供电模式的选择
_ _u8 bMaxPower; //设备从总线提取的最大电流
} _ _attribute_ _ ((packed));
接口描述符
一个配置可以包含一个或多个接口,例如对一个光驱来说,当用于文件传输时,使用其大容量存储接口;而当用于播放 CD 时,使用其音频接口。
接口是端点的集合,可以包含一个或多个可替换设置,用户能够在 USB 处于配置状态时改变当前接口所含的个数和特性。接口描述符用于说明设备中各个接口的特性,如接口所属的设备类及其子类等。 USB 设备的每个接口都必须有一个接口描述符。
struct usb_interface_descriptor
{
_ _u8 bLength; //描述符长度
_ _u8 bDescriptorType; //描述符类型
_ _u8 bInterfaceNumber; // 接口的编号
_ _u8 bAlternateSetting; //备用的接口描述符编号
_ _u8 bNumEndpoints; //该接口使用的端点数,不包括端点 0
_ _u8 bInterfaceClass; //接口类型
_ _u8 bInterfaceSubClass; //接口子类型
_ _u8 bInterfaceProtocol; //接口所遵循的协议
_ _u8 iInterface; //描述该接口的字符串索引值
} _ _attribute_ _ ((packed));
端点描述符
端点是 USB 设备中的实际物理单元, USB 数据传输就是在主机和 USB 设备各个端点之间进行的。端点一般由 USB 接口芯片提供,例如 Freescale 公司 MC68HC908JB8和 MC9S12UF32。 USB 设备中的每一个端点都有唯一的端点号,每个端点所支持的数据传输方向一般而言也是确定的:或是输入( IN),或是输出( OUT)。也有些芯片提供的端点的数据方向是可以配置的,例如 MC68HC908JB8 包含有两个用于数据收发的端点:端点 1 和端点 2。其中端点 1 只能用于数据发送,即支持输入( IN)操作;端点 2 既能用于数据发送,也可用于数据接收,即支持输入( IN)和输出( OUT)操作。而 MC9S12UF32 具有 6 个端点。利用设备地址、端点号和传输方向就可以指定一个端点,并与它进行通信。
端点的传输特性还决定了其与主机通信是所采用的传输类型,例如控制端点只能使用控制传输。根据端点的不同用途,可将端点分为两类: 0 号端点和非 0 号端点。0 号端点比较特殊,它有数据输入 IN 和数据输出 OUT 两个物理单元,且只能支持控制传输。所有的 USB 设备都必须含有一个 0 号端点,用作默认控制管道。 USB系统软件就是使用该管道与 USB 逻辑设备进行配置通信的。 0 号端点在 USB 设备上的以后就可以使用,而非 0 号端点必须要在配置以后才可以使用。根据具体应用的需要, USB 设备还可以含有多个除 0 号端点以外的其他端点。对于低速设备,其附加的端点数最多为 2 个;对于全速/高速设备,其附加的端点数最多为 15 个。
struct usb_endpoint_descriptor
{
_ _u8 bLength; //描述符长度
_ _u8 bDescriptorType; //描述符类型
_ _u8 bEndpointAddress; //端点地址: 0~3 位是端点号,第 7 位是方向(0-OUT,1-IN)
_ _u8 bmAttributes; //端点属性: bit[0:1] 的值为 00 表示控制,为 01 表示同步,为 02 表示批量,为 03 表示中断
_ _le16 wMaxPacketSize; 本端点接收或发送的最大信息包的大小
_ _u8 bInterval;//轮询数据传送端点的时间间隔
//对于批量传送的端点以及控制传送的端点,此域忽略
//对于同步传送的端点,此域必须为 1
_ _u8 bRefresh;
_ _u8 bSynchAddress;
} _ _attribute_ _ ((packed));
字符串描述符
在 USB 设备中通常还含有字符串描述符,以说明一些专用信息,如制造商的名称、设备的序列号等。它的内容以 UNICODE 的形式给出,且可以被客户软件所读取。对 USB 设备来说,字符串描述符是可选的。
struct usb_string_descriptor
{
_ _u8 bLength; //描述符长度
_ _u8 bDescriptorType; //描述符类型
_ _le16 wData[1];
} _ _attribute_ _ ((packed));
管道
在 USB 系统结构中,可以认为数据传输时在 USB 主机软件与 USB 设备的各个端点之间直接进行的,它们之间的连接称为管道。管道是在 USB 设备的配置过程中建立的。管道是对 USB 主机与 USB 设备间通信流的抽象,表示 USB 主机的数据缓冲区与 USB 设备的端点之间存在着逻辑数据传输,而实际的数据传输是由 USB 总线接口层来完成的。管道与 USB 设备中的端点一一对应。一个 USB 设备含有多少个端点,其与USB 主机进行通信时就可以使用多少条管道,且端点的类型决定了管道中数据的传输类型,例如中断端点对应中断管道,且该管道只能进行中断传输。不论存在着多少条管道,在各个管道中进行的数据传输都是相互独立的。
USB 端点分类
USB 通讯的最基本形式是通过端点。一个 USB 端点只能向一个方向传输数据(从主机到设备(称为输出端点)或者从设备到主机(称为输入端点))。端点可被看作一个单向的管道。
USB 端点有 4 种不同类型, 分别具有不同的数据传送方式:
-
1、控制 CONTROL
控制端点被用来控制对 USB 设备的不同部分访问. 通常用作配置设备、获取设备信息、发送命令到设备或获取设备状态报告。这些端点通常较小。每个 USB 设备都有一个控制端点称为"端点 0", 被 USB 核心用来在插入时配置设备。 USB 协议保证总有足够的带宽留给控制端点传送数据到设备.
-
2、中断 INTERRUPT
每当 USB 主机向设备请求数据时,中断端点以固定的速率传送小量的数据。此为 USB 键盘和鼠标的主要的数据传送方法。它还用以传送数据到 USB 设备来控制设备。通常不用来传送大量数据。 USB 协议保证总有足够的带宽留给中断端点传送数据到设备.
-
3、批量 BULK
批量端点用以传送大量数据。这些端点通常比中断端点大得多. 它们普遍用于不能有任何数据丢失的情况。 USB 协议不保证传输在特定时间范围内完成。如果总线上没有足够的空间来发送整个 BULK 包,它被分为多个包进行传输。这些端点普遍用于打印机、 USB Mass Storage 和 USB 网络设备上。
-
4、等时 ISOCHRONOUS
等时端点也批量传送大量数据, 但是这个数据不被保证能送达。这些端点用在可以处理数据丢失的设备中,并且更多依赖于保持持续的数据流。如音频和视频设备等等。
控制和批量端点用于异步数据传送,而中断和等时端点是周期性的。这意味着这些端点被设置来在固定的时间连续传送数据, USB 核心为它们保留了相应的带宽。
struct usb_host_endpoint{
struct usb_endpoint_descriptor desc;//端点描述符
struct list_head urb_list;//此端点的 URB 对列,由 USB 核心维护
void *hcpriv;
struct ep_device *ep_dev; /* For sysfs info */
unsigned char*extra;/* Extra descriptors */
int extralen;
int enabled;
};
当调用 USB 设备驱动调用 usb_submit_urb 提交 urb 请求时,将调用 int usb_hcd_link_urb_to_ep(struct usb_hcd *hcd, struct urb *urb)把此 urb 增加到 urb_list的尾巴上。 (hcd: Host Controller Driver,对应数据结构 struct usb_hcd )
USB 总线驱动概念
在 linux 系统中,现在一般存在 3 中 USB 总线。
-
1、UHCI: intel, 低速(1.5Mbps)/全速(12Mbps)
-
2、OHCI: microsoft 低速/全速
-
3、EHCI: 高速(480Mbps)
USB 总线驱动程序就是用来支持 “ 硬件 -- USB 主机控制器” 的。可以认为“USB 总线驱动程序” 就是“USB 主机控制器” 的驱动程序。USB 设备驱动程序就是用来支持 “ 硬件 -- USB 设备” 的。但是为了访问“USB 设备” ,就要用到了“USB 总线驱动程序” 提供的函数,将命令或数据发给“USB 主机控制器” ,由“USB 主机控制器” 产生信号发送给“USB 设备” 。USB 总线驱动程序的作用:
1、识别 USB 设备。
-
分配地址。
-
并告诉 USB 设备(set address)。
-
发出命令获取描述符
2、查找并安装对应的设备驱动程序
3、提供 USB 读写函数
驱动程序讲解
在 linux 内核中基本集成了所有 USB 设备的驱动,一般研发出一个新型的 USB 设备,都会申请专利,会加载到内核之中,所以一般 USB 设备的驱动是需要编写的。为了让大家深刻了解USB 驱动的流程,下面是一个把鼠标当做按键的例子。为了上系统匹配到我们的驱动程序,我们先要在内核中关闭两个配置。(在配置菜单中可以输入‘ /’查找配置的具体位置)
在内核的配置文件中关闭USB_HID和HID_GENERIC
CONFIG_USB_HID
│ -> Device Drivers
│ -> HID support
│ (1) -> USB HID support
CONFIG_HID_GENERIC
│ -> Device Drivers
│ -> HID support
│ (1) -> HID bus support (HID [=y])
备注:关闭HID非常重要
USB 鼠标用作按键: (相当于输入子系统)
左键 -- L 右键 -- S 中键 -- Enter
在“.probe” 函数里做下面 4 件事情:
-
分配一个 input_dev 结构体。
-
设置,使其能产生按键类事件。
-
注册这个 input_dev 结构。
-
硬件相关操作:
之前的“ 按键” 驱动程序,是注册某中断,在按键的中断里面读那些引脚,确定是按下还是松开,确定按键值。触摸屏驱动程序里,是设置 ADC 控制器等。现在 USB 鼠标里,是使用 USB 总线驱动程序提供的读写函数要收发数据。所以,写 USB 设备驱动程序与以前的以前的驱动程序的差别就是硬件的操作不一样了。
分配/设置 usb_driver 结构体
id_table :表示能支持的设备.
probe :表示“USB 总线驱动程序”发现一个新设备后,就会与 driver 比较,若 id_table 表示能支持它,就调用.probe 函数。
disconnect :拔掉 USB 设备时调用这个函数
代码编写:
函数入口
/* 1. 分配/设置usb_driver */
static struct usb_driver usbmouse_as_key_driver = {
.name = "usbmouse_as_key_",
.probe = usbmouse_as_key_probe,
.disconnect = usbmouse_as_key_disconnect,
.id_table = usbmouse_as_key_id_table,
};
static int usbmouse_as_key_init(void)
{
/* 2. 注册 */
usb_register(&usbmouse_as_key_driver);
return 0;
}
probe函数
static int usbmouse_as_key_probe(struct usb_interface *intf, const struct usb_device_id *id)
{
struct usb_device *dev = interface_to_usbdev(intf);
struct usb_host_interface *interface;
struct usb_endpoint_descriptor *endpoint;
int pipe;
interface = intf->cur_altsetting;
endpoint = &interface->endpoint[0].desc;
/* a. 分配一个input_dev */
uk_dev = input_allocate_device();
/* b. 设置 */
/* b.1 能产生哪类事件 */
set_bit(EV_KEY, uk_dev->evbit);
set_bit(EV_REP, uk_dev->evbit);
/* b.2 能产生哪些事件 */
set_bit(KEY_L, uk_dev->keybit);
set_bit(KEY_S, uk_dev->keybit);
set_bit(KEY_ENTER, uk_dev->keybit);
/* c. 注册 */
if (input_register_device(uk_dev))
{
printk("input register device error\n");
return -1;
}
/* d. 硬件相关操作 */
/* 数据传输3要素: 源,目的,长度 */
/* 源: USB设备的某个端点 */
pipe = usb_rcvintpipe(dev, endpoint->bEndpointAddress);
/* 长度: */
len = endpoint->wMaxPacketSize;
/* 目的: */
usb_buf = usb_alloc_coherent(dev, len, GFP_ATOMIC, &usb_buf_phys);
/* 使用"3要素" */
/* 分配usb request block */
uk_urb = usb_alloc_urb(0, GFP_KERNEL);
/* 使用"3要素设置urb" */
usb_fill_int_urb(uk_urb, dev, pipe, usb_buf, len, usbmouse_as_key_irq, NULL, endpoint->bInterval);
uk_urb->transfer_dma = usb_buf_phys;
uk_urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP;
/* 使用URB */
usb_submit_urb(uk_urb, GFP_KERNEL);
return 0;
}
USB设备中断接口
static void usbmouse_as_key_irq(struct urb *urb)
{
static unsigned char pre_val;
#if 1
int i;
static int cnt = 0;
printk("data cnt %d: ", ++cnt);
for (i = 0; i < len; i++)
{
printk("%02x ", usb_buf[i]);
}
printk("\n");
#endif
/* USB鼠标数据含义
* data[0]: bit0-左键, 1-按下, 0-松开
* bit1-右键, 1-按下, 0-松开
* bit2-中键, 1-按下, 0-松开
*
*/
if ((pre_val & (1<<0)) != (usb_buf[0] & (1<<0)))
{
/* 左键发生了变化 */
input_event(uk_dev, EV_KEY, KEY_L, (usb_buf[0] & (1<<0)) ? 1 : 0);
input_sync(uk_dev);
}
if ((pre_val & (1<<1)) != (usb_buf[0] & (1<<1)))
{
/* 右键发生了变化 */
input_event(uk_dev, EV_KEY, KEY_S, (usb_buf[0] & (1<<1)) ? 1 : 0);
input_sync(uk_dev);
}
if ((pre_val & (1<<2)) != (usb_buf[0] & (1<<2)))
{
/* 中键发生了变化 */
input_event(uk_dev, EV_KEY, KEY_ENTER, (usb_buf[0] & (1<<2)) ? 1 : 0);
input_sync(uk_dev);
}
pre_val = usb_buf[0];
/* 重新提交urb */
usb_submit_urb(uk_urb, GFP_KERNEL);
}
USB设备列表
static struct usb_device_id usbmouse_as_key_id_table [] = {
{ USB_INTERFACE_INFO(USB_INTERFACE_CLASS_HID, USB_INTERFACE_SUBCLASS_BOOT,
USB_INTERFACE_PROTOCOL_MOUSE) },
//{USB_DEVICE(0x1234,0x5678)},
{ } /* Terminating entry */
};
测试程序
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/input.h>
int main(int argc, char * const argv[])
{
if(argc<2)
{
printf("Fomat error need with parameter\n");
return -1;
}
int fd = 0;
struct input_event event[4] = {0}; //3!!!,驱动上传了3个事件,第4个用来装空元素
int ret = 0;
fd = open(argv[1],O_RDONLY);
while(1){
ret = read(fd,&event,sizeof(event));
printf("value = %d\n",event[0].value);
sleep(1);
}
return 0;
}
Makefiel文件
obj-m += mouse_drv.o
KERNELDIR:=/file/RK3399Pro/rk3399pro_git_repo/kernel
PWD:=$(shell pwd)
default:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
test:
aarch64-linux-gnu-gcc mouse_test.c -o mouse_test
clean:
rm -rf *.o *.order .*.cmd *.ko *.mod.c *.symvers *.tmp_versions
测试步骤
编译源码
在ubuntu中输入:
make
得到驱动目标文件mouse_drv.ko
输入:
make test
得到测试目标文件:mouse_test
加载驱动
在开发板命令终端输入:
insmod mouse_drv.ko
执行测试程序
在开发板命令终端输入:
chmod 777 mouse_test
./mouse_test /dev/input/by-id/usb-PixArt_USB_Optical_Mouse-event-mouse
实验现象
按下或者滚动中键时打印的数字发生变化,并通过 input 子系统上报给应用层。