字符设备驱动基础

5.2.字符设备驱动基础

5.2.1.驱动开发之路

5.2.1.1、驱动开发的准备工作

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

5.2.1.2、驱动开发的步骤

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

5.2.1.3、实践

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

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

5.2.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等)

5.2.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帮我们在内部做了记录),但是我们就不用管了。

5.2.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一致?编译模块的内核源码树就是我们编译正在运行的这个内核的那个内核源码树即可。说白了就是模块和内核要同出一门。

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

5.2.3.1、模块卸载

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

5.2.3.2、模块中常用宏

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

5.2.3.3、函数修饰符

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

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

5.2.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命令去查看。

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

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

5.2.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.2.5.用开发板来调试模块

5.2.5.1、设置bootcmd使开发板通过tftp下载自己建立的内核源码树编译得到的zImage

set bootcmd 'tftp 0x30008000 zImage;bootm 0x30008000'

5.2.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.2.5.3、修改Makefile中的KERN_DIR使其指向自己建立的内核源码树
5.2.5.4、将自己编译好的驱动.ko文件放入nfs共享目录下去
5.2.5.5、开发板启动后使用insmod、rmmod、lsmod等去进行模块实验

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

5.2.6.1、系统整体工作原理

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

5.2.6.2、file_operations结构体

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

5.2.6.3、注册字符设备驱动

(1)为何要注册驱动---------因为我写的驱动内核并不知道
(2)谁去负责注册---------驱动自己注册自己
(3)向谁注册--------------内核
(4)注册函数从哪里来--------内核提供的
(5)注册前怎样?注册后怎样?注册产生什么结果?

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

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

(1)作用,驱动向内核注册自己的file_operations
(2)参数
(3)inline和static
在这里插入图片描述Major主设备号,类似于我们的身份证号码,name名字,用于让我们更加清楚这个设备驱动是干嘛用的,
第三个参数是最重要的,类似于把我们注册的信息以结构体的形式传递给内核来完成注册功能,因此基本上是最重要的参数。

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

(1)内核中有一个数组用来存储注册的字符设备驱动,我们向内核注册驱动的时候,通过major这个编号(这个编号有点类似数组的下标)去寻找对应的数组位置,如果这个位置为空,就将其注册进入相应的数组位置。
(2)register_chrdev内部将我们要注册的驱动的信息(主要是 )存储在数组中相应的位置
(3)cat /proc/devices查看内核中已经注册过的字符设备驱动(和块设备驱动)
(4)好好理解主设备号(major)的概念

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

5.2.8.1、思路和框架

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

5.2.8.2、如何动手写驱动代码

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

5.2.8.3、开始动手

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

对应我们2.8代码中的第三步,就是对结构体里面的.open,.release函数进行实际的编写,但是这些函数都是有指定的函数格式要求的,通过下面截图2我们可以知道他的参数是什么,返回值是什么,然后进行相应的编写即可。

在这里插入图片描述
通过上图我们可知这里这个结构实际调用的.open,.releae两个函数,初期我们可以模仿把这两个函数照着样子抄写到我们自己要编写的代码中,然后进行改名字和对应的内容即可。
在这里插入图片描述

上面两个在结构体file_operations中实际要调用的函数我们编写好之后,下面我们就要进行注册这个驱动,那么注册函数就是下面截图这个函数,我们进行调用和相应的初始化。

在这里插入图片描述

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

5.2.9.1、注册驱动

(1)主设备号的选择
(2)返回值的检测

5.2.9.2、驱动测试

(1)编译等 make && make cp
(2)insmod并且查看设备注册的现象
(3)rmmod并且查看设备注销的现象

5.2.9.3、让内核自动分配主设备号

(1)为什么要让内核自动分配-----------就是让注册驱动函数的第一个参数为0,然后内核就会自动分配
(2)如何实现?
(3)测试

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

5.2.10.1、驱动设备文件的创建

(1)何为设备文件---------------主要包含的是主设备号,而这个数组我们也是通过主设备号来进行当做下标的,数组中就放的是相应文件的file_operations结构,结构体中就存放的 真正硬件要做事的驱动函数
(2)设备文件的关键信息是:设备号 = 主设备号 + 次设备号,使用ls -l去查看设备文件,就可以得到这个设备文件对应的主次设备号。(这里要用主设备号与次设备号的说法的原因,就是假如一个设备有多个,比如LED我们有四个,全用主设备号,就过多占用主设备号了,就需要通过主次设备号这样来表示)
(3)使用mknod创建设备文件:mknod /dev/xxx c 主设备号 次设备号----------------这里面的c代表是字符设备文件的意思。在这里插入图片描述
上图中可以看出我们创建了一盒名字叫做test的设备文件,主设备号是250,因为我们在2.9中安装了一个模块,自动分配的设备号是250,所以我们这里就进行利用。而且要记住这个设备文件不能用vi打开,这样会什么都看不见,而是应该用 ls -l这样的方式来查看相应的信息。

