怎样写linux下的USB设备驱动程序

怎样写linux下的USB设备驱动程序,第1张

写一个USB的驱动程序最 基本的要做四件事:驱动程序要支持的设备、注册USB驱动程序、探测和断开、提交和控制urb(USB请求块)

驱动程序支持的设备:有一个结构体struct usb_device_id,这个结构体提供了一列不同类型的该驱动程序支持的USB设备,对于一个只控制一个特定的USB设备的驱动程序来说,struct usb_device_id表被定义为:

/* 驱动程序支持的设备列表 */

static struct usb_device_id skel_table [] = {

{ USB_DEVICE(USB_SKEL_VENDOR_ID, USB_SKEL_PRODUCT_ID) },

{ }/* 终止入口 */

}

MODULE_DEVICE_TABLE (usb, skel_table)

对 于PC驱动程序,MODULE_DEVICE_TABLE是必需的,而且usb必需为该宏的第一个值,而USB_SKEL_VENDOR_ID和 USB_SKEL_PRODUCT_ID就是这个特殊设备的制造商和产品的ID了,我们在程序中把定义的值改为我们这款USB的,如:

/* 定义制造商和产品的ID号 */

#define USB_SKEL_VENDOR_ID0x1234

#define USB_SKEL_PRODUCT_ID0x2345

这两个值可以通过命令lsusb,当然你得先把USB设备先插到主机上了。或者查看厂商的USB设备的手册也能得到,在我机器上运行lsusb是这样的结果:

Bus 004 Device 001: ID 0000:0000

Bus 003 Device 002: ID 1234:2345 Abc Corp.

Bus 002 Device 001: ID 0000:0000

Bus 001 Device 001: ID 0000:0000

得到这两个值后把它定义到程序里就可以了。

注册USB驱动程序:所 有的USB驱动程序都必须创建的结构体是struct usb_driver。这个结构体必须由USB驱动程序来填写,包括许多回调函数和变量,它们向USB核心代码描述USB驱动程序。创建一个有效的 struct usb_driver结构体,只须要初始化五个字段就可以了,在框架程序中是这样的:

static struct usb_driver skel_driver = {

.owner =THIS_MODULE,

.name ="skeleton",

.probe =skel_probe,

.disconnect =skel_disconnect,

.id_table =skel_table,

}

探测和断开:当 一个设备被安装而USB核心认为该驱动程序应该处理时,探测函数被调用,探测函数检查传递给它的设备信息,确定驱动程序是否真的适合该设备。当驱动程序因 为某种原因不应该控制设备时,断开函数被调用,它可以做一些清理工作。探测回调函数中,USB驱动程序初始化任何可能用于控制USB设备的局部结构体,它 还把所需的任何设备相关信息保存到一个局部结构体中,

提交和控制urb:当驱动程序有数据要发送到USB设备时(大多数情况是在驱动程序的写函数中),要分配一个urb来把数据传输给设备:

/* 创建一个urb,并且给它分配一个缓存*/

urb = usb_alloc_urb(0, GFP_KERNEL)

if (!urb) {

retval = -ENOMEM

goto error

}

当urb被成功分配后,还要创建一个DMA缓冲区来以高效的方式发送数据到设备,传递给驱动程序的数据要复制到这块缓冲中去:

buf = usb_buffer_alloc(dev->udev, count, GFP_KERNEL, &urb->transfer_dma)

if (!buf) {

retval = -ENOMEM

goto error

}

if (copy_from_user(buf, user_buffer, count)) {

retval = -EFAULT

goto error

}

当数据从用户空间正确复制到局部缓冲区后,urb必须在可以被提交给USB核心之前被正确初始化:

/* 初始化urb */

usb_fill_bulk_urb(urb, dev->udev,

usb_sndbulkpipe(dev->udev, dev->bulk_out_endpointAddr),

buf, count, skel_write_bulk_callback, dev)

urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP

然后urb就可以被提交给USB核心以传输到设备了:

/* 把数据从批量OUT端口发出 */

retval = usb_submit_urb(urb, GFP_KERNEL)

if (retval) {

err("%s - failed submitting write urb, error %d", __FUNCTION__, retval)

goto error

}

当urb被成功传输到USB设备之后,urb回调函数将被USB核心调用,在我们的例子中,我们初始化urb,使它指向skel_write_bulk_callback函数,以下就是该函数:

static void skel_write_bulk_callback(struct urb *urb, struct pt_regs *regs)

{

struct usb_skel *dev

dev = (struct usb_skel *)urb->context

if (urb->status &&

!(urb->status == -ENOENT ||

urb->status == -ECONNRESET ||

urb->status == -ESHUTDOWN)) {

dbg("%s - nonzero write bulk status received: %d",

__FUNCTION__, urb->status)

}

/* 释放已分配的缓冲区 */

usb_buffer_free(urb->dev, urb->transfer_buffer_length,

urb->transfer_buffer, urb->transfer_dma)

}

