目录

一、模块

1、什么是驱动

能够通过软件操作硬件的这份程序就是驱动

2、驱动和裸机驱动的区别

Linux驱动和ARM裸机驱动的区别:
1)Linux 设备驱动工作时依赖于Linux内核, ARM裸机驱动不依赖于Linux内核,可以单独执行。 
2)Linux 设备驱动工作的空间在内核空间的【3G~4G】中,驱动中不能有main函数出现,也不能有while(1)的死循环, ARM裸机驱动的代码都在一个 main 函数中编写完成的。 
3)Linux 设备驱动可以并行执行(核与核之间可以并行执行,单个核时也可以分时复用执行) ARM裸机驱动的程序是在一个 while 中分时复用执行的 

3、驱动在内核中的位置

3.1、user(应用层)

------------------------------------------------------------------------
user(应用层):
	APP 	APP		glibc(C标准库)
			------------------------------------------------------------
			shell:
-----------------系统调用(swi软中断)---------------------------------------

3.2、kernel(内核层)

-----------------系统调用(swi软中断)---------------------------------------
kernel(内核层):(5大功能)
	1)文件管理
	2)内存管理
	3)进程管理
	4)网络管理:网络协议栈管理(网络的几个层次,TCP/IP四层、OSI七层)
	5)设备管理:设备驱动管理
------------------------------------------------------------------------

3.3、hardware(硬件层)

------------------------------------------------------------------------
hardware(硬件层):
	LED、鼠标、键盘、LCD(帧缓存)、camera、声卡
	emmc、U盘、硬盘
	dm9000、TRL8211
------------------------------------------------------------------------

4、驱动的种类

1、字符设备驱动:
	按照字节流来访问,并且只能顺序访问,不能无序访问的设备。 
2、块设备驱动:
	按照block(块 = 512字节)来访问,可以顺序访问,也可以无序访问的设备。
3、网卡设备驱动:
	和网络通讯相关的设备驱动,没有设备文件,只能通过socket接口来访问的设备驱动 

5、Linux内核模块

Linux内核模块的三要素:

1)入口: 驱动资源的申请,驱动对象的注册 
2)出口: 驱动资源的释放,驱动资源的注销 
3)许可证: 遵循 GPL 协议 
GNU:GNU IS NOT UNIX   一个提倡软件自由的组织--->由理查德·斯托曼提出 
GPL:GNU通用公共许可证   一种协议 

C语言为面向过程的语言,C++为面向对象的语言 
驱动使用的是C语言编程,但要有面向对象的思想和概念 

模块的编写:!!!

#include <linux/init.h>
#include <linux/module.h>
 
//入口
static int __init mycdev_init(void)
{
    //__init作用:告诉编译器将mycdev_init这个函数放到.init.text这个段中
    return0;
}
 
//出口
static void __exit mycdev_exit(void)
{
    //__exit作用:告诉编译器将mycdev_exit这个函数放到.exit.text这个段中
}

module_init(mycdev_init);
//告诉内核入口函数的地址是mycdev_init
module_exit(mycdev_exit);
//告诉内核出口函数的地址是mycdev_exit
MODULE_LICENSE("GPL");
//许可证

系统关键字

1、vi -t __init
	vim ./include/linux/init.h +44
	44 #define __init      __section(.init.text) __cold notr                 驱动模块入口段
	
2、vi -t __exit
	im ./include/linux/init.h +83
	83 #define __exit     __section(.exit.text) __exitused __cold notrace    驱动模块出口段

以"__"开头的内容,是给编译器使用的,不需要太过理解,
如 __init、__exit、__FILE__、__func__、__LINE__

static 关键字

在模块的入口函数和出口函数中使用了 static 关键字进行了修饰

作用:是为了避免,自己自定义的模块入口出口函数名,与未知的函数名重名,而带来的重新定义的问题, 函数可以加上 static 关键字修饰,经过 static 关键字修饰过的函数的链接属性为内部,从而解决了该问题

6、测试

安装 insmod xxx.ko 
查看 lsmod 
卸载 rmmod xxx

7、Makefile编写

KERNELDIR = /lib/modules/$(shell uname -r)/build/
#ubuntu内核的路径

#KERNELDIR = /home/liu/Linux/kernel-3.4.39/
#开发板内核的路径
 
PWD=$(shell pwd)
#当前路径
 
NAME ?= demo.c
#需要编译的模块名字
 
NAMES := $(patsubst %.c,%,$(NAME))
#截取字符串 xx.c 里的 .c 之前的内容

obj-m:=$(NAMES).o
#需要的依赖文件

all:
	make -C $(KERNELDIR) M=$(PWD) modules

	@# mkae -C $(KERNELDIR)
	@# 进入内核顶层目录下,读取目录下的Makefile文件,并执行这个Makefile文件
	@# M=$(PWD) 	指定编译模块的路径
	@# modules 	编译模块的选项
	@# 如:
	@# 		1、cd $(KERNELDIR) 
	@# 		2、mkae M=$(PWD) modules
clean:
	make -C $(KERNELDIR) M=$(PWD) clean
	
	@# mkae -C $(KERNELDIR)
	@# 进入内核顶层目录下,读取目录下的Makefile文件,并执行这个Makefile文件
	@# M=$(PWD)  指定编译模块的路径
	@# clean     清除的选项
	@# 如:
	@# 		1、cd $(KERNELDIR)
	@# 		2、make M=$(PWD) clean
cp:
	cp $(NAMES).ko ~/nfs/rootfs/ 

