知识整理–Linux字符设备驱动开发基础
我理解的linux驱动:封装对底层硬件的操作,向上层应用提供操作接口
文中有些地方没贴出相应的函数原型,请自行查阅,或者用SouceInsight搜索自己的内核源码树(本人就是用该方式查阅函数的使用)
简单设备驱动开发基础知识,暂不考虑驱动框架。文章根据GFM排版https://github.com/TongxinV
开发环境的搭建:内核源码树、nfs挂载的roofs、开发配置好相应的bootcmd和bootargs
驱动开发的步骤:1.驱动源码代码的编写、Makefile文件编写、编译得到;2.insmod装载模块、测试,rmmod卸载模块。
bootcmd和bootargs
1.设置bootcmd使开发板能够通过tftp下载自己建立的内核源码树编译得到的zImage
set bootcmd 'tftp 0x30008000 zImage;bootm 0x30008000'
(注:bootcmd=movi read kernel 30008000; movi read rootfs 30B00000 300000; bootm 30008000 30B00000 这样的bootcmd是从inand启动内核的时候用的)
2.设置bootargs使开发板从nfs去挂载rootfs(内核配置记得打开使能nfs形式的rootfs)
setenv bootargs root=/dev/nfs nfsroot=192.168.1.141:/root/x210_porting/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
编译驱动源码的Makefile文件
#ubuntu的内核源码树,如果要编译在ubuntu中安装的模块就打开这2个
#KERN_VER = $(shell uname -r)
#KERN_DIR = /lib/modules/$(KERN_VER)/build
# 开发板的linux内核的源码树目录,根据自己在源码树存放的目录修改
KERN_DIR = /root/driver/kernel
obj-m += module_test.o //-m 表示我们要将module_test.c编译成一个模块
//-y表示我们要将module_test.c编译链接进zImage
all:
make -C $(KERN_DIR) M=`pwd` modules
//-C 表示进入到某一个目录下去编译
//`pwd`:表示把两个`号中间的内容当成命令执行
//M=`pwd`则表示把pwd打印的内容保存起来,目的是为了编译好了之后能够返回原来的目录
//modules就是真正用来编译模块的命令,在内核的其他地方定义过了
cp:
cp *.ko /root/porting_x210/rootfs/rootfs/driver_test
.PHONY: clean //把clean当成一个伪目标
clean:
make -C $(KERN_DIR) M=`pwd` modules clean
总结:模块的makefile非常简单,本身并不能完成模块的编译,而是通过make -C进入到内核源码树下借用内核源码的体系来完成模块的编译链接的。
字符设备基础1
从一个最简单的模块源码说起
#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"); // 描述模块的许可证
(1)使用printk打印调试信息,printk可以设置打印级别。常见的KERN_DBUG-8\KERN_INFO-7,当前系统也有一个打印信息的级别0-7(比如当前系统打印信息的级别为4,则printk打印小于级别4)。
查看当前系统打印信息的级别:cat /proc/sys/kernel/printk;修改:echo 8 > /proc/sys/kernel/printk
(2)驱动源代码中包含的头文件和原来应用编程程序中包含的头文件不是一回事。应用编程中包含的头文件是应用层的头文件,是应用程序的编译器带来的(譬如gcc的头文件路径在/usr/include下,这些东西是和操作系统无关的)。驱动源码属于内核源码的一部分,驱动源码中的头文件其实就是内核源代码目录下的include目录下的头文件。
(3)函数修饰符__init(前面加下划线的表示这是给内核使用的函数),本质上是个宏定义,在内核源代码中就有#define __init xxxx。这个__init的作用就是将被他修饰的函数放入.init.text段中去(本来默认情况下函数是被放入.text段中)。
#define __init __section(.init.text) __cold notrace
├──#define __section(S) __attribute__ ((__section__(#S)))
整个内核中的所有的这类函数都会被链接器根据链接脚本放入.init.text段中,所以所有的内核模块的__init修饰的函数其实是被统一放在一起的。
内核启动时统一会加载.init.text段中的这些模块安装函数,加载完后就会把这个段给释放掉以节省内存。__exit同理。
字符设备驱动工作原理
可以理解模块是一种机制,驱动使用了模块这种机制来实现
系统整体工作原理:(1)应用层->API->设备驱动->硬件;(2)API:open、read、write、close等;(3)驱动源码中提供真正的open、read、write、close等函数实体

