编写一个通用I2C传输驱动

I2C universal transfer driver

一、背景

R818平台
① 没有串口资源了,但是需要一个高速通信方式(相比串口而言)
② 并且尽可能减少使用的pin数,所以选择了i2c
由于想要尽可能跟串口通信很像,就选择了/dev下生成设备的方式,按理说可以用那个通用的i2c总线控制方式在应用层进行简单的封装,但是仔细看了一下,不太符合当前的需求,所以就重新写了一个类串口的i2c通信驱动。
当前需求:
① 非常像串口通信
② 支持阻塞/非阻塞
③ 支持多线程/多进程
④ 支持中断/非中断方式
⑤ 支持多路io复用,select/poll/epoll
这个背景和需求下,催生了这个驱动

二、代码

/*
  I2C universal transfer driver 
  Copyright (C) 2022  YangJie <yangjie98765@yeah.net>

  This program is free software; you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation; either version 2 of the License, or
  (at your option) any later version.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License along
  with this program; if not, write to the Free Software Foundation, Inc.,
  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/

#include <linux/i2c.h>
#include <linux/module.h>
#include <linux/pm.h>
#include <linux/kref.h>
#include <linux/poll.h>
#include <linux/eventpoll.h>
#include <linux/cdev.h>
#include <linux/printk.h>
#include <linux/errno.h>
#include <linux/irq.h>
#include <linux/interrupt.h>
#include <linux/i2c-dev.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
#include <linux/types.h>
#include <linux/atomic.h>
#include <linux/mutex.h>
#include <linux/gpio/consumer.h>

#define NAME_DRIVER      "i2c-transfer"
#define NAME_CLASS_SLAVE "i2c-slave"

struct i2c_transfer_data {
	struct kref kref;
	dev_t devno;
	struct cdev cdev;
	struct device *device;
	struct i2c_client *client;
	wait_queue_head_t wait_read;
	atomic_t need_r;
	atomic_t s_removed;
	struct gpio_desc *gpio_wakeup;
};

static DEFINE_MUTEX(g_mutex_g_major);
static dev_t g_major;
static struct class *g_class;

enum {
	NEED_R_NO_NEED = 0,
	NEED_R_NEED    = 1,
	NEED_R_READING,
};

static irqreturn_t i2c_transfer_interrupt(int irq, void* dev_id)
{
	int ret;
	struct i2c_transfer_data *data = dev_id;

	if (atomic_read(&data->s_removed))
		return IRQ_NONE;

	ret = atomic_cmpxchg(&data->need_r, NEED_R_NO_NEED, NEED_R_NEED);
	if (NEED_R_NO_NEED == ret)
		wake_up_interruptible(&data->wait_read);

	return IRQ_HANDLED;
}

static void __i2c_transfer_data_free_delay(struct kref *kref)
{
	struct i2c_transfer_data *data = container_of(kref, 
						      struct i2c_transfer_data, kref);
	kfree(data);
}

static int i2c_transfer_open(struct inode *inode, struct file *filp)
{
	struct i2c_transfer_data *data = container_of(inode->i_cdev, 
						      struct i2c_transfer_data, cdev);

	filp->private_data = data;
	kref_get(&data->kref);
	return 0;
}

static int i2c_transfer_release(struct inode *inode, struct file *filp)
{
	struct i2c_transfer_data *data = filp->private_data;

	kref_put(&data->kref, __i2c_transfer_data_free_delay);
	return 0;
}

static int __i2c_transfer_wait_read(struct i2c_transfer_data* data)
{
	int ret = 0;
	DEFINE_WAIT(wait);

	for (;;) {
		if (atomic_read(&data->s_removed)) {
			ret = -ENODEV;
			break;
		}

		if (signal_pending(current)) {
			ret = -ERESTARTSYS;
			break;
		}

		prepare_to_wait_exclusive(&data->wait_read, &wait, TASK_INTERRUPTIBLE);

		if (NEED_R_NEED == atomic_cmpxchg(&data->need_r, NEED_R_NEED, 
						  NEED_R_READING)) {
			break;
		}

		schedule();
	}
	finish_wait(&data->wait_read, &wait);

	return ret;
}

static ssize_t i2c_transfer_read(struct file *filp, char __user *buf, 
				 size_t count, loff_t *ppos)
{
	int ret, need_r;
	u8 *kbuf;
	struct i2c_transfer_data *data = filp->private_data;
	bool nonblock = filp->f_flags & O_NONBLOCK;

	if (unlikely(count <= 0))
		return 0;

	kbuf = kmalloc(count, GFP_KERNEL);
	if (!kbuf)
		return -ENOMEM;

	need_r = atomic_cmpxchg(&data->need_r, NEED_R_NEED, NEED_R_READING);
	if (NEED_R_NO_NEED == need_r || NEED_R_READING == need_r) {
		if (nonblock) {
			ret = NEED_R_READING == need_r ? -EBUSY : -EAGAIN;
			goto err_only_free;
		}

		ret = __i2c_transfer_wait_read(data);
		if (ret) {
			goto err_only_free;
		}
	}

	ret = i2c_master_recv(data->client, kbuf, count);
	if (ret < 0)
		goto err;

	if (copy_to_user(buf, kbuf, ret)) {
		ret = -EFAULT;
		goto err;
	}

	need_r = data->client->irq > 0 ? NEED_R_NO_NEED : NEED_R_NEED;
	atomic_set(&data->need_r, need_r); /* atomic_cmpxchg(&data->need_r, NEED_R_READING, need_r); */
	if (NEED_R_NEED == need_r)
		wake_up_interruptible(&data->wait_read);

	kfree(kbuf);
	return ret;

