驱动开发--汇总-程序员宅基地

技术标签: ubuntu  linux  服务器  驱动开发  

一,【驱动相关概念】

1,什么是驱动

能够驱使硬件实现特定功能的软件代码
根据驱动程序是否依赖于系统内核将驱动分为裸机驱动和系统驱动

2,裸机驱动和系统驱动的区别

裸机驱动:编写的驱动代码中没有进行任何内核相关API的调用,开发者自己配置寄存器完成了相关硬件控制的代码编写。
裸机驱动不依赖于系统内核,由开发者独立即可完成,但是裸机驱动实现的硬件控制工作相对而言比较简单

系统驱动:系统驱动指的是编写的驱动代码中需要调用系统内核中提供到的各种API,驱动最终也会加载到系统内核生效。
系统驱动开发者无法独立完成,需要依赖于系统内核,基于系统驱动实现的硬件功能也更加复杂

3,系统驱动在系统中的层次

1,操作系统的功能

向下管理硬件,向上提供接口

接口类型:
文件管理
内存管理
进程管理
网络管理
设备管理 (设备驱动的管理):linux设备驱动是属于设备管理功能的一部分,它的作用是丰富系统内核的设备管理功能

2,进程上下文的切换

当进程进行系统调用时,进程访问到的资源从用户空间切换到了内核空间,叫做上下文的切换

文件IO通过系统调用实现; 
标准IO通过库函数实现,标准IO = 缓冲区 + 系统调用,当缓冲区刷新时会进行系统调用
缓冲区刷新:
    行缓存(终端相关的stdin,stdout)6种:遇到换行符,关闭文件指针,程序结束,手动调用fflush函数,缓冲区满,输入输出切换
    全缓存(自定义文件指针) 5种:                    关闭文件指针,程序结束,手动调用fflush函数,缓冲区满,输入输出切换
    不缓存(终端相关的stderr) 无:

3,linux设备驱动的分类

字符设备:能够以字节流的形式进行顺序访问的设备叫做字符设备(90%)ex:鼠标、键盘、lcd...
块设备:能够以块(512字节)为单位进行随机访问的设备叫做块设备。(磁盘)
网卡设备:进行网络通信时使用网卡设备实现。网卡设备数据的读取要基于套接字来实现

二,【linux内核模块编程】

1,内核模块的意义

不同于应用程序,驱动是加载到内核空间中的,所以需要按照内核模块的编程框架编写驱动代码

2,内核模块三要素

入口:安装内核模块时执行,主要负责资源的申请工作
出口:卸载内核模块时执行,主要负责资源的释放工作
许可证:声明内核模块遵循GPL协议

3,内核模块的编译

命令: make modules
方式:
    内部编译(静态编译):需要依赖于内核源码树进行编译
        将编写的内核模块源码存放到linux内核指定目录下
        修改该目录下的kconfig文件,添加当前模块文件的选配项
        执行make menuconfig,将当前内核模块源码的选配项选配为【M】
        执行make menuconfig进行模块化编译
    外部编译(动态编译):不需要依赖于内核源码树,在编译时只需要编译当前内核模块文件即可,外部编译需要自己手写当前内核模块编译的Makefile

4,操作内核模块的安装,卸载,查看命令

安装                  insmod ***.ko
查看已经安装的内核模块  lsmod
卸载内核模块            rrmod ***
查看内核模块相关信息    modinfo ***.ko

三,【打印函数printk】

1,使用格式

printk("格式控制符",输出列表);//按照默认的输出级别输出内容
或者
printk(消息输出级别 "格式控制符",输出列表);//让消息按照指定的级别进行输出

2,消息输出级别相关

printk输出的内容属于内核的消息,一般内核的消息有重要的,也有相对不重要的,我们现在想要将比较重要的消息输出到终端,不重要的消息不在终端进行输出。做法是将输出的消息设置为不同的输出级别,终端会有一个默认的级别,只有输出消息的级别高于终端的默认级别,消息才可以在终端输出。printk消息级别分为0-7级共8级,其中数字越小表示级别越高,常用的消息级别是3-7级。

#define KERN_EMERG    KERN_SOH "0"    /* system is unusable */
#define KERN_ALERT    KERN_SOH "1"    /* action must be taken immediately */
#define KERN_CRIT    KERN_SOH "2"    /* critical conditions */
#define KERN_ERR    KERN_SOH "3"    /* error conditions */
#define KERN_WARNING    KERN_SOH "4"    /* warning conditions */
#define KERN_NOTICE    KERN_SOH "5"    /* normal but significant condition */
#define KERN_INFO    KERN_SOH "6"    /* informational */
#define KERN_DEBUG    KERN_SOH "7"    /* debug-level messages */

查看消息默认级别
    终端输入 cat /proc/sys/kernel/printk查看
    4            4              1                      7
    终端默认级别  消息默认级别   终端支持的消息最高级别   终端支持消息的最低级别

修改消息默认级别 
注意:一旦重启,消息默认级别会被重置为修改之前的数值
Ubuntu:
    sudo su//切换到管理员模式
    echo 4 3 1 7 > /proc/sys/kernel/printk
开发板
    修改 ~/nfs/rootfs/etc/init.d/rcS
    在这个文件最后添加一行:echo 4 3 1 7 > /proc/sys/kernel/printk,加上这行不用每次都改了

3,ubuntu虚拟终端

ubuntu由于官方的限制,无论内核消息级别有多高,消息都无法在终端正常显示,此时可以切换到虚拟终端进行消息的显示

切换到虚拟终端方式
    ctrl+alt+[f2-f6](fn)
退出虚拟终端
    ctrl+alt+f1(fn)

4,dmesg命令

功能:输出从内核启动到当前时刻开始所有的打印消息
dmesg -c/dmesg -C:清除当前dmesg的buf中保存的所有打印消息

四,【linux内核模块传参】

什么是内核模块传参
    内核模块传参指的是在安装内核模块时在命令行给内核模块中的变量传递数值
    ex:  insmod demo.ko  a=100  //在安装内核模块的时候给变量a进程传递数值
内核模块传参的意义
    通过内核模块传参的使用,我们可以对内核模块中的一些属性进行修改,让当前的内核模块向下适配多种不同的硬件,向上也可以兼容各自复杂的应用程序

API

module_param(name, type, perm)
功能:声明可以进行命令行传参的变量信息
参数:
    name:要进行命令行传参的变量名
    type:要进行命令行传参的变量类型
           / * Standard types are:
           byte(单字节类型), hexint, short, ushort, int, uint, long, ulong
           charp:     a character pointer(char *)
           bool:      a bool, values 0/1, y/n, Y/N.
           invbool:   the above, only sense-reversed (N = true).
           */
    perm:文件权限,当使用module_param函数声明要传参的变量时,会在/sys/module/当前内核模块名/parameters/目录下生成一个以当前变量名为名的文件,文件的权限就是perm和文件权限掩码运算得到,文件的数值时变量的值

MODULE_PARM_DESC(变量名, 对变量的描述)
功能:添加对要传参的变量的描述,这个描述可以通过modinfo ***.ko查看到

注意:
    1.如果给char类型的变量进行传参的话,要传递字符的十进制形式
    2.如果传参的类型是一个char *类型,传递的字符串中间不要有空格

五,【内核的导出符号表】

内核导出符号表的意义
    实现不同模块之间资源的相互访问,构建模块之间的依赖关系
    内核模块都是加载到同一个内核空间,所以模块2想要访问模块1里的资源,只需要模块1将自己资源的符号表导出,模块2借助模块1的符号表即可以访问模块1的资源

API
    EXPORT_SYMBOL_GPL(变量名|函数名) ,模块2中调用改函数即可

编译模块
    先编译模块1,将模块1编译生成的符号表文件Module.symvers拷贝到模块2的目录下,再编译模块2

注意:
    在新版本内核中不支持符号表文件的复制了,如果模块2想要访问模块1,将模块1的符号表文件直接复制到模块2的路径下,编译模块2,会报未定义错误,
    解决方法:在模块2的Makefile中指定模块1的符号表文件路径
        KBUILD_EXTRA_STMBOLS += /home/ubuntu/23051班驱动/day2/1/Module.symvers

安装&卸载
    因为模块2和模块1构成依赖关系,所以先安装模块1,再安装模块2,先卸载模块2,再卸载模块1

六,【字符设备驱动】

框架图

1,字符设备驱动的注册和注销相关API

注册:
   int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops)
功能:实现字符设备驱动的注册(申请了一定数量(256)的设备资源)
参数:
    major:驱动的主设备号
        ==0:动态申请主设备号
        >0:静态指定一个主设备号
            //次设备号有256个,范围是(0-255)
    name:驱动名字
    fops:操作方法结构体指针
        struct file_operations {
            int (*open) (struct inode *, struct file *);
            ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
            ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
            int (*release) (struct inode *, struct file *);
            };
返回值:失败返回错误码
    成功:
        major==0,返回申请得到的主设备号
        major>0:返回0
        //可以通过 cat /proc/devices查看已经注册成功的驱动的名字以及主设备号

注销 
    void unregister_chrdev(unsigned int major, const char *name)
功能:注销字符设备驱动
参数:
    major:注册时申请的主设备号
    name:注册时填写的名字
返回值:无

2,copy_to_user & copy_from_user 用户和内核之间数据拷贝API

1.long copy_to_user(void __user *to, const void *from, unsigned long n)
    功能:实现内核空间数据向用户空间拷贝
    参数:
        to:用户空间存放拷贝来的数据的buf首地址
        from:内核空间存放要拷贝的数据的buf首地址
        n:要拷贝的数据大小
    返回值:成功返回0失败返回未拷贝的字节数

2.long copy_from_user(void *to, const void __user *from, unsigned long n)
    功能:实现用户空间数据向内核空间拷贝
    参数:
        to:内核空间存放拷贝来的数据的buf首地址
        from:用户空间存放要拷贝的数据的buf首地址
        n:要拷贝的数据大小
    返回值:成功返回0失败返回未拷贝的字节数

3,ioremap物理内存映射虚拟内存API

想要实现硬件的控制,需要对硬件相关的寄存器进行控制,而寄存器对应的内存属于物理内存,驱动是加载虚拟内存上的,想要在驱动中操作硬件寄存器,需要将寄存器对应的物理内存映射为虚拟内存,操作对应的虚拟内存即可控制硬件。
1.void  *ioremap(unsigned long port, unsigned long size)
    功能:映射指定大小的物理内存为虚拟内存
    参数:
        port:要映射的物理内存首地址
        size:映射的物理内存大小
    返回值:成功返回映射得到的虚拟内存首地址,失败返回NULL

2.void iounmap(volatile void __iomem *addr)
    功能:取消物理内存映射
    参数:
        addr:虚拟内存首地址
    返回值:无

七,【手动 / 自动创建设备节点(设备文件)】

1,创建设备文件的机制

mknod命令:手动创建设备节点的命令:
    mknod /dev/mychrdev c 241 0
        解释:
        mknod:创建设备文件的命令码
        /dev/mychrdev:创建的设备文件的名字以及路径
        c:设备文件类型为字符设备文件  b表示块设备文件
        241:主设备号
        0:次设备号(0-255)

devfs:可以用于创建设备节点,创建设备节点的逻辑在内核空间(内核2.4版本之前使用)
udev:自动创建设备节点的机制,创建设备节点的逻辑在用户空间(从内核2.6版本一直使用至今)
mdev:是一种轻量级的udev机制,用于一些嵌入式操作系统中

2,udev自动创建节点过程分析

