简介

本文介绍USB驱动程序编写的流程,分析一个键盘的USB程序,基于linux-2.6.39

USB驱动概要

分层

主机层面的USB驱动的整体架构可以分成4层,自顶到下依次是

1、USB设备驱动:本文主要讲述的内容,利用USB核心提供的编程接口编写具体硬件设备与系统的交互逻辑

2、USB核心:linux内核实现,管理上层的USB设备驱动,并且对下面的USB通信机制做封装,封装的接口提供给上层做驱动编写

3、USB主机控制器驱动:主要负责USB硬件和主机通信机制

3、USB控制器:实际的硬件部分

本文讲述的键盘驱动程序是上面第一层,也就是usb核心之上的驱动程序

USB驱动的逻辑组织

为了方便USB设备驱动的编写,USB核心将USB设备抽象成4个层次,自顶向下依次是

1、设备:代表整个设备,一个设备会有相对应一个文件等

2、配置:一个设备有一个或是多个配置,不同的配置使设备表现出不同的功能组合,在连接期间必须从中选择一个。比如说带话筒的耳机可以可以有3个配置,让硬件有3中工作模式:耳机工作、话筒工作、耳机和话筒同时工作。

3、接口:一个配置包含多个接口。比如一个USB音响可以有音频接口和开关按钮的接口,一个配置可以使所有的接口同时有效,并且被不同的驱动同时连接

4、端点:一个接口可以有多个端点。端点是主机与硬件设备通信的最基本形式,每个端点有属于自己的地址,并且数据的传输是单向的,每一个USB设备在主机看来就是多个端点的集合

在include/linux/usb/ch9.h中,定义了几个结构体:usb_device_descriptor、usb_config_descriptor 、usb_interface_descriptor、usb_endpoint_descriptor。分别表示上面这些含义的描述符。每个设备均会分配一个或是多个。具体见源码

linux中USB驱动整体结构

在linux1中,实现了几个通用的USB设备驱动,划分为如下几个设备类

  • 音频设备类
  • 通信设备类
  • HID人机接口类
  • 显示设备类
  • 海量存储设备类
  • 电源设备类
  • 打印设备类
  • 集线器设备类

所以说一般的统一设备是不需要编写驱动,linux内核中已经包含了。本文的键盘驱动属于HID人机接口类。

linux中提供了USB设备文件系统usbdevfs,同样是内存文件系统,在/etc/fstab中添加一行

none /proc/bus/usb usbfs defaults

或者是

mount -t usbfs none /proc/bus/usb

然后查看proc/bus/usb/devices,可以得到完整的usb信息

USB驱动编写模板

整体骨架

所有 USB 驱动必须创建的主要结构是 struct usb_driver. 这个结构必须被 USB 驱动填充并且包含多个函数回调和变量, 来向 USB 核心代码描述 USB 驱动

位于linux/usb.h中的usb_driver结构定义

struct usb_driver {
const char *name; int (*probe) (struct usb_interface *intf,
const struct usb_device_id *id); void (*disconnect) (struct usb_interface *intf); int (*unlocked_ioctl) (struct usb_interface *intf, unsigned int code,
void *buf); int (*suspend) (struct usb_interface *intf, pm_message_t message);
int (*resume) (struct usb_interface *intf);
int (*reset_resume)(struct usb_interface *intf); int (*pre_reset)(struct usb_interface *intf);
int (*post_reset)(struct usb_interface *intf); const struct usb_device_id *id_table; struct usb_dynids dynids;
struct usbdrv_wrap drvwrap;
unsigned int no_dynamic_id:;
unsigned int supports_autosuspend:;
unsigned int soft_unbind:;
};

其中比较关键的有4个成员,创建一个简单的usb_driver比如:

static struct usb_driver skel_driver = {
.name = "skeleton",
.id_table = skel_table,
.probe = skel_probe,
.disconnect = skel_disconnect,
};

const char *name:指向驱动名子的指针. 它必须在内核 USB 驱动中是唯一的并且通常被设置为和驱动的模块名相同. 它出现在 sysfs 中在 /sys/bus/usb/drivers/ 之下, 当驱动在内核中时.

