本系列导航:
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