×
单片机 > 单片机程序设计 > 详情

[ARM笔记]设备驱动概述

发布时间:2020-05-23 发布时间:
|

《嵌入式Linux初级实验s3c2410》

 

       设备驱动开发=硬件控制+内核API+内核驱动框架

 

1. 设备驱动和操作系统

1.1 无操作系统时的设备驱动

       在没有操作系统的情况下,设备驱动的接口直接提交给应用软件工程师,应用软件没有跨越任何层次就可以直接访问设备驱动的接口。驱动包含的接口函数也与硬件的功能直接吻合,没有任何附加功能。

 

1.2 有操作系统时的设备驱动

       没有操作系统时,设备驱动直接被应用程序调用,不与任何操作系统关联。当系统中包含操作系统后,设备驱动会变得怎样?

       首先,无操作系统时设备驱动的硬件操作仍然是必不可少的,没有这一部分,设备驱动不可能与硬件打交道,也就是说在无操作系统时驱动所做的工作,在有操作系统时也是要做的。

       其次,我们还需要将设备驱动融入操作系统内核。应用程序是通过调用操作系统的API来实现对硬件的操作的,所以设备驱动需要融入到内核中。为了实现这种融合,必须在所有的设备驱动中设计面向操作系统内核的接口,这样的接口由操作系统规定,对一类设备而言结构一致,独立于具体的设备。不同的操作系统中定义的设备驱动架构是不一样的,要将设备驱动融入系统内核中,就需要按照操作系统给出的独立于设备的接口架构设计,如此这般,应用程序就可以使用统一的系统调用接口来访问各种设备。其中内核的API 包括并发/同步控制、阻塞/唤醒、中断底半部调度、内存和I/O 访问等。

       由此可见,当系统中存在操作系统时,设备驱动变成了链接硬件和内核的桥梁,操作系统的存在使得单一的“驱动硬件设备工作”变为操作系统与硬件交互的模块,它对外呈现为操作系统API,不再给应用软件工程师直接提供接口。因此,驱动工程师不仅需要牢固的硬件基础,如硬件的工作原理、寄存器设置等,还需要对驱动中所涉及的内核知识有良好的掌握,包括内核支持的API、内核驱动架构等,才能设计开发出好的设备驱动程序。也就是说设备驱动从无操作系统时的应用程序和硬件设备之间的桥梁转变成操作系统和硬件设备之间的沟通纽带。

 

2. Linux设备驱动

 

2.1 Linux设备的分类及特点

       驱动针对的对象是存储器和外设(包括CPU内部集成的存储器和外设),而不是针对CPU核。Linux系统中将存储器和外设分为3个基础大类:字符设备、块设备和网络设备。

 

2.1.1 字符设备

       概括的讲,字符设备指那些必须以串行顺序依次进行访问的设备,如触摸屏、磁带驱动器、鼠标等。字符设备是一种可以当作一个字节流来存取的设备,字符驱动就负责实现这种行为。这样的驱动常常至少实现open,close,read,和write系统调用。字符驱动很好地展

现了流的抽象,它通过文件系统结点来存取,也就是说,字符设备被当作普通文件来访问。字符设备和普通文件之间唯一的不同就是:你可以在普通文件中移来移去,但是大部分字符设备仅仅是数据通道,你只能顺序存取。然而,也存在看起来象数据区的字符设备,你可以在里面移来移去的访问数据。例如,frame grabber经常这样,应用程序可以使用mmap或者lseek 存取整个要求的图像。

 

2.1.2 块设备

       块设备是可以用任意顺序访问,以块为单位进行操作,如硬盘、软驱等。一般来说,块设备和字符设备并没有明显的界限。如同字符设备,块设备也是通过文件系统结点进行存取。一个块设备是可以驻有一个文件系统的。Linux系统中允许应用程序读写一个块设备象一个字符设备一样,它允许一次传送任意数目的字节,当然也包括一个字节。块和字符设备的区别仅仅在内核在内部管理数据的方式上,如字符设备不经过系统的快速缓冲,而块设备经过系统的快速缓冲,并且在内核/驱动的软件接口上不同。虽然它们之间的区别对用户是透明的,它们都使用文件系统的操作接口open()、close()、read()、write()等函数进行访问,但是它们的驱动设计存在很大的差异。

 