const struct usb_device_id *id_table:指向 struct usb_device_id 表的指针, 包含这个驱动可接受的所有不同类型 USB 设备的列表. 如果这个变量没被设置, USB 驱动中的探测回调函数不会被调用. 如果你需要你的驱动给系统中每个 USB 设备一直被调用, 创建一个只设置这个 driver_info 成员的入口项:

static struct usb_device_id usb_ids[] = {
  {.driver_info = },
  {}
};

int (*probe) (struct usb_interface *intf, const struct usb_device_id *id):指向 USB 驱动中探测函数的指针. 这个函数(在"探测和去连接的细节"一节中描述)被 USB 核心调用当它认为它有一个这个驱动可处理的 struct usb_interface. 一个指向 USB 核心用来做决定的 struct usb_device_id 的指针也被传递到这个函数. 如果这个 USB 驱动主张传递给它的 struct usb_interface, 它应当正确地初始化设备并且返回 0. 如果驱动不想主张这个设备, 或者发生一个错误, 它应当返回一个负错误值.

void (*disconnect) (struct usb_interface *intf):指向 USB 驱动的去连接函数的指针. 这个函数(在"探测和去连接的细节"一节中描述)被 USB 核心调用, 当 struct usb_interface 已被从系统中清除或者当驱动被从 USB 核心卸载.

为注册 struct usb_driver 到 USB 核心, 一个调用 usb_register_driver 带一个指向 struct usb_driver 的指针. 传统上在 USB 驱动的模块初始化代码做这个:

static int __init usb_skel_init(void)
{
int result;
/* register this driver with the USB subsystem */
result = usb_register(&skel_driver);
if (result)
err("usb_register failed. Error number %d", result);
return result;
}

当 USB 驱动被卸载, struct usb_driver 需要从内核注销. 使用对 usb_deregister_driver 的调用做这个. 当这个调用发生, 任何当前绑定到这个驱动的 USB 接口被去连接, 并且去连接函数为它们而被调用.

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

几个重要的结构体

USB请求块

在include/linux/usb.h中,定义了一个urb结构,是USB驱动中用来描述与USB设备通信所有的核心结构,非常类似于网络设备驱动程序中的sk_buff结构体。一个 USB 设备驱动可能分配许多 urb 给一个端点或者可能重用单个 urb 给多个不同的端点, 根据驱动的需要. 设备中的每个端点都处理一个 urb 队列, 以至于多个 urb 在队列清空之前可被发送到相同的端点,urb 的典型生命循环如下:

  • 被一个 USB 设备驱动创建.
  • 安排给一个特定 USB 设备的特定端点
  • 被 USB 设备驱动提交给 USB 核心,
  • 被USB 核心提交给特定设备的指定USB主机控制器驱动
  • 被 USB 主机控制器处理, 它做一个 USB 传送到设备.
  • 当 urb 完成, USB 主机控制器驱动通知 USB 设备驱动

urb 也可被提交这个 urb 的驱动在任何时间取消, 或者被 USB 核心如果设备被从系统中移出. urb 被动态创建并且包含一个内部引用计数, 使它们在这个 urb 的最后一个用户释放它时被自动释放.

USB请求块的操作

同时在driver/usb/core/urb.c中定义了很多urb的操作函数

usb_alloc_urb用来创建一个urb,这个函数分配sizeof(struct urb) +iso_packets * sizeof(struct usb_iso_packet_descriptor)大小的内存,参数1表示等时数据包的大小,如果不需要等时传输,则应该为0,第二个参数表示内存分配的标志

usb_alloc_urb(int iso_packets, gfp_t mem_flags)

usb_free_urb则用来释放urb

usb_free_urb(struct urb *urb)

usb_device_id结构

usb_device_id结构体用来给驱动probing函数和热插拔提供标志符

该结构体定义在include/linux/mod_devicetable.c中

USB键盘驱动源码分析

probe函数

