驱动实现——file_operations路线

153 阅读8分钟

4月「掘金·日新计划」第19天

file_operations路线(直接使用内核提供函数)

image.png

2.1.1、安装卸载驱动

  1. 2个宏

    1. module_init(chrdev_init);
    2. module_exit(chrdev_exit);
  2. 查看lsmod,内核信息dmesg

#include <linux/module.h>       // module_init  module_exit
#include <linux/init.h>         // __init   __exit/****************************************
 模块安装函数
__init修饰符,是一个宏,作用就是把这个函数放入特定的段,驱动加载完后就会释放这个段空间
宏原型  #define __init      __section(.init.text)
位置:include/linux/init.h
***************************************/
static int __init chrdev_init(void)
{   
    printk(KERN_INFO "chrdev_init helloworld init\n");
    //printk("<7>" "chrdev_init helloworld init\n");
    //printk("<7> chrdev_init helloworld init\n");
​
    return 0;
}
​
// 模块下载函数
static void __exit chrdev_exit(void)
{
    printk(KERN_INFO "chrdev_exit helloworld exit\n");
}
​
​
module_init(chrdev_init);
module_exit(chrdev_exit);
​
// MODULE_xxx这种宏作用是用来添加模块描述信息
MODULE_LICENSE("GPL");              // 描述模块的许可证
MODULE_AUTHOR("ZhangXiaowei");              // 描述模块的作者
MODULE_DESCRIPTION("module test");  // 描述模块的介绍信息
MODULE_ALIAS("alias xxx");          // 描述模块的别名信息

2.1.2、注册注销驱动

  1. file_operations结构体,控制硬件的操作方法

    1. owner,防止模块在使用的时候被卸载 THIS_MODULE
    2. llseek,read,write,open
  2. 新旧接口区别

    1. 旧接口是新接口的封装

      __register_chrdev_region
      cdev_alloc
      cdev_add
      
    2. 新接口,可以一次性注册多个次设备

  3. 旧接口

    1. 注册

      1. 向内核注册驱动结构体变量file_operations

      2. register_chrdev(成功返回主设备号,失败负数)

        1. 主设备号,0是让内核自动分配
        2. 设备名
        3. file_operations结构体
      3. 位置 fs.h

    2. 注销

      1. unregister_chrdev

        1. 主设备号
        2. 设备名
  4. 新接口

    1. 宏MKDEV,MAJOR,MINOR 合并,拆分主号,拆分次号

    2. 注册(分2步,分配主次设备号,注册设备驱动)

      1. 分配主次设备号

        1. 手动分配

          1. devno = MKDEV(major,minor),把主次设备号合并起来

          2. register_chrdev_region,分配主次设备号

            1. 参数:主次设备号,有几个次设备,设备名
        2. 自动分配

          1. alloc_chrdev_region

            1. 参数:主次设备号指针,次设备号起始,次设备个数,设备名
      2. 注册设备驱动

        1. struct cdev结构体

          struct cdev {
              struct kobject kobj;                    // 内嵌的内核对象.
              struct module *owner;                   //该字符设备所在的内核模块的对象指针.
              const struct file_operations *ops;      //该结构描述了字符设备所能实现的方法,即file_operations.
              struct list_head list;                 //用来将已经向内核注册的所有字符设备形成链表.
              dev_t dev;                              //字符设备的设备号,由主设备号和次设备号构成.
              unsigned int count;                     //隶属于同一主设备号的次设备号的个数.
          } __randomize_layout;
          
        2. cdev_alloc,实例化结构体变量

          1. 分配内存,类似malloc
        3. cdev_init,初始化cdev结构体

          1. 参数:cdev结构体指针,file_operations结构体指针
        4. cdev_add,注册设备驱动

          1. 参数:cdev结构体指针,主设备号,次设备个数
    3. 注销(2步)

      1. 注销设备驱动

        1. cdev_del,参数cdev结构体指针
      2. 注销主次设备号

        1. unregister_chrdev_region,参数:主次设备号,次设备个数
  5. 查看 cat /proc/devices

