驱动(3种实现方法,2条路线)

一、概念

1.1、驱动认识

  1. 裸机程序中是直接操控硬件的,操作系统中必须通过驱动来操控硬件。这两个的本质区别就是分层
  2. linux驱动本身做了模块化设计,linux驱动本身和linux内核不是强耦合的
  3. 驱动的设计中有一个关键数据结构(结构体),结构体中包含一些变量和一些函数指针
    变量用来记录驱动相关的一些属性,函数指针用来记录驱动相关的操作方法
    这些变量和函数指针加起来就构成了驱动。驱动就被抽象为这个结构体
  4. 一个驱动工作时主要就分几部分:驱动构建(构建一个struct mmc然后填充它)
    驱动运行时(调用这些函数指针指针的函数和变量)
  5. 分离思想
    分离思想就是说在驱动中将操作方法数据分开
    操作方法就是函数,数据就是变量
    在不同的地方来存储和管理驱动的操作方法和变量,这样的优势就是驱动便于移植。
  6. 分层思想
    分层思想是指一个整个的驱动分为好多个层次
    简单理解就是驱动分为很多个源文件,放在很多个文件夹中

1.2、什么是驱动

  1. 设备和用户之间的桥梁,内核结构由用户级,内核级,硬件级
  2. 驱动,操作硬件部分代码

1.3、驱动分为3种,及区别

  1. 字符设备,以字节为单位读写串口,led等
  2. 块设备,以块为单位读写,sd卡等
  3. 网络设备,网卡,socket等

1.4、模块化

  1. 宏内核:操作系统是整体,紧耦合,直接调用函数,简单高效
    微内核:相当于是多个进程,一个错误不会影响其他,Windows
  2. 静态模块化:程序编译内核,编译时可裁剪,加模块要重新编译烧录
    动态模块化:加模块,不需要重新编译

1.5、安全性

  1. 驱动程序崩溃,内核可能崩溃
  2. 驱动效率影响内核效率
  3. 安全,漏洞
    未初始化,例如函数指针
    恶意用户程序,利用驱动的漏洞,例如传参
    缓冲区溢出,例如超出缓存区大小
    竞争状态,自旋锁等

1.6、应用驱动硬件的流程

APP-》C library调用open产生软中断系统调用,中断号0x80-》汇编sys_call-》VFS的sys_open-》内核空间,设备驱动的open-》硬件

1.7、设备文件

  1. 各种设备以文件的形式存放在**/dev目录,称设备文件**。
  2. 主设备号,次设备号
    1. 主设备号:不同的设备(硬盘,led)
    2. 次设备号:同一类的多个设备(led1,led2)

1.8、驱动链表,数组

  1. 管理所有设备的驱动
  2. 添加(驱动插入链表顺序由设备号检索)
    编写驱动,加载到内核
    设备名,设备号,设备驱动函数:操作寄存器来驱动I/O
  3. 查找 调用
    调用驱动程序,用户应用去open

1.9、udev机制

  1. udev是一个设备管理工具,udev以守护进程的形式运行
  2. 通过侦听内核发出来的uevent来管理/dev目录下的设备文件
  3. udev在用户空间运行,而不在内核空间运行。
  4. 它能够根据系统中的硬件设备的状态动态更新设备文件,包括设备文件的创建,删除等。
  5. udev会根据/etc/udev/udev.conf文件中的udev_rules指定的目录,逐个检查该目录下的文件,这个目录下的文件都是针对某类或某个设备应该施行什么措施的规则文件。

1.10、地址概念

  1. 总线地址
    1. 属于一种电脑总线一部分,是由CPU 或有DMA 能力的单元,用来沟通这些单元想要存取(读取/写入)电脑内存元件/地方的实体位址。
    2. CPU能够访问内存的范围
      例如:32位window系统,2的32次方,最大只能识别4G(即便是有8G内存条,也只能识别4G,其实识别不了4G,有用作其他的内存)
  2. 物理地址
    1. 硬件实际地址或者绝对地址
  3. 虚拟地址
    1. 逻辑地址(基于算法的地址,软件层面的地址)称为虚拟地址
    2. 程序在磁盘超过1G,就用到虚拟地址,一点一点的拿来运行(没有虚拟地址就无法运行,例如51,32)
    3. 物理地址通过页表ARM MMU映射成虚拟地址

1.11、设备驱动,设备驱动模型,驱动子系统

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YpYclpGn-1681025761675)(F:\2023秋招\20201126163240999.png)]

  1. 最基本的字符设备的驱动
    1. [字符设备驱动] ==> 自己实现 file_operations, device_create,cdev_add…
  2. 驱动子系统
    1. input 驱动来说,input 驱动子系统帮忙实现了 file_operations,申请设备号,创建设备,cdev_add 等操作
    2. 而 input 驱动只需实现去接收输入,然后向上层报告输入事件和输入的数据即可
  3. 设备模型
    1. 驱动和设备的外衣
    2. 设备硬件参数和驱动分离
    3. 热拔插,检测到有设备,创建驱动文件

