第十二节 I2C 子系统–mpu6050 驱动实验

本文详细介绍了STM32MP1平台上的i2C驱动程序的结构,包括i2C总线驱动、设备驱动以及设备树配置。重点解析了i2C总线驱动的注册、设备驱动的匹配和操作函数,特别是如何使用i2c_transfer函数进行数据传输。此外,还展示了MPU6050传感器的设备驱动编写过程,包括初始化、读写操作以及测试应用程序的实现。
摘要由CSDN通过智能技术生成

本章我们以板载MPU6050 为例讲解i2c 驱动程序的编写,本章主要分为五部分内容。

  • 第一部分,i2c 基本知识,回忆i2c 物理总线和基本通信协议。
  • 第二部分,linux 下的i2c 驱动框架。
  • 第三部分,i2c 总线驱动代码拆解。
  • 第四部分,i2c 设备驱动的核心函数。
  • 第五部分,MPU6050 驱动以及测试程序。

i2c 基本知识

i2c 物理总线

在这里插入图片描述

如上图所示,i2c 支持一主多从,各设备地址独立,标准模式传输速率为100kbit/s,快速模式为400kbit/s。总线通过上拉电阻接到电源。当I2C 设备空闲时,会输出高阻态,而当所有设备都空闲,都输出高阻态时,由上拉电阻把总线拉成高电平。

I2C 物理总线使用两条总线线路,SCL 和SDA。

  • SCL:时钟线,数据收发同步
  • SDA:数据线,传输具体数据

i2c 基本通信协议

起始信号(S) 与停止信号§

在这里插入图片描述
当SCL 线为高电平时,SDA 线由高到低的下降沿,为传输开始标志(S)。直到主设备发出结束信号§,否则总线状态一直为忙。结束标志§ 为,当SCL 线为高电平时,SDA 线由低到高的上升沿。

数据格式与应答信号(ACK/NACK)

在这里插入图片描述

i2c 的数据字节定义为8-bits 长度,对每次传送的总字节数量没有限制, 但对每一次传输必须伴有一个应答(ACK) 信号,其时钟由主设备提供,而真正的应答信号由从设备发出,在时钟为高时,通过拉低并保持SDA 的值来实现。如果从设备忙,它可以使SCL 保持在低电平,这会强制使主设备进入等待状态。当从设备空闲后,并且释放时钟线,原来的数据传输才会继续。

主机与从机通信

在这里插入图片描述
开始标志(S) 发出后,主设备会传送一个7 位的Slave 地址,并且后面跟着一个第8 位,称为Read/Write 位。R/W 位表示主设备是在接受从设备的数据还是在向其写数据。然后,主设备释放SDA 线,等待从设备的应答信号(ACK)。每个字节的传输都要跟随有一个应答位。应答产生时,从设备将SDA 线拉低并且在SCL 为高电平时保持低。数据传输总是以停止标志(P)结束,然后释放通信线路。然而,主设备也可以产生重复的开始信号去操作另一台从设备,而不发出结束标志。综上可知,所有的SDA 信号变化都要在SCL 时钟为低电平时进行,除了开始和结束标志

i2c 对mpu6050 进行数据读写

单字节写入
在这里插入图片描述
连续字节写入
在这里插入图片描述
对MPU6050 进行写操作时,主设备发出开始标志(S) 和写地址(地址位加一个R/W 位,0 为写)。MPU6050 产生应答信号。然后主设备开始传送寄存器地址(RA),接到应答后,开始传送寄存器数据,然后仍然要有应答信号,连续写入多字节时依次类推。

单字节读出
在这里插入图片描述
连续字节读出
在这里插入图片描述
对MPU6050 进行读操作时,主设备发出开始标志(S) 和读地址(地址位加一个R/W 位,1 为读)。等待MPU6050 产生应答信号。然后发送寄存器地址,告诉MPU6050 读哪一个寄存器。紧接着,收到应答信号后,主设备再发一个开始信号,然后发送从设备读地址。MPU6050 产生应答信号并开始发送寄存器数据。通信以主设备产生的拒绝应答信号(NACK) 和结束标志§ 结束。

i2c 驱动框架

在编写单片机裸机i2c 驱动时我们需要根据i2c 协议手动配置i2c 控制寄存器使其能够输出起始信号、停止信号、数据信息等等。

在Linux 系统中则采用了总线、设备驱动模型。我们之前讲解的平台设备也是采用了这种模型,只不过平台总线是一个虚拟的总线。

我们知道一个i2c(例如i2c1) 上可以挂在多个i2c 设备,例如MPU6050、i2c 接口的OLED 显示屏、摄像头(摄像头通过i2c 接口发送控制信息)等等,这些设备共用一个i2c,这个i2c 的驱动我们称为i2c 总线驱动。而对应具体的设备, 例如mpu6050 的驱动就是i2c 设备驱动。这样我们要使用mpu6050 就需要拥有“两个驱动”一个是i2c 总线驱动和mpu6050 设备驱动。

  • i2c 总线驱动由芯片厂商提供(驱动复杂,官方提供了经过测试的驱动,我们直接用),
  • mpu6050 设备驱动可以从mpu6050 芯片厂家那里获得(不确定有),也可以我们手动编写。

在这里插入图片描述
如上图所示,i2c 驱动框架包括i2c 总线驱动、具体某个设备的驱动。

i2c 总线包括i2c 设备(i2c_client) 和i2c 驱动(i2c_driver), 当我们向linux 中注册设备或驱动的时候,按照i2c 总线匹配规则进行配对,配对成功,则可以通过i2c_driver 中.prob 函数创建具体的设备驱动。在现代linux 中,i2c 设备不再需要手动创建,而是使用设备树机制引入,设备树节点是与paltform 总线相配合使用的。所以需先对i2c 总线包装一层paltform 总线,当设备树节点转换为平台总线设备时,我们在进一步将其转换为i2c 设备,注册到i2c 总线中。

设备驱动创建成功,我们还需要实现设备的文件操作接口(file_operations),file_operations 中会使用到内核中i2c 核心函数(i2c 系统已经实现的函数,专门开放给驱动工程师使用)。使用这些函数会涉及到i2c 适配器,也就是i2c 控制器。由于ic2 控制器有不同的配置,所有linux 将每一个i2c控制器抽象成i2c 适配器对象。这个对象中存在一个很重要的成员变量——Algorithm,Algorithm中存在一系列函数指针,这些函数指针指向真正硬件操作代码。

关键数据结构

在开始拆解i2c 驱动框架的源码之前,先了解其中几个重要的对象。

struct i2c_adapter

i2c_ 适配器对应一个i2c 控制器,是用于标识物理i2c 总线以及访问它所需的访问算法的结构。

列表1: i2c_adapter 结构体(内核源码/include/linux/i2c.h)