static int usb_kbd_probe(struct usb_interface *iface,
const struct usb_device_id *id) //usb_interface *iface:由内核自动获取的接口,一个接口对应一种功能, struct usb_device_id *id:设备的标识符
{
printk("Starting probe\n");
struct usb_device *dev = interface_to_usbdev(iface); //获取usb接口结构体中的usb设备结构体,每个USB设备对应一个struct usb_device的变量,由usb core负责申请和赋值
struct usb_host_interface *interface; //连接到的接口的描述
struct usb_endpoint_descriptor *endpoint; //传输数据管道的端点
struct usb_kbd *kbd; //usb设备在用户空间的描述
struct input_dev *input_dev; //表示输入设备
int i, pipe, maxp;
int error = -ENOMEM; interface = iface->cur_altsetting; //将连接到的接口的描述设置为当前的setting
printk("1\n");
if (interface->desc.bNumEndpoints != ) //判断中断IN端点的个数,键盘只有一个端点,如果不为1,则出错,desc是设备描述符
return -ENODEV; endpoint = &interface->endpoint[].desc; //取得键盘中断IN端点的描述符,endpoint[0]表示中断端点
printk("2\n");
if (!usb_endpoint_is_int_in(endpoint)) //查看所获得的端点是否为中断IN端点
return -ENODEV;
printk("3\n");
pipe = usb_rcvintpipe(dev, endpoint->bEndpointAddress); //得到驱动程序的中断OUT端点号,创建管道,用于连接驱动程序缓冲区和设备端口
maxp = usb_maxpacket(dev, pipe, usb_pipeout(pipe)); //得到最大可以传输的数据包(字节) kbd = kzalloc(sizeof(struct usb_kbd), GFP_KERNEL); //为kbd结构体分配内存,GFP_KERNEL是内核内存分配时最常用的标志位,无内存可用时可引起休眠
input_dev = input_allocate_device(); //为输入设备的结构体分配内存,并初始化它
printk("inputdev-1:%s\n",input_dev->name);
printk("4\n");
if (!kbd || !input_dev) //给kbd或input_dev分配内存失败
goto fail1;
printk("5\n");
if (usb_kbd_alloc_mem(dev, kbd)) //分配urb内存空间失败,即创建urb失败
goto fail2;
printk("6\n");
kbd->usbdev = dev; //给kbd的usb设备结构体usbdev赋值
kbd->dev = input_dev; //给kbd的输入设备结构体dev赋值,将所有内容统一用kbd封装,input子系统只能处理input_dev类型的对象
printk("7\n");
if (dev->manufacturer) //将厂商名,产品名赋值给kbd的name成员
{
printk("7.1\n");
strlcpy(kbd->name, dev->manufacturer, sizeof(kbd->name));
}
printk("8\n"); if (dev->product) {
printk("7.2\n");
if (dev->manufacturer) //有厂商名,就在产品名之前加入空格
strlcat(kbd->name, " ", sizeof(kbd->name));
strlcat(kbd->name, dev->product, sizeof(kbd->name));
} printk("9\n"); printk("10\n");
usb_make_path(dev, kbd->phys, sizeof(kbd->phys)); //分配设备的物理路径的地址,设备链接地址,不随设备的拔出而改变
printk("10.1\n");
strlcpy(kbd->phys, "/input0", sizeof(kbd->phys));
printk("10.2\n");
input_dev->name = kbd->name; //给input_dev的name赋值
printk("inputdevname:%s\n",input_dev->name);
printk("kbddevname:%s\n",kbd->dev->name);
input_dev->phys = kbd->phys; //设备链接地址
usb_to_input_id(dev, &input_dev->id); //给输入设备结构体input->的标识符结构赋值,主要设置bustype、vendo、product等
printk("10.3\n");
//input_dev->dev.parent = &iface->dev; //input_set_drvdata(input_dev, kbd);
printk("10.4\n");
input_dev->evbit[] = BIT_MASK(EV_KEY) /*键码事件*/| BIT_MASK(EV_LED) | /*LED事件*/
BIT_MASK(EV_REP)/*自动重覆数值*/; //支持的事件类型
printk("10.5\n");
input_dev->ledbit[] = BIT_MASK(LED_NUML) /*数字灯*/| BIT_MASK(LED_CAPSL) |/*大小写灯*/
BIT_MASK(LED_SCROLLL)/*滚动灯*/ ; //EV_LED事件支持的事件码
printk("10.6\n"); for (i = ; i < ; i++)
set_bit(usb_kbd_keycode[i], input_dev->keybit); // 初始化,每个键盘扫描码都可以出发键盘事件
printk("10.7\n");
clear_bit(, input_dev->keybit); //为0的键盘扫描码不能触发键盘事件
printk("10.8\n");
input_dev->event = usb_kbd_event; //设置input设备的打开、关闭、写入数据时的处理方法
input_dev->open = usb_kbd_open;
input_dev->close = usb_kbd_close;
//初始化中断URB
usb_fill_int_urb(kbd->irq/*初始化kbd->irq这个urb*/, dev/*这个urb要发送到dev这个设备*/, pipe/*这个urb要发送到pipe这个端点*/,
kbd->new/*指向缓冲的指针*/, (maxp > ? : maxp)/*缓冲区长度(不超过8)*/,
usb_kbd_irq/*这个urb完成时调用的处理函数*/, kbd/*指向数据块的指针,被添加到这个urb结构可被完成处理函数获取*/, endpoint->bInterval/*urb应当被调度的间隔*/);
printk("10.9\n");
kbd->irq->transfer_dma = kbd->new_dma; //指定urb需要传输的DMA缓冲区
kbd->irq->transfer_flags |= URB_NO_TRANSFER_DMA_MAP; //本urb有一个DMA缓冲区需要传输,用DMA传输要设的标志 kbd->cr->bRequestType = USB_TYPE_CLASS | USB_RECIP_INTERFACE; //操作的是USB类接口对象
kbd->cr->bRequest = 0x09; //中断请求编号
kbd->cr->wValue = cpu_to_le16(0x200); //大端、小端模式转换
kbd->cr->wIndex = cpu_to_le16(interface->desc.bInterfaceNumber); //接口号
kbd->cr->wLength = cpu_to_le16(); //一次数据传输要传的字节数
//初始化控制URB
printk("10.10\n");
usb_fill_control_urb(kbd->led/*初始化kbd->led这个urb*/, dev/*这个urb要由dev这个设备发出*/, usb_sndctrlpipe(dev, )/*urb发送到的端点*/,
(void *) kbd->cr/*发送的setup packet*/, kbd->leds/*待发送数据的缓冲区*/, /*发送数据长度*/,
usb_kbd_led/*这个urb完成时调用的处理函数*/, kbd/*指向数据块的指针,被添加到这个urb结构可被完成处理函数获取*/);
kbd->led->setup_dma = kbd->cr_dma; //指定urb需要传输的DMA缓冲区
kbd->led->transfer_dma = kbd->leds_dma; //本urb有一个DMA缓冲区需要传输,用DMA传输要设的标志
kbd->led->transfer_flags |= (URB_NO_TRANSFER_DMA_MAP | URB_NO_SETUP_DMA_MAP/*如果使用DMA传输则urb中setup_dma指针所指向的缓冲区是DMA缓冲区而不是setup_packet所指向的缓冲区*/); printk("10.11\n");
printk("kbddevname1:%s\n",kbd->dev->name);
error = input_register_device(kbd->dev); //注册输入设备
printk("11\n");
if (error) //注册失败
goto fail2; usb_set_intfdata(iface, kbd); //设置接口私有数据,向内核注册一个data,这个data的结构可以是任意的,这段程序向内核注册了一个usb_kbd结构,这个data可以在以后用usb_get_intfdata来得到
printk("12\n");
return ; fail2:
usb_kbd_free_mem(dev, kbd); //释放URB内存空间,销毁URB
fail1:
input_free_device(input_dev); //释放input_dev和kbd的空间
kfree(kbd);
return error;
}