二、字符设备驱动

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CYp6NO5f-1681025761676)(C:\Users\戴尔\AppData\Roaming\Typora\typora-user-images\1681008081491.png)]

  1. 驱动实现有3条路线,file_operations和attribute,驱动模型(bus总线)

  2. 相关命令

    1. lsmod 显示模块
    2. insmod 安装模块
    3. rmmod 卸载模块
    4. modinfo 显示模块信息
    5. dmesg 内核打印信息
    6. ifdown eth0 关闭网卡驱动
    7. ifup eth0 打开网卡驱动
  3. 查看驱动相关

    1. lsmod
    2. cat /proc/devices
    3. ls /dev/… -l
    4. /sys/class/leds/出现myled
    5. set nonumber去掉vi行标

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pdgOb6Qf-1681025761678)(C:\Users\戴尔\AppData\Roaming\Typora\typora-user-images\1680960104166.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

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WxG93nXu-1681025761679)(C:\Users\戴尔\AppData\Roaming\Typora\typora-user-images\1680919245694.png)]

#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. 操作寄存器
          使用结构体成员,操作寄存器

          [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uSYWhWH8-1681025761680)(C:\Users\戴尔\AppData\Roaming\Typora\typora-user-images\1680685864675.png)]

  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

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9P9faLaL-1681025761681)(C:\Users\戴尔\AppData\Roaming\Typora\typora-user-images\1680659578975.png)][外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9LqlfoFb-1681025761681)(C:\Users\戴尔\AppData\Roaming\Typora\typora-user-images\1680659590459.png)]
      在这里插入图片描述

2.2、attribute路线(驱动框架)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CZQknDxb-1681025761682)(C:\Users\戴尔\AppData\Roaming\Typora\typora-user-images\1680960148948.png)]

只在sys/class的具体类下面,创建文件,操作文件

2.2.1、驱动框架概念
  1. 每一类都有成熟,标准的,典型的驱动框架
    内核工程师做一部分,驱动做一部分
    功能相同的封装出来,不同的驱动工程师做

    1. 例如led驱动 drivers/leds目录下
      1. 内核工程师做的
        led-class.c
        led-core.c
      2. 驱动需要做的
        leds-xxx.c
    2. 不用驱动框架做驱动
      drivers/char/leds/x210-led.c
  2. misc驱动框架

    1. 杂项设备(蜂鸣器,adc)
      /sys/class/misc
      misc类,驱动框架,典型的字符设备
      内核实现:类的创建,misc.c
      驱动实现:驱动注册
    2. cat /proc/misc
  3. 资源使用前必须申请,例如中断号,内存

  4. 特定接口,数据结构是驱动的直接体现

2.2.2、安装卸载驱动
  1. 安装,有2种
    1. module_init
    2. subsys_initcall
    3. 区别就是,内核执行顺序不同,一个是6,一个是4
      内核启动时要调用的所有函数归类,然后每个类按照一定的次序去调用执行。这些分类名就叫.initcalln.init。n的值从1到8。内核开发者在编写内核代码时只要将函数设置合适的级别,这些函数就会被链接的时候放入特定的段,内核启动时再按照段顺序去依次执行各个段即可
  2. 卸载,和上面一样module_exit
2.2.3、注册注销驱动(在sys类下创建文件接口)
  1. 结构体struct led_classdev

    name:类名myled
    max_bringhtness:最大数
    brightness:读
    brightness_set:写方法,这个函数控制硬件
    
  2. 注册

    1. led_classdev_register,参数:NULL,classdev结构体指针
  3. 注销

    1. led_classdev_unregister,参数:classdev结构体指针
2.2.4、使用
  1. 代码在gpiolib库部分

  2. 目录/sys/class/leds/出现xxx

  3. 加权限777

  4. cat,echo操作文件(相当于操作硬件)

    1. cat执行了 brightness_show方法
      最终执行brightness执行的函数

    2. echo执行 brightness_store方法

      echo 1 > xxx

      brightness_set函数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IAYLdNfh-1681025761682)(C:\Users\戴尔\AppData\Roaming\Typora\typora-user-images\1680917107062.png)]

2.3、平台总线(驱动模型)

在这里插入图片描述

  1. 平台总线的设计是基于设备模型的
  2. 实现device(中断/地址)和driver(操作逻辑)分离
  3. 实现一个driver代码能够驱动多个平台相似的模块
2.3.1、平台总线工作流程
  1. bus中注册platform
  2. 提供platform_deviceplatform_driver结构体
  3. platform的match函数匹配设备和驱动
    匹配成功,driver的probe函数初始化驱动和安装
  4. platform的注册是通过下面的注册安装函数完成
  5. platdata设备注册时有关数据,如gpio等
    这些数据在match后由设备提供给驱动
    驱动是没有数据的,算法数据分离
    保持驱动的独立和适应性
2.3.2、平台总线三要素

