自学内容网 自学内容网

编写一个通用的i2c设备驱动框架

往期内容

I2C子系统专栏:

  1. I2C(IIC)协议讲解-CSDN博客
  2. SMBus 协议详解-CSDN博客
  3. I2C相关结构体讲解:i2c_adapter、i2c_algorithm、i2c_msg-CSDN博客
  4. 内核提供的通用I2C设备驱动I2c-dev.c分析:注册篇
  5. 内核提供的通用I2C设备驱动I2C-dev.c分析:file_ops篇
  6. 设备驱动与设备树匹配机制详解

总线和设备树专栏:

  1. 总线和设备树_憧憬一下的博客-CSDN博客
  2. 设备树与 Linux 内核设备驱动模型的整合-CSDN博客

1.I2C总线驱动

img
I2C Core就是I2C核心层,它的作用:

  • 提供统一的访问函数,比如i2c_transfer、i2c_smbus_xfer等
  • 实现I2C总线-设备-驱动模型,管理:I2C设备(i2c_client)、I2C设备驱动(i2c_driver)、I2C控制器(i2c_adapter)

2.框图

在上一章节已经讲过,只要把i2c_client(一个i2c设备的抽象)当作platform_device就行,其实原理都差不多。具体直接看下图就行:

img

除了i2c client结构体,其它的结构体在之前的章节也全都介绍过:

I2C相关结构体讲解:i2c_adapter、i2c_algorithm、i2c_msg-CSDN博客

3.结构体

3.1 i2c_driver

在 Linux 内核中,I2C (Inter-Integrated Circuit) 驱动框架用于处理连接在 I2C 总线上的设备。驱动程序通过 i2c_driver 结构体来表示,而每个 I2C 设备则通过 i2c_client 结构体来表示。

i2c_driver 是 Linux 内核中的结构体,用于描述 I2C 驱动。它包含与设备匹配的规则和驱动的核心操作函数,如 proberemove。I2C 驱动可通过以下两种方式与设备匹配:

使用 of_match_table 来判断(第二种匹配方式)

  • 设备树匹配:设备树 (Device Tree) 是硬件的描述文件,I2C 驱动程序通过 of_match_table 来与设备树中的设备节点匹配。

    • 当设备树中某个 I2C 控制器节点下的 I2C 设备节点的 compatible 属性与 i2c_driver 中的 of_match_table 表中的compatible[128]相同时,匹配成功。

使用name 匹配:(第四种匹配方式)

  • i2c_client 结构体中的 name 字段与 i2c_driver.device_driver.name 的值相同,也会匹配成功。

使用 id_table 来判断(第三种匹配方式)

  • i2c_client.name 匹配i2c_client 中的 name 字段与 id_table 中的某一项匹配时,也会匹配成功。

i2c_driveri2c_client 成功匹配后,驱动的 probe 函数会被调用,开始初始化设备。

其实和上一章节讲的的平台设备匹配,道理是几乎相同的,具体看下面链接:

img

img

3.2 i2c_client

i2c_client表示一个I2C设备,创建i2c_client的方法有4种:

方法1

  • 通过I2C bus number来创建
    • 函数 i2c_register_board_info() 允许通过 I2C 总线编号创建 i2c_client。常用于板级文件中提前注册设备信息
int i2c_register_board_info(int busnum, struct i2c_board_info, const *info, unsigned len); 
  • 通过设备树来创建
i2c1: i2c@400a0000 {
/* ... master properties skipped ... */
clock-frequency = <100000>;

flash@50 {
compatible = "atmel,24c256";
reg = <0x50>;
};

pca9532: gpio@60 {
compatible = "nxp,pca9532";
gpio-controller;
#gpio-cells = <2>;
reg = <0x60>;
};
};

方法2

当设备的 I2C 总线编号不明确时,可以通过其他方式获取对应的 i2c_adapter,然后通过以下两种函数创建
i2c_client

  • i2c_new_device():即使设备并不存在,该函数仍然可以创建 i2c_client
    可以使用下面两个函数来创建i2c_client:
 static struct i2c_board_info sfe4001_hwmon_info = {
I2C_BOARD_INFO("max6647", 0x4e),
 };

 int sfe4001_init(struct efx_nic *efx)
{
(...)
efx->board_info.hwmon_client =
i2c_new_device(&efx->i2c_adap, &sfe4001_hwmon_info);

(...)
 }
  • i2c_new_probed_device():设备的地址可能会发生变化,如某些设备的地址可通过引脚配置。这种情况下,可以列出一组可能的设备地址,i2c_new_probed_device 会根据地址逐一检测设备是否存在。