file_operations结构体(另外一种为attribute方式后面再讲):(1)元素主要是函数指针,用来挂接实体函数地址;(2)每个设备驱动都需要一个该结构体类型的变量;(3)设备驱动向内核注册时提供该结构体类型的变量。
注册字符设备驱动register_chrdev:
static inline int register_chrdev(unsigned int major, const char *name,const struct file_operations *fops)
{
return __register_chrdev(major, 0, 256, name, fops);
}
(1)作用,驱动向内核注册自己的file_operations结构体,注册的过程其实主要是将要注册的驱动的信息存储在内核中专门用来存储注册的字符设备驱动的数组中相应的位置
(2)参数:设备号major–major传0进去表示要让内核帮我们自动分配一个合适的空白的没被使用的主设备,内核如果成功分配就会返回分配的主设备号;如果分配失败会返回负数
(3)inline和static
inline:当把函数定义在头文件里面的时候,如果你这个头文件被两个及两个以上的函数包含的时候,在链接的时候就会出错。inline的作用就是解决这个问题,原地展开并能够实现静态检查。另外一个原因是函数本身就比较短。
内核如何管理字符设备驱动
(1)内核中用一个数组来存储注册的字符设备驱动;(2)register_chrdev内部将我们要注册的驱动的信息(fops结构体地址)存储在数组中相应的位置;(3)cat /proc/devices查看内核中已经注册过的字符设备驱动(和块设备驱动)
字符设备驱动代码实践–给空模块添加驱动壳子
核心工作:定义file_operations类型变量及其元素填充、注册驱动
简单的驱动程序示例
module_test.c
├── 模块安装函数xxx
│ └── 注册字符设备驱动register_chrdev(MYNMAJOR, MYNAME, &test_module_fops)
├── 模块安装函数yyy
│ └── 注销字符设备驱动unregister_chrdev(MYNMAJOR, MYNAME)
│
├── module_init(模块安装函数xxx);
├── module_exit(模块卸载函数yyy);
│
└── MODULE_LICENSE("GPL");
#include <linux/module.h> // module_init module_exit
#include <linux/init.h> // __init __exit
#include <linux/fs.h> // file_operations 没写会报错:xxx has initializer but incomplete type
#define MYNMAJOR 200
#define MYNAME "test_chrdev"
//file_operations结构体变量中填充的函数指针的实体,函数的格式要遵守
static int test_chrdev_open(struct inode *inode, struct file *file)
{
//这个函数中真正应该放置的是打开这个设备的硬件操作代码部分
//但是现在我们暂时写不了那么多,所以就就用一个printk打印个信息来做代表
printk(KERN_INFO "test_module_open\n");
return 0;
}
static int test_chrdev_release(struct inode *inode, struct file *file)
{
printk(KERN_INFO "test_chrdev_release\n");
return 0;
}
//自定义一个file_operations结构体变量,并填充
static const struct file_operations test_module_fops = {
.owner = THIS_MODULE, //惯例,所有的驱动都有这一个,这也是这结构体中唯一一个不是函数指针的元素
.open = test_chrdev_open, //将来应用open打开这个这个设备时实际调用的函数
.release = test_chrdev_release, //对应close,为什么不叫close呢?详见后面release和close的区别的讲解
};
/*********************************************************************************/
// 模块安装函数
static int __init chrdev_init(void)
{
printk(KERN_INFO "chrdev_init helloworld init\n");
//在module_init宏调用的函数中去注册字符设备驱动
int ret = -1; //register_chrdev 返回值为int类型
ret = register_chrdev(MYNMAJOR, MYNAME, &test_module_fops);
//参数:主设备号major,设备名称name,自己定义好的file_operations结构体变量指针,注意是指针,所以要加上取地址符
//完了之后检查返回值
if(ret){
printk(KERN_ERR "register_chrdev fial\n"); //注意这里不再用KERN_INFO
return -EINVAL; //内核中定义了好多error number 不都用以前那样return -1;负号要加 !!
}
printk(KERN_ERR "register_chrdev success...\n");
return 0;
}
// 模块卸载函数
static void __exit chrdev_exit(void)
{
printk(KERN_INFO "chrdev_exit helloworld exit\n");
//在module_exit宏调用的函数中去注销字符设备驱动
//实验中,在我们这里不写东西的时候,rmmod 后lsmod 查看确实是没了,但是cat /proc/device发现设备号还是被占着
unregister_chrdev(MYNMAJOR,

最低0.47元/天 解锁文章

被折叠的 条评论
为什么被折叠?