err:
	atomic_set(&data->need_r, NEED_R_NEED); /* atomic_cmpxchg(&data->need_r, NEED_R_READING, NEED_R_NEED); */
	wake_up_interruptible(&data->wait_read);
err_only_free:
	kfree(kbuf);
	return ret;
}

static ssize_t i2c_transfer_write(struct file *filp, const char __user *buf, 
				  size_t count, loff_t *ppos)
{
	int ret;
	u8 *kbuf;
	struct i2c_transfer_data *data = filp->private_data;

	if (unlikely(count <= 0))
		return 0;

	kbuf = kmalloc(count, GFP_KERNEL);
	if (!kbuf)
		return -ENOMEM;

	if (copy_from_user(kbuf, buf, count)) {
		ret = -EFAULT;
		goto err;
	}

	ret = i2c_master_send(data->client, kbuf, count);

err:
	kfree(kbuf);
	return ret;
}

static unsigned int i2c_transfer_poll(struct file *filp, struct poll_table_struct *wait)
{
	int ret;
	struct i2c_transfer_data *data = filp->private_data;
	unsigned int mask = 0;

	poll_wait(filp, &data->wait_read, wait);

	if (atomic_read(&data->s_removed)) {
		mask |= POLLHUP | EPOLLHUP | POLLERR;
		goto end;
	}

	ret = atomic_read(&data->need_r);
	if (NEED_R_NEED == ret)
		mask |= POLLIN | EPOLLIN | POLLOUT | EPOLLOUT;
	else if (NEED_R_NO_NEED == ret)
		mask |= POLLOUT | EPOLLOUT;

end:
	return mask;
}

static int i2c_transfer_suspend(struct device *dev)
{
	struct i2c_transfer_data *data = dev_get_drvdata(dev);

	if (!data || atomic_read(&data->s_removed))
		return 0;

	if (data->client->irq > 0)
		disable_irq(data->client->irq);
	return 0;
}

static int i2c_transfer_resume(struct device *dev)
{
	struct i2c_transfer_data *data = dev_get_drvdata(dev);

	if (!data || atomic_read(&data->s_removed))
		return 0;

	if (data->client->irq > 0)
		enable_irq(data->client->irq);
	return 0;
}
static SIMPLE_DEV_PM_OPS(i2c_transfer_pm_ops, i2c_transfer_suspend, 
			 i2c_transfer_resume);

static const struct file_operations i2c_transfer_fops = {
	.owner          = THIS_MODULE,
	.read           = i2c_transfer_read,
	.write          = i2c_transfer_write,
	.open           = i2c_transfer_open,
	.release        = i2c_transfer_release,
	.poll           = i2c_transfer_poll,
};

static int __i2c_transfer_properties_set(struct i2c_transfer_data *data)
{
	int ret;
	struct device *dev = &data->client->dev;
	struct gpio_desc *gpio_wakeup;

	if (data->client->irq > 0)
		return 0;

	gpio_wakeup = gpiod_get_optional(dev, "wakeup", GPIOD_IN);
	if (IS_ERR_OR_NULL(gpio_wakeup)) {
		dev_dbg(dev, "No gpio for wakeup!\n");
		return gpio_wakeup ? PTR_ERR(gpio_wakeup) : 0;
	}

	ret = gpiod_to_irq(gpio_wakeup);
	if (ret <= 0) {
		dev_err(dev, "Failed to get irq number by gpio!\n");
		goto err;
	}
	data->client->irq = ret;
	data->gpio_wakeup = gpio_wakeup;

	dev_notice(dev, "Get irq number(%d) from gpio!\n", 
		   data->client->irq);
	return 0;

err:
	gpiod_put(gpio_wakeup);
	return ret;
}

