Linux I2C核心、总线和设备驱动

编程入门 行业动态 更新时间:2024-10-12 18:23:10

Linux I2C核心、<a href=https://www.elefans.com/category/jswz/34/1769217.html style=总线和设备驱动"/>

Linux I2C核心、总线和设备驱动

目录

  • 更新记录
  • 一、Linux I2C 体系结构
    • 1.1 Linux I2C 体系结构的组成部分
    • 1.2 内核源码文件
    • 1.3 重要的数据结构
  • 二、Linux I2C 核心
    • 2.1 流程
    • 2.2 主要函数
  • 三、Linux I2C 适配器驱动
    • 3.1 I2C 适配器驱动的注册于注销
    • 3.2 probe 成员函数
  • 四、Linux I2C 设备驱动
    • 4.1 设备驱动一般过程
    • 4.2 Linux 的 i2c-dev.c 文件分析
    • 4.3 AT24xx EEPROM的I2C设备驱动实例
  • 五、应用程序开发
  • 参考

更新记录

versionstatusdescriptiondateauthor
V1.0CCreate Document2019.4.9John Wan

status:
C―― Create,
A—— Add,
M—— Modify,
D—— Delete。

注:内核版本 3.0.15,迅为iTop4412开发板

一、Linux I2C 体系结构

1.1 Linux I2C 体系结构的组成部分

  Linux 的 I2C 体系结构分为3个组成部分:I2C核心、I2C总线驱动、I2C设备驱动

1.1.1 I2C 核心层

  I2C 核心:drivers/i2c/i2c-core.c ,主要功能如下:

  1) 注册一根 i2c 总线,以及虚拟 i2c 设备驱动;

  2)给设备驱动层提供接口,如提供 I2C 设备驱动的注册、注销方法, I2C 通信方法(即Algorithm);

  3) 设备与设备驱动之间的匹配检测;

1.1.2 I2C 总线驱动层

  I2C 总线驱动:i2c-s3c2410.c 是对 I2C 硬件体系结构中适配器端的实现,适配器可由 CPU 控制,甚至可以直接集成在 CPU 内部。

  I2C 总线驱动层的功能:

  1)初始化硬件(初始化 i2c 适配器);

  2)实现操作方法:知道怎么发数据,但不知道发什么数据。(根据 i2c 操作时序进行控制 i2c适配器,实现数据的接收/发送)。

  主要包含:

  1) I2C 适配器数据结构;

  2) I2C 适配器的 Algorithm 数据结构 i2c_algorithm

  3) 控制 I2C 适配器产生通信信号的函数。

  经由 I2C 总线驱动的代码,我们可以控制 I2C 适配器以主控方式产生开始位、停止位、读写周期,以及以从设备方式被读写、产生ACK等。

1.1.3 I2C 设备驱动层

  I2C 设备驱动(也称客户驱动):drivers/i2c/i2c-dev.c 是对 I2C 硬件体系结构中设备端的实现,设备一般挂接在受 CPU 控制的 I2C 适配器上,通过 I2C 适配器与 CPU 交换数据。主要功能:

  1)给用户提供接口;

  2)实现策略问题:知道发什么数据,但不知怎么发数据。

1.2 内核源码文件

1.2.1 查看内核中 I2C 设备

  所有的 I2C 设备都在 sysfs 文件系统中显示,存与 /sys/bus/i2c/ 目录下,以适配器地址和芯片地址的形式以树状列出,例如:

topeet@ubuntu:$ tree /sys/bus/i2c/
/sys/bus/i2c/
├── devices
├── drivers
│   ├── 88PM860x
│   │   ├── bind
│   │   ├── uevent
│   │   └── unbind
│   └── aat2870
│       ├── bind
│       ├── uevent
│       └── unbind
├── drivers_autoprobe
├── drivers_probe
└── uevent
1.2.2 Linux 内核源码中 drivers 目录下的 i2c 目录

  包含:

  1) i2c-core.c

    实现 I2C 核心的功能以及 /proc/bus/i2c* 接口。

  2) i2c-dev.c

    实现了 I2C 适配器设备文件的功能,每一个 I2C 适配器都被分配一个设备。通过适配器访问设备时的主设备号都为89,次设备号为 0~255。应用程序通过 "i2c-%d"(i2c-0,i2c-1,……)文件名并使用文件操作接口 open()、write()、read()、ioctl()和 close() 等来访问这个设备。

    i2c-dev.c 并不是针对特定的设备而设计的,只是提供了通用的 read()、ioctl() 等接口,应用层可以借用这些接口访问挂接在适配器上的 i2c 设备的存储空间或寄存器,并控制 i2c 设备的工作方式。

  3) busses 文件夹

    包含一些 I2C 主机控制器的驱动,如 i2c-tegra.ci2c-omap.ci2c-versatile.ci2c-s3c2410.c

  4) algos 文件夹

    实现一些 I2C 总线适配器的通信方法。

1.3 重要的数据结构

  位于 include/linux/i2c.h 头文件中的 i2c_adapteri2c_algorithmi2c_driveri2c_client

