Linux驱动入门(四)——构建第一个驱动程序

文章目录

  • 前言
  • 开发环境配置之内核升级
    • 为什么升级内核
    • 内核升级
  • Hello world 驱动程序
    • 驱动模块的组成
    • Hello World模块
    • 编译Hello World模块
    • 模块的操作
    • Hello World模块加载后文件系统的变化
  • 模块参数和模块之间通信
    • 模块参数
    • 模块的文件格式ELF
    • 模块之间的通信
    • 模块之间的通信实例
  • 将模块加入内核
    • 向内核添加模块
    • Kconfig
    • Kconfig语法
    • 应用实例:在内核中新增加add_sub模块
    • 对add_sub模块进行配置
  • 小结


前言

开发板以STM32MP157为例进行实验
万事开头难,写驱动程序也是一样,本章将构建第一个驱动程序。驱动程序和模块的关系非常密切,所以这里将详细讲解模块的相关知识。而模块编程成败与否的先决条件是要有统一的内核版本,所以这里将讲解怎样升级内核版本。最后为了提高程序员的编程效率,这里将介绍两种集成开发环境。

开发环境配置之内核升级

构建正确的开发环境,对于写驱动程序非常重要。错误的开发环境,编写的驱动程序不能正确运行。特别是关于内核版本的问题,内核版本不匹配,会使驱动程序不能在系统中运行,所以需要对啮合进行升级。实际以STM32MP157为例,参考教程以``Fedora Core9为例进行内核升级注意区分,首先要说明为什么要升级内核。

为什么升级内核

内核时一个提供硬件抽象层、磁盘及文件系统控制、多任务等功能的系统软件。根据内核是否被修改过,可以将内核分为标准内核和厂商内核两类,如下图
Linux驱动入门(四)——构建第一个驱动程序_第1张图片
标准内核源码和标准内核
标准内核源码是指从kernel.org官方网站下载的标准代码。其是Linux内核开发者经过严格测试所构建的内核代码。标准内核是将标准内核源码编译后得到的二进制映象文件,如上图左半部所示。
厂商内核源码和厂商内核
在某些情况下,发行版厂商会对标准内核源码进行适当的修改,以优化内核的性能。这种经过修改后的标准内核源码,就是厂商修改过的内核源码。将厂商修改过的内核源码编译后,会形成厂商发行版内核。所以,厂商发行版内核是对标准内核的修改和优化。这里,需要注意的是,厂商发行版内核和标准内核对于驱动程序是不兼容的,根据不同内核源码编译的驱动程序是不能互用的。
两者兼容性问题
构建驱动程序模块时,必须考虑驱动程序与内核的兼容性。使用标准内核源码构建内核模块,就是标准内核模块,其不能在厂商内核中使用。使用厂商修改过的内核源码构建的内核模块,就是厂商的内核模块,其不能再标准内核中使用。这里,需要注意的是,即使模块代码相同,标准内核模块和特定厂商的内核模块,其模块格式也是不相同的。
可以通过uname -r命令查看使用的内核版本。

内核升级

尽管不少的开发板中可以使用"软件包管理器工具"对内核进行升级,但毕竟是开发厂商编译的内核有其局限性,里面添加了很多驱动开发系统不需要的模块,而驱动开发需要的模块却没有开启。因此,学会自己手动编译升级也是很有必要的。例如将内核升级为linux 2.6.39.4内核升级的步骤如下所示:

  1. http://www.kernel.org/pub/linux/kernel上下载linux-2.6.29.4.tar.bz2内核源码包。
  2. 使用mkdir linux-2.6.29.4在根目录中建立一个目录
  3. 将压缩包复制到创建的目录中
  4. 进入目录解压源码包
  5. 进入第二层源码目录
  6. 执行make menuconfig配置内核并保存。——详细需要参考厂商或者开发板特性
  7. 编译内核make命令
  8. 编译内核模块make modules
  9. 安装内核模块make modules_install
  10. 安装内核make install
  11. 使用reboot重启计算机,选择新内核启动系统。
    内核升级成功后,可以通过命令uname -r来查看内核版本。
    为了节省时间,可以编写一个shell程序。代码如下:
#! /bin/sh
cd /
mkdir linux-2.6.29.4
cp linux-2.6.29.4.tar.bz2 /linux-2.6.29.4/
cd linux-2.6.29.4
tar -xjvf linux-2.6.29.4.tar.bz2
cd linux-2.6.29.4
make menuconfig
make
make modules
make modules_install
make install
reboot