有时候USB驱动程序只是要发送或者接收一些简单的数据,驱动程序也可以不用urb来进行数据的传输,这是里涉及到两个简单的接口函数:usb_bulk_msg和usb_control_msg ,在这个USB框架程序里读 *** 作就是这样的一个应用:

/* 进行阻塞的批量读以从设备获取数据 */

retval = usb_bulk_msg(dev->udev,

usb_rcvbulkpipe(dev->udev, dev->bulk_in_endpointAddr),

dev->bulk_in_buffer,

min(dev->bulk_in_size, count),

&count, HZ*10)

/*如果读成功,复制到用户空间 */

if (!retval) {

if (copy_to_user(buffer, dev->bulk_in_buffer, count))

retval = -EFAULT

else

retval = count

}

usb_bulk_msg接口函数的定义如下:

int usb_bulk_msg(struct usb_device *usb_dev,unsigned int pipe,

void *data,int len,int *actual_length,int timeout)

其参数为:

struct usb_device *usb_dev:指向批量消息所发送的目标USB设备指针。

unsigned int pipe:批量消息所发送目标USB设备的特定端点,此值是调用usb_sndbulkpipe或者usb_rcvbulkpipe来创建的。

void *data:如果是一个OUT端点,它是指向即将发送到设备的数据的指针。如果是IN端点,它是指向从设备读取的数据应该存放的位置的指针。

int len:data参数所指缓冲区的大小。

int *actual_length:指向保存实际传输字节数的位置的指针,至于是传输到设备还是从设备接收取决于端点的方向。

int timeout:以Jiffies为单位的等待的超时时间,如果该值为0,该函数一直等待消息的结束。

如果该接口函数调用成功,返回值为0,否则返回一个负的错误值。

usb_control_msg接口函数定义如下:

int usb_control_msg(struct usb_device *dev,unsigned int pipe,__u8request,__u8requesttype,__u16 value,__u16 index,void *data,__u16 size,int timeout)

除了允许驱动程序发送和接收USB控制消息之外,usb_control_msg函数的运作和usb_bulk_msg函数类似,其参数和usb_bulk_msg的参数有几个重要区别:

struct usb_device *dev:指向控制消息所发送的目标USB设备的指针。

unsigned int pipe:控制消息所发送的目标USB设备的特定端点,该值是调用usb_sndctrlpipe或usb_rcvctrlpipe来创建的。

__u8 request:控制消息的USB请求值。

__u8 requesttype:控制消息的USB请求类型值。

__u16 value:控制消息的USB消息值。

__u16 index:控制消息的USB消息索引值。

void *data:如果是一个OUT端点,它是指身即将发送到设备的数据的指针。如果是一个IN端点,它是指向从设备读取的数据应该存放的位置的指针。

__u16 size:data参数所指缓冲区的大小。

int timeout:以Jiffies为单位的应该等待的超时时间,如果为0,该函数将一直等待消息结束。

如果该接口函数调用成功,返回传输到设备或者从设备读取的字节数;如果不成功它返回一个负的错误值。

这两个接口函数都不能在一个中断上下文中或者持有自旋锁的情况下调用,同样,该函数也不能被任何其它函数取消,使用时要谨慎。

我们要给未知的USB设备写驱动程序,只需要把这个框架程序稍做修改就可以用了,前面我们已经说过要修改制造商和产品的ID号,把0xfff0这两个值改为未知USB的ID号。

#define USB_SKEL_VENDOR_ID 0xfff0

#define USB_SKEL_PRODUCT_ID 0xfff0

还 有就是在探测函数中把需要探测的接口端点类型写好,在这个框架程序中只探测了批量(USB_ENDPOINT_XFER_BULK)IN和OUT端点,可 以在此处使用掩码(USB_ENDPOINT_XFERTYPE_MASK)让其探测其它的端点类型,驱动程序会对USB设备的每一个接口进行一次探测, 当探测成功后,驱动程序就被绑定到这个接口上。再有就是urb的初始化问题,如果你只写简单的USB驱动,这块不用多加考虑,框架程序里的东西已经够用 了,这里我们简单介绍三个初始化urb的辅助函数:

usb_fill_int_urb :它的函数原型是这样的:

void usb_fill_int_urb(struct urb *urb,struct usb_device *dev,

unsigned int pipe,void *transfer_buff,

int buffer_length,usb_complete_t complete,

void *context,int interval)

这个函数用来正确的初始化即将被发送到USB设备的中断端点的urb。

usb_fill_bulk_urb :它的函数原型是这样的:

void usb_fill_bulk_urb(struct urb *urb,struct usb_device *dev,

unsigned int pipe,void *transfer_buffer,

int buffer_length,usb_complete_t complete)