disconnect函数

static void usb_kbd_disconnect(struct usb_interface *intf)
{
struct usb_kbd *kbd = usb_get_intfdata (intf); usb_set_intfdata(intf, NULL);
if (kbd) {
usb_kill_urb(kbd->irq);
printk("disconnect1\n");
input_unregister_device(kbd->dev);
printk("disconnect2\n");
usb_kbd_free_mem(interface_to_usbdev(intf), kbd);
kfree(kbd);
}
}

中断处理的函数

static void usb_kbd_irq(struct urb *urb)
{
printk("Starting irq\n");
struct usb_kbd *kbd = urb->context;
int i; switch (urb->status) { //判断URB的状态
case : //URB被成功接收
break;
case -ECONNRESET: //断开连接错误,urb未终止就返回给了回调函数
case -ENOENT: //urb被kill了,生命周期彻底被终止
case -ESHUTDOWN: //USB主控制器驱动程序发生了严重的错误,或者提交完的一瞬间设备被拔出
return; default: //其它错误,均可以重新提交urb
goto resubmit;
}
printk("irq1\n");
for (i = ; i < ; i++)
input_report_key(kbd->dev, usb_kbd_keycode[i + ], (kbd->new[] >> i) & );/*usb_kbd_keycode[224]-usb_kbd_keycode[231],8次的值依次是:29-42-56-125-97-54-100-126,判断这8个键的状态*/
printk("irq2\n");
//若同时只按下2个按键则另一个键在第[2]个字节,若同时有两个按键则第二个在第[3]字节,类推最多可有6个按键同时按下
for (i = ; i < ; i++) { if (kbd->old[i] > && memscan(kbd->new + , kbd->old[i], ) == kbd->new + ) { //判断那些键的状态改变了,即由按下变为了松开
if (usb_kbd_keycode[kbd->old[i]]) //是键盘所用的按键,就报告按键离开
input_report_key(kbd->dev, usb_kbd_keycode[kbd->old[i]], );
else //不是键盘所用的按键
dev_info(&urb->dev->dev,
"Unknown key (scancode %#x) released.\n", kbd->old[i]);
} if (kbd->new[i] > && memscan(kbd->old + , kbd->new[i], ) == kbd->old + ) { //判断那些键的状态改变了,即由松开变为了按下
if (usb_kbd_keycode[kbd->new[i]]) //是键盘所用的按键,就报告按键被按下
input_report_key(kbd->dev, usb_kbd_keycode[kbd->new[i]], );
else //不是键盘所用的按键
dev_info(&urb->dev->dev,
"Unknown key (scancode %#x) released.\n", kbd->new[i]);
}
} input_sync(kbd->dev); //同步设备,告知事件的接收者驱动已经发出了一个完整的input子系统的报告 printk("irq2.1\n");
memcpy(kbd->old, kbd->new, ); //将本次的按键状态拷贝到kbd->old,用作下次urb处理时判断按键状态的改变
printk("irq3\n");
resubmit:
i = usb_submit_urb (urb, GFP_ATOMIC); //重新发送urb请求块
if (i) //发送urb请求块失败
{
printk("irq4\n");
err_hid ("can't resubmit intr, %s-%s/input0, status %d",
kbd->usbdev->bus->bus_name,
kbd->usbdev->devpath, i);
printk("irq5\n");
}
printk("irq6\n");
}

