目录
一.开启驱动开发之路
二.最简单的模块源码分析1
2.1、常用的模块操作命令
三.最简单的模块源码分析2
3.1、模块卸载
3.2、模块中常用宏
四.最简单的模块源码分析3
4.1、printk函数详解
4.2、关于驱动模块中的头文件
4.3、驱动编译的Makefile分析
五.用开发板来调试模块
5.1、设置bootcmd使开发板通过tftp下载自己建立的内核源码树编译得到的zImage
5.2、设置bootargs使开发板从nfs去挂载rootfs(内核配置记得打开使能nfs形式的rootfs)
六.字符设备驱动工作原理1
6.1、系统整体工作原理
七.字符设备驱动工作原理2
7.1、register_chrdev详解(#include )
八.字符设备驱动代码实践1
8.1、思路和框架
8.2、如何动手写驱动代码
8.3、开始动手(这些结构体变量和函数我们可以直接在内核源码中cp修改)
九.字符设备驱动代码实践2
9.1、注册驱动
9.2、驱动测试
9.3、让内核自动分配主设备号
十.应用程序如何调用驱动
10.1、驱动设备文件的创建
10.2、写应用来测试驱动
10.3、总结
十一.添加读写接口
11.1、在驱动中添加读写接口
十二.读写接口实践
12.1、完成write和read函数
12.2、读写回环测试
12.3、总结
十三.驱动中如何操控硬件
13.1、还是那个硬件
13.2、哪里不同了?
13.3、内核的虚拟地址映射方法
13.4、如何选择虚拟地址映射方法
十四.静态映射操作LED1
14.1、关于静态映射要说的
14.2、三星版本内核中的静态映射表
十五.静态映射操作LED2
15.1、参考裸机中的操作方法添加LED操作代码
15.2、实践测试
15.3、将代码移动到open和close函数中去
十六.静态映射操作LED3
十七.动态映射操作LED
我们的目录源码树为:/root/driver/kernel
E:\Linux\3.AppNet\4.process\2.CharDevBasic\4.1
1.1、驱动开发的准备工作
(1)正常运行linux系统的开发板。要求开发板中的linux的zImage必须是自己编译的,不能是别人编译的。
(2)内核源码树,其实就是一个经过了配置编译之后的内核源码。
(3)nfs挂载的rootfs,主机ubuntu中必须搭建一个nfs服务器。
1.2、驱动开发的步骤
(1)驱动源码编写、Makefile编写、编译
(2)insmod装载模块、测试、rmmod卸载模块
1.3、实践(参考十六.linux开发之Kernel移植——内核的配置和编译原理)
(1)copy原来提供的x210kernel.tar.bz2,找一个干净的目录(/root/driver),解压,并且配置编译。编译完成后得到了:1、内核源码树。2、编译ok的zImage
(参考十六,我们这里直接将事先编译配置好的内核复制到新的/root/driver目录下)
(2)fastboot将第1步中得到的zImage烧录到开发板中去启动(或者将zImage丢到tftp的共享目录,uboot启动时tftp下载启动),将来驱动编译好后,就可以在这个内核中去测试。因为这个zImage和内核源码树是一伙的,所以驱动安装时版本校验不会出错。
(3)测试代码(第一个设备驱动测序的引入):
Makefile代码:
#ubuntu的内核源码树,如果要编译在ubuntu中安装的模块就打开这2个 #KERN_VER = $(shell uname -r) #KERN_DIR = /lib/modules/$(KERN_VER)/build #KERN_DIR = /usr/src/linux-headers-3.13.0-32-generic
#开发板的linux内核的源码树目录 KERN_DIR = /root/driver/kernel
obj-m += module_test.o
all: make -C $(KERN_DIR) M=`pwd` modules
#一下指令将*.ko文件拷贝到nfs共享目录下 cp: cp *.ko /root/porting_x210/rootfs/rootfs/driver_test
.PHONY: clean clean: make -C $(KERN_DIR) M=`pwd` modules clean
|
模块代码:
#include #include
// 模块安装函数 static int __init chrdev_init(void) { printk(KERN_INFO "chrdev_init helloworld init\n"); //KERN_INFO:为打印级别 //https://www.cnblogs.com/mylinux/p/4028787.html //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("aliya"); // 描述模块的作者 MODULE_DESCRIPTION("module one test");//描述模块的介绍信息 MODULE_ALIAS("aliya wwj"); // 描述模块的别名信息 |
(1)lsmod(listmodule,将模块列表显示),功能是打印出当前内核中已经安装的模块列表 |
(2)insmod(install module,安装模块),功能是向当前内核中去安装一个模块,用法是insmod xxx.ko |
(3)modinfo(module information,模块信息),功能是打印出一个内核模块的自带信息。,用法是modinfo xxx.ko |
(4)rmmod(remove module,卸载模块),功能是从当前内核中卸载一个已经安装了的模块,用法是rmmod xxx(注意卸载模块时只需要输入模块名即可,不能加.ko后缀) |
2.2、模块的安装
(1)先lsmod再insmod xxx安装后lsmod查看系统内模块记录。实践测试标明内核会将最新安装的模块放在lsmod显示的最前面。
(2)insmod与module_init宏。模块源代码中用module_init宏声明了一个函数(在我们这个例子里是chrdev_init函数),作用就是指定chrdev_init这个函数和insmod命令绑定起来,也就是说当我们insmod module_test.ko时,insmod命令内部实际执行的操作就是帮我们调用chrdev_init函数。
照此分析,那insmod时就应该能看到c hrdev_init中使用printk打印出来的一个chrdev_init字符串,但是实际没看到。原因是ubuntu中拦截了,要怎么才能看到呢?在ubuntu中使用dmesg命令就可以看到了。
(3)模块安装时insmod内部除了帮我们调用module_init宏所声明的函数外,实际还做了一些别的事(譬如lsmod能看到多了一个模块也是insmod帮我们在内部做了记录),但是我们就不用管了。
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一致?当前插入的模块xxx.ko的版本信息(version magic)与正运行的kernel的版本信息一致。说白了就是模块和内核要同出一门。
当出现这种问题时,使用# dmesg | tail 打印信息,找原因
我出现的问题也是这个提示,但原因并不是版本问题,故使用dmesg | tail 也看不出什么。 版本问题可参考解决方法:http://blog.chinaunix.net/uid-20448327-id-172345.html
后来发现是makefile中将开发板的KERN_DIR目录没有屏蔽,导致这个问题。
屏蔽开发板的KERN_DIR后,从新insmod xxx安装,打印如下,使用rmmod xxx卸载后从新make安装即可。
(1)module_exit和rmmod的对应关系
(2)lsmod查看rmmod前后系统的模块记录变化
(1)MODULE_LICENSE,模块的许可证。一般声明为GPL许可证,而且最好不要少,否则可能会出现莫名其妙的错误(譬如一些明显存在的函数提升找不到)。
(2)MODULE_AUTHOR:// 描述模块的作者
(3)MODULE_DESCRIPTION://描述模块的介绍信息
(4)MODULE_ALIAS:模块别名
3.3、函数修饰符(由内核直接调用的)
(1)__init,本质上是个宏定义,在内核源代码中就有#define __init xxxx。
这个__init的作用就是将被他修饰的函数放入.init.text段中去(本来默认情况下函数是被放入.text段中)。
整个内核中的所有的这类 __init 函数都会被链接器链接放入.init.text段中,所以所有的内核模块的__init修饰的函数其实是被统一放在一起的。内核启动时统一会加载.init.text段中的这些模块安装函数,加载完后就会把这个段给释放掉以节省内存。
(2)__exit,和)__init同理。
(1)printk就是printf的孪生兄弟,一个用于windows,一个用于内核。
(2)printk相比printf来说还多了个:打印级别的设置。printk的打印级别是用来控制printk打印的这条信息是否在终端上显示的。应用程序中的调试信息要么全部打开要么全部关闭,一般用条件编译来实现(DEBUG宏),但是在内核中,因为内核非常庞大,打印信息非常多,有时候整体调试内核时打印信息要么太多找不到想要的,要么一个没有,没法调试。所以才有了打印级别这个概念。
(3)ubuntu中这个printk的打印级别控制没法实践,ubuntu中不管你把级别怎么设置都不能直接打印出来,必须dmesg命令去查看。
(1)驱动源代码中包含的头文件和原来应用编程程序中包含的头文件不是一回事。应用编程中包含的头文件是应用层的头文件,是应用程序的编译器带来的(譬如gcc的头文件路径在 /usr/include下,这些东西是和操作系统无关的)。
驱动源码属于内核源码的一部分,驱动源码中的头文件其实就是内核源代码目录下的include目录下的头文件。
#开发板的linux内核的源码树目录 KERN_DIR = /root/driver/kernel
#这一行就表示我们要将module_test.c文件编译成一个模块 obj-m += module_test.o
#这个命令用来编译实际的模块, make 执行的命令, `pwd`表示我们要把 pwd 当命令去执行, M=`pwd`表示我们 进入到当前这个目录下编译完成后还能回来( 记录当前目录) 。 modules 表示内核中的目标, 然后内核中的目标负责对 我们的程序进行编译 all: make -C $(KERN_DIR) M=`pwd` modules arm-linux-gcc app.c -o app
#一下指令将*.ko文件拷贝到nfs共享目录下 cp: cp *.ko /root/porting_x210/rootfs/rootfs/driver_test cp app /root/porting_x210/rootfs/rootfs/driver_test
.PHONY: clean clean: make -C $(KERN_DIR) M=`pwd` modules clean rm -rf app |
(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是内核源码树的目录,必须根据自己的编译环境输入对应目录!
E:\Linux\3.AppNet\4.process\2.CharDevBasic\4.1
设置好TFTP下载方式及IP地址配对等,详细参考uboot笔记第二节
在控制台设置bootcmd::
set bootcmd 'tftp 0x30008000 zImage;bootm 0x30008000'
并将内核生成的zImage拷贝到tftpboot目录下
重启开发板,自动下载新的zImage。
故我们使用nfs需要设置的bootargs 如下:
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 |
使用tftp烧录zImage后,显示如下,原因是在menuconfig没有开启对NFS的启动支持
(2)在menuconfig中配置支持nfs启动方式
按照如下方法设置make menuconfig,
之后从新make,再次重启开发板即可,打印出如下信息,且进入终端控制台。
上面图片中有一句话 Rreeint init memory:172K 这句话就是系统启动完成后, 释放掉了__init 指定的段。
5.3、修改Makefile中的KERN_DIR使其指向开发板的内核源码树,然后从新make
5.4、将自己编译好的驱动.ko文件放入nfs共享目录下去
在makefile中已写入如下信息,直接make cp即可
cat /proc/devices //只显示主设备号
5.5、开发板启动后使用insmod、rmmod、lsmod等去进行模块实验
使用各个模块操作指令,能打印出对应信息,说明模块实验调试成功。
遇到的问题:提示找不到这个文件
使用 modinfo module_test.ko 时,报错modinfo: can't open '/lib/modules/2.6.35.7/modules.dep': No such file or directory |
解决方法为:
cd /lib/ mkdir modules cd modules/ mkdir 2.6.35.7 cd 2.6.35.7 cp *.ko ./ depmod
modinfo *.ko |
成功打印信息。
(1)应用层->API->设备驱动->硬件
(2)API:open、read、write、close等
(3)驱动源码中提供真正的open、read、write、close等函数实体
6.2、file_operations结构体
(1)元素主要是函数指针,用来挂接实体函数地址
(2)每个设备驱动都需要一个该结构体类型的变量
(3)设备驱动向内核注册时提供该结构体类型的变量
6.3、注册字符设备驱动
(1)为何要注册驱动
对整个设备驱动而言,不注册驱动程序,他是分辨不出来的。
(2)谁去负责注册
编写设备驱动程序向内核注册
(3)注册函数从哪里来
register_chrdev函数用来向内核注册
注册后之后的驱动函数就与内核挂钩了,后面应用API程序就可以调用内核中的驱动程序
(1)作用,驱动向内核注册自己的file_operations结构体,用于挂钩结构体里面的成员
(2)参数
major; // 主设备号 name:// 设备驱动的名称 fops: // 文件系统的接口指针 其中参数major如果等于0,则表示采用系统动态分配的主设备号;不为0,则表示静态注册 |
(3)static inline
这里使用inline,一方面如果inline函数在两个不同的文件中出现,也就是说一个.h被两个不同的文件包含,则会出现重名,链接失败,
另一方面是这个函数大部分表现和普通的static函数一样,只不过在调用这种函数的时候,gcc会在其调用处将其汇编码展开编译而不为这个函数生成独立的汇编码,可以减少开销。
返回值:注册成功为0.失败为-1;
7.2、内核如何管理字符设备驱动
(1)内核中有一个数组用来存储注册的字符设备驱动
(2)register_chrdev内部将我们要注册的驱动的信息(主要是 )存储在数组中相应的位置
(3)cat /proc/devices查看内核中已经注册过的字符设备驱动(和块设备驱动)
(4)好好理解主设备号(major)的概念
主设备号 用于设备文件节点与Linux内核驱动程序进行对应的映射关系
原理:
(1)确定一个主设备号,如果主设备号major=0,则会自动分配设备号
(2)构造一个file_operations结构体, 然后放在chrdevs数组中
(3)注册:register_chrd·ev,cat /proc/devices查看内核中已经注册过的字符设备驱动(和块设备驱动),注意这里并不是驱动文件设备节点!
然后当读写字符设备的时候,就会根据主设备号从chrdevs数组中取出相应的结构体,并调用相应的处理函数
(1)目的:给空模块添加驱动壳子
(2)核心工作量:file_operations及其元素填充、注册驱动
(1)脑海里先有框架,知道自己要干嘛
(2)细节代码不需要一个字一个字敲,可以到内核中去寻找参考代码复制过来改
(3)写下的所有代码必须心里清楚明白,不能似懂非懂
(1)先定义file_operations结构体变量
//自定义一个file_operations结构体,向其填入数据 static const struct file_operations test_fops = { .owner = THIS_MODULE, //惯例,直接写即可 .open = test_chrdev_open, //将来应用open打开这个设备时实际调用的就是这个.open对应的函数 .release = test_chrdev_release, }; |
(2)open和close函数原型确定、内容填充
static int test_chrdev_open(struct inode *inode, struct file *file) { //函数内部真正应该放置的是打开这个设备的硬件操作代码部分 printk(KERN_INFO "test_chrdev_open OK\n"); return 0; }
//在linux内核中,test_chrdev_release就是close函数 static int test_chrdev_release(struct inode *ino, struct file *f) { printk(KERN_INFO "test_chrdev_class OK\n"); return 0; } |
E:\Linux\3.AppNet\4.process\2.CharDevBasic\4.2
(1)主设备号的选择 (我们选择手动分配未使用的主设备号为:200)
(2)返回值的检测
#define MAMAJR 200//主设备号 #define MANAME "TEST_Aliya"//主设备名称
// 模块安装函数 static int __init chrdev_init(void) { int ret=-1;
printk(KERN_INFO "chrdev_init helloworld init\n");
//在module_init宏中去注册字符设备驱动 ret=register_chrdev(MAMAJR, MANAME, &test_fops); if(ret)//注册成功为0 { printk(KERN_ERR "register_chrdev ERROR FOR fail\n"); return -EINVAL; } printk(KERN_ERR "register_chrdev .....OK\n");
return 0; } |
(3)主设备号的注销
// 模块下载函数 static void __exit chrdev_exit(void) { printk(KERN_INFO "chrdev_exit helloworld exit\n");
//在module_exit宏调用的函数中去注销字符设备驱动 unregister_chrdev(MAMAJR, MANAME); } |
代码汇总:
(1)编译等 make && make cp
(2)insmod并且查看设备注册的现象
(3)rmmod并且查看设备注销的现象
(1)为什么要让内核自动分配
.因为自己配置主设备号,还需要使用cat /proc/devices 查询没有使用过的设备号,很麻烦。
(2)如何实现?
在上面的自动分配主设备号程序中将模块安装函数修改下,即将主设备号修改为0即可,表示让内核自动分配未使用的主设备号
如图红色代码,其他代码不变
E:\Linux\4.LinuxDriver\4.3
int MYmajor;//用来返回内核自动分配的主设备号 // 模块安装函数 static int __init chrdev_init(void) {
printk(KERN_INFO "chrdev_init helloworld init\n");
//在module_init宏中去注册字符设备驱动 //major传0进去表示让内核帮我们自动分配一个合适的未被使用的主设备号 //内核如果成功分配会返回分配的主设备号,失败会返回负数 MYmajor=register_chrdev(0, MANAME, &test_fops); if(MYmajor<0)//注册成功为0 { printk(KERN_ERR "register_chrdev ERROR for fail\n"); return -EINVAL; } printk(KERN_ERR "register_chrdev .....OK\n"); printk(KERN_ERR "MYmajor = %d.\n",MYmajor);//打印分配的主设备号 return 0; } |
(3)测试
总结:到这里,我们就完成了一个字符串设备驱动的注册及注销功能测试,虽然我们的open、class函数只是使用了printk,后面会学习在这些函数中放置设备的硬件操作代码部分(即驱动文件),但我们的框架已经建立了。
在这个过程中,尤其是对file_operations结构体变量、open等函数及注册设备号等函数的用法,我们可以直接参考linxu内核源码,直接cp修改。没必要自己一步一步的敲出来。
大多数硬件设备都在/dev目录中有一个对应的设备文件,网络设备除外。
在/dev中的每个文件都有与之相关的主设备号和一个次设备号。内核用这些设备号把对一个设备文件的引用映射到相应的驱动程序上。主设备号标明与文件相关的驱动程序(换句话说是设备类型)。次设备号常常是指定某种给定设备类型的特定实例,次设备号有时被称为单元号。
设备文件分两种类型:
一个块设备文件每次读取或者写入一块数据(一组字节,通常是521的倍数),我们熟知的磁盘就是块设备,在/dev中对应的设备文件就是块设备文件。
块设备文件在用ls -l查看时文件类型为b。
字符设备每次读取或者写入一个字节。磁盘和磁带可以是块设备也可以是字符设备,而终端和打印机不行。字符设备文件在用ls -l查看时文件类型为c
(2)设备文件的关键信息是:设备号 = 主设备号 + 次设备号,使用ls -l去查看设备文件,就可以得到这个设备文件对应的主次设备号。
(3)使用mknod创建设备文件:mknod /dev/xxx c 主设备号 次设备号
c表示字符设备文件
使用如下:mknod /dev/test c 250 0 创建文件为:/dev/test
E:\Linux\3.AppNet\4.process\2.CharDevBasic\4.3
(1)还是上面原来的应用
(2)open、write、read、close等
新建app.c
#include #include #include #include
#define FILE "/dev/test" //和我们使用mknod创建的字符设备名一致
char buf[100];
int main(void) { int fd = -1;
fd = open(FILE, O_RDWR); if (fd < 0) { printf("open %s error.\n", FILE); return -1; } printf("open %s success..\n", FILE);
// 读写文件 write(fd, "helloworld", 10); read(fd, buf, 100);
// 关闭文件 close(fd);
return 0; } |
增加makefile内容:
交叉编译工具链的路径如下图, 我们使用的是符号链接
然后依次 make, make cp。
(3)实验现象预测和验证
在终端执行./app 查看信息
(1)整体流程梳理、注意分层
(2)后续工作:添加读写接口
//自定义一个file_operations结构体,向其填入数据 static const struct file_operations test_fops = { .owner = THIS_MODULE, //惯例,直接写即可 .open = test_chrdev_open, //将来应用open打开这个设备时实际调用的就是这个.open对应的函数 .release = test_chrdev_release, .write = test_chrdev_write, .read = test_chrdev_read, }; |
ssize_t test_chrdev_read(struct file *file, char __user *buf, size_t size, loff_t *ppos) { printk(KERN_INFO "test_chrdev_read ....ok\n"); }
static ssize_t test_chrdev_write(struct file *file, const char __user *buffer, size_t count, loff_t *pos) { printk(KERN_INFO "test_chrdev_write ....ok\n"); } |
11.2、在应用中添加
// 读写文件 write(fd, "helloworld", 10); read(fd, buf, 100); |
11.3、测试
E:\Linux\3.AppNet\4.process\2.CharDevBasic\4.4
①从新make 、make cp 连接终端 进入控制台
②使用insmod xxx.ko 加载模块 cat /proc/devices查看主设备号是否注册
③ 使用mknod创建字符设备文件
使用如下:mknod /dev/test c 250 0 创建文件为:/dev/test
④执行应用程序.app.c
11.4、应用和驱动之间的数据交换
由于内核空间与用户空间的内存不能直接互访,因此借助函数copy_to_user()完成用户空间到内核空间的复制,借助copy_from_user()完成内核空间到用户空间的复制。
(1)copy_from_user,用来将数据从用户空间复制到内核空间
(2)copy_to_user,用来将数据从内核空间复制到用户空间
注意:复制是和mmap的映射相对应去区分的
如写函数的本质就是将应用层传递过来的数据先复制到内核中,然后将之以正确的方式写入硬件完成操作。
E:\Linux\3.AppNet\4.process\2.CharDevBasic\4.5
static inline unsigned long __must_check copy_from_user(void *to, const void __user *from, unsigned long n) { if (access_ok(VERIFY_READ, from, n)) n = __copy_from_user(to, from, n); else /* security hole - plug it */ memset(to, 0, n); return n; }
参数:*to是内核空间的指针,*from是用户空间指针,n表示从用户空间想内核空间拷贝数据的字节数。 返回值:如果成功执行拷贝操作,则返回0,否则返回还没有完成拷贝的字节数。 |
这个函数从结构上来分析,其实都可以分为两个部分:
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; } 参数:*to是用户空间的指针,*from是内核空间指针,n表示从核空间想用户空间拷贝数据的字节数。 返回值:如果成功执行拷贝操作,则返回0,否则返回还没有完成拷贝的字节数。 |
在驱动文件中read和wride函数中添加如下代码:
//读接口 ssize_t test_chrdev_read(struct file *file, char __user *ubuf, size_t count, loff_t *ppos) { int ret = -1;
printk(KERN_INFO "test_chrdev_read ....ok\n");
ret = copy_to_user(ubuf, kbuf, count); if (ret) { printk(KERN_ERR "copy_to_user fail\n"); return -EINVAL; } printk(KERN_INFO "copy_to_user success....ok\n");
return 0; }
//写函数本质就是将应用层传递过来的数据先复制到内核中,然后将之以正确的方式写入硬件完成操作. static ssize_t test_chrdev_write(struct file *file,const char __user *ubuf, size_t count, loff_t *pos) { int ret=-1;
printk(KERN_INFO "test_chrdev_write ....ok\n"); //使用该函数将应用层传过来的ubuf中的内容拷贝到驱动空间中的一个buf中 //memcpy(kbuf,ubuf);不行这样使用,因为2个不在一个地址空间中 ret=copy_from_user(kbuf, ubuf, count); if(ret) { printk(KERN_ERR "copy_from_user error\n"); return -EINVAL; } printk(KERN_INFO "copy_from_user success....ok\n");
//真正的驱动中,数据从应用层复制到驱动中后,我们就要根据这个数据 //去写硬件完成硬件的操作。所以这下面就应该是操作硬件的代码
return 0; } |
在应用文件中添加如下:
// 读写文件 write(fd, "helloworld aliya", 16); read(fd, buf, 100); printf("read buf is:%s.\n", buf); |
测试程序和上面 11.3、测试过程一致,
(1)目前为止应用已经能够读写驱动(中的内存)
(2)后续工作:添加硬件操作代码
(1)硬件物理原理不变
(2)硬件操作接口(寄存器)不变
(3)硬件操作代码不变
(1)寄存器地址不同。原来是直接用物理地址,现在需要用该物理地址在内核虚拟地址空间相对应的虚拟地址。寄存器的物理地址是CPU设计时决定的,从datasheet中查找到的。
(2)编程方法不同。裸机中习惯直接用函数指针操作寄存器地址,而kernel中习惯用封装好的io读写函数来操作寄存器,以实现最大程度可移植性。
(1)为什么需要虚拟地址映射
因为虚拟地址映射能够解决安全隐患、地址不确定问题并缓解了效率的问题
所谓虚拟地址映射就是从虚拟地址映射到物理地址,经由MMU内存管理单元映射到实际的物理地址。MMU是实际的管理内存的硬件。
(2)内核中有2套虚拟地址映射方法:动态和静态
(3)静态映射方法的特点:
内核移植时以代码的形式硬编码,如果要更改必须改源代码后重新编译内核
在内核启动时建立静态映射表,到内核关机时销毁,中间一直有效
对于移植好的内核,你用不用他都在那里
(4)动态映射方法的特点:
比如我们通过芯片手册知道了物理地址而不知道虚拟地址,调用内核给我们提供的动态映射的函数,我们就能得到内存给我们临时分配得虚拟地址,实现机制不用你管,用完之后你在释放它
给你分配的地址就是给你一块可以运行的内存段去运行你的代码(运算、操作你的硬件过程),然后达到目的后,你在释放这段内存,让别人去运行。
映射是短期临时的
(1)2种映射并不排他,可以同时使用
(2)静态映射类似于C语言中全局变量,动态方式类似于C语言中malloc堆内存
(3)静态映射的好处是执行效率高,坏处是始终占用虚拟地址空间;动态映射的好处是按需使用虚拟地址空间,坏处是每次使用前后都需要代码去建立映射&销毁映射(还得学会使用那些内核函数的使用)
(1)不同版本内核中静态映射表位置、文件名可能不同
(2)不同SoC的静态映射表位置、文件名可能不同
(3)所谓映射表其实就是头文件中的宏定义
位于:arch/arm/plat-s5p/include/plat/map-s5p.h
CPU在安排寄存器地址时不是随意乱序分布的,而是按照模块去区分的。每一个模块内部的很多个寄存器的地址是连续的。
所以内核在定义寄存器地址时都是先找到基地址,然后再用基地址+偏移量来寻找具体的一个寄存器。
map-s5p.h中定义的就是要用到的几个模块的寄存器基地址。
map-s5p.h中定义的是模块的寄存器基地址的虚拟地址。
位于:arch/arm/plat-samsung/include/plat/map-base.h
#define S3C_ADDR_BASE (0xFD000000)
// 三星移植时确定的静态映射表的基地址,表中的所有虚拟地址都是以这个地址+偏移量来指定的
位于:arch/arm/mach-s5pv210/include/mach/regs-gpio.h
表中是GPIO的各个端口的基地址的定义
定义位于:arch/arm/mach-s5pv210/include/mach/gpio-bank.h
E:\Linux\3.AppNet\4.process\2.CharDevBasic\4.6
(1)在驱动文件中 添加led寄存器虚拟地址的宏定义
#define GPJ0CONS5PV210_GPJ0CON //LED对应虚拟地址 #define GPJ0DATS5PV210_GPJ0DAT //包含对应头文件:mach/gpio-bank.h
#define rGPJ0CON *((volatile unsigned int *)GPJ0CON) #define rGPJ0DAT *((volatile unsigned int *)GPJ0DAT) |
(2)在init和exit函数中分别点亮和熄灭LED的裸机程序
// 模块安装函数 static int __init chrdev_init(void) { printk(KERN_INFO "chrdev_init helloworld init\n");
//在module_init宏中去注册字符设备驱动 //major传0进去表示让内核帮我们自动分配一个合适的未被使用的主设备号 //内核如果成功分配会返回分配的主设备号,失败会返回负数 MYmajor=register_chrdev(0, MANAME, &test_fops); if(MYmajor<0)//注册成功为0 { printk(KERN_ERR "register_chrdev ERROR for fail\n"); return -EINVAL; } printk(KERN_ERR "register_chrdev .....OK\n"); printk(KERN_ERR "MYmajor = %d.\n",MYmajor);
//insmod时执行的硬件操作 rGPJ0CON = 0x11111111; rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));// led亮 printk(KERN_INFO "GPJ0CON = %p.\n",GPJ0CON);//分析打印的虚拟地址是0xFD500240 printk(KERN_INFO "GPJ0DAT = %p.\n",GPJ0DAT);//分析打印的虚拟地址是0xFD500244
return 0; }
// 模块下载函数 static void __exit chrdev_exit(void) { printk(KERN_INFO "chrdev_exit helloworld exit\n");
//在module_exit宏调用的函数中去注销字符设备驱动 unregister_chrdev(MYmajor, MANAME);
rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));// led灭
} |
(1)insmod和rmmod时观察LED亮灭变化
(2)打印出寄存器的值和静态映射表中的分析相对比
(1)在open和close函数中分别添加点亮和熄灭LED的裸机程序
static int test_chrdev_open(struct inode *inode, struct file *file) { //函数内部真正应该放置的是打开这个设备的硬件操作代码部分 printk(KERN_INFO "test_chrdev_open for OK\n");
rGPJ0CON = 0x11111111; rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));// led亮 printk(KERN_INFO "GPJ0CON = %p.\n",GPJ0CON);//分析打印的虚拟地址是0xFD500240 printk(KERN_INFO "GPJ0DAT = %p.\n",GPJ0DAT); //分析打印的虚拟地址是0xFD500244
return 0; }
//在linux内核中,test_chrdev_release就是close函数 static int test_chrdev_release(struct inode *ino, struct file *f) { printk(KERN_INFO "test_chrdev_class OK\n"); rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));// led灭 return 0; } |
在应用程序app.c中,添加sleep,用于肉眼直观观察到led亮灭。
(2)执行应用文件.app后,观察LED亮灭变化,预期实验现象应该是执行open函数后led亮,然后睡眠3s,class关闭文件时,led熄灭。
实验现象预期一致
E:\Linux\3.AppNet\4.process\2.CharDevBasic\4.7
16.1、添加驱动中的写函数
(1)先定义好应用和驱动之间的控制接口,这个是由自己来定义的。譬如定义为:应用向驱动写"on"则驱动让LED亮,应用向驱动写"off",驱动就让LED灭
//写函数本质就是将应用层传递过来的数据先复制到内核中,然后将之以正确的方式写入硬件完成操作. static ssize_t test_chrdev_write(struct file *file,const char __user *ubuf, size_t count, loff_t *pos) { int ret=-1;
printk(KERN_INFO "test_chrdev_write ....ok\n"); //使用该函数将应用层传过来的ubuf中的内容拷贝到驱动空间中的一个buf中 //memcpy(kbuf,ubuf);不行这样使用,因为2个不在一个地址空间中 memset(kbuf, 0, sizeof(kbuf));//清除kbuf中的内容 ret=copy_from_user(kbuf, ubuf, count); if(ret) { printk(KERN_ERR "copy_from_user error\n"); return -EINVAL; } printk(KERN_INFO "copy_from_user success....ok\n");
//真正的驱动中,数据从应用层复制到驱动中后,我们就要根据这个数据 //去写硬件完成硬件的操作。所以这下面就应该是操作硬件的代码 //方法1
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; } |
(2)应用和驱动的接口定义做的尽量简单,譬如用1个字目来表示。譬如定义为:应用写"1"表示灯亮,写"0"表示让灯灭。对上面代码修改如下,其他不动
//真正的驱动中,数据从应用层复制到驱动中后,我们就要根据这个数据 //去写硬件完成硬件的操作。所以这下面就应该是操作硬件的代码 //方法2
if (kbuf[0] == '1') //写"1"表示灯亮,写"0"表示让灯灭。 { rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5)); } else if (kbuf[0] == '0') { rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5)); } |
16.2、写应用来测试写函数
(1)先定义好应用和驱动之间的控制接口,这个是由自己来定义的。譬如定义为:应用向驱动写"on"则驱动让LED亮,应用向驱动写"off",驱动就让LED灭
#include #include #include #include #define FILE "/dev/test" //和我们使用mknod创建的字符设备名一致
char buf[100];
int main(void) { int fd = -1;
fd = open(FILE, O_RDWR); if (fd < 0) { printf("open %s error.\n", FILE); return -1; } printf("open %s success..\n", FILE);
// 读写文件 //方法1 write(fd, "on", 2);//亮 sleep(2); write(fd, "off", 3);//灭 sleep(2); write(fd, "on", 2);//亮 sleep(2);
// 关闭文件 close(fd);
return 0; } |
(2)应用和驱动的接口定义做的尽量简单,譬如用1个字目来表示。譬如定义为:应用写"1"表示灯亮,写"0"表示让灯灭。对上面代码修改如下,其他不动
// 读写文件 //方法2
write(fd, "1", 1);//亮 sleep(2); write(fd, "0", 1);//灭 sleep(2); write(fd, "1", 1);//亮 sleep(2); |
(3)丰富应用和驱动的接口,具有用户输入功能,让用户自定义输入 on | off决定亮灭
对上面代码修改如下,其他不动
// 读写文件 //方法3 while (1) { memset(buf, 0 , sizeof(buf)); printf("请输入 on | off \n"); scanf("%s", buf); if (!strcmp(buf, "on")) { write(fd, "1", 1); } else if (!strcmp(buf, "off")) { write(fd, "0", 1); } else if (!strcmp(buf, "flash")) { for (i=0; i<3; i++) { write(fd, "1", 1); sleep(1); write(fd, "0", 1); sleep(1); } } else if (!strcmp(buf, "quit")) { break; } } |
(4)对3种不同的应用和驱动的接口定义的代码编写,测试.app应用程序,观察实验现象
方法①:开发板看到led 亮->灭->亮->执行class(灭)
方法②:与方法一实验现象一致,只是程序上是1个字目来表示,wire写"1"表示灯亮,写"0"表示让灯灭
方法③:控制台输入on时,led点亮,输入off时,led熄灭,输入flash时,开发板看到led 亮->灭->亮->灭。
16.3、驱动和应用中来添加读功能(在应用程序中添加如下红色代码)
while (1) { memset(buf, 0 , sizeof(buf)); printf("请输入 on | off | flash \n"); scanf("%s", buf); if (!strcmp(buf, "on")) { write(fd, "1", 1); memset(buf, 0, sizeof(buf)); read(fd, buf, 100); printf("灯亮读出来的内容是:%s.\n", buf); } else if (!strcmp(buf, "off")) { write(fd, "0", 1); memset(buf, 0, sizeof(buf)); read(fd, buf, 100); printf("灯灭读出来的内容是:%s.\n", buf); } else if (!strcmp(buf, "flash")) { for (i=0; i<3; i++) { write(fd, "1", 1); sleep(1); memset(buf, 0, sizeof(buf)); read(fd, buf, 100); printf("灯持续亮灭读出来的内容是:%s.\n", buf);
write(fd, "0", 1); sleep(1); memset(buf, 0, sizeof(buf)); read(fd, buf, 100); printf("灯持续亮灭读出来的内容是:%s.\n", buf); } } else if (!strcmp(buf, "quit")) { break; } } |
在方法3源码的基础上添加读功能,在控制台能观察到灯亮灭读出了buf中的内容
E:\Linux\3.AppNet\4.process\2.CharDevBasic\4.8
17.1、如何建立动态映射
(1)request_mem_region,向内核申请(报告)需要映射的一块输入输出区域。
#define request_region(start,n,name) __request_region(&ioport_resource, (start), (n), (name)) 参数1:io端口的基地址,传的是物理地址 参数2:指定I/O内存资源的大小 字节数。 参数3:使用这段io地址的设备名。 |
(2)ioremap,真正用来实现映射,传给他物理地址他给你映射返回一个虚拟地址。
#define ioremap(cookie,size) __arm_ioremap(cookie, size, MT_DEVICE) 参数1:io端口的基地址,传的是物理地址 参数2:io端口占用的范围字节数。 返回值为虚拟地址 |
17.2、如何销毁动态映射
(1)iounmap:解除映射
#define iounmap(cookie) __iounmap(cookie) 参数:io端口的基地址,传的是虚拟地址 |
(2)release_mem_region:释放申请一块输入输出区域
#define request_mem_region(start,n,name) __request_region(&iomem_resource, (start), (n),(name)) 参数1:io端口的基地址,传的是物理地址 参数2:io端口占用的范围 字节数。 |
注意:映射建立时,是要先申请再映射;然后使用;使用完要解除映射时要先解除映射再释放申请。
17.3、代码实践
只在注册和卸载驱动模块的地方用了动态映射,如需要所有相关的操作都使用动态映射,自行修改。
(1)2个寄存器分开独立映射
在驱动程序中添加如下程序:应用程序文件不变
#define GPJ0CON 0xE0200240 //LED寄存器对应物理地址 #define GPJ0DAT 0xE0200244 //(通过芯片手册得到)
//定义两个指针,用于接收ioremap函数的返回值(虚拟地址) unsigned int *pGPJ0CON; unsigned int *pGPJ0DAT; |
// 模块安装函数 static int __init chrdev_init(void) { printk(KERN_INFO "chrdev_init helloworld init\n");
//在module_init宏中去注册字符设备驱动 //major传0进去表示让内核帮我们自动分配一个合适的未被使用的主设备号 //内核如果成功分配会返回分配的主设备号,失败会返回负数 MYmajor=register_chrdev(0, MANAME, &test_fops); if(MYmajor<0)//注册成功为0 { printk(KERN_ERR "register_chrdev ERROR for fail\n"); return -EINVAL; } printk(KERN_ERR "register_chrdev .....OK\n"); printk(KERN_ERR "MYmajor = %d.\n",MYmajor);//打印主设备号
//使用动态方式操作硬件寄存器 //先申请 if (!request_mem_region(GPJ0CON, 4, "GPJ0CON_name")) { return -EINVAL; } if (!request_mem_region(GPJ0DAT, 4, "GPJ0DAT_name")) { return -EINVAL; } //再映射 pGPJ0CON=ioremap(GPJ0CON, 4);//实现映射关系 pGPJ0DAT=ioremap(GPJ0DAT, 4);
*pGPJ0CON = 0x11111111; //*(pGPJ0CON+1) *pGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));// led亮
return 0; } |
// 模块下载函数 static void __exit chrdev_exit(void) { printk(KERN_INFO "chrdev_exit helloworld exit\n");
printk(KERN_INFO "4 leds off\n"); *pGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));// led灭
//解除映射 //解除映射时要先解除映射再释放申请。
iounmap(pGPJ0CON);//解除映射时,传的是虚拟地址 iounmap(pGPJ0DAT); / /释放申请一块输入输出区域 release_mem_region(GPJ0CON, 4); release_mem_region(GPJ0DAT, 4);
//在module_exit宏调用的函数中去注销字符设备驱动 unregister_chrdev(MYmajor, MANAME); } |
(2)实验现象:
make后,链接开发板,进入控制台,insmod安装模块时可以看到led已经被点亮,使用rmmod卸载模块时led关闭
(3)2个寄存器在一起映射
即只需要#define GPJ0CON 0xE0200240 映射,字节设置为8即可,即把下一个连续的地址也映射了。