1,注册驱动,register_chrdev()函数
2,获取设备信息(设备树相关文件,目前为指定寄存器地址)
3,创建一个设备类(向上提交目录信息),会在内核中申请一个struct class对象,并且初始化,此时会在/sys/class/目录下创建一个以类名为名的目录
4,创建一个设备对象(向上提交设备节点信息),会在内核中申请一个struct device对象,并且初始化,此时会在上一步创建好的目录下创建存放设备节点信息的文件
5,当创建好存放设备节点信息的文件后,内核会发起hotplug event事件,激活用户空间的hotplug进程
6,hotplug进程激活后,会通知udev进程在刚创建的存放设备节点信息的文件中查询设备节点相关信息
7,udev查询设备节点相关信息后,会在/dev目录下创建设备节点

3,udev创建设备节点时使用的API

1.向上提交目录信息
struct class * class_create(struct module *owner,const char *name );
    功能:申请一个设备类并初始化,向上提交目录信息
    参数:
        owner:指向当前内核模块自身的一个模块指针,填写THIS_MODULE
        name:向上提交的目录名
    返回值:成功返回申请的struct class对象空间首地址,失败返回错误码指针
  
2.销毁目录
void class_destroy(struct class *cls)
    功能:销毁目录信息
    参数:cls:指向class对象的指针
    返回值:无

3.向上提交节点信息
struct device *device_create(struct class *class, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...)
    功能:创建一个设备对象,向上提交设备节点信息
    参数:
        cls:向上提交目录时的到的类对象指针
        parent:当前申请的对象前一个节点的地址,不知道就填 NULL
        devt:设备号    主设备号<<20|次设备号
        dridata:申请的device对象的私有数据,填写NULL
        fmt:向上提交的设备节点名
        ...:不定长参数   
    返回值:成功返回申请到的device对象首地址,失败返回错误码指针,指向4K预留空间

4.销毁设备节点信息
void device_destroy(struct class *class, dev_t devt)
    功能:销毁设备节点信息
    参数:
        class:向上提交目录时得到的类对象指针
        devt:向上提交设备节点信息时提交的设备号
    返回值:无

错误相关
    在内核空间最顶层预留4K空间,当struct class函数调用失败后函数会返回一个指向这4K空间的指针
bool __must_check IS_ERR(__force const void *ptr)
    功能:判断指针是否指向4K预留空间
    参数:要判断的指针
    返回值:如果指着指向4K预留空间返回逻辑真,否则返回逻辑假

long __must_check PTR_ERR(__force const void *ptr)
     功能:通过错误码指针得到错误码
         ex:struct class_create *cls=struct class_create(THIS_MODULE,"mycdev");
         if(IS_ERR(cls))
         {
             printk("向上提交目录失败\n");
             return -PRT_ERR(cls);     
         }

获取设备号相关
        MKDEV(主设备号,次设备号):根据主设备号和次设备号得到设备号
        MAJOR(dev):根据设备号获取主设备号
        MINOR(dev):根据设备号获取次设备号

八,【ioctl硬件控制函数】

使用ioctl函数的意义
    linux有意将对硬件的控制划分到不同的系统调用来实现,让read()/write()函数专注于数据的读写,至于对于硬件不同控制功能的选择我们交给ioctl函数来实现。比如在串口通信时让read()/write()进行正常数据读写,至于设置波特率和数据位宽等交给ioctl进行选择控制

ioctl函数分析
*********系统调用函数分析***********

int ioctl(int fd, unsigned long request, ...);
    功能:进行IO选择控制
    参数:
        fd:文件描述符
        request:要进行的功能控制的功能码
        ...:可以写也可以不写,如果写的话传递一个整型变量或者一个地址
    返回值:成功返回0,失败返回错误码

*********驱动中的ioctl操作方法************

    当应用程序中调用ioctl函数时,驱动中的ioctl操作方法被回调
long (*unlocked_ioctl) (struct file *file, unsigned int cmd, unsigned long arg)
{
    
    参数分析:
        file:文件指针
        cmd:功能码,由ioctl第二个参数传递得到
        arg:由ioctl第三个参数传递得到 
}


功能码解析:
    一个ioctl的功能码是一个32位的数值,尽量保证每一个硬件不同功能的功能码都不一样,所以我们需要对功能码进行编码
    查询内核帮助手册:~/linux-5.10.61/Documentation/userspace-api/ioctl
    vi ioctl-decoding.rst
 ====== ==================================
 31-30    00 - no parameters: uses _IO macro
    10 - read: _IOR
    01 - write: _IOW
    11 - read/write: _IOWR

 29-16    size of arguments

 15-8    ascii character supposedly
    unique to each driver

 7-0    function #
 ====== ==================================
31-30:读写方向位
29-16:ioctl第三个参数的大小
15-8:设备的标识,通常用‘a’-‘z’的表示
7-0:功能位,自己设定

构建功能码的API
#define _IO(type,nr)        _IOC(_IOC_NONE,(type),(nr),0)
#define _IOR(type,nr,size)    _IOC(_IOC_READ,(type),(nr),sizeof(size))
#define _IOW(type,nr,size)    _IOC(_IOC_WRITE,(type),(nr),sizeof(size))
#define _IOWR(type,nr,size)    _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),sizeof(size)
例:
//构建LED开关的功能码,不添加ioctl第三个参数
#define  LED_ON _IO('l',1)
#define  LED_OF _IO('l',0)
//构建LED开关的功能码,添加ioctl第三个参数int
#define LED_ON _IOW('l',1,int)
#define LED_OFF _IOW('l',0,int) 
第三个参数通常填指针类型

九,【字符设备驱动的内部实现】

1,字符设备驱动内部注册过程

通过对register_chrdev内部的实现过程进行分析,其实注册字符设备驱动的过程就是下面几步:
    1.分配struct cdev对象空间
    2.初始化struct cdev对象
    3.注册cdev对象
完成上面的三步,就完成了字符设备驱动的注册。

2,注册字符设备驱动分步实现相关API分析

*************注册过程**********
1.分配 字符设备驱动对象
    a.struct cdev cdev;
    b.struct cdev *cdev = cdev_alloc();
    /*
        struct cdev *cdev_alloc(void)
        功能:申请一个字符设备驱动对象空间
        参数:无
        返回值:成功返回申请的空间首地址
        失败返回NULL
    */
 2.字符设备驱动对象初始化
     void cdev_init(struct cdev *cdev, const struct file_operations *fops)
     功能:实现字符设备驱动的部分初始化
     参数:
         cdev:字符设备驱动对象指针
         fops:操作方法结构体指针
    返回值:无
3.设备号的申请
    3.1 静态指定设备号
    int register_chrdev_region(dev_t from, unsigned count, const char *name)
        功能:静态申请设备号并注册一定数量的设备资源
        参数:
            from:静态指定的设备号(第一个设备的设备号)
            count:申请的设备数量
            name:设备名或者驱动名
        返回值:成功返回0,失败返回错误码
    3.2 动态申请设备号
    int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
        功能:动态申请设备号并注册一定数量的设备资源
        参数:
            dev:存放申请的到的设备号的空间首地址
            baseminor:次设备号的起始值
            count:申请的设备资源数量
            name:设备名或者驱动名
         返回值:成功返回0,失败返回错误码   
 4.根据申请的设备号和驱动对象注册驱动
     int cdev_add(struct cdev *p, dev_t dev, unsigned count)
     功能:注册字符设备驱动对象
     参数:
         cdev:字符设备驱动对象指针
         dev:申请的设备号的第一个值
         count:申请的设备资源的数量
    返回值:成功返回0,失败返回错误码

***********注销过程*****************
1.注销驱动对象
void cdev_del(struct cdev *p)
    参数:
        p:要注销的对象空间指针
    返回值:无

2.释放申请的设备号和设备资源
    void unregister_chrdev_region(dev_t from, unsigned count)
    参数:
        from:申请的第一个设备号
        count:申请的设备资源的数量
    返回值:无

3.释放字符设备驱动对象空间
    void kfree(void *addr)
    功能:释放申请的内核空间
    参数:要释放的空间首地址
    返回值:无
    

3,struct cdev 驱动描述相关信息结构体

只要一个驱动存在于系统内核中,就会存在一个struct cdev对象,对象中是关于当前驱动的相关描述信息

struct cdev {
    struct kobject kobj;//基类对象
    struct module *owner;//模块对象指针  THIS_MODULE
    const struct file_operations *ops;//操作方法结构体指针
    struct list_head list;//用于构成链表的成员
    dev_t dev;//第一个设备号  
    unsigned int count;//设备资源数量
    ...
};

4,struct inode 操作系统中文件相关信息结构体

只要文件存在于操作系统上,那么在系统内核中就一定会存在一个struct inode结构体对象用来描述当前文件的相关信息

struct inode {
    umode_t            i_mode;//文件的权限
    unsigned short        i_opflags;
    kuid_t            i_uid;//文件的用户ID
    kgid_t            i_gid;//组ID
    unsigned int        i_flags;
    dev_t            i_rdev;//设备号
    union {
        
        struct block_device    *i_bdev;//块设备
        struct cdev        *i_cdev;//字符设备
        char            *i_link;
        unsigned        i_dir_seq;
    };

5,open函数回调驱动中操作方法open的路线

6,struct file 进程中打开的文件相关信息结构体

    open函数的第一个参数是文件路径,可以进而找到inode对象,从而回调到驱动的方法,但是read()\write()这些函数操作对象不是文件的路径,而是文件描述符,那么如何通过文件描述符回调到驱动的操作方法?
文件描述符是什么?
    文件描述符是在一个进程里面打开文件时得到的一个非负整数,一个进程里最多可以有1024个文件描述符。不同的进程的文件描述符独立的。文件描述符依赖于进程存在。想要探究文件描述符的本质,就要知道文件描述符在进程中的作用,
    通过分析struct task_struct结构体,fd_array是一个指针数组,数组中每一个成员都指向一个struct file类型的对象,而数组的下标就是我们常说的 文件描述符

struct file结构体分析
只要在一个进程里面打开一个文件,在内核中就会存在一个struct file对象,用来描述打开的文件相关的信息
struct file {
  struct path        f_path;//文件路径
    struct inode        *f_inode;    /* cached value */
    const struct file_operations    *f_op;//操作方法结构体
    unsigned int         f_flags;//open函数的第二个参数赋值给f_flags
    fmode_t            f_mode;//打开的文件的权限
    void            *private_data;//私有数据,可以实现函数件数据的传递

        };
    

7,struct task_sturct进程相关信息结构体

只要一个进程存在于操作系统上,在系统内核中一定会存在一个struct task_struct结构体对应保存进程的相关信息
struct task_struct {
    volatile long            state;//进程状态
    int             on_cpu;//表示进程在哪个CPU上执行
    int                prio;//进程优先级
    pid_t                pid;//进程号
    struct task_struct __rcu    *real_parent;//父进程
    struct files_struct        *files;//打开的文件相关结构体

};

struct files_struct {
     
        struct file __rcu * fd_array[NR_OPEN_DEFAULT];//结构体指针数组
        };
  fd_array是一个指针数组,数组中每一个成员都指向一个struct file类型的对象,而数组的下标就是我们常说的 文件描述符

8,通过文件描述符回调驱动操作方法的路线

9,设备文件和设备的绑定

int mycdev_open(struct inode *inode, struct file *file)
{
    int min=MINOR(inode->i_rdev);  //根据打开的文件对应的设备号获取次设备号
    file->private_data=(void *)min; //将次设备号传递给file的私有数据
    printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
    return 0;
}

long mycdev_ioctl(struct file *file, unsigned int cmd, unsigned long arg){
   int min=(int)file->private_data; //将file的私有数据保存的私有数据取出
    switch (min){
        case 0://控制LED1
            switch(cmd){
                case LED_ON:
                    //开灯
                    break;
                case LED_OFF:
                    //关灯
                    break;                                
            }
            break;
    case 1://控制LED2
    case 2://控制LED3
    }
    return 0;
}

十,【linux内核中的并发和竞态】

1,linux内核中产生的原因

表面原因
    多个进程同时访问同一个驱动资源,就会出现对资源争抢的情况
本质原因
    单核处理器,如果支持资源抢占,就会出现竞态
    对于多核处理,核与核之间本身就会出现资源争抢的情况
    对于中断和进程,会出现竞态
    对于中断和中断之间,如果中断控制器支持中断嵌套,则会出现竞态,否则不会。ARM芯片使用的中断控    制器是GIC,gic不支持中断嵌套

2,竞态解决方法

1,中断屏蔽(了解)