1.3.1 i2c_adapter 结构体:
/** i2c_adpater is the structure used to identify a physical i2c bus along* with the access algorithms necessary to access it.*/
struct i2c_adpater {    //i2c 适配器
......const struct i2c_algorithm *algo; //访问总线的通信方法struct device dev;      /* the adpater device */char name[48];          /* 适配器名称 */
......struct list_head userspace_clients; /* client链表头 */
};
1.3.2 i2c_algorithm 结构体:
struct i2c_algorithm {  i2c通信方法/* If an adpater algorithm can't do I2C-level access, set master_xferto NULL. If an adpater algorithm can do SMBus access, setsmbus_xfer. If set to NULL, the SMBus protocol is simulatedusing common I2C messages *//* master_xfer should return the number of messages successfullyprocessed, or a negative value on error */int (*master_xfer)(struct i2c_adpater *adap, struct i2c_msg *msgs,int num);    /* I2C 传输函数指针 */int (*smbus_xfer) (struct i2c_adpater *adap, u16 addr,unsigned short flags, char read_write,u8 command, int size, union i2c_smbus_data *data); /* smbus传输函数指针 *//* To determine what the adpater supports */u32 (*functionality) (struct i2c_adpater *);    /* 返回适配器支持的功能 */
};

  上述中的 “master_xfer” i2c 传输函数指针,I2C 主机驱动的大部分工作聚集在这里。“smbus_xfer” 对应SMBus 传输函数指针,SMBus 不需要增加额外引脚,与 I2C 总线相比,在访问时序上也有一定的差异。

1.3.3 i2c_driver 结构体:
struct i2c_driver {     //i2c 设备驱动int (*attach_adpater)(struct i2c_adpater *) __deprecated; //依附 i2c_adpater 函数指针int (*detach_adpater)(struct i2c_adpater *) __deprecated; //脱离 i2c_adpater 函数指针
......int (*probe)(struct i2c_client *, const struct i2c_device_id *);    //检测函数int (*remove)(struct i2c_client *);
......struct device_driver driver;            //表示驱动const struct i2c_device_id *id_table;   //该驱动所支持的设备ID表/* Device detection callback for automatic device creation */int (*detect)(struct i2c_client *, struct i2c_board_info *);const unsigned short *address_list;struct list_head clients;
};
1.3.4 i2c_client 结构体:
struct i2c_client {     //i2c 设备unsigned short flags;       /* div., see below      */unsigned short addr;        /* chip address - NOTE: 7bit    *//* addresses are stored in the  *//* _LOWER_ 7 bits       */char name[I2C_NAME_SIZE];   // 设备名称struct i2c_adpater *adpater;// 所属的适配器 struct i2c_driver *driver;  // 所对应的设备驱动struct device dev;      // 设备结构体int irq;            /* irq issued by device     */struct list_head detected;  //设备链表
};
1.3.5 i2c_adapteri2c_algorithm

   i2c_adapter 对应与物理上的一个适配器,而 i2c_algorithm 对应一套通信方法。

   一个 I2C 适配器需要 i2c_algorithm 提供的通信函数来控制适配器产生特定的访问周期。缺少 i2c_algorithmi2c_adapter 什么也做不了,因此 i2c_adapter 中包含所使用的 i2c_algorithm 的指针。

   i2c_algorithm 中的关键函数 master_xfer() 用于产生 I2C 访问周期需要的信号,以 i2c_msg (即 I2C 消息的结构体)为单位。i2c_msg 结构体也是非常重要的,它定义于 include/linux/i2c.h中,其中的成员表明 I2C 的传输地址、方向、缓冲区、缓冲区长度等信息:

struct i2c_msg {__u16 addr; /* slave address            */__u16 flags;
......__u16 len;      /* msg length               */__u8 *buf;      /* pointer to msg data          */
};
1.3.6 i2c_driveri2c_client

  i2c_driver 对应与一套驱动方法,其主要成员函数是probe()remove()suspend()resume()等,另外, struct i2c_device_id 形式的 id_table 是该驱动所支持的 I2C 设备的 ID 表。

  i2c_client 的信息通常在 BSP 的板文件中通过 i2c_board_info填充,例如:下面的代码就定义了一个 I2C 设备的 ID 为 "mpu6050"、地址为 0x68、中断号为 EXYNOS4_GPX3(3) 的 i2c_client

/* I2C5 */
static struct i2c_board_info i2c_devs5[] __initdata = {{I2C_BOARD_INFO("mpu6050", 0x68),.platform_data = &mpu6050_data,.irq = EXYNOS4_GPX3(3),},
}

  在 I2C 总线驱动 i2c_bus_typematch() 函数 i2c_device_match() 中,会调用 i2c_match_id() 函数来对在板文件中定义的 ID 和 i2c_driver 所支持的 ID表进行匹配。

1.3.7 i2c_adapteri2c_client

  i2c_adapteri2c_client 的关系与 I2C 硬件体系中适配器和设备的关系一致,即 i2c_client 依附于 i2c_adapter。由于一个适配器可以连接多个 I2C 设备,所以一个 i2c_adapter 也可以被多个 i2c_client 依附,i2c_adapter 中包括依附于它的i2c_client 的链表。

1.3.8 I2C 驱动的各种数据结构间的关系

  假设 I2C 总线适配器 xxx 上有两个使用相同驱动程序的 yyy I2C设备,在打开该 I2C 总线的设备节点后,相关数据结构之间的逻辑组织关系如上图。

  例如 i2c_s3c2410.c 表示总线适配器 xxx_i2c.c,那么 两个相同的 I2C 设备 “mpu6050”对应 i2c_client,那么对应的同一个驱动 i2c_driver。

