Linux驱动-I2C总线驱动

文章目录

  • [1 I2C简介](#1 I2C简介)
    • [1.1 I2C协议](#1.1 I2C协议)
      • [1.1.1 起始位](#1.1.1 起始位)
      • [1.1.2 停止位](#1.1.2 停止位)
      • [1.1.3 数据传输](#1.1.3 数据传输)
      • [1.1.4 应答信号](#1.1.4 应答信号)
      • [1.1.5 I2C写时序](#1.1.5 I2C写时序)
      • [1.1.6 I2C读时序](#1.1.6 I2C读时序)
      • [1.1.7 I2C多字节读写时序](#1.1.7 I2C多字节读写时序)
  • [2 I2C总线框架简介](#2 I2C总线框架简介)
    • [2.1 I2C总线驱动](#2.1 I2C总线驱动)
    • [2.2 I2C总线设备](#2.2 I2C总线设备)
      • [2.2.1 i2c_client结构体](#2.2.1 i2c_client结构体)
      • [2.2.2 i2c_driver结构体](#2.2.2 i2c_driver结构体)
      • [2.2.3 i2c_driver注册](#2.2.3 i2c_driver注册)
    • [2.3 I2C设备和驱动匹配过程](#2.3 I2C设备和驱动匹配过程)
  • [3 I2C设备驱动编写流程](#3 I2C设备驱动编写流程)
    • [3.1 设备树I2C设备信息描述](#3.1 设备树I2C设备信息描述)
      • [3.1.1 AP3216C简介](#3.1.1 AP3216C简介)
    • [3.2 I2C设备数据收发处理流程](#3.2 I2C设备数据收发处理流程)
      • [3.2.1 i2c_transfer函数](#3.2.1 i2c_transfer函数)
        • [3.2.1.1 使用i2c_transfer进行I2C数据收发](#3.2.1.1 使用i2c_transfer进行I2C数据收发)
    • [3.3 辅助函数](#3.3 辅助函数)
  • [4 I2C驱动的应用实例](#4 I2C驱动的应用实例)
    • [4.1 硬件原理图分析](#4.1 硬件原理图分析)
    • [4.2 修改设备树](#4.2 修改设备树)
    • [4.3 驱动程序编写](#4.3 驱动程序编写)
    • [4.4 用户侧测试程序编写](#4.4 用户侧测试程序编写)

1 I2C简介

I2C是一种很常见的同步、串行、低速、近距离通信接口。

Linux内核开发者为了让驱动开发工程师在内核中方便的添加自己的I2C设备驱动程序,更容易的在linux下驱动自己的I2C接口硬件,进而引入了I2C总线框架。与Linux下的platform虚拟总线不同的是,I2C是实际的物理总线,所以I2C总线框架也是Linux下总线、设备、驱动模型的产物。

I2C是很常见的一种总线协议,I2C是NXP公司设计的,I2C使用两条线在主控制器和从机之间进行数据通信。一条是SCL(串行时钟线),另外一条是SDA(串行数据线),这两条数据线需要接上拉电阻,总线空闲的时候SCL和SDA处于高电平。I2C总线标准模式下速度可以达到100Kb/S,快速模式下可以达到400Kb/S。I2C总线工作时按照一定的协议来运行的。

1.1 I2C协议

I2C是支持多从机的,也就是一个I2C控制器下可以挂多个I2C从设备,这些不同的I2C从设备有不同的器件地址,这样I2C主控制器就可以通过I2C设备的器件地址访问指定的I2C设备了,一个I2C总线连接多个I2C设备如图所示:

图中SDA和SCL这两根线必须要接一个上拉电阻,一般是4.7K。

I2C为什么必须接上拉电阻?且必须配置为开漏输出?

  1. 必须接上拉电阻是为了线与(多设备共享总线),解决多设备竞争;
  2. 必须开漏输出是为了允许多个设备共用一根线,防止芯片烧毁;
  3. 绝对不能用推挽输出,推挽会短路烧芯片。

其余的I2C从器件都挂接到SDA和SCL这两根线上,这样就可以通过SDA和SCL这两根线来访问多个I2C设备。

1.1.1 起始位

I2C通信起始标志,通过这个起始位就可以告诉I2C从机,"我"要开始进行I2C通信了。在SCL为高电平的时候,SDA出现下降沿 就表示为起始位,如所示:

1.1.2 停止位

停止位就是停止I2C通信的标志位,和起始位的功能相反。在SCL为高电平的时候,SDA出现上升沿 就表示为停止位,如图所示:

1.1.3 数据传输

I2C总线在数据传输的时候要保证在SCL高电平期间,SDA上的数据稳定,因此SDA上的数据变化只能在SCL低电平期间发生,如图所示:

1.1.4 应答信号

当I2C主机发送完8位数据以后会将SDA设置为输入状态,等待I2C从机应答,也就是等待I2C从机告诉主机它接收到数据了。应答信号是由从机发出的,主机需要提供应答信号所需的时钟,主机发送完8位数据以后紧跟着的一个时钟信号就是给应答信号使用的。从机通过将SDA拉低来表示发出应答信号,表示通信成功,否则表示通信失败。

1.1.5 I2C写时序

主机通过I2C总线与从机之间进行通信不外乎两个操作:写和读,I2C总线单字节写时序如图所示:

写时序的具体步骤:

  1. 开发信号;
  2. 发送I2C设备地址,每个I2C器件都有一个设备地址,通过发送具体的设备地址来决定访问哪个I2C器件。这是一个8位的数据,其中高7位是设备地址,最后1位是读写位,为1的话表示这是一个读操作,为0的话表示这个是写操作;
  3. I2C器件地址后面跟着一个读写为,为1是读操作,为0是写操作;
  4. 从机发送的ACK应答信号;
  5. 重新发送开始信号;
  6. 发送要写入数据的寄存器地址;
  7. 从机发送的ACK应答信号;
  8. 发送要写入寄存器的数据;
  9. 从机发送的ACK应答信号;
  10. 停止信号。

1.1.6 I2C读时序

I2C总线单字节读时序如图所示:

I2C单字节读时序比写时序要复杂一点,读时序分为4大步,

  1. 第一步是发送设备地址;
  2. 第二步是发送要读取的寄存器地址;
  3. 第三步重新发送设备地址;
  4. 最后一步就是I2C从器件输出要读取的寄存器值。

具体来看一下每一个步骤:

  1. 主机发送起始信号;
  2. 主机发送要读取的I2C从设备地址;
  3. 读写控制位,因为是向I2C从设备发送数据,因此是写信号;
  4. 从机发送的ACK应答信号;
  5. 重新发送START信号;
  6. 主机发送要读取数据的寄存器地址;
  7. 从机发送的ACK应答信号;
  8. 重新发送START信号;
  9. 重新发送要读取的I2C从设备地址;
  10. 读写控制位,这里是读信号,表示接下来是从I2C从设备里面读取数据;
  11. 从机发送的ACK应答信号;
  12. 从I2C器件里面读取到的数据;
  13. 主机发送NO ACK信号,表示读取完成,不需要从机再发送ACK信号;
  14. 主机发送STOP信号,停止I2C通信。

1.1.7 I2C多字节读写时序

需要读写多个字节,多字节读写时序和单字节的基本一致,只是在读写数据的时候可以连续发送多个自己的数据,其他的控制时序都是和单字节一样的。

2 I2C总线框架简介

Linux内核开发者为了让驱动开发工程师在内核中方便的添加自己的I2C设备驱动程序,方便大家更容易的在linux下驱动自己的I2C接口硬件,进而引入了I2C总线框架,我们一般也叫作I2C子系统,Linux下I2C子系统总体框架如下所示:

I2C子系统分为三大组成部分:

  1. I2C核心(I2C-core):I2C核心提供了I2C总线驱动(适配器)和设备驱动的注册、注销方法,I2C通信方法(algorithm)与具体硬件无关的代码,以及探测设备地址的上层代码等;
  2. I2C总线驱动(I2C adapter):I2C总线驱动是I2C适配器的软件实现,提供I2C适配器与从设备间完成数据通信的能力。I2C总线驱动由i2c_adapter和i2c_algorithm来描述。I2C适配器是SoC中内置i2c控制器的软件抽象,可以理解为他所代表的是一个I2C主机;
  3. I2C设备驱动(I2C client driver):包括两部分,设备注册和驱动注册。

I2C子系统帮助内核统一管理I2C设备,让驱动开发工程师在内核中可以更加容易添加自己的I2C设备驱动程序。

2.1 I2C总线驱动

一般由半导体厂商提供,驱动开发人员不关注。

2.2 I2C总线设备

I2C设备驱动重点关注两个数据结构:i2c_client和i2c_driver,根据总线、设备和驱动模型I2C总线由半导体厂商提供,还剩下设备和驱动。i2c_client用于描述I2C总线下的设备,i2c_driver则用于描述I2C总线下的设备驱动,类似于platform总线下的platform_device和platform_driver。

2.2.1 i2c_client结构体

i2c_client结构体定义在include/linux/i2c.h文件中,内容如下:

c 复制代码
struct i2c_client {
	/*
	 * 标志位(掩码),用于描述 I2C 设备的特性、工作模式
	 * 下面的宏定义都是 flags 的可用取值
	 */
	unsigned short flags;

#define I2C_CLIENT_PEC		0x04	/* 使用数据包错误校验(CRC校验) */
#define I2C_CLIENT_TEN		0x10	/* 设备使用 10位 芯片地址 */
					/* 注意:该值必须与 I2C_M_TEN 保持一致 */
#define I2C_CLIENT_SLAVE	0x20	/* 当前设备工作在 I2C 从机模式 */
#define I2C_CLIENT_HOST_NOTIFY	0x40	/* 使能 I2C 主机通知功能(从机主动通知主机) */
#define I2C_CLIENT_WAKE		0x80	/* 用于 board_info 配置:标记该设备可唤醒系统 */
#define I2C_CLIENT_SCCB		0x9000	/* 使用 Omnivision(豪威)的 SCCB 协议(摄像头专用) */
					/* 该值必须与 I2C_M_STOP | IGNORE_NAK 匹配 */

	/*
	 * I2C 芯片地址
	 * 重要:这里存储的是 7位 地址
	 * 并且存放在 低7位(最高位不使用)
	 */
	unsigned short addr;

	char name[I2C_NAME_SIZE];	/* 设备名称(驱动匹配、调试时使用) */
	struct i2c_adapter *adapter;	/* 指向该设备挂载的 I2C 适配器(控制器) */
	struct device dev;		/* 标准 Linux 设备结构体(继承核心设备属性) */
	int init_irq;			/* 初始化时配置的中断号(固定值) */
	int irq;			/* 设备产生的中断号(实际使用的中断) */
	struct list_head detected;	/* 用于链接所有已检测到的 I2C 设备的链表节点 */

#if IS_ENABLED(CONFIG_I2C_SLAVE)
	i2c_slave_cb_t slave_cb;	/* I2C 从机模式回调函数(从机收到数据时触发) */
#endif
};

一个I2C设备对应一个i2c_client结构体变量,系统每检测到一个I2C从设备就会给这个设备分配一个i2c_client。

2.2.2 i2c_driver结构体

i2c_driver类似platform_driver,是编写I2C设备驱动重点要处理的内容,i2c_driver结构体定义在include/linux/i2c.h文件中,内容如下:

c 复制代码
struct i2c_driver {
	unsigned int class;
	int (*probe)(struct i2c_client *client, const struct i2c_device_id *id);
	int (*remove)(struct i2c_client *client);
	int (*probe_new)(struct i2c_client *client);
	void (*shutdown)(struct i2c_client *client);
	void (*alert)(struct i2c_client *client, enum i2c_alert_protocol protocol, unsigned int data);
	int (*command)(struct i2c_client *client, unsigned int cmd, void *arg);
	struct device_driver driver;
	const struct i2c_device_id *id_table;
	int (*detect)(struct i2c_client *client, struct i2c_board_info *info);
	const unsigned short *address_list;
	struct list_head clients;
	bool disable_i2c_core_irq_mapping;
};

i2c_driver结构体分析:

  1. 当I2C设备和驱动匹配成功以后probe函数就会执行,和platform驱动一样;
  2. device_driver驱动结构体,如果使用设备树的话,需要设置device_driver的of_match_table成员变量,也就是驱动的兼容(compatible)属性;
  3. id_table是传统的、未使用设备树的设备匹配ID表。

对于I2C设备驱动来说,主要就是构建i2c_driver,构建完成以后需要向I2C子系统注册这个i2c_driver。注册和注销i2c所用的到函数原型如下:

c 复制代码
///
/// \brief i2c_register_driver  注册 i2c_driver
/// \param owner                一般为THIS_MODULE
/// \param driver               要注册的 i2c_driver
/// \return                     0,成功;负值,失败
///
int i2c_register_driver(struct module *owner, struct i2c_driver *driver);


/// 注册 i2c_driver
#define i2c_add_driver(driver) \
	i2c_register_driver(THIS_MODULE, driver)

///
/// \brief i2c_del_driver   注销 i2c_driver
/// \param driver           要注销的 i2c_driver
///
void i2c_del_driver(struct i2c_driver *driver);

2.2.3 i2c_driver注册

i2c_driver的注册示例代码如下:

c 复制代码
static int xxx_probe(struct i2c_client *client, const struct i2c_device_id *id)
{

    return 0;
}


static int xxx_remove(struct i2c_client *client)
{
    return 0;
}

// 设备树匹配列表
static const struct of_device_id xxx_id[] = {
    { .compatible = "xxx" },
    {  }
};

static struct i2c_driver xxx_driver = {
    .probe = xxx_probe,
    .remove = xxx_remove,
    .driver = {
        .owner = THIS_MODULE,
        .name = "xxx",
        .of_match_table = xxx_id,
    },
};

static int __init xxx_init(void)
{
    int ret = 0;

    ret = i2c_add_driver(&xxx_driver);

    return ret;
}

static void __exit xxx_exit(void)
{

    i2c_del_driver(&xxx_driver);

}

module_init(xxx_init);
module_exit(xxx_exit);

2.3 I2C设备和驱动匹配过程

I2C设备和驱动的匹配过程是由I2C子系统核心层来完成的,drivers/i2c/i2c-core-base.c就是I2C的核心部分,I2C核心提供了一些与具体硬件无关的API函数。

设备和驱动的匹配过程也是由核心层完成的,I2C总线的数据结构为i2c_bus_type,定义在drivers/i2c/i2c-core-base.c文件,i2c_bus_type内容如下:

c 复制代码
struct bus_type i2c_bus_type = {
	.name		= "i2c",
	.match		= i2c_device_match,
	.probe		= i2c_device_probe,
	.remove		= i2c_device_remove,
	.shutdown	= i2c_device_shutdown,
};
EXPORT_SYMBOL_GPL(i2c_bus_type);

.match就是I2C总线的设备和驱动匹配函数,在这里就是i2c_device_match这个函数,此函数内容如下:

c 复制代码
static int i2c_device_match(struct device *dev, struct device_driver *drv)
{
	struct i2c_client	*client = i2c_verify_client(dev);
	struct i2c_driver	*driver;


	/* Attempt an OF style match */
	if (i2c_of_match_device(drv->of_match_table, client))
		return 1;

	/* Then ACPI style match */
	if (acpi_driver_match_device(dev, drv))
		return 1;

	driver = to_i2c_driver(drv);

	/* Finally an I2C match */
	if (i2c_match_id(driver->id_table, client))
		return 1;

	return 0;
}

分析i2c_device_match函数,I2C设备和驱动匹配有三种方式:

  1. i2c_of_match_device函数用于完成设备树中定义的设备与驱动匹配过程。比较I2C设备节点的compatible属性和of_device_id中的compatible属性是否相等,如果相当的话就表示I2C设备和驱动匹配;
  2. acpi_driver_match_device函数用于ACPI形式的匹配;
  3. i2c_match_id函数用于传统的、无设备树的I2C设备和驱动匹配过程。比较I2C设备名字和i2c_device_id的name字段是否相等,相等的话就说明I2C设备和驱动匹配成功。

3 I2C设备驱动编写流程

3.1 设备树I2C设备信息描述

使用设备树的时候I2C设备信息通过创建相应的节点就行了,比如在我们的STM32MP1的开发板上有一个I2C器件AP3216C,这是三合一的环境传感器,并且该器件挂在STM32MP1I2C5总线接口上,因此必须在i2c5节点下创建一个子节点来描述AP3216C设备,节点示例如下所示:

c 复制代码
&i2c5 {
	pinctrl-names = "default", "sleep";
	pinctrl-0 = <&i2c5_pins_a>;
	pinctrl-1 = <&i2c5_pins_sleep_a>;
	status = "okay";

	ap3216c@1e {
		compatible = "alientek,ap3216c";
		reg = <0x1e>;
	};
};

i2c5节点分析:

  1. 设置了i2c5的pinmux的配置;
  2. 向i2c5添加ap3216c子节点,"ap3216c@1e"是子节点名字,"@"后面的"1e"就是ap3216c的I2C器件地址。设置compatible属性值为"alientek,ap3216c"。reg属性也是设置ap3216c的器件地址的,因此值为0x1e。

I2C设备节点的创建重点是compatible属性和reg属性的设置,一个用于匹配驱动,一个用于设置器件地址。

3.1.1 AP3216C简介

STM32MP1开发板上通过I2C5连接了一个三合一环境传感器:AP3216C。AP3216C是由敦南科技推出的一款传感器,其支持环境光强度(ALS)、接近距离(PS)和红外线强度(IR)这三个环境参数检测。该芯片可以通过I2C接口与主控制相连,并且支持中断,AP3216C的特点如下:

  1. I2C接口,快速模式下波特率可以到400Kbit/S;
  2. 多种工作模式选择:ALS、PS+IR、ALS+PS+IR、PD等等;
  3. 内建温度补偿电路;
  4. 宽工作温度范围(-30C~+80C);
  5. 超小封装,4.1mmx2.4mmx1.35mm;
  6. 环境光传感器具有16位分辨率;
  7. 接近传感器和红外传感器具有10位分辨率。

AP3216C的设备地址是0X1E,这个地址是设备手册中的。

AP3216C用到的寄存器地址如下所示:

3.2 I2C设备数据收发处理流程

3.2.1 i2c_transfer函数

I2C设备驱动首先要做的就是初始化i2c_driver并向Linux内核注册。当设备和驱动匹配以后i2c_driver里面的probe函数就会执行,probe函数里面所做的就是字符设备驱动那一套了。一般需要在probe函数里面初始化I2C设备,要初始化I2C设备就必须能够对I2C设备寄存器进行读写操作,这里就要用到i2c_transfer函数了。i2c_transfer函数最终会调用I2C适配器中i2c_algorithm里面的master_xfer函数,对于STM32MP1而言就是stm32f7_i2c_xfer这个函数。i2c_transfer函数原型如下:

c 复制代码
///
/// \brief i2c_transfer     传递消息
/// \param adap             所使用的I2C适配器,i2c_client会保存其对应的i2c_adapter
/// \param msgs             I2C要发送的一个或多个消息
/// \param num              消息数量,也就是msgs的数量
/// \return 
///
int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num);

着重看一下msgs这个参数,这是一个i2c_msg类型的指针参数,I2C进行数据收发说白了就是消息的传递,Linux内核使用i2c_msg结构体来描述一个消息。i2c_msg结构体定义在include/uapi/linux/i2c.h文件中,结构体内容如下:

c 复制代码
struct i2c_msg {
	__u16 addr;	/* slave address			*/
	__u16 flags;
#define I2C_M_RD		0x0001	/* read data, from slave to master */
					/* I2C_M_RD is guaranteed to be 0x0001! */
#define I2C_M_TEN		0x0010	/* this is a ten bit chip address */
#define I2C_M_DMA_SAFE		0x0200	/* the buffer of this message is DMA safe */
					/* makes only sense in kernelspace */
					/* userspace buffers are copied anyway */
#define I2C_M_RECV_LEN		0x0400	/* length will be first received byte */
#define I2C_M_NO_RD_ACK		0x0800	/* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_IGNORE_NAK	0x1000	/* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_REV_DIR_ADDR	0x2000	/* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_NOSTART		0x4000	/* if I2C_FUNC_NOSTART */
#define I2C_M_STOP		0x8000	/* if I2C_FUNC_PROTOCOL_MANGLING */
	__u16 len;		/* msg length				*/
	__u8 *buf;		/* pointer to msg data			*/
};
3.2.1.1 使用i2c_transfer进行I2C数据收发

使用i2c_transfer函数发送数据之前要先构建好i2c_msg,使用i2c_transfer进行I2C数据收发的示例代码如下:

c 复制代码
struct xxx_dev {
	// ......
	void *private_data;	/* 私有数据,一般会设置为i2c_client */
};

/*
 * @description	: 读取I2C设备多个寄存器数据
 * @param - dev:  I2C设备
 * @param - reg:  要读取的寄存器首地址
 * @param - val:  读取到的数据
 * @param - len:  要读取的数据长度
 * @return 		: 操作结果
 */
static int xxx_read_regs(struct xxx_dev *dev, u8 reg, void *val, int len)
{
	int ret;
	struct i2c_msg msg[2];
	struct i2c_client *client = (struct i2c_client *)dev->private_data;

	/* msg[0]为发送要读取的首地址 */
	msg[0].addr = client->addr;			/* I2C器件地址 */
	msg[0].flags = 0;					/* 标记为发送数据 */
	msg[0].buf = &reg;					/* 读取的首地址 */
	msg[0].len = 1;						/* reg长度*/

	/* msg[1]读取数据 */
	msg[1].addr = client->addr;			/* ap3216c地址 */
	msg[1].flags = I2C_M_RD;			/* 标记为读取数据*/
	msg[1].buf = val;					/* 读取数据缓冲区 */
	msg[1].len = len;					/* 要读取的数据长度*/

	ret = i2c_transfer(client->adapter, msg, 2);
	if(ret == 2) {
		ret = 0;
	} else {
		ret = -EREMOTEIO;
	}
	return ret;
}

/*
 * @description	: 向I2C设备多个寄存器写入数据
 * @param - dev:  要写入的设备结构体
 * @param - reg:  要写入的寄存器首地址
 * @param - val:  要写入的数据缓冲区
 * @param - len:  要写入的数据长度
 * @return 	  :   操作结果
 */
static s32 ap3216c_write_regs(struct xxx_dev *dev, u8 reg, u8 *buf, u8 len)
{
	u8 b[256];
	struct i2c_msg msg;
	struct i2c_client *client = (struct i2c_client *)dev->private_data;
	
	b[0] = reg;					/* 寄存器首地址 */
	memcpy(&b[1],buf,len);		/* 将要写入的数据拷贝到数组b里面 */
		
	msg.addr = client->addr;	/* I2C器件地址 */
	msg.flags = 0;				/* 标记为写数据 */

	msg.buf = b;				/* 要写入的数据缓冲区 */
	msg.len = len + 1;			/* 要写入的数据长度 */

	return i2c_transfer(client->adapter, &msg, 1);
}

另外还有两个API函数分别用于I2C数据的收发操作,这两个函数最终都会调用i2c_transfer。

c 复制代码
///
/// \brief i2c_master_send      I2C数据发送函数
/// \param client               I2C设备对应的 i2c_client
/// \param buf                  要发送的数据
/// \param count                要发送的数据字节树,要小于64KB,因为i2c_msg的len成员变量是一个u16(无符号16位)类型的数据
/// \return                     负值,失败;其他非负值,发送的字节数
///
int i2c_master_send(const struct i2c_client *client, const char *buf, int count);

///
/// \brief i2c_master_recv      I2C数据接收函数
/// \param client               I2C设备对应的 i2c_client
/// \param buf                  要接收的数据
/// \param count                要接收的数据字节数,要小于64KB,因为i2c_msg的len成员变量是一个u16(无符号16位)类型的数据
/// \return                     负值,失败;其他非负值,接收的字节数
///
int i2c_master_recv(const struct i2c_client *client, char *buf, int count);

I2C收发数据的关键就是i2c_msg的构建和i2c_transfer函数的调用。

3.3 辅助函数

c 复制代码
///
/// \brief 托管式内存分配,自动初始化为 0,设备释放时自动回收
/// \param dev    关联的设备结构体指针,用于绑定生命周期
/// \param size   要分配的内存字节大小
/// \param gfp    内存分配标志位(如 GFP_KERNEL/GFP_ATOMIC)
/// \return       成功返回清零后的内存指针;失败返回 NULL
///
void *devm_kzalloc(struct device *dev, size_t size, gfp_t gfp);

///
/// \brief 向 i2c_client 结构体设置私有数据指针
/// \param client  I2C 客户端设备结构体
/// \param data    要保存的私有数据指针(驱动自定义结构体)
/// \return        无返回值
///
void i2c_set_clientdata(struct i2c_client *client, void *data);

///
/// \brief 从 i2c_client 结构体获取之前设置的私有数据指针
/// \param client  I2C 客户端设备结构体
/// \return        返回保存的私有数据指针;未设置时返回 NULL
///
void *i2c_get_clientdata(const struct i2c_client *client);

///
/// \brief 宏定义,通过结构体成员指针,反推出整个结构体的起始地址
/// \param ptr     结构体成员变量的指针
/// \param type    外层结构体类型
/// \param member  结构体中成员的名字
/// \return        外层结构体的起始地址指针
///
container_of(ptr, type, member)

4 I2C驱动的应用实例

4.1 硬件原理图分析

4.2 修改设备树

打开stm32mp157d-atk.dts文件,通过节点内容追加的方式,向i2c5节点中添加ap3216c@1e"子节点,节点如下所示

c 复制代码
&i2c5 {
	pinctrl-names = "default", "sleep";
	pinctrl-0 = <&i2c5_pins_a>;
	pinctrl-1 = <&i2c5_pins_sleep_a>;
	status = "okay";

	ap3216c@1e {
		compatible = "alientek,ap3216c";
		reg = <0x1e>;
	};
};

4.3 驱动程序编写

c 复制代码
#ifndef AP3216C_H
#define AP3216C_H

#define AP3216C_ADDR    	0X1E	/* AP3216C器件地址  */

/* AP3316C寄存器 */
#define AP3216C_SYSTEMCONG	0x00	/* 配置寄存器       */
#define AP3216C_INTSTATUS	0X01	/* 中断状态寄存器   */
#define AP3216C_INTCLEAR	0X02	/* 中断清除寄存器   */
#define AP3216C_IRDATALOW	0x0A	/* IR数据低字节     */
#define AP3216C_IRDATAHIGH	0x0B	/* IR数据高字节     */
#define AP3216C_ALSDATALOW	0x0C	/* ALS数据低字节    */
#define AP3216C_ALSDATAHIGH	0X0D	/* ALS数据高字节    */
#define AP3216C_PSDATALOW	0X0E	/* PS数据低字节     */
#define AP3216C_PSDATAHIGH	0X0F	/* PS数据高字节     */

#endif
c 复制代码
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of_gpio.h>
#include <linux/semaphore.h>
#include <linux/timer.h>
#include <linux/i2c.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#include "ap3216creg.h"

#define AP3216C_CNT	1
#define AP3216C_NAME	"ap3216c"

struct ap3216c_dev {
	struct i2c_client *client;	/* i2c 设备 */
	dev_t devid;			/* 设备号 	 */
	struct cdev cdev;		/* cdev 	*/
	struct class *class;	/* 类 		*/
	struct device *device;	/* 设备 	 */
	struct device_node	*nd; /* 设备节点 */
	unsigned short ir, als, ps;		/* 三个光传感器数据 */
};

/*
 * @description	: 从ap3216c读取多个寄存器数据
 * @param - dev:  ap3216c设备
 * @param - reg:  要读取的寄存器首地址
 * @param - val:  读取到的数据
 * @param - len:  要读取的数据长度
 * @return 		: 操作结果
 */
static int ap3216c_read_regs(struct ap3216c_dev *dev, u8 reg, void *val, int len)
{
	int ret;
	struct i2c_msg msg[2];
	struct i2c_client *client = (struct i2c_client *)dev->client;

	/* msg[0]为发送要读取的首地址 */
	msg[0].addr = client->addr;			/* ap3216c地址 */
	msg[0].flags = 0;					/* 标记为发送数据 */
	msg[0].buf = &reg;					/* 读取的首地址 */
	msg[0].len = 1;						/* reg长度*/

	/* msg[1]读取数据 */
	msg[1].addr = client->addr;			/* ap3216c地址 */
	msg[1].flags = I2C_M_RD;			/* 标记为读取数据*/
	msg[1].buf = val;					/* 读取数据缓冲区 */
	msg[1].len = len;					/* 要读取的数据长度*/

	ret = i2c_transfer(client->adapter, msg, 2);
	if(ret == 2) {
		ret = 0;
	} else {
		printk("i2c rd failed=%d reg=%06x len=%d\n",ret, reg, len);
		ret = -EREMOTEIO;
	}
	return ret;
}

/*
 * @description	: 向ap3216c多个寄存器写入数据
 * @param - dev:  ap3216c设备
 * @param - reg:  要写入的寄存器首地址
 * @param - val:  要写入的数据缓冲区
 * @param - len:  要写入的数据长度
 * @return 	  :   操作结果
 */
static s32 ap3216c_write_regs(struct ap3216c_dev *dev, u8 reg, u8 *buf, u8 len)
{
	u8 b[256];
	struct i2c_msg msg;
	struct i2c_client *client = (struct i2c_client *)dev->client;
	
	b[0] = reg;					/* 寄存器首地址 */
	memcpy(&b[1],buf,len);		/* 将要写入的数据拷贝到数组b里面 */
		
	msg.addr = client->addr;	/* ap3216c地址 */
	msg.flags = 0;				/* 标记为写数据 */

	msg.buf = b;				/* 要写入的数据缓冲区 */
	msg.len = len + 1;			/* 要写入的数据长度 */

	return i2c_transfer(client->adapter, &msg, 1);
}

/*
 * @description	: 读取ap3216c指定寄存器值,读取一个寄存器
 * @param - dev:  ap3216c设备
 * @param - reg:  要读取的寄存器
 * @return 	  :   读取到的寄存器值
 */
static unsigned char ap3216c_read_reg(struct ap3216c_dev *dev, u8 reg)
{
	u8 data = 0;

	ap3216c_read_regs(dev, reg, &data, 1);
	return data;
}

/*
 * @description	: 向ap3216c指定寄存器写入指定的值,写一个寄存器
 * @param - dev:  ap3216c设备
 * @param - reg:  要写的寄存器
 * @param - data: 要写入的值
 * @return   :    无
 */
static void ap3216c_write_reg(struct ap3216c_dev *dev, u8 reg, u8 data)
{
	u8 buf = 0;
	buf = data;
	ap3216c_write_regs(dev, reg, &buf, 1);
}

/*
 * @description	: 读取AP3216C的数据,读取原始数据,包括ALS,PS和IR, 注意!
 *				: 如果同时打开ALS,IR+PS的话两次数据读取的时间间隔要大于112.5ms
 * @param - ir	: ir数据
 * @param - ps 	: ps数据
 * @param - ps 	: als数据 
 * @return 		: 无。
 */
void ap3216c_readdata(struct ap3216c_dev *dev)
{
	unsigned char i =0;
    unsigned char buf[6];
	
	/* 循环读取所有传感器数据 */
    for(i = 0; i < 6; i++) {
        buf[i] = ap3216c_read_reg(dev, AP3216C_IRDATALOW + i);	
    }

    if(buf[0] & 0X80) 	/* IR_OF位为1,则数据无效 */
		dev->ir = 0;					
	else 				/* 读取IR传感器的数据   		*/
		dev->ir = ((unsigned short)buf[1] << 2) | (buf[0] & 0X03); 			
	
	dev->als = ((unsigned short)buf[3] << 8) | buf[2];	/* 读取ALS传感器的数据 			 */  
	
    if(buf[4] & 0x40)	/* IR_OF位为1,则数据无效 			*/
		dev->ps = 0;    													
	else 				/* 读取PS传感器的数据    */
		dev->ps = ((unsigned short)(buf[5] & 0X3F) << 4) | (buf[4] & 0X0F); 
}

/*
 * @description		: 打开设备
 * @param - inode 	: 传递给驱动的inode
 * @param - filp 	: 设备文件,file结构体有个叫做private_data的成员变量
 * 					  一般在open的时候将private_data指向设备结构体。
 * @return 			: 0 成功;其他 失败
 */
static int ap3216c_open(struct inode *inode, struct file *filp)
{
	/* 从file结构体获取cdev的指针,在根据cdev获取ap3216c_dev结构体的首地址 */
	struct cdev *cdev = filp->f_path.dentry->d_inode->i_cdev;
	struct ap3216c_dev *ap3216cdev = container_of(cdev, struct ap3216c_dev, cdev);

	/* 初始化AP3216C */
	ap3216c_write_reg(ap3216cdev, AP3216C_SYSTEMCONG, 0x04);		/* 复位AP3216C 			*/
	mdelay(50);														/* AP3216C复位最少10ms 	*/
	ap3216c_write_reg(ap3216cdev, AP3216C_SYSTEMCONG, 0X03);		/* 开启ALS、PS+IR 		*/
	return 0;
}

/*
 * @description		: 从设备读取数据 
 * @param - filp 	: 要打开的设备文件(文件描述符)
 * @param - buf 	: 返回给用户空间的数据缓冲区
 * @param - cnt 	: 要读取的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 读取的字节数,如果为负值,表示读取失败
 */
static ssize_t ap3216c_read(struct file *filp, char __user *buf, size_t cnt, loff_t *off)
{
	short data[3];
	long err = 0;

	struct cdev *cdev = filp->f_path.dentry->d_inode->i_cdev;
	struct ap3216c_dev *dev = container_of(cdev, struct ap3216c_dev, cdev);
	
	ap3216c_readdata(dev);

	data[0] = dev->ir;
	data[1] = dev->als;
	data[2] = dev->ps;
	err = copy_to_user(buf, data, sizeof(data));
	return 0;
}

/*
 * @description		: 关闭/释放设备
 * @param - filp 	: 要关闭的设备文件(文件描述符)
 * @return 			: 0 成功;其他 失败
 */
static int ap3216c_release(struct inode *inode, struct file *filp)
{
	return 0;
}

/* AP3216C操作函数 */
static const struct file_operations ap3216c_ops = {
	.owner = THIS_MODULE,
	.open = ap3216c_open,
	.read = ap3216c_read,
	.release = ap3216c_release,
};

 /*
  * @description     : i2c驱动的probe函数,当驱动与
  *                    设备匹配以后此函数就会执行
  * @param - client  : i2c设备
  * @param - id      : i2c设备ID
  * @return          : 0,成功;其他负值,失败
  */
static int ap3216c_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
	int ret;
	struct ap3216c_dev *ap3216cdev;
	
	/* */
	ap3216cdev = devm_kzalloc(&client->dev, sizeof(*ap3216cdev), GFP_KERNEL);
	if(!ap3216cdev)
		return -ENOMEM;
		
	/* 注册字符设备驱动 */
	/* 1、创建设备号 */
	ret = alloc_chrdev_region(&ap3216cdev->devid, 0, AP3216C_CNT, AP3216C_NAME);
	if(ret < 0) {
		pr_err("%s Couldn't alloc_chrdev_region, ret=%d\r\n", AP3216C_NAME, ret);
		return -ENOMEM;
	}

	/* 2、初始化cdev */
	ap3216cdev->cdev.owner = THIS_MODULE;
	cdev_init(&ap3216cdev->cdev, &ap3216c_ops);
	
	/* 3、添加一个cdev */
	ret = cdev_add(&ap3216cdev->cdev, ap3216cdev->devid, AP3216C_CNT);
	if(ret < 0) {
		goto del_unregister;
	}
	
	/* 4、创建类 */
	ap3216cdev->class = class_create(THIS_MODULE, AP3216C_NAME);
	if (IS_ERR(ap3216cdev->class)) {
		goto del_cdev;
	}

	/* 5、创建设备 */
	ap3216cdev->device = device_create(ap3216cdev->class, NULL, ap3216cdev->devid, NULL, AP3216C_NAME);
	if (IS_ERR(ap3216cdev->device)) {
		goto destroy_class;
	}
	ap3216cdev->client = client;
	/* 保存ap3216cdev结构体 */
	i2c_set_clientdata(client,ap3216cdev);

	return 0;
destroy_class:
	device_destroy(ap3216cdev->class, ap3216cdev->devid);
del_cdev:
	cdev_del(&ap3216cdev->cdev);
del_unregister:
	unregister_chrdev_region(ap3216cdev->devid, AP3216C_CNT);
	return -EIO;
}

/*
 * @description     : i2c驱动的remove函数,移除i2c驱动的时候此函数会执行
 * @param - client 	: i2c设备
 * @return          : 0,成功;其他负值,失败
 */
static int ap3216c_remove(struct i2c_client *client)
{
	struct ap3216c_dev *ap3216cdev = i2c_get_clientdata(client);
	/* 注销字符设备驱动 */
	/* 1、删除cdev */
	cdev_del(&ap3216cdev->cdev);
	/* 2、注销设备号 */
	unregister_chrdev_region(ap3216cdev->devid, AP3216C_CNT); 
	/* 3、注销设备 */
	device_destroy(ap3216cdev->class, ap3216cdev->devid);
	/* 4、注销类 */
	class_destroy(ap3216cdev->class); 
	return 0;
}

/* 传统匹配方式ID列表 */
static const struct i2c_device_id ap3216c_id[] = {
	{"alientek,ap3216c", 0},  
	{}
};

/* 设备树匹配列表 */
static const struct of_device_id ap3216c_of_match[] = {
	{ .compatible = "alientek,ap3216c" },
	{ /* Sentinel */ }
};

/* i2c驱动结构体 */	
static struct i2c_driver ap3216c_driver = {
	.probe = ap3216c_probe,
	.remove = ap3216c_remove,
	.driver = {
			.owner = THIS_MODULE,
		   	.name = "ap3216c",
		   	.of_match_table = ap3216c_of_match, 
		   },
	.id_table = ap3216c_id,
};
		   
/*
 * @description	: 驱动入口函数
 * @param 		: 无
 * @return 		: 无
 */
static int __init ap3216c_init(void)
{
	int ret = 0;

	ret = i2c_add_driver(&ap3216c_driver);
	return ret;
}

/*
 * @description	: 驱动出口函数
 * @param 		: 无
 * @return 		: 无
 */
static void __exit ap3216c_exit(void)
{
	i2c_del_driver(&ap3216c_driver);
}

/* module_i2c_driver(ap3216c_driver) */

module_init(ap3216c_init);
module_exit(ap3216c_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("ALIENTEK");
MODULE_INFO(intree, "Y");

4.4 用户侧测试程序编写

c 复制代码
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "sys/ioctl.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
#include <poll.h>
#include <sys/select.h>
#include <sys/time.h>
#include <signal.h>
#include <fcntl.h>
/*
 * @description		: main主程序
 * @param - argc 	: argv数组元素个数
 * @param - argv 	: 具体参数
 * @return 			: 0 成功;其他 失败
 */
int main(int argc, char *argv[])
{
	int fd;
	char *filename;
	unsigned short databuf[3];
	unsigned short ir, als, ps;
	int ret = 0;

	if (argc != 2) {
		printf("Error Usage!\r\n");
		return -1;
	}

	filename = argv[1];
	fd = open(filename, O_RDWR);
	if(fd < 0) {
		printf("can't open file %s\r\n", filename);
		return -1;
	}

	while (1) {
		ret = read(fd, databuf, sizeof(databuf));
		if(ret == 0) { 			/* 数据读取成功 */
			ir =  databuf[0]; 	/* ir传感器数据 */
			als = databuf[1]; 	/* als传感器数据 */
			ps =  databuf[2]; 	/* ps传感器数据 */
			printf("ir = %d, als = %d, ps = %d\r\n", ir, als, ps);
		}
		usleep(200000); /* 200ms */
	}
	close(fd);	/* 关闭文件 */	
	return 0;
}
相关推荐
城管不管12 分钟前
重生——第十次面试之开源中国一面挂
java·linux·开发语言·算法·面试·职场和发展·开源
蜀道山老天师12 分钟前
Zabbix监控Apache与Nginx应用实践完整指南
linux·运维·nginx·apache·zabbix
暖核20 分钟前
Linux实战入门:从零编写第一个Bash Shell脚本
linux·运维·bash
March.s1 小时前
深入理解 Linux 系统启动原理
linux·运维·服务器
YSL0701241 小时前
动态内存管理
c语言
BreezeJuvenile1 小时前
基于C语言的SPSC环形缓冲区程序实现
c语言·环形缓冲区
zbyyd1 小时前
Linux 基础与 Shell 命令
linux·运维·arm开发
起名真的太难了1 小时前
Linux系统运维与远程管理核心命令速查指南
linux·运维·服务器
SuperByteMaster1 小时前
recursive mutex的使用场景和场景代码
c语言
xiaoye-duck2 小时前
《Linux网络编程》吃透 Linux 网络传输全流程:完整梳理局域网通信、封装解包与跨网络传输原理
linux·网络