该shell文件没有可执行权限,需要使用命令chmod让shell文件具有可执行权限,命令如下:

chmod a+x install-new-core

然后执行该shell文件,使升级内核自动进行,使用如下命令:

./install-new-core

注意:对内核的升级并不会破坏现有的内核,也不会破坏系统上的文件等资源。内核升级以后,除了性能上的改变外,对用户来说就像什么也没有发生一样

Hello world 驱动程序

本节将带领读者编写第一个驱动模块,该驱动模块的功能是在加载时,输出"Hello World";在卸载时,输出"Goodbye World"。这个驱动程序模块虽然简单,但是也包含了驱动模块的重要组成部分。在本节的开始,将先对模块的重要组成部分进行介绍。

驱动模块的组成

一个驱动模块主要由如下几部分组成:
Linux驱动入门(四)——构建第一个驱动程序_第2张图片
图中的顺序也是源文件中的顺序。不按照这个顺序来编写驱动模块也不会出错,只是大多数开发人员都喜欢这样的顺序规范。下面对这些结构进行说明。
1、头文件(必须)
驱动模块会使用内核中的许多函数,所以需要包含必要的头文件。有两个头文件是所有驱动模块都必须包含的

#inlcude <linux/module.h>
#inlcude <linux/init.h>

module.h文件包含了加载模块时需要使用的大量符号和函数定义。init.h包含了模块加载函数和释放函数的宏定义。
2、模块参数(可选)
模块参数是驱动模块加载时,需要传递给驱动模块的参数。如果一个驱动模块需要完成两种功能,那么就可以通过模块参数选择使用哪一种功能。
3、模块加载函数(必须)
模块加载函数是模块加载时,需要执行的函数。
4、模块卸载函数(必须)
模块卸载函数是模块卸载时,需要执行的函数
5、模块许可声明(必须)
模块许可声明表示模块受内核支持的程度。有许可权的模块会更受到开发人员的重视。需要使用MODULE_LICENSE表示该模块的许可权限。内核可以识别的许可权限如下:

MODULE_LICENSE("GPL");   /*任一版本的GNU公告许可权*/
MODULE_LICENSE("GPL v2"); /* GPL版本2许可权*/
MODULE_LICENSE("GPL and additional rights"); /*GPL及其附加许可权*/
MODULE_LICENSE("Dual BSD/GPL"); /*BSD/GPL双重许可权*/
MODULE_LICENSE("Dual MPL/GPL"); /*MPL/GPL双重许可权*/
MODULE_LICENSE("Proprietary"); /*专有许可权*/

如果一个模块没有包含任何许可权,那么就会认为是不符合规范的。这时,内核加载这种模块时,会收到内核加载一个非标准模块的警告。开发人员不喜欢维护这种没有遵循许可权标准的内核模块。
以GPL为例,说明许可权的意义。GPL是General Public License的缩写,表示通用公共许可证。GNU通用公共许可证可以保证你有发布自由软件的自由;保证你能收到源程序或者在你需要时能得到它;保证你能修改软件或将它的一部分用于新的自由软件。

Hello World模块

任何一本关于编程的书,几乎都以"Hello World"开始。现在,来看一下最简单的一个驱动模块。

#include    /*定义了一些相关的宏*/
#include  /*定义了模块需要的*/
static int hello_init(void)
{
	printk(KERN_ALERT "Hello World\n"); /*打印 Hello World*/
	return 0;
}
static void hello_exit(void)
{
	printk(KERNEL_ALERT "Goodbye World\n"); /*打印Goodbye World*/
}
module_init(hello_init);   /*指定加载模块函数*/
module_exit(hello_exit);   /*指定模块卸载函数*/
MODULE_LICENSE("Dual BSD/GPL"); /*指定许可权为Dual BSD/GPL*/

源码解析:

  • 1~2行是两个必须的头文件
  • 3~7行是该模块的加载函数,当使用insmod命令加载模块时,会调用该函数。
  • 8~11行是该模块的释放函数,当使用rmmod命令卸载模块时,会调用该函数
  • 12行,module_init是内核模块的一个宏。其用来声明模块的加载函数,也就是使用insmod命令加载模块时,调用的函数hello_init()
  • 13行,module_exit也是内核模块的一个宏。用来声明模块的释放函数,也就是使用rmmod命令卸载模块时,调用的函数hello_exit()
  • 14行,使用MODULE_LICENSE()表示代码遵循的规范,该模块代码遵循BSD和GPL双重规范。这些规范定义了模块在传播过程中的版权问题。

