良许Linux教程网 干货合集 详解嵌入式Linux设备驱动编写原理

详解嵌入式Linux设备驱动编写原理

驱动简介

Linux设备驱动程序是内核的一部分,它具有以下功能:

  • 设备初始化和释放
  • 传送数据从内核到硬件和从硬件读取数据
  • 读取应用程序传输给设备文件的数据并回传应用程序请求的数据
  • 检测和处理设备出现的错误。

系统调用是操作系统内核和应用程序之间的接口,而设备驱动程序是操作系统内核和机器硬件之间的接口。Linux设备驱动程序为应用程序屏蔽了硬件细节。对于应用程序来说,Linux硬件设备只是一个设备文件,它可以像操作普通文件一样对硬件设备进行操作。每个设备驱动程序都具有以下几个特性:

  • 具有一整套与硬件设备通讯的例程,并提供给操作系统一套标准的软件接口;
  • 具有一个可以被操作系统动态调用和移除的自包含组件;
  • 可以控制和管理用户程序和物理设备之间的数据流。

驱动类型

Linux设备分为三种类型:字符设备、块设备和网络设备。

字符设备是指存取时没有缓存,只能顺序访问的设备,一般不能进行任意长度的I/O请求。典型的字符设备包括鼠标、串口、键盘等。字符设备接口支持面向字符的I/O操作,它不经过系统的快速缓存,因此它们负责管理自己的缓冲区结构。下面所描述的I2C接口属于字符设备。

块设备的读/写都有缓存来支持,并且块设备必须能够随机存取,字符设备则没有这个要求。块设备主要是针对磁盘等慢速设备设计的,以免浪费过多的CPU时间来等待。

网络设备在Linux里有专门的处理方式。Linux的网络系统主要基于BSD Unix的Socket机制。在系统和驱动程序之间定义了专门的数据结构进行数据传输。系统支持对发送和接收的数据进行缓存,并提供流量控制机制和多协议支持。

主次设备号

Linux给每个设备都分配一个主设备和次设备号。主设备号一般用来定义这个设备的类型。例如软驱的主设备号是2,并行端口的主设备号是6。次设备号是一个8位的数字,它指定一个特定的设备,例如一台电脑可以有2个软驱,它们都有主设备号2,但是第一个软驱的次设备号为1,而第二个软驱的次设备号为2。
在任何程序使用设备驱动程序之前,设备驱动程序应该向系统进行登记,以便系统在适当的时候调用。向系统增加一个驱动程序即给它一个主设备号,这一过程在驱动程序(模块)的初始化过程中完成,调用如下函数:
int register chrdev(major,*name ,*fops)
参数major是所请求的主设备号,name是设备的名字,它们将在/proc/devices文件中出现,fops是一个指向跳转表的指针,利用这个跳转表完成对设备函数的调用。
从系统中卸载一个模块时,应该释放主设备号。这一操作可以在cleanup_module中调用如下函数完成:
int unregister chrdev (major,*name)
参数是要释放的主设备号和相应的设备名。内核对与这个名字和设备号对应的名字进行比较,如果不同或者主设备号超出了允许的范围或是并未分配给这个设备,内核返回ENINVAL。

文件操作

Linux具有设备的无关性,它把每个设备都抽象为文件系统的一个文件。Linux为每个设备在/dev目录建立一个文件。例如,第一个软驱在文件系统中的文件名为/dev/fd。可以使用以下命令来建立设备文件:
mknod /dev/device_name device_type major_number ninor_number
其中device_name是此设备的文件,device_type是设备的类型(c表示字符设备,b表示块设备)。
Linux系统把设备当作文件一样来访问,访问文件和设备有以下函数:seek、read、write、poll、io-control、memory map、open、flush、release、check、lock等。编写设备驱动程序的主要工作就是编写子函数,并填充file-operations的各个域。并不一定要实现所有的函数,只需要实现设备必须的函数就可以了。
设备驱动使用类型为struct file_operations的一个数据结构来与上面的文件访问函数对应。一般的字符设备驱动程序适用的file-operations结构如下:

struct file_operation dev_fiops{
dev_lseek,
dev_read,
dev_write,
dev_ioctl,
dev_open,
dev_release,
};

lseek:用来修改一个文件当前的读写位置,并将新位置做为返回值返回。出错时返回一个负的返回值。
open:来为以后的操作完成作初始化准备工作的。此外,open还增加设备计数(MOD_INC_USE_COUNT),以便防止文件在关闭前模块被卸载出内核。大部分驱动程序中open完成如下工作:
检查设备相关错误,如设备未就绪或类似的硬件问题。
如果是首次打开,初始化设备。
识别次设备号,如有必要更新fop指针。
分配和填写要放在file->private_data里的数据结构。增加使用计数。驱动程序从来不知道被打开的设备名字,它仅仅知道设备号。
release:使用计数减1,释放在file->private_data中open分配的内存,在最后一次关闭操作时关闭设备。如果open没有被调用,release也不会调用。它们在系统调用间的关系保证了模块使用计数永远是一致的(MOD_INC_USE_COUNT和MOD_DEC_USE_COUNT)。
read、write:通过这两个函数可以像使用文件那样向设备传送数据,ssize(*write)(*filp, *buff, count, *offp)和 ssize(*read)(*filp, *buff, count, *offp)其中filp是文件指针,buff是指向用户的缓冲区,count是传入数据的长度,offp是用户在文件中的位置。当成功时返回值就是写入或读取的数据长度。用write函数向打开的文件写数据,用read函数从打开的文件中读数据,完成到用户空间和来自用户空间的整个数据段的复制。
利用函数copy_to_user和copy_from_user来完成用户空间和内核空间数据的传输。
Unsigned long copy_to_user(*to, *from, count)和unsigned long copy_from_user(*to, *from, count)其中to是指向数据目的缓冲区,from是指向数据源缓冲区,count是数据的长度。当成功时,返回值就是写入或读出长度,失败返回-EFAULT。
ioctl:最常用的通过设备驱动完成控制动作的方法。ioctl的调用为驱动程序执行“命令”提供了一个与设备相关的入口点。与read和其他方法不同,ioctl是与设备相关的,允许应用程序访问被驱动硬件的特殊功能:配置设备以及进入或退出操作模式,这些控制操作通常无法通过read/write文件操作完成。

下面以I2C驱动的编写为例进行简要的说明:

驱动结构

在*系统中,I2C接口主要执行读写操作,完成与部分的数据收发工作。
根据I2C接口所需要的功能,驱动程序的file_operations结构如下:

static struct file_operations si2c_ops={
    open:        si2c_open,
    release:    si2c_release,
    ioctl:        si2c_ioctl,
};

驱动中主要函数如下:

int si2c_init (void):初始化I2C控制器,在系统中注册驱动,并初始化通信处理器CPM。
static int si2c_open (struct inode *inode, struct file *file):打开设备的第一个操作,标示设备打开,并进行加一计数。
static int si2c_release (struct inode *inode, struct file *file):当驱动程序关闭时,系统调用该函数。与open函数对应。
static int si2c_ioctl (struct inode *inode, struct file *file,unsigned int cmd, unsigned long arg):应用程序对驱动的所有操作都通过ioctl来调用。
static void si2c_interrupt (void *dev_id):负责处理收发数据和出错时产生的中断。
static void si2c_reset_params (volatile iic_t *iip):重新设置I2C CPM中控制通道的参数。
static void si2c_force_close (void):使用CPM_CR_CLOSE_RXBD命令关闭I2C通信。
extern ssize_t si2c_read (si2c_request_t *req):读I2C总线上的数据。
extern ssize_t si2c_write(si2c_request_t *req):写I2C总线上的数据。

应用程序通过si2c_ioctl来对I2C控制器进行读写操作,由si2c_ioctl分别对读写函数进行调用。进行读写操作时,I2C控制器使用中断来与驱动进行数据交互。

驱动实现

流程图如图
安装驱动程序时,系统会调用初始化函数si2c_init( )进行初始化工作。在初始化程序中,对I2C设备进行注册,使用register_chrdev( ) 返回主设备号。
I2C接口使通用I/O的PB26、PB27做为信号,在初始化I2C时必需对PB口的状态寄存器进行配置,使这两根信号线实现I2C接口功能。
接下来,在CPM的RAM中为I2C控制器的2个发送缓冲标识符和2个接收缓冲标识符申请空间。使用m8xx_cpm_dpalloc( )函数来申请地址空间,返回申请到的地址。将申请到的缓冲区地址指针分别赋给指针tbase和rbase。
在使用I2C控制器前,必须先配置好CPM ram中的I2C控制器的相关参数,不用的参数置零。做为从设备,多址板使用I2C地址为0x34。在初始化过程中,还需禁止中断,防止影响初始化工作。

图I2C驱动流程及si2c_ioctl函数结构
应用程序调用si2c_ioctl( )函数来控制驱动。分别使用I2C_CMD_READ和I2C_CMD_WRITE执行读写命令。在ioctl中这两个命令会分别调用si2c_read( )和si2c_write( )函数。驱动程序与用户缓冲区交互使用函数copy_from_user( )和copy_to_user( ),前者从用户缓冲区读数据,后者将数据复制到用户数据缓冲区。
读数据:因为I2C控制器做为从设备,在进行读操作之前,只需要初始化接收缓冲标识符,并准备好接收缓冲区,这里的接收缓冲区由ioctl函数通过参数传入。在读函数打开中断,启动I2C控制器进行读操作之后,等待中断产生,待中断返回后,检查状态寄存器是否出错,进行相应操作后返回状态值。
写数据:在启动一次写操作前,驱动程序需预先配置好发送描述符,将描述符指向的ioctl传入的发送缓冲区。些函数打开中断,启动I2C控制器后,等待中断发生,待中断返回后,检查状态寄存器,并返回状态值。
I2C控制器使用中断与驱动通信,中断由Linux系统管理,在Linux系统里,对中断的处理是属于系统核心的部分,读写函数与中断程序交互的操作由信号量实现。读写函数通过interruptible_sleep_on (&iic_wait)进入等待队列,等待中断发生。进入中断处理程序后,将控制器的中断标志位清零,并通过wake_up_interruptible (&iic_wait)唤醒读写函数,返回等待的位置。
驱动调用结束后,系统会使用si2c_release( )函数来进行减一操作并关闭I2C控制器。当使用rmmod name命令卸载驱动程序时,系统会调用cleanup_module( ),释放申请的存储空间,注销驱动设备。
驱动程序编写完毕,编写Makefile文件,具体格式如下:

KERNELDIR = /home/adhoc/linux-2.4.4
LD = powerpc-linux-gcc
CFLAGS = -D__KERNEL__ -I/home/adhoc/linux-2.4.4/include -Wall 
-Wstrict-prototypes -O2 -fomit-frame-pointer -fno-strict-aliasing 
-D__powerpc__ -fsigned-char -msoft-float -pipe -fno-builtin -ffixed-r2
-Wno-uninitialized -mmultiple -mstring -mcpu=860 -DMODULE 
-DMODVERSIONS -include 
/home/adhoc/linux-2.4.4/include/linux/modversions.h
all: clean i2c.o 
i2c.o: i2c.c 
    $(LD) $(CFLAGS) -c $^ -o $@
clean:
    rm -f *.o *. core

Makefile完成之后,在驱动文件所在目录下运行make命令,编译生成可执行文件i2c.o, 使用mknod /dev/i2c c 42 0在系统/dev目录下建立设备文件节点,驱动主设备号42,次设备号0,然后用insmod命令将驱动安装在系统中,供应用程序调用。
调试过程
设备驱动程序仅仅处理硬件,如何使用硬件的问题属于应用程序。要测试驱动程序的正确性,就应该编写相应的应用程序,对驱动的各种功能进行测试。
Linux系统中,应用程序通过open、read、write、ioctl等命令来调用驱动程序。下面以一段调用驱动写操作的应用程序为例,给出系统对应用程序的响应过程。

int main(){
    int file_desc;
    si2c_request_t *i2c_data,*temp;
    int len,i;
    i2c_data=(si2c_request_t *)malloc(sizeof(si2c_request_t));
    temp=(si2c_request_t *)malloc(sizeof(si2c_request_t));
    i2c_data->dlen=1;
    for(i=0;idlen;i++)    {
    i2c_data->data*=i;
    }
    printf("start test .../n");
    file_desc = open("/dev/i2c",O_RDWR);
    if(file_descprintf("Can't open device file:%s/n",DEVICE_NAME);
        exit(-1);
    }
    
          ioctl(file_desc,I2C_CMD_WRITE,i2c_data);
    len=i2c_data->dlen;
    printf("len=%d./n",len);
    close(file_desc);
    free(i2c_data);
    free(temp);
    return 0;
}

*1. 用户程序使用open打开设备节点文件,这时操作系统内核知道该驱动程序工作了,就调用fops方法中的open函数进行相应的工作。open方法一般返回的是文件标示符,实际上并不是直接对它进行操作的,而是有操作系统的系统调用在背后工作。
\2. 当用户使用write函数操作设备文件时,操作系统调用syswrite函数,该函数首先通过文件标识符得到设备节点文件对应的inode指针和flip指针。inode指针中有设备号信息,能够告诉操作系统应该使用哪一个设备驱动程序,flip指针中有fops信息,可以告诉操作系统相应的fops方法函数在哪里可以找到。
\3. 然后这时syswrite才会调用驱动程序中的write方法来对设备进行写的操作。其中1是在用户空间进行的,2-3是在内核空间进行的。通过系统调用sys_write将用户的write函数和操作系统的write函数联系在了一起.
在多址硬件系统中,I2C接口作为从属设备,而从属设备必须有主设备的驱动才能工作,因此要测试驱动程序,还必须模拟出一个主设备。我们用单片机来模拟主设备的工作情况。在测试过程中,可以使用printf函数将驱动中收到或发送的数据打印出来,方便观察和调试。**

以上就是良许教程网为各位朋友分享的Linu系统相关内容。想要了解更多Linux相关知识记得关注公众号“良许Linux”,或扫描下方二维码进行关注,更多干货等着你 !

137e00002230ad9f26e78-265x300
本文由 良许Linux教程网 发布,可自由转载、引用,但需署名作者且注明文章出处。如转载至微信公众号,请在文末添加作者公众号二维码。
良许

作者: 良许

良许,世界500强企业Linux开发工程师,公众号【良许Linux】的作者,全网拥有超30W粉丝。个人标签:创业者,CSDN学院讲师,副业达人,流量玩家,摄影爱好者。
上一篇
下一篇

发表评论

联系我们

联系我们

公众号:良许Linux

在线咨询: QQ交谈

邮箱: yychuyu@163.com

关注微信
微信扫一扫关注我们

微信扫一扫关注我们

关注微博
返回顶部