嵌入式Linux 驱动编译添加基本流程,Makefile,Kconfig

安稳与你 提交于 2020-02-17 17:56:25

linux驱动程序——将驱动程序编译进内核

模块的加载

通常来说,在驱动模块的开发阶段,一般是将模块编译成.ko文件,再使用

    sudo insmod module.ko

或者

    depmod -a
    modprobe module

将模块加载到内核,相对而言,modprobe要比insmod更加智能,它会检查并自动处理模块的依赖,而insmod出现依赖问题时仅仅是告诉你安装失败,自己想办法吧。

将模块编译进内核

这一章节我们并不关注模块的运行时加载,我们要讨论的是将模块编译进内核。

在学习内核的Makefile规则的时候就可以知道,将驱动程序编译成模块时,只需要使用:

    obj-m += module.o

指定相应的源代码(源代码为module.c)即可,所以很多朋友就简单地得出结论:如果要将模块编译进内核,只要执行下面的的指令就可以了:

    obj-y += module.o

事实上,这样是行不通的,要明白怎么将驱动程序编译进内核,我们还是得先了解linux源码的编译规则。

关于linux源码的编译规则和部分细节可以查看我的另一篇博客linux内核Makefile概览
本篇博客的所有实验基于arm平台,beagle bone开发板,内核版本为4.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、System.map,编译出的模块文件为modules

Kconfig的语法简述

上文中提到,在添加源码时,一般会需要一个Kconfig文件,这样就可以在make menuconfig时对其进行配置选择,在对一个源文件进行描述时,遵循相应的语法。

在这里介绍一些常用的语法选项:

source

source:相当于C语言中的include,表示包含并引用其他Kconfig文件

config

新建一个条目,用法:

    source drivers/xxx/Kconfig
    config TEST
            bool "item name"
            depends on NET
            select 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:*

表示当前模块需要依赖另一个选项,如果另一个选项没有没选择编译,当前条目选项不会出现在窗口中

select:*
同样是依赖相关的选项,表示当前选项需要另外其他选项的支持,如果选择了当前选项,那么需要支持的那些选项就会被强制选择编译。


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,不进行编译。
    执行效果是这样的

    转存失败重新上传取消

    uploading.4e448015.gif转存失败重新上传取消

    转存失败重新上传取消

  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)、System.map拷贝到目标主机的/boot目录下。

在编译生成的modules拷贝到目标主机的/lib/modules目录下。

需要注意的是:启动文件也好,模块也好,在目标板上很可能文件名为诸如vmlinuz-$version,会包含版本信息,需要将文件名修改成一致,不然无法启动。对于模块而言,就是相应模块无法加载。

验证

最后一步就是验证自己的驱动程序是否被编译进内核,如果被编译进内核,驱动程序中的module_init()程序将被系统调用,完成一些开发者指定的操作。

这一部分的验证操作就是各显身手了。
***

好了,关于linux将驱动程序编译进内核的讨论就到此为止啦,如果朋友们对于这个有什么疑问或者发现有文章中有什么错误,欢迎留言

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!