这个函数是用来正确的初始化批量urb端点的。

usb_fill_control_urb :它的函数原型是这样的:

void usb_fill_control_urb(struct urb *urb,struct usb_device *dev,unsigned int pipe,unsigned char *setup_packet,void *transfer_buffer,int buffer_length,usb_complete_t complete,void *context)

这个函数是用来正确初始化控制urb端点的。

还有一个初始化等时urb的,它现在还没有初始化函数,所以它们在被提交到USB核心前,必须在驱动程序中手工地进行初始化,可以参考内核源代码树下的/usr/src/~/drivers/usb/media下的konicawc.c文件。

《LINUX设备驱动程序》

USB骨架程序(usb-skeleton),是USB驱动程序的基础,通过对它源码的学习和理解,可以使我们迅速地了解USB驱动架构,迅速地开发我们自己的USB硬件的驱动。

前言

在上篇《Linux下的硬件驱动--USB设备(上)(驱动配制部分)》中,我们知道了在Linux下如何去使用一些最常见的USB设备。但对于做系统设计的程序员来说,这是远远不够的,我们还需要具有驱动程序的阅读、修改和开发能力。在此下篇中,就是要通过简单的USB驱动的例子,随您一起进入 USB驱动开发的世界。

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函数指针,这样就可以与用户空间实现方便地交互。

在学习之前一直对驱动开发非常的陌生,感觉有点神秘。不知道驱动开发和普通的程序开发究竟有什么不同;它的基本框架又是什么样的;他的开发环境有什么特殊的地方;以及怎么写编写一个简单的字符设备驱动前编译加载,下面我就对这些问题一个一个的介绍。

一、驱动的基本框架

1.  那么究竟什么是驱动程序,它有什么用呢:

l     驱动是硬件设备与应用程序之间的一个中间软件层

l 它使得某个特定硬件能够响应一个定义良好的内部编程接口,同时完全隐蔽了设备的工作细节

l     用户通过一组与具体设备无关的标准化的调用来完成相应的 *** 作

l 驱动程序的任务就是把这些标准化的系统调用映射到具体设备对于实际硬件的特定 *** 作上

l     驱动程序是内核的一部分,可以使用中断、DMA等 *** 作

l     驱动程序在用户态和内核态之间传递数据

2.  Linux驱动的基本框架

3.  Linux下设备驱动程序的一般可以分为以下三类

1)        字符设备

a)         所有能够象字节流一样访问的设备都通过字符设备来实现

b)        它们被映射为文件系统中的节点,通常在/dev/目录下面

c)        一般要包含open read write close等系统调用的实现

2)        块设备

d)        通常是指诸如磁盘、内存、Flash等可以容纳文件系统的存储设备。

e)         块设备也是通过文件系统来访问,与字符设备的区别是:内核管理数据的方式不同

f)         它允许象字符设备一样以字节流的方式来访问,也可一次传递任意多的字节。

3)        网络接口设备

g)        通常它指的是硬件设备,但有时也可能是一个软件设备(如回环接口loopback),它们由内核中网络子系统驱动,负责发送和接收数据包。

h)        它们的数据传送往往不是面向流的,因此很难将它们映射到一个文件系统的节点上。

二、怎么搭建一个驱动的开发环境

因为驱动是要编译进内核,在启动内核时就会驱动此硬件设备;或者编译生成一个.o文件, 当应用程序需要时再动态加载进内核空间运行。因此编译任何一个驱动程序都要链接到内核的源码树。所以搭建环境的第一步当然是建内核源码树

1.       怎么建内核源码树

a) 首先看你的系统有没有源码树,在你的/lib/ modules目录下会有内核信息,比如我当前的系统里有两个版本:

#ls /lib/ modules

2.6.15-rc7  2.6.21-1.3194.fc7

查看其源码位置:

## ll /lib/modules/2.6.15-rc7/build

lrwxrwxrwx 1 root root 27 2008-04-28 19:19 /lib/modules/2.6.15-rc7/build ->/root/xkli/linux-2.6.15-rc7

发现build是一个链接文件,其所对应的目录就是源码树的目录。但现在这里目标目录已经是无效的了。所以得自己重新下载

b)下载并编译源码树

有很多网站上可以下载,但官方网址是:

http://www.kernel.org/pub/linux/kernel/v2.6/

下载完后当然就是解压编译了

# tar –xzvf linux-2.6.16.54.tar.gz

#cd linux-2.6.16.54

## make menuconfig (配置内核各选项,如果没有配置就无法下一步编译,这里可以不要改任何东西)

#make

如果编译没有出错。那么恭喜你。你的开发环境已经搭建好了

三、了解驱动的基本知识

1.         设备号

1)        什么是设备号呢?我们进系统根据现有的设备来讲解就清楚了:

#ls -l /dev/