8、内核中的信息打印-->printk

1、打印级别

vim include/linux/printk.h +9 
内核中的打印级别,8个级别,(0~7)级,数值越小,级别越高
(0~2)3个级别不常用,
(3~7)5个级别会常用,

vim ./include/linux/printk.h +9

09 #define 	KERN_EMERG  	"<0>"   系统(不使用)	     /* system is unusable           */
10 #define 	KERN_ALERT  	"<1>"   立即执行(操作系统使用) /* action must be taken immediately */
11 #define 	KERN_CRIT   	"<2>"  	紧急情况(临界条件)     /* critical conditions          */
12 #define 	KERN_ERR    	"<3>"   错误	             /* error conditions         */
13 #define 	KERN_WARNING    "<4>"   警告	             /* warning conditions           */
14 #define 	KERN_NOTICE 	"<5>"   提示	             /* normal but significant condition */
15 #define 	KERN_INFO   	"<6>"   正常打印	             /* informational            */
16 #define 	KERN_DEBUG  	"<7>"   调试	             /* debug-level messages         */

printk(打印级别 "这里和printf的使用方法一样");
printk("这里和printf的使用方法一样"); --->不填打印级别即为默认级别

2、终端显示

终端显示的时候有一个终端级别,只有打印级别大于终端级别的时候,消息才会在终端上显示查看终端级别
cat /proc/sys/kernel/printk  结果为  4     4     1     7
		
4          4       		  1              	7
终端级别    默认消息级别    终端打印最高级别    终端打印最低级别
	
ubuntu终端默认被修改为不能显示内核打印信息
需要进入虚拟终端中进行测试上述效果

进入虚拟终端	Ctrl + Alt + [F1~F6]
退出虚拟终端	Ctrl + Alt + [F7]
	
1、Ubuntu上修改printk文件的默认消息级别
su    root           进入 root 用户模式
echo 4 3 1 7 > /proc/sys/kernel/printk
exit                 退出 root 用户模式

以后在使用的时候就是修改后的消息级别
	
2、开发板上修改printk文件的默认消息级别

vim ~/rootfs/etc/init.d/rcS                 rcS--->内核启动后运行的第一个脚本
添加内容
echo 4 3 1 7 > /proc/sys/kernel/printk
3、dmesg命令

dmesg	将Ubuntu开机到目录的时间点所有的信息显示出来
dmesg -c	清除打印信息,先回显后清除
dmesg -C	清除打印信息,直接清除,不回显
如果不会主动回显,可以使用dmesg命令查看

3、模块编写

#include <linux/init.h>
#include <linux/module.h>
	
static int __init myprintk_init(void)
{
	printk(KERN_EMERG "[0]:%s:%d:system is unusable",__func__,__LINE__);
	printk(KERN_ALERT "[1]:%s:%d:action must be taken immediately",__func__,__LINE__);
	printk(KERN_CRIT "[2]:%s:%d:critical conditions",__func__,__LINE__);
	printk(KERN_ERR "<3>:%s:%d:error conditions",__func__,__LINE__);
	printk(KERN_WARNING "<4>:%s:%d:warning conditions",__func__,__LINE__);
	printk(KERN_NOTICE "<5>:%s:%d:normal but significant confition",__func__,__LINE__);
	printk(KERN_INFO "<6>:%s:%d:informational",__func__,__LINE__);
	printk(KERN_DEBUG "<7>:%s:%d:debug-level messages",__func__,__LINE__);
return 0;
}

static void __exit myprintk_exit(void)
{
	printk(KERN_EMERG "[0]:%s:%d:system is unusable",__func__,__LINE__);
	printk(KERN_ALERT "[1]:%s:%d:action must be taken immediately",__func__,__LINE__);
	printk(KERN_CRIT "[2]:%s:%d:critical conditions",__func__,__LINE__);
	printk(KERN_ERR "<3>:%s:%d:error conditions",__func__,__LINE__);
	printk(KERN_WARNING "<4>:%s:%d:warning conditions",__func__,__LINE__);
	printk(KERN_NOTICE "<5>:%s:%d:normal but significant confition",__func__,__LINE__);
	printk(KERN_INFO "<6>:%s:%d:informational",__func__,__LINE__);
	printk(KERN_DEBUG "<7>:%s:%d:debug-level messages",__func__,__LINE__);

}

module_init(myprintk_init);
module_exit(myprintk_exit);
MODULE_LICENSE("GPL");

二、字符设备驱动

1、字符设备的位置

1.1、用户层

------------------------------------------------------------------------
user(用户层): 
		open()          read()           write()         close() 
		---------------------------------------------------------------
		/dev/mycdev (字符设备文件,inode号)
------------------------------------------------------------------------

1.2、内核层

------------------------------------------------------------------------
kernel(内核层):
	/dev/mycdev (字符设备文件,inode号)
	设备号(32位) 	= 	主设备号(高12位)	+	次设备号(低20位)	

	| 字符设备驱动1 |      | 字符设备驱动2 | …    | 字符设备驱动n |
	| ------------- | ---- | ------------- | ---- | ------------- |
	| driver_open   |      | driver_open   | …    | driver_open   |
	| driver_read   |      | driver_read   | …    | driver_read   |
	| driver_write  |      | driver_write  | …    | driver_write  |
	| driver_close  |      | driver_close  | …    | driver_close  |
------------------------------------------------------------------------

1.3、硬件层

------------------------------------------------------------------------
hardware(硬件层):
	LED,LCD,ADC
------------------------------------------------------------------------

2、字符设备驱动相关 API

