Loading

驱动开发笔记

Linux驱动开发


一、Linux内核模块


【1】什么是驱动?

​ 能够控制硬件工作的软件代码就是驱动

1、ARM裸机开发与Linux设备驱动区别

1.是否有操作系统
2.编程方式不同(逻辑全部自己实现,驱动在内核的基础上开发)
3.是否具备多进程,多线程(arm不可以,驱动可以)
4.一个可以直接操作物理地址,一个不可以直接操作物理地址

共同点:
	驱动硬件工作的代码,就是驱动(软件)
不同点:
	ARM裸机驱动:是不依赖内核代码,直接通过C语言来操作硬件寄存器实现硬件工作的代码
	linux设备驱动:必须依赖linux内核设备驱动的框架,借助内核提供的驱动函数接口,来实现操作硬件工作的过程的代码

【2】linux系统层次

应用层:app(glibc库)(0~3G)

内核层:内核五大功能(3~4G)

硬件层:LED LCD TOUCHSCREEN camera 声卡 网卡 emmc

【2】内核五大功能(3~4G)

1、内存管理

管理内存的申请,释放,映射等等

2、文件管理

通过文件系统ext2/ext3/ext4 jiffs yaffs等来组织管理文件

3、进程管理

进程的创建,销毁,进程调度等

4、网络管理

通过网络协议栈数据的封装和拆分的过程

5、设备管理:设备驱动的管理

字符设备、块设备、网卡设备
安卓手机:linux
苹果手机:ulinux

【3】设备驱动种类的划分

1、字符设备驱动

LED BEEP DS18B20 LCD(帧缓存) Touchscreen camera  声卡 显卡
概念:以字节流来访问的设备,并且只能顺序访问的设备就属于字符设备驱动,百分之九十的设备都是字符设备驱动

补充:什么是字节流?
答:字节流向,一个一个显示完成

2、块设备驱动

U盘 硬盘 
概念:按照块(block:512字节 1K 4K)来进行访问的,可以顺序访问,也可以无序访问

硬盘一个500G
2G           200G          9G   
电影         嵌入式资料     剩余
电影看完删除,释放2G空间
存储虚拟机,假如10G
存储的时候,磁盘就是无序访问

3、网卡设备驱动

cs8900 dm9000 路由器
1.没有设备节点
2.网络设计早于内核设计,只能通过socket进行访问的
3.通过网路协议栈,和网卡驱动进行交互,实现数据收发设备,这些设备就属于网卡设备驱动
bsp-lcd

【4】内核模块三要素

1、入口(安装)

1.硬件资源申请
2.驱动对象的申请

链接脚本作用:告诉编译器如何进行编译的

//入口
static int __init demo_init(void)
{
    //__init:
    section:程序编译完成,段的运行
    内核的链接脚本文件:vmlinux.lds
    0xC0000000
    .init.text:可以找到这个段
    //告诉编译器将demo_init函数放入到.init.text段中
    return 0;
}
//将demo_init的地址告诉给内核
module_init(demo_init);

2、出口(卸载)

1.释放硬件资源
2.释放驱动对象
static void __exit demo_exit(void)                     
{
	//demo_exit:
	//告诉编译器将demo_exit函数放入到.exit.text段中
}
module_exit(demo_init);//告诉内核出口函数

3、许可证

Linux内核是开源,需要遵循GPL协议(GNU开源组织)
MODULE_LICENSE("GPL");

【4】驱动模块的编译

1、在内核源码进行编译

2、在内核源码外进行编译

KERNELDIR:=/home/linux/kernel/kernel-3.4.39/
PWD:=$(shell pwd)
//makefile中的变量
all:
	make -C $(KERNELDIR) M=$(PWD) modules
	//make -C $(KERNELDIR)
	//进入内核的顶层目录下,执行make modules
	//M=$(PWD) 驱动路径,指定要编译模块的路径
	//编译模块,通过M指定编译模块的路径在当前路径下
clean:
	make -C $(KERNELDIR) M=$(PWD) clean
	//清除编译的中间的信息
obj-m:=demo.o //指定内核模块的名字

【5】内核中的打印语句

1、内核中打印级别

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

2、打印格式

printk(打印级别 "控制字段",变量);===>指定的消息级别
printk("控制字段",变量); ===>默认的消息级别

3、如何查看消息打印级别

cat /proc/sys/kernel/printk
4          4              1                     7
(控制台的打印级别)终端级别  默认消息级别   控制台最大级别       控制台最小级别
只有当消息的级别大于终端级别的时候,消息才会在终端上显示

4、Ubuntu默认消息级别的修改方法

1.su root
2.echo 4 3 1 7 >/proc/sys/kernel/printk
3.exit
4.cat /proc/sys/kernel/printk

5、开发板默认消息级别的修改方法

1.vi ~/rootfs/etc/init.d/rcS
2.添加下面一句话
echo 4 3 1 7 /proc/sys/kernel/printk

