驱动开发--汇总

2023-11-07

一,【驱动相关概念】

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)
驱动端

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

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

驱动开发--汇总 的相关文章

  • 在我的 index.php 中加载 CSS 和 JS 等资源时出现错误 403

    我使用的是 Linux Elementary OS 并在 opt 中安装了 lampp My CSS and JS won t load When I inspect my page through browser The console
  • docker容器大小远大于实际大小

    我正在尝试从中构建图像debian latest 构建后 报告的图像虚拟大小来自docker images命令为 1 917 GB 我登录查看尺寸 du sh 大小为 573 MB 我很确定这么大的尺寸通常是不可能的 这里发生了什么 如何获
  • Ubuntu 上的 Python 2.7

    我是 Python 新手 正在 Linux 机器 Ubuntu 10 10 上工作 它正在运行 python 2 6 但我想运行 2 7 因为它有我想使用的功能 有人敦促我不要安装 2 7 并将其设置为我的默认 python 我的问题是 如
  • Gtk-ERROR **:检测到 GTK+ 2.x 符号

    我正在使用 gcc 编译我的 c 应用程序 并使用以下标志 gcc evis c pkg config cflags libs gtk 2 0 libs clutter gtk 1 0 libs gthread 2 0 Wall o evi
  • 是否可以创建一个脚本来保存和恢复权限?

    我正在使用 Linux 系统 需要对一组嵌套文件和目录进行一些权限实验 我想知道是否没有某种方法可以保存文件和目录的权限 而不保存文件本身 换句话说 我想保存权限 编辑一些文件 调整一些权限 然后将权限恢复到目录结构中 将更改的文件保留在适
  • 我不明白 execlp() 在 Linux 中如何工作

    过去两天我一直在试图理解execlp 系统调用 但我还在这里 让我直奔主题 The man pageexeclp 将系统调用声明为int execlp const char file const char arg 与描述 execl exe
  • “make install”将库安装在 /usr/lib 而不是 /usr/lib64

    我正在尝试在 64 位 CentOS 7 2 上构建并安装一个库 为了这个目的我正在跑步 cmake DCMAKE BUILD TYPE Release DCMAKE INSTALL PREFIX usr DCMAKE C COMPILER
  • php exec 返回的结果比直接进入命令行要少

    我有一个 exec 命令 它的行为与通过 Penguinet 给 linux 的相同命令不同 res exec cd mnt mydirectory zcat log file gz echo res 当将命令直接放入命令行时 我在日志文件
  • 进程退出后 POSIX 名称信号量不会释放

    我正在尝试使用 POSIX 命名信号量进行跨进程同步 我注意到进程死亡或退出后 信号量仍然被系统打开 在进程 打开它 死亡或退出后是否有办法使其关闭 释放 早期的讨论在这里 当将信号量递减至零的进程崩溃时 如何恢复信号量 https sta
  • C++ Boost ASIO 简单的周期性定时器?

    我想要一个非常简单的周期性计时器每 50 毫秒调用我的代码 我可以创建一个始终休眠 50 毫秒的线程 但这很痛苦 我可以开始研究用于制作计时器的 Linux API 但它不可移植 I d like使用升压 我只是不确定这是否可能 boost
  • Urwid:使光标不可见

    我正在使用 urwid 它是一个用于在 ncurses 中设计终端用户界面的 Python 框架 但有一件事我在 urwid 中无法做到 而这在 Curses 中很容易做到 使光标不可见 现在 选择按钮时光标是可见的 而且看起来很丑 有办法
  • 高效的内存屏障

    我有一个多线程应用程序 其中每个线程都有一个整数类型的变量 这些变量在程序执行期间递增 在代码中的某些点 线程将其计数变量与其他线程的计数变量进行比较 现在 我们知道在多核上运行的线程可能会无序执行 一个线程可能无法读取其他线程的预期计数器
  • PyQt5 - 无法使用 QVideoWidget 播放视频

    from PyQt5 QtWidgets import from PyQt5 QtMultimedia import from PyQt5 QtMultimediaWidgets import from PyQt5 QtCore impor
  • 捕获实时流量时如何开启纳秒精度?

    如何告诉 libpcap v1 6 2 将纳秒值存储在struct pcap pkthdr ts tv usec 而不是微秒值 捕获实时数据包时 Note This question is similar to How to enable
  • git 错误:无法处理 https

    当我尝试使用 git clone 时https xxx https xxx我收到以下错误我不处理协议 https 有人可以帮我吗 完整消息 dementrock dementrock A8Se git 克隆https git innosta
  • 使用 plistBuddy 获取值数组

    var keychain access groups declare a val usr libexec PlistBuddy c Print var sample plist echo val echo val 0 Ouput Array
  • 限制 Imagemagick 使用的空间和内存

    我在 Rails 应用程序上使用 Imagemagick 使用 rmagick 但我的服务器 Ubuntu 不是很大 当我启动转换进程时 Imagemagick 占据了我的服务器 30GB HDD 的所有位置 内存 我想限制内存和 tmp
  • 如何从 PROC 获取有关子进程的信息

    我正在尝试编写一个以几个进程作为参数的程序 然后父进程执行每个子进程并打印出一些相关的统计信息 示例 generate ls l 将生成一个程序 打印出有关 ls l 的一些统计信息 特别是其系统时间 用户时间和上下文切换次数 我不想使用
  • Ubuntu systemd 自定义服务因 python 脚本而失败

    希望获得有关 Ubuntu 中的 systemd 守护进程服务的一些帮助 我写了一个 python 脚本来禁用 Dell XPS 上的触摸屏 这更像是一个问题 而不是一个有用的功能 该脚本可以工作 但我不想一直启动它 这就是为什么我想到编写
  • Fedora dnf 更新不起作用?

    当我尝试使用 update 命令更新 Fedora 22 时 sudo dnf update 我收到以下错误 错误 无法同步存储库 更新 的缓存 无法准备内部镜像列表 Curl 错误 6 无法解析主机名 无法解析主机 mirrors fed