编译Hello World模块

在对Hello World模块进行编译时,需要满足一定的条件。
1、编译内核模块的条件
正确的编译内核模块满足下面一些重要的先决条件:
I.读者应该确保使用正确版本的编译工具、模块工具和其他必要的工具。不同版本的内核需要不同版本的编译工具。
II.应该有一份内核源码,该源码的版本应该和系统目前使用的内核版本一致。
III.内核源码应该至少被编译过一次,也就是执行过make命令。
2、Makefile文件
编译Hello World模块需要编写一个Makefile文件。首先来看一下一个完整的Makefile文件,以便对该文件有整体的认识。

ifeq ($(KERNELRELEASE),)
	KERNELDIR ?= /linux-2.6.29.4/linux-2.6.29.4
	PWD := $(shell pwd)
modules:
	$(MAKE) -C $(KERNELDIR) M= $(PWD) modules
modules_install:
	$(MAKE) -C $(KERNELDIR) M= $(PWD) modules_install
clean:
	rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions
else
	obj-m := hello.o
endif

Makefile解析

  • 1行,判断KERNELRELEASE变量是否为空,该变量是描述内核版本字符串。只有执行make命令的当前目录为内核源代码目录时,该变量才不为空字符。
  • 2~3行定义了KERNELDIRPWD变量。KERNELDIR是内核路径变量,PWD是由执行pwd命令得到的当前模块路径。
  • 4行是一个标识,以冒号结尾,标识Makefile文件的一个功能选项。
  • 5行make的语法是"Make -C 内核路径 M=模块路径 modules"。该语句会执行内核模块的编译
  • 6行和4行标识同样的意思。
  • 7行是模块安装到模块对应的路径中,当在命令执行make modules_install时,执行该命令,其他时候不执行。
  • 8行是删除多余文件标识
  • 9行是删除编译过程的中间文件的命令
  • 11行,意思是将hello.o编译成hello.ko模块。如果要编译其他模块时,只要将hello.o中的hello改为模块的文件名就可以了。

3、Makefile文件的执行过程
Makefile文件的执行过程有些复杂,为了使读者对该文件的执行过程有个清晰的了解,结合下图进行分析。
Linux驱动入门(四)——构建第一个驱动程序_第3张图片
执行make命令后,将进入Makefile文件。此时KERNELRELEASE变量为空,此时是第一个进入Makefile文件。当执行完2,3行代码后,会根据make命令的参数执行不同的逻辑,如下:

  • make modules_install命令,将执行6,7行将模块安装到操作系统中。
  • make clean命令,会删除目录中的所有临时文件
  • make命令,会执行4,5行编译模块。首先$(MAKE) -C $(KERNELDIR) M=$(PWD) modules中的-C $(KERNELDIR)选项,会使编译进入内核源码目录/linux-2.6.29.4/linux-2.6.29.4,读取Makefile文件,并从中得到一些信息,例如变量KERNELRELEASE将在这里被赋值。当内核源码目录中的Makefile文件读取完成后,编译器会根据选项M=$(PWD)第二次进入模块所在的目录,并再次执行Makefile文件。当第二个执行Makefile文件时,变量KERNELRELEASE的值为内核发布版本信息,也就是不为空,此时会执行10、11、12行代码。这里的代码指明了模块源码中各文件的依赖关系,以及要生成的目标模块名,这里就正式编译模块了。

模块的操作

Linux为用户提供了modutils工具,用来操作模块。这个工具集主要包括:

  • insmod命令加载模块。使用insmod hello.ko可以加载hello.ko模块。模块加载后会自动调用hello_init()函数。该函数会打印"Hello World"信息。如果在终端没有看见信息,则这条信息被发送到了/var/log/message文件中。可以使用demsg | tail命令查看文件的最后几行。如果模块带有参数,那么使用下面的格式可以传递参数给模块:insmod 模块.ko 参数1=值1 参数2=值2 参数3=值3 /*参数之间没有逗号*/
  • rmmod命令卸载模块。如果模块没有被使用,那么执行rmmod hello.ko就可以卸载hello.ko模块。
  • modprobe命令是比较高级的加载和删除模块命令,其可以解决模块之间的依赖性问题。将在后面介绍。
  • lsmod命令列出已经加载的模块信息。在insmod hello.ko之前后分别执行该命令就可以知道hello.ko模块是否被加载。
  • modinfo命令用于查询模块相关的信息,比如作者、版权等。