    中断屏蔽是针对于单核处理器实现的竞态解决方案,如果进程想要访问临界资源,可以在访问资源之前先将中断屏蔽掉,当进程访问临界资源结束后在恢复中断的使能。一般屏蔽中断的时间要尽可能短,长时间屏蔽中断可能会导致用户数据的丢失甚至内核的崩溃。一般中断屏蔽仅仅留给内核开发者测试使用。
    local_irq_disable()//中断屏蔽
    临界资源
    local_irq_enable()//取消中断屏蔽

 2,自旋锁

    一个进程想要访问临界资源,首先要获取自旋锁,如果获取自旋锁成功,就访问临界资源,如果获取自旋锁失败,进程会进入自旋状态,自旋锁又被成为盲等锁
特点
    自旋状态下的进程处于运行态,时刻需要消耗CPU的资源
    自旋锁保护的临界资源尽可能的小,临界区中不能有延时、耗时甚至休眠的操作,也不可以有copy_to_user和copy_from_user
    自旋锁会出现死锁现象
    自旋锁既可以用于进程的上下文,也可以用于中断的上下文
    自旋锁使用时会关闭抢占//尽量保证上锁的时间尽可能的短
API
1.定义自旋锁
    spinlock_t lock;
2.初始化自旋锁
    spin_lock_init(&lock);
3.上锁(获取锁)
    void spin_lock(spinlock_t *lock)
4.解锁(释放锁)
    void spin_unlock(spinlock_t *lock)

 3,信号量

    一个进程想要访问临界资源,先要获取信号量,如果获取不到,进程就切换到休眠状态
特点
    获取不到信号量的进程会切换到休眠状态休眠状态下的进程不消耗CPU的资源,进程状态的切换需要消耗CPU资源
    信号量保护的临界区可以很大,也可以有延时、耗时、休眠的操作
    信号量不会出现死锁
    信号量只能用于进程上下文
    信号量不会关闭抢占
API
1.定义一个信号量
    struct semaphore sema;
2.初始化信号量
    void sema_init(struct semaphore *sem, int val)
    参数:
        sem:信号量指针
        val:给信号量的初始值
3.获取信号量(上锁)
    void down(struct semaphore *sem)//信号量数值-1
4.释放信号量(解锁)
    void up(struct semaphore *sem);

 4,互斥体

    一个进程想要访问临界资源需要先获取互斥体,如果获取不到,进程会切换到休眠状态
特点
    获取不到互斥体的进程会切换到休眠状态休眠状态下的进程不消耗CPU的资源,进程状态的切换需要消耗CPU资源
    互斥体保护的临界区可以很大,也可以有延时、耗时、休眠的操作
    互斥体不会出现死锁
    互斥体只能用于进程上下文
    互斥体不会关闭抢占
    获取不到互斥体的进程不会立即进入休眠状态,而是稍微等一会儿,互斥体的效率要比信号量更高
API
1.定义互斥体
    struct mutex mutex;
2.初始化互斥体
    mutex_init(&mutex);
3.上锁
    void  mutex_lock(struct mutex *lock)
4.解锁
    void  mutex_unlock(struct mutex *lock)

5,原子操作

    将进程访问临界资源的过程看作一个不可分割的原子状态。原子状态的实现通过修改原子变量额数值来实现,而原子变量数值的修改再内核里面是通过内联汇编来完成的。

API
1.定义原子变量并且初始化
    atomic_t atm=ATOMIC_INIT(1);//将原子变量的数值初始化为1
2.int atomic_dec_and_test(atomic_t *v)
    功能:将原子变量的数值-1并且和0比较
    参数:
        v:原子变量的指针
    返回值:如果原子变量-1后结果为0,则返回真,否则返回假
3.void atomic_inc(atomic_t *v)
    功能:原子变量的数值+1
***********************************
或者相反的-1
1.定义原子变量并且初始化
    atomic_t atm=ATOMIC_INIT(-1);//将原子变量的数值初始化为-1
2.int atomic_inc_and_test(atomic_t *v)
    功能:将原子变量的数值+1并且和0比较
    参数:
        v:原子变量的指针
    返回值:如果原子变量-1后结果为0,则返回真,否则返回假
3.void atomic_dec(atomic_t *v)
    功能:原子变量的数值+1

十一,【IO模型】

什么是IO模型?为什么要设计不同的IO模型
    IO模型就是对文件的不同读写方式。
    在驱动中对硬件数据的读写需要通过读写设备文件来实现,而读取设备文件根据需求也有不同的方式,所以在这里我们要研究不同的IO模型的实现。
    IO模型分为非阻塞IO、阻塞IO、IO多路复用、信号驱动IO。

    read/write 是否阻塞跟 open的打开方式有关,通常为阻塞方式打开,打开文件时添加O_NONBLOCK可以实现非阻塞方式
所以在驱动程序中可以通过标志位判断是否为阻塞方式

1,非阻塞IO

非阻塞IO
    当进程通过read()读取硬件数据时,不管数据是否准备好,read函数立即返回。通过非阻塞IO,read函数有可能读到的数据不是本次准备好的数据。在打开文件时可以添加O_NONBLOCK flag来实现文件的非阻打开

***********应用程序************
int fd=open("/dev/mycdev",O_RDWR|O_NONBLOCK);//以非阻塞的模式打开文件
read(fd,buf,sizeof(buf));
*********驱动程序**************
ssize_t mycdev_read(struct file *file, char *ubuf, size_t size, loff_t *lof)
{
    int ret;
      if(file->f_flags&O_NONBLOCK)
      {
          1.读取硬件的数据
          2.copy_to_user将硬件数据传递到用户空间                  
      }
    return 0;
}

2,阻塞IO

    当进程在读取硬件的数据时,如果此时硬件数据准备就绪就读取,没有准备就绪则进程阻塞在read函数位置一直等到数据就绪。当硬件数据准备就绪后,硬件会发起硬件中断将休眠的进程唤醒,被唤醒后的进程停止阻塞,将准备好的硬件数据读走。阻塞状态下的进程处于休眠态,休眠态分为可中断休眠态和不可中断休眠态:
    S    interruptible sleep (waiting for an event to complete)//可中断休眠态,可以被外部信号打断
    D    uninterruptible sleep (usually IO)

    实现过程
***********应用程序************
    int fd=open("/dev/mycdev",O_RDWR);//以阻塞的模式打开文件
    read(fd,buf,sizeof(buf));
*********驱动程序**************
ssize_t mycdev_read(struct file *file, char *ubuf, size_t size, loff_t *lof)
{
    int ret;
      if(file->f_flags&O_NONBLOCK)
      {
          1.读取硬件的数据
          2.copy_to_user将硬件数据传递到用户空间                
      }
      else//阻塞IO
      {
          1.判断硬件数据是否准备好
          2.如果数据没有准备好,将进程切换为休眠状态
          3.读取硬件数据
          4.copy_to_user      
      }
  
    return 0;
}

//硬件的中断处理程序
irq_handler()
{
    1.确定硬件数据准备就绪
    2.唤醒休眠的进程
}

阻塞IO实现相关的API

1.定义一个等待队列头
    wait_queue_head_t wq_head;
2.初始化等待队列
    init_waitqueue_head(&wq_head);
3.wait_event(wq_head, condition)
    功能: 将进程切换为不可中断的休眠态
    参数:
        wq_head:等待队列头
        condition:标识硬件数据是否就绪的标志变量
    返回值:无
4.wait_event_interruptible(wq_head, condition)
    功能:将进程切换为可中断的休眠态
    参数:
        wq_head:等待队列头
        condition:标识硬件数据是否就绪的标志变量
    返回值:当硬件数据准备好后进程正常被唤醒返回0,如果进程被外部信号中断休眠则返回错误码 -ERESTARTSYS

5.wake_up(&wq_head)
    功能:唤醒不可中断休眠态的进程,如果在condition为假的情况下调用此函数,休眠的进程被唤醒后会马上再次休眠
    参数:
    等待队列头指针
    返回值:无

6.wake_up_interruptible(&wq_head)
    功能:唤醒可中断休眠态的进程,如果在condition为假的情况下调用此函数,休眠的进程被唤醒后会马上再次休眠
    参数:
    等待队列头指针
    返回值:无


3,IO多路复用

    当在应用程序中同时实现对多个硬件数据读取时就需要用到IO多路复用。io多路复用有select/poll/epoll三种实现方式。如果进程同时监听的多个硬件数据都没有准备好,进程切换进入休眠状态,当一个或者多个硬件数据准备就绪后,休眠的进程被唤醒,读取准备好的硬件数据。

***********************VFS(虚拟文件系统层)*********
sys_select()
{
    1.在内核申请一片内存用于保存从用户空间的文件描述符集合中拷贝的文件描述符,拷贝完毕后用户的事件集合被清空
    2.根据文件描述符集合中的每一个文件描述符按照fd->fd_array[fd]->struct file对象->操作方法对象->poll方法  ,按照这个路线回调每个fd对应的驱动中的poll方法
    3.判断每个文件描述符的poll方法的返回值,如果所有的poll方法的返回值都为0,表示没有任何硬件数据准备就绪,此时将进程切换为休眠态(可中断休眠态)
    4.当休眠的进程收到一个或者多个事件就绪的唤醒提示后,在这里根据事件集合中的每一个文件描述符再次回调poll方法,找出发生事件的文件描述符
    5.将发生事件的文件描述符重新拷贝回用户空间的事件集合
}
*************************驱动程序****************
//所有的io复用方式在驱动中对应的操作方法都是poll方法
    __poll_t (*poll) (struct file *file, struct poll_table_struct *wait)
    {
              //向上提交等待队列头
       void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
       /*功能:将等待队列头向上层提交
       参数:
       filp:文件指针,将poll方法第一个参数填进去
       wait_address:要向上提交的等待队列头地址
       p:设备驱动和上层关联的通道,将poll方法的第二个参数填进去
       返回值:无*/
            //判断condition的值,根据事件是否发生给一个合适的返回值
            if(condition){
                return POLLIN;//POLLIN表示读   POLLLOUT表示写            
            }else{
                return 0;            
            }
    }

epoll的实现

核心操作:一棵树(红黑树),一张表,三个接口

API:

int epoll_create(int size);
功能:创建一个epoll句柄//创建红黑树根节点
epoll把要监测的事件文件描述符挂载到红黑树上
参数:size 没有意义,但是必须>0
返回值:成功返回根节点对应的文件描述符,失败返回-1

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
功能:实现对于epoll的控制
参数:
epfd:epoll_create创建的句柄
op:控制方式
     EPOLL_CTL_ADD:添加要监测的事件文件描述符
     EPOLL_CTL_MOD:修改epoll检测的事件类型
     EPOLL_CTL_DEL:将文件描述符从epoll删除
fd:要操作的文件描述符
event:事件结构体
typedef union epoll_data {
               void        *ptr;
               int          fd;//使用这个
               uint32_t     u32;
               uint64_t     u64;
           } epoll_data_t;