2.1.3、创建删除设备文件

  1. 2种方法(手动,自动)

    1. 手动

      1. sudo mknod 设备名 设备类型 主设备号 次设备号
      2. 例如:sudo mknod pin4 c 8 1
    2. 自动(内核调用函数,应用层udev创建删除类和设备)

      1. 创建

        1. 结构体struct class
        2. pin4_class =class_create(THIS_MODULE,"myfirstdemo"),创建类 参数:THIS_MODULE,类名 返回值class结构体指针
        3. device_create(pin4_class,NULL,devno,NULL,module_name),创建设备 参数:结构体指针,NULL,主次设备号,NULL,文件名
      2. 删除

        1. device_destroy,删除设备 参数:class结构体指针,主次设备号
        2. class_destroy,删除类 参数:class结构体指针
  2. 查看

    1. ls /dev/xxx

    2. /sys/class/xxx/xxx

#include <linux/fs.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/uaccess.h>
#include <linux/types.h>
#include <asm/io.h>
#include <linux/cdev.h>static struct cdev *pin1;//注册设备
static struct cdev *pin2;
static struct class *pin_class;//类
static struct device *pin1_class_dev;//设备
static struct device *pin2_class_dev;
​
static dev_t devno;
static int major = 231;
static int minor = 0;
​
​
//在内核源码查找struct file_operations看结构体成员,添加用到的函数
static const struct file_operations pin1_fops = {
    .owner = THIS_MODULE
};
static const struct file_operations pin2_fops = {
    .owner = THIS_MODULE
};
​
static int __init pin_init(void)//驱动入口
{
​
    printk("insmod driver pin4 success\n");
    devno = MKDEV(major,minor);//制作合并主、次设备号
​
    alloc_chrdev_region(&devno, 0, 2,"pin");    //分配2个次设备号
    pin1 = cdev_alloc();        //注册设备
    cdev_init(pin1,&pin1_fops); 
    cdev_add(pin1, MKDEV(major,0), 2);
    pin2 = cdev_alloc();
    cdev_init(pin2,&pin2_fops);
    cdev_add(pin2, MKDEV(major,1), 2);
    
    
    pin_class = class_create(THIS_MODULE,"myfirstdemo");//代码自动生成类
    pin1_class_dev = device_create(pin_class,NULL,MKDEV(major,0),NULL, "pin1");//创建设备文件
    pin2_class_dev = device_create(pin_class,NULL,MKDEV(major,1),NULL, "pin2");//创建设备文件return 0;
}
​
static void __exit  pin_exit(void)
{
    device_destroy(pin_class,MKDEV(major,0));//销毁设备
    device_destroy(pin_class,MKDEV(major,1));//销毁设备
    class_destroy(pin_class);//销毁类
​
    cdev_del(pin1); //销毁设备驱动
    cdev_del(pin2); 
    unregister_chrdev_region(devno,2);  //销毁主次设备号
    printk("insmod driver pin4 exit\n");
}
​
module_init(pin_init);//入口,是个宏
module_exit(pin_exit);
​
// MODULE_xxx这种宏作用是用来添加模块描述信息
MODULE_LICENSE("GPL v2");   // 描述模块的许可证
MODULE_AUTHOR("ZhangXiaowei");              // 描述模块的作者
MODULE_DESCRIPTION("module test");  // 描述模块的介绍信息
MODULE_ALIAS("alias xxx");          // 描述模块的别名信息