2.1、注册字符设备驱动

vi -t register_chrdev
int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops)
功能:注册一个字符设备驱动
参数:
	@major:主设备号
        major>0 这个major就是主设备号,但是有可能不能用
        major=0 系统会自动申请设备号
	@name:驱动的名字
        cat /proc/devices
	@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:成功返回0,失败返回错误码
        major=0:成功返回主设备号,失败返回错误码

2.2、注销设备驱动

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

2.3、错误码

vi -t EIO
vi -t EIO
vim ./include/asm-generic/errno-base.h +4

01	EPERM	不允许操作	/* Operation not permitted */
02	ENOENT	无此文件或目录	/* No such file or directory */
03	ESRCH	无此过程	/* No such process */
04	EINTR	中断的系统调用	/* Interrupted system call */
05	EIO	    I/O错误	/* I/O error */
06	ENXIO	无该设备或地址	/* No such device or address */
07	E2BIG	参数列表过长	/* Argument list too long */
08	ENOEXEC	执行格式错误	/* Exec format error */
09	EBADF	文件号错误	/* Bad file number */
10	ECHILD	没有子进程	/* No child processes */
11	EAGAIN	再试一次	/* Try again */
12	ENOMEM	内存不足	/* Out of memory */
13	EACCES	没有权限	/* Permission denied */
14	EFAULT	地址错误	/* Bad address */
15	ENOTBLK	需要块设备	/* Block device required */
16	EBUSY	设备或资源繁忙	/* Device or resource busy */
17	EEXIST	文件已存在	/* File exists */
18	EXDEV	跨设备链接	/* Cross-device link */
19	ENODEV	无此设备	/* No such device */
20	ENOTDIR	不是目录	/* Not a directory */
21	EISDIR	是目录	/* Is a directory */
22	EINVAL	无效参数	/* Invalid argument */
23	ENFILE	文件表溢出	/* File table overflow */
24	EMFILE	打开的文件过多	/* Too many open files */
25	ENOTTY	不是打字机	/* Not a typewriter */
26	ETXTBSY	文本文件忙	/* Text file busy */
27	EFBIG	文件过大	/* File too large */
28	ENOSPC	设备上没有剩余空间 	/* No space left on device */
29	ESPIPE	非法搜索	/* Illegal seek */
30	EROFS	只读文件系统	/* Read-only file system */
31	EMLINK	链接过多	/* Too many links */
32	EPIPE	管道破损	/* Broken pipe */
33	EDOM	数学参数超出函数范围	/* Math argument out of domain of func */
34	ERANGE	数学结果无法表示	/* Math result not representable */
设置驱动文件对应的索引文件,如果文件关闭了,需要重新设置这句话
:set tags=/home/linux/kernel/kernel-3.4.39/tags

2.4、创建设备节

sudo mknod /dev/mycdev c/b 主设备号 次设备号

mknod:创建设备节点的命令
/dev/mycdev:节点的名字及路径 (路径任意)
c/b:字符设备/块设备

3、字符设备驱动实现的流程

1)、编写字符设备驱动

struct file_operations fops = {
    .open = mycdev_open,
    .read = mycdev_read,
    .write = mycdev_write,
    .release = mycdev_close,
};
入口:major = register_chrdev(0,"mycdev",&fops);
出口:unregister_chrdev(major,"mycdev");

2)、编译驱动

make ---->mycdev.ko

3)、安装驱动

sudo insmod mycdev.ko
cat /proc/devices ------>250  mycdev

4)、创建设备节点

sudo mknod /dev/mycdev c 250 

5)、编写编译测试程序

open   read   write  close
gcc test.c-->a.out

6)、执行应用程序

查看驱动是否被调用到了
sudo ./a.out 
如果看到下面的语句,就说明应用程序调用驱动成功了
mycdev_open:10
mycdev_read:16
mycdev_write:22
mycdev_close:27
1.实现的方法
	major = register_chrdev(0,"myled",&fops);
	unregister_chrdev(major,"myled");
2.编译驱动
	demo.ko  驱动文件
3.安装驱动
	sudo insmod demo.ko 
4.为这个驱动创建设备节点
	sudo mknod /dev/myled c 主设备号 次设备号
5.编写编译应用程序去测试写的驱动
6.用户空间和你和空间的数据的传递
    copy_to_user();
    copy_from_user();
7.物理地址向虚拟地址的映射
	虚拟地址 = ioremap(物理地址,长度);
	iounmap(虚拟地址);

4、用户空间和内核空间的数据传输

用户空和内核空间的数据的传输必须借助 copy_to_user()或 copy_from_user()来传递,
"不能"在内核空间获取到用户空间的地址后,"直接"通过指针的方式来操作,如果这样操作,内核的安全性无法得到保障。
copy_to_user()或 copy_from_user()这两个函数
在进行数据传输的时候底层调用的就是 memcpy()函数,但是在调用这个函数前有一个"参数检查"的过程。

4.1、向用户空间拷贝数据

#include <linux/uaccess.h>

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

4.2、从用户空间拷贝数据

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

5、物理地址和虚拟地址的问题

如果在驱动中需要操作灯,操作的是灯对应的"寄存器"。
灯对应寄存器的地址是"物理地址",但是驱动运行在内核的 3-4G "虚拟地址"中,
如何让驱动把这个物理地址当成物理地址使用而不把它当成虚拟地址使用?		

5.1、地址映射