设备,驱动分离
算法,数据分离
为了设备驱动相遇probe执行

  1. bus总线对象

    1. 不需要自己创建,开机的时候自动创建。结构体对象

      struct bus_type platform_bus_type = {
          .name        = "platform",
          .dev_groups    = platform_dev_groups,
          .match        = platform_match,
          .uevent        = platform_uevent,
          .pm        = &platform_dev_pm_ops,
      };
      
    2. linux内核自动匹配设备驱动,匹配规则

      1. 优先匹配pdriver中的id_table,里面包含了支持不同的平台的名字
      2. 直接匹配driver中名字和device中名字
  2. device设备对象

    1. 为驱动提供硬件信息,描述性内容。结构体对象

      struct platform_device {
      	const char	*name;  //用于做匹配
      	int		id;  // 一般都是直接给-1
      	struct device	dev; // 继承了device父类
      	u32		num_resources; // 资源的个数
      	struct resource	*resource; // 资源:包括了一个设备的地址和中断
      }
      
    2. 注册和注销

      int  platform_device_register(struct platform_device * pdev)void  platform_device_unregister(struct platform_device * pdev);
      
  3. driver驱动对象

    1. 实现功能接口,动作性描述。结构体对象

      struct platform_driver {
      		int (*probe)(struct platform_device *); //匹配成功之后被调用的函数
      		int (*remove)(struct platform_device *);//device移除的时候调用的函数
      		struct device_driver driver; //继承了driver父类
      		const struct platform_device_id *id_table; //如果driver支持多个平台,在列表中写出来
      }
      
    2. 注册和注销

      int platform_driver_register(struct platform_driver *drv);
      void platform_driver_unregister(struct platform_driver *drv)
      
2.3.3、使用(input子系统)

在这里插入图片描述

2.3.3.1、概念
  1. platform 驱动框架分为总线、设备和驱动
    1. 总线在Linux内核提供,不需要我们进行考虑,所以只需考虑设备和驱动。
    2. 因为目前的Linux内核都支持设备树了,所以platform_device设备信息都通过设备树进行描述了,因此只需要在代码中编写platform_driver驱动代码(我实现的还是老方法,设备驱动编写)
  2. 应用层 + input event + input core + 硬件驱动
    1. 当按下鼠标,硬件中断执行判断是什么设备
    2. 然后上报core(负责封装为input_event结构体)
    3. 应用层解析事件结构体处理
  3. 结构体struct input_event,事件
    1. time时间
    2. type驱动类型,例如鼠标
      1. type
        EV_SYN 同步类型,一般3个数据包,最后一个就是0,0,0
        EV_KEY 按键
        EV_REL 相对的,鼠标移动
        EV_ABS 绝对的,触摸屏
        EV_MSC 键盘
    3. code鼠标左键
      1. 哪一个按键
    4. value按下了
      1. 非0按下
2.3.3.2、代码流程分析
  1. 应用层操作驱动有2条路,/dev设备文件,和属性文件/sys,input使用的是/dev

  2. 核心层

    1. input.c
      主要创建类,注册主设备驱动(提供了第一种驱动的结构体file_operations)
    2. class_register
      创建类
    3. input_proc_init
      /proc/bus/input
    4. register_chrdev
      注册设备驱动,主设备号
  3. 设备驱动层

    1. drivers/input/xxx
    2. input_allocate_device,申请设备内存
    3. input_set_capability,输入设备的能力set_bit
    4. input_register_device,注册
  4. 事件驱动层

    1. evdev.c

      input_report_key,上报应用层

    2. input_register_handler

    3. input_register_handle

    4. 设备和handler匹配
      成功注册驱动,生成文件/dev/input/xxx

2.3.3.3、使用input
  1. 应用层

    1. 打开文件
    2. 读取结构体变量,解析结构体变量
  2. 驱动

    1. 定时器轮询
    2. platform和input
      platform设备驱动注册,相遇调用probe
    3. probe
      1. 申请,设置上拉,输入模式gpio
      2. input_dev结构体指针
        动态分配内存input_allocate_device
        填充结构体set_bit
        注册设备input_register_device
      3. struct timer_list结构体
        内核定时器初始化init_time
        设置绑定函数 .function
        时间 .expires
        启动定时器 add_time
        del_timer删除定时器
        1. 定时器处理函数time_handler
          读取gpio值gpio_get_value
          判断值和上次是否一样if
          上报应用层input_report_key
          更新定时器mod_timer
  3. 验证使用

    1. /dev/input/下多出event0
      /sys/bus/platform/devices出现设备fire
      /sys/bus/platform/drivers出现驱动fire

    2. 数据+心跳包