USB键盘驱动分析的更多相关文章

  1. Linux 串口、usb转串口驱动分析(2-2) 【转】

    转自:http://blog.chinaunix.net/xmlrpc.php?r=blog/article&uid=26807463&id=4186852 Linux 串口.usb转 ...

  2. linux驱动基础系列--Linux 串口、usb转串口驱动分析

    前言 主要是想对Linux 串口.usb转串口驱动框架有一个整体的把控,因此会忽略某些细节,同时里面涉及到的一些驱动基础,比如字符设备驱动.平台驱动等也不进行详细说明原理.如果有任何错误地方,请指出, ...

  3. Linux 串口、usb转串口驱动分析(2-1) 【转】

    转自:http://blog.chinaunix.net/xmlrpc.php?r=blog/article&uid=26807463&id=4186851 Linux 串口.usb转 ...

  4. linux设备驱动之USB主机控制器驱动分析 【转】

    转自:http://blog.chinaunix.net/uid-20543183-id-1930831.html   ---------------------------------------- ...

  5. 8.2 USB键盘驱动编写和测试

    目标:根据USB驱动分析和上节的USB鼠标驱动,编写键盘驱动,并测试. 一.原理分析 1. 首先通过打印usb_buf[i]中的8字节数据,看一下按键按下之后会接收到什么. 1)通过按完所有键盘按键打 ...

  6. Linux下 USB设备驱动分析(原创)

    之前做过STM32的usb HID复合设备,闲来看看linux下USB设备驱动是怎么一回事, 参考资料基于韦东山JZ2440开发板,以下,有错误欢迎指出. 1.准备知识 1.1USB相关概念: USB ...

  7. linux下usb转串口驱动分析【转】

    转自:http://blog.csdn.net/txxm520/article/details/8934706 首先说一下linux的风格,个人理解 1. linux大小结构体其实是面向对象的方法,( ...

  8. Hi3559AV100外接UVC/MJPEG相机实时采图设计(一):Linux USB摄像头驱动分析

    下面将给出Hi3559AV100外接UVC/MJPEG相机实时采图设计的整体流程,主要实现是通过V4L2接口将UVC/MJPEG相机采集的数据送入至MPP平台,经过VDEC.VPSS.VO最后通过HD ...

  9. usb键鼠标驱动分析

    一.鼠标 linux下的usb鼠标驱动在/drivers/hid/usbhid/usbmouse.c中实现 1.加载初始化过程 1.1模块入口 module_init(usb_mouse_init); ...