2.1.4、编写驱动操作硬件代码

  1. 虚拟地址映射

    1. 静态映射

      1. 内核硬编码,用不用都在那里,开机建立映射表,关机销毁 map开头的头文件,直接使用虚拟地址操作寄存器

      2. 本质是一个结构体数组,数组中每一个元素就是一个映射,这个映射描述了一段物理地址到虚拟地址之间的映射。这个结构体数组所记录的几个映射关系被ictable init所使用,该函数负责将这个结构体数组格式的表建立成MMU所能识别的页表映射关系,这样在开机后可以直接使用相对应的虚拟地址来访问对应的物理地址。

        开机时调用映射表建立函数

    2. 动态映射(查看数据手册,找到寄存器物理地址)

      1. 独立映射

        1. request_mem_region(GPJ0CON_PA, 4, "GPJ0CON"),向内核申请需要映射的内存资源 参数:起始地址,个数字节,名字(会记录起来,证明你申请了)
        2. pGPJ0CON = ioremap(GPJ0CON_PA, 4),物理地址映射为虚拟地址
        3. 建立:先申请,后映射 销毁:先解除映射,后释放申请
      2. 结构体方式映射(多个寄存器一起映射)

        1. 定义一个结构体,内容是int寄存器变量 连续的寄存器,有几个写几个

        2. 使用函数申请内存资源 定义寄存器基地址,个数是结构体的大小

        3. 使用映射函数 寄存器基地址,sizeof结构体大小

        4. 操作寄存器 使用结构体成员,操作寄存器

  2. 实现file_operations结构体,的操作方法(write,read函数)

    1. 指针方式操作

      *((volatile unsigned int *)gpg0con)

    2. 内核接口

      1. writeb,writew,writel 8位,16位,32位 常用writel,readl 写参数:内容数据,寄存器地址指针 读参数:寄存器地址
      2. iowrite32 ioread32
    3. 注意,写寄存器要考虑所有寄存器值

      static ssize_t pin45_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
      {   
          int userCmd;
          unsigned int t = 0;
          //获取上层write值
          copy_from_user(&userCmd,buf,count);
          printk("get value\n");
          //根据值操作io口
          if(userCmd == 1){
              t |= 1<<4;
              *GPSET45 = t;
              printk("set 1\n");
          }else if(userCmd == 0){
              t |= 1<<4;
              *GPCLR45 = t;
              printk("set 0\n");
          }else{
              printk("undo\n");
          }
          return 0;
      }
      
  3. 注意:驱动代码要简洁,占用时间短

  4. 加权限 sudo chmod 666 /dev/pin4