在这里插入图片描述

  1. 驱动

    #include <linux/input.h> 
    #include <linux/module.h> 
    #include <linux/init.h>
    #include <asm/irq.h> 
    #include <asm/io.h>
    
    #include <linux/gpio.h>
    #include <linux/platform_device.h>
    
    #define GPIO_FIRE	17  //io口号,可以使用命令gpio read
    static struct input_dev *input;	//指针,后面动态分配内存
    static struct timer_list timer;	//定时器结构体
    static int history;		//记录上次io值
    
    //定时器处理函数
    static void fire_timer_handler(unsigned long data) 
    { 
    	int flag;
    	flag = gpio_get_value(GPIO_FIRE);
    	if(flag != history){	//和上次值比较
    		if(flag){
    			input_report_key(input, KEY_OK, 1);	//上报应用层
    		}else{
    			input_report_key(input, KEY_OK, 0);
    		}
    		history = flag;
    	}
    	input_sync(input);	//同步包
    
    	mod_timer(&timer, jiffies + HZ/100);	//更新定时器
    }
    
    //匹配成功注册设备
    static int fire_button_probe(struct platform_device *pdev)
    {
    	
    	int error;
    	
    	error = gpio_request(GPIO_FIRE, "GPIO_0_FIRE");	//申请io
    	if(error){
    		printk("fire.c: request gpio GPIO_0 fail");
    	}	
    	gpio_direction_input(GPIO_FIRE);	//输入模式
    	history = gpio_get_value(GPIO_FIRE);
    		
    	input = input_allocate_device();	//输入设备结构体,实例化
    	if (!input) 
    	{ 
    		printk(KERN_ERR "fire.c: Not enough memory\n");
    		error = -ENOMEM;
    		goto err_free_mem;
    	}
    
    	//填充结构体
    	set_bit(EV_KEY, input->evbit);	//按键类型
    	set_bit(KEY_OK, input->keybit); //哪一个按键
    	//注册
    	error = input_register_device(input);
    	if (error) 
    	{ 
    		printk(KERN_ERR "fire.c: Failed to register device\n");
    		goto err_free_device;
    	}
    	
    	//定时器
    	init_timer(&timer);
    	timer.function = fire_timer_handler;	//处理函数
    	timer.expires = jiffies + (HZ/100);	//定时时间
    	add_timer(&timer);		启动
    	return 0;
    //倒影式处理错误
    err_free_device:
    	input_free_device(input);
    err_free_mem:
    	gpio_free(GPIO_FIRE);
    	return error;
    }
    
    //注销
    static int fire_button_remove(struct platform_device *dev)
    {
    	del_timer(&timer);
    	input_unregister_device(input); 
    	gpio_free(GPIO_FIRE);	
    	return 0;
    }
    
    //设备
    static struct platform_device fire_device_button = {
    	.name		= "fire",
    	.id	= -1,
    };
    
    //驱动
    static struct platform_driver fire_button_device_driver = {
    	.probe = fire_button_probe,
    	.remove		= fire_button_remove,
    	.driver		= {
    		.name		= "fire",
    		.owner		= THIS_MODULE,
    	},
    };
    
    //平台总线
    static int __init button_init(void) 
    {
    	platform_device_register(&fire_device_button);
    	return platform_driver_register(&fire_button_device_driver);
    }
    static void __exit button_exit(void) 
    { 
    	platform_driver_unregister(&fire_button_device_driver);
    	platform_device_unregister(&fire_device_button);
    }
    
    module_init(button_init); 
    module_exit(button_exit); 
    
    MODULE_LICENSE("GPL");
    MODULE_AUTHOR("xiaowei");
    MODULE_DESCRIPTION("fire");
    
  2. 应用层

    #include <stdio.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    #include <linux/input.h>
    #include <string.h>
    
    #define DEVICE_KEY			"/dev/input/event1"
    #define DEVICE_MOUSE		"/dev/input/event3"
    #define X210_KEY			"/dev/input/event1"
    int main(void)
    {
    	int fd = -1, ret = -1;
    	struct input_event ev;
    	
    	// 打开驱动文件
    	fd = open(X210_KEY, O_RDONLY);
    	if (fd < 0)
    	{
    		perror("open");
    		return -1;
    	}
    	
    	while (1)
    	{
    		// 初始化事件结构体
    		memset(&ev, 0, sizeof(struct input_event));
    		ret = read(fd, &ev, sizeof(struct input_event));
    		if (ret != sizeof(struct input_event))
    		{
    			perror("read");
    			close(fd);
    			return -1;
    		}
    		
    		// 输出解析
    		printf("-------------------------\n");
    		printf("type: %hd\n", ev.type);	//按键类型
    		printf("code: %hd\n", ev.code);  //哪一个按键
    		printf("value: %d\n", ev.value);	//值
    		printf("\n");
    	}
    	
    	
    	// µÚ4²½£º¹Ø±ÕÉ豸
    	close(fd);
    	
    	return 0;
    }
    

2.4、gpiolib库

2.4.1、概念及接口
  1. gpiolib库的作用是对所有的gpio实行统一管理,因为驱动在工作的时候,会出现好几个驱动共同使用同一个gpio的情况,这样会造成混乱, 所以内核提供了一些方法来管理gpio资源

  2. 目录/sys/class/gpio/有很多gpio设备文件

在这里插入图片描述

  1. 文件目录drivers/gpio/gpiolib.c

  2. 接口函数

    1. gpio_request/gpio_free: 申请释放,返回0成功
    	参数gpio号,名字随便填
    2. gpio_request_one/gpio_request_array多个同时申请
    3. gpio_direction_input/gpio_direction_out:设置gpio输入/输出模式
    	gpio号,高低电平
    4. gpio_get_value/gpio_set_value:读取设置gpio的值
    	gpio号,高低电平
    5. gpiochip_is_request:判断是否占用gpio
    6. gpio_to_desc: 获取gpio实例
    7. gpio_to_irq:获取gpio中断号,方便注册中断
    
2.4.2、使用
  1. 查看gpio使用情况

    1. mount -t debugfs debugfs /tmp 挂载
      umount /tmp 卸载

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GcXUqRmf-1681025761685)(C:\Users\戴尔\AppData\Roaming\Typora\typora-user-images\1681005159803.png)]

    2. 需要进入管理员模式
      sudo su
      exit

    3. 使用cat gpio查看
      /tmp下面

    4. 查看输出,gpio readall

  2. 框架+gpio

#include <linux/module.h>       // module_init  module_exit
#include <linux/init.h>         // __init   __exit
#include <linux/fs.h>
#include <linux/leds.h>
#include <linux/io.h>
#include <linux/ioport.h>
#include <linux/gpio.h>

#define GPIO_LED1   17

#define X210_LED_OFF    0           // X210中LED是正极接电源,负极节GPIO
#define X210_LED_ON 1               // 所以1是灭,0是亮


static struct led_classdev mydev1;          // 定义结构体变量

