Linux字符设备驱动模型

版本 颁布日期 修订章节
0.1 2015.08.13 撰写草稿
0.2 2015.12.07 整合字符型设备驱动
0.3 2015.12.07 整理文档,尚未验证驱动源码
0.4 2016.02.28 补充信息,整理结构关系
0.5 2016.03.13 验证驱动程序,正式发布

文章目录

  • Linux设备驱动一些概念
    • 操作一个文件需要的步骤:(大象放冰箱)
    • 编写一个特定设备的驱动程序
    • 驱动分类
      • 字符设备
      • 块设备
      • 网络设备
  • Linux字符设备
    • 结构体赋值C99新标准
    • 字符设备文件操作核心结构
      • 文件操作方法集合结构体在驱动中的使用
  • 字符型设备的三种驱动模型
    • 字符型设备模型类型以及常用函数和方法
    • 驱动应用程序(app)
      • 头文件
      • 具体函数格式
      • open函数
      • close关闭函数
      • read,write读写操作函数
    • 用户空间与内核空间的数据交换
    • 物理地址转换成虚拟地址
    • 对应的Makefile编写
      • 利用Makefile编译驱动示例

Linux设备驱动一些概念

Linux一切皆是文件,串口、鼠标、键盘等,在内核中都体现为一个文件。如果要操作一个硬件,就通过操作其对应的文件。

操作一个文件需要的步骤:(大象放冰箱)

1、	打开文件:不同类型文件,打开方法不一样。
2、	操作文件:不同文件读写方法不同。操作方法:读、写、定位。
3、	关闭文件。

编写一个特定设备的驱动程序

1、 主要实现这个设备的操作方法;-------驱动编程的主要工作;

2、 把实现的文件操作方法向系统注册;------字符设备模型;

3、 给用户空间提供访问的接口 ----有dev下的设备文件;
Linux下是分层的,分为内核空间和用户空间
默认情况下:
内核空间和用户空间的比例是3:1,在Linux源码配置中可以修改
目的:保证系统安全,不允许应用程序直接访问硬件。
例:任何一个软件,具有直接控制网卡功能,只要把网卡参数设置错误,则整个系统都不能用网卡。
分层操作,如果应用软件出问题,不会影响系统
内核空间:3~4G,运行驱动程序
用户空间:0~3G,运行应用程序-----main这类程序

4、 驱动程序运行在内核空间,应用程序运行在应用空间,不能直接交流,需要驱动程序给应用程序提供访问接口;

5、 每一类设备驱动都有特定的编程框架(模型),学习驱动编写,重点是学习每一类设备的编程框架。每一种文集操作方法也是特定的编程框架;

6、 设备号。每一个设备都会有一个正整数表示的设备号。
设备号由主设备号和次设备号组成
主设备号:表示一种设备
次设备号:表示一种设备中的具体哪一个

例:4个串口驱动都是相同的,(寄存器空间不同,但操作方法、逻辑相同)
就是主设备号相同,次设备号不同

[root@ZX20150811 /dev]# ls -l | grep ttySAC
crw-rw----    1 root     root      204,  64 Aug 12  2015 ttySAC0
crw-rw----    1 root     root      204,  65 Aug 12  2015 ttySAC1
crw-rw----    1 root     root      204,  66 Aug 12  2015 ttySAC2
crw-rw----    1 root     root      204,  67 Aug 12  2015 ttySAC3

串口:ttySAC,有4个串口,主设备号为204
现在要使用串口0
打开/dev/ttySAC0
配置波特率、数据位、停止位、校验位,就可以对串口读写操作.
关闭串口

驱动分类

字符设备

以字节为单位,进行顺序访问的设备。
例:串口、鼠标、键盘、按键、触摸屏、LED……
有个例外:LCD,可以随机访问,因为他是一块内存来实现显示。

实际工作中最常见的也是使用最多的,这类设备一般会在dev目录下有一个对应的设备文件。

块设备

