Linux驱动

1 驱动分类

  常规分类:字符设备、块设备、网络设备

    字符设备:一种按字节来访问的设备,字符驱动负责驱动字符设备,这样的驱动通常实现open、close、read和write系统调用。如串口、LED、按键;

    块设备:以块(一般为512字节)为最小传输单位的设备,块设备不能按字节处理数据。在Linux系统中运行块设备传输任意数目的字节。块设备与字符设备的区别是驱动与内核的接口不同。如硬盘、flash、SD卡。

    网络设备:可以是一个硬件设备,如网卡;也可以是一个纯粹的软件设备,如回环接口(lo)。一个网络接口负责发送和接收数据报文。

  总线分类:USB设备、PCI设备、平台总线设备

2 硬件访问

  

  驱动程序要控制设备是通过设备内寄存器控制的。

  硬件访问步骤:->地址映射->寄存器读写

  在Linux系统中,无论是内核程序还是应用程序,都只能使用虚拟地址,而芯片手册中给出的寄存器地址或者RAM地址则是物理地址,无法直接使用。因此,读写寄存器的第1步就是将它的物理地址映射为虚拟地址。

  地址映射包括动态映射和物理映射;

  动态映射:在驱动程序中采用ioremap()函数将物理地址映射为虚拟地址:

    函数原型: void *ioremap(physaddr,size)

         physaddr:待映射的物理地址

         size:映射的区域长度

          返回值:映射后的虚拟地址

    静态映射:根据用户事先指定的映射关系,在内核启动时,自动将物理地址映射为虚拟地址。

    用户是通过map_desc结构体来指明物理地址与虚拟地址的映射关系

      struct map_desc{
               unsigned long virtual; /* 映射后的虚拟地址 */
               unsigned long pfn; /* 物理地址所在的页帧号 */
               unsigned long length; /* 映射长度 */
               unsigned int type; /* 映射的设备类型 */
             };
      pfn: 利用__phys_to_pfn(物理地址)可以计算出物理地址所在的物理页帧号

  内核寄存器读写函数:

unsigned ioread8(void *addr0)
unsigned ioread16(void *addr0)
unsigned ioread32(void *addr0)

unsigned readb(address)
unsigned readw(address)
unsigned readl(address)

void iowrite8(u8 value,void *addr)
void iowrite16(u16 value,void *addr)
void iowrite32(u32 value,void *addr)

void writeb(unsigned value,address)
void writew(unsigned value,address)
void writel(unsigned value,address)

3 字符设备文件

  字符设备驱动程序是通过字符设备文件被用户调用。

  

  通过字符设备文件,应用程序可以使用相应的字符设备驱动程序来控制字符设备。

  应用程序首先通过文件名找到字符设备文件,假如要从设备中读出或者写入数据都是从字符设备文件展开的,字符设备文件是设备驱动程序和应用程序的一个媒介,应用程序对设备的操作是通过字符设备文件来完成的。

  创建字符设备文件:

    mknod命令

    mknod /dev/文件名 c 主设备号 次设备号          //c表示char

   例:mknod /dev/memdev0 c 253 0

    字符设备文件与驱动程序通过主设备号建立起联系,字符设备文件对应一个主设备号,驱动程序对应一个主设备号,如果两个号相等说明两种之间是一一对应的关系。

    当用户去操作设备的时候内核就会找到相应的驱动程序。

  通过cat /proc/devices打印主设备号

  次设备号0~255

  ls /dev/memdev0   //查看

  显示:/dev/memdev0

  有了字符设备文件,也有了设备驱动程序,就要编写应用程序。就是通过字符设备文件访问设备驱动程序。

  要访问硬件,其实就是访问硬件里的寄存器,假如定义一个数组,数组里头有5个整型的元素,每一个整型元素就可以模拟一个寄存器,要去操作硬件,最后可以变为操作数组,比如要把数据写入寄存器,实际上就变成往数组里头写入数据。通过驱动程序往数组里头写入数据。如果要从设备里头读出数据,通过驱动程序从数组里头读出数据。 

4 字符设备驱动实例

  驱动程序通常采用内核模块的程序结构来进行编码。因此,编译/安装一个驱动程序,其实质就是编译/安装一个内核模块。

  驱动程序文件包含:memdev.c、Makefile;应用程序文件包含:write_mem.c、read_mem.c;

  1 通过命令make,编译驱动程序得到文件memdev.ko,并将其拷贝到nfs开发板挂载的目录,然后安装驱动程序

    insmod memdev.ko       安装memdev.ko

    lsmod         查看驱动

  

  2 使用命令查看设备号:cat /proc/devices

  3 创建字符设备文件:

    mknod /dev/memdev0 c 253 0           //名字不能跟已有的重复

  

  4 查看创建的设备字符文件

    ls /dev/memdev0

    显示: /dev/memdev0

  5 编译应用程序write_mem.c

    arm-linux-gcc write_mem.c -o write_mem

  运行:./write_mem

  报错:-/bin/sh: ./write_mem:not found (应用程序依赖的库找不到)

  通过:arm-linux-readlef -d write_mem 查询应用程序所依赖的动态链接库

  

  通过ls命令查看,开发板中没有这个库,所以报错;

  解决办法:

    1 直接将libc.so.6复制到开发板中

    2 采样静态编译的方法

      arm-linux-gcc -static write_mem.c -o write_mem

  采用静态编译后运行:./write_mem

  6 编译应用程序read_mem.c

    arm-linux-gcc -static read_mem.c -o read_mem

  运行:./read_mem

  结果:dst is 2013