 struct epoll_event {
               uint32_t     events; //EPOLLIN(读) EPOLLOUT(写)
               epoll_data_t data;        /* User data variable */
           };
返回值:成功返回0,失败返回-1

int epoll_wait(int epfd, struct epoll_event *events,
                      int maxevents, int timeout);
功能:阻塞等待准备好的文件描述符
参数:
epfd:epoll句柄
events:存放就绪事件描述符的结构体数组首地址
maxevents:监听的事件最大个数
timeout:超时检测
    >0:毫秒级检测
    ==0:立即返回
    -1:不关心是否超时

返回值:
>0:准备好的文件描述符的个数
==0:超时
<0:失败

4,信号驱动IO

概述:
    信号驱动IO是一种异步IO方式,linux预留了一个信号SIGIO用于进行信号驱动IO,进程主程序注册一个SIGIO信号的信号处理函数,当硬件数据准备就绪后会发起一个硬件中断,在中断的处理函数中向当前进程发送一个SIGIO信号,进程收到SIGIO信号执行信号处理函数,在信号处理函数中读走即可

实现过程:

应用程序:
    1,打开设备文件:
    2,注册信号的信号处理函数signal(SIGIO,信号处理函数名)
    3,回调驱动中的fasync方法,完成发送信号之前的准备工作
        获取文件描述符属性int flags = fcntl(fd, F_GETFL);
        在文件描述符表的flags中添加FASYNC异步处理方式,就可以回调fasync方法
    4,设置当前fd对应的驱动程序接收SIGIO信号 fcntl(fd,F_SETOWN,getpid());
    5,不让主程序结束,等待中断信号

驱动程序:
    1,定义一个异步对象指针
    2,封装异步操作方法的函数,完成异步空间对象的空间分配和初始化
    3,封装处理函数

API:
int fasync_helper(int fd, struct file * filp, int on, struct fasync_struct **fapp)
功能:完成异步对象的空间分配和初始化

void kill_fasync(struct fasync_struct **fp, int sig, int band)
    功能:向进程发送信号
    参数:
    fp:异步对象的二级指针
    sig:要发生的信号  SIGIO
    band:发送信号时添加的事件标志   POLL_IN表述读数据操作

十二,【设备树】

什么是设备树    
    设备树(DeviceTree/DT/of)是用来保存设备信息的一种树形结构。设备树的源码是独立于linux内核源码存在的。设备树上的设备信息在内核启动后被内核解析,加载到内核空间。以树形结构包窜在内核空间中。设备树上的每一个节点都是用来保存一个设备的设备信息。一个设备的信息由多种树形共同描述。一个设备的多个属性在内核空间中是以链表的形式存在,链表的每一个节点都表示这个设备的一个属性

为什么引入设备树
    按照之前驱动的编写方式,在驱动中直接固化了硬件的设备信息,这种形式编写的驱动只适用于特定的硬件,一旦硬件环境更换,驱动就无法正常使用了。现在为了让驱动可以兼容更多硬件,我们不在驱动中指定设备信息,而是引入了设备树。驱动中获取设备树上的设备信息,基于这些设备信息完成硬件的控制

设备树的文件格式
    设备树源码路径/linux-5.10.61/arch/arm/boot/dts/stm32mp157a-fsmp1a.dts
    ***.dts --设备树的源码文件
    ***.dtsi --设备树的补充文件,类似于c语言的h文件
    |
    |通过DTC编译工具 执行 make dtbs 编译设备树文件的命令生成 ***.dtb设备树的镜像文件

如何启用设备树和设备树的编译工具DTC:
    打开内核的顶层目录下的.config文件,在文件中有如下两个配置则说明当前内核已经启用的设备树和DTC工具: 
       CONFIG_DTC = y
       CONFIG_OF  = y

1,设备树语法

设备树linux官方手册:Device Tree Usage - eLinux.org

基本语法格式:
    设备树是节点和属性的简单树结构。属性是键值对,节点可以同时 包含属性和子节点

例:
/dts-v1/;//设备树的版本号

/ {  // ‘/’表示根节点
    node1 {  //node1是根节点的子节点
        a-string-property = "A string";//node1节点的属性键值对
        a-string-list-property = "first string", "second string";
        // hex is implied in byte arrays. no '0x' prefix is required
        a-byte-data-property = [01 23 34 56];
        child-node1 {  //child-node1是node1节点的子节点
            first-child-property;//child-node1节点的额属性键值对,空属性
            second-child-property = <1>;
            a-string-property = "Hello, world";
        };
        child-node2 {
        };
    };
    node2 {// 根节点的子节点
        an-empty-property;
        a-cell-property = <1 2 3 4>; /* each number (cell) is a uint32 */
        child-node1 {
        };
    };
};

设备树节点的命名格式: <name>@<unit-address>
    <name>是一个简单的 ASCII 字符串,长度最多为 31 个字符。通常,节点是根据它所代表的设备类型来命名的。
    如果节点使用地址描述设备,则包含单元地址。通常,单元地址是用于访问设备的主要地址,列在节点的                                属性中。
ex:
    1.gpio@50006000{};//gpioe控制器节点的名字,gpio支持寻址,所以要在名字里加上地址
    2.LED1{};

关于设备树节点的别名和合并问题
1.
aliases {
        serial0 = &uart4;
        serial5 = &usart3;
    };
解释:上面节点中serial0就是给uart4起了一个别名
2.
gpioe: gpio@50006000 {
                gpio-controller;
                #gpio-cells = <2>;
                ...
            };
解释:gpioe是gpio@50006000节点的标签,在别的位置操作gpioe相当于操作到了gpio@50006000节点
3.
    两个文件中有同名节点,按照设备树的编译规则,同级目录下有相同名字的节点,节点会合并,如果相同节点中属性名相同,后一次的值会覆盖前一次的值,如果属性名不同,直接合并

属性键值对的数据类型
    属性是简单的键值对,其中值可以为空或包含任意字节流。虽然数据类型未编码到数据结构中,但可以在设备树源文件中表示一些基本数据表示形式
    文本字符串(以null结尾)用双引号表示:string-property = “a string”;
    “cell”是32位无符号整数,由尖括号分割:cell-property = <0xbeef 123 0xabcd1234>;
    单字节数据用方括号分割:binary-property = [0x01 0x23 0x45 0x67];
    不同表示形式的数据可以用逗号链接在一起: mixed-property = "a string", [0x01 0x02 0x03 0x04], <0x12345678>;
    逗号也用于创建字符串列表:string-list = “red fish”,“blue fish”;

常用标准化的属性键值对
    在设备树中有一些特定的键值对用来表示特定的含义:
compatible = "芯片厂商,芯片型号";//描述当前设备的厂商信息
device_type:用于描述当前设备的设备类型
reg=<地址,内存大小>:用于描述当前节点对应设备的寻址内存首地址和大小
#address-cells=<n>:用于指定子节点中reg属性用来描述地址的u32的个数
#size-cells=<n>:用于指定子节点中reg属性用来描述地址对应内存大小的u32的个数

2,添加一个自定义的设备树节点到设备树源码中被内核解析

1,添加设备树节点
    在stm32mp157a-fsmp1a.dts文件的根节点内部添加如下内容:
    //自定义设备树
    mynode@0x12345678{
        compatible = "hqyj,mynode";
        astring="hello 23051";
        uint  =<0xaabbccdd 0x11223344>;
        binarry=[00 0c 29 7b f9 be];
        mixed ="hello",[11 22],<0x12345678>;
     };
2,编译设备树
    返回到内核顶层目录下执行编译设备树的命令make dtbs
3,将镜像复制到~/tftpboot中,重启开发板
4,查看自己添加的节点是否被成功解析
    开发板系统目录:/proc/device-tree/目录下是否有以节点名为名的文件夹生成

3,在驱动程序中获取设备树中指定的设备信息

设备树节点结构体struct device_node
    当设备树中的信息加载到内核空间后,每一个节点都是一个struct device_node类型
struct device_node {
    const char *name;//设备树节点的名字mynode
    phandle phandle;//节点标识
    const char *full_name;//全名  mynode@0x12345678
    struct  property *properties;//属性链表首地址
    struct  device_node *parent;//父节点指针
    struct  device_node *child;//子节点指针
    struct  device_node *sibling;//兄弟节点指针
};

属性结构体 struct propety
    一个设备树节点中存在多个属性,组成了一个链表,链表中每一个节点保存了设备的一个信息,链表节点的类型是struct propety类型
struct property {
    char    *name;//键名
    int length;//数值的大小
    void    *value;//数值首地址
    struct property *next;//下一个属性对象指针
};

4,设备树节点解析API & 属性解析API

设备树节点解析:

struct device_node *of_find_node_by_name(struct device_node *from, const char *name);
    功能:根据设备树节点的名字解析指定的设备树节点信息
    参数:
        from:要解析的节点所在子树的根节点,填NULL,默认从根节点解析
        name:要解析的设备树节点的名字
    返回值:成功返回目标节点首地址,失败返回NULL

struct device_node *of_find_node_by_path(const char *path);
    功能:根据设备树节点路径解析设备树节点信息
    参数:
        path:设备树所在的节点路径,非文件路径 例:/mynode@0x12345678 
    返回值:成功返回目标节点首地址,失败返回NULL

struct device_node *of_find_compatible_node(struct device_node *from, const char *type, const char *compat);
    功能:根据节点的厂商信息解析指定的节点
    参数:
        from:要解析的节点所在子树的根节点,填NULL,默认从根节点解析
        type:设备类型,填NULL
        compat:compatible值
    返回值:成功返回目标节点首地址,失败返回NULL

__u32 __be32_to_cpup(const __be32 *p)
    功能:将大端字节序32位的数据转换为主机字节序
    参数:要转换的数据指针
    返回值:转换后的值


设备树属性解析:
struct propety *of_find_propety(const struct device_node *np, const char *name, int *lenp)
    功能:解析指定键名的属性信息
    参数:
        np:设备树节点对象指针
        name:要解析的属性键名
        lemp:解析到的属性的值的长度
    返回值:成功返回属性对象指针,失败返回NULL

十三,【GPIO子系统】

概述:
    一个芯片厂商生产芯片后,给linux提供当前芯片中gpio外设的驱动,可以直接调用厂商驱动完成对硬件的控制。而每个厂商提供的驱动并不相同,linux内核就将厂商的驱动进行封装,提供API,我们调用调用内核提供的API即可间接访问厂商驱动,完成控制。

相关API:
1,解析GPIO相关的设备树节点(路径/名字/厂商信息:见设备树)
    struct device_node *of_find_node_by_path(const char *path);
    功能:根据设备树节点路径解析设备树节点信息
    path:设备树所在的节点路径
    返回值:成功返回目标节点首地址,失败返回NULL
2,根据解析的GPIO相关节点信息获取GPIO编号
    int of_get_named_gpio(struct device_node *np, const char *propname, int index);
    功能:获取GPIO编号
    参数:
    np:设备树节点指针
    propname:gpio编号信息对应的键名
    index:引脚在这个属性键值对中的索引号,0,1,2...
    返回值:成功返回GPIO编号,失败返回错误码
3,向内核申请要使用的GPIO编号
    int gpio_request(unsigned gpio, const char *label);
    功能:申请GPIO编号(获得GPIO编号的使用权)
    参数:
    gpio:要申请的gpio编号
    label:标签,填NULL
4,设置GPIO编号对应的引脚的模式
    int gpio_direction_input(unsigned int gpio);
    功能:将gpio编号对应的gpio引脚设置为输入
    参数:gpio:gpio编号
    返回值:成功返回0,失败返回错误码
    