void *ioremap(phys_addr_t offset, unsigned long size)	
功能:将物理地址映射成虚拟地址
参数:
    @offset: 物理起始地址
    @size  : 想映射的长度,单位是字节
 返回值:成功返回虚拟地址(3-4G),失败返回NULL

5.2、取消地址映射

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

6、点灯

6.1、红灯

RGB-led
红灯  gpioa28   0xc001a000
1.功能选择寄存器
	GPIOXALTFN1_28  [25:24]  RW 
	GPIOx[28]: Selects the function of GPIOx 28pin. 
	00 = ALT Function0   <------  
	01 = ALT Function1 
	10 = ALT Function2   
2.输入输出
	GPIOXOUTENB  [31:0]  RW 
	0 = Input Mode   
	1 = Output Mode <-----
3.输出高\低
	GPIOXOUT  [31:0]  RW 
	0 = Low Level    <----------灭
	1 = High Level   <----------亮

6.2、绿灯

绿灯  gpioe13   0xc001e000
1.功能选择寄存器
	GPIOXALTFN0_13  [27:26]  RW 
	GPIOx[13]: Selects the function of GPIOx 13pin. 
	00 = ALT Function0   <------  
	01 = ALT Function1 
	10 = ALT Function2   
2.输入输出
	GPIOXOUTENB  [31:0]  RW 
	0 = Input Mode   
	1 = Output Mode <-----
3.输出高\低
	GPIOXOUT  [31:0]  RW 
	0 = Low Level    <----------灭
	1 = High Level   <----------亮

6.3、蓝灯

蓝灯  gpiob12   0xc001b000
1.功能选择寄存器
	GPIOXALTFN0_12  [25:24]  RW 
	GPIOx[12]: Selects the function of GPIOx 12pin. 
	00 = ALT Function0   
	01 = ALT Function1 
	10 = ALT Function2 <------     
2.输入输出
	GPIOXOUTENB  [31:0]  RW 
	0 = Input Mode   
	1 = Output Mode <-----
3.输出高\低
	GPIOXOUT  [31:0]  RW 
	0 = Low Level    <----------灭
	1 = High Level   <----------亮		

7、ioctl() 函数

7.1、应用层

++man ioctl
----------------------------------------------
#include <sys/ioctl.h>
int ioctl(int fd, int request, ...);
参数:
	@fd:	打开设备驱动得到的文件描述符
	@request:命令码 	_IO _IOR _IOW _IOWR
	@...:	可传递,也可以不传递(传递的是一个地址)

----------------------------------------------

7.2、驱动层

long (*unlocked_ioctl) (struct file *file,unsigned int cmd, unsigned long args);

7.3、命令码的解析

kernel/kernel-3.4.39/Documentation/ioctl$ 
vi ioctl-decoding.txt

bits     meaning
方向:		[31,30]位:
					00 - no parameters: uses _IO macro
					10 - read: _IOR
					01 - write: _IOW
					11 - read/write: _IOWR
	
大小:		[29,16]位:	size of arguments
类型:		[15,08]位:	ascii character supposedly
					  unique to each driver
功能:		[07,00]位:	function #

7.4、宏定义

其实你就可以通过下面的四个宏,来得到上述的32位的整数
#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))

上述的这四个宏最终都是调用的 _IOC 实现的,_IOC 的实现如下:
#define _IOC(dir,type,nr,size) 	
	(	((dir)    << _IOC_DIRSHIFT)  |  左移30位
	 	((type) << _IOC_TYPESHIFT)   |  左移8位
	 	((nr)     << _IOC_NRSHIFT)   |  左移0位
	 	((size)   << _IOC_SIZESHIFT)	左移16位
    )	 

方向    大小      类型    功能
dir<<30|size <<16|type<<8|nr <<0 
2位     14位       8位     8

7.5、注意

#define RED_ON _IO('a',0)
注:通过这 _IO _IOR _IOW _IOWR 宏就是为了得到一个 32位的不重复的整数,
只要保证在当前的驱动中它的功能唯一即可,驱动和驱动间功能可以不唯一。
    
练习:
	1.ioctl函数的使用
	2.尝试使用ioctl传递数组
	3.尝试使用ioctl传递整数(不需要取地址了)地址也是一个整数

8、API

//1、注册一个字符设备驱动
int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops);
//2、注销一个字符设备驱动
void unregister_chrdev(unsigned int major, const char *name);
//3、向用户空间拷贝数据(在驱动的read函数中调用)
unsigned long copy_to_user(void __user *to,const void *from, unsigned long n);
//4、从用户空间拷贝数据(在驱动的write函数中调用)
unsigned long copy_from_user(void *to,const void __user *from, unsigned long n);
//5、将物理地址映射成虚拟地址
void *ioremap(phys_addr_t offset, unsigned long size);
//6、取消物理地址映射
void iounmap(void *addr);
//7、应用层 ioctl
int ioctl(int fd, int request, ...);
//8、驱动层 ioctl
long (*unlocked_ioctl) (struct file *file,unsigned int cmd, unsigned long args);
//9、ioctl四个宏函数
_IO 	//无数据操作
_IOR	//读数据
_IOW 	//写数据
_IOWR 	//读写数据
    
    
#define RED_ON _IO('a',0)

三、自动创建设备节点

3.1、udev/mdev

// udev
// mdev(轻量级的udev)

user:
		hotplug --------------------> udev
		//监测是否有信息的提交        //创建设备节点的应用程序
		 /sys/class/目录名/信息       /dev/设备节点
		  ^
		  |
