您的位置:首页 > 其它

字符设备驱动之"Hello, World!"

2013-01-22 00:11 375 查看
从零开始,尝试写一个“hello world!”级别的驱动测试程序。

1. 创建驱动文件,简单的命名为test1.c,代码如下:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/device.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <asm/uaccess.h>
#include <asm/irq.h>
#include <asm/io.h>
#include <mach/regs-gpio.h>
#include <mach/hardware.h>

static int test1_open(struct inode *inode, struct file *file)
{
printk("test1 open test...\n");
return 0;
}

static int test1_read(struct file *filp, char __user *buff, size_t count, loff_t *offp)
{
return 0;
}

static int test1_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
{
printk("test1 write test...\n");
return 0;
}

static struct file_operations test1_fops = {
.owner = THIS_MODULE;
.open = test1_open,
.read = test1_read,
.write = test1_write,
};

#define DEVICE_NAME "fortest1"
#define TEST_MAJOR 111
static int __init test1_init(void)
{
int ret;
ret = register_chrdev(TEST_MAJOR, DEVICE_NAME, &test1_fops);
if(ret < 0){
printk(DEVICE_NAME " can't register major number\n");
return ret;
}

static void __exit test1_exit(void)
{
register_chrdev(TEST_MAJOR, DEVICE_NAME);
}

module_init(test1_init);
module_exit(test1_exit);
MODULE_LICENSE("GPL");


可以通过两种方式编译:

a. 通过编写Makefile文件,Makefile内容如下:

KERN_DIR = /work/system/linux-2.6.30.4
all:
make -C $(KERN_DIR) M=$(pwd) modules
clean:
make -C $(KERN_DIR) M=$(pwd) modules clean
rm -rf modules.order
obj-m += test1.o

对于这个Makefile,暂缓详细解释~
把此Makefile和test1.c文件放到一个目录,然后在此目录下执行make指令完成编译,得到test1.ko文件。
b. 放在内核文件中编译,具体操作如下:
将驱动代码test1.c放到Linux内核drivers/char目录下,然后修改此目录下的Makefile,增加一项“obj-m += test1.o”,然后回到Linux顶层目录执行“make modules”指令,完成编译后便可在drivers/char目录下看到test1.ko文件(驱动模块可加载文件)。

2. 测试驱动
将此test1.ko文件拷贝到根文件系统中,放到哪里都行(待会儿加载的时候注意地址就行了),然后烧写到开发板中
在加载此驱动之前首先通过“cat /proc/devices”看一下系统设备信息,如下:



然后加载test1.ko文件,如下:

#insmode test1.ko


此时再执行“cat /proc/devices”指令查看系统设备信息得到发现多了一项信息,如下(红色边框标记):


fortest1是注册设备驱动的名字~

初步判定,驱动加载没啥问题,继续测试驱动逻辑,即通过应用程序调用驱动程序,观察执行情况。

3. 编写应用程序(文件命名为app_test1.c)如下:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>

int main(int argc, char **argv)
{
int fd;
int val = 1;
fd = open("/dev/xxx",O_RDWR);
if(fd < 0)
printf("can not open\n");
write(fd, &val, 4);
return 0;
}
在PC上编译好然后烧到根文件系统中,然后烧到开发板中,加载驱动后执行应用程序可看到如下信息:



第一次运行出现“can not open”的报错信息,回头看应用程序代码,原因是没成功打开“/dev/xxx”设备,没能成功打开的原因是此设备不存在,所以使用“mknod/dev/xxx c 111 0”创建设备节点,有三点重要属性:设备类别(字符设备还是块设备)、主设备号、设备名称。设备命令保证应用程序能够打开它,设备类别和主设备号保证能够找到对应的驱动。
至此,一个“hello world!”级别的驱动应用便搞定了。
将以上过程给简要缕一下:
譬如一个用程序,它想往某个字符设备写数据,首先它得打开这个设备,对于大多数设备的打开,都是调用open库函数,然而对这么多的设备的打开操作,不可能在一个open库函数完成(即便有再多的case分支来判断),这得由驱动程序来完成,驱动程序必然有一个与应用程序对应的打开函数(名字不一定是open),那么应用程序如何能够调用正确的驱动程序呢?没见着它有传递特别的参数啊!当然有办法,这个关键点就是设备节点,譬如应用程序里执行的“open(“/dev/xxx”,…)”,/dev/xxx是一个关键点,它是设备文件,通过它的文件属性能够指定对应的并且唯一的驱动程序来执行应用程序对设备的操作。设备文件有上文所述的三个非常重要的属性:设备类别(字符设备还是块设备)、主设备号、设备名称。VFS(虚拟文件系统,暂时不作过多了解)通过应用程序所打开的文件节点(譬如“/dev/xxx”)的属性找到相应的驱动程序为应用程序服务。这是如何办到的呢?
要弄清这么些问题:驱动是如何存在的,驱动有什么样的结构,VFS如何找到对应的驱动程序...首先是“驱动如何存在的”“驱动加载又是什么一回事儿”,驱动加载(指令格式为“insmod驱动”)做了一件很重要的事情,就是向内核注册,可以肯定的是驱动带有一系列的函数,如何注册呢?这同时涉及驱动组织结构,也就是重要的file_operations结构体,它作为数据结构,身上绑定了大量的驱动函数,驱动向内核注册,把它告诉内核就好了。就像中断程序,重要的是它的中断号,有了这个唯一标识符,在中断发生时就能够指定唯一的、对应的驱动程序为之服务。file_operations同样如此,它存在内核某个地方必然也有一个唯一标识符,这个标识符就是主设备号,对,就是主设备号!由此便想到,应用程序中的open(“/dev/xxx”,...)里的设备/dev/xxx的主设备号必然与所对应的驱动注册到内核的主设备号(唯一标识符)一致,也得出一个重要信息,应用程序操作某个设备,必定有open(“/dev/xxx”,...)这个打开设备节点的过程。
应用程序和驱动之间的关系搞清楚了,再回过头来看驱动注册的问题,注册是如何完成的?注册的实质是告诉内核它的存在。注册工作是在加载驱动时,它一般是在驱动初始化函数里完成,驱动初始化函数也即驱动入口函数,如何知道它是驱动入口函数呢?有两种方法,一种是把驱动这个函数用一个统一的名字固定(init_module),要么用一个宏标记这个函数。

---------------------------------------------------------------------------------------------------------------------------------------------------------------

继续分析,上述驱动向内核注册的主设备号111是手动分配的,注册成功的前提是111这个主设备号没有被已经向内核注册的设备使用,这样手动分配在实际应用中当然不方便,最好是能够自动分配,这样就省去了“cat /proc/devices”命令查看设备信息的步骤。所以要做的事情就是实现驱动向内核注册时由自动分配主设备号,稍微修改程序代码就行,如下:



register_chrdev函数的第一个参数设为0,系统就为驱动自动分配一个主设备号并返回。
驱动注册自动分配主设备号比较简单。对于应用程序,它每次成功执行驱动程序得先打开设备节点文件来嫁接与驱动关系,这个设备往往需要手动创建,创建的时候还得查看对应设备的驱动主设备号,对于比较庞大的系统,事情更复杂,所以解决这个问题就比较有意义了,即不用过多花心思在设备节点的文件创建上,也即由系统帮忙解决节点文件创建工作。
实际上Linux内核提供了一组函数,可以用来在模块加载的时候自动在/dev目录下创建相应设备节点,并在卸载模块时删除该节点,当然前提是用户空间移植了udev。
ps:关于udev,找到这么一些解释,udev是Linuxkernel 2.6系列的设备管理器。它主要的功能是管理/dev目录底下的设备节点。它同时也是用来接替devfs及hotplug的功能,这意味着它要在添加/删除硬件时处理/dev目录以及所有用户空间的行为,包括加载firmware时。Linux传统上使用静态设备创建方法,因此大量设备节点在/dev下创建(有时上千个),而不管相应的硬件设备是否真正存在。通常这由一个MAKEDEV脚本实现。采用udev的方法,只有被内核检测到的设备才会获取为它们创建的设备节点。因为这些设备节点在每次系统启动时被创建,设备节点不需要大量磁盘空间,因此它使用的内存可以忽略。此外,还有博文讲到:提醒一点,udev是应用层的,不要试图在内核的配置选项里找到它...对此不太理解。
内核中定义了struct class结构体,顾名思义,一个struct class结构体类型变量对应一个类,内核同时提供了class_create函数,可以用它来创建一个类,这个类存放于sysfs下面,一旦创建好了这个类,再调用device_create函数来在/dev目录下创建相应的设备节点。这样,加载模块的时候,用户空间中的udev会自动响应device_create函数,去/sysfs下寻找对应的类从而创建设备节点。
值得注意的是,在2.6较早的内核版本中,device_create函数名称不同,是class_device_create,所以在新的内核中编译以前的模块程序有时会报错,就是因为函数名称不同,而且里面的参数设置也有一些变化。
具体修改如下:



ps:struct class和class_create函数以及device_create都定义在/include/linux/device.h中,使用的时候一定要包含这个头文件,否则编译器会报错。
烧入开发板通过“cat /proc/devices”查看设备信息,可以看到系统已经给驱动自动分配了一个主设备号,如下:



ps:在加载应用程序的时候可能会遇到“test1: Unknown symbol class_create...”之类的错误信息,这时一般加上“MODULE_LICENSE(“GPL”);”就可以了。
稍微修改应用程序app_test1源码在开发板上调用信息如下:



至此,修改成功!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: