一,【驱动相关概念】
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, ulongcharp: 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的资源APIEXPORT_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:映射的物理内存大小返回值:成功返回映射得到的虚拟内存首地址,失败返回NULL2.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_MODULEname:向上提交的目录名返回值:成功返回申请的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:当前申请的对象前一个节点的地址,不知道就填 NULLdevt:设备号 主设备号<<20|次设备号dridata:申请的device对象的私有数据,填写NULLfmt:向上提交的设备节点名...:不定长参数 返回值:成功返回申请到的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/ioctlvi ioctl-decoding.rst====== ==================================31-30 00 - no parameters: uses _IO macro10 - read: _IOR01 - write: _IOW11 - read/write: _IOWR29-16 size of arguments15-8 ascii character supposedlyunique to each driver7-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_MODULEconst 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;//文件的用户IDkgid_t i_gid;//组IDunsigned 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_flagsfmode_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://控制LED1switch(cmd){case LED_ON://开灯break;case LED_OFF://关灯break; }break;case 1://控制LED2case 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实现相关的API1.定义一个等待队列头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,如果进程被外部信号中断休眠则返回错误码 -ERESTARTSYS5.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
返回值:成功返回根节点对应的文件描述符,失败返回-1int 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,失败返回-1int 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:要发生的信号 SIGIOband:发送信号时添加的事件标志 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 = yCONFIG_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 requireda-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;//设备树节点的名字mynodephandle phandle;//节点标识const char *full_name;//全名 mynode@0x12345678struct 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:要解析的设备树节点的名字返回值:成功返回目标节点首地址,失败返回NULLstruct device_node *of_find_node_by_path(const char *path);功能:根据设备树节点路径解析设备树节点信息参数:path:设备树所在的节点路径,非文件路径 例:/mynode@0x12345678 返回值:成功返回目标节点首地址,失败返回NULLstruct device_node *of_find_compatible_node(struct device_node *from, const char *type, const char *compat);功能:根据节点的厂商信息解析指定的节点参数:from:要解析的节点所在子树的根节点,填NULL,默认从根节点解析type:设备类型,填NULLcompat: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.dtsipinctrl: 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";//指定控制器名字为GPIOEstatus = "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.txtThe 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,注销定时器jiffiesjiffies是内核中用于保存内核节拍数的一个变量。它的值从内核启动开始不断的从0开始增加内核频率:内核节拍数一秒增加的数量称为内核的频率,内核的频率在内核顶层目录下的.config文件中被设置linux中: CONFIG_HZ = 100内核定时器对象分析:
struct timer_list {struct hlist_node entry;//用于构成一个对象链表unsigned long expires;//设置的时间阈值 == 定时一秒:jiffies+CONFIG_HZvoid (*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:设备类型,填NULLcompat: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>;//引用中断父节点为extigpiof: 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>;//中断父节点为intcexti: 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.txt1) Interrupt client nodes
Example:interrupt-parent = <&intc1>;interrupts = <5 0>, <6 0>;
Example:interrupts-extended = <&intc1 5 1>, <&intc2 1 0>;
2) Interrupt controller nodesb) two cellsbits[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个)。软中断一般留给内核开发者使用。tasklettasklet是基于软中断的工作原理进行的,可以进行一些耗时任务,但是不能在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,则被设置为trueunion {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,否则为0struct 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 71resource_size_t end;//资源的终止值 0X50006000+4 0X50006000+0X400 71const 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开始返回值:成功返回要获取的资源对象指针,失败返回NULLint 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];//要匹配的设备树节点的compatibleconst void *data;//当前匹配项的私有数据
};构建用于设备树匹配的表
struct of_device_id oftable[] = {{ .compatible = "hqyj,myplatform", },{ /* end node */ },//防止数组越界
};
十七,【I2C子系统】
1,将核心层和总线驱动层配置进内核
*********************配置核心层*************************1.找到核心层代码目录:内核顶层目录/drivers/i2c2. 内核顶层目录执行make menuconfig3. > Device Drivers > I2C support ->-*-I2C support4.保存退出********************配置总线驱动层************1.找到iic总线驱动层代码目录:内核顶层目录/drivers/i2c/busses2.内核顶层目录执行make menuconfig3. > Device Drivers > I2C support > I2C Hardware Bus support-》<*> STMicroelectronics STM32F7 I2C support 4.保存退出*************编译**********1.内核顶层目录下执行make uImage LOADADDR=0XC20000002.cp 内核层目录/arch/arm/boot/uImage ~/tftpboot3.重启开发板
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.txtRequired 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)
-----------------------------
- pinctrladd extra pinctrl to configure SCL/SDA pins to GPIO function for busrecovery, call it "gpio" or "recovery" (deprecated) state
Required properties (per child device)
--------------------------------------- compatiblename of I2C slave device- regOne 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->adaptermsgs:要传输的一个或者多个消息 一个消息是以起始信号作为出发点num:传输的消息的数量
返回值:成功返回传输的消息个数,失败返回错误码消息结构体 struct i2c_msgI2C总线上传输的内容属于消息,一条消息中要包含从机地址、读写标志位以及消息的正文
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+stopchar 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+stopchar 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, },
};