2. I2C子系统–mpu6050驱动实验

您所在的位置:网站首页 linux驱动设备树添加i2c 2. I2C子系统–mpu6050驱动实验

2. I2C子系统–mpu6050驱动实验

2024-07-06 07:29:10| 来源: 网络整理| 查看: 265

2. I2C子系统–mpu6050驱动实验¶

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

第一部分,i2c基本知识,回忆i2c物理总线和基本通信协议。

第二部分,linux下的i2c驱动框架。

第三部分,i2c总线驱动代码拆解。

第四部分,i2c设备驱动的核心函数。

第五部分,MPU6050驱动以及测试程序。

2.1. i2c基本知识¶ 2.1.1. i2c物理总线¶

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

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

SCL: 时钟线,数据收发同步

SDA: 数据线,传输具体数据

2.1.2. i2c基本通信协议¶ 2.1.2.1. 起始信号(S)与停止信号(P)¶

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

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

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

2.1.2.3. 主机与从机通信¶

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

2.1.2.4. i2c对mpu6050进行数据读写¶

单字节写入

连续字节写入

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

单字节读出

连续字节读出

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

学过单片机的用户对i2c协议并不陌生,这里只是简单的讲解,如果忘记可参考 【野火®】零死角玩转STM32 中i2c章节。

2.2. 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中存在一系列函数指针,这些函数指针指向真正硬件操作代码。

2.2.1. 关键数据结构¶

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

struct i2c_adapter

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

i2c_adapter结构体(内核源码/include/linux/i2c.h)¶ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27/* * 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的总线驱动中会实现这些(部分)函数。

i2c_algorithm结构体(内核源码/include/linux/i2c.h)¶ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21struct 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从设备

i2c_client结构体(内核源码/include/linux/i2c.h)¶ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 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设备驱动程序

i2c_driver结构体(内核源码/include/linux/i2c.h)¶ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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: 设备探测函数

2.3. 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总线定义

i2c总线定义(内核源码/drivers/i2c/i2c-core-base.c)¶ 1 2 3 4 5 6 7 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。

i2c总线注册(内核源码/drivers/i2c/i2c-core-base.c)¶ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22static 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驱动匹配规则

i2c设备和i2c驱动匹配规则(内核源码/drivers/i2c/i2c-core-base.c)¶ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21static 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驱动源码。 通常情况下,看驱动程序首先要找到驱动的入口和出口函数,驱动入口和出口位于驱动的末尾,如下所示。

驱动入口和出口函数(内核源码/drivers/i2c/busses/i2c-st.c)¶ 1 2 3 4 5 6 7 8 9 10 11static 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”,平台驱动结构体如下所示。

平台设备驱动结构体(内核源码/drivers/i2c/busses/i2c-st.c)¶ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16static 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函数的内容。

i2c驱动 .probe函数(内核源码/drivers/i2c/busses/i2c-st.c)¶ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76static 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_dev->irq, 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行:获取时钟配置并配置。

剩余的都是pm功耗管理,adapter的配置了,比较简单, 最终使用i2c_add_adapter将平台驱动注册到bus中。

下面我们来看看st_i2c_dev结构体,它是切实用于产商芯片和linux平台关联的桥梁。

st_i2c_dev结构体¶ 1 2 3 4 5 6 7 8 9 10 11 12 13struct st_i2c_dev { struct i2c_adapter adap; 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”定义如下。

i2c_algorithm结构体实例st_i2c_algo¶ 1 2 3 4static 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函数定义如下:

st_i2c_xfer函数¶ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29static 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 dev); clk_disable_unprepare(i2c_dev->clk); i2c_dev->busy = false; return (ret 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结构体,并将其添加到系统中。

2.4. i2c设备驱动核心函数¶

i2c_add_adapter()

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

注册一个i2c适配器 (内核源码/drivers/i2c/i2c-core-base.c)¶ 1 2 3 4//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()宏

注册一个i2c驱动(内核源码/include/linux/i2c.h)¶ 1#define i2c_add_driver(driver)

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

i2c_register_driver()函数

注册一个i2c驱动(内核源码/drivers/i2c/i2c-core-base.c)¶ 1int i2c_register_driver(struct module *owner, struct i2c_driver *driver)

参数:

owner: 一般为 THIS_MODULE

driver: 要注册的 i2c_driver.

返回值:

成功: 0

失败: 负数

i2c_transfer()函数

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

收发i2c消息(内核源码/drivers/i2c/i2c-core-base.c)¶ 1int 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结构体

描述一个iic消息(内核源码/include/uapi/linux/i2c.h)¶ 1 2 3 4 5 6 7struct 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()函数

发送一个i2c消息(内核源码/include/linux/i2c.h)¶ 1 2 3 4 5static 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()函数

接收一个i2c消息(内核源码/include/linux/i2c.h)¶ 1 2 3 4 5static 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()函数

发送一个i2c消息(内核源码/drivers/i2c/i2c-core-base.c)¶ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19int 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设备驱动。

2.5. mpu6050驱动实验¶ 2.5.1. 硬件介绍¶

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

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

2.5.1.1. 硬件原理图分析¶

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

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

2.5.1.2. 设备树¶

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

i2c设备树插件¶ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50// SPDX-License-Identifier: (GPL-2.0+ OR BSD-3-Clause) /* * Copyright (C) STMicroelectronics 2018 - All Rights Reserved * Author: Alexandre Torgue . */ /dts-v1/; /plugin/; //#include "../stm32mp157c.dtsi" #include #include #include #include /{ fragment@0{ target=; __overlay__{ pinctrl-names = "default", "sleep"; pinctrl-0 = ; pinctrl-1 = ; i2c-scl-rising-time-ns = ; i2c-scl-falling-time-ns = ; status = "okay"; /delete-property/dmas; /delete-property/dma-names; }; }; fragment@1{ target=; __overlay__{ i2c1_pins_a: i2c1-0 { pins { pinmux = , /* I2C1_SCL */ ; /* I2C1_SDA */ bias-disable; drive-open-drain; slew-rate = ; }; }; i2c1_pins_sleep_a: i2c1-1 { pins { pinmux = , /* I2C1_SCL */ ; /* 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的状态使用的引脚功能。