-----------|--------------------------------------
kernel:    |
		  |
		1.提交目录的信息(目录名)
		2.提交设备信息 (设备名,主设备号,次设备号)

	从上述的图中知道,如果想去创建设备节点,只需要在
	驱动中提交上述的两个信息即可。

3.2、宏定义

1、"带"返回值
#define MAX(a,b)	({int c;if(a>b) c=a;else c=b;c;})
// 宏的最后一个";"前的内容,为宏的换回值
printf("max(100,200) = \n",MAX(100,200));
2、"不带"返回值
int c = 0;
#define MIN(a,b)	do{if(a>b) c=a;else c=b;}while(0);

min(100,200);
printf("min(100,200) = %d\n",c);
//例如:
#include <stdio.h>
#define MAX(a,b)	({int s;if(a>b) s=a;else s=b;s;})
int c = 0;
#define MIN(a,b)	do{if(a<b) c=a;else c=b;}while(0);

int main()
{
	printf("max(100,200) = %d \n",MAX(100,200));
	MIN(100,200);
	printf("min(100,200) = %d\n",c);
   
	return 0;
}
//结果:
max(100,200) = 200 
min(100,200) = 100

3.3、提交目录的信息(目录名)

//1、注册目录信息
class_create(owner, name) 	这是一个有"返回值"的"宏"
功能:在/sys/class下创建目录
参数:
	owner:THIS_MODULE
	name : 目录名
		
返回值:成功返回 class 的结构体指针 struct class * --> 目录句柄
	  失败返回错误码指针(void *)-1;
//2、注销目录信息
void class_destroy(struct class *class)
功能:删除在/sys/class下创建目录
参数:
	@class : 目录的句柄	
返回值:无

3.4、提交设备信息

//提交设备信息
struct device *device_create(struct class *class
							,struct device *parent
							,dev_t devt
							,void *drvdata
							,const char *fmt
							, ...)		
功能:提交设备信息
参数:
	@class : 目录的句柄
	@parent: NULL 		//提交 父级目录信息 创建新的目录时(即没有父级目录) 可以填 NULL
	@devt  :设备号
	@drvdata:NULL  		//向驱动设备传递的私有的字符串数据,一般不传,可以填NULL
	@fmt   :设备名
返回值:成功返回device的结构体指针struct device *
	  失败返回错误码指针(void *)-1;

MAJOR(dev)	//根据设备号得到主设备号
MINOR(dev)  //根据设备号获取次设备号
MKDEV(ma,mi) //根据主,次设备号合成设备号

//注销设备信息
void device_destroy(struct class *class,dev_t devt)
功能:注销设备信息
参数:
	@class : 目录的句柄
	@devt  :设备号
返回值:无

3.5、判断错误码

在内核的中有 "4k" 的空间存放"错误码",在 [(4G-4k)~4G]地址空间中
//原码,反码,补码
正数:正数的原码,反码,补码都是相同的。
负数:
	负数原码和反码的相互转换:
		符号位不变,数值位按位取反。
	负数原码和补码的相互转换:
		符号位不变,数值位按位取反,末位再加1。
-1 
原码: 0x1000 0000 ··· 0000 0001
反码: 0xfffffffe
补码: 0xffffffff 	--->4G
  
IS_ERR(const void *ptr);
	如果在错误码区间,返回真
	如果不在错误码的区间,返回假
		
PTR_ERR(const void *ptr);
	将错误码指针转化为错误码
		
ERR_PTR(long error);
	将错误码转化为错误码指针

3.6、内核层驱动代码

#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/device.h>

#define NAME "class_device_create"
struct class * cls = NULL;
struct device *dev = NULL;
int major = 0;

char kbuf[128] = {0};

ssize_t mycdev_read(struct file *file, char __user *ubuf, size_t size, loff_t *loff)
{
	if(size > strlen(kbuf)){
		size = strlen(kbuf);
	}
	copy_to_user((void __user *)ubuf,(const void *)kbuf,size);
	return size;
}

ssize_t mycdev_write(struct file *file, const char __user *ubuf, size_t size, loff_t *loff)
{
	if(size > (sizeof(kbuf)-1)){
		size = sizeof(kbuf)-1;
	}
	copy_from_user((void *)kbuf, (const void __user *)ubuf,size);
	printk(KERN_INFO "kbuf = %s\n",kbuf);
	return size;
}

int mycdev_open(struct inode *inode, struct file *file)
{
	return 0;
}

int mycdev_close(struct inode *inode, struct file *file)
{
	return 0;
}

struct file_operations fops = {
	.open = mycdev_open,
	.read = mycdev_read,
	.write = mycdev_write,
	.release = mycdev_close,
};

static int __init mycdev_init(void)
{
	int i,ret;
	//注册设备
	major = register_chrdev(major,NAME,&fops);
	if(major < 0){
		printk(KERN_INFO "register_chrdec() error\n");
		ret = -EINVAL;
		goto ERR1;
	}

	//1、注册目录信息
	cls = class_create(THIS_MODULE,NAME);
	if(IS_ERR(cls)){
		printk(KERN_INFO "class_create() error\n");
		ret = PTR_ERR(cls);
		goto ERR2;
	}
	//2、注册设备信息
	for(i=0;i<3;i++){
		dev = device_create(cls,NULL,MKDEV(major,i),NULL,"class_device_%d",i);
		if(IS_ERR(dev)){
			printk(KERN_INFO "device_create() error\n");
			ret = PTR_ERR(dev);
			goto ERR3;
		}
	}

	return 0;
ERR3:
	//注销设备信息
	for(--i;i>=0;i--){
		device_destroy(cls,MKDEV(major,i));
	}
	//注销目录信息
	class_destroy(cls);
ERR2:
	//注销设备
	unregister_chrdev(major,NAME);	
ERR1:
	return ret;
}

