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

时间:2023-02-17 23:56:51

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"的菜单选项,

    执行效果是这样的

    linux设备驱动程序——将驱动程序编译进内核
  2. 在"cdev test dir"的菜单选项下创建一个"cdev test support"的条目,这个条目的选项只有两个,[*]表示编译进内核和[]表示不进行编译,默认选择n,不进行编译。

    执行效果是这样的

    linux设备驱动程序——将驱动程序编译进内核
  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将驱动程序编译进内核的讨论就到此为止啦,如果朋友们对于这个有什么疑问或者发现有文章中有什么错误,欢迎留言

原创博客,转载请注明出处!

祝各位早日实现项目丛中过,bug不沾身.

linux设备驱动程序——将驱动程序编译进内核的更多相关文章

  1. 【Linux高级驱动】平台设备驱动机制的编程流程与编译进内核

    [平台设备驱动机制的编程流程] [如何将驱动静态的编译进内核镜像] 1.添加资源(dev-led.c) 1.1:一般来说,系统习惯上将资源放在arch/arm/plat-samsung/目录中 cp ...

  2. 【转】Linux驱动模块编译进内核中

    原文网址:http://blog.chinaunix.net/uid-29287950-id-4573481.html BQ27501驱动编译进内核 一.       驱动程序编译进内核的步骤 在 l ...

  3. 如何编写Linux设备驱动程序

    一.Linux device driver 的概念 系统调用是操作系统内核和应用程序之间的接口,设备驱动程序是操作系统内核和机器硬件之间的接口.设备驱动程序为应用程序屏蔽了硬件的细节,这样在应用程序看 ...

  4. linux设备驱动程序--hello-world

    linux字符设备驱动程序--hello_world 基于4.14内核, beagleBone green平台 PC端的设备驱动程序 有过电脑使用经验的人都知道,当我们将外部硬件设备比如鼠标键盘插入到 ...

  5. Android系统篇之—-编写简单的驱动程序并且将其编译到内核源码中【转】

    本文转载自:大神 通过之前的一篇文章,我们了解了 Android中的Binder机制和远程服务调用 在这篇文章中主要介绍了Android中的应用在调用一些系统服务的时候的原理,那么接下来就继续来介绍一 ...

  6. Linux设备驱动程序学习----1.设备驱动程序简介

    设备驱动程序简介 更多内容请参考Linux设备驱动程序学习----目录 1. 简介   Linux系统的优点是,系统内部实现细节对所有人都是公开的.Linux内核由大量复杂的代码组成,设备驱动程序可以 ...

  7. 【转】6.4.6 将驱动编译进Linux内核进行测试

    原文网址:http://www.apkbus.com/android-98520-1-1.html 前面几节都是将Linux驱动编译成模块,然后动态装载进行测试.动态装载驱动模块不会随着Android ...

  8. 将驱动编译进Linux内核

    *:first-child { margin-top: 0 !important; } body>*:last-child { margin-bottom: 0 !important; } /* ...

  9. Linux 编译安装内核

    一.简介 内核,是一个操作系统的核心.它负责管理系统的进程.内存.设备驱动程序.文件和网络系统,决定着系统的性能和稳定性.Linux作为一个*软件,在广大爱好者的支持下,内核版本不断更新.新的内核修 ...

随机推荐

  1. 【leetcode】Valid Sudoku

    题目简述: Determine if a Sudoku is valid, according to: Sudoku Puzzles - The Rules. The Sudoku board cou ...

  2. Numpy Python

    如果一个数组太长,则NumPy自动省略中间部分而只打印两端的数据: 可通过设置printoptions参数来禁用NumPy的这种行为并强制打印整个数组. set_printoptions(thresh ...

  3. leetcde37. Sudoku Solver

    Write a program to solve a Sudoku puzzle by filling the empty cells. Empty cells are indicated by th ...

  4. Opencv step by step - 配置文件

    有时候,我们需要用配置文件存储一些图像或者视频的信息. 先来一个简单的例子: #include <cv.h> #include <highgui.h> int main(int ...

  5. (二)win7下用Intelij IDEA 远程调试spark standalone 集群

    关于这个spark的环境搭建了好久,踩了一堆坑,今天 环境: WIN7笔记本  spark 集群(4个虚拟机搭建的) Intelij IDEA15 scala-2.10.4 java-1.7.0 版本 ...

  6. LA 4123 &lpar;计数 递推&rpar; Glenbow Museum

    题意: 这种所有边都是垂直或水平的多边形,可以用一个字符串来表示,一个270°的内角记作O,一个90°的内角记作R. 如果多边形内存在一个点,能看到该多边形所有的点,则这个多边形对应的序列是合法的.这 ...

  7. Android-RecyclerView-Item点击事件设置

    在上一篇博客Android-RecylerView初识中提到,RecyclerView不再负责Item视图的布局及显示,所以RecyclerView也没有为Item开放OnItemClick等点击事件 ...

  8. ROS&lpar;indigo&rpar;&lowbar;pr2&lowbar;simulator仿真&lpar;gazebo&rpar;示例

    ROS(indigo)_pr2_simulator仿真(gazebo)示例 1 开启pr2仿真 ~$ roslaunch gazebo_ros empty_world.launch ~$ roslau ...

  9. Java公开课-06&period;单例

    一. 什么是单例模式 因程序需要,有时我们只需要某个类同时保留一个对象,不希望有更多对象,此时,我们则应考虑单例模式的设计. 二. 单例模式的特点 1. 单例模式只能有一个实例. 2. 单例类必须创建 ...

  10. C&num;中用DateTime的ParseExact方法解析日期时间(excel中使用系统默认的日期格式)

    最近做的项目中服务器是英文的系统,系统需要通过excel的单元格导入日期,excel中的日期格式是系统默认的日期格式,如下图所示 以上日期格式,会跟着操作系统设置的日期格式相同例如我的中文系统的日期格 ...