以块为单位进行访问,(可以随机访问)的设备。
块:512字节
一次可以读写一块或多块。
例子:SD卡、U盘、硬盘、eMMC、Flash等存储类设备。

网络设备

网络通信设备
例:网卡、WiFi(无线网卡)

这类设备不同于字符设备和块设备,在dev目录下没有对应的设备文件,操作方法也不一样。

Linux字符设备

结构体赋值C99新标准

标准C99结构体赋值方法

struct miscdevice  
{
	.minor=255,
	.name=”zxng”,
	.fops=&misc_fops,
}

新方法解释:
1、赋值可以选其中部分进行初始化;
2、赋值与顺序无关;
3、赋值的开头是“.”加结构体成员,并以“,”结束;
4、kill编译器默认不能认识
要使kill能够识别新标准,在 C/C++选项,头文件下面的选项框上加上-c99

标准C89结构体赋值方法

struct miscdevice  {
	255;
    “zxng”;
	&misc_fops;
}

赋值只能按顺序初始化结构体成员,并且不能跳过不需要赋值的选项

字符设备文件操作核心结构

文件操作集合
file_operations *fops
这个结构是一个通用结构,不针对某一项设备,它考虑的是所有字符型设备可能需要的操作方法,对具体一个设备,只需要实现一部分接口就可以了。

对于LED灯而言可以操作的有
1、 控制亮灭,------可以使用write
2、 查询状态。------可以使用read
3、 操作文件第一步是打开文件 ------可以使用open
4、 操作文件最后一步是关闭文件 ------release

file_operations结构体所包含的函数指针:

struct file_operations {
	struct module *owner;
	loff_t (*llseek) (struct file *, loff_t, int);
	ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
	ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
	ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
	ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
	int (*readdir) (struct file *, void *, filldir_t);
	unsigned int (*poll) (struct file *, struct poll_table_struct *);
	long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
	long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
	int (*mmap) (struct file *, struct vm_area_struct *);
	int (*open) (struct inode *, struct file *);
	int (*flush) (struct file *, fl_owner_t id);
	int (*release) (struct inode *, struct file *);
	int (*fsync) (struct file *, loff_t, loff_t, int datasync);
	int (*aio_fsync) (struct kiocb *, int datasync);
	int (*fasync) (int, struct file *, int);
	int (*lock) (struct file *, int, struct file_lock *);
	ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
	unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
	int (*check_flags)(int);
	int (*flock) (struct file *, int, struct file_lock *);
	ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
	ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
	int (*setlease)(struct file *, long, struct file_lock **);
	long (*fallocate)(struct file *file, int mode, loff_t offset,
			  loff_t len);
};

有两个函数很特殊,open和release,他可以不实现,系统使用默认的方式实现一个,找到驱动之后,其实什么也没做,直接返回
release:不需要应用程序调用对应的调用接口,当应用程序关闭时自动调用

常用函数
read、write、open、release、mmap、unlocked、ioctl、fasync

文件操作方法集合结构体在驱动中的使用

结构体前面加static,将本次使用的操作集合限制在这个驱动模块中
misc_fops:后面的操作集合都加前缀misc,表示是这个驱动所使用的函数

static const  struct file_operations misc_fops = {
    .open		= misc_open,
    .read		= misc_read,
    .write		= misc_write,
    .release	= misc_release,
};

字符型设备的三种驱动模型

1、 驱动就是按模块的格式进行编写;
2、 模块不一定是驱动,但驱动(代码格式)就一定是模块;
3、 普通模块基础上添加上对应的驱动模型代码,就构成了驱动。

字符型设备模型类型以及常用函数和方法

Linux字符型设备分为三类:
1、杂项设备
2、早期的经典标准字符驱动模型
3、Linux2.6版本新出的标准字符驱动模型

驱动应用程序(app)

写一个应用程序,通过系统调用接口,来调用驱动程序,注意,这个应用程序是在用户空间上运行的。
应用程序通过设备号来查找驱动