#include <linux/fs.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/uaccess.h>
#include <linux/types.h>
#include <asm/io.h>
#include <linux/cdev.h>static struct cdev *pin1;//注册设备
static struct cdev *pin2;
static struct class *pin_class;//类
static struct device *pin1_class_dev;//设备
static struct device *pin2_class_dev;
​
static dev_t devno;
static int major = 231;
static int minor = 0;
​
#define FSEL 0x3f200000
#define SET0 0x3f20001C
#define CLR0 0x3f200028
typedef struct GPFSEL{
    volatile unsigned int GPFSEL0;
    volatile unsigned int GPFSEL1;
    volatile unsigned int GPFSEL2;
}gpfsel;
gpfsel *pgpfsel = NULL;
​
​
volatile unsigned int* GPSET0  = NULL;
volatile unsigned int* GPCLR0  = NULL;
​
​
​
​
static ssize_t pin1_read(struct file *file, char __user *buf, size_t count, loff_t *ppos)
{
    printk("pin1_read\n");
    return 0;
}
static ssize_t pin2_read(struct file *file, char __user *buf, size_t count, loff_t *ppos)
{
    printk("pin2_read\n");
    return 0;
}
static int pin1_open(struct inode * inode, struct file * filp)
{
    pgpfsel->GPFSEL0 &= ~(6<<18);
    pgpfsel->GPFSEL0 |= 1<<18;
​
    printk("pin1_open\n");//内核的打印函数
    return 0;
}
​
static int pin2_open(struct inode * inode, struct file * filp)
{
    pgpfsel->GPFSEL1 &= ~(6<<9);
    pgpfsel->GPFSEL1 |= 1<<9;
​
    printk("pin2_open\n");//内核的打印函数
    return 0;
}
static ssize_t pin1_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
{   
    int userCmd;
    unsigned int t = 0;
    //获取上层write值
    copy_from_user(&userCmd,buf,count);
    printk("get value1\n");
    //根据值操作io口
    if(userCmd == 1){
        t |= 1<<6;
        *GPSET0 = t;
        printk("set 1\n");
    }else if(userCmd == 0){
        t |= 1<<6;
        *GPCLR0 = t;
        printk("set 0\n");
    }else{
        printk("undo\n");
    }
    return 0;
}
​
static ssize_t pin2_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
{   
    int userCmd;
    unsigned int t = 0;
    //获取上层write值
    copy_from_user(&userCmd,buf,count);
    printk("get value2\n");
    //根据值操作io口
    if(userCmd == 1){
        t |= 1<<13;
        *GPSET0 = t;
        printk("set 1\n");
    }else if(userCmd == 0){
        t |= 1<<13;
        *GPCLR0 = t;
        printk("set 0\n");
    }else{
        printk("undo\n");
    }
    return 0;
}
​
//在内核源码查找struct file_operations看结构体成员,添加用到的函数
static const struct file_operations pin1_fops = {
    .owner = THIS_MODULE,
    .write = pin1_write,//函数指针
    .open  = pin1_open,
    .read = pin1_read
};
​
static const struct file_operations pin2_fops = {
    .owner = THIS_MODULE,
    .write = pin2_write,//函数指针
    .open  = pin2_open,
    .read = pin2_read
};
​
static int __init pin_init(void)//驱动入口
{
    struct resource *r1;
    printk("insmod driver pin4 success\n");
    devno = MKDEV(major,minor);//制作合并主、次设备号alloc_chrdev_region(&devno, 0, 2,"pin");    //分配2个次设备号
    pin1 = cdev_alloc();        //注册设备
    cdev_init(pin1,&pin1_fops); 
    cdev_add(pin1, MKDEV(major,0), 2);
    pin2 = cdev_alloc();
    cdev_init(pin2,&pin2_fops);
    cdev_add(pin2, MKDEV(major,1), 2);
    
    
    pin_class = class_create(THIS_MODULE,"myfirstdemo");//代码自动生成类
    pin1_class_dev = device_create(pin_class,NULL,MKDEV(major,0),NULL, "pin1");//创建设备文件
    pin2_class_dev = device_create(pin_class,NULL,MKDEV(major,1),NULL, "pin2");//创建设备文件
​
​
    /*r1 = request_mem_region(FSEL,sizeof(gpfsel), "pin");
    if (!r1)
        printk("pgpfsel exit\n");*/
    pgpfsel = ioremap(FSEL,sizeof(gpfsel));
    //request_mem_region(SET0,4, "xwGPIO1");
    //request_mem_region(CLR0,4, "xwGPIO2");
    GPSET0  = (volatile unsigned int *)ioremap(SET0,4);
    GPCLR0  = (volatile unsigned int *)ioremap(CLR0,4);
​
    return 0;
}
​
static void __exit  pin_exit(void)
{
    iounmap(GPCLR0);
    iounmap(GPSET0);
    iounmap(pgpfsel);
    printk("iounmap exit\n");
    //release_mem_region(CLR0,4);
    //release_mem_region(SET0,4);
    release_mem_region(FSEL,sizeof(gpfsel));
    printk("release_mem_region exit\n");
    device_destroy(pin_class,MKDEV(major,0));//销毁设备
    device_destroy(pin_class,MKDEV(major,1));//销毁设备
    class_destroy(pin_class);//销毁类cdev_del(pin1); //销毁设备驱动
    cdev_del(pin2); 
    unregister_chrdev_region(devno,2);  //销毁主次设备号
    printk("insmod driver pin exit\n");
}
​
module_init(pin_init);//入口,是个宏
module_exit(pin_exit);
​
// MODULE_xxx这种宏作用是用来添加模块描述信息
MODULE_LICENSE("GPL v2");   // 描述模块的许可证
MODULE_AUTHOR("ZhangXiaowei");              // 描述模块的作者
MODULE_DESCRIPTION("module test");  // 描述模块的介绍信息
MODULE_ALIAS("alias xxx");          // 描述模块的别名信息

2.1.5编译驱动

  1. 分离式模块化编译

    #ubuntu的内核源码树,如果要编译在ubuntu中安装的模块就打开这3个
    #KERN_VER = $(shell uname -r)
    #KERN_DIR = /lib/modules/$(KERN_VER)/build  
        
    # 开发板的linux内核的源码树目录
    KERN_DIR = /home/xw/xiaowei/linux-rpi-4.14.y
    ​
    #obj-m := module_test.o
    #modulename-objs := file1.o file2.o   有多个文件编译方式
    obj-m   += module_test.o    #模块化,y编入,n去除,m模块化#进入到源码树目录下编译模块,然后把生成文件拷贝到原目录
    #M=’pwd’代表将编译完的.ko文件放回到当前目录下
    all:
        make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- KERNEL=kernel7 O=output/ -C $(KERN_DIR) M=`pwd` modules 
    ​
    #这里有问题,去除不了
    .PHONY: clean   
    clean:
        make O=output/ -C $(KERN_DIR) M=`pwd` modules clean
    
  2. 编译进内核

    1. 驱动文件放在合适目录,如char下

    2. 修改Makefile

    3. 修改Kconfig

    4. 配置make menuconfig