linux内核驱动基础

Author: sx                                                          E-mail:598726409@qq.com

资料整理于自己的笔记(有许多的图片和代码块没有添加,后面会慢慢补齐),也有些是博客之类 ,如有侵权,请及时联系我,我会及时删除。因为都是手码的,可能会有些错别字,理解有误的地方请联系我,以便及时修改
< 驱动 >

什么是驱动(怎么理解驱动)?
< 驱动 >
字面意义就是让某个物件动起来(物理意义)
< 硬件驱动 >
对于硬件来说,驱动就是给各种各样的元器件加不同的电压,让它工作。
< Linux内核驱动(软件驱动)>
基于Linux操作系统内核(加载到操作系统中)来操控硬件工作的逻辑方法

< Linux体系架构>
(1)分层:

大体上来说Linux是分层的,上层为应用层,下层为操作系统层,操作系统中包含了驱动,驱动功能完成以后,开放接口给系统调用,应用层通过系统调用来调用相应的驱动程序工作。
(2)驱动的上层是系统调用
(3)驱动的下层是硬件(硬件的表现即为寄存器)
(4)驱动本身也是分层的





< 模块化设计 >

< 模块化设计 >
(1)宏内核(单核心)
宏内核被视作为运行在单一地址空间的单一的进程,核心提供的所有服务,都以特权模式,在这个大型的核心地址空间中运作,这个地址空间被称为核心空间(kernel space)。它通常是以单一静态二进制文件的方式被存储在磁盘,或是高速缓存上,在开机之后被加载存储器中的核心空间,开始运作。
其操作系统的代码依然是高度紧密的,很难修改成其他类型的操作系统架构。此外,所有的模块也都在同一块寻址空间内运行,倘若某个模块有错误、瑕疵(Bug),运行时就会损及整个操作系统运作。相反,如果设计完美,运行效率就会很高。
(2)微内核
是提供操作系统核心功能的内核的精简版本,它设计成在很小的内存空间内增加移植性,提供模块化设计,以使用户安装不同的接口。
(3)Linux内核
Linux本质上是宏内核的,但是增加了模块化的设计
1)静态加载
静态加载即把功能模块加入内核,重新编译执行(典型就是需要重启)
2)动态加载
动态加载即不需要重新编译,直接动态加载入内核,随时可卸载


< 设备驱动分类 >

< 设备驱动分类 >
(1)字符设备
字符设备准确的来说应该是字节型设备,它是在I/O过程中以字节为单位来进行传输的设备
(2)块设备
相类比与字符设备,块设备就是以块为单位来进行传输的,需要注意的是,这里的块不是一个确定大小,因为块的大小是硬件决定的,eg:块设备典型的例子是存储类设备,比如SD卡(512k),当我们处理SD卡中某个字节的数据时,我们需要都读取一个块,加载到内存中,然后在内存中进行修改,再写回块设备。



TIPS:内存是可以以字节为单位进行处理的,所以需要加载到内存中,相比于块设备,字符设备效率要低,但是块设备不具备处理以字节为单位的数据。

(3)网络设备
就是和网络相关的设备

典型和非标准型的字符设备
1)典型的字符设备就是一些常见的外设,比如串口,LED,LCD之类
2)非标准的字符设备就是根据需要自己编写的字符驱动


< 字符设备基础 >

先找一个kernel,对其进行配置。
(1)配置路径 arch/arm/configs/xxxx
make xxx
(2)编译内核生成内核源码树
编译的内核必须和编译模块是一个内核源码树,否则会不能通过模块的版本校验。
可以避免加载驱动的时间有版本不匹配问题。
(3)修改内核元码数路径,开始编译驱动

1.自己编写一个驱动并加载

__init/__exit是两个宏,在linux/includ下



作用:简单来说是指示gcc把标记的数据或者函数放到指定段
linux中把一些启动及初始化时候用的数据用__init标识,然后在适当的时候把它们释放,回收内存。
这些宏是一些辅助信息,注意,许可证必须填写(GPL/BSD)