Hello World模块加载后文件系统的变化

当使用insmod hello.ko加载模块后文件系统会发生什么样的变化呢?文件系统存储着有关模块的属性信息。程序员可以从这些属性信息中了解目前模块在系统中的状态,这些状态对开发调试非常重要。
/proc/modules发生变化,在modules文件中会增加如下一行
在这里插入图片描述
这几个字段的信息分别是模块名、使用的内存、引用计数、分隔符、活动状态和加载到内核中的地址。
lsmod命令就是通过读取/proc/modules文件列出内核当前已经加载的模块信息的。lsmod去掉了部分信息,并使显示时更为整齐。执行lsmod命令的结构如下:
在这里插入图片描述
/proc/devices文件没有变化,因为hello.ko模块并不是一个设备模块
在/sys/module/目录会增加hello这个模块的基本信息
在/sys/module/目录下会增加一个hello目录。该目录中包含了一些以层次结构组织的内核模块的属性信息。使用tree -a hello目录可以得到下面的目录结构
Linux驱动入门(四)——构建第一个驱动程序_第4张图片

模块参数和模块之间通信

为了增加模块的灵活性,可以给模块添加参数。模块参数可以控制模块的内部逻辑。从而使模块可以在不同的情况下,完成不同的功能,下面首先对模块参数进行介绍。

模块参数

用于空间的应用程序可以接受用户的参数,设备驱动程序有时候也需要接受参数。例如一个模块可以实现两种相似的功能,这时可以传递一个参数到驱动模块,以决定其使用哪一种功能。参数需要在加载模块时指定,例如insmod xxx.ko param=1
可以使用"module_param(参数名,参数数据类型,参数读写权限)"来为模块定义参数。例如下面代码定义了一个长整型和整型参数:

static long a = 1;
static int b = 1;
module_param(a, long, S_IRUGO);
module_param(b, int , S_IRUGO);

参数数据类型可以是byte、short、ushort、int、uint、long、ulong、bool、charp(字符指针类型)。细心的读者可以看出,模块参数的类型中没有浮点类型。这是因为,内核并不能完美地支持浮点数操作。在内核中使用浮点数时,除了要人工保存和恢复浮点数寄存器外还有一些琐碎的事情要做。为例避免麻烦通常不在内核中使用浮点数。除此之外printk()函数也不支持浮点类型。

模块的文件格式ELF

了解模块以何种格式存储在硬盘中,对于理解模块间怎样通信是非常有必要的。使用file命令可以知道hello.ko模块使用的是ELF文件格式,命令如下:
在这里插入图片描述
此命令在linux驱动开发中经常使用,请读者留意其使用方法。
下图描述的是ELF目标文件的总体结构。图中省去了ELF一些繁琐的结构,把最主要的结构提取出来,形成了下图所示的ELF文件基本结构图。
Linux驱动入门(四)——构建第一个驱动程序_第5张图片

  1. ELF Header头位于文件的最前部。其包含了描述整个文件的基本属性,例如ELF文件版本、目标机器型号、程序入口地址等。
  2. .text表示代码段,存放文件的代码部分。
  3. .data表示数据段,存放已经初始化的数据等。
  4. .Section Table表描述了ELF文件包含的所有段的信息,例如每个段的段名,段的长度、在文件中的偏移、读写权限及段的其他属性。
  5. .symtab表示符号表。符号表是一种映射函数到真实内存地址的数据结构。其就像一个字典,其记录了在编译阶段,无法确定地址的函数。该符号表将在模块文件加载阶段,由系统赋予真实的内存地址。

模块之间的通信

模块是为了完成某种特定任务而设计的。其功能比较单一,为了丰富系统的功能,所以模块之间常常进行通信。它们之间可以共享变量、数据结构,也可以调用对方提供的功能函数。
以下图所示来进行分析模块1是如何调用模块2的功能函数的。为了讲清楚这个过程需要从模块2的加载讲起。
Linux驱动入门(四)——构建第一个驱动程序_第6张图片
模块2的加载过程如下:
(1)使用insmod 模块2.ko加载磨块2
(2)内核为模块2分配空间,然后将模块的代码和数据装入分配的内存中。
(3)内核发现符号表中有函数1、函数2可以导出,于是将其内存地址记录在内核符号表中
模块1在加载进内核时,系统会执行以下操作:
(1)insmod命令会为模块分配空间,然后将模块的代码和数据装入内存中。
(2)内核在模块1的符号表(symtab)中发现一些未解析的函数。上图中这些为解析的函数是“函数1”、“函数2”,这些函数位于模块2的代码中。所以模块1会通过内核符号表,查到相应的函数,并将函数地址填到模块1的符号表中。
通过模块1加载的过程后,模块1就可以使用模块2提供的“函数1”和“函数2”了。