1.3.9 实际写驱动的内容

  虽然 I2C 硬件体系结构比较简单,但 I2C 体系结构在 Linux 中的实现却相当复杂。理清各层次的内容、关系,哪些需要驱动工程师写,哪些由内核提供。

  一方面,适配器驱动可能是 Linux 内核还不包含的;另一方面,挂接在适配器上的具体设备驱动可能也是 Linux 内核还不包含的。

  • 提供 I2C 适配器的硬件驱动,探测、初始化 I2C 适配器(如申请 I2C 的 I/O 地址和中断号)、驱动 CPU 控制的 I2C 适配器从硬件上产生各种信号以及处理 I2C 中断等。
  • 实现 I2C 设备驱动中的 i2c_driver 接口,用具体设备 yyy 的 yyy_probe()、yyy_remove()、yyy_suspend()、yyy_resume() 函数指针和 i2c_device_id 设备ID 表赋值给 i2c_driver 的 probe()、remove()、suspend()、resume() 和 id_table 指针。
  • 实现 I2C 设备所对应类型的具体驱动,i2c_driver 只是实现设备与总线的挂接,而挂接在总线上的设备则千差万别。例如,如果是字符设备,就实现文件操作接口,即实现具体设备 yyy 的 yyy_read() 、yyy_write()、yyy_ioctl() 函数等;如果是声卡,就实现 ALSA驱动。

  上述工作中,前两个属于 I2C 总线驱动,后两个属于 I2C 设备驱动。下面进行详细分析。

二、Linux I2C 核心

   I2C 核心 (drivers/i2c/i2c-core.c)中提供一组不依赖与硬件平台的接口函数,该文件一般无需被工程师修改,但其作为 I2C 总线驱动和设备驱动之间纽带,理解其所包含的主要函数与流程非常重要:

2.1 流程

struct bus_type i2c_bus_type = {.name       = "i2c",.match      = i2c_device_match,.probe      = i2c_device_probe,.remove     = i2c_device_remove,.shutdown   = i2c_device_shutdown,.pm     = &i2c_device_pm_ops,
};static int __init i2c_init(void)
{int retval;retval = bus_register(&i2c_bus_type);if (retval)return retval;
......retval = i2c_add_driver(&dummy_driver);if (retval)goto class_err;return 0;
......
}static void __exit i2c_exit(void)
{i2c_del_driver(&dummy_driver);
#ifdef CONFIG_I2C_COMPATclass_compat_unregister(i2c_adapter_compat_class);
#endifbus_unregister(&i2c_bus_type);
}/* We must initialize early, because some subsystems register i2c drivers* in subsys_initcall() code, but are linked (and initialized) before i2c.*/
postcore_initcall(i2c_init);
module_exit(i2c_exit);

  通过 postcore_initcall(i2c_init);module_exit(i2c_exit); 对 i2c 进行相关的初始化与卸载。postcore_initcall() 与 module_init() 初始化顺序。
  在 “i2c_init()” 函数中,主要是做了以下事情:

  1)注册名为 " i2c " ,总线类型为 "i2c_bus_type" 的总线;

  2)注册一个虚拟的 I2C 设备驱动。

i2c_add_driver-> driver_register -> bus_add_driver-> driver_attach-> bus_for_each_dev-> __driver_attach-> driver_match_device  //调用 .match (即i2c_device_match)-> driver_probe_device-> really_probe //调用 .probe (即i2c_device_probe)

  在注册虚拟 I2C 设备驱动时,当调用 driver_register() 成功后:

  • 调用 i2c_bus_type 中的 ".match" 成员函数 i2c_device_match() 来对该设备驱动与设备进行匹配。进行两种方式匹配:1)首先通过 of_driver_match_device 的接口来判断 dts 中配置的device与驱动是否匹配,实际是拿 device tree 中的 compatible 属性跟 driver 中的 of_match_table->compatible 做字符串的比较;2)然后通过调用 i2c_match_id() 将该驱动 (struct i2c_driver)所支持的设备ID表(struct i2c_device_id)与设备(struct i2c_client)进行比较。
  • 当设备与设备驱动匹配成功后,调用 i2c_bus_type 中的成员函数 ".probe" 对应的 i2c_device_probe() 函数,进而最终会调用 设备驱动的 ".probe" 成员函数。

2.2 主要函数

2.2.1 增加/删除 I2C 适配器 的函数
int i2c_add_adapter(struct i2c_adapter *adapter);
int i2c_add_numbered_adapter(struct i2c_adapter *adap)
int i2c_del_adapter(struct i2c_adapter *adap);

   提供给总线驱动层调用,对 I2C 适配器进行注册/注销。

2.2.2 增加/删除 I2C 设备驱动 的函数
int i2c_register_driver(struct module *owner, struct i2c_driver *driver)
void i2c_del_driver(struct i2c_driver *driver);

  在 include/linux/i2c.h 中定义了:

static inline int i2c_add_driver(struct i2c_driver *driver)
{return i2c_register_driver(THIS_MODULE, driver);
}

   提供给设备驱动层调用,对 I2C 设备驱动进行注册/注销。

2.2.3 I2C 数据传输、发送和接收 的函数
int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num);
int i2c_master_send(const struct i2c_client *client, const char *buf, int count);
int i2c_master_recv(const struct i2c_client *client, char *buf, int count);
struct i2c_msg {__u16 addr; /* slave address            */__u16 flags;
......__u16 len;      /* msg length               */__u8 *buf;      /* pointer to msg data          */
};int i2c_master_send(const struct i2c_client *client, const char *buf, int count)
{int ret;struct i2c_adapter *adap = client->adapter;struct i2c_msg msg;msg.addr = client->addr;msg.flags = client->flags & I2C_M_TEN;msg.len = count;msg.buf = (char *)buf;ret = i2c_transfer(adap, &msg, 1);/* If everything went ok (i.e. 1 msg transmitted), return #bytestransmitted, else error code. */return (ret == 1) ? count : ret;
}

  i2c_transfer() 函数用于进行 I2C 适配器和 I2C 设备驱动之间的一组消息交互,其中第2个参数是一个指向 i2c_msg 结构体的指针。

  而 i2c_master_send() 函数和 i2c_master_recv() 函数,其内部是先将数据构建成一个 i2c_msg 结构,然后再调用 i2c_transfer() 函数来完成发送、接收 。不过这两个函数每次只能发送、接收 1 个字节的数据

  因此相比之下 i2c_transfer()更具备通用性,可以一次传输多个字节(许多外设读写波形较复杂,例如读寄存器之前可能要先写)。

