2.字符设备驱动基础

版权声明:转载请声明 https://blog.csdn.net/qq_40732350/article/details/82918531

1.开启驱动开发之路


1.1、驱动开发的准备工作

(1)正常运行linux系统的开发板。要求开发板中的linux的zImage必须是自己编译的,不能是别人编译的。
(2)内核源码树,其实就是一个经过了配置编译之后的内核源码。
(3)nfs挂载的rootfs,主机ubuntu中必须搭建一个nfs服务器。


1.2、驱动开发的步骤

(1)驱动源码编写、Makefile编写、编译
(2)insmod装载模块、测试、rmmod卸载模块


1.3、实践

(1)copy原来提供的x210kernel.tar.bz2,找一个干净的目录(/root/driver),解压之,并且配置编译。编译完成后得到了:1、内核源码树。2、编译ok的zImage
(2)fastboot将第1步中得到的zImage烧录到开发板中去启动(或者将zImage丢到tftp的共享目录,uboot启动时tftp下载启动),将来驱动编译好后,就可以在这个内核中去测试。因为这个zImage和内核源码树是一伙的,所以驱动安装时版本校验不会出错。


2.最简单的模块源码分析1

2.1、常用的模块操作命令

(1)lsmod(list module,将模块列表显示),功能是打印出当前内核中已经安装的模块列表
(2)insmod(install module,安装模块),功能是向当前内核中去安装一个模块,用法是insmod xxx.ko
(3)modinfo(module information,模块信息),功能是打印出一个内核模块的自带信息。,用法是modinfo xxx.ko
(4)rmmod(remove module,卸载模块),功能是从当前内核中卸载一个已经安装了的模块,用法是rmmod xxx(注意卸载模块时只需要输入模块名即可,不能加.ko后缀)
(5)如modprobe、depmod

modprobe命令比insmod命令要强大,它在加载某模块时,会同时加载该模块所依赖的其他模块。使用modprobe命令加载的模块若以"modprobe-r filename"的方式卸载,将同时卸载其依赖的模块。

模块之间的依赖关系存放在根文件系统,的/ib/modules//modules.dep文件中,实际上是在整体编译内核的时候由depmod工具生成的,它的格式非常


2.2、模块的安装

(1)先lsmod再insmod看安装前后系统内模块记录。

实践测试标明内核会将最新安装的模块放在lsmod显示的最前面。


(2)insmod与module_init宏。

模块源代码中用module_init宏声明了一个函数(在我们这个例子里是chrdev_init函数),作用就是指定chrdev_init这个函数和insmod命令绑定起来,也就是说当我们insmod module_test.ko时,insmod命令内部实际执行的操作就是帮我们调用chrdev_init函数。
照此分析,那insmod时就应该能看到chrdev_init中使用printk打印出来的一个chrdev_init字符串,但是实际没看到。原因是ubuntu中拦截了,要怎么才能看到呢?在ubuntu中使用dmesg命令就可以看到了。


(3)模块安装时insmod内部除了帮我们调用module_init宏所声明的函数外,实际还做了一些别的事(譬如lsmod能看到多了一个模块也是insmod帮我们在内部做了记录),但是我们就不用管了。

module_init(chrdev_init);  //模块安装

module_exit(chrdev_exit);  //模块卸载

模块加载函数:

在Linux内核中,可以使用request_module(constchar*fmt, …)函数加载内核模块,驱动开发人员可以通过调
用下列代码:
request_module(module_name);


模块参数

我们可以用“module_param(参数名,参数类型,参数读/写权限) ”为模块定义一个参数,例如下列代码定义了1个整型
参数和1个字符指针参数:

static char *book_name = "dissecting Linux Device Driver";
module_param(book_name, charp, S_IRUGO);
static int book_num = 4000;
module_param(book_num, int, S_IRUGO);

在装载内核模块时,用户可以向模块传递参数,形式为“insmode(或modprobe)模块名参数名=参数值”,如果不传
递,参数将使用模块内定义的缺省值。

如果模块被内置,就无法insmod了,但是bootloader可以通过在bootargs里设置“模块名.参数名=值”的形式给该内置的模块传递参数


2.3、模块的版本信息