// 这个函数就是要去完成具体的硬件读写任务的
static void s5pv210_led1_set(struct led_classdev *led_cdev,
                enum led_brightness value)
{
    printk(KERN_INFO "s5pv210_led1_set\n");

    //writel(0x11111111, GPJ0CON);

    // 在这里根据用户设置的值来操作硬件
    // 用户设置的值就是value
    if (value == LED_OFF)
    {
        // 用户给了个0,希望LED灭
        //writel(0x11111111, GPJ0CON);
        // 读改写三部曲
        //writel((readl(GPJ0DAT) | (1<<3)), GPJ0DAT);
        gpio_set_value(GPIO_LED1, X210_LED_OFF);
    }
    else
    {
        // 用户给的是非0,希望LED亮
        //writel(0x11111111, GPJ0CON);
        //writel((readl(GPJ0DAT) & ~(1<<3)), GPJ0DAT);
        gpio_set_value(GPIO_LED1, X210_LED_ON);
    }
}

static int __init s5pv210_led_init(void)
{
    // 用户insmod安装驱动模块时会调用该函数
    // 该函数的主要任务就是去使用led驱动框架提供的设备注册函数来注册一个设备
    int ret = -1;

    // 在这里去申请驱动用到的各种资源,当前驱动中就是GPIO资源
    if (gpio_request(GPIO_LED1, "led1_gpio_0"))
    {
        printk(KERN_ERR "gpio_request failed\n");
    }
    else
    {
        // 设置为输出模式,并且默认输出1让LED灯灭
        gpio_direction_output(GPIO_LED1, 1);
    }



    // led1
    mydev1.name = "led1";
    mydev1.brightness = 0;
    mydev1.brightness_set = s5pv210_led1_set;

    ret = led_classdev_register(NULL, &mydev1);
    if (ret < 0) {
        printk(KERN_ERR "led_classdev_register failed\n");
        return ret;
    }

    return 0;
}

static void __exit s5pv210_led_exit(void)
{
    led_classdev_unregister(&mydev1);
    gpio_free(GPIO_LED1);
}


module_init(s5pv210_led_init);
module_exit(s5pv210_led_exit);

// MODULE_xxx这种宏作用是用来添加模块描述信息
MODULE_LICENSE("GPL");                          // 描述模块的许可证
MODULE_AUTHOR("aston <1264671872@qq.com>");     // 描述模块的作者
MODULE_DESCRIPTION("s5pv210 led driver");       // 描述模块的介绍信息
MODULE_ALIAS("s5pv210_led");                    // 描述模块的别名信息

模型框架

在这里插入图片描述

3.1、概念

3.1.1、什么是设备驱动模型
  1. 设备驱动模型其实是Linux内核为了管理硬件上的设备和对应的驱动制定的一套软件体系
  2. 类(class)、总线(bus)、设备(device)、驱动(driver)、udev(自动创建设备节点和设备类)sysfs等都属于设备驱动模型的范畴
  3. 类(class)、总线(bus)、设备(device)、驱动(driver)
    都是 Linux 里面的一个结构体
    每一个结构体变量都能代表一个实例
  4. kobject和对象生命周期
    kobject:kernel object 处在最高层的模型,包含所有的模型。
    对象生命周期:有一种机制,让对象可以自动 free,自动释放内存空间。
  5. sysfs
    可以 cat 、echo
  6. udev
    自动创建设备节点和设备类。(class_creat 、 device_creat)
3.1.2、为什么需要设备驱动模型
  1. 设备驱动模型负责统一实现和维护一些特性,诸如:电源管理热插拔对象生命周期用户空间和驱动空间的交互、等基础设施
  2. 设备驱动模型目的是简化驱动程序编写,但是客观上设备驱动模型本身设计和实现很复杂。
  3. 代码重用。将对象抽象为总线、驱动、设备三种,各司其职。同一总线的多个驱动使用相同的总线对象。同一驱动可以关联驱动多个设备。
  4. 通过sysfs文件系统,清晰了展示内核驱动模型中的层次关系。同时sysfs文件系统还提供了方便的同用户控件交互的接口。