int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num)
{
......if (adap->algo->master_xfer) {
......
}

  i2c_transfer() 函数本身不具备驱动适配器物理硬件以完成消息交互的能力,它只是寻找到与 i2c_adapter 对应的 i2c_algorithm, 并使用 i2c_algorithmmaster_xfer() 函数真正驱动硬件流程。

三、Linux I2C 适配器驱动

  由于 I2C 总线控制器通常是在内存上的,所以它本身也连接在 platform 总线上,要通过 platform_driver 和 platform_device 的匹配来执行。因此尽管 I2C 适配器给别人提供了总线,它自己也被认为是接在 platform 总线上的一个客户。Linux 的总线、设备和驱动模型实际上是一个树形结构,每个节点虽然可能成为别人的总线控制器,但是自己也是被认为是从上一级总线枚举出来的。

  前面了解过 I2C 总线驱动层的主要功能是:

  1)初始化硬件(初始化 i2c 适配器);

  2)实现操作方法:知道怎么发数据,但不知道发什么数据。(根据 i2c 操作时序进行控制 i2c适配器,实现数据的接收/发送)。

  通常会在与 I2C 适配器所对应的 platform_driver 的 probe() 函数中完成两个工作。

  • 初始化 I2C 适配器所使用的硬件资源,如申请 I/O 地址、中断号、时钟等;
  • 构建 i2c 通信方法 i2c_algorithm,并依附于相应的 i2c_adapter 数据结构,然后通过 i2c_add_adapter() 添加 i2c_adapter 数据结构。

  通常会在 remove() 函数中完成与加载函数相反的工作。

3.1 I2C 适配器驱动的注册于注销

   以 drivers/i2c/busses/i2c-s3c2410.c 为例:

static struct platform_device_id s3c24xx_driver_ids[] = {{.name       = "s3c2410-i2c",.driver_data    = TYPE_S3C2410,}, {.name       = "s3c2440-i2c",.driver_data    = TYPE_S3C2440,}, {.name       = "s3c2440-hdmiphy-i2c",.driver_data    = TYPE_S3C2440_HDMIPHY,}, { },
};
MODULE_DEVICE_TABLE(platform, s3c24xx_driver_ids);static struct platform_driver s3c24xx_i2c_driver = {.probe      = s3c24xx_i2c_probe,.remove     = s3c24xx_i2c_remove,.id_table   = s3c24xx_driver_ids,.driver     = {.owner  = THIS_MODULE,.name   = "s3c-i2c",.pm = S3C24XX_DEV_PM_OPS,},
};static int __init i2c_adap_s3c_init(void)
{return platform_driver_register(&s3c24xx_i2c_driver);
}
subsys_initcall(i2c_adap_s3c_init);static void __exit i2c_adap_s3c_exit(void)
{platform_driver_unregister(&s3c24xx_i2c_driver);
}
module_exit(i2c_adap_s3c_exit);

  1)构建 platform_driver 结构体,id_table 结构体;

  2)模块的加载:注册 platform 设备驱动,将驱动结构体加入到驱动链表中;

  3)进行 platform设备与 platform设备驱动之间的匹配,以及调用 probe 函数(原理参考 “二、Linux I2C 核心”)。

3.2 probe 成员函数

  probe 函数,是整个 I2C 总线驱动层的重点,主要的功能都是在这实现,即初始化硬件、实现操作方法。

static int s3c24xx_i2c_probe(struct platform_device *pdev)
{struct s3c24xx_i2c *i2c;struct s3c2410_platform_i2c *pdata;struct resource *res;int ret;/* 获取平台设备数据资源 */pdata = pdev->dev.platform_data;/* 将 i2c通信方法 依附于 i2c适配器 */i2c->adap.algo    = &s3c24xx_i2c_algorithm;/* 获取 时钟,并使能*/i2c->clk = clk_get(&pdev->dev, "i2c");clk_enable(i2c->clk);/* 获取平台设备 IO内存地址资源,并将其映射 */res = platform_get_resource(pdev, IORESOURCE_MEM, 0);i2c->ioarea = request_mem_region(res->start, resource_size(res),pdev->name);i2c->regs = ioremap(res->start, resource_size(res));/* 将 s3c24xx_i2c结构体 赋给 i2c适配器的私有数据 */i2c->adap.algo_data = i2c;/* 初始化 i2c 控制器 */ret = s3c24xx_i2c_init(i2c);/* 获取 IRQ 资源,注册中断服务函数 */i2c->irq = ret = platform_get_irq(pdev, 0);ret = request_irq(i2c->irq, s3c24xx_i2c_irq, IRQF_DISABLED,dev_name(&pdev->dev), i2c);/* 向总线添加 i2c 适配器 */ret = i2c_add_numbered_adapter(&i2c->adap);
}
3.2.1 平台设备资源的由来

  在 probe 中,需要获取很多的平台资源,包括数据、IO内存地址、中断等等,而这些资源与具体的硬件息息相关,前面了解过 linux 设备驱动的分层、分离思想,那么与具体硬件相关的,肯定是不具备良好的通用性,一般都是由芯片产商提供,而这样的设备资源,将其暂称为 “设备资源层”(板级支持包),例如 arch/arm/plat-samsung 平台文件下的各个文件,针对 i2c 相关文件有:dev-i2c0.c ~ dev-i2c7.c

  例如 arch/arm/plat-samsung/dev-i2c0.c