5.2.10.2、写应用来测试驱动

(1)还是原来的应用
(2)open、write、read、close等
(3)实验现象预测和验证

5.2.10.3、总结

(1)整体流程梳理、注意分层
(2)后续工作:添加读写接口

5.2.11.添加读写接口

5.2.11.1、在驱动中添加
5.2.11.2、在应用中添加
5.2.11.3、测试
5.2.11.4、应用和驱动之间的数据交换

(1)copy_from_user,用来将数据从用户空间复制到内核空间
(2)copy_to_user,从内核空间复制到用户空间
注意:复制是和mmap的映射相对应去区分的

在这里插入图片描述
上图中,是我们自己定义的read和write函数,虽然函数名和函数体都是我们自己随便写的,但是函数的参数和官方内核里面的是一样的,这里我们重点分析一下他们的参数中都有__user *buf这个参数,他代表用户空间的buf,当我们读的时候是char __user *buf,他相当于是个传出参数,我们read是读从内核传到这个buf的数据,里面的函数体应copy_to_user,从内核空间复制到用户空间,这样的关系;

const char __user *ubuf;

这个函数参数是我们write函数的参数,一看const修饰的就是传入参数,这个buf里面的数据是我们要写入内核的,因此用const修饰,防止修改,然后函数体应该实现的是copy_from_user,将数据从用户空间复制到内核空间。

5.2.12.读写接口实践

5.2.12.1、完成write和read函数

(1)copy_from_user函数的返回值定义,和常规有点不同。返回值如果成功复制则返回0,如果 不成功复制则返回尚未成功复制剩下的字节数。

在这里插入图片描述

5.2.12.2、读写回环测试

5.2.12.3、总结

(1)目前为止应用已经能够读写驱动(中的内存)
(2)后续工作:添加硬件操作代码

5.2.13.驱动中如何操控硬件

5.2.13.1、还是那个硬件

(1)硬件物理原理不变
(2)硬件操作接口(寄存器)不变
(3)硬件操作代码不变

5.2.13.2、哪里不同了?

(1)寄存器地址不同。原来是直接用物理地址,现在需要用该物理地址在内核虚拟地址空间相对应的虚拟地址。寄存器的物理地址是CPU设计时决定的,从datasheet中查找到的。
(2)编程方法不同。裸机中习惯直接用函数指针操作寄存器地址,而kernel中习惯用封装好的io读写函数来操作寄存器,以实现最大程度可移植性。

5.2.13.3、内核的虚拟地址映射方法

(1)为什么需要虚拟地址映射
(2)内核中有2套虚拟地址映射方法:动态和静态
(3)静态映射方法的特点:
内核移植时以代码的形式硬编码,如果要更改必须改源代码后重新编译内核
在内核启动时建立静态映射表,到内核关机时销毁,中间一直有效
对于移植好的内核,你用不用他都在那里
(4)动态映射方法的特点:
驱动程序根据需要随时动态的建立映射、使用、销毁映射
映射是短期临时的

5.2.13.4、如何选择虚拟地址映射方法

(1)2种映射并不排他,可以同时使用
(2)静态映射类似于C语言中全局变量,动态方式类似于C语言中malloc堆内存
(3)静态映射的好处是执行效率高,坏处是始终占用虚拟地址空间;动态映射的好处是按需使用虚拟地址空间,坏处是每次使用前后都需要代码去建立映射&销毁映射(还得学会使用那些内核函数的使用)

5.2.14.静态映射操作LED1

5.2.14.1、关于静态映射要说的

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

5.2.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

在这里插入图片描述

我们在上图所示的文件(gpio-bank.h)找到led相关的两个寄存器的基地址,这个地址就是静态映射表形成的虚拟地址。

下图中是我们编写的驱动模块的代码,就包含这个这个文件这样的头文件的意思,然后在其中定义了相应的宏。

在这里插入图片描述
下图就是我们数据手册查到的两个寄存器真正的物理地址是什么,然后就形成了上图中所示的静态映射表形成的虚拟地址通过mmu到下图中的物理地址的映射。
在这里插入图片描述

5.2.15.静态映射操作LED2

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

5.2.16.静态映射操作LED3

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

5.2.17.动态映射操作LED

5.2.17.1、如何建立动态映射

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

5.2.17.2、如何销毁动态映射

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

5.2.17.3、代码实践

(1)2个寄存器分开独立映射
(2)2个寄存器在一起映射

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值