(1)使用modinfo查看模块的版本信息
(2)内核zImage中也有一个确定的版本信息
(3)insmod时模块的vermagic必须和内核的相同,

否则不能安装,

报错信息为:insmod: ERROR: could not insert module module_test.ko: Invalid module format

(4)模块的版本信息是为了保证模块和内核的兼容性,是一种安全措施
(5)如何保证模块的vermagic和内核的vermagic一致?编译模块的内核源码树就是我们编译正在运行的这个内核的那个内核源码树即可。说白了就是模块和内核要同出一门。

2.4得到输出信息

正常的是不能在终端输出驱动打印的信息

用dmesg命令可以得到


3.最简单的模块源码分析2

3.1、模块卸载

(1)module_exit和rmmod的对应关系
(2)lsmod查看rmmod前后系统的模块记录变化


3.2、模块中常用宏

(1)MODULE_LICENSE,模块的许可证。

一般声明为GPL许可证,而且最好不要少,否则可能会出现莫名其妙的错误(譬如一些明显存在的函数提升找不到)。
(2)MODULE_AUTHOR:作者
(3)MODULE_DESCRIPTION:描述
(4)MODULE_ALIAS:别名

(5)MODULE_DEVICE_TABLE:设备表

对于USB、 PCI等设备驱动,通常会创建一个MODULE_DEVICE_TABLE,以表明该驱动模块所支持的设备,如代码清单4.6所示

(6)MODULE_VERSION:版本


3.3、函数修饰符

(1)__init,本质上是个宏定义,在内核源代码中就有#define __init xxxx。这个__init的作用就是将被他修饰的函数放入.init.text段中去(本来默认情况下函数是被放入.text段中)。
整个内核中的所有的这类函数都会被链接器链接放入.init.text段中,所以所有的内核模块的__init修饰的函数其实是被统一放在一起的。内核启动时统一会加载.init.text段中的这些模块安装函数,加载完后就会把这个段给释放掉以节省内存。
(2)__exit
3.4、static

实例代码:

#include <linux/init.h>
#include <linux/module.h>

static char *book_name = "dissecting Linux Device Driver";
module_param(book_name, charp, S_IRUGO);

static int book_num = 4000;
module_param(book_num, int, S_IRUGO);

 static int __init book_init(void)
{
    printk(KERN_INFO "book name:%s\n", book_name);
    printk(KERN_INFO "book num:%d\n", book_num);
    return 0;
}
 module_init(book_init);

 static void __exit book_exit(void)
 {
     printk(KERN_INFO "book module exit\n ");
 }
 module_exit(book_exit);

 MODULE_AUTHOR("Barry Song <[email protected]>");
 MODULE_LICENSE("GPL v2");
 MODULE_DESCRIPTION("A simple Module for testing module params");
 MODULE_VERSION("V1.0")

运行命令:“insmod book.ko book_name='GoodBook' book_num=5000”

4.最简单的模块源码分析3

4.1、printk函数详解

(1)printk在内核源码中用来打印信息的函数,用法和printf非常相似。
(2)printk和printf最大的差别:printf是C库函数,是在应用层编程中使用的,不能在linux内核源代码中使用;printk是linux内核源代码中自己封装出来的一个打印函数,是内核源码中的一个普通函数,只能在内核源码范围内使用,不能在应用编程中使用。
(3)printk相比printf来说还多了个:打印级别的设置。printk的打印级别是用来控制printk打印的这条信息是否在终端上显示的。应用程序中的调试信息要么全部打开要么全部关闭,一般用条件编译来实现(DEBUG宏),但是在内核中,因为内核非常庞大,打印信息非常多,有时候整体调试内核时打印信息要么太多找不到想要的要么一个没有没法调试。所以才有了打印级别这个概念。
(4)操作系统的命令行中也有一个打印信息级别属性,值为0-7。当前操作系统中执行printk的时候会去对比printk中的打印级别和我的命令行中设置的打印级别,小于我的命令行设置级别的信息会被放行打印出来,大于的就被拦截的。譬如我的ubuntu中的打印级别默认是4,那么printk中设置的级别比4小的就能打印出来,比4大的就不能打印出来。
(5)ubuntu中这个printk的打印级别控制没法实践,ubuntu中不管你把级别怎么设置都不能直接打印出来,必须dmesg命令去查看。