static struct resource s3c_i2c_resource[] = {[0] = {.start = S3C_PA_IIC,.end   = S3C_PA_IIC + SZ_4K - 1,.flags = IORESOURCE_MEM,},[1] = {.start = IRQ_IIC,.end   = IRQ_IIC,.flags = IORESOURCE_IRQ,},
};struct platform_device s3c_device_i2c0 = {.name         = "s3c2410-i2c",
#ifdef CONFIG_S3C_DEV_I2C1.id       = 0,
#else.id       = -1,
#endif.num_resources    = ARRAY_SIZE(s3c_i2c_resource),.resource     = s3c_i2c_resource,
};struct s3c2410_platform_i2c default_i2c_data __initdata = {.flags      = 0,.slave_addr = 0x10,.frequency  = 100*1000,.sda_delay  = 100,
};void __init s3c_i2c0_set_platdata(struct s3c2410_platform_i2c *pd)
{struct s3c2410_platform_i2c *npd;if (!pd)pd = &default_i2c_data;npd = s3c_set_platdata(pd, sizeof(struct s3c2410_platform_i2c),&s3c_device_i2c0);if (!npd->cfg_gpio)npd->cfg_gpio = s3c_i2c0_cfg_gpio;
}

  而上面这些平台资源的注册,则是通过板级文件,例如 exynos-4412 芯片,则在 arch/arm/mach-exynos/match-itop4412.c 文件中的初始化中进行:

static void __init smdk4x12_machine_init(void)
{s3c_i2c0_set_platdata(NULL);i2c_register_board_info(0, i2c_devs0, ARRAY_SIZE(i2c_devs0));s3c_i2c1_set_platdata(NULL);i2c_register_board_info(1, i2c_devs1, ARRAY_SIZE(i2c_devs1));s3c_i2c2_set_platdata(NULL);i2c_register_board_info(2, i2c_devs2, ARRAY_SIZE(i2c_devs2));
}
3.2.2 初始化 I2C 控制器

  前面了解到 具体硬件相关资源的由来,那么获取这些到这些资源之后,如何进行使用?也就是寄存器的配置过程,则通过 probe 中的 s3c24xx_i2c_init(i2c)

static int s3c24xx_i2c_init(struct s3c24xx_i2c *i2c)
{unsigned long iicon = S3C2410_IICCON_IRQEN | S3C2410_IICCON_ACKEN;struct s3c2410_platform_i2c *pdata;unsigned int freq;/* get the plafrom data */pdata = i2c->dev->platform_data;/* inititalise the gpio */if (pdata->cfg_gpio)pdata->cfg_gpio(to_platform_device(i2c->dev));/* write slave address */writeb(pdata->slave_addr, i2c->regs + S3C2410_IICADD);dev_dbg(i2c->dev, "slave address 0x%02x\n", pdata->slave_addr);writel(iicon, i2c->regs + S3C2410_IICCON);/* we need to work out the divisors for the clock... */if (s3c24xx_i2c_clockrate(i2c, &freq) != 0) {writel(0, i2c->regs + S3C2410_IICCON);dev_err(i2c->dev, "cannot meet bus frequency required\n");return -EINVAL;}
}
3.2.3 I2C 总线的通信方法

  为特定的 I2C 适配器实现通信方法,主要是实现 i2c_algorithm 结构体中的 functionality() 函数和master_xfer() 函数。

  例如上面 probe 函数中 s3c24xx_i2c_algorithm 的源码:

static const struct i2c_algorithm s3c24xx_i2c_algorithm = {.master_xfer        = s3c24xx_i2c_xfer,.functionality      = s3c24xx_i2c_func,
};
3.2.3.1 functionality() 函数

  functionality() 函数非常简单,用于返回 algorithm 所支持的通信协议,如 I2C_FUNC_I2C、I2C_FUNC_10BIT_ADDR、I2C_FUNC_SMBUS_READ_BYTE、I2C_FUNC_SMBUS_WRITE_BYTE等。

/* declare our i2c functionality */
static u32 s3c24xx_i2c_func(struct i2c_adapter *adap)
{return I2C_FUNC_I2C | I2C_FUNC_SMBUS_EMUL | I2C_FUNC_PROTOCOL_MANGLING;
}
3.2.3.2 master_xfer() 函数

  master_xfer() 函数在 I2C 适配器上完成传递给它的 i2c_msg 数组中的每个 I2C 消息。

   xxx 设备的 master-xfer() 函数模板:

static int i2c_adapter_xxx_xfer(struct i2c_adapter * adap, struct i2c_msg *
msgs, int num)
{......for (i = 0; i < num; i++) {i2c_adapter_xxx_start();                            /* 产生开始位 *//* 是读消息 */if (msgs[i]->flags & I2C_M_RD) {i2c_adapter_xxx_setaddr((msgs->addr << 1) | 1); /* 发送从设备地址 */i2c_adapter_xxx_wait_ack();                     /* 获得从设备的ack */i2c_adapter_xxx_readbytes((msgs[i]->buf, msgs[i]->len));/* 读取len长度的数据 */ } else {                                            /* 是写消息 */i2c_adapter_xxx_setaddr(msgs->addr << 1);       /* 发送从设备写地址 */i2c_adapter_xxx_wait_ack();                     /* 获得从设备的ack */i2c_adapter_xxx_writebytes(msgs[i]->buf, msgs[i]->len); /* 写入len长度的数据 */}}i2c_adapter_xxx_stop();                                 /* 产生停止位 */
}

  上述代码实际上给出了一个 master_xfer() 函数处理 I2C 消息数据的流程,对于数组中的每个消息,都是按照该流程跑(硬件的时序),模板中调用的各个函数,用于完成适配器的底层硬件操作,与 I2C 适配器和 CPU 的具体硬件直接相关,根据芯片手册实现。

  master_xfer() 函数的实现形式有很多种,多数驱动以中断方式来完成该流程,比如发起硬件操作请求后,将自己调度出去,因此中间会伴随着睡眠的动作(例如 drivers/i2c/busses/i2c-s3c2410.c 中的 s3c24xx_i2c_xfer() 函数)。