模块之间的通信实例

本实例通过两个模块介绍模块之间的通信。模块add_sub提供了两个导出函数add_integer()sub_integer(),分别完成两个数字的加法和减法。模块test用来调试模块add_sub提供的两个方法,完成加法或减法的操作。
1、add_sub模块
模块add_sub中提供了一个加法函数和一个减法函数,其add_sub.c文件如下:

#inlcude <linux/init.h>
#include 
#include "add_sub.h" 
long add_integer(int a, int b)
{
	return a+b;
}
long sub_integer(int a, int b)
{
   return a-b;
}
EXPORT_SYMBOL(add_integer);        	/*导出加法函数*/
EXPORT_SYMBOL(sub_integer);          /*导出减法函数*/
MODULE_LICENSE("Dual BSD/GPL");

该文件定义了一个加法和减法函数,这两个函数需要导出到内核符号表,才能够被其他模块所调用。EXPORT_SYMBOL就是导出宏。该宏的功能就是让内核知道其定义的函数可以被其他函数使用。
使用EXPORT_SYMBOL使函数变为导出函数是很方便的,但是不能随便使用。一个Linux内核源码中有几百万行代码,函数数以万计,模块中很有可能出现同名函数。幸运的是编译器认为模块中的函数都是私有的,不同模块出现相同的函数名,并不会对编译产生影响,前提是不能使用EXPORT_SYMBOL导出符号。
为了测试模块add_sub的功能,这里建立了另一个test模块。test模块需要知道add_sub模块提供了那些功能函数,所以定义了一个add_sub.h头文件,代码如下:

#ifndef _ADD_SUB_H_
#define _ADD_SUB_H_
long add_integer(int a, int b);
long sub_integer(int a, int b);
#endif

2、test模块
test模块用来测试add_sub模块提供的两个方法,同时test模块也可以接受一个AddOrSub,用来决定是调用add_integer()函数还是sub_integer()函数。当AddOrSub为1时,调用add_integer()函数;当AddOrSub不为1时,调用sub_integer()函数。test模块的代码如下:

#include 
#include 
#include "add_sub.h"      		/*不要使用<>包含文件,否则找不到该文件*/
/*定义模块传递的参数a, b*/
static long a = 1;
static long b = 1;
static int AddOrSub = 1;
static int test_init(void)     /*模块加载函数*/
{
	long result = 0;
	printk(KERN_ALERT "test init\n");
	if(1==AddOrSub)
	{
		result=add_integer(a, b);
	}
	else
	{
		result=sun_integer(a, b);
	}
	printk(KERN_ALERT "The %s result is %ld", AddOrSub==1?"Add":
	"Sub", result);
	return 0;
}
static void test_exit(void)     /*模块卸载函数*/
{
	printk(KERN_ALERT "test exit\n");
}
module_init(test_init);
module_exit(test_exit);
module_param(a, long, S_IRUGO);
module_param(b, long, S_IRUGO);
module_param(AddOrSub, int, S_IRUGO);
/*描述信息*/
MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("Jacky Hu");
MODULE_DESCRIPTION("A module for testing module params and EXPORT_SYMBOL");
MODULE_VERSION("V1.0");

3、编译模块
分别对两个模块进行编译,得到两个模块文件。add_sub模块的Makefile文件与Hello World模块的Makefile文件有所不同。在add_sub模块的Makefile文件中,变量PRINT_INC表示add_sub.h文件所在的目录,该文件声明了add_integer()函数和sub_integer()函数的原型。EXTRA_CFLAGS变量表示在编译模块时,需要添加的目录。编译器会从这些目录中找到需要的头文件。add_sub模块的Makefile如下:

ifeq($(KERNELRELEASE),)
	KERNELDIR ?=/linux-2.6.29.4/linux-2.6.29.4
	PWD := $(shell pwd)
	PRINT_INC = $(PWD)/../include
	EXTRA_CFLAGS += -I $(PRINT_INC)
modules:
	$(MAKE) -I $(PRINT_INC) -C $(KERNELDIR) M=$(PWD) modules
modules_install:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules_install
clean:
	rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_version