static const unsigned short normal_i2c[] = { 0x2c, 0x2d, >I2C_CLIENT_END };

static int usb_hcd_nxp_probe(struct platform_device *pdev)
{
 (...)
 struct i2c_adapter *i2c_adap;
 struct i2c_board_info i2c_info;

 (...)
 i2c_adap = i2c_get_adapter(2);
 memset(&i2c_info, 0, sizeof(struct i2c_board_info));
 strscpy(i2c_info.type, "isp1301_nxp", sizeof(i2c_info.type));
 isp1301_i2c_client = i2c_new_probed_device(i2c_adap, &i2c_info,
    normal_i2c, NULL);
 i2c_put_adapter(i2c_adap);
 (...)
}

方法3(不推荐): 通过 i2c_driver.detect 函数可以检测到设备并生成 i2c_client,但这种方法较少使用。

方法4: 可以通过用户空间命令手动生成 i2c_client,例如在调试过程中或不方便通过代码生成时:

  // 创建一个i2c_client, .name = "eeprom", .addr=0x50, .adapter是i2c-3
  # echo eeprom 0x50 > /sys/bus/i2c/devices/i2c-3/new_device
  
  // 删除一个i2c_client
  # echo 0x50 > /sys/bus/i2c/devices/i2c-3/delete_device

4.通用模板编写

4.1 代码

驱动程序:

#include "linux/i2c.h"
#include <linux/module.h>
#include <linux/poll.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/gpio/consumer.h>
#include <linux/platform_device.h>
#include <linux/of_gpio.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
#include <linux/slab.h>
#include <linux/fcntl.h>
#include <linux/timer.h>

/* Global variables */
static int major = 0;
static struct class *my_i2c_class;
struct i2c_client *g_client;
static DECLARE_WAIT_QUEUE_HEAD(gpio_wait);
struct fasync_struct *i2c_fasync;

/* Implementing open/read/write functions for the file_operations structure */
static ssize_t i2c_drv_read(struct file *file, char __user *buf, size_t size, loff_t *offset)
{
int err;
struct i2c_msg msgs[2];

/* Initialize i2c_msg for reading */
/* Assuming two i2c messages: One for writing register address, the other for reading data */
err = i2c_transfer(g_client->adapter, msgs, 2);
if (err < 0)
return err;

/* Copy data to user space */
/* Handle buffer size and offset accordingly */
if (copy_to_user(buf, msgs[1].buf, size)) {
return -EFAULT;
}

return size;
}

static ssize_t i2c_drv_write(struct file *file, const char __user *buf, size_t size, loff_t *offset)
{
int err;
struct i2c_msg msgs[2];

/* Allocate buffer and copy data from user space */
char *kbuf = kmalloc(size, GFP_KERNEL);
if (!kbuf)
return -ENOMEM;

if (copy_from_user(kbuf, buf, size)) {
kfree(kbuf);
return -EFAULT;
}

/* Initialize i2c_msg for writing */
/* First message: the register address, Second message: data */
err = i2c_transfer(g_client->adapter, msgs, 2);
kfree(kbuf);

return (err < 0) ? err : size;
}

static unsigned int i2c_drv_poll(struct file *fp, poll_table *wait)
{
poll_wait(fp, &gpio_wait, wait);

/* Returning available read/write events */
// return is_key_buf_empty() ? 0 : POLLIN | POLLRDNORM;
return 0;
}

static int i2c_drv_fasync(int fd, struct file *file, int on)
{
return fasync_helper(fd, file, on, &i2c_fasync) >= 0 ? 0 : -EIO;
}

/* Define file_operations structure */
static struct file_operations i2c_drv_fops = {
.owner = THIS_MODULE,
.read = i2c_drv_read,
.write = i2c_drv_write,
.poll = i2c_drv_poll,
.fasync = i2c_drv_fasync,
};

/* Probe function called when a compatible I2C device is found */
static int i2c_drv_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
/* Save the client handle for later use */
g_client = client;

/* Register the character device */
major = register_chrdev(0, "my_i2c_device", &i2c_drv_fops);
if (major < 0)
return major;

/* Create device class and device file node */
my_i2c_class = class_create(THIS_MODULE, "my_i2c_class");
if (IS_ERR(my_i2c_class)) {
unregister_chrdev(major, "my_i2c_device");
return PTR_ERR(my_i2c_class);
}

device_create(my_i2c_class, NULL, MKDEV(major, 0), NULL, "myi2c");
return 0;
}