随机推荐

  • mybatis-plus-generator生成实体类时添加jdbcType

    效果 需要修改的文件 1 基本思路 1 使用变量 useJdbcType 控制是否需要生成jdbcType 2 生成时拼接相关字段信息 2 步骤 2 1重写TableField 添加jdbcType属性 在com baomidou myba
  • rollup怎么处理.node文件

    Rollup 是一个 JavaScript 模块打包器 它可以将多个模块合并成单个文件 它可以帮助你将你的代码打包成可以在浏览器中运行的文件 要使用 Rollup 处理 node 文件 你需要使用一个 Rollup 插件 如 rollup
  • 数据挖掘基础

    提示 文章写完后 目录可以自动生成 如何生成可参考右边的帮助文档 文章目录 前言 一 数据挖掘定义及用途 1 定义 2 用途 二 决策树 1 理论知识 1 概念 2 算法一般过程 C4 5为例 2 小结 三 关联规则 1 概述 2 关联分析
  • MySQL之多表关联删除/更新

    日常测试的时候 需要连接其他表而删除某些脏数据 按照正常的查询的写法 会这样写删除语句 DELETE from order where id in SELECT o id from order o LEFT JOIN customer c
  • minicom键盘失效,不能输入问题

    转 http blog sina com cn s blog 5d0e8d0d01015svy html 这个问题可以参考secureCRT的时候遇到的问题 问题与minicom的一样 RTS 请求发送 CTS 清除发送 默认情况下mini
  • vue3配置proxy解决跨域

    跨域问题是前端开发中较常见的问题 因为javascript的浏览器会支持同源策略 如果域名 协议 端口任意不同就会产生跨域 如果非同源 那么在请求数据时 浏览器会在控制台中报一个异常 提示拒绝访问 错误信息如下 Access to XMLH
  • Linux常用命令整理(适合初学)

    关机 重启操作 帮助文档 1 帮助命令 1 1 help help cd 查看cd命令的帮助信息
  • 服务器推送消息SSE,HTTP 服务器消息推送之SSE

    HTTP 服务器推送也称 HTTP 流 是一种客户端 服务器通讯模式 它将信息从 HTTP 服务器异步推送到客户端 而无需客户端请求 如今的 web 和 app 中 愈来愈多的场景使用这种通讯模式 好比实时的消息提醒 IM在线聊天 多人文档
  • 将字符串转化为整数

    Java内部实现 param s the code String containing the integer representation to be parsed param radix the radix to be used whi
  • unet测试评估metric脚本

    全部复制的paddleseg的代码转torch import argparse import logging import os import numpy as np import torch import torch nn functio
  • 第十三届蓝桥杯 2022年省赛真题(Java 大学C组)

    蓝桥杯 2022年省赛真题 Java 大学C组 目录 试题 A 排列字母 试题 B 特殊时间 试题 C 纸张尺寸 试题 D 求和 试题 E 矩形拼接 试题 F 选数异或 试题 G GCD 试题 H 青蛙过河 试题 I 因数平方和 试题 J
  • 43 openEuler搭建Apache服务器-配置文件说明和管理模块

    文章目录 43 openEuler搭建Apache服务器 配置文件说明和管理模块 43 1 配置文件说明 43 2 管理模块和SSL 43 2 1 概述 43 2 2 加载模块 43 2 3 SSL介绍 43 openEuler搭建Apac
  • 浅谈(Java)JUC线程池ScheduledThreadPoolExecutor

    博主介绍 程序员悟啦 乌拉 个人仓库 码云 座右铭 懒 对一个人的毁灭性有多大 早起的重要性就多大 免责声明 文章由博主原创 部分文章整理于网络 仅供学习和知识分享 相遇是缘 既然来了就拎着小板凳 坐下来一起唠会儿 如果在文中有所收获 请别
  • 《银行法律法规》二、银行业务——3、结算、代理、托管业务

    第三章 结算 代理及托管业务 第一节 支付结算业务 考点1 国内结算 支付结算 是指结算客户之间由于商品交易 劳务供应等经济活动而产生的债权债务关系 通过银行实现资金转移而完成的结算过程 支付结算是银行的一项基础性服务 支付结算应遵循恪守信
  • 《北京市政务服务领域区块链应用创新蓝皮书(第一版)》正式发布

    为加快推动区块链技术和产业创新发展 北京市政务服务管理局 北京市科委 北京市经济和信息化局组织相关单位抓紧推进政务服务领域区块链应用建设 取得阶段性成果 在此基础上 北京市区块链工作专班专家组编制了 北京市政务服务领域区块链应用创新蓝皮书
  • SDC设计约束——IO延时约束

    原文链接 https juejin cn post 7123461617299226660 约束命令 set input delay clock CLK max 2 0 get ports IN set input delay clock
  • 百度地图+谷歌地图 勾画 行政区块边界

    最近公司做项目 需要用在谷歌地图上勾勒出搜索的行政区块的轮廓 于是在找各种资料 最后好像是有末尾CSDN的网友提供了思路 具体看正文 效果如下 先贴代码 html view plain copy
  • npm更换成淘宝镜像源以及cnpm

    需求由来 由于node安装插件是从国外服务器下载 受网络影响大 速度慢且可能出现异常 所以如果npm的服务器在中国就好了 所以我们乐于分享的淘宝团队 阿里巴巴旗下业务阿里云 干了这事 来自官网 这是一个完整 npmjs org 镜像 你可以
  • EasyPoi实现excel多sheet导入

    EXCEL 导入模板 MAVEN
  • 驱动开发--汇总

    一 驱动相关概念 1 什么是驱动 能够驱使硬件实现特定功能的软件代码 根据驱动程序是否依赖于系统内核将驱动分为裸机驱动和系统驱动 2 逻辑驱动和系统驱动的区别 裸机驱动 编写的驱动代码中没有进行任何内核相关API的调用 开发者自己配置寄存器