3.1.3、底层架构
  1. kobject结构体
    位置:Linux/kobject.h

  2. 这个结构体相当于一个总基类,被其他结构体所继承。
    所以其他结构体当中包含这个基类

    struct kobject {
        const char		*name; //kobject的名字,且作为一个目录的名字
        struct list_head	entry; //连接下一个kobject结构
        struct kobject		*parent; //指向父亲kobject结构体,如果父设备存在
        struct kset		*kset;  //指向kset集合
        struct kobj_type	*ktype;  //指向kobject的属性描述符
        struct sysfs_dirent	*sd;     //对应sysfs的文件目录
        struct kref		kref;   //kobject的引用计数
        unsigned int state_initialized:1; //表示该kobject是否初始化
        unsigned int state_in_sysfs:1;   //表示是否加入sysfs中
        unsigned int state_add_uevent_sent:1;
        unsigned int state_remove_uevent_sent:1;
        unsigned int uevent_suppress:1;
    };
    
  3. 各对象最基本单元,提供:每个对象,都会包含一个kobject结构体类

    1. 对象引用计数,自动释放功能
    2. 维护对象链表
    3. 对象上锁
    4. 用户空间表示
  4. 每一个在内核中注册的kobject对象都对应于sysfs文件系统中的一个目录(class目录),而不是文件。

  5. kobj_type结构体

    struct kobj_type {
    	void (*release)(struct kobject *kobj);//释放kobject和其占用的函数
    	const struct sysfs_ops *sysfs_ops;  //操作一个属性数组的方法
    	struct attribute **default_attrs;  //属性数组的方法
    	const struct kobj_ns_type_operations *(*child_ns_type)(struct kobject *kobj);
    	const void *(*namespace)(struct kobject *kobj);
    };
    
    1. 每一个kobject都需要绑定一个ktype来提供相应功能

    2. sysfs_ops,提供该对象在sysfs中的操作方法(show和store)

      struct sysfs_ops
      {
          ssize t (*show) (struct kobject *, struct attribute *, char *);/*读属性操作函数*/
          ssize t (*store) (struct kobject *,struct attribute *,const char *buf, size_t count);/*写属性操作函数*/
      }
      
    3. attribute,提供在sysfs中以文件形式存在的属性,其实就是应用接口

      struct attribute {
      	const char		*name;  //属性的名字
      	struct module		*owner;//指向用于该属性的模块,已经不常使用
      	mode_t			mode;  //属性读写权限
      };
      
  6. kset结构体

    struct kset {
    	struct list_head list;  //连接链表
    	spinlock_t list_lock;  //链表的自旋锁
    	struct kobject kobj;  //内嵌的kobject结构体,说明kset本身也是一个目录
    	const struct kset_uevent_ops *uevent_ops;  //热插拔事件
    };
    
    1. 同样也是被每一个kobject都需要绑定的结构体,但是 kset 当中包含kobject
    2. kset的主要作用是做顶层kobject的容器类
      用来装载 kobject
    3. kset的主要目的是将各个kobject(代表着各个对象)组织出目录层次架构
    4. 可以认为kset就是为了在sysfs中弄出目录,从而让设备驱动模型中的多个对象能够有层次有逻辑性的组织在一起

3.2、总线设备组织方式

3.2.1、总线
  1. 总线,管理设备和驱动2条分支

  2. sys/bus/里面device和driver

  3. bus_type结构体

    struct bus_type {
        const char		*name;   //总线类型名
        struct bus_attribute	*bus_attrs;  //总线属性和导出到sysfs中的方法
        struct device_attribute	*dev_attrs;  //设备属性和导出到sysfs中的方法
        struct driver_attribute	*drv_attrs;  //驱动程序属性和导出到sysfs中的方法
        //匹配函数,检验参数2中的驱动是否支持参数1中的设备
        int (*match)(struct device *dev, struct device_driver *drv);
        int (*uevent)(struct device *dev, struct kobj_uevent_env *env);
        int (*probe)(struct device *dev);  //探测设备
        int (*remove)(struct device *dev); //移除设备
        void (*shutdown)(struct device *dev); //关闭函数
        int (*suspend)(struct device *dev, pm_message_t state);//改变设备供电状态,使其节能
        int (*resume)(struct device *dev);  //恢复供电状态,使其正常工作
        const struct dev_pm_ops *pm;  //关于电源管理的操作符
        struct bus_type_private *p;  //总线的私有数据
    };
    
    1. 关键是 match 函数和 uevent 函数

      1. Linux系统内核使用 bus_type 结构体表示总线,例子:内核中的platform总线以及pci总线
      struct bus_type platform_bus_type = {
      	.name		= "platform",
      	.dev_groups	= platform_dev_groups,
      	.match		= platform_match,
      	.uevent		= platform_uevent,
      	.pm		= &platform_dev_pm_ops,
      };
      
      struct bus_type pci_bus_type = {
      	.name		= "pci",
      	.match		= pci_bus_match,
      	.uevent		= pci_uevent,
      	.probe		= pci_device_probe,
      	.remove		= pci_device_remove,
      	.shutdown	= pci_device_shutdown,
      	.dev_groups	= pci_dev_groups,
      	.bus_groups	= pci_bus_groups,
      	.drv_groups	= pci_drv_groups,
      	.pm		= PCI_PM_OPS_PTR,
      };
      
3.2.2、设备
  1. struct device 是硬件设备在内核驱动框架中的抽象

  2. device_register 用于向内核驱动框架注册一个设备

  3. 通常device不会单独使用,而是被包含在一个具体设备结构体中,如 struct usb_device

    是一个基类,会被其他类所继承。

  4. 设备驱动模型中的总线式设计中会包含设备和驱动两个

    platform总线:platform_deviceplatform_driver

    struct device {
        struct klist_klist children;/*连接子设备的链表*/
        struct device *parent;/*指向父设备的指针*/
        struct kobject kobj;/*内嵌的kobject结构体*/
        char bus_id[BUS ID SIZE];/*连接到总线上的位置*/ 
        unsigned uevent suppress:1;/*是否支持热插拔事件*/
        const char init_name;/*设备的初始化名字*/
        struct device_type *type;/*设备相关的特殊处理函数*/
        struct bus_type *bus;/*指向连接的总线指针*/
        struct device_driver *driver;/*指向该设备的驱动程序*/
        void *driver data;/*指向驱动程序私有数据的指针*/
        struct dev_pm info power;/*电源管理信息*/ 
        dev t deyt;/*设备号*/
        struct class *class;/*指向设备所属类*/ 
        struct attribute_group **groups;/*设备的组属性*/ 
        void (*release) (struct device *dev);/*释放设备描述符的回调函数*/
    }
    
  5. 设备属性

    struct device_attribute {
    	struct attribute	attr;
    	ssize_t (*show)(struct device *dev, struct device_attribute *attr,
    			char *buf);
    	ssize_t (*store)(struct device *dev, struct device_attribute *attr,
    			 const char *buf, size_t count);
    };
    