mpu6050设备树插件¶ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31// SPDX-License-Identifier: (GPL-2.0+ OR BSD-3-Clause) /* * Copyright (C) STMicroelectronics 2018 - All Rights Reserved * Author: Alexandre Torgue . */ /dts-v1/; /plugin/; //#include "../stm32mp157c.dtsi" #include #include #include #include #include /{ fragment@0{ target=; __overlay__ { #address-cells = ; #size-cells = ; mpu6050@68 { // compatible = "invensense,mpu6050"; compatible = "fire,i2c_mpu6050"; reg = ; interrupt-parent = ; interrupts = ; }; }; }; };

第18行: 添加MPU6050子节点

第23-24行: 设置MPU6050子节点属性为”fire,i2c_mpu6050”,和驱动保持一致即可。我们注释掉了”invensense,mpu6050”, 此属性可以使用到内核自带的mpu6050驱动,自带的mpu6050驱动是使用ii0子系统来实现的,感兴趣可自行研究。

第25行: 设置reg属性,reg属性只需要指定MPU6050在i2c总线上的地址,原理图分析可知为0x68。

第26-27行: 设置中断引脚信息

2.5.2. 实验代码讲解¶ 2.5.2.1. 编程思路¶

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

分析硬件原理图,编写mpu6050的设备树插件,前面已实现。

编写mpu6050驱动程序,

编写简单测试应用程序。

2.5.2.2. mpu6050驱动实现¶

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

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

mpu6050驱动程序结构¶ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75static 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设备驱动程序实现。

2.5.2.2.1. 驱动入口和出口函数实现¶

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

mpu6050驱动入口和出口函数实现(linux_driver/I2c_MPU6050/i2c_mpu6050.c)¶ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46/*定义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设备驱动结构体。

2.5.2.2.2. .prob函数和.remove函数实现¶

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

mpu6050驱动.prob和.remove函数实现(linux_driver/I2c_MPU6050/i2c_mpu6050.c)¶ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24static 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 addr; //mpu6050在 iic 总线上的地址 send_msg.flags = 0; //标记为发送数据 send_msg.buf = write_data; //写入的首地址 send_msg.len = 2; //reg长度 /*执行发送*/ 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函数源码如下所示。

 .read函数实现 (linux_driver/I2c_MPU6050/i2c_mpu6050.c)¶ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49/*字符设备操作函数集,.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


【本文地址】

公司简介

联系我们

今日新闻


点击排行

实验室常用的仪器、试剂和
说到实验室常用到的东西,主要就分为仪器、试剂和耗
不用再找了,全球10大实验
01、赛默飞世尔科技(热电)Thermo Fisher Scientif
三代水柜的量产巅峰T-72坦
作者:寞寒最近,西边闹腾挺大,本来小寞以为忙完这
通风柜跟实验室通风系统有
说到通风柜跟实验室通风,不少人都纠结二者到底是不
集消毒杀菌、烘干收纳为一
厨房是家里细菌较多的地方,潮湿的环境、没有完全密
实验室设备之全钢实验台如
全钢实验台是实验室家具中较为重要的家具之一,很多

推荐新闻


图片新闻

实验室药品柜的特性有哪些
实验室药品柜是实验室家具的重要组成部分之一,主要
小学科学实验中有哪些教学
计算机 计算器 一般 打孔器 打气筒 仪器车 显微镜
实验室各种仪器原理动图讲
1.紫外分光光谱UV分析原理:吸收紫外光能量,引起分
高中化学常见仪器及实验装
1、可加热仪器:2、计量仪器:(1)仪器A的名称:量
微生物操作主要设备和器具
今天盘点一下微生物操作主要设备和器具,别嫌我啰嗦
浅谈通风柜使用基本常识
 众所周知,通风柜功能中最主要的就是排气功能。在

专题文章

    CopyRight 2018-2019 实验室设备网 版权所有 win10的实时保护怎么永久关闭