linux驱动由浅入深系列:驱动程序的基本结构概览之一(第一个驱动程序)

本系列导航:

linux驱动由浅入深系列:驱动程序的基本结构概览之一(第一个驱动程序)

linux驱动由浅入深系列:驱动程序的基本结构概览之二(详解驱动注册过程)



提到linux驱动程序,首先应该知道它是linux的内核模块。那么想要编写驱动程序,就要首先认识一下linux的内核模块机制。Linux内核模块是使得复杂而庞大的linux内核条理清晰、可裁剪、高兼容性的重要特性。

Linux内核模块的特点:

1,  模块本身不被编译进内核镜像,能够控制内核的大小。

2,  模块可以在需要的时候中被动态加载,一旦加载完成就和内核其它部分完全一样。

 

下面便是linux内核模块的helloworld程序,结构十分固定。编译完成后生成hello.ko,通过insmod hello.ko进行加载,加载时输出” hello module has been mount!”,使用rmmod hello进行卸载,卸载时输出” hellomodule has been remove!”。

#include 
#include 

MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("Radia");

static int hello_init()
{
	printk(KERN_EMERG "hello module has been mount!\n");
	return 0;
}

static void hello_exit()
{
	printk(KERN_EMERG "hello module has been remove!\n");
}

module_init(hello_init);
module_exit(hello_exit);

在Linux下可以通过两种方式加载驱动程序:静态加载和动态加载。

静态加载就是把驱动程序直接编译进内核,系统启动后可以直接调用。静态加载的缺点是调试起来比较麻烦,每次修改一个地方都要重新编译和下载内核,效率较低。若采用静态加载的驱动较多,会导致内核容量很大,浪费存储空间。

动态加载利用了Linux的module特性,可以在系统启动后用insmod命令添加模块(.ko),在不需要的时候用rmmod命令卸载模块,采用这种动态加载的方式便于驱动程序的调试,同时可以针对产品的功能需求,进行内核的裁剪,将不需要的驱动去除,大大减小了内核的存储容量。

在台式机上,一般采用动态加载的方式;在嵌入式产品里,可以先采用动态加载的方式进行调试,调试成功后再编译进内核。

 

下面是一个最基础的helloworld版的linux驱动,加载入内核后生成/dev/hello节点,打开该文件输出” hello open”。这个驱动并不具有任何控制硬件的行为,只是为了展示linux驱动的通用结构。这几乎是所有驱动程序的通用模版,如led的驱动程序,只需要在hello_ioctl函数中根据不同的传入参数操作gpio寄存器即可。(应用层没有操作硬件的权限,而内核中具有所有权限。驱动程序的作用就是高效的、封装的、有限的向应用层提供服务)

#include 
#include 

#include 
#include 
#include 

#define DRIVER_NAME "hello"
#define DEVICE_NAME "hello"

MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("Radia");

static int hello_open(struct inode *inode, struct file *file){
	printk(KERN_EMERG "hello open\n");
	return 0;
}

static int hello_release(struct inode *inode, struct file *file){
	printk(KERN_EMERG "hello release\n");
	return 0;
}

static long hello_ioctl(struct file *file, unsigned int cmd, unsigned long arg){
	printk("cmd is %d, arg is %d\n", cmd, arg);
	return 0;
}

static struct file_operations hello_fops = {
	.owner = THIS_MODULE,
	.open = hello_open,
	.release = hello_release,
	.unlocked_ioctl = hello_ioctl,
};

static struct miscdevice hello_dev = {
	.minor = MISC_DYNAMIC_MINOR,
	.name = DEVICE_NAME,
	.fops = &hello_fops,
};

static int hello_probe(struct platform_device *pdv)
{
	printk(KERN_EMERG "hello probe\n");
	misc_register(&hello_dev);
	return 0;
}

static int hello_remove(struct platform_device *pdv)
{
	printk(KERN_EMERG "hello remove\n");
	misc_deregister(&hello_dev);
	return 0;
}

static void hello_shutdown(struct platform_device *pdv)
{
}

static int hello_suspend(struct platform_device *pdv, pm_message_t pmt)
{
	return 0;
}

static int hello_resume(struct platform_device *pdv)
{
	return 0;
}