【6】Linux内核模块传参

1、接受命令行传递的参数接口

module_param(name,type,perm)
功能:接受命令行传递的参数
参数:@name:变量的名字
	 @type:变量的类型
	 @perm:权限

1.1 在安装驱动的时候传递参数

sudo insmod demo.ko num=300

1.2 通过sysfs文件系统中的属性文件来修改

/sys/module/驱动的名字/parameters
num 《========================》文件属性
su root
echo 400 > num
cat num
400
假如在公司做led屏幕,厂商只给.ko文件,编译器版本不一样,源文件不给你,但是驱动模块给你用

2、对传参的变量进行字符串描述

MODULE_PARM_DESC(_parm,desc)
功能:对传参的变量进行字符串描述
参数:@_parm:变量名
     @desc:描述的字符串    

3、模块传参的作用

在模块运行的时候,通过模块传参的方式修改里面的值

【7】模块导出符号表

补充:A和B两个进程,假如A进程有个add函数,B进程无法调用这个函数,A和B进程拥有自己的0~3G,两个独立的进程,需要借助内核空间

假如有两个模块,都在3G~4G空间

1、内核为什么要提供导出符号表的函数

1.解决内核中代码冗余的问题
2.模块和模块之间功能独立,只需要通过到处符号表的过程就可以让一个复杂驱动,通过调用别人写好的函数完成对应的功能

2、导出符号表函数接口

EXPORT_SYMBOL_GPL(sym)
功能:将sym的符号表导出
参数:@sym:符号表(函数名,变量名)

3、提供者和调用者编译

1.先编译提供函数的模块,编译之后生成module.symvers
2.将这个文件拷贝到调用者模块下,在编译调用者模块

4、提供者和调用者安装

先安装提供者模块
在安装调用者模块

5、提供者和调用者卸载

先卸载调用者
在卸载提供者

6、如何查看一个内核模块依赖的模块

输入命令:modinfo  xxx.ko
打印结果:
filename:demoB.ko
license:GPL
srcversion: 34D79D4CA1EFC3A13165092
depends:  demoA
vermagic: 3.5.0-23-generic SMP mod_unload modversions 686 

【8】安装依赖模块的命令

1、安装命令

2、卸载命令

二、字符设备驱动


【1】字符设备驱动框架

应用层:提供操作逻辑

内核:提供工具,提供操作硬件的方法

设备号:设备驱动的标识,32位无符号整数

设备号 = 主设备(高12) + 次设备(低20)

【2】字符设备驱动接口

1、注册字符设备驱动函数

register_chrdev(unsigned int major, const char *name,  const struct file_operations *fops)
函数功能:注册字符设备驱动
参数:
1.major:主设备号 (查看路径:cat /proc/devices)
	major > 0 系统认为驱动工程师自动指定设备号
	major = 0 系统自动分配设备号
2.name:设备的名字 (cat /proc/devices)
3.fops:操作方法结构体
	struct file_operations {
	ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
	ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
	}
返回值:
	如果主设备号:major > 0 成功返回0
	如果主设备号:major = 0 成功 返回设备号
	失败返回错误码

2、注销字符设备驱动函数

void unregister_chrdev(unsigned int major, const char *name)
函数功能:注销字符设备驱动
参数:
	1.major:主设备号
	2.name:设备的名字
返回值:无

【3】创建设备节点

1.问题:字符设备创建成功,如何查看字符设备主设备号
  答:在/proc/devices目录下,查看注册字符设备驱动对应名字,申请到的设备号
2.创建设备节点命令
    sudo mknod /dev/mycdev0 c 250 10
    命令解析:
    mknod:创建设备节点的命令
    /dev/mycdev0:创建设备节点路径和名字(路径可以是任意的)
    c/b:字符设备驱动/块设备驱动
    250:主设备号
    0:次设备号【0~255】任意的一个值
3.如何查看自己创建设备节点
	输入命令:ls /dev/mycdev0 -l
	crw-r--r-- 1 root root 250, 10 Apr 21 11:17 /dev/mycdev0

【4】编写应用程序测试是否能调到驱动

【5】用户空间和内核空间数据传输

思考:用指针传输是否可以?

内核空间定义指针p,访问用户空间,如果ctrl+c结束,内核崩溃,这样使用不是很安全,只有内核驱动才可以让内核奔溃

数据修改都是在内核空间或者用户空间

1、用户空间数据拷贝到内核空间

#include <linux/uaccess.h>
long copy_from_user(void *to,
		const void __user * from, unsigned long n)
函数功能:从用户空间拷贝数据到内核空间(驱动中write函数)
参数:
	1.to:内核空间首地址
	2.from:用户空间首地址
	3.n:拷贝的大小(单位是字节)
返回值:
	成功返回:0
	失败返回未拷贝的字节个数

2、用户空间读内核空间数据

long copy_to_user(void __user *to,
		const void *from, unsigned long n)
