linux设备驱动程序——将驱动程序编译进内核
linux驱动程序——将驱动程序编译进内核
模块的加载
通常来说,在驱动模块的开发阶段,⼀般是将模块编译成.ko⽂件,再使⽤
sudo insmod
或者
depmod -a
modprobe module
将模块加载到内核,相对⽽⾔,modprobe要⽐insmod更加智能,它会检查并⾃动处理模块的依赖,⽽insmod出现依赖问题时仅仅是告诉你
安装失败,⾃⼰想办法吧。
将模块编译进内核
这⼀章节我们并不关注模块的运⾏时加载,我们要讨论的是将模块编译进内核。
在学习内核的Makefile规则的时候就可以知道,将驱动程序编译成模块时,只需要使⽤:
obj-m += module.o
指定相应的源代码(源代码为module.c)即可,所以很多朋友就简单地得出结论:如果要将模块编译进内核,只要执⾏下⾯的的指令就可以
了:
obj-y += module.o
事实上,这样是⾏不通的,要明⽩怎么将驱动程序编译进内核,我们还是得先了解linux源码的编译规则。
关于linux源码的编译规则和部分细节可以查看我的另⼀篇博客
本篇博客的所有实验基于平台,开发板,内核版本为
armbeagle bone4.14.79
编译平台
注:在以下的讨论中,⽬标主机和本机指加载运⾏驱动程序的机器,是开发的对象。⽽开发机指只负责编译的机器,⼀般指机。
PC
本机编译
在对驱动程序进⾏编译时,⼀般会有两种不同的做法:
直接在⽬标主机上编译
在其他平台上构建交叉编译环境,⼀般是在PC机上编译出可在⽬标板上运⾏的驱动程序
直接在⽬标主机上编译是⽐较⽅便的做法,本机编译本机运⾏。
通常,本机系统中⼀般不会⾃带linux内核源码的头⽂件,我们需要做的就是在系统中安装头⽂件:
sudo apt-get install linux-headers-$(uname -r)
$(uname -r)获取当前主机运⾏的linux版本号。
有了头⽂件,那么源代码从哪⾥来呢?答案是并不需要源代码,或者说是并不需要C⽂件形式的源代码,⽽是直接引⽤当前运⾏的镜像,在
编译时,将/boot/vmlinuz-$(version)镜像当成库⽂件进⾏链接即可。
值得注意的是,/boot/vmlinuz-$(version)是linux启动时读取的镜像,但是在本机中进⾏驱动程序编译的时候并不会影响到这个镜像,换句话
说,即使是指定了obj-y,驱动程序也不会编译到/boot/vmlinuz-$(version)镜像中,⾃然达不到将驱动编译进内核的效果。
交叉编译
本机(⽬标机)编译是⽐较⽅便的,但是⽆法改变⽣成的镜像,当然也可以将源码下载到本机(⽬标机)中进⾏编译,就可以⽣成相应的linux镜
像。
但是⼀般情况下,在嵌⼊式开发中,不论是⽹络、内存还是执⾏速率,⽬标主机的性能⼀般不会太⾼,如果需要编译完整的源码时,⽤户会
更倾向于在PC端构建编译环境以获取更好的编译性能。
选择在开发机上编译⽽不是本机编译时,需要注意的⼀点就是:通常嵌⼊式开发都是基于arm、mips等嵌⼊式架构,⽽PC常⽤X86架构,在
编译时就不能使⽤开发机上的gcc编译器,因为开发机上编译器是针对开发平台(X86),⽽⾮运⾏平台(arm、mips),所以需要使⽤交叉编译
⼯具链,同时在编译时指定运⾏的主机平台。
指令是这样的:
make arch=arm CROSS_COMPILE=$COMPILE_PATH/$COMPILE_TOOL
也可以在makefile中给相应的arch和CROSS_COMPILE变量赋值,直接执⾏make指令即可。
显然,这种交叉编译⽅式是对linux内核源码的完整编译,主要⽣成这⼀些⽬标⽂件:
⽣成linux的可启动镜像,通常是zImage或者vmlinuz,这是⼀个可boot执⾏的压缩⽂件
伴随着的还有镜像对应的map⽂件,这个⽂件对应镜像中的编译符号以及符号的地址信息
未编译进内核的模块,也就是在配置时被选为M的选项
linux内核头⽂件等等
在上⽂中有提到,⽬标主机中,linux的启动镜像放置在/boot⽬录下,所以如果我们需要替换linux的镜像,需要替换/boot⽬录下的以下两个
⽂件:
linux的可启动镜像,也就是⽣成的zImage或者vmlinuz
.map⽂件
在主机中,模块⼀般被放置在/lib/modules⽬录中,如果交叉编译出的版本与本机中模块版本不⼀致,将⽆法识别,所以编译出的模块也需
要替换。
驱动程序编译进内核
根据上⽂,可以得出的结论是:在试图将驱动程序编译进内核时,我们需要编译完整的linux内核源码以⽣成相应的镜像⽂件,然后将其替换
到⽬标主机的/boot⽬录下即可。
那么,怎样将驱动的源码C⽂件编译进内核呢?
这个问题得从makefile的执⾏流程说起:
make的执⾏
⾸先,如果你有基本的linux内核编译经验,就知道在编译linux源码前,需要进⾏config(配置),以决定哪些部分编译进内核、哪些部分编译
成模块,
通常使⽤make menuconfig,不同的config⽅式通常只是选择界⾯的不同,其中稍微特殊的make oldconfig则是沿⽤之前的配置。
在配置完成之后将⽣成⼀个.config⽂件,makefile根据.config⽂件选择性地进⼊⼦⽬录中执⾏编译⼯作。
流程基本如上所说,但是我们需要知道更多的细节:
make menuconfig执⾏的原理是什么?
顶层makefile是怎样执⾏⼦⽬录中的编译⼯作的?
答案是这样的:
make menuconfig肯定是读取某个配置⽂件来陈列出所有的配置选项,递归地进⼊到⼦⽬录中,发现⼏乎每个⼦⽬录中都有⼀个名为
Kconfig的⽂件,这个⽂件负责配置驱动在配置菜单中的显⽰以及配置⾏为,且Kconfig遵循某种语法,make menuconfig就是读取这些
⽂件来显⽰配置项。
递归地进⼊到每个⼦⽬录中,发现其中都有⼀个makefile中,可以想到,makefile递归地进⼊⼦⽬录,然后通过调⽤⼦⽬录中的
makefile来执⾏各级⼦⽬录的编译,最后统⼀链接。
整个linux内核的编译都是采⽤⼀种分布式的思想,需要添加⼀个驱动到模块中,我们需要做的事情就是:
1. 将驱动源⽂件放在内核对应⽬录中,⼀般的驱动⽂件放在drivers⽬录下,字符设备放在drivers/char中,块设备就放在drivers/blok中,
⽂件的位置遵循这个规律,如果是单个的字符设备源⽂件,就直接放在drivers/char⽬录下,如果内容较多⾜以构成⼀个模块,则新建
⼀个⽂件夹。
2. 如果是新建⽂件夹,因为是分布式编译,需要在⽂件夹下添加⼀个Makefile⽂件和Kconfig⽂件并修改成指定格式,如果是单个⽂件直
接添加,则直接修改当前⽬录下的Makefile和Kconfig⽂件将其添加进去即可。
3. 如果是新建⽂件夹,需要修改上级⽬录的Makefile和Kconfig,以将⽂件夹添加到整个源码编译树中。
4. 执⾏make menuconfig,执⾏make
5. 将⽣成的新镜像以及相应boot⽂件拷贝到⽬标主机中,测试。
beagle bone的启动⽂件包括:vmlinuz、,编译出的模块⽂件为modules
Kconfig的语法简述
上⽂中提到,在添加源码时,⼀般会需要⼀个Kconfig⽂件,这样就可以在make menuconfig时对其进⾏配置选择,在对⼀个源⽂件进⾏描述
时,遵循相应的语法。
在这⾥介绍⼀些常⽤的语法选项:
source
source:相当于C语⾔中的include,表⽰包含并引⽤其他Kconfig⽂件
config
新建⼀个条⽬,⽤法:
source drivers/xxx/Kconfig
config TEST
bool "item name"
depends on NET
lect NET
help
just for test
config是最常⽤的关键词了,它负责新建⼀个条⽬,对应linux中的编译模块,条⽬前带有选项。
config TEST:
config后⾯跟的标识会被当成名称写⼊到.config⽂件中,⽐如:当此项被选择为[y],即编译进内核时,最后会在.config⽂件中添加这样⼀个
条⽬:
CONFIG_TEST=y
CONFIG_TEST变量被传递给makefile进⾏编译⼯作。
bool "item name":
其中bool表⽰选项⽀持的种类,bool表⽰两种,编译进内核或者是忽略,还有另⼀种选项就是tristate,它更常⽤,表⽰⽀持三种配置选项:
编译进内核、编译成可加载模块、忽略。⽽item name就是显⽰在menu中的名称。
depends on:
表⽰当前模块需要依赖另⼀个选项,如果另⼀个选项没有没选择编译,当前条⽬选项不会出现在窗⼝中
lect
:
同样是依赖相关的选项,表⽰当前选项需要另外其他选项的⽀持,如果选择了当前选项,那么需要⽀持的那些选项就会被强制选择编译。
help
:
允许添加⼀些提⽰信息
menu/menuend
⽤法:
menu "Test option"
...
endmenu
这⼀对关键词创建⼀个选项⽬录,该选项⽬录不能被配置,选项⽬录中可以包含多个选项
menuconfig
相当于menu+config,此选项创建⼀个选项⽬录,⽽且当前选项⽬录可配置。
编译⽰例
梳理了整个添加的流程,接下来就以⼀个具体的⽰例来进⾏详细的说明。
背景如下:
⽬标开发板为开源平台beagle bone,基于4.14.79内核版本,arm平台
需要添加的源代码为字符设备驱动,名为cdev_test.c,新建⼀个⽬录cdev_test
字符设备驱动实现的功能:在/dev⽬录下⽣成⼀个basic_demo⽂件,⽤来检测是否成功将源代码编译进内核
将驱动编译进内核
放置⽬录
鉴于是字符设备,所以将源⽂件⽬录放置在$KERNEL_ROOT/drivers/char/下⾯。
如果是块设备,就会被放置在block下⾯,但是这并不是绝对的,类似USB为字符设备,但是独⽴了⼀个⽂件出来。
放置后⽬标⽂件位置为:$KERNEL_ROOT/drivers/char/cdev_test
Kconfig⽂件
在$KERNEL_ROOT/drivers/char/cdev_test⽬录下创建⼀个Kconfig⽂件,并修改⽂件如下:
menu "cdev test dir"
config CDEV_TEST
bool "cdev test support"
default n
help
just for test ,hehe
endmenu
根据上⽂中对Kconfig⽂件的语法描述,可以看出,这个Kconfig⽂件的作⽤就是:
1. 在menuconfig的菜单中,在Device Driver(对应drivers⽬录) ---> Character devices(对应char⽬录)菜单下创建⼀个名为"cdev test dir"的
菜单选项,
执⾏效果是这样的
2. 在"cdev test dir"的菜单选项下创建⼀个"cdev test support"的条⽬,这个条⽬的选项只有两个,[*]表⽰编译进内核和[]表⽰不进⾏编译,
默认选择n,不进⾏编译。
执⾏效果是这样的
3. 选择help可以查看相应的提⽰信息。
在上⽂中还提到,Kconfig分布式地存在于⼦⽬录下,同时需要注意的是,在编译时,配置⼯具并⾮⽆差别地进⼊每个⼦⽬录,收集所有的
Kconfig信息,⽽是遵循⼀定的规则递归进⼊。
那么,既然是新建的⽬录,怎么让编译器知道要进⼊到这个⼦⽬录下呢?答案是,在上级⽬录的Kconfig中包含当前路径下的Kconfig⽂件。
打开char⽬录下的Kconfig⽂件,并且在⽂件的靠后位置添加:
source "drivers/char/xillybus/Kconfig"
就把新的Kconfig⽂件包含到系统中检索⽬录中了,那么drivers/char/⼜是怎么被检索到的呢?
就是在drivers的Kconfig中添加drivers/char/⽬录下的Kconfig索引,以此类推。
Makefile⽂件
在$KERNEL_ROOT/drivers/char/cdev_test⽬录下创建⼀个Makefile⽂件,并且编译Makefile⽂件如下:
obj-$(CONFIG_CDEV_TEST) += cdev_test.o
表⽰当前⼦⽬录下Makefile的作⽤就是将cdev_test.c源⽂件编译成cdev_test.o⽬标⽂件。
值得注意的是,这⾥的编译选项中使⽤的是:
obj-$(CONFIG_CDEV_TEST)
⽽⾮
obj-y
如果确定要将驱动程序编译进内核永远不变,那么可以直接写死,使⽤obj-y,如果需要进⾏灵活的定制,还是需要选择第⼀种做法。
CONFIG_CDEV_TEST是怎么被配置的呢?在上⽂提到的Kconfig⽂件编写时,有这么⼀⾏:
config CDEV_TEST
...
在Kconfig被添加到配置菜单中,且被选中编译进内核时,就会在$KERNEL_ROOT/.config⽂件中添加⼀个变量:
CONFIG_CDEV_TEST=y
⾃动添加CONFIG_前缀,⽽名称CDEV_TEST则是由Kconfig指定,看到这⾥,我想你应该明⽩了这是怎么回事了。
是不是这样就已经将当前⼦⽬录添加到内核编译树中了呢?其实并没有,就像Kconfig⼀样,Makefile也是分布式存在于整个源码树中,顶层
makefile根据配置递归地进⼊到⼦⽬录中,调⽤⼦⽬录中的Makefile进⾏编译。
同样地,需要修改drivers/char/⽬录下的Makefile⽂件,添加⼀⾏:
obj-$(CONFIG_CDEV_TEST) += cdev_test/
在编译时,如果CONFIG_CDEV_TEST变量为y,cdev_test/Makefile就会被调⽤。
在make menuconfig中选中
⽣成配置的部分完成,就需要在menuconfig菜单中进⾏配置,执⾏:
make menuconfig
进⼊⽬录选项Device Driver --> Character devices--->cdev test dir.
然后按'y'选中模块cdev test support
保存退出,然后执⾏编译:
make
拷贝到⽬标主机
将vmlinuz(zImage)、拷贝到⽬标主机的/boot⽬录下。
在编译⽣成的modules拷贝到⽬标主机的/lib/modules⽬录下。
需要注意的是:启动⽂件也好,模块也好,在⽬标板上很可能⽂件名为诸如vmlinuz-$version,会包含版本信息,需要将⽂件名修改成⼀
致,不然⽆法启动。对于模块⽽⾔,就是相应模块⽆法加载。
验证
最后⼀步就是验证⾃⼰的驱动程序是否被编译进内核,如果被编译进内核,驱动程序中的module_init()程序将被系统调⽤,完成⼀些开发者
指定的操作。
这⼀部分的验证操作就是各显⾝⼿了。
好了,关于linux将驱动程序编译进内核的讨论就到此为⽌啦,如果朋友们对于这个有什么疑问或者发现有⽂章中有什么错误,欢迎留⾔
原创博客,转载请注明出处!
祝各位早⽇实现项⽬丛中过,bug不沾⾝.
本文发布于:2023-05-23 20:00:34,感谢您对本站的认可!
本文链接:https://www.wtabcd.cn/zhishi/a/1684843235175982.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文word下载地址:linux设备驱动程序——将驱动程序编译进内核.doc
本文 PDF 下载地址:linux设备驱动程序——将驱动程序编译进内核.pdf
留言与评论(共有 0 条评论) |