一、驱动开发前导
1. 前提条件
1.正常运行linux系统的开发板,开发板的内核(zImage)必须是自己编译的(驱动版本与内核版本移植)。
2.内核源码树:也就是在自己编译内核(zImage)时使用的内核源码。 3.nfs挂载的rootfs,在Ubuntu主机中需要搭建一个nfs的服务器(这样会更方便,但不是必须,若不搭建就要每次重新烧录内核、根文件系统)。
2.开发步骤
1.驱动源代码的编写、Makefile的编写、驱动源代码的编译。
2.insmod装载模块,测试模块是否正常运行,rmmod卸载模块。
3.测试通过后将驱动源代码添加到内核,编译成新的zImage。
3.常用的模块操作命令
1.lsmod(list module,将模块列表显示),功能是打印出当前内核中已经安装的模块列表
2.insmod(install module,安装模块),功能是向当前内核中去安装一个模块,用法是insmod xxx.ko
3.modinfo(module information,模块信息),功能是打印出一个内核模块的自带信息。,用法是modinfo xxx.ko
4.rmmod(remove module,卸载模块),功能是从当前内核中卸载一个已经安装了的模块,用法是rmmod xxx(注意卸载模块时只需要输入模块名即可)
- 最简单的驱动源码
/************************************************/
/* file name:module_test.c */
/* author: */
/************************************************/
#include <linux/module.h> // module_init module_exit
#include <linux/init.h> // __init __exit
// 模块安装函数
static int __init chrdev_init(void)
{
printk(KERN_INFO "chrdev_init helloworld init\n");
return 0;
}
// 模块下载函数
static void __exit chrdev_exit(void)
{
printk(KERN_INFO "chrdev_exit helloworld exit\n");
}
module_init(chrdev_init);
module_exit(chrdev_exit);
// MODULE_xxx这种宏作用是用来添加模块描述信息
MODULE_LICENSE("GPL"); // 描述模块的许可证
MODULE_AUTHOR("aston"); // 描述模块的作者
MODULE_DESCRIPTION("module test"); // 描述模块的介绍信息
MODULE_ALIAS("alias xxx"); // 描述模块的别名信息
- 模块的安装
使用insmod module_test.ko
安装模块命令后,使用lsmod
查看,最新安装的模块会在最前面。在这个过程中,通过insmod
命令与moudle_init
宏的关联(内核的机制)来调用moudle_init
宏声明的函数chrdev_init
函数。执行insmod
命令实际上不仅调用了chrdev_init
函数,还会进行一些其他工作,譬如lsmod能看到多了一个模块也是insmod帮我们在内部做了记录),但是我们就不用管了。
调试的输出:在安装驱动时,内核通过printk
函数帮助我们打印了条调试信息chrdev_init helloworld init
这个函数是内核专用的调试输出函数。用于与C库函数printf
类似,但是多了一个打印级别的参数。内核打印的信息还可以通过dmesg
命令查看。
- 模块的卸载
与模块的安装类似,使用rmmod
命令卸载模块。会调用moudle_exit
宏生命的函数chrdev_exit
.
- 模块的版本信息
可以使用modinfo查看模块的版本信息(vermagic),在内核zImage
中也有一个确定的版本信息。在安装驱动模块时,内核会比对这两个版本信息,若版本信息不同则不能安装。报错信息为:insmod: ERROR: could not insert module module_test.ko: Invalid module format
这也就是为什么驱动开发前提条件必须有自己内核源码树和由该内核源码树编译的内核zImage
的原因。这样做的目是为了保证内核和驱动模块的兼容性,是一种安全上的措施。
4.最简单的模块源码分析
函数源码就是上面的module_test.c
。
- 模块中常用的宏
// MODULE_xxx这种宏作用是用来添加模块描述信息
MODULE_LICENSE("GPL"); // 描述模块的许可证,一般声明为GPL许可证,而且最好不要少,否则可 // 能会出现莫名其妙的错误(譬如一些明显存在的函数提升找不到)。
MODULE_AUTHOR("aston"); // 描述模块的作者
MODULE_DESCRIPTION("module test"); // 描述模块的介绍信息
MODULE_ALIAS("alias xxx"); // 描述模块的别名信息
- 函数修饰符
(1)__init
,本质上是个宏定义,在内核源代码中就有#define __init xxxx。这个__init的作用就是将被他修饰的函数放入.init.text段中去(本来默认情况下函数是被放入.text段中)。
整个内核中的所有的这类函数都会被链接器链接放入.init.text段中,所以所有的内核模块的__init
修饰的函数其实是被统一放在一起的。内核启动时统一会加载.init.text段中的这些模块安装函数,加载完后就会把这个段给释放掉以节省内存。
(2)__exit:
(3)static:在驱动程序中,很多代码都需要有严格的限制,因为内核中文件众多,很有可能自己写的函数会与其文件中的函数名重复,自己写的驱动函数一般都要限定在本文件中使用。
- 驱动包含的头文件
驱动源代码中包含的头文件和应用编程程序中包含的头文件并不是一码事,在应用程序中包含的头文件是应用程序的头文件,是由编译器带来的(譬如gcc的头文件路径为usr/include下,这些东西与操作系统无关)驱动源码中包含的头文件属于内核源码的一部分,位于内核源码目录下的include目录下。
- 驱动编译的
Makefile
分析
# 开发板的linux内核的源码树目录
KERN_DIR = /root/driver/kernel
obj-m += module_test.o
all:
make -C $(KERN_DIR) M=`pwd` modules
.PHONY: clean
clean:
make -C $(KERN_DIR) M=`pwd` modules clean
(1)KERN_DIR,变量的值就是我们用来编译这个模块的内核源码树的目录。
(2)obj-m += module_test.o,这一行就表示我们要将module_test.c文件编译成一个模块。
(3)make -C $(KERN_DIR) M=pwd
modules 这个命令用来实际编译模块,工作原理就是:利用make -C进入到我们指定的内核源码树目录下,然后在源码目录树下借用内核源码中定义的模块编译规则去编译这个模块,编译完成后把生成的文件还拷贝到当前目录下,完成编译。
(4)make clean ,用来清除编译痕迹。
总结:模块的makefile非常简单,本身并不能完成模块的编译,而是通过make -C进入到内核源码树下借用内核源码的体系来完成模块的编译链接的。这个Makefile本身是非常模式化的,3和4部分是永远不用动的,只有1和2需要动。1是内核源码树的目录,你必须根据自己的编译环境
- 使用开发板来调试模块
调试开发板比较方便的一种方式是开发板中烧录u-boot,在u-boot中设置一些环境变量,根据这些环境变量获取开发板内核的加载方式,开发阶段通常使用。tftp下载的方式加载内核,然后通过nfs服务将ubuntu中的根文件系统加载挂在在开发板上。
设置开发板通过tftp
下载的方式要设置u-boot
的bootcmd
参树。来下载内核源码树编译得到的zImage。
set bootcmd 'tftp 0x30008000 zImage;bootm 0x30008000'
设置bootargs
使开发板从nfs
去挂载rootfs
(内核配置记得打开使能nfs
形式的rootfs
)
setenv bootargs root=/dev/nfs nfsroot=192.168.1.141:/root/porting_x210/rootfs/rootfs ip=192.168.1.10:192.168.1.141:192.168.1.1:255.255.255.0::eth0:off init=/linuxrc console=ttySAC2,115200
root=/dev/nfs,并非真的设备,而是一个告诉内核经由网络取得根文件系统的旗标。在文件系统为基于nfs的文件系统的时候使用。当然指定root=/dev/nfs之后,还需要指定nfsroot,
nfsroot这个参数告诉内核以哪一台机器,哪个目录以及哪个网络文件系统选项作为根文件系统使用。参数的格式如下:nfsroot=[<server-ip>:]<root-dir>[,<nfs-options>]
ip=[开发板ip]:[服务器ip]:[网关]:[子网掩码]
init= nit 指定的是内核启起来后,进入系统中运行的第一个脚本,一般init=/linuxrc, 或者init=/etc/preinit,preinit的内容一般是创建console,null设备节点,运行init程序,挂载一些文件系统等等操作。请注意,很多初学者以为init=/linuxrc是固定写法,其实不然,/linuxrc指的是/目录下面的linuxrc脚本,一般是一个连接罢了。
上一篇: armbian学习笔记二:如何开始使用armbian?
下一篇: php读取文件内容的三种方法
推荐阅读
-
详解Android应用开发--MP3音乐播放器代码实现(一)
-
作为一个JavaEye的老会员,我感到羞愧 博客分类: 软件开发 AndroidGoogleiOS工作OS
-
作为一个JavaEye的老会员,我感到羞愧 博客分类: 软件开发 AndroidGoogleiOS工作OS
-
javascript pattern只允许数字,英文字母和一些特殊字符 博客分类: Java编程PHP编程Web前端开发 HTMLWebJavaScriptjQuery
-
javascript pattern只允许数字,英文字母和一些特殊字符 博客分类: Java编程PHP编程Web前端开发 HTMLWebJavaScriptjQuery
-
ltnmp 3.0 发布,PHP 开发环境一键安装包
-
Struts2(Webwork2)一些实战开发技巧 博客分类: Struts2 BeanHibernateMyeclipseEclipseStruts
-
如何解决一些项目开发和维护中的问题——Hibernate实战篇 博客分类: hibernate Hibernate项目管理SpringSQLDAO
-
Struts2(Webwork2)一些实战开发技巧 博客分类: Struts2 BeanHibernateMyeclipseEclipseStruts
-
关于PHP中MVC的一点开发心得