2.1.3 网络设备

       网络设备是面向数据包的接收和发送而设计的,它与字符设备、块设备不同,并不对应于文件系统中的节点。内核与网络设备的通信和内核与字符设备、块设备的通信方式可以说是完全不同的。任何网络事务都通过一个接口来进行,就是说,一个能够与其他主机交换数据的设备。通常,一个接口是一个硬件设备,但是它也可能是一个纯粹的软件设备,比如环回接口,因此网络设备也可以称为网络接口。在内核网络子系统的驱动下,网络设备负责发送和接收数据报文。网络驱动对单个连接一无所知,它只处理报文。

       既然网络设备不是一个面向流的设备,一个网络接口就不象字符设备、块设备那么容易映射到文件系统的一个结点上。Linux提供的对网络设备的存取方式仍然是通过给它们分配一个名字,但是这个名字在文件系统中没有对应的入口,其并不用read和write等函数,而是通过内核调用和报文传递相关的函数来实现。

       近年来,某些设备驱动类别也已经添加到Linux内核中,如FireWire驱动。与内核处理USB和SCSI驱动相同的方式,内核开发者集合了类别范围内的特性,并把它们输出给驱动实现者,以避免重复工作,因此简化和加强了编写类似驱动的过程。

       除了上面对设备的分类的方式之外,还有其他的划分方式,与上面的设备类型是正交的。通常,某些类型的驱动与给定类型设备其他层的内核支持函数一起工作。例如,你可以说USB模块,串口模块,SCSI模块等等。每个USB设备由一个USB模块驱动,与USB子系统一起工作,但是设备自身在系统中表现为一个字符设备(比如一个USB串口),一个块设备(一个USB内存读卡器),或者一个网络设备(一个USB以太网接口)。

 

2.2 不同设备的驱动设计概述

       上述的三类设备,除了网络设备外,字符设备与块设备都被映射到Linux文件系统的文件和目录,通过文件系统的系统调用接口open()、write()、read()、close()等函数访问。块设备比字符设备复杂,在它上面会有一个磁盘/Flash文件系统,该文件系统对存储介质上的文件和目录进行规范化的组织。

 

2.2.1 字符设备驱动

       Linux字符设备驱动的核心是file_operations结构体,驱动的主体是实现其中的read()、write()、ioctl()、open()、release()等方法,这些方法将完成系统需要对设备进行的操作功能。

       其结构形式如下所示:

struct file_operations xxx_fops =

{

       .owner = THIS_MODULE,

       .read = xxx_read,

       .write = xxx_write,

       .ioctl = xxx_ioctl,

       ...

}

       open()方法:该方法提供给驱动程序初始化设备的能力,从而为以后的设备操作做好准备,主要完成如下工作:检查设备特定的错误(例如设备没准备好,或者类似的硬件错误);如果它第一次打开,初始化设备;如果需要,更新file_operations指针;分配并填充要放进filp->private_data的任何数据结构等。此外open操作一般还会递增使用计数,用以防止文件关闭前模块被卸载出内核。

       release()方法:与open方法相反,它主要是释放由open分配的filp->private_data中的所有内容;在最后一次关闭操作时关闭设备;使用计数减1等操作。

       read()和write()方法:read方法完成将数据从内核拷贝到应用程序空间,write方法相反,将数据从应用程序空间拷贝到内核。

       ioctl()方法:ioctl 方法主要用于对设备进行读写之外的其他控制,比如配置设备、进入或退出某种操作模式,这些操作一般都无法通过 read/write文件操作来完成。

 

2.2.2 块设备驱动

       Linux块设备驱动并不直接实现file_operations成员函数,其主体变成处理实现block_device_operations成员函数以及处理上层下达的I/O请求。block_device_operations结构体中包含了ioctl()、open()、release()方法,因为字符设备和块设备的存取方法不同,其I/O处理请求可以看作是块设备中的read()和write()方法。块设备调用函数block_read( )和block_write( )来进行数据读写,这两个函数将向设备请求表中增加读写请求,以便Linux内核可以对请求顺序进行优化。由于是对内存缓冲区而不是直接对设备进行操作的,因此很大程度上加快了读写速度。如果内存缓冲区中没有所要读入的数据,或者需要执行写操作将数据写入设备,那么就要执行真正的数据传输。

       处理I/O请求的典型流程如下所示:

static void xxx_request(request_queue_t *q)