static void __exit mycdev_exit(void)

{
	int i;
	//注销设备信息
	for(i=0;i<3;i++){
		device_destroy(cls,MKDEV(major,i));
	}
	//注销目录信息
	class_destroy(cls);

	//注销设备
	unregister_chrdev(major,NAME);
}

module_init(mycdev_init);
module_exit(mycdev_exit);
MODULE_LICENSE("GPL");

3.7、应用层测试代码

#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, const char *argv[])
{
	int fd = 0;
	char buf[128] = "abcdefg_abcdefg";
	
	if(0 > (fd = open("/dev/class_device_2",O_RDWR))){
		perror("open()");
		return -1;
	}

	printf("buf = %s\n",buf);
	write(fd,buf,strlen(buf));
	memset(buf,0,strlen(buf));
	printf("buf = %s\n",buf);
	read(fd,buf,sizeof(buf)-1);
	printf("buf = %s\n",buf);
	
	close(fd);

	return 0;
}

3.8、结果

>>> dmesg -c
>>> insmod class_device_create.ko   
    
>>> sudo ./a.out                                                                                                 
buf = abcdefg_abcdefg
buf = 
buf = abcdefg_abcdefg
    
>>> dmesg                                                                                                        
[ 4059.087071] kbuf = abcdefg_abcdefg
    
>>> rmmod class_device_create                                                                                      

3.9、source insight的使用

1.双击安装source insight.exe即可(sn.txt激活秘钥)
2.使用source insight创建索引
	2.1在window上解压一个linux内核压缩包 D:\code\kernel-3.4.39
	2.2使用source insight先创建一个工程 project->new project-> 填写工程的名字和路径 linux-kernel-3.4.39
		D:\code\kernel-3.4.39\linux-kernel-3.4.39
	2.3在工程中添加文件
		找到内核的层目录->add all->选上两个对号->
		显示出来2万多个文件->点击close
	2.4为刚才添加的内核文件创建索引
		project->synchronize files->选中1-3,5的对号->
		等待进度条走完即可(10min)
		
3.如何使用source insight
	如何搜索,点击R图标,输入要搜索的字符串,点击搜索
	跳进去--->ctrl+鼠标左键
	退出来--->alt+,(或者回退的图标)
		
4.设置配色
	C:\Users\Administrator\Documents\Source Insight\Settings\GLOBAL.CF3
	options->loade configures->选中GLOBAL.CF3

3.10、读写寄存器数据的函数

writel(v,r)
功能:向寄存器中写值
参数:
	@v:要写的值
	@r:寄存器的地址
writel() 4字节
writew() 2字节 
writeb() 1字节

readl(addr) 
功能:读取寄存器中的值
参数:
	@addr:寄存器的地址
返回值:读取到的值
readl() 4字节
readw() 2字节 
readb() 1字节

3.11、API

//1、注册目录信息
class_create(owner, name);

//2、注销目录信息
void class_destroy(struct class *cls);

//3、注册设备信息
struct device *device_create(struct class *cls,struct device *parent,dev_t dev,void *drvdata,const char *fmt,···);

//4、注销设备信息
void device_destroy(struct class *class,dev_t devt);

//5、判断错误码
IS_ERR(const void *ptr);

//6、错误码指针-->错误码
PTR_ERR(const void *ptr);

//7、错误码-->错误码指针
ERR_PTR(long error);

//8、向寄存器中写值
writel(v,r);

//9、从寄存器中读值
readl(r);

3.12、作业:

1> 让adc驱动支持自动创建设备节点
2> adc操作寄存器的时候使用writel/readl
3> adc寄存器的设置通过ioctl让用户来操作

四、字符设备驱动的实现

4.1、文件打开原理

user:
	fd=open("/dev/myled")   read(fd,)  write(fd,) close(fd);
	 |
	 |
	 /dev/myled----ls -i ---->inode号(stat)
								|
-----------------------------------|---------------------
kernel:	
		struct inode{ //只要有一个文件存在,就有一个inode结构体存在,inode是对文件的所有信息的描述
			umode_t  i_mode; //文件的权限
			uid_t	 i_uid;  //用户id
			gid_t	 i_gid;  //组的id
			unsigned long i_ino; //inode号
			dev_t	 i_rdev; //设备号
			union {
				struct block_device	*i_bdev;	//块设备驱动
				struct cdev			*i_cdev; //重点		//字符设备驱动
			};
		};

4.2、驱动的实现的流程

1.分配对象
	struct cdev  cdev;
	struct cdev  *pcdev = cdev_alloc();
2.对象的初始化
	cdev_init(pcdev,&fops);
	静态申请设备号	register_chrdev_region();
	动态申请设备号	alloc_chrdev_region();
3.硬件相关的操作
		
4.注册设备    
	cdev_add();
5.注销设备
	cdev_del()
----------------------------------------------
hardware:  LED
    

//  我的步骤:
01、申请设备号:(需要设备名)
02、分配设备:(什么都不需要)
03、初始化设备:(需要设备结构体+操作方法)
04、注册设备:(需要设备结构体+设备号)
05、自动创建设备节点-->1、提交目录信息:(需要:目录名)(返回:目录句柄)
06、自动创建设备节点-->2、提交设备信息:(需要:目录句柄 + 设备号 + 设备名)

