韦东山git
用户首先确定一个设备。
电脑里需要先有对应的驱动文件才能对这个设备能进行open()等操作
Linux内核里给驱动文件规定了一个结构体,驱动开发人员可以挑选一些需要的功能进行实现,最基础的就是open()/write()/read().
现在开始写的代码都是在内核里面了,不是在用户空间了。
先写一个能打开/关闭设备,能读/写内容 这4个功能的驱动。
编写驱动程序的流程【具体代码参照内核里的misc.c文件作为范例】
初始化一个结构体,结构体成员是函数名,这个赋值就是c99给结构体赋值的一种格式而已,并不是新定义一个结构体【毕竟类型都没提呢】。所以对应成员的函数的输入和返回值是被严格要求的【对嘛,这样才能统一】结构体里面还有不少函数,但是咱不需要去实现的函数就不用赋值。
C99标准的结构体赋值注解
__user 是一个空的宏,只是一个给我们看的标志而已,它代表这个传入参数是从用户空间传过来的,在内核里的函数不能直接读取使用。
要用下图的这两个函数copy_to_user() 和 copy_from_user()进行数据的获取和转移。
printk()的调试信息不会打印在用户的终端窗口,要看的话用dmesg这个指令,它会输出所有的内核运行时的打印信息。
C语言 __ FILE__ - C语言零基础入门教程
前面主设备号major =0 只是暂时写的,在安装驱动时需要内核分配合适的主设备号。所以驱动程序应该是必备register_chrdev() 这个函数的吧。
注册主设备号要提交俩材料:设备名和设备功能。跟开公司似的。后续会搞个皮包公司,注册时上传的函数里面写着的是调用其他函数,鬼精鬼精的,又能保护代码机密,又有很好的去耦合性。
但是上图的入口/出口函数这样写完只不过是一个普通的子函数。它们需要用下图77,78这两行代码注册到内核里,到时候内核才会自动调用。这俩其实是个宏,它们会定义一个结构体,然后把这个函数存进来。
可以把内核里面的多个驱动文件看作一个链表,所以需要主设备号来编号,然后用init把这个驱动文件挂到内核的葡萄藤上,函数就是这一串上的一粒粒葡萄。
下图第79行是声明这个驱动遵守GPL协议,是一个开源协议,必须写
这个驱动的功能完成了,但是我们要在用户终端使用的话 就需要把这个驱动设备挂载到dev/上
用device_create()函数,这个函数需要传入一个class句柄,就是下图第一步创建的那个。MKDEV(主设备号,次设备号)
真严谨,class创建失败还顺便把驱动给卸载了。毕竟就算存在了也没法使用嘛。。
//上文的代码在编译的时候有点小bug 比如函数返回值未使用,输入参数要加一个const 这样。
写一个abc 然后读出来
/dev/hello这个路径写在代码里面了
识别参数,还真是极简。。。
用makefile编译。
内核编译就是单单第13行那句,这句话是叫内核重新编译pwd文件,$变量里就是内核代码的路径
编译好的驱动程序是.ko类型的文件
教程没细讲。。咋办,后面的课程都没法练?
教程是使用交叉编译,他将文件挂载到Linux的/mnt这里
在/mnt这个挂载点能看到刚写的代码
insmod命令 装载驱动
cat 查看我们设备目录里有没有这个hello驱动了
lsmod 看内核中已经加载的驱动程序
查看dev/文件夹内有没有我们驱动的设备节点,这里显示这个驱动的设备号是236
romod 卸载驱动程序
linux下的各种文件夹主要用于什么?
下图提到一种寄存器,专门用来给我们向bit赋值用的。
如果想给某一bit置1 就用set_reg = 1<< n
如果想给第n位清零 就用clr_reg = 1<< n 对的,这里的1代表的是对bit的选择。
【不过只有少数芯片支持这种方法,一般还是从一个寄存器里取出一排bit的值,然后自己移位赋值,然后再把这一整排bit赋值回寄存器】
要驱动一个LED,也是搞这些寄存器【我主要还是纳闷怎么编译到内核里去】
老熟人了:时钟使能、对应引脚使能、GPIO属性设置 【IO的速度是指电平跳变的速度,速度快的同时信号干扰也会变大】、IO电平的置位/清零,PPT最后一张图是上图提到的另一种置位/清零方式。
把上面两节的内容融合一下 就能实现:用户代码 - 内核代码 - 硬件 这一连串的控制。
用户代码 <=> 内核代码 需要copy_to_user() copy_from_user() 来进行数据交换
内核代码 <=> 硬件 需要虚拟地址对物理地址进行映射 virt_addr = ioremap(phy_addr)
先将寄存器作为全局变量定义下来。这时候只是一个普通的指针,我们未来写程序要用到。
注释里的地址是从手册查到的,下一步要用来初始化指针。
加volatile 是为了在代码里对这个变量快速多次赋值的时候不被编译器优化掉。
在入口函数做地址映射,也就是说在这个驱动申请到主设备号并且挂载到dev目录下的时候,我们把物理地址映射到 我们程序的指针里。
不要在init里使能时钟。本来使能时钟这个设置就是为了省电才设计的。结果你一装上驱动就给你使能了,装上驱动又不是当场就要用到这个设备。open了才是马上要用。
已知一个程序有一个全局变量,开两个进程同时运行这个程序,并打印出变量地址。
这两个地址是一样的。
而在物理地址上,两个进程显然需要两个不同的内存去存储全局变量。
真相只有一个,打印出来的地址是虚拟地址,这个虚拟地址结合进程号才能找到真实的物理地址。干这事的就是MMU【内存管理】
总之就是这样啦。
我寻思着应该也能用
*GPIOA_MODER &= ~(1<<21);
*GPIOA_MODER |= (1<<20);
这样来实现吧。不知道为什么大佬们都不这么写呢。
这个专门用来置位/清零的寄存器就是好,不需要操作的bit肯定是0,不用顾忌,所以可以直接赋值。
GPIOA有16个引脚,所以BSRR这个寄存器的0-15bit是用来给对应引脚清零的 16-32bit是用来给对应引脚置1的.
这样驱动就写好了,记得上一节的流程,搞这个结构体。咱这驱动就open和write函数就能用了。
【韦老师没有写两个设备的代码,我得自己猜,好慌,下文内容不保证对】
每次新写一个设备驱动都要从头写那些流程:申请主设备号、申请挂载点,都是一样的。
但,要是我只是想点另外一盏灯呢?那就没必要申请新的主设备号了。但是不得不挨个改寄存器对应引脚,每个灯都申请自己的次设备号。
这样一般是写一大堆宏吗?感觉也挺麻烦的?而且还不是得去代码里改么。。
或者模板写一个.c文件;对应设备的具体的驱动函数 【就是file_operation结构体里的那些】比如write 我就写在另一个.c文件.【每盏灯用不同的.c文件?不是的!只是多写一份.c文件而已!4个灯就用if-else去换到对应的寄存器!】【图片里换了.c文件是因为人家换掉了整个开发板,芯片手册都不一样了!】
led_ctl() 就是之前写的那个write()
输入参数which是指明用哪个灯,所以就很明显了,换灯的话就是在这里添加代码控制寄存器。
led_init()
哇塞,这时候怎么在init函数里给时钟使能了,这不打脸吗。
没打脸,小兄弟。这个在驱动文件那边是从open()函数里调用的。所以并不是使能挤进init,而是这个init挤进了使能哦。
而且为了防止重复进行映射,所以要检查一下这个指针是不是之前已经被赋值过了。
但是写好的函数怎么对接到leddev.c的驱动流程里呢?
韦老师说不能直接把结构体拿过去用,不合规矩。是怕全局变量到处跑吗?
对,如果直接把变量拿过去用,那各个子函数里都需要出现这个跨文件的结构体,万一需要换一个结构体,那就得挨个替换了。
如果是先传回一个地址,然后用替身去处理的话,模板文件leddev.c就只需要改那一个结构体。
如果是用函数传回那个地址,就完全不需要修改模板文件,只要在自定义文件里改返回值就行,太绝了。。。。。哦好吧,函数名和对应头文件好像还是得改。。
反正不能写extern 跨文件给变量,得记着了。
leddev.c里也有一个结构体led_dev哇,这不巧了嘛,是不是赋值给那个结构体哇?【笑死,这俩结构体不能混用的!醒醒!】
从board_X.c返回的结构体句柄是给了leddev.c里的一个全局指针p_led_opr,然后是直接放进leddev.c的函数里面调用一下结构体里的函数就可以了!
还记得which是指灯的编号吗,那同时也是次设备号。
那个led_dev是用来申请主设备号的!咱所有的LED用的是同一个主设备号,但是我们每个灯要各自申请一个次设备号。具体要几个灯是由我们在board_X.c赋值的num决定的。
按需申请的次设备会挨个挂载到我们的dev/路径下!
在我们需要点亮某个灯的时候,我们输入的路径就得详细到灯,后续从内核的结构体里可以查到次设备号,然后内核就能调用对应的函数。
来,看一看
board_X.c的实现寄存器操作结构体
和
leddev.c的用来注册主设备号的结构体
iminor()函数是用来读取出次设备号的【截图里是错的,应该写node】,为什么node里会有次设备号信息咧,因为我们运行程序的时候就会输入dev/led0这样的路径,这个路径和当时挂载的次设备号是绑定的。
再看一看实现的write()函数
输入里没有inode这个参数,我们可加不了输入参数的类型,只能参照内核的代码,发现file里有inode ,这样就能获得次设备号了。
然后再从用户空间输入的buf里取出用户指定的LED状态
就可以传给board_X.c里的函数执行了。
需要board_X.c里的结构体【leddev.c里也会用到的】
和
传给leddev.c用的子函数
上一小节里,board_X.c的寄存器操作还是有很多重复代码。
如果我们要换一个灯,那引脚寄存器都是要成套地挪动一点的。
就比如下图这个代码,就得else if(which == 1) {*GPIOA_MODER &= ~(3<<22)}
重复,且不直观,需要重新查阅手册。
上一小节是把驱动文件分成了2个.c文件
现在要分成3个.c文件,把这个寄存器操作给封装起来。
寄存器封装层 board_X.c
驱动函数封装层 chip_gpio.c
内核驱动注册层 leddev.c
chip_gpio.c现在就只需要给定【我要点亮的是哪个灯,要设置哪些引脚】就可以了,他不用管哪个寄存器究竟移了多少位。board_X.c降格成了resource资源文件,就是要把置位和找对应位置这俩动作搞成直观一点的宏或者函数。
当然了,也是用结构体进行封装【用结构体封装就是所谓的面向对象编程了,他们真喜欢搞一些高大上的名词。】
GPIO的定位有GPIOA3这样用字母+数字的组合,也有GPIO3_0这样数字跟数字的组合。总之是需要两组信息来确认一个引脚。【时钟使能、引脚使能、引脚属性、引脚置位/清零操作】这整套流程,就只需要知道group,pin和引脚属性这三个信息。group,pin的指定完全可以自动化,被封装到board_X.c里去。
封装当然可以用
struct led_resource
{
int group;
int pin;
};
在寄存器封装层board_X.c文件里初始化就是下图这样的了。
在驱动函数封装层chip_gpio.c就这样用
switch里面用的是led_rsc这个全局变量
韦老师没有再具体下去了
【这里看着有点乱没关系,这里只是过渡用的,下一小节我梳理得很清楚。
每个芯片上有灯、蜂鸣器、LCD等不同资源,每个资源要3个.c文件,不同的芯片也都要写不同的驱动。这些.c文件全都要放到内核里去编译运行。非常糟糕。
但是这三个文件的功能区分还是很有必要的,只是没必要把那些.c文件作为内核的一部分,嫌弃。
内核虽然不可能统一规划那些外接设备里的寄存器是怎么操作的,但是只要是LCD设备,那各种厂商的LCD所需要的资源和操作肯定都差不多,只是具体的寄存器地址不一样而已。
因此内核可以像之前那样,先搞个结构体,留有【IO端口资源物理地址范围】【中断资源物理地址范围】【资源数量】之类的成员变量,把大概的操作抽象到【用这个寄存器】这个程度。【是platform_device结构体】
新来一个chip.c文件向下拿资源,向上拿设备编号,进行切实操作。而且这个可以不开源。
board_X.c降格成资源文件,实打实拿着底层寄存器信息,有就是有,没有就是没有
把leddev.c搞成傀儡皇帝,他负责向内核申请调度,实际操作都是拿现成的
【这个.c的文件名随便起,不用太纠结这里,我本来想把board_X.c说成是实权驱动文件的,但是怕大家看韦老师的源码被我弄糊涂,只好照实讲。我下文就不提这名字了,直接按结构体来区分,更清楚】
所以现在要向内核注册__init 三个.ko文件了。实际上挂载的主设备仍然是那个傀儡file_operations结构体,但另外要向内核申请注册一个平台设备结构体platform_device和一个平台驱动结构体platform_driver。然后内核里有一个bus结构体专门负责匹配平台设备和平台驱动
【在后续,还会有一个dts文件,它编译成dtb文件,然后传给内核,内核能解析出platform_device来用,这篇文章里不细讲】
init() 获取 从开源驱动文件传来的 次设备号,调用设备资源文件封装好的宏,进行具体的初始化操作:包括时钟使能和对应引脚的属性设置
ctl()获取 从开源驱动文件传来的 次设备号和用户的控制指令,进行具体的控制操作
上面俩函数封装到operations结构体【在开源驱动文件里会被用到】
probe() 通过内核函数获取设备资源的引脚资源,存到全局变量 并把次设备号传给开源驱动文件,让其挂载到dev/路径下
remove() 把次设备号传给开源驱动文件,让其卸载掉dev/路径下的此设备
上面俩函数+驱动文件名 封装到platform_driver结构体【在__init()里就立刻被初始化】
__init()内核的.ko初始化函数 注册函数是platform_driver_register 注册platform_driver结构体 ;把operations结构体发到开源驱动文件里
__exit()内核的.ko卸载函数 注销platform_driver结构体
release() 韦老师说如果没有这个函数会有问题。
resource 具体的引脚资源的指定,实权驱动文件会来获取,新增设备时只需要添加这里的信息。
上面的空函数和资源结构体+设备文件名 封装到platform_device结构体
__init()内核的.ko初始化函数 注册函数是platform_device_register 注册platform_device结构体
__exit()内核的.ko卸载函数 注销platform_device结构体
create_device() 用内核的class和内核的函数挂载驱动
destroy_device() 用内核的class和内核的函数卸载驱动
operations() 从实权驱动文件取来operation结构体,存到全局变量
上面仨函数传给实权驱动文件,让其调用
write() 从内核变量file获取次设备号,用内核函数从用户空间获取控制指令;调用operation结构体的控制函数
open() 从内核变量inode获取次设备号;调用operation结构体的初始化函数
上面俩函数+内核的宏 封装到file_operations结构体
__init()内核的.ko初始化函数 注册函数是register_chrdev 注册file_operations结构体 获取主设备号;用内核函数创建class,等着实权驱动文件挂载的时候用
__exit()内核的.ko卸载函数 注销file_operations结构体 删掉class
Linux内核API之class_create与class_destroy【就是创建路径文件夹用的】
【我自个儿琢磨着,既然傀儡从用户空间拿控制指令,那如果设备要传一些数据给用户应该也是在傀儡文件里调用内核函数赋值到用户层里去吧
笑死,看了一下我之前写的应用基础,不是那么搞的
是直接在evdev_do_ioctl()函数里就switch分类 然后用内核函数赋值给用户层了。
也不知道这个函数被封装在哪个步骤里【还不死心】
】
驱动开发最主要需要学习的是系统里的驱动框架是怎么做的,然后按人家的规则办事。
就像我上文说的
类似于内核里规定了驱动文件的结构体【里面给了一些函数让你挑着实现,但不能修改它的输入参数,也不能增加新的函数成员】
现在内核里又规定了一个platform_device 结构体【可以用来表示设备的所有资源】 结构体里面的.resource 也是内核先设定好的结构体,如果我们要使用,就得按照其规则进行赋值。
【左边两个结构体同在platform_device 文件里,右边这个结构体在platform_driver文件里,注意:name是在平台驱动里的驱动成员里的名字,而平台设备结构体里直接就是名字】
resource结构体要指明寄存器的起始地址
资源的名字
资源的类别flags 查下表。【就算自己写的时候找不到对应分类也没事,关键是要和驱动文件那边的查询要一一对应。】
更自由一点的方法就是把数据放在.platform_data【私有数据】里,它可以存放各种类型的数据
下面两个设备结构体就是具体芯片用的结构体,他们的名字相同,他们的驱动文件是一样的
但id是他们自己的标志,是必须不同的。【啧】
下图是驱动文件的结构体,它的名字和上面两种芯片设备起的名字一样,可以匹配并使用【下文还会提到驱动文件里还会保存一个可支持的设备的列表名称可以查询,当然这个列表是可以缺省的,可以直接匹配名字】
在 驱动文件的结构体platform_driver这个结构体的probe里,向平台设备文件探查资源,同时probe也负责让傀儡把设备挂载到dev/目录下【毕竟挂载的数量是靠recource的数量决定的】【这里如果要调用傀儡那边的函数,需要傀儡那边EXPORT_SYMBOL(函数名); ,就类似extern这个效果】
诶,这里可以直接用platform_device具体芯片文件传来的句柄啊,那就是说只有这个句柄的.source成员要靠内核函数来获取。
以前是将两三个.c文件编译后共同注册到一个.ko文件里去对吧
但是现在要注册3个.ko文件,用的函数也跟文章开头的那个不一样了喔。
这里注册的是设备文件,同时把具体操作交给傀儡。
这里注册的是实权驱动文件
这是内核里的,这个__init函数好复杂
这里注册的是傀儡驱动文件
内核系统里有一个虚拟的总线,它会分别挂载设备文件platform_device和驱动文件platform_driver.
当我们注册一个平台设备的时候,总线会把现存的平台驱动所支持的设备列出来进行对比,再不济就把所有的驱动名都拉出来对比,挨个进行字符串匹配。
下面这个函数返回的是已经成功匹配到的driver,如果匹配不成功,中途就return 0 了
【见鬼,一路跟下来全是if-else 和函数调用。没看到任何的for/while/next 。内核究竟是怎么遍历driver的。
驱动文件platform_driver.有这个驱动自己的名字,而且还有一个结构体里面保存着这个驱动支持的各类设备的名单。
设备文件platform_device则有自己的名字,少数的设备文件有自己指定的白马王子,但大部分设备文件还是提交自己的名字上去和驱动文件进行匹配。
用BUS那边的结构体里的.match进行匹配。匹配的流程是:
就调用实权驱动里的probe函数进行挂载。
在加载.ko文件时 实权驱动文件必须在最后加载,因为它用到了另外两个文件的函数
【因此,傀儡那边不能像以前那样主动调用别人的函数,用返回值来获得结构体句柄;而是傀儡自己写一个函数,自己的全局变量在函数里被赋值,等别人调用这个函数。不这样改的话会造成交叉依赖
如果需要加一个灯,就只需要打开platform_device文件,在这个resources加这几行就可以了,别的代码完全不用改。
重新编译,卸载原来的ko文件rmmod,重新加载ko文件insmod
就好了
insmod / modprobe 加载驱动
rmmod 卸载驱动
lsmod 查看系统中所有已经被加载了的所有的模块以及模块间的依赖关系
modinfo 获得模块的信息
lsmod 能够显示驱动的大小以及被谁使用
cat /proc/modules 能够显示驱动模块大小、在内核空间中的地址
cat /proc/devices 只显示驱动的主设备号,且是分类显示
/sys/modules 下面存在对应的驱动的目录,目录下包含驱动的分段信息等等。
cd /sys/bus/ ls
cd /dev/
linux驱动加载模块查看命令