(1)正常运行linux系统的开发板。要求开发板中的linux的zImage必须是自己编译的,不能是别人编译的。因为需要一个内核源码树。
(2)内核源码树,其实就是一个经过了配置编译之后的内核源码。编译驱动时,需要用到内核源码树。内核源代码树 就是内核源代码。 编译模块,需要内核源码。
(3)nfs挂载的rootfs,主机ubuntu中必须搭建一个nfs服务器。
1.nfs方式启动,挂载根文件系统;
2.将根文件系统烧录进开发板,正常启动,启动后通过mount命令挂载;
(1)驱动源码编写、Makefile编写(有着固定的模式,可以参考已有的编写)、编译
(2)insmod装载模块m动态安装驱动)、测试(完整的驱动是为了被应用程序调用,驱动封装成API函数,被应用程序调用来工作)、rmmod卸载模块
(1)copy开发板的内核源码,找一个干净的目录(比如/root/driver),解压之,并且配置编译。编译完成后得到了:
<1>内核源码树(/root/driver/kernel)
<2>编译好的zImage
编译步骤1.修改makefile,指定架构和交叉编译链目录
编译步骤2.make x210ii_qt_defconfig(记不住,报错会有提示make的目标在那个目录)
(2)使用fastboot工具将第1步中得到的zImage烧录到开发板中去启动(或者将zImage丢到tftp的共享目录,uboot启动时tftp下载启动),将来驱动编译好后,就可以在这个内核中去测试。因为这个zImage和内核源码树是一伙的,所以驱动安装时版本校验不会出错(否则有可能会出错)。
编译驱动文件最后生成的需要的是.ko文件(kernel object),驱动文件。
//module_test.c
#include // module_init module_exit
#include // __init __exit
// 模块安装函数
static int __init chrdev_init(void)
{
printk(KERN_INFO "chrdev_init helloworld init\n");
//printk("<7>" "chrdev_init helloworld init\n");
//printk("<7> chrdev_init helloworld init\n");
return 0;
}
// 模块卸载函数
static void __exit chrdev_exit(void)
{
printk(KERN_INFO "chrdev_exit helloworld exit\n");
}
module_init(chrdev_init);
module_exit(chrdev_exit);
// MODULE_xxx这种宏作用是用来添加模块描述信息
MODULE_LICENSE("GPL"); // 描述模块的许可证
MODULE_AUTHOR("aston"); // 描述模块的作者
MODULE_DESCRIPTION("module test"); // 描述模块的介绍信息
MODULE_ALIAS("alias xxx"); // 描述模块的别名信息
(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:加载或卸载内核模块,需要根据modules.dep.bin文件进行加载操作,可以自动解决模块间的依赖关系表
(6)depmod:查找/lib/moduels/(uname -r)/中的所有模块并建立modules.dep.bin文件,该文件记录了模块位置及依赖关系.
(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帮我们在内部做了记录),但是我们就不用管了。
(1)使用modinfo查看模块的版本信息
(2)内核zImage中也有一个确定的版本信息
(3)insmod时模块的vermagic必须和内核的相同,否则不能安装,报错信息为:insmod: ERROR: could not insert module module_test.ko: Invalid module format
(4)模块的版本信息是为了保证模块和内核的兼容性,是一种安全措施
(5)如何保证模块的vermagic和内核的vermagic一致?
编译模块的内核源码树就是我们编译正在运行的这个内核的那个内核源码树即可。说白了就是模块和内核要同出一门。
static int _init chrdev_init(void);
static void _exit chrdev_exit(void);
module_init(chrdev_init);
module_exit(chrdev_exit);
在编写驱动模块的时候有两个东西经常被使用也必须被使用,分别是module_init和module_exit,这两个分别在加载和卸载驱动时被调用,即调用insmod和rmmod命令的时候,但是insmod和rmmod不能识别这两个,它识别init_module和cleanup_module,其实init_module和cleanup_module相当于是module_init和module_exit的别名
详解:
https://blog.csdn.net/u013216061/article/details/72511653
(1)module_exit和rmmod的对应关系
(2)lsmod查看rmmod前后系统的模块记录变化
(1)MODULE_LICENSE,模块的许可证。一般声明为GPL许可证,而且最好不要少,否则可能会出现莫名其妙的错误(譬如一些明显存在的函数提升找不到)。
(2)MODULE_AUTHOR,描述模块的作者
(3)MODULE_DESCRIPTION,描述模块的介绍信息
(4)MODULE_ALIAS,模块的别名
(1)__init,本质上是个宏定义,在内核源代码中就有#define __init xxxx。gcc扩展语法,这个__init的作用就是将被他修饰的函数放入.init.text段中去(本来默认情况下函数是被放入.text段中)。
整个内核中的所有的这类函数都会被链接器链接放入.init.text段中,所以所有的内核模块的__init修饰的函数其实是被统一放在一起的。内核启动时统一会加载.init.text段中的这些模块安装函数,加载完后就会把这个段给释放掉以节省内存。
(2)__exit
作用类似于__init段中,将代码放到一个.exit段中;
(3)static
该关键字使得修饰函数只在本文件可用;修改了该函数的链接属性
(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大的就不能打印出来。
查看方法:cat /proc/sys/kernel/printk
修改方法:echo 4 > /proc/sys/kernel/printk
(5)ubuntu中这个printk的打印级别控制没法实践,ubuntu中不管你把级别怎么设置都不能直接打印出来,必须dmesg命令去查看。
(1)驱动源代码中包含的头文件和原来应用编程程序中包含的头文件不是一回事。
应用编程中包含的头文件是应用层的头文件,是应用程序的编译器带来的(譬如gcc的头文件路径在 /usr/include下,这些东西是和操作系统无关的)。驱动源码属于内核源码的一部分,驱动源码中的头文件其实就是内核源代码目录下的include目录下的头文件。
包含这个目录下的头文件格式为:
#include
#Makefile
#第一部分
#ubuntu的内核源码树,如果要编译在ubuntu中安装的模块就打开这2个
#KERN_VER = $(shell uname -r)
#KERN_DIR = /lib/modules/$(KERN_VER)/build
# 开发板的linux内核的源码树目录
KERN_DIR = /root/driver/kernel
#第二部分
obj-m += module_test.o
#第三、四部分
all:
make -C $(KERN_DIR) M=`pwd` modules
#
# KERN_DIR表示内核源码目录,这种方式适用于嵌入式开发的交叉编译,KERN_DIR目录中包含了内核驱动模块所需要的各##种头文件及依赖。
# -C表示 指定进入指定的目录即KERN_DIR,是内核源代码目录,调用该目录顶层下的Makefile,目标为modules。
# M=$(shell pwd )选项让该Makefile在构造modules目标之前返回到模块源代码目录并在当前目录生成obj-m指定的xxx.o目
#标模块。
#
cp:
cp *.ko /root/porting_x210/rootfs/rootfs/driver_test
.PHONY: clean
clean:
make -C $(KERN_DIR) M=`pwd` modules clean
(1)KERN_DIR,变量的值就是我们用来编译这个模块的 内核源码树 的目录
(2)obj-m += module_test.o,这一行就表示我们要将module_test.c文件编译成一个模块(-m表示模块,-y表示要编译链接进zImage中)
(3)make -C $(KERN_DIR) M=pwd
modules 这个命令用来实际编译模块,工作原理就是:利用make -C进入到我们指定的内核源码树目录下,然后在源码目录树下借用内核源码中定义的模块编译规则去编译这个模块,编译完成后把生成的文件拷贝到当前目录下,完成编译。
(4)make clean ,用来清除编译痕迹
总结:模块的makefile非常简单,本身并不能完成模块的编译,而是通过make -C进入到内核源码树下(再make modules)借用内核源码的体系来完成模块的编译链接的。这个Makefile本身是非常模式化的,3和4部分是永远不用动的,只有1和2需要动。1是内核源码树的目录,你必须根据自己的编译环境设置;2是要生成的.o文件。
1、设置uboot的bootcmd使开发板通过tftp下载自己建立的内核源码树编译得到的zImage
set bootcmd tftp 0x30008000 zImage;bootm 0x30008000
2、设置uboot的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
根据自己的文件目录进行修改。
3、修改Makefile中的KERN_DIR使其指向自己建立的内核源码树
4、将自己编译好的驱动.ko文件放入nfs共享目录下去
5、开发板启动后使用insmod、rmmod、lsmod等去进行模块实验
若使用modinfo失败,百度搜索解决方法:https://blog.csdn.net/weixin_39888281/article/details/93136861
尝试修改printk打印级别,使得程序的打印信息可以在调用命令(本质上是调用执行程序module_test.c中的函数)时打印出来。
模块是一种机制。驱动使用了这种机制。
(1)应用层->API->设备驱动->硬件
(2)API:open、read、write、close等
(3)驱动源码中提供真正的open、read、write、close等函数实体
结构体file_operations在头文件linux/fs.h中定义,用来存储驱动模块提供的对设备进行各种操作的函数的指针。该结构体的每个元素都对应着驱动模块用来处理某个被请求的 事务的函数的地址。
详解:https://www.cnblogs.com/sunyubo/archive/2010/12/22/2282079.html
http://www.voidcn.com/article/p-vtmfwdfb-dm.html
(1)元素主要是函数指针,用来挂接实体函数(真正做事的函数)地址
(2)每个设备驱动都需要一个该结构体类型的变量
(3)设备驱动向内核注册时提供该结构体类型的变量
(1)为何要注册驱动
否则应用程序调用无法找到驱动
(2)谁去负责注册
驱动本身
(3)向谁注册
向内核注册
(4)注册函数(register_chrdev)从哪里来
从内核来
(5)注册前怎样?注册后怎样?注册产生什么结果?
内核会向其发放一个证明(例如数字编号),使得应用程序可以找到并调用驱动
static inline int register_chrdev(unsigned int major, const char *name,
const struct file_operations *fops)
{
return __reg ister_chrdev(major, 0, 256, name, fops);
}
(1)作用,驱动向内核注册自己的file_operations
(2)参数
major:用于动态分配的主要设备号,参数major如果等于0,则表示采用系统动态分配的主设备号;不为0,则表示静态注册。
name:这一系列设备的名称
fops:与此设备相关联的文件操作
(3)inline和static
inline修饰符,表示为内联函数。inline只适合涵数体内代码简单的函数数使用,不能包含复杂的结构控制语句例如while、switch,并且内联函数本身不能是直接递归函数(自己内部还调用自己的函数)。
内联是以代码膨胀(复制)为代价,仅仅省去了函数调用的开销,从而提高函数的执行效率。如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。
将内联函数放在头文件里实现是合适的,省却你为每个文件实现一次的麻烦.而所以声明跟定义要一致,其实是指,如果在每个文件里都实现一次该内联函数的话,那么,最好保证每个定义都是一样的,否则,将会引起未定义的行为,即如果不是每个文件里的定义都一样,那么,编译器展开的是哪一个,那要看具体的编译器而定.所以,最好将内联函数定义放在头文件中.
静态局部变量使用static修饰符定义,即使在声明时未赋初值,编译器也会把它初始化为0。且静态局部变量存储于进程的全局数据区,即使函数返回,它的值也会保持不变。变量在全局数据区分配内存空间;编译器自动对其初始化其作用域为局部作用域,当定义它的函数结束时,其作用域随之结束。
(1)内核中有一个数组(结构体数组,同时最多装载255个,但不可能255个字符设备驱动同时工作,一般连十个都不会超过)用来存储注册的字符设备驱动
(2)register_chrdev内部将我们要注册的驱动的信息(主要是 )存储在数组中相应的位置
(3)cat /proc/devices查看内核中已经注册过的字符设备驱动(和块设备驱动)
(4)好好理解主设备号(major)的概念
多重含义,设备的编号,设备数组的下标(应该减一,从一开始计数的),多个设备的主设备号相同,表明几者之间是有关联的
其内的文件是内核用数据结构虚拟出来的.Proc文件系统是一个伪文件系统,做为一个特殊接口来访问内核,常常挂载在/proc下,里面的大多数文件时只读的,但是我们仍然可以设置其中一些变量来改变内核设置。
详解: https://blog.csdn.net/acs713/article/details/78887800
https://blog.csdn.net/qq_42014600/article/details/90301888
/proc文件系统提供了一个基于文件的Linux 内部接口。它可以用于确定系统的各种不同设备和进程的状态。
/proc文件系统是一种特殊的、由软件创建的文件系统,内核使用它向外界导出信息,/proc系统只存在内存当中,而不占用磁盘空间。
/proc下面的每个文件都绑定于一个内核函数,用户读取文件时,该函数动态地生成文件的内容。也可以通过写/proc文件修改内核参数
(1)Linux 驱动常见错误返回值:https://blog.csdn.net/encourage2011/article/details/53680056
(2)module license 'unspecified' taints kernel
解决方法:https://blog.csdn.net/qq_37600027/article/details/100797451
(3)unregister_chrdev() -- 老版本字符设备注销函数
功能:注销设备
static inline void unregister_chrdev(unsigned int major, const char *name)
{
__unregister_chrdev(major, 0, 256, name);
}
说明:
注销设备驱动程序的内核函数
变量:
major 主设备号
name 设备文件
(1)目的:给空模块添加驱动壳子
(2)核心工作量:file_operations及其元素填充、注册驱动
(1)脑海里先有框架,知道自己要干嘛
(2)细节代码不需要一个字一个字敲,可以到内核中去寻找参考代码复制过来改
(3)写下的所有代码必须心里清楚明白,不能似懂非懂
(1)先定义file_operations结构体变量
Linux 内核驱动中的指定初始化详解:https://zhuanlan.zhihu.com/p/55768099
eg:
跟数组类似,在标准 C 中,结构体变量的初始化也要按照固定的顺序。在 GNU C 中我们也可以通过结构域来初始化指定某个成员。
struct student{
char name[20];
int age;
};
int main(void)
{
struct student stu1={ "wit",20 };
printf("%s:%d\n",stu1.name,stu1.age);
struct student stu2=
{
.name = "wanglitao",
.age = 28
};
printf("%s:%d\n",stu2.name,stu2.age);
return 0;
}
在程序中,我们定义一个结构体类型 student,然后分别定义两个结构体变量 stu1 和 stu2。初始化 stu1 时,我们采用标准 C 的初始化方式,即按照固定顺序直接初始化。初始化 stu2 时,我们采用 GNU C 的初始化方式,通过结构域名 .name 和 .age,我们就可以给结构体变量的某一个指定成员直接赋值。非常方便。
在 Linux 内核驱动中,大量使用GNUC的这种指定初始化方式,通过结构体成员来初始化结构体变量。比如在字符驱动程序中,我们经常见到这样的初
始化:
static const struct file_operations ab3100_otp_operations = {
.open = ab3100_otp_open,
.read = seq_read,
.llseek = seq_lseek,
.release = single_release,
};
在驱动程序中,我们经常使用 file_operations 这个结构体变量来注册我们开发的驱动,然后以回调的方式来执行我们驱动实现的相关功能。结构体 file_operations 在 Linux 内核中的定义如下:
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 (*read_iter) (struct kiocb *, struct iov_iter *);
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
int (*iterate) (struct file *, struct dir_context *);
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 *);//对应close函数
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 **, void **);
long (*fallocate)(struct file *file, int mode, loff_t offset,
loff_t len);
void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
unsigned (*mmap_capabilities)(struct file *);
#endif
};
结构体 file_operations 里面定义了很多结构体成员,而在这个驱动中,我们只初始化了部分成员变量,通过访问结构体的成员来指定初始化,非常方便。
(2)根据file_operations结构体使得open和close函数原型确定、内容填充
(1)主设备号的选择
通过cat /proc/devices查看空闲的设备号
(2)返回值的检测
(1)编译等 make && make cp
(2)insmod并且查看设备注册的现象
(3)rmmod并且查看设备注销的现象
(1)为什么要让内核自动分配
当你使用一个设备号时,可能在你这里没有被占用,当你这个代码移植到另一个平台上这个设备号被占用了,导致程序无法执行,失败。
(2)如何实现?
通过给注册设备驱动的函数传不同的参数实现,传参数为0,即可实现动态自动分配设备号
(3)测试
udev 是Linux kernel 2.6系列的设备管理器。它主要的功能是管理/dev目录底下的设备节点。它同时也用来接替devfs及热插拔的功能,这意味着它要在添加/删除硬件时处理/dev目录以及所有用户空间的行为,包括加载固件及Linux 2. 6.13内核。要想使用udev的最新版本依赖于升级后的的uevent接口是否是最新版本。使用新版本udev的系统版本必须高于2.6.13,除非使用noudev参数来禁用udev并使用传统的/dev来进行设备读取。
详解:http://www.jinbuguo.com/kernel/device_files.html
https://www.jianshu.com/p/579a7b715aab
(1)何为设备文件
设备文件通常提供与标准设备(如打印机和串行端口)的简单接口,但也可用于访问这些设备(如磁盘分区)上的特定独特资源。此外,设备文件对于访问与任何实际设备(如数据接收器和随机数生成器)无关的系统资源非常有用。
(2)设备文件的关键信息是:**设备号 = 主设备号 + 次设备号,**使用ls -l去查看设备文件,就可以得到这个设备文件对应的主次设备号。
主设备号相同的设备是同类设备,使用同一个驱动程序(虽然目前的内核允许多个驱动共享一个主设备号,但绝大多数设备依然遵循一个驱动对应一个主设备号的原则)。可以通过 cat /proc/devices 命令查看当前已经加载的设备驱动程序的主设备号。
在/dev目录下除了各种设备节点之外还通常还会存在:FIFO管道、Socket、软/硬连接、目录。这些东西并不是设备文件,因此也就没有主/次设备号。
(3)使用mknod创建设备文件:mknod /dev/xxx c 主设备号 次设备号
xxx自己起名,主设备号和次设备号是自己写的驱动程序分配的,次设备号默认为0,设备文件vi打开是看不到什么信息的,要通过open、write、read、close等操作
(1)open、write、read、close等
(2)借助实验现象预测和验证
(1)在驱动中添加
(2)在应用中添加
(3)测试
(4)应用和驱动之间的数据交换
<1>copy_from_user,用来将数据从用户空间复制到内核空间
<2>copy_to_user
注意:复制是和mmap的映射相对应去区分的
在学习驱动时,对于用到的很多函数可以在sourceinsight这个软件中搜索查看内核相应的代码。
这是我在内核的一个驱动文件中找到的一个函数:
static inline long copy_from_user(void *to,
const void __user * from, unsigned long n)
{
might_sleep();
if (access_ok(VERIFY_READ, from, n))
return __copy_from_user(to, from, n);
else
return n;
}
static inline long copy_to_user(void __user *to,
const void *from, unsigned long n)
{
might_sleep();
if (access_ok(VERIFY_WRITE, to, n))
return __copy_to_user(to, from, n);
else
return n;
}
(1)copy_from_user函数的返回值定义,和常规有点不同。返回值如果成功复制则返回0,如果 不成功复制则返回尚未成功复制剩下的字节数。
如何包含这两个函数的头文件,可参照内核源代码,从调用他们的函数文件中找到定义函数的头文件,在知晓头文件名后,去调用它们的文件中查看函数如何包含,有时采用了间接包含,所以有时候前面的方法不一定可用,需要参考调用该函数的文件,找寻尝试包含头文件
(1)硬件物理原理不变
(2)硬件操作接口(寄存器)不变
(3)硬件操作代码不变
(1)寄存器地址不同。原来是直接用物理地址,现在需要用该物理地址在内核虚拟地址空间相对应的虚拟地址。寄存器的物理地址是CPU设计时决定的,从datasheet中查找到的。
(2)编程方法不同。裸机中习惯直接用指针操作寄存器地址,而kernel中习惯用封装好的io读写函数来操作寄存器,以实现最大程度可移植性。
(1)为什么需要虚拟地址映射
可以提高效率,比如LCD显示,将显示的内容存储空间和显示使用的空间(显存)映射到同一个内存空间,可以提高效率
(2)内核中有2套虚拟地址映射方法:动态和静态
(3)静态映射方法的特点:
内核移植时以代码的形式硬编码,如果要更改必须改源代码后重新编译内核在内核启动时建立静态映射表,到内核关机时销毁,中间一直有效,对于移植好的内核,你用不用他都在那里
(4)动态映射方法的特点:驱动程序根据需要随时动态的建立映射、使用、销毁映射, 映射是短期临时的.
(1)2种映射并不排他,可以同时使用
(2)静态映射类似于C语言中全局变量,动态方式类似于C语言中malloc堆内存
(3)静态映射的好处是执行效率高,坏处是始终占用虚拟地址空间(直到内核关机销毁);动态映射的好处是按需使用虚拟地址空间,坏处是每次使用前后都需要代码去建立映射&销毁映射(还得学会使用那些内核函数的使用)
两个物理地址不可以同时对应一个虚拟地址,否则mmu映射时不知道这个虚拟地址对应的那个物理地址,反之可以。
(1)不同版本内核中静态映射表位置、文件名可能不同
(2)不同SoC的静态映射表位置、文件名可能不同
(3)所谓映射表其实就是头文件中的宏定义
(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
/*
*用C语言点灯
*作者:Mr.Zhang
*/
#define rGPJ0CON *((unsigned int *)0xE0200240)
#define rGPJ0DAT *((unsigned int *)0xE0200244)
void delay(void);
void led_blink(void )
{
while(1)
{
rGPJ0CON = 0x11111111;//把GPJ0CON设置为输出模式
rGPJ0DAT = ((0<<3)|(0<<4)|(0<<5));//输出低电平,让三个灯亮
delay();//延时
rGPJ0DAT = ((1<<3)|(1<<4)|(1<<5));
//输出高电平,让三个灯灭
delay();//延时
}
}
void delay(void)
{
volatile unsigned int i =10000;
while(i--);
}
(1)宏定义
(2)在init和exit函数中分别点亮和熄灭LED
(1)insmod和rmmod时观察LED亮灭变化
(2)打印出寄存器的值和静态映射表中的分析相对比
要记得创建设备文件
(1)先定义好应用和驱动之间的控制接口,这个是由自己来定义的。譬如定义为:应用向驱动写"on"则驱动让LED亮,应用向驱动写"off",驱动就让LED灭
(2)应用和驱动的接口定义做的尽量简单,譬如用1个字目来表示。譬如定义为:应用写"1"表示灯亮,写"0"表示让灯灭。
驱动程序中使用memset函数和应用程序不同,包含的头文件也不同,应用层调用的是库函数,而库函数是内核提供的,无法在内核使用库函数,可以找到内核中声明实现memset函数的头文件,把该文件包含。可以通过查找该函数名称,找到内核中使用该函数的文件是如何包含这个头文件的,通过参考内核源码来得知如何包含该头文件
驱动程序一般只是用来操作硬件,而与用户需求有关的应用代码应该放在应用程序中。
上述代码如下所示:
#include //module_init module_exit
#include //__init __exit
#include
#include
#include
#include // arch/arm/mach-s5pv210/include/mach/gpio-bank.h
#include
#define MYMAJOR 200
#define MYNAME "testchar"
#define GPJ0CON S5PV210_GPJ0CON //内核中寄存器地址相关的宏定义,静态映射表
#define GPJ0DAT S5PV210_GPJ0DAT
#define rGPJ0CON *((volatile unsigned int *)GPJ0CON)
#define rGPJ0DAT *((volatile unsigned int *)GPJ0DAT)
int mymajor = 0;
char kbuf[100] = {0};//内核空间的buf
ssize_t test_chrdev_read(struct file *file, char __user *ubuf, size_t count, loff_t *ppos)
{
int ret = -1;
printk(KERN_INFO "this is test_chrdev_read.\n");
ret = copy_to_user(ubuf, kbuf, count);
if (ret)
{
printk(KERN_ERR "copy_to_user fail.\n");
return -EINVAL;
}
else
{
printk(KERN_INFO "copy_to_user successfully.\n");
}
return 0;
}
//写函数的本质就是,将应用层传递过来的数据先复制到内核中,然后以正确的
//方式写入硬件完成操作
ssize_t test_chrdev_write(struct file *file, const char __user *ubuf, size_t count, loff_t *ppos)
{
int ret = -1;
printk(KERN_INFO "this is test_chrdev_write.\n");
//使用该函数将应用层传过来的ubuf的内容拷贝到驱动空间中的一个buf中
//memcpy(kbuf, ubuf); //错误,这两个不在一个地址空间中,一个属于内核,一个属于应用
memset(kbuf, 0, sizeof(kbuf));
ret = copy_from_user(kbuf, ubuf, count);
if (ret)
{
printk(KERN_ERR "copy_from_user fail.\n");
return -EINVAL;
}
else
{
printk(KERN_INFO "copy_from_user successfully.\n");
printk(KERN_INFO "GPJ0CON = %p.\n", GPJ0CON);
printk(KERN_INFO "GPJ0DAT = %p.\n", GPJ0DAT);
}
//真正驱动中,数据从应用层复制到驱动中后, 我们要根据这个数据
//去操作硬件,所以下面应该就是操作硬件的代码
//方式1:
if (kbuf[0] == '0')//灯灭
{
rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));
}
else if (kbuf[0] == '1')//灯亮
{
rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));
}
//方式2:
if (!strcmp(kbuf, "on"))
{
rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));
}
else if (!strcmp(kbuf, "off"))
{
rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));
}
return 0;
}
int test_chrdev_open(struct inode *inode, struct file *file)
{
//该函数实现打开这个设备的硬件操作代码
printk(KERN_INFO "this is test_chrdev_open.\n");
rGPJ0CON = 0x11111111;
rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5)); // 亮
return 0;
}
int test_chrdev_release(struct inode *node, struct file *file)//对应close函数
{
printk(KERN_INFO "this is test_chrdev_release.\n");
rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5)); // 灭
return 0;
}
//定义一个file_operations结构体变量,并且去填充
//可在内核源代码搜索file_operations复制一个进行修改
/* File operations struct for character device */
static const struct file_operations test_fops = {
.owner = THIS_MODULE, // 惯例,直接写即可
.open = test_chrdev_open, //将来应用open打开这个设备时,实际调用的就是这个.open对应的函数
.write = test_chrdev_write,
.release = test_chrdev_release, //对应close函数
.read = test_chrdev_read,
};
//模块安装函数
static int __init chrdev_init(void)
{
printk(KERN_INFO "chrdev_init helloworld init\n");
//在module_init宏调用的函数中去注册字符设备驱动
//major 传0进去表示让内核帮我们自动分配一个合适的没被使用的主设备号
//内核如果成功分配就会返回分配的设备号:如果分配失败则会返回负数
mymajor = register_chrdev(0, MYNAME, &test_fops);
if (mymajor < 0)
{
printk(KERN_ERR "register_chrdev fail.\n");
return -EINVAL;
}
else
{
printk(KERN_ERR "register_chrdev successfully.\n");
printk(KERN_ERR "mymajor = %d.\n", mymajor);
}
return 0;
}
//模块卸载函数
static void __exit chrdev_exit(void)
{
printk(KERN_INFO "chrdev_exit hellowworld exit\n");
//在module_exit宏调用的函数中去注销字符设备驱动
unregister_chrdev(mymajor, MYNAME);//释放设备号
}
//这两个函数分别在加载和卸载驱动时被调用即调用insmod和rmmod命令的时候,
//但是insmod和rmmod不能识别这两个函数,它只能识别init_module和cleanup_module,
//其实init_module和cleanup_module相当于是module_init和module_exit的别名
module_init(chrdev_init);//宏定义,根据是否定义MODULE宏,宏展开不同的内容
module_exit(chrdev_exit);//宏定义,根据是否定义MODULE宏,宏展开不同的内容
//MODULE_XXX这种宏的作用用来添加模块描述信息
MODULE_LICENSE("GPL"); // 描述模块的许可证
MODULE_AUTHOR("aston"); // 描述模块的作者
MODULE_DESCRIPTION("module test"); // 描述模块的介绍信息
MODULE_ALIAS("alias xxx"); // 描述模块的别名信息
(1)request_mem_region,向内核申请(报告)需要映射的内存资源。避免出现混乱,例如多个驱动程序同时访问一个寄存器。
#define request_mem_region(start,n,name) __request_region(&iomem_resource, (start), (n), (name), 0)
对于 request_region, 三个参数 start,n,name 表示你想使用从 start 开始的 size 为 n 的 I/O port 资源 ,name 自然就是你的名字了 .
(2)ioremap,真正用来实现映射,传给他物理地址他给你映射返回一个虚拟地址
网上资料:
#include
static inline void __iomem *ioremap(phys_addr_t offset, unsigned long size);
参数:
offset:要映射的物理内存区的起始地址。
size:物理·地址的范围。
内核找到的:
#define ioremap(cookie,size) __arm_ioremap(cookie, size, MT_DEVICE)
void __iomem *__arm_ioremap(unsigned long phys_addr, size_t size,
unsigned int mtype)
{
return (void __iomem *)phys_addr;
}
这两个函数可去查看内核源代码,搞清楚实现的原理及过程。
(1)iounmap 解除映射
#include
#define iounmap(cookie) __iounmap(cookie)
参数:
cookie:虚拟地址的指针。
函数原型:
void __iounmap(volatile void __ iomem *io_addr);
参数:
io_addr:虚拟地址的指针。
(2)release_mem_region 释放申请
注意:映射建立时,是要先申请再映射;然后使用;使用完要解除映射时要先解除映射再释放申请。避免出现还没有解除映射又有别的程序来申请动态映射
头文件间接包含、直接包含,不知道头文件如何包含时,可参考内核源码。
(1)2个寄存器分开独立映射
(2)2个寄存器在一起映射映射一个 ,对于地址挨着的寄存器可通过以下的方式实现,* p,*(p+1)
#include //module_init module_exit
#include //__init __exit
#include
#include
#include
#include // arch/arm/mach-s5pv210/include/mach/gpio-bank.h
#include
#include
#include
#define MYMAJOR 200
#define MYNAME "testchar"
#define GPJ0CON_PA 0xe0200240
#define GPJ0DAT_PA 0xe0200244
int mymajor = 0;
unsigned int *rGPJ0CON;
unsigned int *rGPJ0DAT;
char kbuf[100] = {0};//内核空间的buf
ssize_t test_chrdev_read(struct file *file, char __user *ubuf, size_t count, loff_t *ppos)
{
int ret = -1;
printk(KERN_INFO "this is test_chrdev_read.\n");
ret = copy_to_user(ubuf, kbuf, count);
if (ret)
{
printk(KERN_ERR "copy_to_user fail.\n");
return -EINVAL;
}
else
{
printk(KERN_INFO "copy_to_user successfully.\n");
}
return 0;
}
//写函数的本质就是,将应用层传递过来的数据先复制到内核中,然后以正确的
//方式写入硬件完成操作
ssize_t test_chrdev_write(struct file *file, const char __user *ubuf, size_t count, loff_t *ppos)
{
int ret = -1;
printk(KERN_INFO "this is test_chrdev_write.\n");
//使用该函数将应用层传过来的ubuf的内容拷贝到驱动空间中的一个buf中
//memcpy(kbuf, ubuf); //错误,这两个不在一个地址空间中,一个属于内核,一个属于应用
memset(kbuf, 0, sizeof(kbuf));
ret = copy_from_user(kbuf, ubuf, count);
if (ret)
{
printk(KERN_ERR "copy_from_user fail.\n");
return -EINVAL;
}
else
{
printk(KERN_INFO "copy_from_user successfully.\n");
}
//真正驱动中,数据从应用层复制到驱动中后, 我们要根据这个数据
//去操作硬件,所以下面应该就是操作硬件的代码
//方式1:
if (kbuf[0] == '0')//灯灭
{
*rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));
}
else if (kbuf[0] == '1')//灯亮
{
*rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));
}
//方式2:
if (!strcmp(kbuf, "on"))
{
*rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));
}
else if (!strcmp(kbuf, "off"))
{
*rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));
}
return 0;
}
int test_chrdev_open(struct inode *inode, struct file *file)
{
//该函数实现打开这个设备的硬件操作代码
printk(KERN_INFO "this is test_chrdev_open.\n");
*rGPJ0CON = 0x11111111;
*rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5)); // 亮
return 0;
}
int test_chrdev_release(struct inode *node, struct file *file)//对应close函数
{
printk(KERN_INFO "this is test_chrdev_release.\n");
*rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5)); // 灭
return 0;
}
//定义一个file_operations结构体变量,并且去填充
//可在内核源代码搜索file_operations复制一个进行修改
/* File operations struct for character device */
static const struct file_operations test_fops = {
.owner = THIS_MODULE, // 惯例,直接写即可
.open = test_chrdev_open, //将来应用open打开这个设备时,实际调用的就是这个.open对应的函数
.write = test_chrdev_write,
.release = test_chrdev_release, //对应close函数
.read = test_chrdev_read,
};
//模块安装函数
static int __init chrdev_init(void)
{
printk(KERN_INFO "chrdev_init helloworld init\n");
//在module_init宏调用的函数中去注册字符设备驱动
//major 传0进去表示让内核帮我们自动分配一个合适的没被使用的主设备号
//内核如果成功分配就会返回分配的设备号:如果分配失败则会返回负数
mymajor = register_chrdev(0, MYNAME, &test_fops);
if (mymajor < 0)
{
printk(KERN_ERR "register_chrdev fail.\n");
return -EINVAL;
}
else
{
printk(KERN_ERR "register_chrdev successfully.\n");
printk(KERN_ERR "mymajor = %d.\n", mymajor);
}
//使用动态映射的方式来操纵寄存器
#if 0
//方式1:
//申请
if (!request_mem_region(GPJ0CON_PA, 4, "GPJ0CON"))
return -EINVAL;
if (!request_mem_region(GPJ0DAT_PA, 4, "GPJ0DAT"))
return -EINVAL;
//建立连接,真正实现映射
rGPJ0CON = ioremap(GPJ0CON_PA, 4);
rGPJ0DAT = ioremap(GPJ0DAT_PA, 4);
#endif
#if 1
//方式2:
//申请
if (!request_mem_region(GPJ0CON_PA, 4, "GPJ0CON"))
return -EINVAL;
//建立连接,真正实现映射
rGPJ0CON = ioremap(GPJ0CON_PA, 4);
rGPJ0DAT = rGPJ0CON + 1;
#endif
return 0;
}
//模块卸载函数
static void __exit chrdev_exit(void)
{
printk(KERN_INFO "chrdev_exit hellowworld exit\n");
//解除映射
iounmap(rGPJ0CON);
iounmap(rGPJ0DAT);
release_mem_region(GPJ0CON_PA, 4);
release_mem_region(GPJ0DAT_PA, 4);
//在module_exit宏调用的函数中去注销字符设备驱动
unregister_chrdev(mymajor, MYNAME);
}
//这两个函数分别在加载和卸载驱动时被调用即调用insmod和rmmod命令的时候,
//但是insmod和rmmod不能识别这两个函数,它只能识别init_module和cleanup_module,
//其实init_module和cleanup_module相当于是module_init和module_exit的别名
module_init(chrdev_init);//宏定义,根据是否定义MODULE宏,宏展开不同的内容
module_exit(chrdev_exit);//宏定义,根据是否定义MODULE宏,宏展开不同的内容
//MODULE_XXX这种宏的作用用来添加模块描述信息
MODULE_LICENSE("GPL"); // 描述模块的许可证
MODULE_AUTHOR("aston"); // 描述模块的作者
MODULE_DESCRIPTION("module test"); // 描述模块的介绍信息
MODULE_ALIAS("alias xxx"); // 描述模块的别名信息
若需本文章所述完整项目文件,请私信我或者评论区留下邮箱发你。
注:本资料大部分由朱老师物联网大讲堂课程笔记整理而来,如有侵权,联系删除!水平有限,如有错误,欢迎各位在评论区交流。