/*
 * i2c_adapter is the structure used to identify a physical i2c bus along
 * with the access algorithms necessary to access it.
 */
 struct i2c_adapter {
 struct module *owner;
 unsigned int class; /* classes to allow probing for */
 const struct i2c_algorithm *algo; /* the algorithm to access the bus */
 void *algo_data;

 /* data fields that are valid for all devices */
 struct rt_mutex bus_lock;

 int timeout; /* in jiffies */
 int retries;
 struct device dev; /* the adapter device */

 int nr;
 char name[48];
 struct completion dev_released;

 struct mutex userspace_clients_lock;
 struct list_head userspace_clients;

 struct i2c_bus_recovery_info *bus_recovery_info;
 const struct i2c_adapter_quirks *quirks;
 };
  • algo: struct i2c_algorithm 结构体,访问总线的算法;
  • dev: struct device 结构体,控制器,表明这是一个设备。

struct i2c_algorithm

i2c_algorithm 是对i2c 通信方法的抽象接口,这个抽象接口使得不同芯片上的i2c 外设,能使用i2c 总线模型。

struct i2c_algorithm 结构体用于指定访问总线(i2c)的算法,结构体中包含了几个函数指针成员,不同的厂商根据自身硬件的特性,来自行实现自己的i2c 传输功能。

更直白的说,i2c 设备例如mpu6050、i2c 接口的oled 屏等等,就会通过这些函数接口使用i2c 总线实现收、发数据的。在i2c 的总线驱动中会实现这些(部分)函数。

列表2: i2c_algorithm 结构体(内核源码/include/linux/i2c.h)

struct i2c_algorithm {
 /* If an adapter algorithm can't do I2C-level access, set master_xfer
 to NULL. If an adapter algorithm can do SMBus access, set
 smbus_xfer. If set to NULL, the SMBus protocol is simulated
 using common I2C messages */
 /* master_xfer should return the number of messages successfully
 processed, or a negative value on error */
 	int (*master_xfer)(struct i2c_adapter *adap, struct i2c_msg *msgs,
	 					int num);
 	int (*smbus_xfer) (struct i2c_adapter *adap, u16 addr,
						unsigned short flags, char read_write,
 						u8 command, int size, union i2c_smbus_data *data);

 /* To determine what the adapter supports */
 u32 (*functionality) (struct i2c_adapter *);

#if IS_ENABLED(CONFIG_I2C_SLAVE)
 int (*reg_slave)(struct i2c_client *client);
 int (*unreg_slave)(struct i2c_client *client);
 #endif
 };
  • master_xfer:作为主设备时的发送函数,应该返回成功处理的消息数,或者在出错时返回负值。
  • smbus_xfer: smbus 是一种i2c 协议的协议,如硬件上支持,可以实现这个接口。

struct i2c_client

表示i2c 从设备

列表3: i2c_client 结构体(内核源码/include/linux/i2c.h)

struct i2c_client {
	 	unsigned short flags; /* div., see below  */
 		unsigned short addr; /* chip address - NOTE: 7bit  */

		char name[I2C_NAME_SIZE];
 		struct i2c_adapter *adapter; /* the adapter we sit on  */
 		struct device dev; /* the device structure  */
 		int init_irq; /* irq set at initialization  */
 		int irq; /* irq issued by device  */
 		struct list_head detected;
 		#if IS_ENABLED(CONFIG_I2C_SLAVE)
				i2c_slave_cb_t slave_cb; /* callback for slave mode */
	 	#endif
};
  • flags::I2C_CLIENT_TEN 表示设备使用10 位芯片地址,I2C 客户端PEC 表示它使用SMBus数据包错误检查
  • addr: addr 在连接到父适配器的I2C 总线上使用的地址。
  • name:表示设备的类型,通常是芯片名。
  • adapter: struct i2c_adapter 结构体,管理托管这个I2C 设备的总线段。
  • dev: Driver model 设备节点。
  • init_irq:作为从设备时的发送函数。
  • irq:表示该设备生成的中断号。
  • detected: struct list_head i2c 的成员_ 驱动程序. 客户端列表或i2c 核心的用户空间设备列表。
  • slave_cb:使用适配器的I2C 从模式时回调。适配器调用它来将从属事件传递给从属驱动程序。i2c_ 客户端识别连接到i2c 总线的单个设备(即芯片)。暴露在Linux 下的行为是由管理设备的驱动程序定义的。

struct i2c_driver

i2c 设备驱动程序

列表4: i2c_driver 结构体(内核源码/include/linux/i2c.h)

struct i2c_driver {
 		unsigned int class;

	 	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;

 		int (*detect)(struct i2c_client *, struct i2c_board_info *);

 		const unsigned short *address_list;
 		struct list_head clients;

 		...
 };
  • probe: i2c 设备和i2c 驱动匹配后,回调该函数指针。
  • id_table: struct i2c_device_id 要匹配的从设备信息。
  • address_list:设备地址
  • clients:设备链表
  • detect:设备探测函数

i2c 总线驱动

i2c 总线驱动由芯片厂商提供,如果我们使用ST 官方提供的Linux 内核,i2c 总线驱动已经保存在内核中,并且默认情况下已经编译进内核。

下面结合源码简单介绍i2c 总线的运行机制。

  • 1、注册I2C 总线
  • 2、将I2C 驱动添加到I2C 总线的驱动链表中
  • 3、遍历I2C 总线上的设备链表,根据i2c_device_match 函数进行匹配,如果匹配调用i2c_device_probe 函数
  • 4、i2c_device_probe 函数会调用I2C 驱动的probe 函数

i2c 总线定义

列表5: i2c 总线定义(内核源码/drivers/i2c/i2c-corebase.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,
 };

i2c 总线维护着两个链表(I2C 驱动、I2C 设备),管理I2C 设备和I2C 驱动的匹配和删除等

i2c 总线注册

linux 启动之后,默认执行i2c_init。

列表6: i2c 总线注册(内核源码/drivers/i2c/i2c-corebase.
c)

static int __init i2c_init(void)
 {
 		int retval;
 		...
 		retval = bus_register(&i2c_bus_type);
		if (retval)
 				return retval;

 		is_registered = true;
 		...
 		retval = i2c_add_driver(&dummy_driver);
 		if (retval)
				goto class_err;

	 	if (IS_ENABLED(CONFIG_OF_DYNAMIC))
 				WARN_ON(of_reconfig_notifier_register(&i2c_of_notifier));
 		if (IS_ENABLED(CONFIG_ACPI))
 				WARN_ON(acpi_reconfig_notifier_register(&i2c_acpi_notifier));

 		return 0;
 		...
 }
  • 第5 行:bus_register 注册总线i2c_bus_type,总线定义如上所示。
  • 第11 行:i2c_add_driver 注册设备dummy_driver。

i2c 设备和i2c 驱动匹配规则

