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;
}
相关推荐
IMPYLH3 小时前
Linux 的 dirname 命令
linux·运维·服务器·数据库
always_TT3 小时前
C语言保留字与标识符规则
c语言·开发语言
扛枪的书生4 小时前
Nginx 学习总结
linux
认真的薛薛5 小时前
Docker网络模式
linux·运维·数据库·面试·github
AI科技星5 小时前
光速螺旋量子几何统一场论——基于 v ≡ c 公理的四大基本力全维度求导证明与精准数值验证
c语言·开发语言·人工智能·算法·机器学习·平面
程序猿编码5 小时前
隐匿注入型ELF加壳器:原理、设计与实现深度解析(C/C++ 代码实现)
c语言·网络·c++·elf·代码注入
UP_Continue5 小时前
Linux--日志的模拟实现
linux·运维·服务器
xlp666hub5 小时前
深度剖析Linux Input子系统(1):宏观架构与核心原理
linux
东北甜妹5 小时前
playbook
linux·服务器·网络