3.2.4 s3c24xx_i2c 结构体

  多数 I2C 总线驱动会定义一个 xxx_i2c 结构体,作为 i2c_adapter 的 algo_data(类似“私有数据”),其中包含 I2C 消息数组指针、数组索引及 I2C 适配器 Algorithm 访问控制用的自旋锁、等待队列等,而 master_xfer() 函数在完成 i2c_msg 数组中消息的处理时,也经常需要访问 xxx_i2c 结构体的成员以获取寄存器基地址。锁等消息。

  例如 芯片 s3c2410 的 drivers/i2c/busses/i2c_s3c2410.c 文件中定义:

struct s3c24xx_i2c {spinlock_t      lock;wait_queue_head_t   wait;unsigned int        suspended:1;struct i2c_msg      *msg;unsigned int        msg_num;unsigned int        msg_idx;unsigned int        msg_ptr;unsigned int        tx_setup;unsigned int        irq;enum s3c24xx_i2c_state  state;unsigned long       clkrate;void __iomem        *regs;struct clk      *clk;struct device       *dev;struct resource     *ioarea;struct i2c_adapter  adap;#ifdef CONFIG_CPU_FREQstruct notifier_block   freq_transition;
#endif
};

四、Linux I2C 设备驱动

  I2C 设备驱动要使用 i2c_driveri2c_client 数据结构并填充 i2c_driver 中的成员函数。 i2c_client 一般被包含在设备的私有信息结构体 yyy_data 中,而 i2c_driver 则适合被定义为全局变量并初始化。

4.1 设备驱动一般过程

4.1.1 i2c_driver 数据结构的定义

  示例:

static struct i2c_driver yyy_driver = {.driver = {.name = "yyy",},.probe          = yyy_probe,.remove         = yyy_remove,.id_table       = yyy_id,
};
4.1.2 Linux I2C 设备驱动的模块加载与卸载
static int __init yyy_init(void)
{return i2c_add_driver(&yyy_driver);
}
module_initcall(yyy_init);static void __exit yyy_exit(void)
{i2c_del_driver(&yyy_driver);
}
module_exit(yyy_exit);

  在模块加载时进行驱动注册,注册时会将驱动与设备链表中的设备进行匹配,并调用 i2c_driver 中的 probe

4.1.3 Linux I2C 设备驱动的数据传输

  在 I2C 设备上读写数据的时序且数据通常通过 i2c_msg 数据进行组织,最后通过 i2c_transfer() 函数完成,示例:一个读取指定偏移 offs 的寄存器。

struct i2c_msg msgs[2];/* 第一条消息是写消息 */
msgs[0].addr = client->addr;
msgs[0].flags = 0;
msgs[0].len = 1;
msgs[0].buf = &offs;/* 第二条消息是读消息 */
msgs[1].addr = client->addr;
msgs[1].flags = I2C_M_RD;
msgs[1].len = sizeof(buf);
msgs[1].buf = &buf[0];i2c_transfer(client->adapter, msg, 2);

4.2 Linux 的 i2c-dev.c 文件分析

  i2c-dev.c 文件完全可以被看做是一个 I2C 设备驱动,不过,它实现的 i2c_client 是虚拟、临时的,主要是为了便于从用户空间操作 I2C 外设。i2c-dev.c 针对每个 I2C 适配器生成一个主设备号为89的设备文件,实现了 i2c_driver 的成员函数以及文件操作接口,因此 i2c-dev.c 的主题是 “ i2c_driver 成员函数 + 字符设备驱动”。

  在前面总结了,I2C 设备驱动层i2c-dev.c 的主要功能是:

  1)给用户提供接口;

  2)实现策略问题:知道发什么数据,但不知怎么发数据。

4.2.1 设备驱动的加载
static const struct file_operations i2cdev_fops = {.owner      = THIS_MODULE,.llseek     = no_llseek,.read       = i2cdev_read,.write      = i2cdev_write,.unlocked_ioctl = i2cdev_ioctl,.open       = i2cdev_open,.release    = i2cdev_release,
};static int __init i2c_dev_init(void)
{/* 注册主设备号 ,注册硬件操作方法,提供文件操作接口*/res = register_chrdev(I2C_MAJOR, "i2c", &i2cdev_fops);/* 创建设备类 */i2c_dev_class = class_create(THIS_MODULE, "i2c-dev");/* 绑定适配器,搜索i2c设备链表,每搜索到一个设备都调用i2cdev_attach_adapter函数,* 以生成对应的设备文件。之后再将设备与适配器进行绑定*/i2c_for_each_dev(NULL, i2cdev_attach_adapter);
}static void __exit i2c_dev_exit(void)
{bus_unregister_notifier(&i2c_bus_type, &i2cdev_notifier);i2c_for_each_dev(NULL, i2cdev_detach_adapter);class_destroy(i2c_dev_class);unregister_chrdev(I2C_MAJOR, "i2c");
}static int i2cdev_attach_adapter(struct device *dev, void *dummy)
{struct i2c_adapter *adap;struct i2c_dev *i2c_dev;
....../* 创建设备文件: /dev/i2c-0, /dev/i2c-1... */i2c_dev->dev = device_create(i2c_dev_class, &adap->dev,MKDEV(I2C_MAJOR, adap->nr), NULL,"i2c-%d", adap->nr);res = device_create_file(i2c_dev->dev, &dev_attr_name);
}

  在 i2c-dev.c 文件的入口 i2c_dev_init() 函数中,以字符设备驱动的形式注册硬件的操作方法,为用户提供文件操作的接口。I2C 主设备号固定为 89。