printk 头文件 makefile

< 1.printk有打印级别 >

echo x > /proc/sys/kernel/printk  可以更改打印级别

< 2.Linux中的头文件>
都是类似与这样的

其实的路径是 内核/include/linux/xxxx

< 3.编译的Makefile分析>

obj-m:动态链接到内核,不会编译到内核(动态编译)
obj-y:静态编译到内核
-C: 指定进入某个目录进编译
M:保存当前目录,即编译生成文件回归当前目录
modules:编译模块
总的来说:这个Makefile其实是将模块从内核中编译到当前目录


字符设备体系框架

< file_operations结构体 >
应用层->API(read/write/open/close)->设备驱动->硬件

当应用层去调用API时会去调用驱动设备,那么这里的系统调用就必须和驱动设备进行关联,就是利用file_opeartions结构体。

< 字符设备的注册 >register_chrdev
用来(注册file_operatinos)
当我们写好了一个设备驱动程序,必须想内核注册,否则内核不会知道这个程序的存在,那么也就无法加载入内核。
< 注册的原理 >
在内核中有一个数组用来存储字符设备驱动,register_chrdev内部是将字符设备的信息存储在这个数组的相应位置。
cat /proc/devices可以查看当前已注册的设备
/proc是一个虚拟文件,它其实是内核中的一些数据结构,我们查看这个文件也就是查看内核中这些数据结构的值。
register_chrdev< 注册设备>(放在__init中)insmod

unregister_chrdev< 注销设备 >(放在__exit中)rmmod

< 自动注册设备驱动 >(注意:容错条件只有当正确返回值为0是才能)if(val)否则会出错。


应用程序调用


我们编译生成好的.ko文件,将其加载到内核中后,应用层是通过调用API,对我们生成的文件节点进行open,然后调用驱动工作。
注意:这里的应用程序必须要用交叉编译工具链进行编译。
file xxx  可以查看文件属性
应用程序打开的文件名必须是文件节点名  /dev/xxx   (mknod 生成)

添加读写接口

(1)现在file_operations结构体中添加

(2)<  read >

(3)<  write >
然后就可以在应用程序中利用读写接口,操作设备驱动。
传参:
<  copy_from_user> <  copy_to_user>
copy_from_user   copy_to_user 的返回值
这两个函数的返回值是还未读取完成的字节数,比如发送的字节数为50,这次传送只传了30,那么还剩下20,那么它就返回20,当传输完毕以后返回0

应用read/write的对象都是驱动设备,所以当两边需要通信时,都是以内核角度来看,所以read时,是copy_to_user,write时是copy_from_user

TIPS:内核和应用本来就不在一个层级,所以通信需要调用这两个函数,内核独享1G内空间,而应用都是剩下的4G而且是虚拟地址空间。

虚拟地址映射

当我们启动内核时,我们就开启的MMU,开启MMU以后它对所有的物理地址进行了转换(便于管理),也就是从物理地址到虚拟地址的转换。
(1)静态映射
静态映射是在内核启动时就依据了一张虚拟地址转换表进行的映射,此时定下了物理地址到虚拟地址的转换,那么就无法更改,所以静态映射只此一份。它占用资源,直到内核结束运行才释放,效率高。
(2)动态映射
动态映射是随时的映射,在用的时间去申请,在不用的时间去是释放,可以实现一个物理地址多个虚拟地址的映射,占用内存灵活,相比之下效率低。

动态映射

request_mem_region 向内核申请虚拟地址映射(PA)
ioremap 建立映射(PA---->VA)
iounmap 解除映射(VA)
release_mem_region 向内核注销申请的虚拟地址(PA)

< 字符设备高级 >

老接口:regist_chrdev file_operations ...
新接口:register_chrdev_region(手动分配) +  cdev族
< cdev族 >
这个结构体的定义在/Cdev.h中





< dev_t dev > 设备号  主设备 + 次设备
MKDEV(MAJIO + MINOR)

