1.开启驱动开发之路
1.1、驱动开发的准备工作
(1)正常运行linux系统的开发板。要求开发板中的linux的zImage必须是自己编译的,不能是别人编译的。
(2)内核源码树,其实就是一个经过了配置编译之后的内核源码。
(3)nfs挂载的rootfs,主机ubuntu中必须搭建一个nfs服务器。
1.2、驱动开发的步骤
(1)驱动源码编写、Makefile编写、编译
(2)insmod装载模块、测试、rmmod卸载模块
2.最简单的模块源码分析
2.1、常用的模块操作命令
(1)lsmod(list module,将模块列表显示),功能是打印出当前内核中已经安装的模块列表,就是读取/proc/modules
(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);
模块参数和模块之间通信 ########
1 模块参数
我们可以用“module_param(参数名,参数类型,参数读/写权限) ”为模块定义一个参数,例如下列代码定义了1个整型
参数和1个字符指针参数:
static char *book_name = "dissecting Linux Device Driver";
module_param(book_name, char, S_IRUGO);
static int book_num = 4000;
module_param(book_num, int, S_IRUGO);
在装载内核模块时,用户可以向模块传递参数,形式为“insmode(或modprobe)模块名 参数名=参数值”,如果不传
递,参数将使用模块内定义的缺省值。
如果模块被内置,就无法insmod了,但是bootloader可以通过在bootargs里设置“模块名.参数名=值”的形式给该内置的模块传递参数
如:insmod book.ko book_name='GoodBook' book_num=5000
但是参数不能是浮点型,prink也不能传浮点型
2 模块的文件格式ELF
# file module_test.ko
module_test.ko: ELF 32-bit LSB relocatable, ARM, EABI5 version 1 (SYSV), BuildID[sha1]=f7537a581ad48be373e793407416a544e45a97f4, not stripped
3 模块之间的通信
模块是为了完成某种特定任务而设计的。其功能比较单一,为了丰富系统的功能,所以模块之间常常进行通信。它们之间可以共享变量、数据结构,也可以调用对方提供的功·能函数。
模块2的加载过程如下:
(1)使用insmod模块2.ko加载模块2。
(2)内核为模块2分配空间,然后将模块的代码和数据装入分配内存中。
(3)内核发现符号表中有函数1,函数2可以导出,于是将其内存地址记录在内核符号表中。
模块1在加载进内核时,系统会执行以下操作:
(1) insmod命令会为模块分配空间,然后将模块的代码和数据装入内存中。
(2)内核在模块1的符号表(symtab)中发现一些未解析的函数。
图中这些未解析的函数是“函数1”、“函数2”,这些函数位于模块2的代码中。所以模块1会通过内核符号表,查到相应的函数,并将函数地址填到模块1的符号表中。
通过模块1加载的过程后,模块1就可以使用模块2提供的“函数1”和“函数2”了。
详细参考:linux驱动开发入门与实战PDF(典藏大典)
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 | tail 命令可以得到/var/log/messages最后几行
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
实例代码:
static char *book_name = "dissecting Linux Device Driver";
module_param(book_name, char, 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 <baohua@kernel.org>");
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)printf是C库函数,是在应用层编程中使用的,不能在linux内核源代码中使用;
(2)printk是linux内核源代码中自己封装出来的一个打印函数,是内核源码中的一个普通函数,只能在内核源码范围内使用,不能在应用编程中使用。
(3)printk相比printf来说还多了个:打印级别的设置。
(4)操作系统的命令行中也有一个打印信息级别属性,值为0-7。当前操作系统中执行printk的时候会去对比printk中的打印级别和我的命令行中设置的打印级别,小于我的命令行设置级别的信息会被放行打印出来,大于的就被拦截的。譬如我的ubuntu中的打印级别默认是4,那么printk中设置的级别比4小的就能打印出来,比4大的就不能打印出来。
1、查看当前控制台的打印级别
cat /proc/sys/kernel/printk
4 4 1 7
其中第一个“4”表示内核打印函数printk的打印级别,只有级别比他高的信息才能在控制台上打印出来,既 0-3级别的信息2、修改打印
echo "新的打印级别 4 1 7" >/proc/sys/kernel/printk3、不够打印级别的信息会被写到日志中可通过dmesg 命令来查看
4、printk的打印级别
(5)ubuntu中这个printk的打印级别控制没法实践,ubuntu中不管你把级别怎么设置都不能直接打印出来,必须dmesg命令去查看。
return 的输出宏定义 如:return -EINVAL;
/****** \include\asm-generic\errno-base.h ********/
/* Operation not permitted */
/* No such file or directory */
/* No such process */
/* Interrupted system call */
/* I/O error */
/* No such device or address */
/* Argument list too long */
/* Exec format error */
/* Bad file number */
/* No child processes */
/* Try again */
/* Out of memory */
/* Permission denied */
/* Bad address */
/* Block device required */
/* Device or resource busy */
/* File exists */
/* Cross-device link */
/* No such device */
/* Not a directory */
/* Is a directory */
/* Invalid argument */
/* File table overflow */
/* Too many open files */
/* Not a typewriter */
/* Text file busy */
/* File too large */
/* No space left on device */
/* Illegal seek */
/* Read-only file system */
/* Too many links */
/* Broken pipe */
/* Math argument out of domain of func */
/* Math result not representable */
4.2、关于驱动模块中的头文件
(1)驱动源代码中包含的头文件和原来应用编程程序中包含的头文件不是一回事。应用编程中包含的头文件是应用层的头文件,是应用程序的编译器带来的(譬如gcc的头文件路径在 /usr/include下,这些东西是和操作系统无关的)。驱动源码属于内核源码的一部分,驱动源码中的头文件其实就是内核源代码目录下的include目录下的头文件。
4.3、驱动编译的Makefile分析
ifeq ($(KERNELRELEASE) ,)
KERNELDIR ?= /1inux-2.6.29.4/1inux-2.6.29.4
PWD := $(shel1 pwd)
modules:
$(MAKE) -C $(KERNELDIR) M=S (PWD) modules
modules_instal1:
$(MAKE) -C $(KERNELDIR) M=S (PWD) modules_install
clean:
rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions
else
obj-m := hel1o.o
endif
(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)的概念
8.字符设备驱动代码实践
开始动手
1.
(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,
};
2.
(1)主设备号的选择
(2)返回值的检测
3.
(1)编译等 make && make cp
(2)insmod并且 cat /proc/devices 查看设备注册的现象
(3)rmmod并且 cat /proc/devices 查看设备注销的现象
问:怎样让内核自动分配主设备号
(1)为什么要让内核自动分配
自己分配麻烦,可移植性差
(2)如何实现?
系统自动设备号分配:alloc_chrdev_region()
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
const char *name)
(3)测试
static dev_t dev_from;
int retval = -1;
retval = alloc_chrdev_region(&dev_from, 0, MYCOUNT, MYNAME);
9.应用程序如何调用驱动
9.1、驱动设备文件的创建
(1)何为设备文件
通过设备文件可以进行读写操作,当然前提是file_operations 中有相应的操作。
(2)设备文件的关键信息是:设备号 = 主设备号 + 次设备号,使用ls -l去查看设备文件,就可以得到这个设备文件对应的主次设备号。
(3)使用mknod创建设备文件:mknod /dev/xxx c 主设备号 次设备号
9.2、写应用来测试驱动
(1)还是原来的应用
(2)open、write、read、close等
(3)实验现象预测和验证
9.3、总结
(1)整体流程梳理、注意分层
(2)后续工作:添加读写接口
应用的实例代码:
//我们自己创建的设备文件
char read_buf[100] = {0};
char write_buf[100] = {0};
int
main(void)
{
int fd = -1;
//open file
fd = open(TEST_FILE, O_RDWR);
if(fd < 0){
printf("open error \n");
}else{
printf("open sucess \n");
}
for (;;)
{
printf("请输入 \n");
scanf("%s", write_buf);
if(!strcmp(write_buf, "on")){
write(fd, "1", 1);
}else if(!strcmp(write_buf, "off")){
write(fd, "0", 1);
}else if(!strcmp(write_buf, "quit")){
break;
}
memset(write_buf, 0, sizeof(write_buf));
}
//close file
close(fd);
return 0;
}
10.应用和驱动之间的数据交换
(1)copy_from_user,用来将数据从用户空间复制到内核空间
(2)copy_to_user
注意:复制是和mmap的映射相对应去区分的
从用户空间拷贝数据到内核空间,失败返回没有被拷贝的字节数,成功返回0.
这么简单的一个函数却含盖了许多关于内核方面的知识,比如内核关于异常出错的处理.从用户空间拷贝数据到内核中时必须很小心,假如用户空间的数据地址是个非法的地址,或是超出用户空间的范围,或是那些地址还没有被映射到,都可能对内核产生很大的影响,如oops,或被造成系统安全的影响.所以copy_from_user函数的功能就不只是从用户空间拷贝数据那样简单了,他还要做一些指针检查连同处理这些
问题的方法.
copy_from_user主要是这个函数提供了两个功能:
1. 对用户进程传过来的地址范围进行合法性检查;
2.当用户传来的地址没有分配物理地址时,定义了缺页处理后的异常发生地址,保证程序顺利执行;
另外一个问题,直接使用memcpy时为什么没有出现异常?
在上面的分析过程中我们知道,只有用户传来的地址空间没有分配对应的物理地址时才会进行修复,
如果用户进程之前已经使用过这段空间,代表已经分配了物理地址,自然不会发生缺页异常。
返回值:和常规有点不同
成功复制则返回0
不成功返回尚未成功复制剩下的字节数
11.驱动中如何操控硬件
裸机和驱动的操作硬件的方式不同
(1)寄存器地址不同。
原来是直接用物理地址,现在需要用该物理地址在内核虚拟地址空间相对应的虚拟地址。寄存器的物理地址是CPU设计时决定的,从datasheet中查找到的。
(2)编程方法不同。
裸机中习惯直接用函数指针操作寄存器地址,而kernel中习惯用封装好的io读写函数来操作寄存器,以实现最大程度可移植性。
内核的虚拟地址映射方法
虚拟地址映射方法:动态和静态
1.静态映射方法的特点:
内核移植时以代码的形式硬编码,如果要更改必须改源代码后重新编译内核
在内核启动时建立静态映射表,到内核关机时销毁,中间一直有效
对于移植好的内核,你用不用他都在那里
2.动态映射方法的特点:
驱动程序根据需要随时动态的建立映射、使用、销毁映射
映射是短期临时的
如何选择虚拟地址映射方法
(1)2种映射并不排他,可以同时使用
(2)静态映射类似于C语言中全局变量,动态方式类似于C语言中malloc堆内存
(3)静态映射的好处是执行效率高,坏处是始终占用虚拟地址空间;动态映射的好处是按需使用虚拟地址空间,坏处是每次使用前后都需要代码去建立映射&销毁映射(还得学会使用那些内核函数的使用)
静态映射要依靠映射表
动态映射要依靠动态分配内存
1.静态映射操作LED
1、关于静态映射要说的
(1)不同版本内核中静态映射表位置、文件名可能不同
(2)不同SoC的静态映射表位置、文件名可能不同
(3)所谓映射表其实就是头文件中的宏定义
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
3、添加驱动中的写函数
(1)先定义好应用和驱动之间的控制接口,这个是由自己来定义的。譬如定义为:应用向驱动写"on"则驱动让LED亮,应用向驱动写"off",驱动就让LED灭
(2)应用和驱动的接口定义做的尽量简单,譬如用1个字目来表示。譬如定义为:应用写"1"表示灯亮,写"0"表示让灯灭。
2.动态映射操作LED
1、如何建立动态映射
(1)request_mem_region,向内核申请(报告)需要映射的内存资源。
(2)ioremap,真正用来实现映射,传给他物理地址他给你映射返回一个虚拟地址
2、如何销毁动态映射
(1)iounmap
(2)release_mem_region
注意:映射建立时,是要先申请再映射;然后使用;使用完要解除映射时要先解除映射再释放申请。
3、代码实践
(1)2个寄存器分开独立映射
(2)2个寄存器在一起映射
//真实物理地址(通过芯片手册得到)
unsigned int *pGPJ0CON = NULL;
unsigned int *pGPJ0DAT = NULL;
//动态分配虚拟空间
if (!request_mem_region(FB_GPJ0_BASE + FB_GPJ0_CON, 4, "GPJ0CON")) //为物理地址分配虚拟地址
{
printk(KERN_ERR "request_mem_region error\n");
goto err_mem_region_con;
}
if (!request_mem_region(FB_GPJ0_BASE + FB_GPJ0_DAT, 4, "GPJ0DAT"))
{
printk(KERN_ERR "request_mem_region error\n");
goto err_mem_region_dat;
}
pGPJ0CON = ioremap(FB_GPJ0_BASE + FB_GPJ0_CON, 4); //获取分配的虚拟地址
pGPJ0DAT = ioremap(FB_GPJ0_BASE + FB_GPJ0_DAT, 4);
*(pGPJ0CON) = 0x11111111;
*(pGPJ0DAT) = ((0<<3) | (0<<4) | (0<<5));