在Linux虚拟机下
[root@phoenix works]# man 2 open

头文件

#include 
#include 
#include 
#include 

具体函数格式

int open(const char *pathname, int flags);
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);

open函数

使用open函数的操作类型

flags
O_RDONLY   (只读)
O_WRONLY   (只写)
O_RDWR.     (可读可写)

写读写函数,分别通过flags开控制,观察结果。

例:
fd = open(DEV_NAME, O_RDWR);

close关闭函数

close(fd);
这个函数可以不写,程序关闭时会调用close函数

read,write读写操作函数

1、Linux系统调用,app程序在用户空间上的调用
通过输入命令:man 2 write 查询得到

ssize_t write(int fd, const void *buf, size_t count);
ssize_t read(int fd, void *buf, size_t count);

输入参数解释:

fd		:文件描述符号,传入是open的返回值,标识一个在进程中打开的文件
buf	:数据源指针
count	:要写入的数据字节数量或要读出的字节数量

返回值
成功	:write返回写入的字节数量,read返回读取字节数量。
失败	:-1,并且会设置全局错误代码变量errno;

2、对应到内核驱动文件操作方法接口

通过SI工程进入file_operations结构体中得到

static ssize_t misc_write (struct file *pfile, const char __user *buff, size_t count, loff_t *off)
static ssize_t misc_read (struct file *pfile, char __user *buff, size_t size, loff_t *off)

输入参数解释

pfile :struct file每调用一次open,都会创建这个结构
buff :用户空间传递下来的数据缓冲区指针
off  :是文件读写指针(当前光标所在的位置)

3、验证参数对应关系

在驱动的read和write操作中,都会用到char __user *buff的指针,
实际上在驱动中不建议直接使用const char __user *buff参数指针,因为这个指针有可能是一个非法的指针,后果是在安装驱动的时候会导致系统异常,安装驱动会有一大串代码。
非法指针会导致内核异常,只需要把应用程序write修改一下,write(fd,(void *10),1);
当运行app时候,内核异常,同于会导致出错,需要进行参数检测,修正数量。
要想安全的使用用户空间传下来的数据,必须对指针进行合法性检测,合法再去访问。
内核提供的专用函数给我们访问。

用户空间与内核空间的数据交换

在内核驱动代码不能直接使用用户空间的指针来存取数据,需要通过专用的函数来完成数据的复制。
头文件
#include

从用户空间读取数据(应用程序传数据给内核)
copy_from_user 用在write接口

复制数据到用户空间(内核传数据给应用程序)
copy_to_user 用在read接口

这两个函数先检查指针的合法性,合法才进行复制操作

原型
unsigned long copy_from_user(void *to, const void __user *from, unsigned long n)
unsigned long copy_to_user(void __user *to, const void *from, unsigned long n)

输入参数解释:

unsigned long copy_from_user(void *to, const void __user *from, unsigned long n)
to		:内核空间的缓冲区指针
from	:用户空间的缓冲区指针
n		:要复制的字节数

返回值
成功	:返回0(没有成功复制的字节数量为0)。
失败	:返回没有成功复制的字节数量(正数)。
unsigned long copy_to_user(void __user *to, const void *from, unsigned long n)
to		:用户空间的缓冲区指针
from	:内核空间的缓冲区指针
n		:要复制的字节数

返回值
成功	:返回0(没有成功复制的字节数量为0)。
失败	:返回没有成功复制的字节数量(正数)。

物理地址转换成虚拟地址

虚拟地址映射

函数原型
static inline void __iomem *ioremap(phys_addr_t offset, unsigned long size)

输入参数解释:

offset  :要映射的物理起始地址
size    :要映射的大小(例如:0x18)

返回值	:物理地址映射后得到的虚拟地址首地址

例:base_addr = (unsigned int *)ioremap (GPM4BASE, GPM4SIZE);
初始化先映射虚拟地址,通过虚拟地址来操作寄存器