列表7: i2c 设备和i2c 驱动匹配规则(内核源码/drivers/i2c/i2c-core-base.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;
 }
  • of_driver_match_device: 设备树匹配方式, 比较I2C 设备节点的compatible 属性和of_device_id 中的compatible 属性
  • acpi_driver_match_device: ACPI 匹配方式
  • i2c_match_id: i2c 总线传统匹配方式,比较I2C 设备名字和i2c 驱动的id_table->name 字段是否相等

在i2c 总线驱动代码源文件中,我们只简单介绍重要的几个点,如果感兴趣可自行阅读完整的i2c驱动源码。通常情况下,看驱动程序首先要找到驱动的入口和出口函数,驱动入口和出口位于驱动的末尾,如下所示。

列表8: 驱动入口和出口函数(内核源码/drivers/i2c/busses/i2c-st.c)

static struct platform_driver st_i2c_driver = {
 	.driver = {
 		.name = "st-i2c",
		.of_match_table = st_i2c_match,
		.pm = ST_I2C_PM,
 },
 .probe = st_i2c_probe,
 .remove = st_i2c_remove,
 };

module_platform_driver(st_i2c_driver);

驱动注册函数module_platform_driver 很简单,我们可以从中得到i2c 驱动是一个平台驱动,并且我们知道平台驱动结构体是“st_i2c_driver”,平台驱动结构体如下所示。

列表9: 平台设备驱动结构体(内核源码/drivers/i2c/busses/i2c-st.c)

static const struct of_device_id st_i2c_match[] = {
 { .compatible = "st,comms-ssc-i2c", },
 { .compatible = "st,comms-ssc4-i2c", },
 {},
 };
 MODULE_DEVICE_TABLE(of, st_i2c_match);

 static struct platform_driver st_i2c_driver = {
	.driver = {
 		.name = "st-i2c",
 		.of_match_table = st_i2c_match,
	 	.pm = ST_I2C_PM,
 	},
 	.probe = st_i2c_probe,
 	.remove = st_i2c_remove,
 };
  • 第1-5 行:是i2c 驱动的匹配表,用于和设备树节点匹配,
  • 第8-16 行:是初始化的平台设备结构体,从这个结构体我们可以找到.prob 函数,.prob 函数的作用我们都很清楚,通常情况下该函数实现设备的基本初始化。

以下是.porbe 函数的内容。

列表10: i2c 驱动.probe 函数(内核源码/drivers/i2c/busses/i2c-st.c)

static int st_i2c_probe(struct platform_device *pdev)
{
 struct device_node *np = pdev->dev.of_node;
 struct st_i2c_dev *i2c_dev;
 struct resource *res;
 u32 clk_rate;
 struct i2c_adapter *adap;
 int ret;

 i2c_dev = devm_kzalloc(&pdev->dev, sizeof(*i2c_dev), GFP_KERNEL);
 if (!i2c_dev)
 	return -ENOMEM;

 res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
 i2c_dev->base = devm_ioremap_resource(&pdev->dev, res);
 if (IS_ERR(i2c_dev->base))
 	return PTR_ERR(i2c_dev->base);

 i2c_dev->irq = irq_of_parse_and_map(np, 0);
 if (!i2c_dev->irq) {
 	dev_err(&pdev->dev, "IRQ missing or invalid\n");
 	return -EINVAL;
 }

 i2c_dev->clk = of_clk_get_by_name(np, "ssc");
 if (IS_ERR(i2c_dev->clk)) {
 	dev_err(&pdev->dev, "Unable to request clock\n");
 	return PTR_ERR(i2c_dev->clk);
 }

 i2c_dev->mode = I2C_MODE_STANDARD;
 ret = of_property_read_u32(np, "clock-frequency", &clk_rate);
 if (!ret && (clk_rate == I2C_MAX_FAST_MODE_FREQ))
 i2c_dev->mode = I2C_MODE_FAST;

 i2c_dev->dev = &pdev->dev;

 ret = devm_request_threaded_irq(&pdev->dev, i2c_devirq,
 		NULL, st_i2c_isr_thread,
 		IRQF_ONESHOT, pdev->name, i2c_dev);
 if (ret) {
 dev_err(&pdev->dev, "Failed to request irq %i\n", i2c_dev->irq);
 return ret;
 }

 pinctrl_pm_select_default_state(i2c_dev->dev);
 /* In case idle state available, select it */
 pinctrl_pm_select_idle_state(i2c_dev->dev);

 ret = st_i2c_of_get_deglitch(np, i2c_dev);
 if (ret)
 	return ret;

 adap = &i2c_dev->adap;
 i2c_set_adapdata(adap, i2c_dev);
 snprintf(adap->name, sizeof(adap->name), "ST I2C(%pa)", &res->start);
 adap->owner = THIS_MODULE;
 adap->timeout = 2 * HZ;
 adap->retries = 0;
 adap->algo = &st_i2c_algo;
 adap->bus_recovery_info = &st_i2c_recovery_info;
 adap->dev.parent = &pdev->dev;
 adap->dev.of_node = pdev->dev.of_node;

 init_completion(&i2c_dev->complete);

 ret = i2c_add_adapter(adap);
 if (ret)
 	return ret;

 platform_set_drvdata(pdev, i2c_dev);

 dev_info(i2c_dev->dev, "%s initialized\n", adap->name);

 return 0;
 }
  • 第10-12 行:为st_i2c_dev 结构体申请空间,后面会详述这个结构体。
  • 第14-17 行:获取reg 属性,这里使用的是内核提供的“platform_get_resource”它实现的功能和我们使用of 函数获取reg 属性相同。这里的代码获取得到了i2c 的基地址,并且使用“devm_ioremap_resource”将其转化为虚拟地址。
  • 第19-23 行:获取中断号,在i2c 的设备树节点中定义了中断,这里获取得到的中断号申请中断时会用到,获取函数使用的是内核提供的函数irq_of_parse_and_map。
  • 第25-34 行:获取时钟配置并配置。

列表11: st_i2c_dev 结构体

struct st_i2c_dev {
	struct i2c_adapter a	dap;
	struct device		 	*dev;
	void __iomem 			*base;
	struct completion 		complete;
	int 					irq;
	struct clk 				*clk;
	int 					mode;
	u32 					scl_min_width_us;
	u32 					sda_min_width_us;
	struct 					st_i2c_client client;
	bool 					busy;
};

st_i2c_dev 结构体成员较多,保存了厂商的i2c 控制器信息以及即将注册到总线中的adapter 适配器,通过这个结构体,可以关联linux 下的i2c 总线模型和产商芯片驱动功能。

  • adap:即将注册到总线中的adapter 适配器
  • irq:保存i2c 的中断号
  • clk: clk 结构体保存时钟相关信息
  • busy:驱动状态

在前面的probe 函数函数中,第7 行定义了一个adap 结构体指针,用于指向初始化st_i2c_dev 结构体中的adap 成员。又通过55 行的i2c_set_adapdata 函数,将adap 关联回st_i2c_dev 变量i2c_dev。这里可以简单的理解成一个环形,两个结构体互相关联,