/* Remove function to cleanup on driver removal */
static int i2c_drv_remove(struct i2c_client *client)
{
/* Destroy the device and class */
device_destroy(my_i2c_class, MKDEV(major, 0));
class_destroy(my_i2c_class);
unregister_chrdev(major, "my_i2c_device");

return 0;
}

/* Device tree match table */
static const struct of_device_id my_i2c_dt_match[] = {
{ .compatible = "myvendor,my_i2c_device" },
{},
};

/* I2C driver structure */
static struct i2c_driver my_i2c_driver = {
.driver = {
.name = "my_i2c_driver",
.owner = THIS_MODULE,
.of_match_table = my_i2c_dt_match,
},
.probe = i2c_drv_probe,
.remove = i2c_drv_remove,
};

/* Module initialization function */
static int __init i2c_drv_init(void)
{
return i2c_add_driver(&my_i2c_driver);
}

/* Module exit function */
static void __exit i2c_drv_exit(void)
{
i2c_del_driver(&my_i2c_driver);
}

/* Module macros */
module_init(i2c_drv_init);
module_exit(i2c_drv_exit);

MODULE_LICENSE("GPL");

测试:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <poll.h>
#include <signal.h>

static int fd;

int main(int argc, char **argv)
{
int val;
struct pollfd fds[1];
int timeout_ms = 5000;
int ret;
intflags;

int i;

if (argc != 2) 
{
printf("Usage: %s <dev>\n", argv[0]);
return -1;
}


fd = open(argv[1], O_RDWR | O_NONBLOCK);
if (fd == -1)
{
printf("can not open file %s\n", argv[1]);
return -1;
}

for (i = 0; i < 10; i++) 
{
if (read(fd, &val, 4) == 4)
printf("get button: 0x%x\n", val);
else
printf("get button: -1\n");
}

flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flags & ~O_NONBLOCK);

while (1)
{
if (read(fd, &val, 4) == 4)
printf("get button: 0x%x\n", val);
else
printf("while get button: -1\n");
}

close(fd);

return 0;
}

4.2 分析

i2c_add_driver()

当作platform_driver_register吧

可以在上面编写的init中看到,调用了i2c_add_driver:

i2c_add_driver 是一个 Linux 内核函数,用于注册一个 I2C 驱动程序。当你有一个新的 I2C 设备驱动程序时,可以使用这个函数将其注册到内核中,以便在相应的 I2C 总线上匹配到设备时调用该驱动程序。

/* Device tree match table */
static const struct of_device_id my_i2c_dt_match[] = {
{ .compatible = "myvendor,my_i2c_device" },
{},
};

/* I2C driver structure */
static struct i2c_driver my_i2c_driver = {
.driver = {
.name = "my_i2c_driver",
.owner = THIS_MODULE,
.of_match_table = my_i2c_dt_match,
},
.probe = i2c_drv_probe,
.remove = i2c_drv_remove,
};

/* Module initialization function */
static int __init i2c_drv_init(void)
{
return i2c_add_driver(&my_i2c_driver);
}

之前不是在内核提供的通用I2C设备驱动I2c-dev.c分析:注册篇-CSDN博客讲解了关于内核提供的i2c-dev.c设备驱动程序吗,它里面的init的函数是这样的:

static int __init i2c_dev_init(void)
{
    int res;  // 用于存储函数调用的返回结果

    // 打印内核信息,通知已启动 I²C 字符设备驱动
    printk(KERN_INFO "i2c /dev entries driver\n");

    // 注册字符设备区域,指定主设备号和次设备号范围
    res = register_chrdev_region(MKDEV(I2C_MAJOR, 0), I2C_MINORS, "i2c");
    if (res)
        goto out;  // 如果注册失败,跳转到错误处理

    // 创建一个设备类,用于 sysfs 中设备的表示和设备节点的自动创建
    i2c_dev_class = class_create(THIS_MODULE, "i2c-dev");
    if (IS_ERR(i2c_dev_class)) {
        res = PTR_ERR(i2c_dev_class);  // 获取错误码
        goto out_unreg_chrdev;  // 跳转到字符设备取消注册
    }
    i2c_dev_class->dev_groups = i2c_groups;  // 设置设备类的属性组

    // 注册 I²C 总线的通知器,用于追踪总线上的设备添加和移除事件
    res = bus_register_notifier(&i2c_bus_type, &i2cdev_notifier);
    if (res)
        goto out_unreg_class;  // 如果通知器注册失败,跳转到类注销

    // 绑定当前系统中已存在的 I²C 适配器
    i2c_for_each_dev(NULL, i2cdev_attach_adapter); //---- (1)进入i2cdev_attach_adapter函数看下

    // 成功返回 0 表示初始化完成
    return 0;

out_unreg_class:
    // 如果类创建失败,销毁之前创建的类
    class_destroy(i2c_dev_class);
out_unreg_chrdev:
    // 如果字符设备区域注册失败,取消设备号注册
    unregister_chrdev_region(MKDEV(I2C_MAJOR, 0), I2C_MINORS);
out:
    // 打印错误信息,表明驱动初始化失败
    printk(KERN_ERR "%s: Driver Initialisation failed\n", __FILE__);
    return res;  // 返回错误码
}

