1 总线设备驱动模型概述
随着技术的不断进步,系统的拓扑结构也越来越复杂,对智能电源管理,热插拔的支持要求也越来越高,2.3内核已经难以满足这些要求,为了适应这种形势需要,linux2.6内核提供了全新的内核设备模型。
总线的作用就是感知设备是否连接上usb,网卡等等,总线设备驱动模型更好的支持热插拔的设备,也更好的提供移植性。
在设备模型中,我们将看到,设备驱动主要是由总线,驱动程序,设备三个部分构成,通过这三个标准部件,把各种纷繁杂乱的设备归结过来,达到简化设备驱动编写的目的,下面我们就将按三个部分总线,驱动和设备来学习一下。
2 总线
1 总线的描述
在Linux 内核中, 总线由bus_type 结构表示,在这个结构中最重要的是总线名称和匹配函数。
struct bus_type {
const char *name; /*总线名称*/
int (*match) (struct device *dev, struct device_driver *drv); /*驱动与设备的匹配函数*/
………
}
2 总线的注册
总线的注册使用如下函数
bus_register(struct bus_type *bus)
若成功,新的总线将被添加进系统,并可在/sys/bus 下看到相应的目录。
3 总线的注销
总线的注销使用如下函数
void bus_unregister(struct bus_type *bus)
代码例程bus.c:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/device.h>//for bus_type
int my_match (struct device *dev, struct device_driver *drv)//新加入的总线和新加入的设备进行匹配,若能匹配上返回非零,匹配不上返回零
{
return 0;
}
struct bus_type my_bus_type = { //定义总线
.name="my_bus",
.match=my_match,
};
int my_bus_init()
{
int ret;
ret=bus_register(&my_bus_type);//注册一个总线
return ret;
}
void my_bus_exit()
{
bus_unregister(&my_bus_type);//注销总线
}
module_init(my_bus_init);
module_exit(my_bus_exit);
MODULE_LICENSE("GPL");
编好代码后,将bus.ko代码复制到开发板中去,加载模块后,可以通过ls /sys/bus 指令查看我们添加到linux系统中的总线
驱动程序
1 驱动的描述
在Linux内核中, 驱动由device_driver结构表示。
struct device_driver {
{
const char *name; /*驱动名称*/
struct bus_type *bus; /*驱动程序所在的总线*/
int (*probe) (struct device *dev);
………
}
当设备添加到总线上时,总线和设备匹配的时候会调用probe函数对设备进行相应的处理。
2 驱动的注册
驱动的注册使用如下函数
int driver_register(struct device_driver *drv)
3 驱动的注销
驱动的注销使用如下函数
void driver_unregister(struct device_driver *drv)
代码例程driver.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/device.h>
#include <linux/kernel.h>
extern struct bus_type my_bus_type; //使用外部一个模块
int my_probe(struct device *dev)
{
printk(KERN_WARNING"driver found the device !\n");
return 0;
}
struct device_driver my_driver = {
.name="my_dev", //这个名字很重要,要和设备的名字相同
.bus=&my_bus_type, //要挂载到的那条总线上
.probe=my_probe, //当设备名字和驱动的名字相同时,调用probe函数
};
int my_driver_init()
{
int ret;
ret = driver_register(&my_driver); //注册驱动
return ret;
}
void my_driver_exit()
{
driver_unregister(&my_driver); //注销驱动
}
module_init(my_driver_init);
module_exit(my_driver_exit);
MODULE_LICENSE("GPL");
代码编号后,将driver.ko代码复制到开发板中去,加载insmod driver.ko模块后,可以通过ls /sys/bus 指令查看我们添加到linux系统中的总线,从实验结果看,在my_bus这条总线上,我们已经挂载上了drivers驱动了,进入drivers中,可以看到我们创建的驱动程序名称my_dev。
4 设备
1 设备的描述
在Linux内核中, 设备由struct device结构表示。
struct device {
{
const char *init_name; /*设备的名字*/
struct bus_type *bus; /*设备所在的总线*/
………
}
设备的名字和驱动程序下的驱动的名字要一致。
2 设备的注册
设备的注册使用如下函数
int device_register(struct device *dev)
3 设备的注销
设备的注销使用如下函数
void device_unregister(struct device *dev)
代码例程:
device.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/device.h>
#include <linux/kernel.h>
extern struct bus_type my_bus_type; //调用bus.c中的my_bus_type总线
struct device my_dev={
.init_name="my_dev", /*设备的名字,要和驱动的名字一样*/
.bus=&my_bus_type, /*设备所在的总线*/
};
int my_device_init()
{
int ret;
ret= device_register(&my_dev); //设备的注册
return ret;
}
void my_device_exit()
{
device_unregister(&my_dev); //设备的注销
}
module_init(my_device_init);
module_exit(my_device_exit);
MODULE_LICENSE("GPL");
bus.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/device.h>//for bus_type
int my_match (struct device *dev, struct device_driver *drv)//新加入的总线和新加入的设备进行匹配,若能匹配上返回非零,匹配不上返回零
{
//匹配驱动模块中的名字和设备模块中的名字
//return !strncmp(dev->init_name,drv->name,strlen(drv->name));
/*在内核源码中Core.c文件中,device_register()函数中dev->init_name = NULL 会被清空,
后面匹配的时候会匹配空指针。*/
return !strncmp(dev->kobj_name,drv->name,strlen(drv->name));
/*实际上这个init_name被赋值到kobj.name中了。所以在strncmp()函数中,应该是dev->kobj.name*/
//return 0;
}
struct bus_type my_bus_type = {
.name="my_bus",
.match=my_match,
};
EXPORT_SYMBOL(my_bus_type); //输出模块给driver.c device.c
int my_bus_init()
{
int ret;
ret=bus_register(&my_bus_type);
return ret;
}
void my_bus_exit()
{
bus_unregister(&my_bus_type);
}
module_init(my_bus_init);
module_exit(my_bus_exit);
MODULE_LICENSE("GPL");
driver.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/device.h>
#include <linux/kernel.h>
extern bus_type my_bus_type; //使用外部一个模块
int my_probe(struct device *dev)
{
printk(KERN_WARNING"driver found the device it can handle!\n");
return 0;
}
struct device_driver = my driver{
.name="my_dev"; //这个名字很重要,要和设备的名字相同
.bus=&my_bus_type; //要挂载到的那条总线上
.probe=my_probe; //当设备名字和驱动的名字相同时,调用probe函数
};
int my_driver_init()
{
int ret;
ret = driver_register(&my_driver); //注册驱动
return ret;
}
void my_driver_exit()
{
void driver_unregister(&my_driver); //注销驱动
}
module_init(my_driver_init);
module_exit(my_driver_exit);
MODULE_LICENSE("GPL");
Makefile
obj-m := bus.o device.o driver.o
KDIR :=/home/S5-driver/lesson7/linux-ok6410
all:
make -C $(KDIR) M=$(PWD) modules CROSS_COMPILE=arm-linux- ARCH=arm
clean:
rm -f *.o *.ko *.bak
因为这里bus.c中的匹配函数修改了代码,具体见bus.c中match代码,所以这里编译好模块之后要重启开发板,将bus.ko,driver.ko 和device.ko拷贝到开发板上去。我们看到了我们期望看到的结果,”driver found the device!”,也就是说总线上的match函数将驱动和设备已经匹配上了,然后调用probe函数,就会出现我们所期望的结果。
5 平台总线设备设计
1 平台总线概述
平台总线: 平台总线(Platform bus)是linux2.6内核加入的一种虚拟总线,其优势在于采用了总线的模型对设备与驱动进行了管理,这样提高了程序的可移植性。有了平台总线后,我们就不需要自己创建总线,只要挂载到平台总线上即可。
在内核代码platform.c中有个结构struct bus_type platform_bus_type,在这个结构中有个匹配函数match,match主要是通过设备的名字和驱动的名字进行匹配的,这和前文所说的match函数是一样的,都是匹配设备的名字和驱动的名字。
2 平台总线的描述
平台设备使用struct platform_device来描述
struct platform_device {
const char *name; /*设备名*/
int id; /*设备编号,配合设备名使用*/
struct device dev;
u32 num_resources;
struct resource *resource; /*设备资源,包括基地址和中断号*/
}
struct resource {
resource_size_t start;
resource_size_t end;
const char *name;
unsigned long flags; /*资源的类型*/
struct resource *parent, *sibling, *child;
};
3 平台总线的注册
注册平台设备使用函数platform_device_register
int platform_device_register(struct platform_device *pdev)
4 平台总线的注销
注销平台设备使用函数 platform_device_register
int platform_device_register(struct platform_device *pdev)
结合上面的基础知识,将上节按键驱动修改为平台驱动模式,下面给出平台设备的模块代码key_dev.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/platform_device.h> //for platform_device
#include <linux/interrupt.h>
#define GPNCON 0x7f008830
/*struct source 定义设备资源*/
struct resource key_resource[] = {
[0]={ //控制寄存器的基地址
.start=GPNCON,
.end=GPNCON+8,
.flags=IORESOURCE_MEM,/*资源类型由flags设置,寄存器属于内存地址的资源*/
},
[1]={ //中断号基地址
.start=IRQ_EINT(0),
.end=IRQ_EINT(1),
.flags=IORESOURCE_IRQ,/*中断号属于IORESOURCE_IRQ类型*/
},
};
/*定义平台总线key_device*/
struct platform_device key_device = {
.name="my-key",
.id=0,
.num_resources = 2, //平台资源的数目
.resource = key_resource, //需要定义resource
};
int key_device_init()
{
platform_device_register(&key_device); /*注册平台设备*/
return 0;
}
void key_device_exit()
{
platform_device_unregister(&key_device);/*注销平台设备*/
}
module_init(key_device_init);
module_exit(key_device_exit);
MODULE_LICENSE("GPL");
将编译好的key_dev.ko模块复制到开发板中去,安装insmod key_dev.ko后,进入总线中可以看到有一条platform总线,进入这个总线中可以看到里面有devices和drivers,再进入到devices中查看发现有个设备名为my-dev.o,这就说明我们已经把这个设备添加到平台总线上去了。
6 平台驱动
1 平台驱动的描述
平台驱动使用struct platform_driver 描述:
struct platform_driver {
int (*probe)(struct platform_device *);
int (*remove)(struct platform_device *);
……
}
2 平台驱动注册使用函数
int platform_driver_register(struct platform_driver *)
3 平台驱动的卸载
int platform_driver_unregister(struct platform_driver *)
接下来就是对按键驱动模式修改为平台驱动模式。将上节博客中的key.c复制过来修改名称key_dri.c并修改代码如下
#include<linux/module.h>
#include<linux/init.h>
#include<linux/miscdevice.h>
#include<linux/interrupt.h>
#include<linux/io.h>
#include<linux/fs.h> /* for iormap */
#include<asm/uaccess.h> //for copy_to_user
#include<linux/slab.h> /* for kmalloc */
#include <linux/sched.h> //for wait_queue
#include <linux/platform_device.h>
//#define GPNCON 0x7f008830
//#define GPNDAT 0x7F008834
unsigned int key_num = 0;
struct work_struct *work1;
wait_queue_head_t key_q;//定义等待队列
struct resource *res_irq;//定义一个变量可以在probe函数中从设备中取出相应的中断号
struct resource *res_mem;//定义一个变量可以在probe函数中从设备中取出相应的基地址
//unsigned int *gpio_data;
unsigned int *key_base;//整个按键的虚拟地址
struct timer_list key_timer;
void key_timer_func(unsigned long data)
{
unsigned int key_val;
//key_val = readw(gpio_data)&0x2;
key_val = readw(key_base+1)&0x2;
if (key_val == 0)
{
printk(KERN_WARNING"OK6410 S3 key down!\n");
key_num=3;
}
//key_val = readw(gpio_data)&0x1;
key_val = readw(key_base+1)&0x1; //由于这里要读的是GPNDAT寄存器中的值,而GPNDAT的地址是GPNCON寄存器的地址+4。但是这里因为是整型int,只+1
if (key_val == 0)
{
printk(KERN_WARNING"OK6410 S2 key down!\n");
key_num=2;
}
//wake_up(&key_q);//唤醒等待队列
}
void work1_func(struct work_struct *work)
{
mod_timer(&key_timer, jiffies + (HZ /10)); //设置100ms超时 1HZ=1S
}
irqreturn_t key_int(int irq,void *dev_id)
{
//1 检测中断是否产生
//2 清除已经发生的按键中断
//3. 提交下半部
schedule_work(work1);
//return 0;
return IRQ_HANDLED;
}
void key_hw_init()
{
//unsigned int *gpio_config;
unsigned short data;
//gpio_config=ioremap(GPNCON,4);
data=readw(key_base);
data&=~0b00;
data |= 0b10;
writew(data,key_base);
//gpio_data=ioremap(GPNDAT,4);
}
int key_open(struct inode *node,struct file *filp)
{
return 0;
}
ssize_t key_read(struct file *filp, char __user *buf, size_t size, loff_t *pos)//应用程序读取按键的时候,要实现read的设备方法被应用程序调用
{
//wait_event(key_q,key_num); //若key_num为0,就是没有数据的时候,进入睡眠,并挂在key_q这个等待队列上
printk(KERN_WARNING"in kernel :key num is %d\n",key_num);
copy_to_user(buf, &key_num, 4);//提供4个字节给应用程序
key_num=0;//此时清零用来下次读取
return 4;
}
struct file_operations key_fops=
{
.open = key_open,
.read = key_read,
};
struct miscdevice key_miscdev = {
.minor = 200, //次设备号
.name = "6410key",
.fops = &key_fops,
};
static int __devinit key_probe(struct platform_device *pdev)
{
int size;
misc_register(&key_miscdev); //将原来在模块初始化中的程序放到probe函数中来,这样只有在驱动和设备匹配到的时候才进行原来的初始化,简化代码
//按键硬件初始化
res_mem=platform_get_resource(pdev,IORESOURCE_MEM,0); //把物理地址转化为虚拟地址
size=(res_mem->end - res_mem->start)+1;
key_base=ioremap(res_mem->start,size);
key_hw_init();
//注册中断处理程序
res_irq=platform_get_resource(pdev,IORESOURCE_IRQ,0); //使用platform_get_resource函数取出资源
request_irq(res_irq->start,key_int,IRQF_TRIGGER_FALLING,"key",0); //相对比之前的key.c,这里修改了硬件相关部分,提高代码的可移植性
request_irq(res_irq->end,key_int,IRQF_TRIGGER_FALLING,"key",1);
//创建工作
work1=kmalloc(sizeof(struct work_struct),GFP_KERNEL);
INIT_WORK(work1,work1_func);
//初始化定时器
init_timer(&key_timer);
key_timer.function = key_timer_func;
//注册定时器
add_timer(&key_timer);
//初始化等待队列
// init_waitqueue_head(&key_q);
return 0;
}
static int __devexit key_remove(struct platform_device *pdev)
{
misc_deregister(&key_miscdev); //注销混杂设备
//注销中断处理程序
free_irq(IRQ_EINT(0),0);
free_irq(IRQ_EINT(1),1);
return 0;
}
struct platform_driver key_driver={
.probe = key_probe,
.remove = key_remove,
.driver = {
.name = "my-key", //和设备模块中的名字一致
.owner = THIS_MODULE,
},
};
static int keys_init() //在模块的初始化中只做一个工作就是注册平台驱动,重要的一点是驱动和设备要分开
{
return platform_driver_register(&key_driver);//注册平台驱动
}
static void key_exit()
{
platform_driver_unregister(&key_driver);//注销平台驱动
}
module_init(keys_init);
module_exit(key_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("key driver");
同样将编译好的代码key_dri.ko复制到开发板上去,可以发现按下按键S2和S3的时候,打印相应的按键信息。