函数功能:从内核空间拷贝数据到用户空间(驱动中read函数)
参数:
	1.to:用户空间首地址
	2.from:内核空间首地址
	3.n:拷贝大小(单位是字节)
返回值:
	成功返回:0
	失败返回未拷贝的字节个数	

【6】地址映射

1、驱动中为什么需要地址映射

1.如果想让RGB_LED亮灭,需要设置led灯的寄存器2.但是寄存器的地址是物理地址,驱动中使用的地址是虚拟地址3.如果使用驱动操作led灯,需要将led灯的物理地址映射成虚拟地址(mmu)4.映射之后在内核中操作虚拟地址就相当于操作物理地址

2、地址映射API接口

void *ioremap(phys_addr_t offset, unsigned long size)功能:将物理地址映射程虚拟地址参数:	1.offset:物理地址	2.size:映射的大小返回值:	成功:返回虚拟地址	失败:返回NULL以点亮led为例:红灯:GPIOA28 0xc001a000绿灯:GPIOE13 0xc001e000蓝灯:GPIOB12 0xc001b000

3、取消地址映射API接口

void iounmap(void __iomem *addr)功能:取消映射参数:	1.addr:虚拟地址返回值:无

【7】ioctl函数的使用

1、应用层ioctl函数

 int ioctl(int fd, int request, ...) 函数功能:控制设备 参数: 	1.fd:文件描述符 	2.request:命令码 	3........:可变参数,填写地址 返回值:成功返回0 	    失败返回-1,并置位错误码

2、内核层unlocked_ioctl函数

long (*unlocked_ioctl) (struct file *file, unsigned int cmd, unsigned long args);#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)))命令码各个bit位的含义:#define _IOC(dir,type,nr,size) \		(((dir)  << 30) | 		 ((type) << 8) | 		 ((nr)   << 0) | 		 ((size) << 16))方向       大小      类型       功能31~30     29~16     15~8       7~0

【8】如何自动创建设备节点(代替mknod)

1、自动创建设备节点流程

devfs:自动创建设备节点机制(在2.4内核之前),创建节点放在内核里面,内核用来服务于用户

udev:2.6内核之后使用,创建设备节点

udev/mdev:mdev是轻量级udev,进行瘦身(类似于strip),在嵌入式设备中使用的是mdev创建的节点

2、提交目录信息函数接口 class_create

#include <linux/device.h>struct class * class_create(owner, name)函数功能:在/sys/class下创建name名字的目录(提交目录)参数:	1.owner:THIS_MODULE(给编译器使用的)	2.name:目录名,是一个文件,填写字符串返回值:成功返回struct class*的结构体指针	失败返回错误码指针		如何判断cls是成功还是失败呢?答:判断地址区间是否在内核顶端的4K位置,如果在就是错误例:if(IS_ERR(cls))	{		return PTR_ERR(cls);		//将错误码指针转化为错误码	}	思考:宏定义时do...while(0)和({})区别?({}):有返回值do...while(0):没有返回值

3、提交创建节点信息函数接口device_create

struct device *device_create(struct class *class, 			struct device *parent,dev_t devt, void *drvdata,const char *fmt, ...)函数功能:提交设备节点信息参数:	1.class:目录的句柄 	2.parent:NULL	3.devt:设备号	4.drvdata:NULL	5.fmt:设备节点的名字返回值:成功返回struct device *的结构体指针		失败返回错误码指针

【9】获取主次设备号函数

1、通过设备号获取主设备号

#define MAJOR(dev)	((unsigned int) ((dev) >> MINORBITS))

2、通过设备号拿到次设备号

#define MINOR(dev)	((unsigned int) ((dev) & MINORMASK))

3、通过major和minor合成设备号

#define MKDEV(ma,mi)	(((ma) << MINORBITS) | (mi))

【10】字符设备驱动详细框架

应用层:

1、当fd=open("/dev*myled")

2、通过执行ls -i命令,可知每个文件对应唯一个inode号,inode号是文件系统识别文件的唯一的编号

内核层:

1、应用层每个文件对应一个inode号,在内核层对应一个struct inode结构体

2、struct inode结构体信息