{

       struct request *req;

       while ((req = elv_next_request(q)) != NULL)

       {

              struct xxx_dev *dev = req->rq_disk->private_data;

              if (!blk_fs_request(req)) //不是文件系统请求

                     {

                            printk(KERN_NOTICE "Skip non-fs request ");

                            end_request(req, 0);//通知请求处理失败

                     }

              continue;

       }

       xxx_transfer(dev, req->sector, req->current_nr_sectors, req->buffer,rq_data_dir(req)); //处理这个请求

       end_request(req, 1); //通知成功完成这个请求

}

 

2.2.3 网络设备驱动

       网络设备和字符设备、块设备不同,Linux系统对其有专门的处理函数和机制。所有的Linux网络驱动程序都遵循通用的接口,设计时采用的是面向对象的方法。把所有网络设备都抽象为一个接口对象。由数据结构struct device来表示网络设备在内核中的运行情况,即网络设备接口,该结构提供了对所有网络设备的操作集合。它由以dev_base为头指针的设备链表来集中管理所有网络设备。该设备链表中的每个元素代表一个网络设备接口。数据结构device中有很多提供给系统访问和协议层调用的设备方法,包括提供给设备初始化和向系统注册用的init函数、打开和关闭网络设备的open和stop函数、处理数据包发送的函数hard_start_xmit,以及中断处理函数等。一般来讲,一个网络设备最基本的方法有初始化(initialize)、发送和接收。初始化,当把驱动程序载入系统的时候会调用此程序,主要完成检测设备、配置和初始化硬件、初始化device结构中的变量等。设备驱动各函数是网络设备接口层net_device数据结构的具体成员,它通过hard_start_xmit()函数启动发送操作,并通过网络设备上的中断触发接收操作。

       Linux下编写网络设备驱动的主体工作是完成net_device结构体的填充以及成员函数的实现,底层最核心的工作是:发送数据包和接收数据包,接收数据包是由中断触发的。发送数据包函数的典型结构如下——网络设备驱动发送数据包的典型结构。

int xxx_tx(struct sk_buff *skb, struct net_device *dev)

{

       int len;

       char *data, shortpkt[ETH_ZLEN];

       /* 获得有效数据指针和长度 */

       data = skb->data;

       len = skb->len;

       if (len < ETH_ZLEN)

       {

       /* 如果帧长小于以太帧最小长度,补0 */

              memset(shortpkt, 0, ETH_ZLEN);

              memcpy(shortpkt, skb->data, skb->len);

              len = ETH_ZLEN;

              data = shortpkt;

       }

       dev->trans_start = jiffies; /* 记录发送时间戳 */

       /* 设置硬件寄存器让硬件把数据包发送出去 */

       xxx_hw_tx(data, len, dev);

       //...

}

       接收数据包的典型结构如下——网络设备驱动接受数据包的典型结构。

static void xxx_interrupt(int irq, void *dev_id, struct pt_regs *regs)

{

       //...

       switch (status &ISQ_EVENT_MASK)

       {

              case ISQ_RECEIVER_EVENT:

              /* 获取数据包 */

                     xxx_rx(dev);

                     break;

              /* 其他类型的中断 */

       }

}

 

static void xxx_rx(struct xxx_device *dev)

{

       //...

       length = get_rev_len (...);

       /* 分配新的套接字缓冲区 */

       skb = dev_alloc_skb(length + 2);

       skb_reserve(skb, 2); /* 对齐 */

       skb->dev = dev;

       /* 读取硬件上接收到的数据 */

       insw(ioaddr + RX_FRAME_PORT, skb_put(skb, length), length >> 1);

       if (length &1)

              skb->data[length - 1] = inw(ioaddr + RX_FRAME_PORT);

       /* 获取上层协议类型 */

       skb->protocol = eth_type_trans(skb, dev);

       /*把数据包交给上层 */

       netif_rx(skb);

       /* 记录接收时间戳 */

       dev->last_rx = jiffies;

       //...

}

 

2.3 Linux设备文件的创建

       Linux是一种类Unix系统,Unix的一个基本特点是“一切皆为文件”,它抽象了设备的处理,将所有的硬件设备都像普通文件一样看待,也就是说硬件可以跟普通文件一样打开、关闭和读写。系统中的设备都用一个特殊文件代表,叫做设备文件。在 Linux2.4以后的内核版本中引入了设备文件系统(devfs),所有的设备文件作为一个可以挂装的文件系统,这样就可以被文件系统进行统一管理,从而设备文件就可以挂装到任何需要的地方。

       在前面也讲过,字符设备和块设备都可以通过文件节点来存取,而与字符设备和块设备不同,网络设备的访问是通过Socket而不是设备节点,在系统里根本就不存在网络设备节点,所以在此我们仅讨论块设备和字符设备。

       那么如何在内核中创建设备文件的挂载节点呢?简单的说,设备文件是由系统调用创建的,在命令行中,mknod命令会调用同名的程序来创建文件节点。rename和unlink系统调用可以用于移动和删除节点,相应的命令是mv和rm。在使用cp命令时加上-R或-a参数,可以创建一个与原设备节点具有同样属性的节点。mknod命令,该命令形式如下:

#mknod [OPTION] NAME TYPE [MAJOR MINOR]

       说明:option选项设置,最常用的就是-m,基本上可以不用;name自定义设备名称;type设备类型,有b和c还有p;MAJOR主设备号;MINOR次设备号。

       mknod命令建立一个目录项和一个特殊文件的对应索引节点。第一个参数Name项是设备的名称,选择一个描述性的设备名称。

       mknod命令有两种形式,它们有不同的标志。mknod命令的第一种形式只能由root用户或系统组成员执行。在第一种形式中,使用了b或c标志。b标志表示这个特殊文件是面向块的设备(磁盘、软盘或磁带)。c标志表示这个特殊文件是面向字符的设备(其他设备)。在 mknod 命令的第二种形式中,使用了p标志来创建FIFO(已命名的管道)。

       因此,标志集合总共有三种选择,如下:

*      b 表示特殊文件是面向块的设备(磁盘、软盘或磁带)。

*      c 表示特殊文件是面向字符的设备(其他设备)。

*      p 创建 FIFO(已命名的管道)。

       在介绍创建设备文件时,主设备号和从设备号是不可或缺的。传统方式中的设备管理中,除了设备类型外,内核还需要一对主次设备号的参数,才能唯一标识一个设备。主设备号相同的设备使用相同的驱动程序,次设备号用于区分具体设备的实例。比如PC机中的IDE设备,一般主设备号使用3,WINDOWS下进行的分区,一般将主分区的次设备号为1,扩展分区的次设备号为2、3、4,逻辑分区使用5、6…。

       第一种形式的最后两个参数便是指定主设备号和次设备号,它帮助操作系统查找设备驱动程序代码,和指定具体的次设备。一个设备的主设备号和次设备号由该设备的配置方法分配。主设备号是由/usr/src/linux/include/linux/major.h定义的,如下定义了一个DOC设备:

#define IGEL_FLASH_MAJOR 62

       如命令mknod doc b 62 0

       其中的doc为定义的名字,b指块设备,0指的是整个DOC。如果把0换为1,则1指的是DOC的第一个分区。2是第2个,依次类推。

mknod console c 5 1

       console是设备的名字;c指字符设备,还可选b(块设备);5是该设备在major.h中定义的标记,主设备号/dev/devices里面记录现有的设备,创建设备文件时,找个系统中还没有用过的就可以了;1是指第一个子设备。当你要给两个同样的设备加载驱动的时候就要用到这些区别了。

 

3. Linux驱动程序的加载和卸载

       Linux内核中采用可加载的模块化设计,一般情况下编译的Linux内核是支持可插入式模块的,也就是将最基本的核心代码编译在内核中,其它的代码可以选择是在内核中,或者编译为内核的模块文件。如果需要某种功能,比如需要访问一个NTFS分区,就加载相应的NTFS模块。这种设计可以使内核文件不至于太大,但是又可以支持很多的功能,必要时动态地加载。这是一种跟微内核设计不太一样,但却是切实可行的内核设计方案。

      

3.1 Linux驱动的加载方式

       由于Linux系统内核有如上的特点,所以设备驱动程序也秉承了这种特性。常见的驱动程序就是作为内核模块动态加载的,比如声卡驱动和网卡驱动等。而Linux最基础的驱动,如CPU、PCI总线、TCP/IP协议、VFS等驱动程序则编译在内核文件中。因此,Linux驱动的加载可分为静态加载和动态加载两种不同的方式。

*      静态加载:系统启动时自动加载驱动到内核,自动的注册设备并创建设备接点,也就是说把驱动程序直接编译到内核,系统启动后应用程序可以直接运行、调用。静态加载的缺点是调试起来比较麻烦,每次修改一个地方都要重新编译下载内核,效率较低。

*      动态加载:即模块加载,系统启动时不会进行加载驱动程序,需要人为手动加载,就是说系统启动后我们的应用程序不能直接应用驱动,而是我们必须手动的用insmod命令去加载模块,然后才能使用相应的设备和应用,在不需要的时候用rmmod命令来卸载。

       其中动态加载我们又可以分为三种去研究:

       加载驱动后,我们自己去创建主设备号,从设备号,利用cat /proc/devices 去查看主设备号是否重复,然后根据应用程序中使用的设备名称用mknod命令去创建设备文件接点。

       加载驱动后,驱动程序会利用register_chrdev()函数自动产生主设备号去在内核中注册设备,我们利用cat /proc/devices命令和驱动程序中注册的设备名去查询主设备号和从设备号后,在根据应用程序使用的设备名,去利用mknod去创建。(利用驱动中注册的设备名是查询自动生成的主设备号,驱动中的设备名称不一定要和创建的设备接点名相同,他们之间可以用主设备号去关联,而应用程序的设备名称则必须和创建的设备接点名相同)。

       加载驱动后,驱动程序利用devfs系统,这个系统可以自动的产生主设备号,然后自动的创建设备接点。我们只要加载驱动后,直接运行应用程序就行了。

       一般嵌入式驱动开发者会先用动态加载的方式来调试,调试完毕后再编译到内核里。下面我们将向读者介绍下如何使用insmod动态加载模块。

 

3.2 Linux驱动加载和卸载

       当我们编写好需要加载的模块、创建了其在内核的设备挂载节点之后,下一步要进行的操作就是将该设备模块加载到内核,也就是把编译后的驱动程序的.ko文件加载到内核。这个工作将由insmod完成。这个程序将加载模块的代码段和数据段到内核,接着,执行一个类似ld的函数,它连接模块中任何未解决的符号连接到内核的符号表上。

       insmod接收许多命令行选项,它能够在连接到当前内核之前,为模块中的参数赋值,加载时配置比编译时配置给了用户更多的灵活性,感兴趣的读者可以查阅相关的资料。一般常用的命令方式为:

#insmod /路径 模块编译后生成文件.ko

       模块可以用rmmod工具从内核去除。需要注意的是,如果内核认为模块还在用,或者内核被配置成不允许模块去除,模块去除会失败。除了上述两种命令,还有一些相关的命令,在模块加载时可以用到。如下所示:

lsmod:列出当前系统中加载的模块,其中显示信息中分为三列,依次是:模块名、模块大小、模块使用的数量。

modprobe:使用modprobe命令,可以智能插入模块,它可以根据模块间的依存关系,以及/etc/modules.conf文件中的内容智能插入模块。

insmod:也是插入模块的命令,但是它不会自动解决依存关系。

modinfo:用来查看模块信息。

 

4. 学习Linux驱动程序的基础及方法

       Linux设备驱动的学习是一项浩大的工程,读者需要一定的基础。在前面,我们专门讲到过驱动程序是连接硬件设备和操作系统的桥梁。

       因此,驱动的开发不仅要有良好的硬件基础,懂得SRAM、Flash、SDRAM、磁盘的读写方式,UART、I2C、USB等设备的接口,轮询、中断、DMA的原理,PCI总线的工作方式以及CPU的内存管理单元(MMU)等硬件处理的方式;还需要对Linux内核有一定的了解,虽然并不要求工程师对内核各个部分有深入的研究,但至少要了解设备驱动与内核的接口,尤其是对于块设备、网络设备、Flash设备、串口设备等复杂设备的驱动框架等。

       另外,在应用中很有可能多个程序访问同一个设备,这也就需要具有良好的多任务并发控制和同步的基础。动手实践永远是学习任何软件开发的最好方法,学习Linux设备驱动也不例外。

       一般来说,编写一个Linux设备驱动程序的大致流程如下:
(1)查看原理图、数据手册,了解设备的操作方法。
(2)在内核中找到相近的驱动程序,以它为模板进行开发,有时候需要从零开始。
(3)实现驱动程序的初始化:比如向内核注册这个驱动程序,这样应用程序传入文件名时,内核才能找到相应的驱动程序。
(4)设计所要实现的操作,比如:open、close、read、write等函数。
(5)实现中断函数(中断并不是每个设备驱动所必需的)。
(6)编译该驱动程序到内核中,或者用insmod命令加载。
(7)测试驱动程序。


关键字:ARM  设备驱动 

『本文转载自网络,版权归原作者所有,如有侵权请联系删除』

热门文章 更多
ARM 汇编的必知必会