4.2、关于驱动模块中的头文件

(1)驱动源代码中包含的头文件和原来应用编程程序中包含的头文件不是一回事。应用编程中包含的头文件是应用层的头文件,是应用程序的编译器带来的(譬如gcc的头文件路径在 /usr/include下,这些东西是和操作系统无关的)。驱动源码属于内核源码的一部分,驱动源码中的头文件其实就是内核源代码目录下的include目录下的头文件。

4.3、驱动编译的Makefile分析

(1)KERN_DIR,变量的值就是我们用来编译这个模块的内核源码树的目录
(2)obj-m += module_test.o,这一行就表示我们要将module_test.c文件编译成一个模块    
(3)make -C $(KERN_DIR) M=`pwd` modules     这个命令用来实际编译模块,工作原理就是:利用make -C进入到我们指定的内核源码树目录下,然后在源码目录树下借用内核源码中定义的模块编译规则去编译这个模块,编译完成后把生成的文件还拷贝到当前目录下,完成编译。
(4)make clean ,用来清除编译痕迹
总结:模块的makefile非常简单,本身并不能完成模块的编译,而是通过make -C进入到内核源码树下借用内核源码的体系来完成模块的编译链接的。这个Makefile本身是非常模式化的,3和4部分是永远不用动的,只有1和2需要动。1是内核源码树的目录,你必须根据自己的编译环境


5.用开发板来调试模块

5.1、设置bootcmd使开发板通过tftp下载自己建立的内核源码树编译得到的zImage
set bootcmd 'tftp 0x30008000 zImage;bootm 0x30008000'

5.2、设置bootargs使开发板从nfs去挂载rootfs(内核配置记得打开使能nfs形式的rootfs)
setenv bootargs root=/dev/nfs nfsroot=192.168.1.141:/root/porting_x210/rootfs/rootfs ip=192.168.1.10:192.168.1.141:192.168.1.1:255.255.255.0::eth0:off  init=/linuxrc console=ttySAC2,115200 

5.3、修改Makefile中的KERN_DIR使其指向自己建立的内核源码树
5.4、将自己编译好的驱动.ko文件放入nfs共享目录下去
5.5、开发板启动后使用insmod、rmmod、lsmod等去进行模块实验


6.字符设备驱动工作原理1

6.1、系统整体工作原理

(1)应用层->API->设备驱动->硬件
(2)API:open、read、write、close等
(3)驱动源码中提供真正的open、read、write、close等函数实体


6.2、file_operations结构体

(1)元素主要是函数指针,用来挂接实体函数地址
(2)每个设备驱动都需要一个该结构体类型的变量
(3)设备驱动向内核注册时提供该结构体类型的变量

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 *);
	int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
	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 *, 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 **);
}


6.3、注册字符设备驱动

(1)为何要注册驱动
(2)谁去负责注册
(3)向谁注册
(4)注册函数从哪里来
(5)注册前怎样?注册后怎样?注册产生什么结果?

用于字符设备的函数

自己指定注册:register_chrdev_region()

int register_chrdev_region(dev_t from, unsigned count, const char *name)

系统自动分配:alloc_chrdev_region()

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
			const char *name)

注销:unregister_chrdev_region()

void unregister_chrdev_region(dev_t from, unsigned count)



7.字符设备驱动工作原理2