在57 到67 行里,将即将要注册到系统中的adap 进行了初始化。

我们重点看60 行的:“adap->algo = &st_i2c_algo;”,它就是用于初始化“访问i2c 总线的传输算法”。“st_i2c_algo”定义如下。

列表12: i2c_algorithm 结构体实例st_i2c_algo

static const struct i2c_algorithm st_i2c_algo = {
	.master_xfer = st_i2c_xfer,
	.functionality = st_i2c_func,
};

st_i2c_algo 结构体内指定了两个函数,它们就是外部访问i2c 总线的接口:

  • 函数st_i2c_func 只是用于返回I2C 总线提供的功能。
  • 函数st_i2c_xfer 真正实现访问i2c 外设,进行数据传输。

st_i2c_xfer 函数定义如下:

列表13: st_i2c_xfer 函数

static int st_i2c_xfer(struct i2c_adapter *i2c_adap,
						struct i2c_msg msgs[], int num)
{
	struct st_i2c_dev *i2c_dev = i2c_get_adapdata(i2c_adap);
	int ret, i;

	i2c_dev->busy = true;

	ret = clk_prepare_enable(i2c_dev->clk);
	if (ret) {
		dev_err(i2c_dev->dev, "Failed to prepare_enable clock\n");
		return ret;
}

	pinctrl_pm_select_default_state(i2c_dev->dev);

	st_i2c_hw_config(i2c_dev);

	for (i = 0; (i < num) && !ret; i++)
ret = st_i2c_xfer_msg(i2c_dev, &msgs[i], i == 0, i == num - 1);

	pinctrl_pm_select_idle_state(i2c_dev->dev);

	clk_disable_unprepare(i2c_dev->clk);

	i2c_dev->busy = false;

	return (ret < 0) ? ret : i;
}

在编写设备驱动如mpu6050 的驱动时,我们会使用“i2c_transfer”函数执行数据的传输,i2c_transfer函数最终就是调用st_i2c_xfer 函数实现具体的收发工作。届时我们会详细介绍i2c_transfer 函数的用法。

在st_i2c_xfer 中,实际的收发又是通过st_i2c_xfer_msg 来完成,函数实现如下:

列表14: st_i2c_xfer_msg 函数

static int st_i2c_xfer_msg(struct st_i2c_dev *i2c_dev, struct i2c_msg *msg,
				bool is_first, bool is_last)
{
	struct st_i2c_client *c = &i2c_dev->client;
	u32 ctl, i2c, it;
	unsigned long timeout;
	int ret;
	
	c->addr = i2c_8bit_addr_from_msg(msg);
	c->buf = msg->buf;
	c->count = msg->len;
	c->xfered = 0;
	c->result = 0;
	c->stop = is_last;
	
	reinit_completion(&i2c_dev->complete);
	
	ctl = SSC_CTL_EN | SSC_CTL_MS | SSC_CTL_EN_RX_FIFO | SSC_CTL_EN_TX_FIFO;
	st_i2c_set_bits(i2c_dev->base + SSC_CTL, ctl);
	
	i2c = SSC_I2C_TXENB;
	if (c->addr & I2C_M_RD)
		i2c |= SSC_I2C_ACKG;
	st_i2c_set_bits(i2c_dev->base + SSC_I2C, i2c);
	
	/* Write slave address */
	st_i2c_write_tx_fifo(i2c_dev, c->addr);
	
	/* Pre-fill Tx fifo with data in case of write */
	if (!(c->addr & I2C_M_RD))
		st_i2c_wr_fill_tx_fifo(i2c_dev);
		
	it = SSC_IEN_NACKEN | SSC_IEN_TEEN | SSC_IEN_ARBLEN;
	writel_relaxed(it, i2c_dev->base + SSC_IEN);
	
	if (is_first) {
		ret = st_i2c_wait_free_bus(i2c_dev);
		if (ret)
			return ret;
			
		st_i2c_set_bits(i2c_dev->base + SSC_I2C, SSC_I2C_STRTG);
	}
	
	timeout = wait_for_completion_timeout(&i2c_dev->complete,
			i2c_dev->adap.timeout);
	ret = c->result;
	
	if (!timeout) {
		dev_err(i2c_dev->dev, "Write to slave 0x%x timed out\n",
				c->addr);
	ret = -ETIMEDOUT;
	}
	i2c = SSC_I2C_STOPG | SSC_I2C_REPSTRTG;
	st_i2c_clr_bits(i2c_dev->base + SSC_I2C, i2c);
	
	writel_relaxed(SSC_CLR_SSCSTOP | SSC_CLR_REPSTRT,
			i2c_dev->base + SSC_CLR);
	return ret;
}

这里就不带大家展开了,操作内容都是比较针对底层外设寄存器的。

至此我们的i2c 平台驱动就给大家分析完了,probe 函数完成了i2c 的基本初始化并将其添加到了系统中。驱动中也实现i2c 对外接口函数。我们在初始化i2c_adapter 结构体时已经初始化了访问总线算法结构体i2c_algorithm,在前面也介绍过了。

那么总结整个probe 函数,主要完成了两个工作。第一,初始化i2c 硬件,第二,初始化一个可以访问i2c 外设的i2c_adapter 结构体,并将其添加到系统中。

i2c 设备驱动核心函数

i2c_add_adapter()

向linux 系统注册一个i2c 适配器

列表15: 注册一个i2c 适配器(内核源码/drivers/i2c/i2ccore-base.c)

//linux 系统自动设置i2c 适配器编号(adapter->nr)
int i2c_add_adapter(struct i2c_adapter *adapter)
//手动设置i2c 适配器编号(adapter->nr)
int i2c_add_numbered_adapter(struct i2c_adapter *adapter)

参数

  • adapter: i2c 物理控制器对应的适配器

返回值

  • 成功: 0
  • 失败:负数

i2c_add_driver() 宏

列表16: 注册一个i2c 驱动(内核源码/include/linux/i2c.h)

#define i2c_add_driver(driver)

这个宏函数的本质是调用了i2c_register_driver() 函数,函数如下。

i2c_register_driver() 函数

列表17: 注册一个i2c 驱动(内核源码/drivers/i2c/i2ccore-base.c)

int i2c_register_driver(struct module *owner, struct i2c_driver *driver)

参数

  • owner:一般为THIS_MODULE
  • driver:要注册的i2c_driver.

返回值

  • 成功: 0
  • 失败:负数

i2c_transfer() 函数

i2c_transfer() 函数最终就是调用我们前面讲到的st_i2c_xfer() 函数来实现数据传输。

列表18: 收发i2c 消息(内核源码/drivers/i2c/i2c-corebase.c)

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