struct inode{        umode_t	i_mode; //文件的权限        uid_t	i_uid;  //用户名(id linux)        gid_t	i_gid;  //组名        unsigned long i_ino;//inode号        dev_t	i_rdev;  //设备号        union {            struct block_device	*i_bdev;             //块设备驱动            struct cdev		*i_cdev;             //字符设备驱动        };    }

3、字符设备驱动结构体

struct cdev {        struct module *owner; //THIS_MODULE        const struct file_operations *ops;        //操作方法结构体        struct list_head list;        //构成内核中的链表        dev_t dev;        //设备号        unsigned int count;        //设备的个数    };

1、什么是fd

fd是文件描述符,在每一个进程中都有自己唯一的一套文件描述符

2、什么是文件描述符

1.如果想知道什么是文件描述符,就需要知道文件描述符什么时候创建的2.在打开文件的时候,产生的文件描述符3.文件是在哪里打开的4.在进程中通过调用open函数产生的文件描述5.所以在进程中就可以找到文件描述符6.进程在内核空间通过task_struct结构体来描述的7.在task_struct结构体中应该可以找到文件描述符

3、分析task_struct结构体

struct task_struct {    volatile long state; //进程的状态    int prio, static_prio, normal_prio; //优先级    pid_t pid;//进程号    struct task_struct *real_parent;     struct task_struct  *parent; //父进程    struct list_head children;//子进程    struct list_head sibling;//兄弟进程    struct task_struct *group_leader;    struct thread_struct thread;//线程结构体    struct files_struct *files;    //打开文件时所有信息 }struct files_struct {		struct file  * fd_array[32];};fd就是fd_array的数组下标,通过下标能提取到struct file结构体

4、什么时候产生file结构体

1.只要打开一次文件就会产生一个file结构体2.file结构体中记录的就是打开文件时候的信息(比如:读写的方式,阻塞或者非阻塞打开,打开时候创建文件的权限)struct file {    unsigned int f_flags; //O_RDWR O_NONBLOCK    fmode_t		f_mode;  //文件权限    loff_t		f_pos;   //偏移    const struct file_operations *f_op;	     //操作方法结构体	    void		*private_data; //私有数据}		struct结构体中包含操方法结构体,包含打开,读写,关闭函数

【11】分布实现字符设备驱动

1、分配cdev结构体

struct cdev *cdev_alloc(void)函数功能:为cdev结构体分配内存参数:无返回值:成功返回结构体指针,失败返回NULLvoid kfree(const void *);函数:释放为cdev结构体分配内存

2、初始化cdev结构体

void cdev_init(struct cdev *cdev, const struct file_operations *fops)函数功能:初始化cdev结构体参数:@cdev:cdev结构体指针@fops:操作方法结构体返回值:无

3、申请设备号

1.静态申请设备号int register_chrdev_region(dev_t from, unsigned count, const char *name)函数功能:申请静态指定设备号参数:@from:开始的设备号@count:个数@name:名字返回值:成功返回0,失败返回错误码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)函数功能:注册字符设备驱动参数:@p:复设备驱动的结构体指针@dev:设备号@count:设备的个数返回值:成功返回0,失败返回错误码

三、linux内核中的并发和竞态的解决方法

【1】什么时候产生竞态

同一个驱动程序,同时被多个应用程序(进程)所访问,并且访问的是同一个临界资源,就会产生竞态临界资源:同一块代码

【2】竞态产生的根本原因

1.在单核CPU上,如果内核支持抢占,就可能产生竞态2.对多核CPU来说,核与核之间本省就会产生竞态3.中断和进程之间也会产生竞态4.中断与中断之间也会产生竞态(ARM架构这句话是错误的)

【3】解决竞态的方法

1.顺序执行(无法保证用户什么时候执行进程)2.互斥执行

【4】互斥执行

1、中断屏蔽(了解)

中断屏蔽:就是把中断关了,屏蔽每一个核的中断1.中断使用场合中断屏蔽是针对单核CPU有效,将中断禁止之后就不会产生竞态了2.劣势:①中断屏蔽的时间尽可能的短②如果中断屏蔽的时间比较长就会导致用户数据的丢失③甚至可导致内核的崩溃保证中断屏蔽的时间尽可能的短④中断保护的临界资源比较小
函数接口:local_irq_disable();//关闭中断//临界区local_irq_enable();//开启中断

2、自旋锁(重点掌握)

自旋锁针对多核处理器设计的,如果一个进程获取到自旋锁后,此时另外一个进程也想获取这把锁,此时后一个进程就处于自旋状态(忙等)1.自旋状态是需要消耗CPU资源的2.如果在同一个进程内,想多次获取同一把未解锁的锁,此时当前的CPU就死锁了3.自旋锁保护的临界资源尽可能的(时间片)短,在自旋锁保护的临界区中不能有延时,耗时,休眠或者copy_to_user或者copy_from_user等操作,schedule()函数(主动放弃cpu)拷贝一会,允许执行其它进程,拷贝时间会很长,可以发生状态切换不允许调用,能让cpu切换进程状态的函数4.自旋锁在上锁前会关闭抢占5.自旋锁可以工作在中断上下文6.自旋锁针对多核处理器设计的(很霸道)
API:spinlock_t lock; //1.定义自旋锁spin_lock_init(&lock); //2.初始化自旋锁void spin_lock(spinlock_t *lock) //3.上锁void spin_unlock(spinlock_t *lock)//4.解锁
自旋锁死锁的四个必要条件:1.互斥2.请求保持3.循环等待4.不可剥夺

3、信号量(重点掌握)

如果一个进程获取到信号量之后,此时另外一个进程也想获取信号量,此时后一个进程就处于休眠状态1.信号量在休眠的时候不需要消耗CPU资源2.信号量保护的临界区可以很大,里面可以有延时耗时升值休眠的操作3.信号量工作在进程上下文中4.信号量不会死锁5.信号链上锁的时候不会关闭抢占
APIstruct semaphore sem; //1.定义信号量void sema_init(struct semaphore *sem, int val)//2.信号量的初始化,当val设置为1的时候才有互斥的效果//如果val设置为0表示同步效果。void down(struct semaphore *sem);//3.上锁int down_trylock(struct semaphore *sem)//尝试获取锁,如果成功返回0,失败返回1void up(struct semaphore *sem);//4.解锁

4、互斥体(会用)

如果一个进程获取到互斥体之后,此时另外一个进程也想获取互斥体,此时后一个进程就处于休眠状态思考:什么是同步,什么互斥?答: 同步:顺序执行	互斥:谁先抢到,谁先执行1.互斥体在休眠的时候不需要消耗CPU资源2.互斥体保护的临界区可以很大,里面可以有延时韩式升值休眠的操作3.互斥体工作在进程上下文中4.互斥体不会死锁5.互斥体上锁的时候不会关闭抢占6.互斥体和信号量的区别,互斥体本身设计的时候功能就是互斥,没有同步的概念,类似于sema_init(&sem,1)7.互斥体对于短时间内需要返回休眠和唤醒的进程,来说它比信号量的效率高
API:struct mutex mutex; //1.定义互斥体mutex_init(&mutex)	//2.初始化锁,没有value值,本身互斥void  mutex_lock(struct mutex *lock)//3.上锁int  mutex_trylock(struct mutex *lock)//尝试获取锁,如果成功返回1,失败返回0void mutex_unlock(struct mutex *lock)//解锁

5、原子操作(原子变量)

typedef struct {	int counter;} atomic_t;  原子变量本省就是一个int类型的数值,但是这个数值的修改通过内联汇编完成了,不能被多个核同时修改smp_mb();多核处理器的英文缩写atomic_inc_and_test内联汇编实现好处:内存-》casche-》寄存器r0-》CPU处理只要有缓冲,数据就有可能不是最新的原子:不可在分的整体,赋值以及读取都是在同一个整体
API:atomic_t atm=ATOMIC_INIT(1); //定义原子变量atomic_dec_and_test(&atm);//让原子变量的值减去1和0去比较,如果结果为0 //表示获取锁成功了,返回真,否者表示获取锁失败。atomic_inc(atomic_t *v)//解锁atomic_t atm=ATOMIC_INIT(-1);atomic_inc_and_test(&atm);atomic_dec(&atm);

四、IO模型


【1】非阻塞

1、概念

当应用程序访问驱动的时候,不管驱动的数据是否准备好了,都需要将数据立即返回用户空间为什么是&而不是==? 掩码值

2、框架

【2】阻塞

1、概念

当用户以阻塞的方式打开一个文件的时候,如果调用read函数去读取数据的时候,如果硬件的数据没有准备好,此时进程休眠,当硬件中的数据准备好的时候,硬件会产生中断,在中断处理函数中(驱动中),唤醒休眠的进程,这个休眠的进程就会被唤醒了,休眠的进程读取引荐的数据,将硬件的数据拷贝到用户空间即可思考:阻塞等待和休眠区别?休眠对象:进程(从运行态到休眠态)阻塞:read和write函数

2、框架

3、进程如何进行休眠

wait_event(wq,condition) //让进程进入不可中断的休眠状态(主语:信号)当进程从运行态TASK_RUNNING调用到wait_event函数的时候,判断condition是否为假,如果不为假wait_event就不会执行了(不会休眠),否则将进程放入到wait_queue_t结构体中,并将他放入到wait_queue_head_t这个等待对头后。然后设置进程的状态TASK_UNINTERRUPTIBLE,调用schedule()函数的时候进程真正的进去到休眠的状态wait_queue_head_t wq;//定义队列头init_waitqueue_head(wq);//初始化等待队列头如果condition为假 表示进程需要休眠如果condition为真,进程唤醒
wait_event_interruptible(wq,condition)//让进程进入可中断的休眠状态(信号)当进程从运行状态TASK_RUNNING调用到wait_event_interruptible函数的时候,判断condition是否为假,如果不为假wait_event_interruptible就不会执行了(不会休眠),否则将进程放入到wait_queue_t结构体中,并将它放入到wait_queue_head_t这个等待对头后,然后设置进程的状态为休眠的妆态TASK_UNINTERRUPTIBLE,调用schedule()函数的时候进程真正的进入到休眠的状态

4、进程如何唤醒休眠

如果被调用wake_up,schedule()就返回了,判断condition是否为真,如果为真就唤醒当前的进程,否则继续休眠
如果被调用了wake_up_interruptible,schedule()就返回了,判断condition是否为真如果为真返回0,如果condition不为真继续休眠。或者schedule接收到信号也会返回。如果收到信号返回-ERESTARTSYS错误码。
当硬件的数据准备好的时候,会进入中断的处理函数,在中断的处理函数中唤醒((wake_up/wake_up_interruptible(x)))休眠的进程

5、代码编写流程

wait_queue_head_t wq; //定义等待队列头init_waitqueue_head(&wq)//初始化等待队列头wait_event(wq, condition) //让进程进入不可中断的等待态wait_event_interruptible(wq, condition)	//让进程进入可中断的等待态wake_up(&wq); //唤醒wake_up_interruptible(&wq)//唤醒

【3】IO多路复用

1、概念

在同一个应用程序中同时监听多个文件描述符对应的数据是否准备好了,如果数据准备好了,就读取对应文件描述符中的数据即可

2、监听对个文件描述符机制

select/poll/epoll

3、框架

【4】select/poll/epoll有什么区别

【5】异步通知(信号驱动IO)

1、概念

在用户空间通过signal(SIGIO,信号处理函数)为SIGIO绑定一个信号处理函数,如果硬件中数据准备好了产生中断,在中断的处理函数中发送信号应用层的进程收到信号之后,在信号处理函数中调用read函数读取数据即可

2、框架

应用层:void signal_handle(int no){	read(fd,buf,sizeof(buf))}//1.注册信号处理函数signal(SIGIO,signal_handle)//2.如何调到驱动的fasync函数unsigned int flags = fcntl(fd,F_GETFL)fcntl(fd,F_SETFL,flags | fasync)//3.指定当前进程接受信号fcntl(fd,F_SETOWN,getpid())
虚拟文件系统层sys_fcntl//arg = file->f_flags | FASYNC;if (((arg ^ filp->f_flags) & FASYNC) && filp->f_op &&				filp->f_op->fasync) {//调用驱动中的fops中的fasync函数执行error = filp->f_op->fasync(fd, filp, (arg & FASYNC) != 0); }   
内核层int (*fasync) (int fd, struct file *file, int on){   //1.int fasync_helper(int fd, struct file * filp,      int on, struct fasync_struct **fapp)   //初始化fasync_struct结构体,并将fasync_struct结构体放    //入到异步通知的队列中}2.发信号kill_fasync(&fasync,SIGIO,POLL_IN)

【6】寄存器的读写操作

1、写操作writel

writel(val,register);函数功能:向register的地址中写入val的值

2、读操作readl

readl(register);函数功能:读取register地址中的值,返回值就是读到的值

五、linux内核中的中断


【1】Linux内核中的中断

1、框架

2、内核中断函数接口

①在linux内核中注册中断

int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,const char *name, void *dev)函数功能:在linux内核中注册中断参数:	@irq:软中断号	1.gpio号如何获取?	GPIO A  B  C  D  E	     0  1  2  3  4	 m:组号	 n:组内偏移号	 gpiono = m*32+n	 2.软中断号的获取方式	 irqno = gpio_to_irq(gpiono)	 软中断号             gpio号                            	@handeler:中断处理函数	注意:中断处理函数红不能有延时或者耗时,设置休眠的操作typedef irqreturn_t (*irq_handler_t)(int, void *);中断处理函数原型:irqreturn_t key_irq_handle(int irqno, void *dev){	return IRQ_NONE;//不是这个设备产生的中断	return IRQ_HANDLED;//是这个设备产生的中断,处理完成}	@flags:中断的触发方式	IRQF_TRIGGER_NONE   	    IRQF_TRIGGER_RISING	    IRQF_TRIGGER_FALLING    IRQF_TRIGGER_HIGH	    IRQF_TRIGGER_LOW	    IRQF_DISABLED    //快速中断    IRQF_SHARED      //中断共享        @ name:中断的名字    查看目录:cat /proc/interrupts        @dev:向中断处理函数传递的参数返回值:	成功返回0	失败返回错误码

②注销中断

void free_irq(unsigned int irq, void *dev_id)函数功能:注销中断参数:	@irq:软中断号	@dev_id:向中断处理函数传递的参数返回值:无

3、安装驱动的时候出现错误解决方法

1.安装按键中断驱动#insmod key_irq.ko	register irq 146 error	insmod: can't insert 'key_irq.ko': Device or resource busy通过上述的错误提示,知道中断号这个资源应该是被占用了。2.通过 cat /proc/interrupts查看146:GPIO  nxp-keypad  确认146号中断被占用了154:GPIO  nxp-keypad	3.解决方法将上述的驱动从内核中取消掉即可,在内核中找到对用的驱动的.c文件,通过make menuconfig将这个驱动从内核中选配掉即可4.grep "nxp-keypad" * -nR路径:arch/arm/mach-s5p6818/include/mach/devices.h:48:#define DEV_NAME_KEYPAD    "nxp-keypad"5.grep "nxp-keypad" * -nR路径:/drivers/input/keyboard/nxp_io_key.c:324:	      .name	= DEV_NAME_KEYPAD,       6.通过上述的搜索找到驱动文件是nxp_io_key.c  7.Makefile:	obj-$(CONFIG_KEYBOARD_NXP_KEY) += nxp_io_key.o 8.Kconfig: config KEYBOARD_NXP_KEY                              	tristate "SLsiAP push Keypad support"  depends on ARCH_CPU_SLSI  help  Say Y here to enable the gpio keypad on SLsiAP SoC based board.    9.make menuconfig:  -> Device Drivers                             	    -> Input device support                      	      -> Generic input layer    	        -> Keyboards (INPUT_KEYBOARD [=y])       	            <>   SLsiAP push Keypad support      10.make uImage:   		重新编译内核   		   11.将内核拷贝到tftpboot目录下   cp  arch/arm/boot/uImage ~/tftpboot/      12.重启开发板让它家在最新的内核   [root@farsight]#insmod key_irq.ko     [root@farsight]#    [   38.242000] left key down############    [   38.254000] left key down############    [   38.256000] left key down############    [   38.301000] left key down############    [   38.303000] left key down############    [   38.473000] left key down############    [   39.343000] right key down...........    [   39.570000] right key down...........    [   40.269000] left key down############   出现上述的现象说明驱动安装成功了

【2】Linux中定时器使用

如何消除给予linux内核案件抖动现象ARM:在按下第一次按键的时候进入中断的处理函数中,在中断处理函数中延时5~10ms时间在次判断按键是否按下,如果按键按下了执行用户的操作即可linux:在内核的中断处理函数中不能够做延时,耗时甚至休眠的操作,所以这里的按键小东欧就不能通过延时来完成,如何消抖?使用linux内核定时器完成

1、内核中定时器如何查看

在内核的.config有如下变量CONFIG_HZ=1000是内核定时器的频率开发板:CONFIG_HZ=1000Ubuntu:CONFIG_HZ=250 

2、内核中定时器每增加1走的时间是多少

开发板:CONFIG_HZ=1000=====>1ms	  ubuntu: CONFIG_HZ=250 =====>4ms频率所对应的时间怎么求出来?1/1000 = 0.001s = 1ms1/250  = 0.004s = 4ms

3、内核当前的时间如何进行获取

jiffies:内核始终节拍数,内核从启动开始,这个值随着定时器的频率一直在增加

4、如何设置内核的定时时间

jiffies+1000 ==>未来的1s

4、内核定时器函数接口

①分配内核定时器的对象

struct timer_list {	unsigned long expires;	//定时的时间	void (*function)(unsigned long);	//定时器的处理函数,当定时时间到回调的函数	unsigned long data;	//向定时器处理函数传递的参数}struct timer_list mytimer;

②定时器初始化

void timer_function(unsigned long data){}mytimer.expires = jiffies+1000;mytimer.function = timer_function;mytimer.data = 0;init_timer(&mytimer);

③启动定时器

void add_timer(struct timer_list *timer)//在调用add_timer定时器就开始启动了,并且//只会执行一次。如果向再次启动定时器int mod_timer(struct timer_list *timer, unsigned long expires)

④删除定时器

int del_timer(struct timer_list *timer)

【3】gpio子系统函数接口

1、框架

2、gpio子系统函数接口

①申请gpio去使用

int gpio_request(unsigned gpio, const char *label)函数功能:申请gpio去使用参数:	@gpio:gpio号	@label:标签字符串(一般写为NULL)返回值:	成功返回0	失败返回错误码

②设置gpio的方向为输入

int gpio_direction_input(unsigned gpio)函数功能:设置gpio的方向为输入参数:	@gpio:gpio号返回值:	成功返回0	失败返回错误码	

③设置gpio的方向为输出

int gpio_direction_output(unsigned gpio, int value)函数功能:设置gpio的方向为输出参数:	@gpio:gpio号返回值:	成功返回0	失败返回错误码	

④获取gpio的电平状态

int gpio_get_value(unsigned gpio)函数功能:获取gpio电平的状态参数:	@gpio:gpio号返回值:	成功返回0	失败返回错误码	

⑤设置gpio的电平状态

void gpio_set_value(unsigned gpio, int value)函数功能:设置gpio电平的状态参数:	@gpio:gpio号	@value:高低电平返回值:	无

⑥释放gpio号

void gpio_free(unsigned gpio)函数功能:释放gpio号参数:	@gpio:gpio号返回值:	无

【4】linux中断底半部

1、概念

1.在linux中断的中断顶半部,要求不能做延时、耗时,甚至休眠的操作2.在中断顶半部中只能做简短的部耗时的操作3.但是在工作开发过程中有向在中断到来的时候做相对耗时的操作4.例如在网卡中断到来的时候,需要接受网络数据包,这就是耗时操作5.所以就和内核中断顶半部设计产生矛盾6.内核就是设计了中断底半部

2、中断底半部机制

①软中断

软中断是有个数限制的,共计32个,所以软中断一般都是给内核使用的,驱动工程师一般不使用软中断,软中断工作在中断上下文,在软中断中不能够有休眠的操作

②tasklet

tasklet是基于软中断实现的,tasklet也是工作在中断上下文的,tasklet没有个数限制 (tasklet在内核中是一个链表,这个链表有软中断来顺序解析)1.分配对象struct tasklet_struct{    struct tasklet_struct *next;    //构成内核链表    unsigned long state;    //tasklet的状态    atomic_t count;    //底半部被触发的次数    void (*func)(unsigned long data);    //底半部处理函数    unsigned long data;    //向底半部处理函数传递的参数};struct tasklet_struct tasklet;2.初始化void tasklet_init(struct tasklet_struct *tasklet,void (*func)(unsigned long),unsigned long data)3.调用执行void tasklet_schedule(struct tasklet_struct *tasklet)调用底半部去执行

③工作队列

在内核启动的时候会启动一个events的内核线程,events显示默认是休眠的状态,如果你有一个work需要去执行,将这个work放入到work queue中,唤醒events线程,这个events线程就去回调你的work中的底半部处理函数,没有个数限制,他工作在进程上下文,它可以脱离中断执行,里面可以做延时,耗时,甚至休眠操作1.分配对象struct work_struct {        atomic_long_t data;        //数据的变量        struct list_head entry;        //链表        work_func_t func;        //底半部处理函数};typedef void (*work_func_t)(struct work_struct *work);struct work_struct work;2.初始化对象INIT_WORK(&work, _func);3.调用执行schedule_work(&work);

六、Linux中的设备模型(platform总线驱动)


【1】概念

1.在内核中设计了device、bus、driver的设备模型2.device 就是用来描述设备的通用的结构体3.bus是内核架构师实现的对象,功能就是完成device跟driver的匹配的过程4.driver设备驱动需要分配的结构体对象5.platform采用了设备信息和设备驱动分离的思想6.platform_device继承了device的结构体,用来描述具体的设备信息7.platfom_driver就是驱动所要分配的结构体对象,继承于driver的对象8.当设备信息端和设备驱动端通过name匹配成功之后就会执行的probe函数,分离的时候执行remove函数

【2】设备信息端API接口

struct platform_device {    const char	* name;    //用于匹配的名字    int		id;    //由于没有真实的硬件设备对应,所以直接填写为-1    struct device	dev;    //父类    u32	 num_resources;    //设备信息的个数    struct resource	* resource;    //设备信息结构体}
struct device{	void (*release)(struct device *dev);	//释放资源};struct resource {    resource_size_t start;    //资源的起始值    resource_size_t end;    //资源的结束值    //const char *name;    //资源的名,一般不需要填充    unsigned long flags;    //资源的类型IORESOURCE_IO IORESOURCE_IRQ    //IORESOURCE_MEM IORESOURCE_DMA    //struct resource *parent, *sibling, *child;    //父结构体  兄弟结构体  孩子结构体    //不需要驱动工程师填充};
注册int platform_device_register(struct platform_device *pdev)注销void platform_device_unregister(struct platform_device *pdev)

【3】设备驱动端API端口

struct platform_driver {    int (*probe)(struct platform_device *);    //匹配成功后执行的函数    int (*remove)(struct platform_device *);    //分离的时候执行的函数    struct device_driver driver;    //const struct platform_device_id *id_table;    //2.idtable匹配方式};
struct device_driver {        const char		*name;        //1.用于匹配的名字        const struct of_device_id	*of_match_table;        //3.设备树匹配}
注册int platform_driver_register(struct platform_driver *drv)注销void platform_driver_unregister(struct platform_driver *drv)

七、i2C总线驱动/SPI总线驱动


【1】i2C硬件知识

1、2根线

半双工的串行同步总线scl:时钟线sda:数据线

2、3种信号

start(起始信号):	当scl为高电平期间,sda从高到低的跳变stop(停止信号):	当SCL为高电平期间,sda从低到高的跳变ack(应答信号):	当第九个始终周期的时候,sda上是低电平,这个低电平就是应	 答信号

3、i2c的写时序

start + (7位从机地址 0写) + ack + (8位的寄存器的地址) + ack + (8位数据) + ack + stop

4、i2c的读时序

start + (7位从机地址 0写) + ack + (8位的寄存器的地址) + ack + start + (7位从机地址 1读) + ack + (从机给主机发送的8位数据) + No ack + stop

5、i2c通讯的速率

100kbps 400kbps    3.4Mbps

八、块设备驱动


九、网卡设备驱动


十、摄像头驱动


posted @ 2021-07-01 19:17  Yangtai  阅读(387)  评论(0编辑  收藏  举报