07、硬件相关操作:(根据情况是否需要)
08、注销硬件相关操作:(根据情况是否需要)

09、自动释放设备节点-->2、注销设备信息:(需要:目录句柄 + 设备号)
10、自动释放设备节点-->1、注销目录信息:(需要:目录句柄)
11、注销设备:(需要:设备结构体)
12、释放设备:(需要:设备结构体)
13、释放设备号:((主设备号+次设备起始号)+次设备号个数)	


4.3、字符设备驱动的结构体和API

1、字符设备结构体

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

2、参数变量

#define CDEVNAME "mydevice1" 	//设备名
//01、申请设备号
int major = 0; 	//主设备号 
int minor = 0; 	//次设备起始号
const unsigned count= 3; 		//次设备号个数

//02、分配设备:	
struct cdev *pcdev = NULL; 				//字符设备结构体

//03、初始化设备:
const struct file_operations *ops; //操作方法结构体

//04、注册设备:

//05、提交目录信息
struct class *cls = NULL; 		//目录信息结构体

//06、提交设备信息
struct device *dev = NULL; 		//字符设备信息结构体

3、API

// 01、申请设备号:
//静态指定:
    int register_chrdev_region(dev_t from,unsigned count,const char *name)
    功能:静态指定设备号
    参数:
        @from :设备号的起始的值
        @count:设备号的个数
        @name :名字 cat /proc/devices
    返回值:成功返回0,失败返回错误码
//动态申请:
    int alloc_chrdev_region(dev_t *dev,unsigned baseminor,unsigned count,const char *name)
    功能:动态申请设备号
    参数:
        @dev:申请到的设备号 (output)
        @baseminor:起始的次设备号
        @count:设备的个数
        @name:名字
    返回值:成功返回0,失败返回错误码	
                
// 02、分配设备:-->分配字符设备驱动的内存
struct cdev *cdev_alloc(void);     
功能:分配cdev结构体大小的内存
参数:
    @无
返回值:成功返回首地址,失败返回NULL;

// 03、字符设备驱动初始化的函数
void cdev_init(struct cdev *cdev,const struct file_operations *fops);
功能:字符设备驱动的初始化
参数:
    @cdev:cdev的结构体指针
    @fops:操作方法结构体
返回值:无	
        
// 04、注册设备
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
功能:注册字符设备驱动
参数:
    @p:cdev的结构体指针
    @dev:设备号
    @count:个数
返回值:成功返回0,失败返回错误码
            
// 05、自动创建设备节点-->1、提交目录信息	-->cat /sys/class
class_create(owner, name) 	这是一个有"返回值"的"宏"
功能:在/sys/class下创建目录
参数:
	owner:THIS_MODULE
	name : 目录名		
返回值:成功返回 class 的结构体指针 struct class * --> 目录句柄
	  失败返回错误码指针(void *)-1;        
            
// 06、自动创建设备节点-->2、提交设备信息
struct device *device_create(struct class *class,struct device *parent,dev_t devt,void *drvdata,
                             const char *fmt, ...)		
功能:提交设备信息
参数:
	@class : 目录的句柄
	@parent: NULL 		//提交 父级目录信息 创建新的目录时(即没有父级目录) 可以填 NULL
	@devt  :设备号
	@drvdata:NULL  		//向驱动设备传递的私有的字符串数据,一般不传,可以填NULL
	@fmt   :设备名
返回值:成功返回device的结构体指针struct device *
	  失败返回错误码指针(void *)-1;

MAJOR(dev)	//根据设备号得到主设备号
MINOR(dev)  //根据设备号获取次设备号
MKDEV(ma,mi) //根据主,次设备号合成设备号           
            
// 07、硬件相关操作:(根据情况是否需要)
// 08、注销硬件相关操作:(根据情况是否需要)   
    
    
// 09、自动释放设备节点-->2、注销设备信息:(需要:目录句柄 + 设备号)
void device_destroy(struct class *class,dev_t devt)
功能:注销设备信息
参数:
	@class : 目录的句柄
	@devt  :设备号
返回值:无 
    
// 10、自动释放设备节点-->1、注销目录信息:(需要:目录句柄)
void class_destroy(struct class *class)
功能:删除在/sys/class下创建目录
参数:
	@class : 目录的句柄	
返回值:无	

// 11、注销设备:(需要:设备结构体)
void cdev_del(struct cdev *p)
功能:注销字符设备驱动
参数:
    @p:cdev结构体指针
返回值:无
        
// 12、释放设备:(需要:设备结构体)
kfree(struct cdev *p) 
功能:释放字符设备驱动
参数:
    @p:cdev结构体指针
返回值:无
        
// 13、释放设备号:((主设备号+次设备起始号)+次设备号个数)	   
void unregister_chrdev_region(dev_t from, unsigned count)
功能:释放申请的设备号
参数:
    @dev:申请到的设备号 (output)
    @count:设备的个数
返回值:成功返回0,失败返回错误码

4.4、如何通过 fd 找到驱动¥¥¥¥¥

fd:文件描述符,它是一个数组的下标?由于fd是在进程中产生的,所以在进程的结构体中就可以找到fd产生的相关的信息
//进程结构体
struct task_struct{
	volatile long state; //进程的状态
	int prio, static_prio, normal_prio; //进程的优先级
	unsigned int rt_priority; //进程的优先级
	int pdeath_signal;  /*  The signal sent when the parent dies  */
	pid_t pid;   //进程的PID
	struct task_struct  *parent;
	struct thread_struct thread; //进程内线程的描述
	struct files_struct *files; /* open file information */
						|--->struct file * fd_array[NR_OPEN_DEFAULT];// 文件描述符数组
}