参数

  • adap :struct i2c_adapter 结构体,收发消息所使用的i2c 适配器,i2c_client 会保存其对应的i2c_adapter
  • msgs: struct i2c_msg 结构体,i2c 要发送的一个或多个消息
  • num :消息数量,也就是msgs 的数量

返回值

  • 成功:发送的msgs 的数量
  • 失败:负数

i2c_msg 结构体

列表19: 描述一个iic 消息(内核源码/include/uapi/linux/i2c.h)

struct i2c_msg {
	 __u16 addr; /* slave address */
 	__u16 flags;
 	...
	 __u16 len; /* msg length */
 	__u8 *buf; /* pointer to msg data */
};
  • addr: iic 设备地址
  • flags:消息传输方向和特性。I2C_M_RD:表示读取消息;0:表示发送消息。
  • len:消息数据的长度
  • buf:字符数组存放消息,作为消息的缓冲区

i2c_master_send() 函数

列表20: 发送一个i2c 消息(内核源码/include/linux/i2c.h)

static inline int i2c_master_send(const struct i2c_client *client,
				const char *buf, int count)
{
	return i2c_transfer_buffer_flags(client, (char *)buf, count, 0);
};

i2c_master_recv() 函数

static inline int i2c_master_recv(const struct i2c_client *client,
				char *buf, int count)
{
	return i2c_transfer_buffer_flags(client, buf, count, I2C_M_RD);
};

i2c_transfer_buffer_flags() 函数

列表22: 发送一个i2c 消息(内核源码/drivers/i2c/i2ccore-base.c)

int i2c_transfer_buffer_flags(const struct i2c_client *client, char *buf,
				int count, u16 flags)
{
	int ret;
	struct i2c_msg msg = {
		.addr = client->addr,
		.flags = flags | (client->flags & I2C_M_TEN),
		.len = count,
		.buf = buf,
};

	ret = i2c_transfer(client->adapter, &msg, 1);

/*
 * If everything went ok (i.e. 1 msg transferred), return #bytes
 * transferred, else error code.
 */
	return (ret == 1) ? count : ret;
}

下面以mpu6050 为例讲解如何编写i2c 设备驱动。

mpu6050 驱动实验

硬件介绍

本节实验使用到STM32MP1 开发板上的MPU6050。

MPU6050 是一款运动处理传感器,它集成了3 轴MEMS 陀螺仪,3 轴MEMS 加速度计。

硬件原理图分析

在这里插入图片描述

MPU6050 是通过i2c 连接到开发板的,其中传感器上的SDA 和SCL 连到开发板i2c1; 开发板要控制MPU6050 需要先复用这两个引脚为i2c 控制器引脚。

查看MPU 芯片手册我们可以知道,MPU6050 的slave 地址为b110100X,七位字长,最低有效位X 由AD0 管脚上的逻辑电平决定。从原理图上可以看到,AD0 接地,则地址为b1101000,也就是0x68。

设备树

由上面的原理图分析,我们可以得到下面的mpu6050 的设备树插件。

在这里插入图片描述

列表23: i2c 设备树插件

// SPDX-License-Identifier: (GPL-2.0+ OR BSD-3-Clause)
/*
 * Copyright (C) STMicroelectronics 2018 - All Rights Reserved
 * Author: Alexandre Torgue <alexandre.torgue@st.com>.
*/

/dts-v1/;
/plugin/;
//#include "../stm32mp157c.dtsi"
#include <dt-bindings/pinctrl/stm32-pinfunc.h>
#include <dt-bindings/input/input.h>
#include <dt-bindings/mfd/st,stpmic1.h>
#include <dt-bindings/gpio/gpio.h>

/{
	fragment@0{
		target=<&i2c1>;
		__overlay__{
			pinctrl-names = "default", "sleep";
			pinctrl-0 = <&i2c1_pins_a>;
			pinctrl-1 = <&i2c1_pins_sleep_a>;
			i2c-scl-rising-time-ns = <100>;
			i2c-scl-falling-time-ns = <7>;
			status = "okay";
			/delete-property/dmas;
			/delete-property/dma-names;
		};
	};
	fragment@1{
		target=<&pinctrl>;
		__overlay__{
			i2c1_pins_a: i2c1-0 {
				pins {
					pinmux = <STM32_PINMUX('F', 14, AF5)>, /* I2C1_SCL */
							<STM32_PINMUX('F', 15, AF5)>; /* I2C1_SDA */
							bias-disable;
							drive-open-drain;
							slew-rate = <0>;
						};
			};

		i2c1_pins_sleep_a: i2c1-1 {
			pins {
				pinmux = <STM32_PINMUX('F', 14, ANALOG)>, /* I2C1_SCL */
						<STM32_PINMUX('F', 15, ANALOG)>; /* I2C1_SDA */
			};
		};
	};
  };
};
  • 第17 行:在i2c1 节点下面添加新属性内容
  • 第19-26 行:添加pinctrl 信息,以及i2c 的各种属性
  • 第30 行:在pinctrl 节点下面添加新的子节点i2c1_pins_a、i2c1_pins_sleep_a
  • 第32-40 行:设置i2c1_pins_a 的状态使用的引脚功能。
  • 第42-47 行:设置i2c1_pins_sleep_a 的状态使用的引脚功能。

列表24: mpu6050 设备树插件

// SPDX-License-Identifier: (GPL-2.0+ OR BSD-3-Clause)
/*
 * Copyright (C) STMicroelectronics 2018 - All Rights Reserved
 * Author: Alexandre Torgue <alexandre.torgue@st.com>.
 */

/dts-v1/;
/plugin/;
//#include "../stm32mp157c.dtsi"
#include <dt-bindings/pinctrl/stm32-pinfunc.h>
#include <dt-bindings/input/input.h>
#include <dt-bindings/mfd/st,stpmic1.h>
#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/interrupt-controller/irq.h>

/{
	fragment@0{
		target=<&i2c1>;
			__overlay__ {
			#address-cells = <1>;
			#size-cells = <0>;
			mpu6050@68 {
				// compatible = "invensense,mpu6050";
				compatible = "fire,i2c_mpu6050";
				reg = <0x68>;
				interrupt-parent = <&gpiof>;
				interrupts = <13 IRQ_TYPE_EDGE_RISING>;
			};
		};
	};
};
  • 第18 行:添加MPU6050 子节点
  • 第23-24 行:设置MPU6050 子节点属性为”fire,i2c_mpu6050”,和驱动保持一致即可。我们注释掉了”invensense,mpu6050”,此属性可以使用到内核自带的mpu6050 驱动,自带的mpu6050 驱动是使用ii0 子系统来实现的,感兴趣可自行研究。
  • 第25 行:设置reg 属性,reg 属性只需要指定MPU6050 在i2c 总线上的地址,原理图分析可知为0x68。
  • 第26-27 行:设置中断引脚信息

实验代码讲解

编程思路