可以看到并没有像我们所编写的驱动框架那样去调用到i2c_add_driver(),其实来看一下这个函数内部实现,我们可以发现它和i2c-dev.c驱动模板的init函数其实是差不多的:

\Linux-4.9.88\include\linux\i2c.h
#define i2c_add_driver(driver) \
i2c_register_driver(THIS_MODULE, driver) //进入看
\Linux-4.9.88\drivers\i2c\i2c-core.c
int i2c_register_driver(struct module *owner, struct i2c_driver *driver)
{
int res;

/* Can't register until after driver model init */
if (WARN_ON(!is_registered))
return -EAGAIN;

/* add the driver to the list of i2c drivers in the driver core */
driver->driver.owner = owner;
driver->driver.bus = &i2c_bus_type;
INIT_LIST_HEAD(&driver->clients);

/* When registration returns, the driver core
 * will have called probe() for all matching-but-unbound devices.
 */
res = driver_register(&driver->driver); //诶,注册驱动
if (res)
return res;

pr_debug("driver [%s] registered\n", driver->driver.name);

/* Walk the adapters that are already present */
i2c_for_each_dev(driver, __process_new_driver); //猜猜,没错,这里就是找到adapter进行绑定
    /*
        static int __process_new_driver(struct device *dev, void *data)
        {
        if (dev->type != &i2c_adapter_type)
        return 0;
        return i2c_do_add_adapter(data, to_i2c_adapter(dev));
        }
    */

return 0;

无非就是注册驱动绑定该i2c设备的adapter等,至于register_chrdev 注册、初始化字符设备等操作,使其在/dev下显示出来,则放在了probe中实现,其实道理是一样的。

i2c_transfer()

img

i2c_transfer 是 Linux 内核中用于在 I2C 总线上进行数据传输的函数之一。它允许驱动程序向设备发送数据或从设备接收数据。

以下是 i2c_transfer 函数的原型:

int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num);

参数解释:

  • adap:指向表示特定 I2C 适配器的 struct i2c_adapter 结构的指针。这个结构提供了对 I2C 总线的访问方法。
  • msgs:一个指向 struct i2c_msg 结构数组的指针,其中每个元素描述了一条 I2C 消息(包含了要发送的数据、接收的数据、目标设备地址等信息)。
  • num:指定了消息数组的元素数量。

struct i2c_msg 结构包含了一条 I2C 消息的描述信息,包括以下字段:

  • addr:目标设备的 I2C 地址。
  • flags:标志位,指示是读操作、写操作还是特殊控制信息。
  • len:要传输的数据的字节数。
  • buf:指向包含要传输数据的缓冲区的指针。

调用 i2c_transfer 函数后,内核将按照消息数组中的顺序执行每一条消息,通过相应的 I2C 适配器在 I2C 总线上进行数据传输。根据每条消息的属性,数据可以从设备读取到缓冲区中,也可以从缓冲区写入到设备中。

这个函数通常由 I2C 设备驱动程序使用,用于与连接在 I2C 总线上的设备进行通信。

这个就不多讲了,主要是去配置i2c_msg的相关成员,然后就可以实现传输,要想深入了解可以看以往的内容:

内核提供的通用I2C设备驱动I2C-dev.c分析:file_ops篇-CSDN博客

I2C相关结构体讲解:i2c_adapter、i2c_algorithm、i2c_msg-CSDN博客

I2C(IIC)协议讲解-CSDN博客

需要特别注意的是,这个i2c_transfer函数(i2c_core.c提供的接口)是将app要传输的数据 借助i2c device driver传输给i2c控制器,其内部是调用了adapter->algo->master_xfer的,也就是i2c控制器的驱动(比如i2c-imx.c)中定义的函数(i2c_imx_xfer,下一文章将讲解)去传输给对应的实际的i2c硬件设备:

img


原文地址:https://blog.csdn.net/caiji0169/article/details/143029740

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!