static struct platform_driver hello_driver = {
	.probe = hello_probe,
	.remove = hello_remove,
	.shutdown = hello_shutdown,
	.suspend = hello_suspend,
	.resume = hello_resume,
	.driver = {
		.name = DRIVER_NAME,
		.owner = THIS_MODULE,
	}
};

static int hello_init(void)
{
	int driver_state;
	printk(KERN_EMERG "hello module has been mount!\n");
	driver_state = platform_driver_register(&hello_driver);
	printk(KERN_EMERG "platform_driver_register driver_state is %d\n", driver_state);
	platform_device_register_simple(DRIVER_NAME, -1, NULL, 0);
	printk(KERN_EMERG "platform_device_register_simple end\n");
	return 0;
}

static void hello_exit(void)
{
	printk(KERN_EMERG "hello module has been remove!\n");
	platform_driver_unregister(&hello_driver);
}

module_init(hello_init);
module_exit(hello_exit);

程序分析:

1,  可以看出这个驱动模版也是基于我们上一个内核模块模版的。

2,  驱动模块一般在开机时逐次加载,加载成功后就会调用hello_init函数使用platform_driver_register向内核中注册一个驱动。搞定,就是如此简单!

3,  相关函数

字符设备申请设备号函数register_chrdev、register_chrdev_region

register_chrdev注册指定主次设备号的设备,手动在/dev下创建该设备的设备节点

register_chrdev_region 注册一个指定主次设备号的设备,后利用class类在/dev/目录下自动创建一个该设备的节点。

字符设备注册函数cdev_add。

本实例为简洁起见使用了misc_register函数,该函数已包含了上面两个步骤。

字符设备驱动注册函数platform_driver_register。

4,  probe函数的调用

当设备和驱动的名字匹配,BUS就会调用驱动的probe函数。这分两种情况:

a 先注册设备,后注册驱动

此种方式最为常见,大多数设备先于驱动注册到内核中。

在内核源代码中,platform 设备的初始化(注册)用arch_initcall()调用,它的initcall 的level为3;而驱动的注册用module_init()调用,即device_initcall(),它的initcall 的level为6。kernel 初始化时(kernel_init@init/main.c),按照内核链接文件中(arm系统:kernel/arch/arm/vmlinux.lds)的__initcall_start段的序列依次执行,这样level小的初始化函数先于level大的初始化函数被调用。

所以platform设备先被注册,驱动加载时会调用驱动程序中的probe(),扫描系统中已注册的设备,找到匹配设备后将驱动和设备绑定。

 

当设备注册的时候,由于驱动尚未注册,所以执行"__device_attach"时直接返回,未执行"driver_probe_device"进行设备和驱动匹配;后来驱动注册的时候,执行"__driver_attach"的时候设备已经注册,所以进入"driver_probe_device",接着进入"driver_probe_device",然后"really_probe"完成设备和驱动的绑定。

 

b 先注册驱动,再注册设备

当驱动注册的时候,由于设备尚未注册,所以执行"__driver_attach"时直接返回,未执行"driver_probe_device"进行驱动和设备匹配;后来设备注册的时候,执行"__device_attach"的时候驱动已经注册,所以进入"driver_probe_device",接着进入"driver_probe_device",然后"really_probe"完成驱动和设备的绑定。

 

本示例使用了最为简洁明了的展示方法,在内核模块的加载函数hello_init中依次注册了设备与驱动。可知注册后者时将会触发hello_probe函数的调用,其中调用misc_register使用了主设备号10,动态分配了次设备号等,完成设备注册。

 

5,  验证

编译驱动进内核,重新启动在开机过程中看到hello模块成功挂载,probe函数调用。

在/dev目录下生成了hello设备节点,主设备号10,次设备号56。

编写应用层程序进行测试:

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 


int main(int argc, char *argv[])
{
	int fd;
	printf("enter driver test %s %s \r\n", argv[1], argv[2]);
	char *hello = "/dev/hello";

	if((fd = open(hello, O_RDWR | O_NOCTTY | O_NDELAY)) < 0)
	{
		printf("open %s failed\n", hello);
	}
	else
	{
		printf("%s fd is %d \r\n", hello, fd);
		ioctl(fd, atoi(argv[1]), atoi(argv[2]));
	}
	close(fd);
	return 1;
}


编译后运行结果如下:

Shell 打印


Kernel log







你可能感兴趣的:(Linux,Driver)