i2c_mpu6050 驱动实验编程思路如下:

  • 分析硬件原理图,编写mpu6050 的设备树插件,前面已实现。
  • 编写mpu6050 驱动程序,
  • 编写简单测试应用程序。

mpu6050 驱动实现

由于ST 官方已经写好了i2c 的总线驱动,mpu6050 这个设备驱动就变得很简单,下面结合代码介绍mpu6050 设别驱动实现。

和平台设备驱动类似,mpu6050 驱动程序结构如下:

列表25: mpu6050 驱动程序结构

static int i2c_write_mpu6050(struct i2c_client *mpu6050_client, u8 address, u8 data)
{
	return 0;
}
static int i2c_read_mpu6050(struct i2c_client *mpu6050_client, u8 address, void *data, u32 length)
{
	return 0;
}
static int mpu6050_init(void)
{
	return 0;
}

/* 字符设备操作函数集,open 函数实现*/
static int mpu6050_open(struct inode *inode, struct file *filp)
{
	return 0;
}
/* 字符设备操作函数集,.read 函数实现*/
static ssize_t mpu6050_read(struct file *filp, char __user *buf, size_t cnt, loff_t *off)
{
	return 0;
}
/* 字符设备操作函数集,.release 函数实现*/
static int mpu6050_release(struct inode *inode, struct file *filp)
{
	return 0;
}
/* 字符设备操作函数集*/
static struct file_operations mpu6050_chr_dev_fops =
{
	.owner = THIS_MODULE,
	.open = mpu6050_open,
	.read = mpu6050_read,
	.release = mpu6050_release,
};

/*i2c 总线设备函数集*/
static int mpu6050_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
	return 0;
}
static int mpu6050_remove(struct i2c_client *client)
{
	/* 删除设备*/
	return 0;
}

/* 定义i2c 总线设备结构体*/
struct i2c_driver mpu6050_driver = {
	.probe = mpu6050_probe,
	.remove = mpu6050_remove,
	.id_table = gtp_device_id,
};

/*
* 驱动初始化函数
*/
static int __init mpu6050_driver_init(void)
{
	return 0;
}

/*
* 驱动注销函数
*/
static void __exit mpu6050_driver_exit(void)
{

}

module_init(mpu6050_driver_init);
module_exit(mpu6050_driver_exit);

MODULE_LICENSE("GPL");

驱动程序可分为如下四部分内容(从下往上看):

  • 第49-73 行:定义i2c 总线设备结构体并实现i2c 总线设备的注册和注销函数,在这里就是程驱动程序的入口和出口函数。
  • 第38-47 行:实现i2c 总线设备结构体中定义的操作函数,主要是.prob 匹配函数,在.prob函数中添加、注册一个字符设备,这个字符设备用于实现mpu6050 的具体功能。
  • 第14-36 行:定义并实现字符设备操作函数集。在应用程序中的open、read 操作传到内核后就是执行这些函数,所以他们要真正实现对mpu6050 的初始化以及读取转换结果。
  • 第1-12 行:具体的读、写mpu6050 的函数,它们被第三部分的函数调用,用户自行定义。

下面我们将按照这四部分内容介绍mpu6050 设备驱动程序实现。

驱动入口和出口函数实现

驱动入口和出口函数仅仅用于注册、注销i2c 设备驱动,代码如下:

列表26: mpu6050 驱动入口和出口函数实现(linux_driver/I2c_MPU6050/i2c_mpu6050.c)

/* 定义ID 匹配表*/
static const struct i2c_device_id gtp_device_id[] = {
	{"fire,i2c_mpu6050", 0},
	{}};

/* 定义设备树匹配表*/
static const struct of_device_id mpu6050_of_match_table[] = {
	{.compatible = "fire,i2c_mpu6050"},
	{/* sentinel */}};

/* 定义i2c 设备结构体*/
struct i2c_driver mpu6050_driver = {
	.probe = mpu6050_probe,
	.remove = mpu6050_remove,
	.id_table = gtp_device_id,
	.driver = {
	.name = "fire,i2c_mpu6050",
	.owner = THIS_MODULE,
	.of_match_table = mpu6050_of_match_table,
	},
};

/*
* 驱动初始化函数
*/
static int __init mpu6050_driver_init(void)
{
	int ret;
	pr_info("mpu6050_driver_init\n");
	ret = i2c_add_driver(&mpu6050_driver);
	return ret;
}

/*
* 驱动注销函数
*/
static void __exit mpu6050_driver_exit(void)
{
	pr_info("mpu6050_driver_exit\n");
	i2c_del_driver(&mpu6050_driver);
}

module_init(mpu6050_driver_init);
module_exit(mpu6050_driver_exit);

MODULE_LICENSE("GPL");
  • 第1-9 行:定义设备树匹配表。
  • 第13-14 行:.probe 和.remove,它们是i2c 设备的操作函数,.prob 函数在匹配成功后会执行, 设备注销之前.remove 函数会执行,稍后我们会实现这两个函数。
  • 第12-21 行:定义的i2c 设备驱动结构体mpu6050_driver,和我们之前学习的平台设备驱动类似,一个“结构体”代表了一个设备。结构体内主要成员介绍如下,“.id_table”和“.of_match_table”,它们用于和匹配设备树节点,具体实现如代码如第二行、第七行。
  • 第26-41 行:就是我们常说的驱动入口和出口函数。在入口函数内我们调用“i2c_add_driver”函数添加一个i2c 设备驱动。在出口函数内调用“i2c_del_driver”函数删除一个i2c 设备驱动。它们的参数都只有一个i2c 设备驱动结构体。
.prob 函数和.remove 函数实现

通常情况下.prob 用于实现一些初始化工作,.remove 用于实现退出之前的清理工作。mpu6050 需要初始化的内容很少,我们放到了字符设备的.open 函数中实现.prob 函数只需要添加、注册一个字符设备即可。程序源码如下所示:

列表27: mpu6050 驱动.prob 和.remove 函数实现(linux_driver/I2c_MPU6050/i2c_mpu6050.c)

static int mpu6050_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
	int ret = -1; //保存错误状态码
	printk(KERN_EMERG "\t match successed \n");
	//采用动态分配的方式,获取设备编号,次设备号为0
	ret = alloc_chrdev_region(&mpu6050_devno, 0, DEV_CNT, DEV_NAME);
	if (ret < 0)
	{
		printk("fail to alloc mpu6050_devno\n");
		goto alloc_err;
	}
	...
}


static int mpu6050_remove(struct i2c_client *client)
{
	/* 删除设备*/
	device_destroy(class_mpu6050, mpu6050_devno); //清除设备
	class_destroy(class_mpu6050); //清除类
	cdev_del(&mpu6050_chr_dev); //清除设备号
	unregister_chrdev_region(mpu6050_devno, DEV_CNT); //取消注册字符设备
	return 0;
 }
  • .prob 函数仅仅注册了一个字符设备,注册字符设备已经在之前的驱动程序中多次使用,这里不再赘述。
  • .remove 函数工作是注销字符设备。
