欢迎加入QQ讨论群258996829
麦子学院 头像
苹果6袋
6
麦子学院

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

发布时间:2017-09-24 22:00  回复:0  查看:2535   最后回复:2017-09-24 22:00  

本文和大家分享的主要是嵌入式linux设备驱动编写原理相关内容,一起来看看吧,希望对大家学习嵌入式有所帮助。

  驱动简介

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

  l  对设备初始化和释放

  l 把数据从内核传送到硬件和从硬件读取数据

  l  读取应用程序传送给设备文件的数据和回送应用程序请求的数据

  l  检测和处理设备出现的错误。

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

  1.具有一整套的和硬件设备通讯的例程,并且提供给操作系统一套标准的软件接口;

  2.具有一个可以被操作系统动态地调用和移除的自包含组件;

  3.可以控制和管理用户程序和物理设备之间的数据流。

  驱动类型

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

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

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

  网络设备在Linux里做专门的处理。Linux的网络系统主要是基于BSD UnixSocket机制。在系统和驱动程序之间定义有专门的数据结构进行数据的传递。系统里支持对发送资料和接受资料的缓存,提供流量控制机制,提供对多协议的支持。

  主次设备号

  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系统把设备当作文件一样来访问,访问文件和设备有以下函数:seekreadwritepollio-controlmemory mapopenflushreleasechecklock等。编写设备驱动程序的主要工作就是编写子函数,并填充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_dataopen分配的内存,在最后一次关闭操作时关闭设备。如果open没有被调用,release也不会调用。它们在系统调用间的关系保证了模块使用计数永远是一致的(MOD_INC_USE_COUNTMOD_DEC_USE_COUNT)

  readwrite:通过这两个函数可以像使用文件那样向设备传送数据,ssize(*write)(*filp, *buff, count, *offp)ssize(*read)(*filp, *buff, count, *offp)其中filp是文件指针,buff是指向用户的缓冲区,count是传入数据的长度,offp是用户在文件中的位置。当成功时返回值就是写入或读取的数据长度。用write函数向打开的文件写数据,用read函数从打开的文件中读数据,完成到用户空间和来自用户空间的整个数据段的复制。

  利用函数copy_to_usercopy_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/OPB26PB27做为信号,在初始化I2C时必需对PB口的状态寄存器进行配置,使这两根信号线实现I2C接口功能。

  接下来,在CPMRAM中为I2C控制器的2个发送缓冲标识符和2个接收缓冲标识符申请空间。使用m8xx_cpm_dpalloc( )函数来申请地址空间,返回申请到的地址。将申请到的缓冲区地址指针分别赋给指针tbaserbase

  在使用I2C控制器前,必须先配置好CPM ram中的I2C控制器的相关参数,不用的参数置零。做为从设备,多址板使用I2C地址为0x34。在初始化过程中,还需禁止中断,防止影响初始化工作。

  图I2C驱动流程及si2c_ioctl函数结构

  应用程序调用si2c_ioctl( )函数来控制驱动。分别使用I2C_CMD_READI2C_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系统中,应用程序通过openreadwriteioctl等命令来调用驱动程序。下面以一段调用驱动写操作的应用程序为例,给出系统对应用程序的响应过程。

  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_desc<0){

  printf("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函数将驱动中收到或发送的数据打印出来,方便观察和调试。

 

 

来源:嵌入式Linux中文站

您还未登录,请先登录

热门帖子

最新帖子