7.1、register_chrdev详解(#include <linux/fs.h>)

(1)作用,驱动向内核注册自己的file_operations
(2)参数

static inline int register_chrdev(unsigned int major, const char *name,
				  const struct file_operations *fops)
{
	return __register_chrdev(major, 0, 256, name, fops);
}

major:设备号(可以自己指定)

name:设备名字

fops:file_operation结构体

(3)inline和static

inline:内联函数


7.2、内核如何管理字符设备驱动

(1)内核中有一个数组用来存储注册的字符设备驱动
(2)register_chrdev内部将我们要注册的驱动的信息(主要是 )存储在数组中相应的位置
(3)cat /proc/devices查看内核中已经注册过的字符设备驱动(和块设备驱动)
(4)好好理解主设备号(major)的概念


7.3、回顾和展望

(1)回顾:inline、static等关键字
(2)回顾:/proc文件系统的作用
(3)展望:将来深入学习驱动时可以去跟register_chrdev到内部看,验证我们上面讲的原理


8.字符设备驱动代码实践1

8.1、思路和框架

(1)目的:给空模块添加驱动壳子
(2)核心工作量:file_operations及其元素填充、注册驱动


8.2、如何动手写驱动代码

(1)脑海里先有框架,知道自己要干嘛
(2)细节代码不需要一个字一个字敲,可以到内核中去寻找参考代码复制过来改
(3)写下的所有代码必须心里清楚明白,不能似懂非懂


8.3、开始动手

(1)先定义file_operations结构体变量
(2)open和close函数原型确定、内容填充

static const struct file_operations test_fops = {
	.open = test_open,    //打开文件
	.write = test_write,
	.read = test_read,
	.release = test_release, //关闭文件
	.owner = THIS_MODULE,
};


9.字符设备驱动代码实践2

9.1、注册驱动
(1)主设备号的选择
(2)返回值的检测
9.2、驱动测试
(1)编译等 make && make cp
(2)insmod并且查看设备注册的现象
(3)rmmod并且查看设备注销的现象
9.3、让内核自动分配主设备号
(1)为什么要让内核自动分配
(2)如何实现?
(3)测试


10.应用程序如何调用驱动

10.1、驱动设备文件的创建
(1)何为设备文件
(2)设备文件的关键信息是:设备号 = 主设备号 + 次设备号,使用ls -l去查看设备文件,就可以得到这个设备文件对应的主次设备号。
(3)使用mknod创建设备文件:mknod /dev/xxx c 主设备号 次设备号
10.2、写应用来测试驱动
(1)还是原来的应用
(2)open、write、read、close等
(3)实验现象预测和验证
10.3、总结
(1)整体流程梳理、注意分层
(2)后续工作:添加读写接口


11.添加读写接口

11.1、在驱动中添加
11.2、在应用中添加
11.3、测试
11.4、应用和驱动之间的数据交换
(1)copy_from_user,用来将数据从用户空间复制到内核空间
(2)copy_to_user
注意:复制是和mmap的映射相对应去区分的

从用户空间拷贝数据到内核空间,失败返回没有被拷贝的字节数,成功返回0.


这么简单的一个函数却含盖了许多关于内核方面的知识,比如内核关于异常出错的处理.从用户空间拷贝数据到内核中时必须很小心,假如用户空间的数据地址是个非法的地址,或是超出用户空间的范围,或是那些地址还没有被映射到,都可能对内核产生很大的影响,如oops,或被造成系统安全的影响.所以copy_from_user函数的功能就不只是从用户空间拷贝数据那样简单了,他还要做一些指针检查连同处理这些
问题的方法.

copy_from_user主要是这个函数提供了两个功能:
1. 对用户进程传过来的地址范围进行合法性检查;
2.当用户传来的地址没有分配物理地址时,定义了缺页处理后的异常发生地址,保证程序顺利执行;

另外一个问题,直接使用memcpy时为什么没有出现异常?
在上面的分析过程中我们知道,只有用户传来的地址空间没有分配对应的物理地址时才会进行修复,
如果用户进程之前已经使用过这段空间,代表已经分配了物理地址,自然不会发生缺页异常。


12.读写接口实践

12.1、完成write和read函数
(1)copy_from_user函数的返回值定义,和常规有点不同。返回值如果成功复制则返回0,如果 不成功复制则返回尚未成功复制剩下的字节数。
12.2、读写回环测试
12.3、总结
(1)目前为止应用已经能够读写驱动(中的内存)
(2)后续工作:添加硬件操作代码


13.驱动中如何操控硬件

13.1、还是那个硬件
(1)硬件物理原理不变
(2)硬件操作接口(寄存器)不变
(3)硬件操作代码不变
13.2、哪里不同了?
(1)寄存器地址不同。原来是直接用物理地址,现在需要用该物理地址在内核虚拟地址空间相对应的虚拟地址。寄存器的物理地址是CPU设计时决定的,从datasheet中查找到的。
(2)编程方法不同。裸机中习惯直接用函数指针操作寄存器地址,而kernel中习惯用封装好的io读写函数来操作寄存器,以实现最大程度可移植性。
13.3、内核的虚拟地址映射方法
(1)为什么需要虚拟地址映射
(2)内核中有2套虚拟地址映射方法:动态和静态
(3)静态映射方法的特点:
    内核移植时以代码的形式硬编码,如果要更改必须改源代码后重新编译内核
    在内核启动时建立静态映射表,到内核关机时销毁,中间一直有效
    对于移植好的内核,你用不用他都在那里
(4)动态映射方法的特点:
    驱动程序根据需要随时动态的建立映射、使用、销毁映射
    映射是短期临时的
13.4、如何选择虚拟地址映射方法
(1)2种映射并不排他,可以同时使用
(2)静态映射类似于C语言中全局变量,动态方式类似于C语言中malloc堆内存
(3)静态映射的好处是执行效率高,坏处是始终占用虚拟地址空间;动态映射的好处是按需使用虚拟地址空间,坏处是每次使用前后都需要代码去建立映射&销毁映射(还得学会使用那些内核函数的使用)


14.静态映射操作LED1

14.1、关于静态映射要说的
(1)不同版本内核中静态映射表位置、文件名可能不同
(2)不同SoC的静态映射表位置、文件名可能不同
(3)所谓映射表其实就是头文件中的宏定义


14.2、三星版本内核中的静态映射表
(1)主映射表位于:arch/arm/plat-s5p/include/plat/map-s5p.h
CPU在安排寄存器地址时不是随意乱序分布的,而是按照模块去区分的。每一个模块内部的很多个寄存器的地址是连续的。所以内核在定义寄存器地址时都是先找到基地址,然后再用基地址+偏移量来寻找具体的一个寄存器。
map-s5p.h中定义的就是要用到的几个模块的寄存器基地址。
map-s5p.h中定义的是模块的寄存器基地址的虚拟地址。


(2)虚拟地址基地址定义在:arch/arm/plat-samsung/include/plat/map-base.h
#define S3C_ADDR_BASE    (0xFD000000)        // 三星移植时确定的静态映射表的基地址,表中的所有虚拟地址都是以这个地址+偏移量来指定的


(3)GPIO相关的主映射表位于:arch/arm/mach-s5pv210/include/mach/regs-gpio.h
表中是GPIO的各个端口的基地址的定义


(4)GPIO的具体寄存器定义位于:arch/arm/mach-s5pv210/include/mach/gpio-bank.h


15.静态映射操作LED2

15.1、参考裸机中的操作方法添加LED操作代码
(1)宏定义
(2)在init和exit函数中分别点亮和熄灭LED
15.2、实践测试
(1)insmod和rmmod时观察LED亮灭变化
(2)打印出寄存器的值和静态映射表中的分析相对比
15.3、将代码移动到open和close函数中去


16.静态映射操作LED3

16.1、添加驱动中的写函数
(1)先定义好应用和驱动之间的控制接口,这个是由自己来定义的。譬如定义为:应用向驱动写"on"则驱动让LED亮,应用向驱动写"off",驱动就让LED灭
(2)应用和驱动的接口定义做的尽量简单,譬如用1个字目来表示。譬如定义为:应用写"1"表示灯亮,写"0"表示让灯灭。
16.2、写应用来测试写函数
16.3、驱动和应用中来添加读功能


17.动态映射操作LED

17.1、如何建立动态映射


(1)request_mem_region,向内核申请(报告)需要映射的内存资源。
(2)ioremap,真正用来实现映射,传给他物理地址他给你映射返回一个虚拟地址


17.2、如何销毁动态映射
(1)iounmap
(2)release_mem_region
注意:映射建立时,是要先申请再映射;然后使用;使用完要解除映射时要先解除映射再释放申请。


17.3、代码实践
(1)2个寄存器分开独立映射
(2)2个寄存器在一起映射


 

猜你喜欢

转载自blog.csdn.net/qq_40732350/article/details/82918531
今日推荐