实现字符设备操作函数集

在.prob 函数中添加了一个字符设备,mpu6050 的初始化以及转换结果的读取都在这个字符设备的操作函数中实现,其中最主要是.open 和.read 函数。下面是这两个函数的实现。

.open 函数实现(我们在.open 函数中配置mpu6050),具体代码如下:

列表28: open 函数实现(linux_driver/I2c_MPU6050/i2c_mpu6050.c)

/* 字符设备操作函数集,open 函数实现*/
static int mpu6050_open(struct inode *inode, struct file *filp)
{
	// printk("\n mpu6050_open \n");
	/* 向mpu6050 发送配置数据,让mpu6050 处于正常工作状态*/
	mpu6050_init();
	return 0;
}

/* 初始化i2c
* 返回值,成功,返回0。失败,返回-1
*/
static int mpu6050_init(void)
{
	int error = 0;
	/* 配置mpu6050*/
	error += i2c_write_mpu6050(mpu6050_client, PWR_MGMT_1, 0X00);
	error += i2c_write_mpu6050(mpu6050_client, SMPLRT_DIV, 0X07);
	error += i2c_write_mpu6050(mpu6050_client, CONFIG, 0X06);
	error += i2c_write_mpu6050(mpu6050_client, ACCEL_CONFIG, 0X01);

	if (error < 0)
	{
		/* 初始化错误*/
		printk(KERN_DEBUG "\n mpu6050_init error \n");
 		return -1;
	}
	return 0;
}


/* 通过i2c 向mpu6050 写入数据
*mpu6050_client:mpu6050 的i2c_client 结构体。
*address, 数据要写入的地址,
*data, 要写入的数据
* 返回值,错误,-1。成功,0
*/
static int i2c_write_mpu6050(struct i2c_client *mpu6050_client, u8 address, u8 data)
{
	int error = 0;
	u8 write_data[2];
	struct i2c_msg send_msg; //要发送的数据结构体

	/* 设置要发送的数据*/
	write_data[0] = address;
	write_data[1] = data;

	/* 发送iic 要写入的地址reg*/
	send_msg.addr = mpu6050_client->addr; //mpu6050 在iic 总线上的地址
	send_msg.flags = 0; //标记为发送数据
	send_msg.buf = write_data; //写入的首地址
	send_msg.len = 2; //reg 长度

	/* 执行发送*/
	55 error = i2c_transfer(mpu6050_client->adapter, &send_msg, 1);
	if (error != 1)
	{
		printk(KERN_DEBUG "\n i2c_transfer error \n");
		return -1;
	}
	return 0;
}
  • 第2 行:在.open 函数中仅仅调用了我们自己编写的mpu6050_init 函数。
  • 第13-29 行:调用i2c_write_mpu6050 函数向mpu6050 发送控制参数,控制参数可参考芯片手册,我们重点讲解函数i2c_write_mpu6050 实现。
  • 第33 行:参数mpu6050_client 是i2c_client 类型的结构体,填入mpu6050 设备对应的i2c_client 结构体即可。
  • 第34 行:参数address,用于设置要写入的地址这个地址是要写入mpu6050 的内部地址。
  • 第35 行:参数data, 指定要写入的数据。
  • 第42 行:定义struct i2c_msg 结构体,用来装要发送数据。
  • 第45-46 行:写入数据时要先发送写入的地址然后发送要写入的数据,这里用长度为二的数组保存地址和数据
  • 第49-52:i2c_msg 结构体填入总线上的地址,标记发送数据,首地址,以及reg 长度。
  • 第55 行:i2c_write_mpu6050 函数,该函数是对i2c_transfer 函数的封装,而i2c_transfer 是系统提供的i2c 设备驱动发送函数,根据之前讲解这个函数最终会调用i2c 总线驱动里的函数,最终由i2c 总线驱动执行收、发工作。我们这里要做的就是按照规定的格式编写要发送的数据。

mpu6050_read 函数源码如下所示。

列表29: .read 函数实现(linux_driver/I2c_MPU6050/i2c_mpu6050.c)

/* 字符设备操作函数集,.read 函数实现*/
static ssize_t mpu6050_read(struct file *filp, char __user *buf, size_t cnt, loff_t *off)
{

	char data_H;
	char data_L;
	int error;
	short mpu6050_result[6]; //保存mpu6050 转换得到的原始数据

	i2c_read_mpu6050(mpu6050_client, ACCEL_XOUT_H, &data_H, 1);
	i2c_read_mpu6050(mpu6050_client, ACCEL_XOUT_L, &data_L, 1);
	mpu6050_result[0] = data_H << 8;
	mpu6050_result[0] += data_L;

	i2c_read_mpu6050(mpu6050_client, ACCEL_YOUT_H, &data_H, 1);
	i2c_read_mpu6050(mpu6050_client, ACCEL_YOUT_L, &data_L, 1);
	mpu6050_result[1] = data_H << 8;
	mpu6050_result[1] += data_L;

	i2c_read_mpu6050(mpu6050_client, ACCEL_ZOUT_H, &data_H, 1);
	i2c_read_mpu6050(mpu6050_client, ACCEL_ZOUT_L, &data_L, 1);
	mpu6050_result[2] = data_H << 8;
	mpu6050_result[2] += data_L;

	i2c_read_mpu6050(mpu6050_client, GYRO_XOUT_H, &data_H, 1);
	i2c_read_mpu6050(mpu6050_client, GYRO_XOUT_L, &data_L, 1);
	mpu6050_result[3] = data_H << 8;
	mpu6050_result[3] += data_L;

	i2c_read_mpu6050(mpu6050_client, GYRO_YOUT_H, &data_H, 1);
	i2c_read_mpu6050(mpu6050_client, GYRO_YOUT_L, &data_L, 1);
	mpu6050_result[4] = data_H << 8;
	mpu6050_result[4] += data_L;

	i2c_read_mpu6050(mpu6050_client, GYRO_ZOUT_H, &data_H, 1);
	i2c_read_mpu6050(mpu6050_client, GYRO_ZOUT_L, &data_L, 1);
	mpu6050_result[5] = data_H << 8;
	mpu6050_result[5] += data_L;

	/* 将读取得到的数据拷贝到用户空间*/
	error = copy_to_user(buf, mpu6050_result, cnt);

	if(error != 0)
	{
		printk("copy_to_user error!");
		return -1;
	}
	return 0;
}

.read 函数很简单,大致分为如下两部分,重点是i2c_read_mpu6050 函数的实现。

  • 第10-38 行:调用i2c_read_mpu6050 函数读取mpu6050 转换结果。
  • 第41 行:调用copy_to_user 函数将转换得到的数据拷贝到用户空间。