.PHYONY:modules modules_install clean
else
	#called from kernel build system: just declare what our modules are
	obj-m := add_sub.o
endif

test模块的Makefile文件如下代码所示。SYMBOL_INC是包含目录,该目录包含了add_sub.h头文件。该文件中定义了两个在test模块中调用的函数。KBUILD_EXTRA_SYMBOLS包含了在编译add_sub模块时,产生的符号表文件Module.symvers,这个文件中列出了add_sub模块中函数的地址。在编译test模块时,需要这个符号表。

obj-m := test.o
KERNELDIR ?= /linux-2.6.29.4/linux-2.6.29.4
PWD := $(shell pwd)
SYMBOL_INC = $(obj)/../include
EXTRA_CFLAGS += -I $(SYMBOL_INC)
KBUILD_EXTRA_SYMBOL=$(obj)/../print/Module.symvers
modules:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
modules_install:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules_install
clean:
	rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions
.PHONY:modules modules_install clean

4、测试模块
在加载test模块之前,需要先加载add_sub模块,test模块才能访问add_sub模块提供的导出函数,命令如下:
在这里插入图片描述
使用insmod加载模块,并传递参数到模块中。参数AddOrSub=2表示执行a-b
Linux驱动入门(四)——构建第一个驱动程序_第7张图片
/sys/module/目录下会创建一个test目录,其中可以清楚地看到paramters下有3个文件,分别表示3个参数。
Linux驱动入门(四)——构建第一个驱动程序_第8张图片

将模块加入内核

当编译了模块后,如果希望模块随着系统一起启动,那么需要将模块静态编译进内核。将模块静态编译入内核,需要完成一些必要的步骤。

向内核添加模块

向Linux内核中添加驱动模块,需要完成3个工作:
(1)编写驱动程序文件
(2)将驱动程序文件放到Linux内核源码的相应目录中,如果没有合适的目录,可以自己建立一个目录存放驱动程序文件。
(3)在目录的Kconfig文件中添加新驱动程序对应的项目编译选择。
(4)在目录的Makefile文件中添加新驱动的编译语句。

Kconfig

内核源码树的目录下都有两个文件KconfigMakefile。分布到各目录的Kconfig文件构成了一个分布式的内核配置数据库每个Kconfig文件分别描述了所属目录源文档相关的内核配置菜单。在内核配置make menuconfig(或xconfig等)时,从Kconfig中读出菜单,用户选择后保存到.config这个内核配置文档中。在内核编译时,主目录中的Makefile调用这个.config文件,就知道用户的选择。
上面的内容说明了,Kconfig就是对应着内核的配置菜单。如果想要添加新的驱动到内核源码中,就需要修改Kconfig文件。
为了使读者对Kconfig文件有一个直观的认识,这里举一个简单的例子,这个例子是IIC驱动。在linux-2.6.29.4/drivers/i2c目录中包含了I2c设备驱动的源代码,其目录结构如下:
Linux驱动入门(四)——构建第一个驱动程序_第9张图片
该目录中包含了一个Kconfig文件,该文件中包含I2C_CHARDEV配置选项。
Linux驱动入门(四)——构建第一个驱动程序_第10张图片
上述Kconfig文件的这段脚本配置了I2C_CHARDEV选项。这个选项tristate是一个三态配置选项,它意味着模块要么编译为内核,要么编译为内核模块。当选项为Y时,表示编译入内核;当选项为M时,表示编译为模块;当选项为N时,表示不编译。如下图所示,“I2C device interface”选项设置为M,表示编译为内核模块。help后面的内容为帮助信息,在单击“快捷键?”时,会显示帮助信息。
Linux驱动入门(四)——构建第一个驱动程序_第11张图片

Kconfig语法

Kconfig语法较为简单,其语法在Documentation/kbuild/kconfig-language.txt文件中做了介绍。归纳起来Kconfig的语法主要包括以下几个方面。
1、主要语法总览
Kconfig配置文件描述了一系列的菜单入口。除了帮助信息之外,每一行都以一个关键字开始,这些关键字如下:

config
menuconfig
choice/endchoice
comment
menu/endmenu
if/endif

前5个关键字都定义了一个菜单选项,if/endif是一个条件选项。下面对常用的一些菜单语法进行说明。
2、菜单入口(config)
大多数内核配置选项都对应Kconfig中的一个菜单,该菜单可以在make menuconfig中显示,写法如下:

config MODVERSIONS
	bool "Set version infomation on all module symbols"
	depends on MODULES
	help