5 字符设备驱动程序模板

  

  1 设备描述结构

  在任何一种驱动模型中,设备都会用内核中的一种结构来描述。我们的符设备在内核中使用struct cdev来描述。

    struct cdev {
            struct kobject kobj;
            struct module *owner;
            const struct file_operations *ops; //设备操作集
            struct list_head list;
            dev_t dev; //设备号
            unsigned int count; //设备数
            };

  查看设备号ls –l dev下都是设备文件

   eg:crw-r----- 1 root root 10, 223 12月 15:00.10 uinput

    10:主设备号   223:次设备号

  主设备号反映设备类型,次设备号区分同类型设备

  

  设备号操作 

    Linux内核中使用dev_t类型来定义设备号,dev_t这种类型其实质为32位的unsigned int,其中高12位为主设备号,低20位为次设备号.

    问1:如果知道主设备号,次设备号,怎么组合成dev_t类型
    答:dev_t dev = MKDEV(主设备号,次设备号)
    问2: 如何从dev_t中分解出主设备号?
    答: 主设备号 = MAJOR(dev_t dev)
    问3: 如何从dev_t中分解出次设备号?
    答: 次设备号=MINOR(dev_t dev)

  设备号分配:静态申请和动态分配

  静态申请:

    开发者自己选择一个数字作为主设备号,然后通过函数register_chrdev_region向内核申请使用。缺点:如果申请使用的设备号已经被内核中的其他驱动使用了,则申请失败。

  动态分配:

    使用alloc_chrdev_region由内核分配一个可用的主设备号。优点:因为内核知道哪些号已经被使用了,所以不会导致分配到已经被使用的号。

  设备号注销

    不论使用何种方法分配设备号,都应该在驱动退出时,使用unregister_chrdev_region函数释放这些设备号。

  2 操作函数集

  

  Struct file_operations是一个函数指针的集合,定义能在设备上进行的操作。结构中的函数指针指向驱动中的函数, 这些函数实现一个针对设备的操作, 对于不支持的操作则设置函数指针为 NULL。例如:

  https://blog.csdn.net/littlelee111/article/details/10133759

    struct file_operations dev_fops ={
                      .llseek = NULL,
                      .read = dev_read,
                      .write = dev_write,
                      .ioctl = dev_ioctl,
                      .open = dev_open,
                      .release = dev_release,
                     };

  3 字符设备初始化

  分配cdev:可以采用静态和动态两种办法

  静态分配:
    struct cdev mdev;
  动态分配:
    struct cdev *pdev = cdev_alloc();

  初始化cdev

      structcdev的初始化使用cdev_init函数来完成。
    cdev_init(struct cdev *cdev, const struct file_operations *fops)
    参数:
      cdev: 待初始化的cdev结构
      fops: 设备对应的操作函数集

  注册cdev:

         注册使用cdev_add函数来完成。

    cdev_add(structcdev *p, dev_t dev, unsigned count)
    参数:
      p: 待添加到内核的符设备结构
      dev: 设备号
      count: 该类设备的设备个数

  4 设备操作原型

    int (*open)(struct inode *, struct file *) 打开设备,响应open系统

    int (*release)(struct inode *, struct file *);关闭设备,响应close系统调用

    loff_t (*llseek)(struct file *, loff_t, int);重定位读写指针,响应lseek系统调用

    ssize_t (*read)(struct file *,char __user *,size_t,loff_t *)从设备读取数据,响应read系统调用

    ssize_t(*write)(struct file*,const char __user*,size_t,loff_t*)向设备写入数据,响应write系统调用

  struct file

    在Linux系统中,每一个打开的文件,在内核中都会关联一个struct file,它由内核在打开文件时创建, 在文件关闭后释放。

    重要成员:
      loff_t f_pos /*文件读写指针*/
      struct file_operations *f_op /*该文件所对应的操作*/

  struct inode

  每一个存在于文件系统里面的文件都会关联一个inode 结构,该结构主要用来记录文件物理上的信息。因此, 它和代表打开文件的file结构是不同的。一个文件没有被打开时不会关联file结构,但是却会关联一个inode 结构。

  重要成员:
    dev_t i_rdev:设备号

    设备操作open

  open设备方法是驱动程序用来为以后的操作完成初始化准备工作的。在大部分驱动程序中,open完成如下工作:

  标明次设备号
  启动设备

  设备操作release

  release方法的作用正好与open相反。这个设备方法有时也称为close,它应该:关闭设备。

  设备操作read

  read设备方法通常完成2件事情:
    从设备中读取数据(属于硬件访问类操作)
    将读取到的数据返回给应用程序
  ssize_t (*read) (struct file *filp, char __user *buff, size_t count, loff_t *offp)
  参数分析:
    filp:与符设备文件关联的file结构指针, 由内核创建。
    buff : 从设备读取到的数据,需要保存到的位置。由read系统调用提供该参数。
    count: 请求传输的数据量,由read系统调用提供该参数。
    offp: 文件的读写位置,由内核从file结构中取出后,传递进来

    buff参数是来源于用户空间的指针,这类指针都不能被内核代码直接引用,必须使用专门的函数

      int copy_from_user(void *to, const void __user *from, int n)
      int copy_to_user(void __user *to, const void*from, intn)

  设备操作write

  write设备方法通常完成2件事情:
    从应用程序提供的地址中取出数据
    将数据写入设备(属于硬件访问类操作)
  ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *)

  5 驱动注销

  当我们从内核中卸载驱动程序的时候,需要使用cdev_del函数来完成符设备的注销。

posted @ 2019-04-10 09:07  dongry  阅读(714)  评论(0编辑  收藏  举报