列表30: i2c_read_mpu6050 函数实现(linux_driver/I2c_MPU6050/i2c_mpu6050.c)

static int i2c_read_mpu6050(struct i2c_client *mpu6050_client, u8 address, void *data, u32 length)
{
	int error = 0;
	u8 address_data = address;
	struct i2c_msg mpu6050_msg[2];

	/* 设置读取位置i2c_msg*/
	mpu6050_msg[0].addr = mpu6050_client->addr; //mpu6050 在iic 总线上的地址
	mpu6050_msg[0].flags = 0; //标记为发送数据
	mpu6050_msg[0].buf = &address_data; //写入的首地址
	mpu6050_msg[0].len = 1; //写入长度

	/* 读取i2c_msg*/
	mpu6050_msg[1].addr = mpu6050_client->addr; //mpu6050 在iic 总线上的地址
	mpu6050_msg[1].flags = I2C_M_RD; //标记为读取数据
	mpu6050_msg[1].buf = data; //读取得到的数据保存位置
	mpu6050_msg[1].len = length; //读取长度

	error = i2c_transfer(mpu6050_client->adapter, mpu6050_msg, 2);

	if (error != 2)
	{
	printk(KERN_DEBUG "\n i2c_read_mpu6050 error \n");
	return -1;
	}
	return 0;
}

它与我们之前讲解的i2c_write_mpu6050 函数很相似,结合源码介绍如下:

  • 第1 行:参数mpu6050_client 是i2c_client 类型的结构体,填入mpu6050 设备对应的i2c_client结构体即可。参数address,用于设置要读取的地址这个地址是要读取的mpu6050 的内部地址。参数data, 保存读取得到的数据。参数length,指定去取长度,单位字节。
  • 第3-5 行:定义的一些变量,其中i2c_msg 结构体,读取工作与写入不同,读取时需要先写入要读取的地址然后再执行读取。
  • 第8-17 行:初始化i2c_msg 结构体。这里初始化了两个,第一个是写入要读取的地址,第二个执行读取,特别注意的是第一个i2c_msg 结构体的flags 设置为0(或者I2C_M_RD |I2C_M_REV_DIR_ADDR),第二个i2c_msg 结构体的flags 设置为1(或者I2C_M_RD)。
  • 第19 行:和i2c_write_mpu6050() 函数相同,调用i2c_transfer 函数,最终由i2c 总线驱动执行收、发工作。

mpu6050 测试应用程序实现

这里编写一个简单地测试应用程序测试驱动是否正常,很简单,只需要打开、读取、打印即可。测试代码如下所示。

列表31: mpu6050 测试程序

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
	short resive_data[6];
	printf("led_tiny test\n");

	/* 打开文件*/
	fd = open("/dev/I2C1_mpu6050", O_RDWR);
	if(fd < 0)
	{
		printf("open file : %s failed !\n", argv[0]);
		return -1;
	}

	/* 读取数据*/
	int error = read(fd,resive_data,12);
	if(error < 0)
	{
		printf("write file error! \n");
		close(fd);
		/* 判断是否关闭成功*/
	}

	printf("AX=%d, AY=%d, AZ=%d ",(int)resive_data[0],(int)resive_data[1],(int)resive_data[2]);
	printf("GX=%d, GY=%d, GZ=%d \n \n",(int)resive_data[3],(int)resive_data[4],(int)resive_data[5]);

	/* 关闭文件*/
	error = close(fd);
	if(error < 0)
	{
		printf("close file error! \n");
	}

	return 0;
}
  • 第8 行:保存收到的mpu6050 转换结果数据,依次为AX(x 轴角度), AY, AZ 。GX(x 轴加速度), GY ,GZ
  • 第12-17 行:打开MPU6050 设备文件。
  • 第20-29 行:读取传感器是并打印

测试应用程序很简单,我们不过多介绍,只说明一点,在驱动的.read 函数中我们每次读取了6050的AX, AY, AZ ,GX, GY ,GZ 共六个short 类型数据,在应用程序中每次读取也要读这么多。

实验准备

编译设备树插件

将linux_driver/I2c_MPU6050/stm-fire-mpu6050-overlay.dts 拷贝到内核源码/arch/arm/boot/dts/overlays/ 目录下。

如下命令编译设备树插件:

make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- stm32mp157_ebf_defconfig

make ARCH=arm -j4 CROSS_COMPILE=arm-linux-gnueabihf- dtbs

编译成功后生成的设备树插件文件(stm-fire-mpu6050.dtbo) 位于源码目录下的内核源码/arch/arm/boot/dts/overlays/

编译驱动程序和应用程序

将linux_driver/I2c_MPU6050/ 拷贝到内核源码同级目录, 执行里面的MakeFile, 生成i2c_mpu6050.ko 和6050_test_app

在这里插入图片描述

程序运行结果

加载设备树插件

通过SCP 或NFS 将编译好的设备树插件拷贝到开发板上。替换掉原来的设备树插件文件/usr/lib/linux-image-4.19.94-stm-r1/overlays/stm-fire-mpu6050.dtbo 。

除此以外还需要将开发板上的i2c 设备打开,打开/boot/目录下的uEnv.txt 文件,修改如下内容。

在这里插入图片描述
重启开发板。

测试效果

将先前编译好的i2c_mpu6050.ko 驱动及测试app 上传至开发板中。

加载i2c_mpu6050.ko,拿起开发板,运行6050_test_app 即可看到如下效果。

在这里插入图片描述

注意:这里采集的是原始数据,所以波动较大是正常的。


参考资料:嵌入式Linux 驱动开发实战指南-基于STM32MP1 系列

单片机,又称单片微控制器,并非仅完成某一逻辑功能的芯片,而是将整个计算机系统集成到一个芯片上。其相当于一个微型计算机,与标准计算机相比,单片机仅缺少I/O设备。简而言之,一块芯片即构成了一台计算机。单片机具有体积小、质量轻、价格便宜的特点,为学习、应用和开发提供了便利条件。学习使用单片机是了解计算机原理与结构的最佳选择。 单片机的使用领域十分广泛,如智能仪表、实时工控、通讯设备、导航系统、家用电器等。一旦产品用上了单片机,就能实现产品的升级换代,使产品具有更高的智能化水平,常在产品名称前冠以“智能型”形容词,如智能型洗衣机等。此外,单片机在国防、电子玩具、厨房和家居设备等领域也有广泛的应用。 单片机技术还在不断发展,其在智能家居和智能城市、物联网设备和系统、边缘计算和边缘人工智能等领域的应用日益广泛。例如,通过单片机与传感器、执行器等设备的连接,可以实现智能家居设备的远程控制、自动化调和智能化管理;作为物联网设备的核心控制单元,单片机能够实现物联网设备之间的互联互通,为物联网系统的运行提供基础支持;在边缘计算和边缘人工智能方面,单片机可以与人工智能技术结合,实现设备端数据的实时处理和智能分析。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值