关闭进程123
kill -9 123

虚拟地址释放

释放虚拟地址,映射取消
static inline void iounmap(volatile void __iomem *addr)

addr:就是使用ioremap虚拟出来的虚拟地址,
例:iounmap(base_addr);

在相反的方向使用,
如果在open使用ioremap,则要在release接口使用iounmap
完整流程,前面映射成功,后面操作失败
注册失败也要释放虚拟地址

对应的Makefile编写

这里编写的Makefile是针对在Linux源码外编译驱动模块的,Linux驱动的编译依赖于Linux源码,吧Linux驱动单独拿出来在外边编写程序,但当驱动编译成机器码时就需要依赖于Linux源码,由于Linux驱动是在内核空间进行操作的,所以对驱动对于系统的安全非常关键。因此在Linux源码上有可以写入作者的标识,只有标识相同的Linux内核系统以及依赖于相同标识的Linux源码编译出来的驱动才能够正常安装,也就是别的Linux驱动的机器码不能安装到自己的Linux系统上,只有拿到该驱动的源码,将源码依赖于自己Linux源码进行再次编译,才能够安装驱动。
在make menuconfig的配置中有自定义的标识

下面是一个常规的编译驱动模块的Makefile模板。

#需要编译的目标
obj-m += misc_zxled.o

#Linux源码

#编译基于X86所在目录
#KDIR := /lib/modules/$(shell uname -r)/build
#编译基于ARM所在目录,该目录对应Linux源码	
KDIR := /works/linux-3.5/

all:
	#编译驱动模块
	make -C $(KDIR) M=$(PWD) modules
	#如果需要的话,编译app程序
	arm-linux-gcc app.c -o app
	#删除编译产生的临时文件,前面加@则是在终端不打印输出
	@rm -f *.o *.mod.o *.mod.c *.symvers *.markers *.unsigned *.order *~ *.bak
	#复制驱动以及应用程序到相应的根文件系统目录,也可以手动将文件复制
	cp *.ko app /work/root_nfs/home

clean:
	make -C $(KDIR) M=`pwd` modules clean
	rm -f app

利用Makefile编译驱动示例

[root@phoenix misc]# ls
app.c  Makefile  misc_zxled.c
[root@phoenix misc]# make
make -C /works/linux-3.5/ M=/mnt/hgfs/share/workspace/misc modules
make[1]: Entering directory `/works/linux-3.5'
  CC [M]  /mnt/hgfs/share/workspace/misc/misc_zxled.o
  Building modules, stage 2.
  MODPOST 1 modules
  CC      /mnt/hgfs/share/workspace/misc/misc_zxled.mod.o
  LD [M]  /mnt/hgfs/share/workspace/misc/misc_zxled.ko
make[1]: Leaving directory `/works/linux-3.5'
arm-linux-gcc app.c -o app
[root@phoenix misc]# ls
app  app.c  Makefile  misc_zxled.c  misc_zxled.ko
[root@phoenix misc]# make clean
make -C /works/linux-3.5/ M=`pwd` modules clean
make[1]: Entering directory `/works/linux-3.5'
  LD      /mnt/hgfs/share/workspace/misc/built-in.o
  CC [M]  /mnt/hgfs/share/workspace/misc/misc_zxled.o
  Building modules, stage 2.
  MODPOST 1 modules
  CC      /mnt/hgfs/share/workspace/misc/misc_zxled.mod.o
  LD [M]  /mnt/hgfs/share/workspace/misc/misc_zxled.ko
  CLEAN   /mnt/hgfs/share/workspace/misc/.tmp_versions
  CLEAN   /mnt/hgfs/share/workspace/misc/Module.symvers
make[1]: Leaving directory `/works/linux-3.5'
rm -f app
[root@phoenix misc]# ls
app.c  Makefile  misc_zxled.c
[root@phoenix misc]#

你可能感兴趣的:(Linux,驱动,Linux驱动)