随机推荐

  1. vue子组件改变父组件的值

    1 在父组件的coment绑定事件 <template> <div :class="classObj" class="app-wrapper" ...

  2. easyUI的datagrid控件日期列格式化

    转自:https://blog.csdn.net/idoiknow/article/details/8136093 EasyUI是一套比较轻巧易用的Jquery控件,在使用过程中遇到一个问题,它的列表 ...

  3. LeetCode 88. 合并两个有序数组(Merge Sorted Array)

    题目描述 给定两个有序整数数组 nums1 和 nums2,将 nums2 合并到 nums1 中,使得 num1 成为一个有序数组. 说明: 初始化 nums1 和 nums2 的元素数量分别为 m ...

  4. 模拟LinkedList

    Linkedlist是一个集合容器,具有增删改查基本功能,本例中模拟增删查,对于修改,只需要将原节点处的val更新为新值即可,增加和删除在链表中速度是比较快的,查找,修改慢,因为要从头或者从尾开始遍历 ...

  5. Win10 的微软输入法输入稍快竟然会导致死机

    一周前,新装机器一次,竟然死机两三次,多发生在敲字时,最近逐步排查发现的这个问题,查阅了一下网上方案,果断采用了第三方输入法,至今没再死机过. 不过第三方输入法也不安分,是不是推送点头条新闻过来,和驱 ...

  6. 对opencv读取的图片进行像素调整(1080, 1920) 1.cv2.VideoCapture(构造图片读取) 2.cv2.nameWindow(构建视频显示的窗口) 3.cv2.setWindowProperty(设置图片窗口的像素) 4.video_capture(对图片像素进行设置)

    1. cv2.VideoCapture(0) #构建视频抓捕器 参数说明:0表示需要启动的摄像头,这里也可以写视频的路径 2. cv2.nameWindow(name, cv2.WINDOW_NORM ...

  7. ubuntu下编译linux内核之前需要做哪些准备?

    答: 安装必要的工具(笔者使用的ubuntu代号为bionic) sudo apt-get install -y bison flex

  8. C代码输出日志

    模板代码,在实际开发中可以使用: Android.mk文件增加(放到 include $(CLEAR_VARS)下面) LOCAL_LDLIBS += -llog C代码中增加 #include &l ...

  9. 网站集成Paypal

    国际化Paypal是一个不错的选择,现在很多的app都是H5,所以网站集成即可操作了. 最方便快捷的集成方式,目前Paypal的网站收款需要企业账号,不过它最开始的老版本是可以个人账号收款的.如下是个 ...

  10. subprocess.call 使用

    1.subprocess.call 里面的命令分开写,实例如下: subprocess.call 是不能作为赋值的,需要用到 subprocess.check_output 函数,而且如果要引用赋值就 ...