    void gpio_direction_output(unsigned int gpio, int value);
    功能:将gpio编号对应的gpio引脚设置为输出
    参数:gpio:gpio编号
        value:默认输出的值, 1:高电平, 0:低电平
    返回值:无
5,设置GPIO引脚输出高低电平
    void gpio_set_value(unsigned int gpio, int value);
	功能:设置gpio编号对应的gpio引脚输出高低电平
	参数:gpio:gpio编号
		value:默认输出的值, 1:高电平, 0:低电平
	返回值:无

6.获取引脚状态
    int gpio_get_value(unsigned int gpio);
	功能:获取gpio编号对应的GPIO引脚状态值
	参数:gpio:gpio编号
	返回值:1:高电平状态 0:低电平状态

7,释放GPIO信号
	void gpio_free(unsigned gpio);
	参数:要释放的GPIO编号

********************新版API*********************
    正常向内核申请一个gpio编号,其实就是在内核中申请了一个struct gpio_desc类型的对象并且完成了初始化,而gpio编号可以理解为是这个gpio_desc对象的索引号,新版    GPIO子系统API的操作核心就是gpio对象

struct gpio_desc *gpiod_get_from_of_node(struct device_node *node,
					const char *propname, int index,
					enum gpid_flags dflags,
					const char *label);
功能:在设备树节点中解析处GPIO对象,获得首地址,并向内核申请
参数:
	node:设备树节点信息指针
	propname:键名
	index:索引号
	dflags:设置GPIO默认状态 
		枚举值:GPIOD_IN:输入
			GPIOD_OUT_LOW:输出低电平
			GPIOD_OUT_HIGH:输出高电平
	label:标签,填NULL
返回值:成功返回gpio对象指针,失败返回内核4K预留空间(错误码指针)

int gpiod_direction_output(struct gpio_desc *desc, int value)
int gpiod_direction_input(struct gpio_desc *desc)
void gpiod_set_value(struct gpio_desc *desc, int value)
int gpiod_get_value(const struct gpio_desc *desc)
void gpiod_put(struct gpio_desc *desc)//释放gpi对象指针

        

1,分析GPIO控制器节点

定义位置:stm32mp151.dtsi
    pinctrl: pin-controller@50002000 {
            #address-cells = <1>;//子节点中reg属性中1个u32描述地址
            #size-cells = <1>;//子节点中reg属性中1个u32描述地址大小
            compatible = "st,stm32mp157-pinctrl";//描述厂商信息
            ranges = <0 0x50002000 0xa400>;//指定当前节点映射的地址范围            
                    gpioe: gpio@50006000 {
                gpio-controller;//空属性,起到标识作用
                #gpio-cells = <2>;
//用于指定在别的节点中引用当前节点用于gpio控制时需要有2个u32进行描述                
                reg = <0x4000 0x400>;//gpio的地址信息
                clocks = <&rcc GPIOE>;//当前控制器的使能时钟
                st,bank-name = "GPIOE";//指定控制器名字为GPIOE
                status = "disabled";
//GPIO控制器状态为disable
                        //okay:使能   disable:不工作
            };
引用gpio节点的位置:stm32mp15xxac-pinctrl.dtsi
&pinctrl {
        gpioe: gpio@50006000 {
        status = "okay";
//描述当前gpio状态为使能
        ngpios = <16>;
//当前gpio控制器管理的管脚有16个
        gpio-ranges = <&pinctrl 0 64 16>;
//指定管脚范围
    };
 };

2,添加LED的设备树节点信息

查询内核帮助文档:
~/linux-5.10.61/Documentation/devicetree/bindings/gpio
vi gpio.txt

The following example could be used to describe GPIO pins used as device enable
and bit-banged data signals:

gpio0: gpio1 {
        gpio-controller;
        #gpio-cells = <2>;
    };

    data-gpios = <&gpio0 12 0>,
             <&gpio0 13 0>,
             <&gpio0 14 0>,
             <&gpio0 15 0>;
In the above example, &gpio0 uses 2 cells to specify a gpio. The first cell is
a local offset to the GPIO line and the second cell represent consumer flags,
such as if the consumer desire the line to be active low (inverted) or open
drain. This is the recommended practice.
Example of a node using GPIOs:

    node {
        enable-gpios = <&qe_pio_e 18 GPIO_ACTIVE_HIGH>;
    };



*********添加LED的设备树节点************
在stm32mp157a-fsmp1a.dts文件的根节点中添加如下内容
myled{
    led1-gpio=<&gpioe 10 0>;//10表示使用的gpioe第几个管脚  0,表示gpio默认属性
    led2-gpio=<&gpiof 10 0>;
    led3-gpio=<&gpioe 8 0>;
};
或者
myled{
    led-gpios=<&gpioe 10 0>,<&gpiof 10 0>,<&gpioe 8 0>;
};


添加完毕,返回内核顶层目录,执行make dtbs编译设备树
将编译生成的设备树镜像拷贝到~/tftpboot目录下,重启开发板

十四,【linux内核定时器】

	应用层定时:可以用sleep() == 进程无法向下执行
或者14) SIGALRM信号,结合signal(),alarm()函数实现 == 进程可以向下继续执行
	linux内核定时器的使用是设置一个定时事件,当定时事件到达之后可以执行档期那的定时器处理函数,
在定时器处理函数中完成一些周期行的任务。linux内核定时器的工作原理和硬件定时器原理一致。
只需如下几步:
	1,分配一个定时器对象
	2,初始化定时器对象
	3,注册定时器
	4,启用定时器
	5,注销定时器

jiffies
	jiffies是内核中用于保存内核节拍数的一个变量。它的值从内核启动开始不断的从0开始增加

内核频率:
	内核节拍数一秒增加的数量称为内核的频率,内核的频率在内核顶层目录下的.config文件中被设置
	linux中: CONFIG_HZ = 100

内核定时器对象分析:
struct timer_list {
    struct hlist_node entry;//用于构成一个对象链表
    unsigned long  expires;//设置的时间阈值  == 定时一秒:jiffies+CONFIG_HZ
    void (*function)(struct timer_list *);//定时器处理函数指针
    u32  flags;//标志,新版才有,填0即可
};

***********API***********
1,分配定时器对象
	struct timer_list timer;
2,初始化定时器对象
	void timer_setup(struct timer_list *timer,
			void (*func)(struct timer_list *), unsigned int flags);
	功能:初始化定时器对象,定时器对象中的expires需要手动初始化
	参数:
		timer:定时器对象指针
		func:定时器处理函数的函数指针
		flags:0
	返回值:无
3,注册定时器对象并启用定时器
	void add_timer(struct timer_list *timer);
	功能:注册定时器对象并启用定时器
	参数:timer:定时器对象指针
	返回值:无
4,再次启用定时器 modified 改进的
	int mod_timer(struct timer_list *timer, unsigned long expires);
	功能:再次启用定时器
	参数:timer:定时器对象指针
	expires:重新设置的定时器阈值
	返回值:启用之前没启用的定时器返回0,启用之前启用的定时器返回1
5,注销定时器
	int del_timer(struct timer_list *timer);

十五,【Linux内核中断】

    linux内核中断引入的目的是用于对设备不用进行轮询访问,而是当设备事件发生后主动通知内核,内核再去访问设备。
    中断注册进内核之后,中断信息会保存至一个struct irq_desc对象中,内核中存在一个struct irq_desc类型的数组,数组中每一个成员都是保存了一个注册进内核的设备中断信息

中断子系统API
1,解析中断相关的设备树节点 ()
	struct device_node *of_find_compatible_node(struct device_node *from, const char *type, const char *compat);
	功能:根据节点的厂商信息解析指定的节点
	参数:from 要解析的节点所在子树的跟节点,填NULL,默认从根节点解析
		 type:设备类型,填NULL
		 compat:compatible值
	返回值:成功返回目标节点首地址,失败返回NULL
2,解析设备中断的软中断号
	unsigned int irq_of_parse_and_map(struct device_node *node, int index);
	功能:解析设备中断的软中断号
	参数:
		node:设备树节点指针
		index:索引号
	返回值:成功返回软中断号,失败返回0	 
3,注册中断
	int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev);
	功能:将中断注册进内核
	参数:irq:当前那中断的软中断号
		handler:中断的中断处理函数
		/* typedef enum irqreturn irqreturn_t;
		typedef irqreturn_t (*irq_handler_t)(int ,void *);
			中断处理函数:
					返回值: enum irqreturn {
					IRQ_NONE  = (0 << 0),//这个中断不是被这个设备触发,没被处理
					IRQ_HANDLED = (1 << 0),//中断被正常处理
					IRQ_WAKE_THREAD = (1 << 1),//唤醒一个线程处理中断
					};
		*/
		flags:注册中断时添加的设备中断相关标志
			 /* IRQF_TRIGGER_RISING 上升沿
				IRQF_TRIGGER_FALLING 下降沿
				IRQF_TRIGGER_HIGH 	 高电平
				IRQF_TRIGGER_LOW 	 低电平
				IRQF_SHARED //共享中断,多个设备共享一个中断线
			*/
		name:中断名
		dev:传递给中断处理函数的参数,也用于标识irqaction对象
		返回值:成功返回0,失败返回错误码
4,注销中断
	void *free_irq(unsigned int irq, void *dev_id)
	功能:注销中断
	参数:irq::软中断号
		dev_id:注册时填写的传递给中断处理函数的参数,这里用于释放对应的irqaction空间
	返回值:成功返回注册时填写的name

1,添加按键中断的设备树

添加节点位置:/linux-5.10.61/arch/arm/boot/dts/stm32mp151.dts

******************GPIOF******************
定义位置:
stm32mp151.dtsi
pinctrl: pin-controller@50002000 {
            #address-cells = <1>;
            #size-cells = <1>;
            compatible = "st,stm32mp157-pinctrl";
            ranges = <0 0x50002000 0xa400>;
            interrupt-parent = <&exti>;//引用中断父节点为exti
                       gpiof: gpio@50007000 {
                
                interrupt-controller;//空属性,标识当前设备为一个中断控制器节点
                #interrupt-cells = <2>;//在别的节点中引用当前节点为中断父节点时添加两个u32进行描述
                reg = <0x5000 0x400>;
                clocks = <&rcc GPIOF>;
                st,bank-name = "GPIOF";
                status = "disabled";
            };
   };
引用位置:stm32mp15xxac-pinctrl.dtsi
&pinctrl {
    gpiof: gpio@50007000 {
        status = "okay";
        ngpios = <16>;
        gpio-ranges = <&pinctrl 0 80 16>;
    };
 };
 
  ***************exti*************
  soc {
        compatible = "simple-bus";
        #address-cells = <1>;
        #size-cells = <1>;
        interrupt-parent = <&intc>;//中断父节点为intc
                exti: interrupt-controller@5000d000 {
            compatible = "st,stm32mp1-exti", "syscon";
            interrupt-controller;
            #interrupt-cells = <2>;
            reg = <0x5000d000 0x400>;
                };
          };
***************GIC*****************   
    intc: interrupt-controller@a0021000 {
        compatible = "arm,cortex-a7-gic";
        #interrupt-cells = <3>;
        interrupt-controller;
        reg = <0xa0021000 0x1000>,
              <0xa0022000 0x2000>;
    };     

查询内核帮助手册:
~/linux-5.10.61/Documentation/devicetree/bindings/interrupt-controller/interrupts.txt
 
1) Interrupt client nodes
Example:
    interrupt-parent = <&intc1>;
    interrupts = <5 0>, <6 0>;
Example:
    interrupts-extended = <&intc1 5 1>, <&intc2 1 0>;
2) Interrupt controller nodes
    b) two cells
    bits[3:0]不需要关注,一设置写0即可

***********************************************************
在stm32mp157a-fsmp1a.dtsi文件的根节点内部添加如下内容:
   myirq{
       compatible="hqyj,myirq";
       interrupt-parent=<&gpiof>; 
       interrupts=<9 0>,<7 0>,<8 0>;  
   };
   或者
     myirq{
       compatible="hqyj,myirq";
       interrupts-extended=<&gpiof 9 0>,<&gpiof 7 0>,<&gpiof 8 0>;//8表示索引号,0表示不设置触发状态  
   };
添加完毕,在内核顶层目录下执行make dtbs编译设备树源码,将设备树源码拷贝到~/tftpboot下
重启开发板   

2,中断底半部

    当一个中断被触发以后,会关闭抢占。一个CPU处理当前中断任务时,当前CPU无法处理其他任务,所有的CPU都会关闭当前中断线。在这种情况下,如果一个中断中有延时、耗时甚至休眠操作,最终会导致整个系统功能的延迟。所以一般在中断处理过程中不允许有延时、耗时甚至休眠的操作。但是有的时候又必须在中断的处理程序中进行一些耗时任务。
    这样就会产生一个冲突:中断不允许又耗时但是有时候又不得不进行耗时的冲突
    为了解决这个冲突,内核引入了中断底半部的概念:
    将一个中断处理得分过程分为了中断顶半部和中断底半部,中断顶半部就是通过 request_irq注册的中断处理函数,在顶半部中主要进行一些重要的、不耗时的任务;中断底半部则是区进行一些耗时,不紧急的任务。在执行中断底半部时,会将执行中断顶半部时关闭的中断线启用以及抢占开启,这样进程以及其他的中断就可以正常的工作了。
    中断底半部的实现机制有softirq(软中断)、tasklet以及工作队列

软中断
    当顶半部即将执行结束时开启软中断,在软中断处理函数中取处理当前中断里的耗时任务。软中断存在数量限制(32个)。
    软中断一般留给内核开发者使用。

tasklet
    tasklet是基于软中断的工作原理进行的,可以进行一些耗时任务,但是不能在tasklet底半部进行休眠操作。tasklet是工作在中断上下文,在进程中不可以使用。tasklet没有使用数量的限制,当顶半部即将执行结束时,可以开启tasklet底半部进行一些耗时任务。
    在顶半部即将执行结束时,会清除中断标志位。此时内核区判tasklet底半部标志位是否被置位,如果被置位,需要开启底半部,在底半部中处理一些耗时任务。tasklet处理的底半部一般不要超过5个。超过5个需要开启内核线程,由内核线程去处理多余的底半部

工作队列
    工作队列用于底半部原理:内核中存在工作队列对应的内核线程,这个线程从内核启动就存在,处于休眠态。当有任务需要执行时,只需要将任务提交到工作队列中,然后唤醒休眠的内核线程,由内核线程去处理对应的任务即可。工作队列既可以用于中断,也可以用于进程。

tasklet 和 工作队列API

**********************tasklet API**************************
1.分配一个tasklet对象
    struct tasklet_struct
    {
        struct tasklet_struct *next;//指向下一个tasklet对象
        unsigned long state;//底半部标志位
        atomic_t count;//用于记录当前触发的底板次数
        bool use_callback;//根据选择的底半部处理函数的不同设置为不同的值
        //如果使用func类型处理函数,这个值会被设置为false,如果使用callback,则被设置为true
        union {
            void (*func)(unsigned long data);
            void (*callback)(struct tasklet_struct *t);
        };
        unsigned long data;//传递给func回调函数的参数
    };

struct tasklet_struct tasklet;//分配对象

2.初始化taklet对象
    void tasklet_init(struct tasklet_struct *t,
          void (*func)(unsigned long), unsigned long data)
    功能:当底半部处理函数是func类型时用此函数初始化对象

    void tasklet_setup(struct tasklet_struct *t,
           void (*callback)(struct tasklet_struct *))
    功能:当底半部处理函数是callback类型时用此函数初始化对象

3.开启底半部
    void tasklet_schedule(struct tasklet_struct *t)

**********************工作队列 API***********************
    结构体:
    struct work_struct {
    /*  atomic_long_t data; */
        unsigned long data;//保存底半部触发次数
        struct list_head entry;//用于构成内核链表
        work_func_t func;//底半部函数指针
        /*typedef void (*work_func_t)(struct work_struct *work);*/
    };

1,分配工作队列项    
    struct work_struct work;//分配对象

2.初始化队列项
    INIT_WORK(&work,底半部函数指针);

3.开启底半部
    bool schedule_work(struct work_struct *work)

十六,【platform驱动模型】

总线驱动模型    
    linux中将一个挂载在总线上的驱动的驱动模型分为三部分:device、driver和bus。
device是用来保存设备信息的对象,存放在内核中一个klist_device链表中进行管理。
driver当前设备的驱动信息对象,存放在内核中一个klist_driver链表中进行管理。
bus是当前设备挂载的总线的总线驱动。bus负责完成device和driver到的匹配,这一步通过总线驱动中的match函数来实现。
当device和driver匹配成功后执行driver端的probe函数,在probe函数中完成驱动的注册、设备节点的创建、以及后续的硬件控制工作。

platform总线驱动模型
    为了让没有挂载在总线上的设备也能够按照总线驱动模型进行驱动的编写,引入了paltform总线。引入platform之后就统一我们的设备驱动模型。
    platform是一段内核抽象出来的总线驱动代码,现实中并没有和platform总线驱动对应的真实总线。
它的作用就是管理没有挂载在总线上的设备,让这些设备有也可以按照总线驱动模型编写驱动。
    将一个platform总线驱动模型分为三部分:设备端、驱动端、总线端。
    由总线负责完成驱动和设备信息的匹配(platform_match),当匹配成功之后会执行驱动端的probe函数。在probe函数中实现驱动的注册、设备节点的创建以及后续的硬件控制工作

1,API

设备端

***************设备信息对象分析**********************
#include<linux/platform_device.h>
struct platform_device { 
    const char  *name;//设备名字,可以用于和驱动端的匹配
    int     id;//总线编号   PLATFORM_DEVID_AUTO  内核自动分配总线编号
    bool        id_auto;//id若为自动分配,该值为1,否则为0
    struct device   dev;// *是platform_device结构体的父类对象
    u32     num_resources;//用于记录保存的设备信息的个数
    struct resource *resource;// **存放设备信息的数组首地址
};

// *父类结构体
struct device {
    void   (*release)(struct device *dev);//设备信息从内核卸载时用这个函数释放资源
    };

// **资源结构体
struct resource {
    resource_size_t start;//资源的起始值 0X50006000     0X50006000          71
    resource_size_t end;//资源的终止值   0X50006000+4   0X50006000+0X400     71
    const char *name;//资源的名称
    unsigned long flags;//资源的类型  IORESOURCE_IO|IORESOURCE_MEM|IORESOURCE_IRQ
};

API:
1,分配设备信息对象并且初始化
1.1 **定义资源结构体数组并且初始化资源信息
1.2 * 封装release函数用于释放资源
1.3 分配设备信息并初始化
    struct platform_device pdev={......};
2,注册设备信息
    int platform_device_register(struct platform_device *pdev)
3,注销设备信息
    void platform_device_unregister(struct platform_device *pdev)
驱动端

****************驱动信息对象结构体分析***********

struct platform_driver {                                                         
    int (*probe)(struct platform_device *); //当驱动和设备匹配成功后执行
    int (*remove)(struct platform_device *); //当设备和驱动分离时执行
    struct device_driver driver;  //platform_driver的父类,用于描述驱动,设备信息匹配成功后会在这里面填充
    //在driver成员里面也可以设置和设备的匹配方式
    const struct platform_device_id *id_table;//用于设置名字表匹配,用于匹配的名字表首地址
};
//父类对象结构体
struct device_driver {
    const char      *name;//驱动名,可用于和设备的匹配
    const struct of_device_id  *of_match_table;//用于通过设备树的形式匹配设备信息
};

******************驱动端编写过程*****************
1.分配驱动信息对象并且初始化
//封装probe函数
int pdrv_probe(struct platform_device *pdev)
{
    printk("%s:%s:%d\n",__FINE__,__func__,__LINE__);
     return 0;   
}
//封装remove函数
int pdrv_remove(struct platform_device *pdev)
{
    printk("%s:%s:%d\n",__FINE__,__func__,__LINE__);
     return 0;   
}
//定义驱动信息对象并初始化
struct platform_driver pdrv={
    .probe=pdrv_probe,
    .remove=pdrv_remove,
    .driver={
        .name="aaaaa",    
    },
};

2.注册对象进内核
#define platform_driver_register(drv) \
    __platform_driver_register(drv, THIS_MODULE)
extern int __platform_driver_register(struct platform_driver *pdrv,
                    struct module *);
3.注销驱动对象
void platform_driver_unregister(struct platform_driver *drv)

******************驱动端一键注册宏使用****************
module_platform_driver(__platform_driver)

2,驱动端获取设备端的设备信息

struct resource *platform_get_resource(struct platform_device *dev, unsigned int type, unsigned int num)
功能:获取任意类型的设备资源
参数:
 * @dev: 设备信息对象指针
 * @type: 获取到资源类型  IORESOURCE_IO|IORESOURCE_MEM|IORESOURCE_IRQ
 * @num: 要获取分资源在同类型中的索引号,从0开始
 返回值:成功返回要获取的资源对象指针,失败返回NULL
 
 int platform_get_irq(struct platform_device *dev, unsigned int num)
 功能:获取中断类型的资源
 参数:
     dev:设备信息对象指针
     num:要获取的中断资源在中断资源中的索引号
返回值:成功返回中断号,失败返回错误码

3,platform_match函数分析

platform设备端和驱动端由总线驱动调用platform_match函数完成匹配
static int platform_match(struct device *dev, struct device_driver *drv)
{
    //根据设备端和驱动端父类对象获取platfotm设备对象和platform驱动对象
    struct platform_device *pdev = to_platform_device(dev);
    struct platform_driver *pdrv = to_platform_driver(drv);

    /* When driver_override is set, only bind to the matching driver */
    //第一优先级 使用设备端的driver_override与驱动端的name进行匹配,无论匹配是否成功,都不再继续往下执行
    if (pdev->driver_override)
        return !strcmp(pdev->driver_override, drv->name);

    /* Attempt an OF style match first */
    //第二优先级 设备树匹配
    if (of_driver_match_device(dev, drv))
        return 1;

    /* Then try ACPI style match */
    //第三优先级 电源管理相关的匹配
    if (acpi_driver_match_device(dev, drv))
        return 1;

    /* Then try to match against the id table */
    //第四优先级 名字表匹配
    if (pdrv->id_table)
        return platform_match_id(pdrv->id_table, pdev) != NULL;

    /* fall-back to driver name match */
    //第五优先级 名字匹配
    return (strcmp(pdev->name, drv->name) == 0);
}

4,设备端的名字表匹配方式

概述
    如果使用名字匹配,一个驱动只能匹配和他名字一样的设备信息,这会使得驱动的使用范围很狭窄,为了能够让驱动更加适配,我们可以在驱动端构建一个名字表。只要设备的名字和名字表中的任何一个名字一样,都可以执行驱动端probe函数

名字的类型
struct platform_device_id {
    char name[PLATFORM_NAME_SIZE];//保存名字的数组
    kernel_ulong_t driver_data;//当前设备对应的私有数据
};

名字表的构建
struct platform_device_id idtable[]=
{
    {"aaaaa",0},
    {"bbbbb",1},
    {"ccccc",2},
    {"ddddd",3},
    {},//防止数组越界
}

5,设备树匹配

概述
    内核3.10版本以后要求将所有的设备信息都保存在设备树中,所有以后驱动端获取设备信息都在设备树中获取,所以需要使用驱动端的设备树匹配方式

设备树匹配匹配项的类型
struct of_device_id {
    char    name[32];//要匹配的节点名
    char    type[32];//要匹配的设备类型
    char    compatible[128];//要匹配的设备树节点的compatible
    const void *data;//当前匹配项的私有数据
};

构建用于设备树匹配的表
struct of_device_id oftable[] = {
    { .compatible = "hqyj,myplatform",    },
    { /* end node */ },//防止数组越界
};

十七,【I2C子系统】

1,将核心层和总线驱动层配置进内核

