基于Linux3.10的设备驱动实例,
首先是字符设备的框架搭建,该框架可以被很多场合稍加修改即可使用;在完成字符设备驱动之后,该字符设备并不依赖于真实物理硬件,调试起来很方便,LDD一书中scull例子比这里稍微要复杂,主要体现在其基于数据结构的读写方法上,其实字符驱动设备的框架和读写没有关系,所以下面的例子还是挺有用的
在字符设备完成的基础之上搭建一个i2c设备侧驱动,关于i2c驱动的client侧和adapter侧的关系见《编写i2c驱动-基于Linux3.10》,这里的驱动是让client能够正常工作的驱动,所以这依赖于adapter侧驱动必须先加载,由于SOC芯片厂商的SDK中都会提供其芯片自身的i2c控制器的驱动,所以这通常不是一个问题。但是这是依赖于具体硬件的,毕竟没有使用两条线模拟时钟线和数据线的读写时序。
在完成i2c驱动设备之后,由于示例较为简单,中断中处理了所有的接收字符的工作,通常在中断中处理函数中会使用tasklet或者work queue的方式,将任务推后执行以尽量提高中断可用的状态,所以在此基础之上使用work queue对接收i2c接收进行了优化。当然还有中断的共享,这里没有体现了。
字符设备
字符设备的驱动简单模板如下:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/irq.h>
#include <linux/interrupt.h>
#include <linux/i2c.h>
#include <linux/proc_fs.h>
#include <linux/device.h>
#include <linux/slab.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#include <linux/poll.h>
#include <linux/delay.h>
#include <linux/io.h>
#include <linux/stat.h>
#include <linux/workqueue.h>
#include <linux/spinlock_types.h>
#include <linux/clk.h>
#include <plat/timer.h>
#define DEV_NAME "template_cdev"
static int dev_major = 0;
static int dev_minor = 0;
static int NUMBER_DEVICES = 1;
struct class *cdev_class = NULL;
dev_t cdev_devno;
struct cdev_dev {
struct cdev cdev;
};
struct cdev_dev *cdev_device = NULL;
static volatile int cdev_interrupt_flag = 0;
static DEFINE_SEMAPHORE(cdev_lock);
static DEFINE_SEMAPHORE(cdev_write_lock);
static int cdev_open(struct inode * inode , struct file * filp)
{
struct cdev_dev *dev = NULL;
dev = container_of(inode->i_cdev, struct cdev_dev, cdev);
if (dev == NULL){
printk(KERN_EMERG "DEV == null\n");
return -ENXIO;
}
filp->private_data = dev;
if(down_trylock(&cdev_lock)) {
printk("template_cdev is busy,please try later\n");
return -EBUSY;
}
}
static int cdev_release(struct inode * inode, struct file *filp)
{
struct cdev_dev *i_dev = filp->private_data;
up(&cdev_lock);
return 0;
}
static ssize_t cdev_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos)
{
int ret;
struct cdev_dev *i_dev = filp->private_data;
return ret;
}
static ssize_t cdev_write(struct file *filp, const char __user *buf, size_t size, loff_t *ppos)
{
int ret = 0;
struct cdev_dev *i_dev = filp->private_data;
return ret;
}
struct file_operations cdev_fops = {
.owner = THIS_MODULE,
.open = cdev_open,
.release = cdev_release,
.read = cdev_read,
.write = cdev_write,
};
static int cdev_setup(struct class *dev_class, struct cdev_dev *dev, int index)
{
int err = 0;
struct device *class_dev = NULL;
int devno = MKDEV(dev_major, index);
if ((NULL == dev_class) || (NULL == dev)) {
pr_err("The parameter is NULL!\n");
return -EFAULT;
}
cdev_init(&dev->cdev, &cdev_ops);
dev->cdev.owner = THIS_MODULE;
err = cdev_add(&dev->cdev, devno, 1);
printk (KERN_WARNING " cdev_setup\n");
if (err) {
pr_err("Error %d adding %s%d", err, DEV_NAME, index);
return err;
}
class_dev = device_create(dev_class, NULL, devno,"template cdev",NULL)
if(IS_ERR(class_dev)) {
pr_err("class_device_create() failure!\n");
err = -EFAULT;
}
return err;
}
static __init int cdev_init(void)
{
int ret;
if (dev_major) {
cdev_devno = MKDEV(dev_major, dev_minor);
ret = register_chrdev_region(cdev_devno, NUMBER_DEVICES, DEV_NAME);
} else {
ret = alloc_chrdev_region(&cdev_devno, dev_minor, NUMBER_DEVICES, DEV_NAME);
dev_major = MAJOR(cdev_devno);
}
if (ret<0) {
printk (KERN_WARNING "can't get major number %d\n", dev_major);
return ret;
}
cdev_device = kzalloc(NUMBER_DEVICES*sizeof(struct cdev_dev), GFP_KERNEL);
if (NULL == cdev_device){
ret = -ENOMEM;
goto fail_request_region;
}
cdev_class = class_create(THIS_MODULE, DEV_NAME);
if (IS_ERR(cdev_class)) {
printk("Err: failed in creating template cdev class.\n");
ret = -PTR_ERR(cdev_class);
goto fail_alloc;
}
ret = cdev_setup(cdev_class, cdev_device, dev_minor);
if (ret != 0) {
goto fail_setup;
}
return 0;
fail_setup:
class_destroy(cdev_class);
fail_alloc:
if (cdev_device != NULL) {
kfree(cdev_device);
cdev_device = NULL;
}
fail_request_region:
unregister_chrdev_region(cdev_devno, NUMBER_DEVICES);
return ret;
}
static void cdev_exit(void)
{
dev_t devno = MKDEV(dev_major, dev_minor);
device_destroy(cdev_class, devno);
cdev_del(&cdev_device->cdev);
class_destroy(cdev_class);
kfree(cdev_device);
cdev_device = NULL;
unregister_chrdev_region(devno, NUMBER_DEVICES);
}
module_init(cdev_init);
module_exit(cdev_exit);
MODULE_LICENSE("GPL");
字符设备之上的i2c设备
设备树如下:
/ {
mode = “ABCD corp Board”;
compatible = “ABCD,exp”;
chosen = {
bootargs = “console=ttySA0 init=/linuxrc root=…”
};
apb@80000000{
i2c0:i2c80006000{
ak4951:code@5{
compatible=”A, ak4951”;
reg =<0x12>;
};
i2c2: i2c@e8007000 {
compatible = "A,i2c";
status = "ok";
Template: Template@48 {
compatible = "Template_i2c";
reg = <0x48>;
};
Template {
Template_i2c= <&Template>;
irq_pin = <13>;
};
};
# fdt getnode /
/
/chosen
/aliases
/memory
/cpus
/cpus/cpu@0
/apb@e8000000
/apb@e8000000/timer@e800b000
/apb@e8000000/i2c@e8007000/Template@90
/ahb@e0000000
/ahb@e0000000/dma@e0005000
/ahb@e0000000/nand@e0001000
/ahb@e0000000/uart@e0032000
...
i2c外设驱动
如果不是基于设备树方法,则会走register_driver路线,在这个函数中prob额方法会被调用,该函数完成扫描设备链表,查找和驱动是否匹配,如果匹则会进行绑定。基于设备树方法,设备信息的获取是通过解析设备树实现的。
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/irq.h>
#include <linux/interrupt.h>
#include <linux/i2c.h>
#include <linux/proc_fs.h>
#include <linux/device.h>
#include <linux/slab.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#include <linux/poll.h>
#include <linux/delay.h>
#include <linux/of.h>
#include <linux/of_irq.h>
#include <linux/of_address.h>
#include <linux/of_fdt.h>
#include <linux/of_gpio.h>
#include <linux/of_platform.h>
#include <linux/of_i2c.h>
#include <linux/gpio.h>
#include <linux/io.h>
#include <linux/stat.h>
#include <linux/workqueue.h>
#include <linux/spinlock_types.h>
#include <linux/clk.h>
#include <plat/timer.h>
#define template_i2c_DEV_NAME "template_i2c"
static int dev_major = 0;
static int dev_minor = 0;
static int NUMBER_DEVICES = 1;
struct class *template_i2c_class = NULL;
dev_t template_i2c_devno;
struct template_i2c_dev {
int irq;
unsigned int irq_pin;
struct cdev cdev;
struct i2c_client *template_i2c_client;
};
struct template_i2c_dev *template_i2c_device = NULL;
static int i2c_write_reg(struct i2c_client *client, u8 reg, u8 val);
static int i2c_write_reg(struct i2c_client *client, u8 reg, u8 val)
{
int err = 0;
struct i2c_msg msg[1];
u8 data[2];
if (!client->adapter) {
err = -ENODEV;
goto out;
} else {
msg[0].addr = client->addr;
msg[0].flags = 0;
msg[0].len = 2;
msg[0].buf = data;
data[0] = reg;
data[1] = val;
err = i2c_transfer(client->adapter, msg, 1);
if(err < 0) {
err = -EIO;
goto out;
}
}
return 0;
out:
return err;
}
static int i2c_read_reg(struct i2c_client *client, u8 reg, u8 *val)
{
int err = 0;
struct i2c_msg msg[2] = {
{
.addr = client->addr,
.flags = 0,
.len = 1,
.buf = ®,
}, {
.addr = client->addr,
.flags = I2C_M_RD,
.len = 1,
.buf = val,
}
};
if (!client->adapter) {
err = -ENODEV;
goto out;
} else {
err = i2c_transfer(client->adapter, msg, 2);
if(err < 0) {
err = -EIO;
goto out;
}
}
out:
return err;
}
static int template_i2c_open(struct inode * inode , struct file * filp)
{
struct template_i2c_dev *dev = NULL;
dev = container_of(inode->i_cdev, struct template_i2c_dev, cdev);
if (dev == NULL){
printk(KERN_EMERG "DEV == null\n");
return -ENXIO;
}
filp->private_data = dev;
if(down_trylock(&template_i2c_lock)) {
printk("template_i2c_dev is busy,please try later\n");
return -EBUSY;
}
if (gpio_request(dev->irq_pin, "irq_line") < 0) {
printk(KERN_ERR "fail to requet gpio%d.\r\n", dev->irq_pin);
return -EIO;
}
if(gpio_direction_input(dev->irq_pin) < 0) {
printk(KERN_ERR "set GPIO%d to input failed.\r\n", dev->irq_pin);
return -EIO;
}
printk(KERN_EMERG "template_i2c_open!request_irq\n");
if (request_irq(dev->irq, &template_i2c_irq, IRQ_TYPE_EDGE_FALLING, "template_i2c", template_i2c_device)) {
printk("request_irq failed!\n");
free_irq(dev->irq, NULL);
return -EBUSY;
}
return 0;
}
static int template_i2c_release(struct inode * inode, struct file *filp)
{
struct template_i2c_dev *i_dev = filp->private_data;
gpio_free(i_dev->irq_pin);
free_irq(i_dev->irq, i_dev);
up(&template_i2c_lock);
return 0;
}
static ssize_t template_i2c_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos)
{
int err;
return err;
}
static ssize_t template_i2c_write(struct file *filp, const char __user *buf, size_t size, loff_t *ppos)
{
int i = 0;
return i;
}
struct file_operations template_i2c_fops = {
.owner = THIS_MODULE,
.open = template_i2c_open,
.release = template_i2c_release,
.read = template_i2c_read,
.write = template_i2c_write,
};
static int of_template_i2c_parse(struct template_i2c_dev *i2cdev)
{
int rval = 0;
struct device_node *of_node, *i2c_node;
if (NULL == i2cdev) {
return -EINVAL;
}
of_node = of_find_node_by_path("/template_i2c");
if (NULL == of_node) {
pr_err("get template_i2c device-tree error!\n");
return -ENXIO;
} else {
i2c_node = of_parse_phandle(of_node, "template_i2c_i2c", 0);
if (NULL == i2c_node) {
pr_err("get i2c_client device-tree property error!\n");
return -ENXIO;
}
}
i2cdev->template_i2c_client = of_find_i2c_device_by_node(i2c_node);
if (NULL == i2cdev->template_i2c_client) {
pr_err("get i2c_client device error!\n");
return -ENXIO;
}
rval= of_property_read_u32(of_node, "irq_pin", &i2cdev->irq_pin);
printk(KERN_EMERG "The PIn is :%d", i2cdev->irq_pin);
i2cdev->irq = gpio_to_irq(i2cdev->irq_pin);
return rval;
}
static int template_cdev_setup(struct class *dev_class, struct template_i2c_dev *dev, int index)
{
int err = 0;
struct device *class_dev = NULL;
int devno = MKDEV(dev_major, index);
if ((NULL == dev_class) || (NULL == dev)) {
pr_err("The parameter is NULL!\n");
return -EFAULT;
}
cdev_init(&dev->cdev, &template_i2c_fops);
dev->cdev.owner = THIS_MODULE;
err = cdev_add(&dev->cdev, devno, 1);
if (err) {
pr_err("Error %d adding %s%d", err, template_i2c_DEV_NAME, index);
return err;
}
class_dev = device_create(dev_class, NULL, devno, "template cdev", NULL);
if(IS_ERR(class_dev)) {
pr_err("class_device_create() failure!\n");
err = -EFAULT;
}
return err;
}
static __init int template_i2c_init(void)
{
int ret;
if (dev_major) {
template_i2c_devno = MKDEV(dev_major, dev_minor);
ret = register_chrdev_region(template_i2c_devno, NUMBER_DEVICES, template_i2c_DEV_NAME);
} else {
ret = alloc_chrdev_region(&template_i2c_devno, dev_minor, NUMBER_DEVICES, template_i2c_DEV_NAME);
dev_major = MAJOR(template_i2c_devno);
}
if (ret<0) {
printk (KERN_WARNING "can't get major number %d\n", dev_major);
return ret;
}
template_i2c_device = kzalloc(NUMBER_DEVICES*sizeof(struct template_i2c_dev), GFP_KERNEL);
if (NULL == template_i2c_device){
ret = -ENOMEM;
goto fail_request_region;
}
if ((ret = of_template_i2c_parse(template_i2c_device)) < 0) {
printk("Err: parse template_i2c_device node failed!\n");
goto fail_alloc;
}
template_i2c_class = class_create(THIS_MODULE, template_i2c_DEV_NAME); //类名为template_i2c_class
if (IS_ERR(template_i2c_class)) {
ret = -PTR_ERR(template_i2c_class);
goto fail_alloc;
}
ret = template_cdev_setup(template_i2c_class, template_i2c_device, dev_minor);
if (ret != 0) {
goto fail_setup;
}
return 0;
fail_setup:
class_destroy(template_i2c_class);
fail_alloc:
if (template_i2c_device != NULL){
kfree(template_i2c_device);
template_i2c_device = NULL;
}
fail_request_region:
unregister_chrdev_region(template_i2c_devno, NUMBER_DEVICES);
return ret;
}
static void template_i2c_exit(void)
{
dev_t devno = MKDEV(dev_major, dev_minor);
device_destroy(template_i2c_class, devno);
cdev_del(&template_i2c_device->cdev);
class_destroy(template_i2c_class);
kfree(template_i2c_device);
template_i2c_device = NULL;
unregister_chrdev_region(devno, NUMBER_DEVICES);
}
module_init(template_i2c_init);
module_exit(template_i2c_exit);
MODULE_LICENSE("GPL");
中断使用工作队列推迟执行
<pre name="code" class="cpp">#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/irq.h>
#include <linux/interrupt.h>
#include <linux/i2c.h>
#include <linux/proc_fs.h>
#include <linux/device.h>
#include <linux/slab.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#include <linux/poll.h>
#include <linux/delay.h>
#include <linux/of.h>
#include <linux/of_irq.h>
#include <linux/of_address.h>
#include <linux/of_fdt.h>
#include <linux/of_gpio.h>
#include <linux/of_platform.h>
#include <linux/of_i2c.h>
#include <linux/gpio.h>
#include <linux/io.h>
#include <linux/stat.h>
#include <linux/sched.h>
#include <linux/kthread.h>
#incldue <linux/semaphore.h>
#include <linux/workqueue.h>
#include <linux/spinlock_types.h>
#include <linux/clk.h>
#include <plat/timer.h>
#define template_i2c_DEV_NAME "template_i2c"
static int dev_major = 0;
static int dev_minor = 0;
static int NUMBER_DEVICES = 1;
struct class *template_i2c_class = NULL;
dev_t template_i2c_devno;
struct template_i2c_dev {
int irq;
unsigned int irq_pin;
struct cdev cdev;
struct i2c_client *template_i2c_client;
struct semaphore sem;
wait_queue_head_t recv_wq;
struct workqueue_struct *recv_work;
struct work_struct restart;
};
struct template_i2c_dev *template_i2c_device = NULL;
static DECLARE_WAIT_QUEUE_HEAD(waitq);
static void template_delay_workqueue(struct work_struct *data)
{
stuct template_i2c_device *i_dev = container_of(data, struct template_i2c_dev, restart);
if (down_interruptible(&i_dev->sem) != 0)
printk(" semaphore get failed~!\n");
....
wake_up_interruptible(&waitq);
up(&i_dev->sem)
}
static int i2c_write_reg(struct i2c_client *client, u8 reg, u8 val);
static irqreturn_t template_i2c_irq(int irq, void *dev_id)
{
queue_work(template_i2c_device->recv_work, & template_i2c_device->restart);
...
return IRQ_RETVAL(IRQ_HANDLED);
}
static int i2c_write_reg(struct i2c_client *client, u8 reg, u8 val)
{ int err = 0;
struct i2c_msg msg[1];
u8 data[2];
if (!client->adapter) {
err = -ENODEV;
goto out;
} else {
msg[0].addr = client->addr;
msg[0].flags = 0;
msg[0].len = 2;
msg[0].buf = data;
data[0] = reg;
data[1] = val;
err = i2c_transfer(client->adapter, msg, 1);
if(err < 0) {
err = -EIO;goto out;}
}
return 0;
out:
return err;
}
static int i2c_read_reg(struct i2c_client *client, u8 reg, u8 *val)
{
int err = 0;
struct i2c_msg msg[2] = {
{.addr = client->addr,.flags = 0,.len = 1,.buf = ®,},
{.addr = client->addr,.flags = I2C_M_RD,.len = 1,.buf = val,}
};
if (!client->adapter) {
err = -ENODEV;
goto out;
} else {
err = i2c_transfer(client->adapter, msg, 2);
if(err < 0) {
err = -EIO;
goto out; }
}
out:
return err;
}
static int template_i2c_open(struct inode * inode , struct file * filp)
{
struct template_i2c_dev *dev = NULL;
dev = container_of(inode->i_cdev, struct template_i2c_dev, cdev);
if (dev == NULL){
printk(KERN_EMERG "DEV == null\n");
return -ENXIO;
}
filp->private_data = dev;
if(down_trylock(&template_i2c_lock)) {
printk("template_i2c_dev is busy,please try later\n");return -EBUSY;
}
if (gpio_request(dev->irq_pin, "irq_line") < 0) {
printk(KERN_ERR "fail to requet gpio%d.\r\n", dev->irq_pin);
return -EIO;
}
if(gpio_direction_input(dev->irq_pin) < 0) {
printk(KERN_ERR "set GPIO%d to input failed.\r\n", dev->irq_pin);
return -EIO;
}
printk(KERN_EMERG "template_i2c_open!request_irq\n");
if (request_irq(dev->irq, &template_i2c_irq, IRQ_TYPE_EDGE_FALLING, "template_i2c", template_i2c_device)) {
printk("request_irq failed!\n");free_irq(dev->irq, NULL);
return -EBUSY;
}return 0;
}
static int template_i2c_release(struct inode * inode, struct file *filp)
{
struct template_i2c_dev *i_dev = filp->private_data;
gpio_free(i_dev->irq_pin);
free_irq(i_dev->irq, i_dev);
up(&template_i2c_lock); return 0;
}
static ssize_t template_i2c_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos)
{
int err;
return err;
}
static ssize_t template_i2c_write(struct file *filp, const char __user *buf, size_t size, loff_t *ppos)
{ int i = 0;
return i;
}
unsigned int template_i2c_poll(struct file *file, struct poll_table_struct *wait)
{
poll_wait(file, &waitq, wait);
...
mask |= POLLIN;
return mask;
}
struct file_operations template_i2c_fops = {
.owner = THIS_MODULE,
.open = template_i2c_open,
.release = template_i2c_release,
.read = template_i2c_read,
.write = template_i2c_write,
.poll = template_i2c_poll;
};
static int of_template_i2c_parse(struct template_i2c_dev *i2cdev)
{
int rval = 0;
struct device_node *of_node, *i2c_node;
if (NULL == i2cdev) {
return -EINVAL;
}
of_node = of_find_node_by_path("/template_i2c");
if (NULL == of_node) {
pr_err("get template_i2c device-tree error!\n");
return -ENXIO;
} else {
i2c_node = of_parse_phandle(of_node, "template_i2c_i2c", 0);
if (NULL == i2c_node) {
pr_err("get i2c_client device-tree property error!\n");
return -ENXIO;
}
}
i2cdev->template_i2c_client = of_find_i2c_device_by_node(i2c_node);
if (NULL == i2cdev->template_i2c_client) {
pr_err("get i2c_client device error!\n");
return -ENXIO;
}
rval= of_property_read_u32(of_node, "irq_pin", &i2cdev->irq_pin);
printk(KERN_EMERG "The PIn is :%d", i2cdev->irq_pin);
i2cdev->irq = gpio_to_irq(i2cdev->irq_pin);
return rval;
}
static int template_cdev_setup(struct class *dev_class, struct template_i2c_dev *dev, int index)
{
int err = 0;
struct device *class_dev = NULL;
int devno = MKDEV(dev_major, index);
if ((NULL == dev_class) || (NULL == dev)) {
pr_err("The parameter is NULL!\n");
return -EFAULT;
}
cdev_init(&dev->cdev, &template_i2c_fops);
dev->cdev.owner = THIS_MODULE;
err = cdev_add(&dev->cdev, devno, 1);
if (err) {
pr_err("Error %d adding %s%d", err, template_i2c_DEV_NAME, index);
return err;
}
class_dev = device_create(dev_class, NULL, devno, "template cdev", NULL);
if(IS_ERR(class_dev)) {
pr_err("class_device_create() failure!\n");
err = -EFAULT;
}
return err;
}
static __init int template_i2c_init(void)
{
int ret;
if (dev_major) {
template_i2c_devno = MKDEV(dev_major, dev_minor);
ret = register_chrdev_region(template_i2c_devno, NUMBER_DEVICES, template_i2c_DEV_NAME);
} else {
ret = alloc_chrdev_region(&template_i2c_devno, dev_minor, NUMBER_DEVICES, template_i2c_DEV_NAME);
dev_major = MAJOR(template_i2c_devno);
}
if (ret<0) {
printk (KERN_WARNING "can't get major number %d\n", dev_major);
return ret;
}
template_i2c_device = kzalloc(NUMBER_DEVICES*sizeof(struct template_i2c_dev), GFP_KERNEL);
if (NULL == template_i2c_device){
ret = -ENOMEM;
goto fail_request_region;
}
if ((ret = of_template_i2c_parse(template_i2c_device)) < 0) {
printk("Err: parse template_i2c_device node failed!\n");
goto fail_alloc;
}
template_i2c_class = class_create(THIS_MODULE, template_i2c_DEV_NAME); //类名为template_i2c_class
if (IS_ERR(template_i2c_class)) {
ret = -PTR_ERR(template_i2c_class);
goto fail_alloc;
}
ret = template_cdev_setup(template_i2c_class, template_i2c_device, dev_minor);
if (ret != 0) {
goto fail_setup;
}
INIT_WORK(&template_i2c_device->restart,template_delay_workqueue );
init_waitqueue_head(&template_i2c_device->recv_wq);
sema_init(&template_i2c_device->sem,1);
return 0;
fail_setup:
class_destroy(template_i2c_class);
fail_alloc:
if (template_i2c_device != NULL){
kfree(template_i2c_device);
template_i2c_device = NULL;
}
fail_request_region:
unregister_chrdev_region(template_i2c_devno, NUMBER_DEVICES);
return ret;
}
static void template_i2c_exit(void)
{
dev_t devno = MKDEV(dev_major, dev_minor);
device_destroy(template_i2c_class, devno);
cdev_del(&template_i2c_device->cdev);
class_destroy(template_i2c_class);
kfree(template_i2c_device);
template_i2c_device = NULL;
unregister_chrdev_region(devno, NUMBER_DEVICES);
}
module_init(template_i2c_init);
module_exit(template_i2c_exit);
MODULE_LICENSE("GPL");