Usually, modules have to be recompiled whenever you switch to a new kernel...

每一行都以关键字开始,并可以有多个参数。config关键字定义一个新的配置选项,之后几行定义了该配置选项的属性。属性可以有类型、输入提示(input prompt)、依赖关系、帮助信息和默认值等。
可以出现两个相同的配置选项,但每个选项只能有一个输入提示并且类型还不能冲突。
每个配置选项都必须指定一种类型,包括bool、tristate、string、hex和int,其中tristate和string是两种基本类型,其他类型都是基于这两种类型的。如下定义的是一个bool类型:
在这里插入图片描述
类型定义后面紧跟输入提示,这些提示将显示在配置菜单中。下面的两种方法可以用来输入提示:
方式1:

bool "Neworking support"

方式2:

bool
prompt "Networking support"

输入提示的一般语法如下:

prompt <prompt> ["if" <expr>]

其中prompt是关键字,表示一个输入提示。是一个提示信息。可选项if用来表示该提示的依赖关系。
默认值的语法如下:

default <expr> [if <expr>]

一个配置选项可以有多个默认值,但是只有第一个默认值是有效的。只有config选项可能配置默认值。
依赖关系的如下:

depends on <expr>

如果定义了多个依赖关系,那么可以用"&&"来连接,表示与的关系。依赖关系可以应用到菜单的所有其他选择中,下面两个例子是等价的。
例子1:

bool "foo" if BAR  #如果定义BAR选项,那么就使能foo选项
default y if BAR  #如果定义BAR选项,那么foo的默认值就是y,表示编译入内核

例子2:

depends on BAR  # foo选项的可配置与否,依赖于BAR选项
bool "foo"
default y

depends能够限定一个config选项的能力,即如果A依赖于B,则在B被配置为Y的情况下,A可以为Y、M、N;在B被配置为M的情况下,A可以为M,N;在B被配置为N的情况下,A只能为N,表示禁用该功能。

help(或者---help---)
begin
...
end

可以用“help”或者“—help—”定义帮助信息。帮助信息可以在开发人员配置内核时给出提示。
3、菜单结构(menu)
菜单结构一般作为菜单入口的父菜单。菜单入口在菜单结构中的位置可由两种方式决定。第一种方式如下:

menu "Network device support"
	depends on NET
config NETDEVICES
...
endmenu

menuendmenu为菜单结构关键字,处于其中的config选项是菜单入口。菜单入口NETDEVICES是菜单结构Network device support的子菜单。depends on NET是主菜单menu的依赖项,只有在配置NET的情况下,才可以配置Network device support菜单项。而且,所有子菜单选项都会继承父菜单的依赖关系,例如,Network device supportNET的依赖将被加到配置选项NETDEVICES的依赖关系中。
第二种方式是通过分析依赖关系生成菜单结构。如果一个菜单选项在一定程度上依赖另一个菜单选项,那么它就成为该选项的子菜单。如果父菜单选项为Y或者M,那么子菜单可见;如果父菜单为N,那么子菜单就不可见。例如:

config MODULES
	bool "Enable loadable module support"
config MODVERSIONS
	bool "Set version information on all module symbols"
	depends on MODULES
comment "module support disabled"
	depends on !MODULES

由语句“depends on MODULES”可知,MODVERSIONS直接依赖于MODULES,所以MODVERSIONSMODULES的子菜单。如果MODULES不为N,那么MODVERSIONS是可见的。
4、选择菜单(choice)
选择菜单定义一组选项。此选项的类型只能是booleantristate型。该选项的语法如下:

"choice"
<choice options>
<choice block>
"endchoice"

在一个硬件有多个驱动的情况下可以使用choice菜单,使用choice菜单可以实现最终只有一个驱动被编译进内核中。choice菜单可以接受的另一个选项是optional,这样选项被设置为N,表示内核没有被选中。
5、注释菜单(comment)
注释菜单定义了配置过程中显示给用户的注释。此注释也可以被输出到文件中,以备查看。注释的语法如下:

comment <prompt>
<comment options>

在注释中唯一可以定义的属性是依赖关系,其他的属性不可以定义。

应用实例:在内核中新增加add_sub模块

下面讲解一个综合实例,假设我们将要在内核中添加一个add_sub模块。考虑add_sub模块的功能,决定将该模块加到内核源码的drivers目录中。在drivers目录中增加一个add_sub_Kconfig子目录。add_sub模块的源码目录add_sub_Kconfig如下:
在这里插入图片描述
在内核中增加了子目录,需要为相应的目录创建KconfigMakefile文件,才能对模块进行配置和编译。同时子目录的父目录中的KconfigMakefile文件也需要修改,以使子目录中的KconfigMakefile文件能够被引用。
在新增加的add_sub_Kconfig目录中,应该包含如下的Kconfig文件:

#
# add_sub configuration
#
menu "ADD_SUB"  #主菜单
	comment "ADD_SUB"
config CONFIG_ADD_SUB    #子菜单,添加add_sub模块的功能
	boolean "ADD_SUB support"
	default y
#子菜单,添加test模块的功能,只有配置CONFIG_ADD_SUB选项时,该菜单才会显示
config CONFIG_TEST
	tristate "ADD_SUB test support"
	depends on CONFIG_ADD_SUB    #依赖CONFIG_ADD_SUB
	default y
endmenu

由于ADD_SUB对于内核来说是新功能,所以首先需要创建一个ADD_SUB菜单;然后用comment显示ADD_SUB,等待用户选中;接下来判断用户是否选择了ADD_SUB,如果选择了ADD_SUB,那么将显示ADD_SUB support,该选项默认设置为Y,表示编译入内核。接下来,如果ADD_SUB support被配置为Y,即变量CONFIG_ADD_SUB=y,那么将显示ADD_SUB test support,此选项依赖于CONFIG_ADD_SUB。由于CONFIG_TEST可以被编译入内核,也可以编译为内核模块,所以这里的选项类型设置为tristate
为了使这个Kconfig文件能起作用,需要修改linux-2.6.29.4/drivers/Kconfig文件,在文件的末尾增加以下内容:

source "drivers/add_sub_Kconfig/Kconfig"

脚本中的source表示引用新的Kconfig文件,参数为Kconfig文件的相对路径名。同时为了使add_subtest模块能够被编译,需要在add_sub_Kconfig目录中增加一个Makefile文件,该Makefile文件如下:

obj-$(CONFIG_ADD_SUB) +=add_sub.o
obj-$(CONFIG_TEST) +=test.o

变量CONFIG_ADD_SUBCONFIG_TEST就是Kconfig文件中定义的变量。该脚本根据配置变量的取值构建obj-*列表。例如obj-$(CONFIG_ADD_SUB)等于obj-y时,表示构建add_sub.o模块,并编译入内核中;当obj-$(CONFIG_ADD_SUB)等于obj-n时,表示不构建add_sub.o模块;当obj-$(CONFIG_ADD_SUB)等于obj-m时,表示单独编译模块,不放入内核中。
为了使整个add_sub_Kconfig目录能够引起编译器的注意,add_sub_Kconfig的父目录drivers中的Makefile也需要增加如下脚本:

obj-$(ADD_SUB) +=add_sub_Kconfig/

在linux-2.6.29.4/drivers/Makefile中添加obj-$(ADD_SUB)+=add_sub_Kconfig/,使得用于在进行内核编译时能够进入add_sub_Kconfig目录中。增加了Kconfig和Makefile文件之后的新的add_sub_Kconfig树形目录如下:
Linux驱动入门(四)——构建第一个驱动程序_第12张图片

对add_sub模块进行配置

当将add_sub模块的源文件加入内核源代码中后,需要对其进行配置,才能编译模块。配置的步骤如下:
(1)在内核源代码目录中执行make menuconfig命令。
在这里插入图片描述
(2)选择Device Drivers选项,再选择Select选项。
Linux驱动入门(四)——构建第一个驱动程序_第13张图片
(3)在进入的界面中选择ADD_SUB选项,该选项就是Kconfig文件中menu菜单定义的,再选择Select选项。
Linux驱动入门(四)——构建第一个驱动程序_第14张图片
(4)如下图所示,有ADD_SUB support和ADD_SUB test support两个选项可以选择。其中ADD_SUB supportADD_SUB test support的父选项,只有在ADD_SUB support选中时,才能对ADD_SUB test support进行选中。图中*表示选中的意思;如果为空,表示不选中。
Linux驱动入门(四)——构建第一个驱动程序_第15张图片

小结

本篇博客主要讲解了怎样构建一个驱动程序。首先讲解了为什么要升级内核。然后对Hello World程序进行了简单介绍。在这个基础上,又详细地讲解了模块之间的通信,这些都是驱动程序开发的基础。最后讲解了怎样将模块加入到内核中,让模块运行起来。

你可能感兴趣的:(Linux驱动开发,linux,stm32,单片机)