3.2.3、驱动
  1. struct device_driver 是驱动程序在内核驱动框架中的抽象

    struct device_driver {
    	const char		*name;//设备驱动程序的名字
    	struct bus_type		*bus;//指向驱动属于的总线
     
    	struct module		*owner;//设备驱动自身模块
    	const char		*mod_name;	/* used for built-in modules */
     
    	bool suppress_bind_attrs;	/* disables bind/unbind via sysfs */
     
    #if defined(CONFIG_OF)
    	const struct of_device_id	*of_match_table;
    #endif
     
    	int (*probe) (struct device *dev);//探测设备的方法,并检测设备驱动可以控制哪些设备
    	int (*remove) (struct device *dev);//移除设备调用的方法
    	void (*shutdown) (struct device *dev);//关闭设备的方法
    	int (*suspend) (struct device *dev, pm_message_t state);//设备处于低功耗的方法
    	int (*resume) (struct device *dev);//恢复正常的方法
    	const struct attribute_group **groups;//属性组
     
    	const struct dev_pm_ops *pm;//电源管理
     
    	struct driver_private *p;//设备驱动私有数据
    };
    
  2. name,驱动程序的名字,很重要,经常被用来作为驱动和设备的匹配依据

  3. probe,驱动程序的探测函数,用来检测一个设备是否可以被该驱动所管理

3.2.4、类
  1. 另一种管理设备和驱动的方式,和总线一样
    一个是/class下
    一个是/bus下
    管理的文件是同一个

  2. 相关结构体:struct classstruct class_device

  3. udev 的使用离不开class

    struct class {
    	const char		*name; //类名称
    	struct module		*owner;
     
    	struct class_attribute		*class_attrs; //class给自己添加的属性
    	const struct attribute_group	**dev_groups; //class给所包含的设备添加的属性
    	struct kobject			*dev_kobj;
     
    	int (*dev_uevent)(struct device *dev, struct kobj_uevent_env *env);
    	char *(*devnode)(struct device *dev, umode_t *mode);
     
    	void (*class_release)(struct class *class);
    	void (*dev_release)(struct device *dev);
     
    	int (*suspend)(struct device *dev, pm_message_t state); //设备休眠时调用
    	int (*resume)(struct device *dev);
     
    	const struct kobj_ns_type_operations *ns_type;
    	const void *(*namespace)(struct device *dev);
     
    	const struct dev_pm_ops *pm; //电源管理
     
    	struct subsys_private *p;
    };
    
  4. class的真正意义在于作为同属于一个class的多个设备的容器

三、块设备驱动

3.1、概念

  1. 块设备驱动支持缓冲区,字符不支持
  2. 块和字符是2种不同访问策略
    同一设备可以同时支持块和字符访问
    设备物理特性,觉得更适合那种访问
    块设备驱动更适合存储设备
  3. 块设备可以随机访问字符只能顺序访问
    nand,sd卡随机等同于顺序效率;硬盘等顺序效率高
  4. 扇区(sector)块的本身特性,512倍数
    块(block)内核对文件系统处理基本单位,多个扇区
    段(section)多个块组成
    页(page)映射管理基本单位,虚拟内存映射
    应用层对块访问,直接操作/dev/block,/dev/sda

3.2、框图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-so9kBIEq-1681025761686)(C:\Users\戴尔\AppData\Roaming\Typora\typora-user-images\1681008686793.png)]

  1. 虚拟文件系统vfs,把各种文件系统变成一类方便操作
  2. 结构体
    request,对设备的操作读写
    request_queue,操作队列
    bio,管理一个请求
    gendisk,表示一个磁盘设备或分区

3.3、实践

  1. 查看驱动
    lsmod
    ls /dev
    cat /proc/devices
    cat /proc/partitions
  2. 格式化,挂载
    mkfs.ext2 /dev/xxx
    mount -t ext2 /dev/xxx /xxx
    unmount /xxx
    之后正常操作目录
  3. 驱动代码
    1. 使用第一种实现路线
      虚拟出来的设备
    2. 安装
      1. register_blkdev
        注册主设备驱动
      2. 实例化
        gendlisk
        设备结构体
        1. 结构体里有
          主设备号,等待队列,operation结构体,dev下的文件名,等
      3. 实例化
        request_queue
        等待队列
        1. blk_init_queue
          参数1,回调函数,处理request请求
          参数2,自旋锁,会提供给等待队列
      4. set_capacity
        设置设备容量
      5. add_disk
        注册
    3. 处理request回调函数
      blk_fetch_queue
      从队列中取出一个请求
      然后判断是读还是写请求,操作硬件
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/errno.h>
#include <linux/interrupt.h>
#include <linux/mm.h>
#include <linux/fs.h>
#include <linux/kernel.h>
#include <linux/timer.h>
#include <linux/genhd.h>
#include <linux/hdreg.h>
#include <linux/ioport.h>
#include <linux/init.h>
#include <linux/wait.h>
#include <linux/blkdev.h>
#include <linux/blkpg.h>
#include <linux/delay.h>
#include <linux/io.h>
#include <asm/system.h>
#include <asm/uaccess.h>
#include <asm/dma.h>