static int __i2c_transfer_properties_put(struct i2c_transfer_data *data)
{
	if (!data->gpio_wakeup)
		return 0;

	gpiod_put(data->gpio_wakeup);
	data->gpio_wakeup = NULL;
	data->client->irq = 0;
	return 0;
}

static int i2c_transfer_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
	int ret;
	struct i2c_transfer_data *data;
	struct device *dev = &client->dev;
	int minor = (client->adapter->nr << 8) | client->addr;

	if (!i2c_check_functionality(client->adapter, I2C_FUNC_I2C))
		return -ENODEV;

	data = kzalloc(sizeof(*data), GFP_KERNEL);
	if (!data) {
		dev_err(dev, "Failed to alloc memory for private data!\n");
		ret = -ENOMEM;
		goto err_alloc;
	}

	kref_init(&data->kref);

	data->client = client;
	ret = __i2c_transfer_properties_set(data);
	if (ret < 0)
		goto err_properties;

	atomic_set(&data->s_removed, 0);
	atomic_set(&data->need_r, !client->irq);
	init_waitqueue_head(&data->wait_read);

	if (client->irq > 0) {
		ret = request_irq(client->irq, i2c_transfer_interrupt, 
				  IRQF_TRIGGER_FALLING, "i2c_transfer", data);
		if (ret) {
			dev_err(dev, "Failed to request irq(%d)!\n", 
				client->irq);
			goto err_irq;
		}
	}

	cdev_init(&data->cdev, &i2c_transfer_fops);

	mutex_lock(&g_mutex_g_major);
	if (!g_major) {
		ret = alloc_chrdev_region(&data->devno, minor, 1, "i2c-slave");
		if (ret) {
			dev_err(dev, "Failed to alloc cdev number!\n");
			mutex_unlock(&g_mutex_g_major);
			goto err_devno;
		}
		g_major = MAJOR(data->devno);
	} else {
		data->devno = MKDEV(g_major, minor);
		ret = register_chrdev_region(data->devno, 1, "i2c-slave");
		if (ret) {
			dev_err(dev, 
				"Failed to register cdev number(0x%x, 0x%x)!\n", 
				MAJOR(data->devno), MINOR(data->devno));
			mutex_unlock(&g_mutex_g_major);
			goto err_devno;
		}
	}
	mutex_unlock(&g_mutex_g_major);

	ret = cdev_add(&data->cdev, data->devno, 1);
	if (ret) {
		dev_err(dev, "Failed to add cdev(%x, %x) to kernel!\n", 
			MAJOR(data->devno), MINOR(data->devno));
		goto err_cdev_add;
	}

	data->device = device_create(g_class, NULL, data->devno, NULL, 
				     "i2c-slave-%03x", minor);
	if (IS_ERR(data->device)) {
		dev_err(dev, "Failed to create i2c-slave(0x%x, 0x%x) device!\n", 
			MAJOR(data->devno), MINOR(data->devno));
		ret = PTR_ERR(data->device);
		data->device = NULL;
		goto err_device_create;
	}

	i2c_set_clientdata(client, data);
	return 0;

err_device_create:
	cdev_del(&data->cdev);
err_cdev_add:
	unregister_chrdev_region(data->devno, 1);
err_devno:
	data->devno = 0;
	free_irq(client->irq, data);
err_irq:
	atomic_set(&data->s_removed, 1);
	atomic_set(&data->need_r, NEED_R_NO_NEED);
	__i2c_transfer_properties_put(data);
err_properties:
	data->client = NULL;
	kref_put(&data->kref, __i2c_transfer_data_free_delay);
err_alloc:
	return ret;
}

static int i2c_transfer_remove(struct i2c_client *client)
{
	struct i2c_transfer_data *data = i2c_get_clientdata(client);

	if (!data)
		return 0;

	atomic_set(&data->s_removed, 1);
	atomic_set(&data->need_r, NEED_R_NO_NEED);
	wake_up_all(&data->wait_read);

	if (client->irq > 0) {
		disable_irq(client->irq);
		free_irq(client->irq, data);
	}

	device_destroy(g_class, data->devno);
	cdev_del(&data->cdev);
	unregister_chrdev_region(data->devno, 1);
	__i2c_transfer_properties_put(data);

	kref_put(&data->kref, __i2c_transfer_data_free_delay);
	return 0;
}