crwxrwxrwx 1 root root     1,   3 2009-05-11 16:36 null

crw------- 1 root root     4,   0 2009-05-11 16:35 systty

crw-rw-rw- 1 root tty      5,   0 2009-05-11 16:36 tty

crw-rw---- 1 root tty      4,   0 2009-05-11 16:35 tty0

在日期前面的两个数(如第一列就是1,3)就是表示的设备号,第一个是主设备号,第二个是从设备号

2)        设备号有什么用呢?

l 传统上, 主编号标识设备相连的驱动. 例如, /dev/null 和 /dev/zero 都由驱动 1 来管理, 而虚拟控制台和串口终端都由驱动 4 管理

l 次编号被内核用来决定引用哪个设备. 依据你的驱动是如何编写的自己区别

3)        设备号结构类型以及申请方式

l   在内核中, dev_t 类型(在 中定义)用来持有设备编号, 对于 2.6.0 内核, dev_t 是 32 位的量, 12 位用作主编号, 20 位用作次编号.

l   能获得一个 dev_t 的主或者次编号方式:

MAJOR(dev_t dev)//主要

MINOR(dev_t dev)//次要

l   但是如果你有主次编号, 需要将其转换为一个 dev_t, 使用: MKDEV(int major, int minor)

4)        怎么在程序中分配和释放设备号

在建立一个字符驱动时需要做的第一件事是获取一个或多个设备编号来使用. 可以达到此功能的函数有两个:

l       一个是你自己事先知道设备号的

register_chrdev_region, 在 中声明:

int register_chrdev_region(dev_t first, unsigned int count, char *name)

first 是你要分配的起始设备编号. first 的次编号部分常常是 0,count 是你请求的连续设备编号的总数. name 是应当连接到这个编号范围的设备的名子 它会出现在 /proc/devices 和 sysfs 中.

l       第二个是动态动态分配设备编号

int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name)

使用这个函数, dev 是一个只输出的参数, 它在函数成功完成时持有你的分配范围的第一个数. fisetminor 应当是请求的第一个要用的次编号 它常常是 0. count 和 name 参数如同给 request_chrdev_region 的一样.

5)        设备编号的释放使用

不管你是采用哪些方式分配的设备号。使用之后肯定是要释放的,其方式如下:

void unregister_chrdev_region(dev_t first, unsigned int count)

6)

2.         驱动程序的二个最重要数据结构

1)         file_operation

倒如字符设备scull的一般定义如下:

struct file_operations scull_fops = {

.owner =  THIS_MODULE, 

 .llseek =  scull_llseek, 

 .read =  scull_read, 

 .write =  scull_write, 

 .ioctl =  scull_ioctl, 

 .open =  scull_open, 

 .release =  scull_release,  

}

file_operation也称为设备驱动程序接口

定义在 , 是一个函数指针的集合. 每个打开文件(内部用一个 file 结构来代表)与它自身的函数集合相关连( 通过包含一个称为 f_op 的成员, 它指向一个 file_operations 结构). 这些 *** 作大部分负责实现系统调用, 因此, 命名为 open, read, 等等

2)         File

定义位于include/fs.h

struct file结构与驱动相关的成员

l         mode_t f_mode      标识文件的读写权限

l         loff_t f_pos           当前读写位置

l         unsigned int_f_flag 文件标志,主要进行阻塞/非阻塞型 *** 作时检查

l         struct file_operation * f_op  文件 *** 作的结构指针

l         void * private_data 驱动程序一般将它指向已经分配的数据

l         struct dentry* f_dentry  文件对应的目录项结构

3.         字符设备注册

1)        内核在内部使用类型 struct cdev 的结构来代表字符设备. 在内核调用你的设备 *** 作前, 必须编写分配并注册一个或几个这些结构. 有 2 种方法来分配和初始化一个这些结构.

l             如果你想在运行时获得一个独立的 cdev 结构,可以这样使用:

struct cdev *my_cdev = cdev_alloc()

my_cdev->ops = &my_fops

l             如果想将 cdev 结构嵌入一个你自己的设备特定的结构 你应当初始化你已经分配的结构, 使用:

void cdev_init(struct cdev *cdev, struct file_operations *fops)

2)        一旦 cdev 结构建立, 最后的步骤是把它告诉内核, 调用:

int cdev_add(struct cdev *dev, dev_t num, unsigned int count)

说明:dev 是 cdev 结构, num 是这个设备响应的第一个设备号, count 是应当关联到设备的设备号的数目. 常常 count 是 1, 但是有多个设备号对应于一个特定的设备的情形.

3)        为从系统去除一个字符设备, 调用:

void cdev_del(struct cdev *dev)

4.         open 和 release


欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/yw/6243559.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2023-03-19
下一篇 2023-03-19

发表评论

登录后才能评论

评论列表(0条)

保存