在内核中主设备号,和次设备号共同组合成一个设备号
MMAJIO和MINOR所占位数会根据内核不同有所改变。
步骤:
register_chrdev_region(dev_t dev, cnt,name");
dev_t  dev = MKDEV(major, minor);
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
struct cdev test_cdev;
cdev_del(&test_cdev);
struct cdev test_cdev;
unregister_chrdev_region(MY_DEV, 1);
dev_t  cnt

< 自动分配设备号 >
alloc_chrdev_region
alloc_chrdev_region(&MY_DEV, 0, 1, "led_device");
传出行参数,会分配到一个设备号,后面指定次设备号的起始,个数,名字
major = MAJOI(主次设备号)      利用这个宏会得到主设备号
minor = MINOR(主次设备号)   利用这个宏会得到次设备号

< 倒影式的处理错误机制 >
(1)申请设备号
如果这步出错,那么剩下的就无意义,就应该直接返回
-->返回
(2)向内核注册驱动
如果这步出错,那么应该先去把前面申请的设备号给注销,然后返回
注销设备号  ->  返回
(3)虚拟地址映射
如果这步出错,那么应该先注销设备驱动,再注销设备号。  
注销设备驱动  --> 注销设备号  --> 返回


< cdev_alloc >
之前的代码都是定义一个 struct cdev 类型的结构体,因为是全局变量,有诸多不便,利用cdev_alloc可以给这个结构体一块内存,从而不用那么多的.data的内存。
cdev_alloc申请的内存会在cdev_del的时候释放。



接口实现分析


< register_chrdev >
__register_chrdev
__register_chrdev_region






所以这是这就是register_chrdev 能直接一步申请设备号和注册驱动的原因  ---> 在这里申请了一个cdev指针

这就是设备号的数组






接着对这个数组(结构体指针数组)进行判断,当majir == 0时,自动寻找这个数组(从高到低的顺序查找)返回这个数组的下标,就是设备号。





register_chrdev_region这个函数,是根据我们传递的参数,来多次调用,创建设备号。


关于内核中处理kzlloc的申请并释放内存的问题,在申请设备号的时间,内核会有一个计数操作(kcnt++)当释放时,会自动检查,当这个值为0的时间,就表明没有内存在被使用,就会释放。

自动创建设备文件

< class_create> ---->struct class * pcls
< device_create>    ---->struct device * pdev

device_destroy
class_destroy




原理分析
class_create
在内核中,用了很多这样的技巧,给结构体定义一个指针,然后用kcalloc去实例化,然后填充,在填充之初就设立好了释放函数,当我们需要释放时就可以直接调用。
class_create:会在/sys/class下生成一个设备类文件其实这个设备类是内核数据结构的读写



devices_create其实就是根据这些数据创建这些文件,然后通过给Udev发送一个通知把这些信息发送给udev,然后udev自动进行了创建。

静态映射表的原理

(1)映射表:
在每块板子的头文件中有这样定义好了的头文件,就是静态映射表
(2)映射页表的建立:
在内核启动时,当加载到机器相关(s5pv210_init)的初始化中有一个map_io,中包含了建立一个页表来映射的结构体,这个结构体是以4K为单位来建立映射的。
(3)内核启动如何调用
内核启动时有一个MACH宏,其中就包含了一个映射页表的信息xxx->map_io,当启动加载到机器相关的代码时,就会调用map_io.从而建立映射

动态映射用结构体来操作寄存器

(1)定义一个封装结构体
(2)定义一个这样的结构体指针
(3)建立映射

void __iomem *baseaddr
读写接口来写入寄存器。


< 驱动框架 >

怎么理解驱动框架?
(1)驱动框架即内核维护者,针对每个种类的驱动设计出来的一种成熟的,规范的,标准的,典型的驱动实现,即把各厂家相同部分自己实现好,不同部分提供接口,以便开发者使用
(2)驱动框架维护了管理系统的资源(GPIO/中断号。。)
(3)驱动框架的表现就是:一些特定的数据结构 + 一些接口
















  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值