*********************配置核心层*************************
 1.找到核心层代码目录:内核顶层目录/drivers/i2c
 2. 内核顶层目录执行make menuconfig
 3. > Device Drivers > I2C support  ->-*-I2C support
 4.保存退出
 ********************配置总线驱动层************
 1.找到iic总线驱动层代码目录:内核顶层目录/drivers/i2c/busses
 2.内核顶层目录执行make menuconfig
 3. > Device Drivers > I2C support > I2C Hardware Bus support-》
 <*> STMicroelectronics STM32F7 I2C support 
 4.保存退出
 
 
 *************编译**********
 1.内核顶层目录下执行make uImage   LOADADDR=0XC2000000
 2.cp 内核层目录/arch/arm/boot/uImage ~/tftpboot
 3.重启开发板

2,驱动层API

对象结构体
struct i2c_driver {
    //与设备匹配成功执行
    int (*probe)(struct i2c_client *client, const struct i2c_device_id *id);
    //设备分离时执行
    int (*remove)(struct i2c_client *client);
    //设置名字匹配和设备树匹配
    struct device_driver driver;
    //设置id_table匹配
    const struct i2c_device_id *id_table;
};

struct device_driver {
    const char      *name;
    const struct of_device_id  *of_match_table;
    };
1.给对象分配空间并且初始化
int i2c_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
    return 0;
}
int i2c_remove(struct i2c_client *client)
{
    return 0;
}
struct i2c_driver i2c_drv={
    .probe=i2c_probe,
    .remove=i2c_remove,
    .driver={
        .name="si7006",
        .of_match_table=设备树匹配表名,   
    },
};
2.注册
  #define i2c_add_driver(struct i2c_driver *driver) \
    i2c_register_driver(THIS_MODULE, driver)
3.注销
    void i2c_del_driver(struct i2c_driver *driver)

4.一键注册宏 代替 2.注册,3.注销
#define module_i2c_driver(__i2c_driver) \
    module_driver(__i2c_driver, i2c_add_driver, \
            i2c_del_driver)

3,IIC设备树修改

查看已经添加好的i2c1设备树节点
在stm32mp151.dtsi内部,有如下内容:
i2c1: i2c@40012000 {
            compatible = "st,stm32mp15-i2c";//厂商信息
            reg = <0x40012000 0x400>;//地址信息
            interrupt-names = "event", "error";//中断模式列表
            interrupts-extended = <&exti 21 IRQ_TYPE_LEVEL_HIGH>,
                          <&intc GIC_SPI 32 IRQ_TYPE_LEVEL_HIGH>;
            clocks = <&rcc I2C1_K>;//使能时钟
            resets = <&rcc I2C1_R>;//复位时钟
            #address-cells = <1>;
            #size-cells = <0>;
            dmas = <&dmamux1 33 0x400 0x80000001>,
                   <&dmamux1 34 0x400 0x80000001>;
            dma-names = "rx", "tx";
            
            status = "disabled";//控制器的工作状态
        };

修改I2C1设备树节点以及添加si7006的子节点
查询内核帮助手册:~/linux-5.10.61/Documentation/devicetree/bindings/i2c
vi i2c.txt

Required properties (per bus)
-----------------------------

- #address-cells  - should be <1>. Read more about addresses below.
- #size-cells     - should be <0>.
- compatible      - name of I2C bus controller
Optional properties (per bus)
-----------------------------
- pinctrl
    add extra pinctrl to configure SCL/SDA pins to GPIO function for bus
    recovery, call it "gpio" or "recovery" (deprecated) state
Required properties (per child device)
--------------------------------------

- compatible
    name of I2C slave device

- reg
    One or many I2C slave addresses.


*********************************************
在stm32mp157a-fsmp1a.dts文件的根节点外部添加如下内容:
&i2c1 {
    pinctrl-names = "default", "sleep";//描述当前控制器的工作模式
 //"default"表示默认工作模式  "sleep"表示低功耗工作模式
    pinctrl-0 = <&i2c1_pins_b>;//设置默认工作模式下的管脚复用
    pinctrl-1 = <&i2c1_sleep_pins_b>;//设置低功耗模式下的管脚复用
    i2c-scl-rising-time-ns = <100>;//时钟线下降沿的时间
    i2c-scl-falling-time-ns = <7>;//时钟线上升沿的时间
    status = "okay";//状态设置为OKAY
    /delete-property/dmas;//屏蔽不必要的属性
    /delete-property/dma-names;
          si7006@40{
      compatible="hqyj,si7006";
      reg=<0X40>;  
  };  
};

4,收发相关API

struct i2c_client结构体
    当驱动匹配设备信息成功后内核中就会存在一个struct i2c_client 对象,对象内部保存的是匹配成功的设备的信息以及总线相关的信息
struct i2c_client {
    unsigned short flags;//读写标志   0写   1读
    unsigned short addr; //匹配到的设备的从机地址
    char name[I2C_NAME_SIZE];//匹配到分设备的名字
    struct i2c_adapter *adapter; //用于索引总线驱动的对象指针
    struct device dev;//设备端的父类对象
    int init_irq;//中断初始化的标志
    int irq;  //中断号
    struct list_head detected;//构成内核链表
};

i2c数据传输的函数  i2c_transfer()
int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num)
功能:基于I2C总线进行数据传输
参数:
    adap:用于索引总线驱动的对象指针  client->adapter
    msgs:要传输的一个或者多个消息   一个消息是以起始信号作为出发点
    num:传输的消息的数量
返回值:成功返回传输的消息个数,失败返回错误码

消息结构体 struct i2c_msg
    I2C总线上传输的内容属于消息,一条消息中要包含从机地址、读写标志位以及消息的正文
struct i2c_msg {
    __u16 ddr; //从机地址client->addr 
    __u16 flags;//读写标志位   0写    1读
        __u16 len;   //消息正文的大小
    __u8 *buf;//保存消息正文的buf首地址   
};

5,消息的封装

封装消息的原则:根据时序来,有几个起始信号就要有几条消息

写消息的封装
    start+7bit从机地址(高位在前低位在后)+1bit写(0)+ack(从机给主机发)+寄存器的地址(不同芯片寄存器地址不一样,有的是8bit址,也有16bit,如果是16bit,会拆分成高8bit和低8bit)+ack+向寄存器中写的数据(8bit)+ack+stop

char w_buf[]={寄存器地址,data};
struct i2c_msg w_msg={
    .addr=client->addr,
    .flags=0,
    .len=sizeof(w_buf),
    .buf=w_buf,
};

读消息的封装
    start+7bit从机地址(高位在前低位在后)+1bit写(0)+ack(从机给主机发)+寄存器的地址(不同芯片寄存器地址不一样,有的是8bit址,也有16bit,如果是16bit,会拆分成高8bit和低8bit)+ack+start+7bit从机地址(高位在前低位在后)+1bit读(1)+ack+8bit的数据位+NACK+stop

char r_buf[]={寄存器地址};
char value;
struct i2c_msg r_msg[]={
    [0]={
        .addr=client->addr,
        .flags=0,
        .len=sizeof(r_buf),
        .buf=r_buf,   
    },
    [1]={
        .addr=client->addr,
        .flags=1,
        .len=1,
        .buf=&value,    
    },
};

十八,【SPI子系统】

1,分配设备驱动对象注册注销API

1.分配设备驱动对象
    struct spi_driver {
    const struct spi_device_id *id_table;
    int         (*probe)(struct spi_device *spi);
    int         (*remove)(struct spi_device *spi);
    struct device_driver    driver;
};
2.注册对象
    spi_register_driver(struct spi_driver *driver)
3.注销
    void spi_unregister_driver(struct spi_driver *driver)

4.一键注册宏 代替2.注册 3.注销
    module_spi_driver(__spi_driver) 

查看已经添加好的SPI4的设备树节点
spi4: spi@44005000 {
            #address-cells = <1>;
            #size-cells = <0>;
            compatible = "st,stm32h7-spi";
            reg = <0x44005000 0x400>;
            interrupts = <GIC_SPI 84 IRQ_TYPE_LEVEL_HIGH>;
            clocks = <&rcc SPI4_K>;
            resets = <&rcc SPI4_R>;
            dmas = <&dmamux1 83 0x400 0x01>,
                   <&dmamux1 84 0x400 0x01>;
            dma-names = "rx", "tx";
            power-domains = <&pd_core>;
            status = "disabled";
        };

修改spi4设备树节点以及添加m74hc595从机节点
    在stm32mp157a-fsmp1a.dts文件的根节点外部添加如下内容:
&spi4{
    pinctrl-names="default","sleep";
    pinctrl-0=<&spi4_pins_b>;//设置管脚复用
    pinctrl-1=<&spi4_sleep_pins_b>;
    cs-gpios = <&gpioe 11 0>;//设置片选引脚
      status="okay";      
      m74hc595@0{//添加m74hc595从机节点
      compatible="hqyj,m74hc595";
      reg=<0>;  
      spi-max-frequency=<10000000>;//max频率59MHZ
  };               
};

2,SPI数据收发相关的API

1,struct spi_device结构体
    当SPI设备与驱动匹配成功之后在内核中会存在一个struct spi_device对象保存匹配成功的设备的相关信息
struct spi_device {
    struct device       dev;//父类对象
    struct spi_controller   *controller;//用于索引总线驱动的对象指针
    struct spi_controller   *master;    /* compatibility layer */
    u32         max_speed_hz;//传输最大频率
    u8          chip_select;//片选编号
    u8          bits_per_word;//每个数据包包含的字节数
    bool            rt;//数据收发的标志
    u32         mode;//工作模式
        int         irq;
    void            *controller_state;
    void            *controller_data;
    char            modalias[SPI_NAME_SIZE];
    const char      *driver_override;
    int         cs_gpio;  //片选的GPIO编号
    struct gpio_desc    *cs_gpiod;  /* chip select gpio desc */
    struct spi_delay    word_delay; /* inter-word delay */

    /* the statistics */
    struct spi_statistics   statistics;

};

2,数据收发的函数
    int spi_read(struct spi_device *spi, void *buf, size_t len)
读
    int  spi_write(struct spi_device *spi, const void *buf, size_t len)
写
    int spi_write_then_read(struct spi_device *spi, const void *txbuf, unsigned n_tx, void *rxbuf, unsigned n_rx)

十九,【块设备驱动】

    块设备驱动一般用于访问存储设备,磁盘
    1. 块设备驱动的概念:系统中能够随机访问固定大小(1block 512byte)数据片的设备被称之为块设备。块设备一般都是以安装文件系统的方式使用,这也是块设备通常的访问方式。块设备访问方式是随机的

    2. 块设备中最小的可寻址单位是扇区,扇区大小一般是2的整数倍。最常见的大小是512字节。块是文件系统的一种抽象,只能基于块来访问文件系统。物理磁盘寻址是按照扇区的级别进行的,内核访问的所有磁盘操作又都是按照块进行的。扇区是设备的最小可寻址单位,所以块不能比扇区还小,只能数倍与扇区大小。

    3. 内核对块大小的要求是:必须是扇区大小的整数倍,并且小于页面的大小,所以块的大小通常是512,字节、1K或者4K。
    linux内存管理机制(页式管理)  windows(段式管理)

字符设备驱动和块设备驱动的对比
    1. 块设备接口相对复杂,不如字符设备明晰易用
    2. 块设备驱动程序对整个系统的性能影响较大,速度和效率是设计块设备驱动程序要重点考虑的问题
    3. 系统中使用缓冲区与访问请求的优化管理(电梯优化算法)(合并与重新排序)来提高系统性能