//文件 file 结构体
struct file {
	struct path	 f_path; //文件路径
	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; //私有数据
}


通过open()从路径找到文件的inode号,通过唯一的inode号找到inode结构体,
    若文件存在则填充进程中的文件数组结构体,并返回数组下标-->文件描述符:fd
//通过fd找到驱动
fd---------->fd_array[fd]--->struct file *--->fops
文件描述符--->文件数组------->文件结构体-------->文件操作方法
    
在一个进程内只要调用一次open,就会产生一个file结构体,可以通过fd作为fd_array的下标的方式找到file结构体。
file 结构体就是对你打开文件时候的各种信息的描述(文件打开的读写的权限,文件打开时候是否以阻塞方式打开)
    

4.5、简述字符设备框架¥¥¥¥¥

1、先说应用层的 open()、read()、write()、close()函数怎么调到
   驱动层file_operations结构体中的 open()、read()、write()、close()函数
    
2、然后再详细说如何通过 file结构体、inode结构体来找到驱动的
    进程在通过open()函数打开一个文件时,传入一个文件路径参数
    通过文件路径找到文件的 inode 号 			// ionde号在内核中以链表的形式管理
    inode 号:文件系统识别文件的唯一标号
    通过 inode 号找到唯一的inode结构体	 //只要有一个文件存在,就有一个inode结构体存在,inode结构体是对文件所有信息的描述
	struct inode{ 
        unsigned long i_ino; //inode号
        dev_t	 i_rdev; //设备号
        union {	
			struct block_device	*i_bdev;	//块设备驱动
            struct cdev			*i_cdev;	//字符设备驱动 重点
        };
    }
inode 结构体中有
    对应的inode号(unsigned long i_ino; "inode号")
    对应的设备号(dev_t i_rdev; "设备号")
    对应的字符设备驱动(struct cdev *i_cdev; "字符设备驱动" "重点")
    
    
总分总模式述说

4.6、进程中找到对应的文件

1、使用open()打开一个文件

1.1、文件路径 ---> inode 号

//根据传入的文件路径,找到文件对应的inode号
//inode 号:文件系统识别文件的唯一标号
查看 inode 号 的命令:ls - i

使用下列函数可以得到文件的 inode 号
int stat(const char *pasth, struct stat *buf);
int fstat(int fd, struct stat *buf);
int lstat(const char *path, struct stat *buf);

通过 man 2 stat  查看函数

1.2、inode 号 ---> inode 结构体

//根据inode号,找到文件对应的inode结构体
//inode 结构体:对文件所有信息的描述,只要有一个文件存在,就有一个inode结构体存在

//inode 结构体
struct inode{
    umode_t  i_mode; 	//文件的权限
    uid_t	 i_uid;  	//用户id
    gid_t	 i_gid; 	//组的id
    unsigned long i_ino; //inode号
    dev_t	 i_rdev; 	 //设备号
    union {
        struct block_device	*i_bdev;	//块设备驱动的结构体
        struct cdev			*i_cdev;	//字符设备驱动的结构体->重点
    };			
}

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

1.3、inode 结构体 ---> 创建 file 结构体

//在进程运行中只要使用了open()函数打开了一次文件,就会在进程结构体中创建一个 file 文件结构体,
//file结构体中的参数对应着inode结构体中的参数,并记录着文件打开时的各种状态
//文件 file 结构体

struct file {
	struct path	 f_path;	//文件路径
	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;	//私有数据
}

1.4、file 结构体 ---> 文件描述符

//将这个 file 文件结构体"放入"task_struct 进程结构体中的 files_struct 进程中打开的文件信息结构体中的文件描述符"数组"中,
//并返回一个数据下标,即文件描述符
//进程 task_struct 结构体

struct task_struct{
	struct files_struct *files; //打开文件的信息
}
//进程中打开的文件信息结构体
struct files_struct {
	struct file __rcu * fd_array[NR_OPEN_DEFAULT];// 文件描述符数组
};

2、第二次使用read()、write()操作一个文件时

2.1、文件描述符 ---> file 结构体

//在 task_struct 进程结构体中的 files_struct 进程中打开的文件信息结构体中的 文件描述符数组中,
//根据传入的参数有:文件描述符 即数组下标找到对应的 file 文件结构体

//进程 task_struct 结构体
struct task_struct{
	struct files_struct *files; //打开文件的信息
}

//进程中打开的文件信息结构体
struct files_struct {
	struct file __rcu * fd_array[NR_OPEN_DEFAULT];// 文件描述符数组
};

//文件 file 结构体
struct file {
	struct path	 f_path;	//文件路径
	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;	//私有数据
}

2.2、file 结构体 ---> file_operations 操作方法结构体

//根据 file 文件结构体中的 file_operations 文件操作方法结构体,对文件进行具体操作
//文件 file 结构体
struct file {
	struct path	 f_path;	//文件路径
	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 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 *);
	int (*open) (struct inode *, struct file *);
	int (*release) (struct inode *, struct file *);
};

进程中找到对应的文件

练习:字符设备驱动分步实现的练习
	echo 1 > /dev/myled0 ===>红灯亮
	echo 0 > /dev/myled0 ===>红灯熄灭
	
		myled0   myled1   myled2
	-----|----------|--------|----------
		RED      GREEN     BLUE
posted on 2021-02-01 13:33  八杯水  阅读(389)  评论(0)    收藏  举报