#ifdef CONFIG_OF
static const struct of_device_id i2c_transfer_of_match[] = {
	{ .compatible = "universal,i2c-transfer", },
	{},
};
MODULE_DEVICE_TABLE(of, i2c_transfer_of_match);
#endif

static const struct i2c_device_id i2c_transfer_id[] = {
	{ NAME_DRIVER, 0 },
	{},
};
MODULE_DEVICE_TABLE(i2c, i2c_transfer_id);

static struct i2c_driver i2c_transfer_driver = {
	.driver = {
		.name  = NAME_DRIVER,
		.owner = THIS_MODULE,
		.of_match_table = i2c_transfer_of_match,
		.pm    = &i2c_transfer_pm_ops,
	},
	.probe  = i2c_transfer_probe,
	.remove = i2c_transfer_remove,
	.id_table = i2c_transfer_id,
};

static int __init i2c_transfer_init(void)
{
	g_major = 0;
	if (!g_class) {
		struct class *t_class = class_create(THIS_MODULE, 
						     NAME_CLASS_SLAVE);
		if (IS_ERR(t_class)) {
			pr_err("Failed to create class(%s)!\n", 
			       NAME_CLASS_SLAVE);
			return PTR_ERR(t_class);
		}
		g_class = t_class;
	}

	return i2c_add_driver(&i2c_transfer_driver);
}

static void __exit i2c_transfer_exit(void)
{
	i2c_del_driver(&i2c_transfer_driver);

	if (g_class)
		class_destroy(g_class);
	g_class = NULL;
	g_major = 0;
}

module_init(i2c_transfer_init);
module_exit(i2c_transfer_exit);

MODULE_DESCRIPTION("I2C universal transfer driver");
MODULE_LICENSE("GPL");
MODULE_VERSION("1.0.0");

        i2c_transfer@66 {
                compatible = "universal,i2c-transfer";
                reg = <0x66>;
                wakeup-gpios = <&pio PH 10 1 1 1 0>;
                status = "okay";
        };

        i2c_transfer@22 {
                compatible = "universal,i2c-transfer";
                reg = <0x22>;
                status = "okay";
        };

三、测试

编写代码涵盖以下几项即可
① 阻塞/非阻塞读写 * 常规/select/poll/epoll,2 * 4 = 8种情况
② ①的情况还可以分为中断方式(得到中断允许read,没有中断就阻塞)和非中断方式(也就是read不会阻塞,只要发起read就允许)
③ 读写测试和insmod/rmmod搭配
④ 测试过程和休眠/唤醒搭配

四、阻塞/非阻塞优先级

当前代码在同时满足以下条件时,存在不同优先级。
① 如果同时存在阻塞(block)和非阻塞(nonblock)读取两种情况。
那么当前代码的现象是nonblock的优先级高于block(但不会出现“饿死”现象)。
如果做试验的话,现象是相同次数要求情况下,多个nonblock读取,多个block读取,前期nonblock和block读取穿插进行,nonblock总体偏多,后期只剩下block,nonblock已经完成。

由于当前没有这样的需求,所以暂未添加策略来平衡优先级。如果对于这种优先级有需求的话,可以考虑采取一定的策略进行平衡。

0、原因分析

同时存在多个task进行读取时,nonblock和block都在“公平竞争”原子变量或者说是“换了种写法的临界区”。
由于block在“竞争失败”时,采取的策略是挂到队列上进行timeout方式的让出cpu,等待条件达成时,被唤醒,重新参与调度和“竞争”。
而nonblock采取的策略则是直接退出并返回应用层,除了返回应用层前可能存在的抢占检查(不论是否是CONFIG_PREEMPT抢占内核都涉及检查),中断上下文等这几种方式可能导致nonblock对应的task会让出cpu外,很难让这个task让出cpu,除非应用层自行sleep。
虽然read结束时会尝试唤醒,但是唤醒也仅仅时让task进入就绪队列,并不意味着直接调度。
在这个背景下,nonblock确实更加可能更早一步进行“临界区”的“竞争”,并且还很容易出现“连成片”的“竞争成功”(多个nonblock的task可能成为“帮凶”,参考读写锁的“饿死现象”)。

五、结语

代码仅供参考,欢迎指正。

posted @ 2025-07-18 17:28  J&YANG  阅读(17)  评论(0)    收藏  举报