#define RAMBLOCK_SIZE (1024*1024)   			// 1MB,2048扇区

static struct gendisk *my_ramblock_disk;		// 磁盘设备的结构体
static struct request_queue *my_ramblock_queue;	// 等待队列
static DEFINE_SPINLOCK(my_ramblock_lock);
static int major;
static unsigned char *my_ramblock_buf;			// 虚拟块设备的内存指针


static void do_my_ramblock_request(struct request_queue *q)
{

	struct request *req;
	static int r_cnt = 0; 			//实验用,打印出驱动读与写的调度方法
	static int w_cnt = 0;
	
	req = blk_fetch_request(q);
	
	while (NULL != req)
	{
		unsigned long start = blk_rq_pos(req) *512;
		unsigned long len = blk_rq_cur_bytes(req);
		
		if(rq_data_dir(req) == READ)
		{
			// 读请求
			memcpy(req->buffer, my_ramblock_buf + start, len); 	//读操作,
			printk("do_my_ramblock-request read %d times\n", r_cnt++);
		}
		else
		{
			// 写请求
			memcpy( my_ramblock_buf+start, req->buffer, len); 	//写操作
			printk("do_my_ramblock request write %d times\n", w_cnt++);
		}

		if(!__blk_end_request_cur(req, 0)) 
		{
			req = blk_fetch_request(q);
		}
	}
}


static int blk_ioctl(struct block_device *dev, fmode_t no, unsigned cmd, unsigned long arg)
{
	return -ENOTTY;
}

static int blk_open (struct block_device *dev , fmode_t no)
{
	printk("11111blk mount succeed\n");
	return 0;
}
static int blk_release(struct gendisk *gd , fmode_t no)
{
	printk("11111blk umount succeed\n");
	return 0;
}

static const struct block_device_operations my_ramblock_fops =
{
	.owner 		= THIS_MODULE,
	.open 		= blk_open,
	.release 	= blk_release,
	.ioctl 		= blk_ioctl,
};

static int my_ramblock_init(void)
{
	major = register_blkdev(0, "my_ramblock");
	if (major < 0)
	{
		printk("fail to regiser my_ramblock\n");
		return -EBUSY;
	}
	
	// 实例化
	my_ramblock_disk = alloc_disk(1);		//次设备个数 ,分区个数 +1
	
	//分配设置请求队列,提供读写能力
	my_ramblock_queue = blk_init_queue(do_my_ramblock_request, &my_ramblock_lock);
	//设置硬盘属性 
	my_ramblock_disk->major = major;
	my_ramblock_disk->first_minor = 0;
	my_ramblock_disk->fops = &my_ramblock_fops;
	sprintf(my_ramblock_disk->disk_name, "my_ramblcok");		// /dev/name
	my_ramblock_disk->queue = my_ramblock_queue;
	set_capacity(my_ramblock_disk, RAMBLOCK_SIZE / 512);
	/* 硬件相关操作 */
	my_ramblock_buf = kzalloc(RAMBLOCK_SIZE, GFP_KERNEL);
	add_disk(my_ramblock_disk);				// 向驱动框架注册一个disk或者一个partation的接口
	
	return 0;
}


static void my_ramblock_exit(void)
{
	unregister_blkdev(major, "my_ramblock");
	del_gendisk(my_ramblock_disk);
	put_disk(my_ramblock_disk);
	blk_cleanup_queue(my_ramblock_queue);
	kfree(my_ramblock_buf); 
}

module_init(my_ramblock_init);
module_exit(my_ramblock_exit);

MODULE_LICENSE("GPL"); 
  • 2
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
作为一个初学者,你可以按照以下路线来学习Linux驱动开发: 1. 学习Linux基础知识:熟悉Linux操作系统的基本概念、文件系统、进程管理等。 2. 掌握C语言编程:作为Linux驱动开发的主要语言,你需要熟悉C语言的基本语法和编程技巧。 3. 理解Linux设备模型:学习Linux的设备模型,包括字符设备、块设备、网络设备等,了解设备驱动程序的基本原理。 4. 学习Linux内核:深入了解Linux内核的架构、模块机制、调度器等,熟悉内核的编译、调试和配置方法。 5. 开发简单的字符设备驱动:从简单的字符设备驱动入手,了解设备驱动程序的框架和基本操作,例如设备注册、数据传输等。 6. 开发块设备驱动:深入学习块设备驱动程序的实现原理,包括磁盘分区、缓存机制等。 7. 学习Linux驱动框架:掌握常用的Linux驱动框架,例如Platform驱动、USB驱动、I2C驱动等。 8. 调试和优化驱动程序:学习使用调试工具和技术,例如GDB、Kprobe等,以及优化驱动程序的性能和稳定性。 9. 了解特殊设备的驱动开发:学习针对特殊设备的驱动开发,例如网络设备驱动、显卡驱动等。 10. 深入研究内核源码:阅读和理解Linux内核的源码,通过实践和探索进一步提升自己的驱动开发能力。 记住,Linux驱动开发是一个复杂而庞大的领域,需要不断学习和实践。通过综合运用这些知识和经验,你可以逐步成为一名优秀的Linux驱动程序员。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

dz小伟

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值