4.2.2 数据的发送与接收

  在 i2c-dev.c 提供的文件接口中,可以直接与用户空间传递数据,但控制具体的硬件进行收发数据,则是通过 调用 I2C 核心的相关函数,例如 i2c_master_recv()i2c_master_send()i2c_transfer()

4.2.2.1 读写函数 i2cdev_read()i2cdev_write()

  i2c-dev.c 提供的 i2cdev_read()i2cdev_write() 函数对应与用户空间要使用的 read()write() 文件操作接口,这两个函数分别调用 I2C 核心的 i2c_master_recv()i2c_master_send() 函数来构造一条 I2C 消息并引发适配器 Algorithm 通信函数的调用,以完成消息的传输。时序:

  但是,大多数稍微复杂点的 I2C 设备的读写流程并不对应于一条消息,往往在一次读写周期中传输多条消息,如下时序:

  在这样的情况下,在应用层仍然调用 read()、write() 文件 API 来读写 I2C 设备,将不能正确读写。因此上面的方式在通用性方面较差。

  那么对于多条消息的读写,可以在用户空间组织成 i2c_msg 消息数组并通过调用 I2C_RDWR_IOCTL 命令实现。

4.2.2.2 i2cdev_ioctl() 函数
static long i2cdev_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{struct i2c_client *client = file->private_data;
......switch (cmd) {case I2C_SLAVE:case I2C_SLAVE_FORCE:......case I2C_TENBIT:......case I2C_PEC:......case I2C_FUNCS:......case I2C_RDWR:return i2cdev_ioctl_rdrw(client, arg);case I2C_SMBUS:......case I2C_RETRIES:......case I2C_TIMEOUT:......default:return -ENOTTY;}return 0;
}

  常用的 IOCTL 包括 I2C_SLAVE(设置从设备地址)、I2C_RETRIES(未收到设备ACK情况下的重试次数,默认1、I2C_TIMEOUT(超时)以及 I2C_RDWR

4.3 AT24xx EEPROM的I2C设备驱动实例

  drivers/misc/eeprom/at24.c 文件支持大多数 I2C 接口的 EEPROM,如之前所述,一个具体的 I2C 设备驱动由 i2c_driver 的形式进行组织,用于将设备挂接与 I2C 总线,组织好了后,再完成设备本身所属类型的驱动。

4.3.1 设备驱动模块的加载与卸载
static int __init at24_init(void)
{
......return i2c_add_driver(&at24_driver);
}
module_init(at24_init);static void __exit at24_exit(void)
{i2c_del_driver(&at24_driver);
}
module_exit(at24_exit);
4.3.2 设备驱动的绑定

  在前面了解 I2C 核心i2c-core.c时,在进行设备驱动的注册时,会调用总线的 .match 成员函数来完成设备和驱动的绑定,接着便会调用驱动提供的 probe 方法。

  而以下就是drivers/misc/eeprom/at24.c 文件中提供的设备 id 表(struct i2c_device_id):

static struct i2c_driver at24_driver = {.driver = {.name = "at24",.owner = THIS_MODULE,},.probe = at24_probe,.remove = __devexit_p(at24_remove),.id_table = at24_ids,
};static const struct i2c_device_id at24_ids[] = {/* needs 8 addresses as A0-A2 are ignored */{ "24c00", AT24_DEVICE_MAGIC(128 / 8, AT24_FLAG_TAKE8ADDR) },/* old variants can't be handled with this generic entry! */{ "24c01", AT24_DEVICE_MAGIC(1024 / 8, 0) },{ "24c02", AT24_DEVICE_MAGIC(2048 / 8, 0) },/* spd is a 24c02 in memory DIMMs */{ "spd", AT24_DEVICE_MAGIC(2048 / 8,AT24_FLAG_READONLY | AT24_FLAG_IRUGO) },{ "24c04", AT24_DEVICE_MAGIC(4096 / 8, 0) },/* 24rf08 quirk is handled at i2c-core */{ "24c08", AT24_DEVICE_MAGIC(8192 / 8, 0) },{ "24c16", AT24_DEVICE_MAGIC(16384 / 8, 0) },{ "24c32", AT24_DEVICE_MAGIC(32768 / 8, AT24_FLAG_ADDR16) },{ "24c64", AT24_DEVICE_MAGIC(65536 / 8, AT24_FLAG_ADDR16) },{ "24c128", AT24_DEVICE_MAGIC(131072 / 8, AT24_FLAG_ADDR16) },{ "24c256", AT24_DEVICE_MAGIC(262144 / 8, AT24_FLAG_ADDR16) },{ "24c512", AT24_DEVICE_MAGIC(524288 / 8, AT24_FLAG_ADDR16) },{ "24c1024", AT24_DEVICE_MAGIC(1048576 / 8, AT24_FLAG_ADDR16) },{ "at24", 0 },{ /* END OF LIST */ }
};
MODULE_DEVICE_TABLE(i2c, at24_ids);

  drivers/misc/eeprom/at24.c 不依赖于具体的 CPU 和 I2C控制器的硬件特性,前面也是一直了解驱动是如何如何,那么具体所对应的硬件外设(struct i2c_client)在哪里呢?两种方式:

  • 一种在板级文件中添加,例如在 arch/arm/mach-s5pv210.c 文件中,向名为 "smdkv210_i2c_devs0" 的i2c 驱动中添加两种硬件外设(名字与设备地址):

    static struct i2c_board_info smdkv210_i2c_devs0[] __initdata = {{ I2C_BOARD_INFO("24c08", 0x50), },     /* Samsung S524AD0XD1 */{ I2C_BOARD_INFO("wm8580", 0x1b), },
    };
    
  • 一种是在支持设备树的情况下,在 ".dts" 文件中添加一个节点:

    i2c@11000 {status = "okay";...eeprom@50 {compatible = "atmel, 24c08";reg = <0x50>;};
    };
    
4.3.3 文件操作接口

  在 i2c-dev.c 中,是将设备驱动注册成字符型驱动的机制为用户空间提供文件操作接口 read()write() 等,而在 drivers/misc/eeprom/at24.c 中并没有注册任何字符设备或杂项设备,那么是如何给用户提供文件操作接口的呢?

   是先向 sys 文件系统注册二进制属性的文件,然后通过该二进制文件,访问设备。设备本身的驱动以 bin_attribute 二进制 sysfs 节点形式呈现:

struct at24_data {struct at24_platform_data chip;...struct bin_attribute bin;...
};static ssize_t at24_bin_read(struct file *filp, struct kobject *kobj,struct bin_attribute *attr,char *buf, loff_t off, size_t count)
{struct at24_data *at24;at24 = dev_get_drvdata(container_of(kobj, struct device, kobj));return at24_read(at24, buf, off, count);
}static ssize_t at24_bin_write(struct file *filp, struct kobject *kobj,struct bin_attribute *attr,char *buf, loff_t off, size_t count)
{struct at24_data *at24;at24 = dev_get_drvdata(container_of(kobj, struct device, kobj));return at24_write(at24, buf, off, count);
}static int at24_probe(struct i2c_client *client, const struct i2c_device_id *id)
{...struct at24_data *at24;...at24->bin.read = at24_bin_read;...at24->bin.write = at24_bin_write;   ...
}

五、应用程序开发

  通过 read()、write()文件接口:

#include <stdio.h>
#include <linux/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <linux/i2c.h>
#include <linux/i2c-dev.h>#define  BUFF_SIZE      32int main(int argc, char **argv)
{unsigned int fd;unsigned short mem_addr;unsigned short size;unsigned short idx;char buf[BUFF_SIZE];char cswap;union {unsigned short addr;char bytes[2];}tmp;if (argc < 3) {printf("Use:\n%s /dev/i2c-x mem_addr size \n", argv[0]);return 0;}sscanf(argv[2], "%d", &mem_addr);sscanf(argv[3], "%d", &size);if (size > BUFF_SIZE)size = BUFF_SIZE;fd = open(argv[1], O_RDWR);if (!fd) {printf("Error on opening the device file\n");return 0;}ioctl(fd, I2C_SLAVE, 0x50);     /* 设置 EEPROM 地址 */ioctl(fd, I2C_TIMEOUT, 1);      /* 设置 超时 */ioctl(fd, I2C_RETRIES, 1);      /* 设置 重试次数 */for (idx = 0; idx < size; ++idx, ++mem_addr) {tmp.addr = mem_addr;cswap = tmp.bytes[0];tmp.bytes[0] = tmp.bytes[1];tmp.bytes[1] = cswap;write(fd, &tmp.addr, 2);read(fd, &buf[idx], 1);}buf[size] = 0;close(fd);printf("Read %d char: %s \n", size, buf);return 0;
}

  通过 ioctl 文件接口:

#include <stdio.h>
#include <linux/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <stlib.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <errno.h>
#include <assert.h>
#include <string.h>
#include <linux/i2c.h>
#include <linux/i2c-dev.h>int main(int argc, char **argv)
{struct i2c_rdwr_ioctl_data work_queue;unsigned int idx;unsigned int fd;unsigned int slave_addr, reg_addr;unsigned char val;int i;int ret;if (argc < 4) {printf("Use:\n%s /dev/i2c-x mem_addr size \n", argv[0]);return 0;}fd = open(argv[1], O_RDWR);if (!fd) {printf("Error on opening the device file\n");return 0;}sscanf(argv[2], "%x", &slave_addr);sscanf(argv[3], "%x", &reg_addr);work_queue.nmsgs = 2;work_queue.msgs = (struct i2c_msg)malloc(work_queue.nmsgs *sizeof(struct i2c_msg));if (!work_queue.nmsgs) {printf("Memory alloc error\n");close(fd);return 0;}ioctl(fd, I2C_TIMEOUT, 2);      /* 设置 超时 */ioctl(fd, I2C_RETRIES, 1);      /* 设置 重试次数 */for (i = reg_addr; idx < (reg_addr + 16); i++) {val = i;(work_queue.nmsgs[0]).len = 1;(work_queue.nmsgs[0]).addr = slave_addr;(work_queue.nmsgs[0]).buf = &val;(work_queue.nmsgs[1]).len = 1;(work_queue.nmsgs[1]).flags = I2C_M_RD;(work_queue.nmsgs[1]).addr = slave_addr;(work_queue.nmsgs[1]).buf = &val;ret = ioctl(fd, I2C_RDWR, (unsigned long)&work_queue);if (ret < 0)printf("Error during I2C_RDWR ioctl with error code: %d\n", ret);elseprintf("reg:%02x val:%02x\n", i, val);}close(fd);return 0;
}

参考

  1. 《Linux设备驱动开发详解:基于最新的Linux 4.0内核》第15章 - 宋宝华
  2. linux 内核说明文档(Documentation/i2c/dev-interface
  3. Linux i2c 子系统
  4. 基于S3C2440的嵌入式Linux驱动——AT24C02(EEPROM I2C接口)驱动解读
  5. 麦子学院—linux嵌入式开发教程第四阶段—I2C设备驱动及子系统开发

转载于:.html

更多推荐

Linux I2C核心、总线和设备驱动

本文发布于:2024-02-07 00:20:31,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1751660.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:总线   核心   设备   Linux   I2C

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!