一些概念
    磁头:一个磁盘有多少个面就有多少个磁头
    磁道:在一个磁头上可以有很多环,这些环就叫做磁道
    扇区:磁道上访问数据的最小的单位就是扇区,一个扇区的大小就是512字节
    1block = 512字节 1024字节 2048字节 4096字节
    1扇区 = 512字节
    块设备的能存储的数据=磁头*磁道*扇区*512

    1. 虚拟文件系统(VFS):隐藏了各种硬件的具体细节,为用户操作不同的硬件提供了一个统一的接口。其基于不同的文件系统格式,比如EXT,FAT等。用户程序对设备的操作都通过VFS来完成,在VFS上面就是诸如open、close、write和read的函数API。 
    2. Disk Cache:硬盘的高速缓存,用户缓存最近访问的文件数据,如果能在高速缓存中找到,就不必去访问硬盘,毕竟硬盘的访问速度慢很多。
    3. 映射层(mapping layer):这一层主要用于确定文件系统的block size,然后计算所请求的数据包含多少个block。同时调用具体文件系统函数来访问文件的inode,确定所请求的数据在磁盘上面的逻辑地址(内核地址)。
    4. Generic Block Layer:通用块层,Linux内核把块设备看做是由若干个扇区组成的数据空间,上层的读写请求在通用块层被构造成一个或多个bio结构 
    5. I/O Scheduler Layer :I/O调度层,负责将通用块层的块I/O操作进行(电梯调度算法)调度、插入、暂存、排序、合并、分发等操作,对磁盘的操作更为高效。负责将通用块层的块I/O操作进行整合
    6. 块设备驱动层:在块系统架构的最底层,由块设备驱动根据排序好的请求,对硬件进行数据访问。

相关API

1.gendisk的结构体对象 
struct gendisk { 
    int major; //块设备的主设备号 
    int first_minor; //起始的次设备号 
    int minors; //设备的个数,分区的个数 
    char disk_name[DISK_NAME_LEN]; //磁盘的名字 
    struct disk_part_tbl *part_tbl; //磁盘分区表//磁盘的分区表的首地址 
    struct hd_struct part0; //part0分区的描述 
    const struct block_device_operations *fops; //块设备的操作方法结构体 
    struct request_queue *queue; //队列(重要) 
    void *private_data; //私有数据 
};
分区的结构体 
struct hd_struct { 
    sector_t start_sect; //起始的扇区号 
    sector_t nr_sects; //扇区的个数 
    int partno; //分区号 
};
//块设备的操作方法结构体 
struct block_device_operations { 
    int (*open) (struct block_device *, fmode_t); 
    int (*release) (struct gendisk *, fmode_t); 
    int (*ioctl) (struct block_device *, fmode_t, unsigned, unsigned 
long); 
    int (*getgeo)(struct block_device *, struct hd_geometry *); //设置磁盘的磁头,磁道,扇区的个数的。  
}

2.结构体对象的初始化 
    struct gendisk *mydisk; 
    struct gendisk *alloc_disk(int minors) //为块设备驱动对象申请空间
    //void put_disk(struct gendisk *disk) //释放对象空间
    功能:分配gendisk的内存,然后完成必要的初始化 
    参数:
        @minors:分区的个数 
    返回值:成功返回分配到的内存的首地址,失败返回NULL 

    int register_blkdev(unsigned int major, const char *name) 
    //void unregister_blkdev(unsigned int major, const char *name) 
    功能:申请设备设备驱动的主设备号 
    参数:
        @major : 0:自动申请[1..254] 
        >0 :静态指定 0-2^12 
        @name :名字 cat /proc/devices 
    返回值:
        major=0 ;成功返回主设备号,失败返回错误码 
        major>0 :成功返回0 ,失败返回错误码 

    void set_capacity(struct gendisk *disk, sector_t size)
    功能:用于设置扇区的大小
    每个扇区大小为512字节

    struct request_queue *blk_mq_init_sq_queue(struct blk_mq_tag_set *set,const struct blk_mq_ops *ops,unsigned int queue_depth,unsigned int set_flags) 
    //void blk_cleanup_queue(struct request_queue *q) 
    功能:用于在给定队列深度的情况下使用mq ops设置队列的助手,以及通过mq ops标志传递 的助手 
    参数: 
        set@被初始化的tag对象,tag被上层使用,里面包含硬件队列的个数,队列的操作方法结构体,标志位等
        @放入到tag中的操作方法结构体 ,主要完成队列中成员的各种操作
        @ tag中指定支持的队列深度 ,一个队列中tag对象的个数
        @将tag中队列的处理标志位,例如BLK_MQ_F_SHOULD_MERGE, BLK_MQ_F_BLOCKING等 
    返回值:成功返回队列指针,失败返回错误码指针 
3.注册、注销 
    void add_disk(struct gendisk *disk) //注册 
    void del_gendisk(struct gendisk *disk) //注销 

二十,【内核空间的内存分配】

API

void *kmalloc(size_t s, gfp_t gfp)
    功能:分配对应的虚拟内存 (物理内存映射区)
    参数:size:分配内存区的大小
        flags:内存分配标志
        GFP_KERNEL:内核可能被休眠,用于进程上下文中
        GFP_ATOMIC:处理紧急的事务,用在中断上下文
    返回值:对应虚拟地址 
    特点:最大128k , 分配虚拟地址,其虚拟地址空间连续, 
    物理地址空间也是连续,分配的内存必须是2的次幂的形式 
    类似函数:kzalloc = kmalloc+memset(,0,):分配虚拟内存区并清零 

void kfree(const void *x) 
    功能:释放对应的虚拟内存 
    参数:x:虚拟内存的起始地址 
    返回值:无 

void *vmalloc(unsigned long size) 
    功能:分配对应的虚拟内存 
    参数:size:分配内存区的大小 
    返回值:对应虚拟地址 
    特点:分配虚拟地址,其虚拟地址空间连续, 但是物理地址空间不一定连续 

void vfree(const void *addr) 
    功能:释放对应的虚拟内存 
    参数:addr:虚拟内存区的首地址 
    返回值:无 

unsigned long __get_free_page(gfp_t gfp) 
    功能:分配一个页的内存 4K 

void free_page(unsigned long addr) 
    释放一个页的内存 

unsigned long __get_free_pages(gfp_t gfp_mask, get_order(57600)) 
    功能:分配多个页的内存 
        57600-->2^n :第二个参数填写的是n 
    n = get_order(57600)  

void free_pages(unsigned long addr, unsigned long order) 
    释放多个页的内存

                                                                                        完

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/MaGuangming001/article/details/132889589

智能推荐

JWT(Json Web Token)实现无状态登录_无状态token登录-程序员宅基地

文章浏览阅读685次。1.1.什么是有状态?有状态服务,即服务端需要记录每次会话的客户端信息,从而识别客户端身份,根据用户身份进行请求的处理,典型的设计如tomcat中的session。例如登录:用户登录后,我们把登录者的信息保存在服务端session中,并且给用户一个cookie值,记录对应的session。然后下次请求,用户携带cookie值来,我们就能识别到对应session,从而找到用户的信息。缺点是什么?服务端保存大量数据,增加服务端压力 服务端保存用户状态,无法进行水平扩展 客户端请求依赖服务.._无状态token登录

SDUT OJ逆置正整数-程序员宅基地

文章浏览阅读293次。SDUT OnlineJudge#include<iostream>using namespace std;int main(){int a,b,c,d;cin>>a;b=a%10;c=a/10%10;d=a/100%10;int key[3];key[0]=b;key[1]=c;key[2]=d;for(int i = 0;i<3;i++){ if(key[i]!=0) { cout<<key[i.

年终奖盲区_年终奖盲区表-程序员宅基地

文章浏览阅读2.2k次。年终奖采用的平均每月的收入来评定缴税级数的,速算扣除数也按照月份计算出来,但是最终减去的也是一个月的速算扣除数。为什么这么做呢,这样的收的税更多啊,年终也是一个月的收入,凭什么减去12*速算扣除数了?这个霸道(不要脸)的说法,我们只能合理避免的这些跨级的区域了,那具体是那些区域呢?可以参考下面的表格:年终奖一列标红的一对便是盲区的上下线,发放年终奖的数额一定一定要避免这个区域,不然公司多花了钱..._年终奖盲区表

matlab 提取struct结构体中某个字段所有变量的值_matlab读取struct类型数据中的值-程序员宅基地

文章浏览阅读7.5k次,点赞5次,收藏19次。matlab结构体struct字段变量值提取_matlab读取struct类型数据中的值

Android fragment的用法_android reader fragment-程序员宅基地

文章浏览阅读4.8k次。1,什么情况下使用fragment通常用来作为一个activity的用户界面的一部分例如, 一个新闻应用可以在屏幕左侧使用一个fragment来展示一个文章的列表,然后在屏幕右侧使用另一个fragment来展示一篇文章 – 2个fragment并排显示在相同的一个activity中,并且每一个fragment拥有它自己的一套生命周期回调方法,并且处理它们自己的用户输_android reader fragment

FFT of waveIn audio signals-程序员宅基地

文章浏览阅读2.8k次。FFT of waveIn audio signalsBy Aqiruse An article on using the Fast Fourier Transform on audio signals. IntroductionThe Fast Fourier Transform (FFT) allows users to view the spectrum content of _fft of wavein audio signals

随便推点

Awesome Mac:收集的非常全面好用的Mac应用程序、软件以及工具_awesomemac-程序员宅基地

文章浏览阅读5.9k次。https://jaywcjlove.github.io/awesome-mac/ 这个仓库主要是收集非常好用的Mac应用程序、软件以及工具,主要面向开发者和设计师。有这个想法是因为我最近发了一篇较为火爆的涨粉儿微信公众号文章《工具武装的前端开发工程师》,于是建了这么一个仓库,持续更新作为补充,搜集更多好用的软件工具。请Star、Pull Request或者使劲搓它 issu_awesomemac

java前端技术---jquery基础详解_简介java中jquery技术-程序员宅基地

文章浏览阅读616次。一.jquery简介 jQuery是一个快速的,简洁的javaScript库,使用户能更方便地处理HTML documents、events、实现动画效果,并且方便地为网站提供AJAX交互 jQuery 的功能概括1、html 的元素选取2、html的元素操作3、html dom遍历和修改4、js特效和动画效果5、css操作6、html事件操作7、ajax_简介java中jquery技术

Ant Design Table换滚动条的样式_ant design ::-webkit-scrollbar-corner-程序员宅基地

文章浏览阅读1.6w次,点赞5次,收藏19次。我修改的是表格的固定列滚动而产生的滚动条引用Table的组件的css文件中加入下面的样式:.ant-table-body{ &amp;amp;::-webkit-scrollbar { height: 5px; } &amp;amp;::-webkit-scrollbar-thumb { border-radius: 5px; -webkit-box..._ant design ::-webkit-scrollbar-corner

javaWeb毕设分享 健身俱乐部会员管理系统【源码+论文】-程序员宅基地

文章浏览阅读269次。基于JSP的健身俱乐部会员管理系统项目分享:见文末!

论文开题报告怎么写?_开题报告研究难点-程序员宅基地

文章浏览阅读1.8k次,点赞2次,收藏15次。同学们,是不是又到了一年一度写开题报告的时候呀?是不是还在为不知道论文的开题报告怎么写而苦恼?Take it easy!我带着倾尽我所有开题报告写作经验总结出来的最强保姆级开题报告解说来啦,一定让你脱胎换骨,顺利拿下开题报告这个高塔,你确定还不赶快点赞收藏学起来吗?_开题报告研究难点

原生JS 与 VUE获取父级、子级、兄弟节点的方法 及一些DOM对象的获取_获取子节点的路径 vue-程序员宅基地

文章浏览阅读6k次,点赞4次,收藏17次。原生先获取对象var a = document.getElementById("dom");vue先添加ref <div class="" ref="divBox">获取对象let a = this.$refs.divBox获取父、子、兄弟节点方法var b = a.childNodes; 获取a的全部子节点 var c = a.parentNode; 获取a的父节点var d = a.nextSbiling; 获取a的下一个兄弟节点 var e = a.previ_获取子节点的路径 vue