首页 > 学院 > 开发设计 > 正文

最简单的驱动模块

2019-11-06 09:50:20
字体:
来源:转载
供稿:网友
一. 驱动开发的准备工作    1. 正常运行linux系统的开发板    要求开发板中的linux的zImage必须是自己编译的    在这里的目录路径是:/home/zhao/zhao/5.linux_drive/kernel/arch/arm/boot    2. 内核源码树,就是一个经过了配置编译之后的内核源码    1)Ubuntu的内核源码树,如果要编译在ubuntu中安装的模块就打开这2个                          KERN_VER = $(shell uname -r)           KERN_DIR = /lib/modules/$(KERN_VER)/build    2)开发板linux系统的内核源码树在这里的目录路径是:/home/zhao/zhao/5.linux_drive/kernel    3. nfs挂载的rootfs    在这里ubuntu系统下的路径是:/root/rootfs/rootfs1/driver_test二. 常用的模块操作命令    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后缀)三. 模块的安装与卸载    1. 模块的安装       insmod与module_init宏    1)模块源代码中用module_init宏声明了一个函数(在模块代码的这个例子里是chrdev_init函数),作用就是指定chrdev_init这个函数和insmod命令绑定起来,也       就是说当insmod module_test.ko时,insmod命令内部实际执行的操作就是调用chrdev_init函数    2)在没有指定主设备号的情况下内核会将最新安装的模块放在lsmod显示的最前面    3)正常情况执行了insmod安装模块,会打印出相应的提示信息,但是由于内核的PRintk设置了打印级别,所以这里就算安装成功后也不会显示出提示信息;要想看到       提示信息,在ubuntu中使用dmesg命令就可以看到了    4)内核中printk的打印级别        (1)printk是linux内核源代码中自己封装出来的一个打印函数,是内核源码中的一个普通函数,只能在内核源码范围内使用,不能在应用编程中使用        (2)printk的打印级别是用来控制printk打印的这条信息是否在终端上显示的,内核非常庞大,打印信息非常多,所以才有了打印级别        (3)操作系统的命令行中也有一个打印信息级别属性,值为0-7。当前操作系统中执行printk的时候会去对比printk中的打印级别和命令行中设置的打印级别,小        于命令行设置级别的信息会被放行打印出来,大于的就被拦截的。譬如ubuntu中的打印级别默认是4,那么printk中设置的级别比4小的就能打印出来,比4        大的就不能打印出来        (4)参照内核的打印级别的文档......    2. 模块的版本信息    (1)使用modinfo查看模块的版本信息    (2)内核zImage中也有一个确定的版本信息    (3)insmod时模块的vermagic必须和内核的相同,否则不能安装       不能安装时的报错信息为:insmod: ERROR: could not insert module module_test.ko: Invalid module format    (4)编译模块的内核源码树要与编译正在运行的这个内核的那个内核源码树是同一个    3. 模块卸载    上面安装模块时insmod与module_init宏是对应关系    这里卸载模块时module_exit和rmmod是对应关系    4. 驱动模块中的头文件    驱动源码属于内核源码的一部分,驱动源码中的头文件其实就是内核源代码目录下的include目录下的头文件四. 用开发板来调试模块    1. 用开发板来调试模块    可以设置bootcmd使开发板通过tftp下载自己建立的内核源码树编译得到的zImage;设置如下    set bootcmd 'tftp 0x30008000 zImage;bootm 0x30008000'    2. 设置bootargs使开发板从nfs去挂载rootfs    3. 修改Makefile中的KERN_DIR使其指向自己建立的内核源码树    在这里源码树的目录是:/home/zhao/zhao/5.linux_drive/kernel    4. 将编译好的驱动.ko文件放入nfs共享目录下    在这里nfs的共享目录是:/root/rootfs/rootfs1/driver_test    5. 启动开发板后使用insmod、rmmod、lsmod等去进行模块测试五. 最简单的模块    1. 模块的安装    执行insmod xxx.ko时,调用的是 module_init(chrdev_init)宏,最终通过这个宏来调用chrdev_init函数,来实现模块的安装    2. 模块的卸载    执行rmmod xxx.ko时,调用的是  module_exit(chrdev_exit)宏,最终通过这个宏来调用chrdev_exit函数,来实现模块的卸载六. 添加相关结构体,添加应用层对应的相关函数,添加对应的注册与注销模块    1. 添加file_Operations结构体    1)这个结构体中的元素是应用层与驱动层衔接的对接函数接口    2)元素主要是函数指针,用来挂接实体函数地址    3)每个设备驱动都需要一个该结构体类型的变量    4)设备驱动向内核注册时提供该结构体类型的变量    2. 添加应用层对应的相关函数    1)在上面结构体中填充了应用层中对应的接口,需要在添加对应的实体函数    2)这里是下面这几个函数        (1)zhao_test_chrdev_open()函数        (2)zhao_test_chrdev_close()函数    3. 添加注册与注销模块    1)手动自己分配主设备号        (1)在chrdev_init函数中添加注册驱动的模块           ret = register_chrdev(MYMAJOR, MYNAME, &zhao_test_fops);        1>左值是用来接收注册函数返回来的主设备好        2>参数1:是自定义的主设备号        3>参数2:自定义的设备名称        4>参数3:结构体变量名的地址        (2)在chrdev_exit函数中添加注销驱动的模块           unregister_chrdev(mymajor, MYNAME);        1>参数1:是上面注册函数返回来的驱动的主设备号        2>参数2:是自定义的设备名称    2)让内核自动分配主设备号        定义一个全局变量,这个全局变量用来接收内核分配的主设备号,定义成全局变量是因为在注册与注销的时候都会用到这个变量        (1)在chedev_init函数中           mymajor = register_chrdev(0, MYNAME, &zhao_test_fops);        1>左值是定义的那个全局变量        2>参数1:当让内核自动分配主设备号的时候,这里的参数1是设置为0        3>参数2:自定义的设备名称        4>参数3:结构体变量名的地址七. 添加应用层的代码,实现应用层调用驱动    1. 添加应用层的代码    1)上面已经在驱动层的代码中填充了应用层对应的接口(open、close、write、read),这里完成那个结构体中填充的在应用层中对应的接口      具体代码如实际的app.c文件    2)当insmod注册驱动号成功后,再完成驱动文件的创建,如下命令操作       mknod /dev/zhao_test c 250 0       这个设备文件"zhao_test"要与应用层声明的那个设备文件名字相同,否则执行应用层代码时会报错        当设备文件创建后,只要不关机,注销与注册设备号等操作不会影响设备文件,设备文件是一直存在的,除非关机...    2. 应用层调用驱动    完成驱动号的注册,驱动文件的创建,驱动层去应用层的接口(file_operations结构体以及内部元素的填充、驱动层的接口函数、应用层的接口函数)实现,就可以实现简单的应层调用驱动了    3. 读写接口    1)要实现读写数据,就要在驱动层使用下面两个函数       copy_from_user    用来将数据从用户空间复制到内核空间       copy_to_user        用来将数据从内核空间复制到用户空间       使用这两个函数需要定义一个字符数组,作为内核空间的buf,这个全局变量用来传输应用层和驱动层之间的数据       这两个函数的返回值如果成功复制则返回0,如果不成功复制则返回尚未成功复制剩下的字节数       两个函数的使用方法如下       (1)copy_from_user(kbuf, ubuf, count)        在内核空间的write函数中添加这个函数        参数1:内核空间的字符数组        参数2:应用空间的字符数组        参数3:用来记录传输数量       (2)copy_to_user(ubuf, kbuf, count)        在内核空间的read函数中添加这个函数        参数1:应用空间的字符数组        参数2:内核空间的字符数组        参数3:用来记录传输数量    2)在应用层的代码中添加读与写对应的函数,具体代码参见具体文件        开机,注册insmod驱动,创建字符设备(mknod),执行应用层的文件,实现读写的回环测试八. 添加控制硬件的代码    1. 在驱动中和在裸机中通过寄存器控制硬件有些不同       1)寄存器地址不同     原来是直接用物理地址,现在需要用该物理地址在内核虚拟地址空间相对应的虚拟地址。寄存器的物理地址是CPU设计时决定的,从datasheet中查找到的。       2)编程方法不同     裸机中习惯直接用函数指针操作寄存器地址,而kernel中习惯用封装好的io读写函数来操作寄存器,以实现最大程度可移植性    2. 内存的映射方法有两种    1)静态映射内存          静态映射方法的特点:              内核移植时以代码的形式硬编码,如果要更改必须改源代码后重新编译内核              在内核启动时建立静态映射表,到内核关机时销毁,中间一直有效              对于移植好的内核,用不用他都在那里    2)动态映射内存          动态映射方法的特点:              驱动程序根据需要随时动态的建立映射、使用、销毁映射              映射是短期临时的    (1)2种映射并不排他,可以同时使用    (2)静态映射类似于C语言中全局变量,动态方式类似于C语言中malloc堆内存    (3)静态映射的好处是执行效率高,坏处是始终占用虚拟地址空间;动态映射的好处是按需使用虚拟地址空间,坏处是每次使用前后都需要代码去建立映射&销毁映射    3. 静态映射内存操作led    1)三星版本内核中的静态映射表      (1)主映射表位于:arch/arm/plat-s5p/include/plat/map-s5p.h         1>CPU在安排寄存器地址时不是随意乱序分布的,而是按照模块去区分的。每一个模块内部的很多个寄存器的地址是连续的。所以内核在定义寄存器地址时都是           先找到基地址,然后再用基地址+偏移量来寻找具体的一个寄存器         2>map-s5p.h中定义的就是要用到的几个模块的寄存器基地址。         3>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    2)在驱动层代码中用宏定义定义好寄存器的虚拟地址,例如下         #define GPJ0CON          S5PV210_GPJ0CON         #define GPJ0DAT          S5PV210_GPJ0DAT         #define GPD0CON          S5PV210_GPD0CON         #define GPD0DAT          S5PV210_GPD0DAT         #define rGPJ0CON        *((volatile unsigned int *)GPJ0CON)          #define rGPJ0DAT        *((volatile unsigned int *)GPJ0DAT)         #define rGPD0CON        *((volatile unsigned int *)GPD0CON)         #define rGPD0DAT        *((volatile unsigned int *)GPD0DAT)      在chrdev_init函数中添加对控制寄存器的数据写入代码,例如下        rGPJ0CON = 0X11111111;            rGPD0CON = 0X1111;      在open函数模块中添加对数据寄存器的数据写入代码,例如下        rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));            rGPD0DAT = (0<<1);    这样添加这些代码后,    开机,insmod,mknod,执行应用层的代码后就会控制硬件LED的点亮与熄灭    还可以通过操作write接口从应用层向驱动层传入用户的具体操作来实现对硬件LED具体控制(如:on,off,Flash等),具体代码可参见具体文件...    4. 动态映射内存操作led    1)使用时:要先申请再映射      使用完:先解除映射再释放申请      注:在解除映射之前要关闭对硬件的所有操作    2)寄存器分开映射来控制硬件        分下面4步来完成:        (1)申请内存:使用request_mem_region函数;用法如下:        if(!request_mem_region(GPJ0CON_PA, 4, "GPJ0CON")){                return -EINVAL;            }        函数中:            参数1:寄存器的物理地址            参数2:寄存器所占空间大小        申请成功的话函数返回0;如果不成功的话,return -EINVAL;        (2)动态映射虚拟内存:使用ioremap函数        pGPJ0CON = ioremap(GPJ0CON_PA, 4);        函数中:            左值是映射到的虚拟地址            参数1:寄存器物理地址            参数2:所占空间大小        (3)解除映射:使用iounmap(pGPJ0CON);函数        函数中:            参数:映射到的虚拟地址        (4)释放申请:使用release_mem_region(GPJ0CON_PA, 4);函数        函数中:            参数1:寄存器的物理地址            参数2:所占空间大小    3)寄存器一起映射来控制硬件      具体的实现见下一章的最后......
发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表