7. Linux内核I2C子系统¶
在嵌入式Linux驱动开发中,I2C(Inter-Integrated Circuit,集成电路总线)是一种常用的串行通信总线,具有引脚少、通信速率适中、占用资源少、支持多主从设备等特点, 广泛应用于连接各类外设,如传感器、EEPROM、RTC实时时钟等。
Linux内核为了简化I2C设备驱动的开发,设计并实现了I2C子系统,它是一套标准化的驱动框架,将I2C总线的管理、设备的枚举、数据的传输等功能进行抽象和封装,为驱动开发者提供统一的接口。 I2C子系统屏蔽了底层硬件的差异(如不同芯片的I2C控制器),使得开发者无需关注总线的底层实现细节,只需按照子系统规范编写设备驱动,即可实现I2C设备的正常通信。
7.1. I2C基本知识¶
7.1.1. I2C总线简介¶
I2C总线由飞利浦(Philips)公司于1982年推出,最初用于连接主板上的低速外设,目前已成为嵌入式系统中最常用的串行通信总线之一。 I2C总线仅需两根信号线即可实现双向通信:SDA(Serial Data,串行数据线)和SCL(Serial Clock,串行时钟线), 两根信号线均为开漏输出,需通过上拉电阻连接到电源,实现线与逻辑。
如上图所示,I2C支持一主多从,各设备地址独立,标准模式传输速率为100kbit/s,快速模式为400kbit/s。 总线通过上拉电阻接到电源,当I2C设备空闲时,会输出高阻态,而当所有设备都空闲,都输出高阻态时,由上拉电阻把总线拉成高电平。
I2C物理总线使用两条总线线路,SCL和SDA。
SCL: 时钟线,数据收发同步
SDA: 数据线,传输具体数据
7.1.2. I2C基本通信协议¶
7.1.2.1. 起始信号(S)与停止信号(P)¶
当SCL线为高电平时,SDA线由高到低的下降沿,为传输开始标志(S)。 直到主设备发出结束信号(P),否则总线状态一直为忙。 结束标志(P)为,当SCL线为高电平时,SDA线由低到高的上升沿。
7.1.2.2. 数据格式与应答信号(ACK/NACK)¶
I2C的数据字节定义为8-bits长度,对每次传送的总字节数量没有限制,但对每一次传输必须伴有一个应答(ACK)信号, 其时钟由主设备提供,而真正的应答信号由从设备发出,在时钟为高时,通过拉低并保持SDA的值来实现。 如果从设备忙,它可以使SCL保持在低电平,这会强制使主设备进入等待状态。 当从设备空闲后,并且释放时钟线,原来的数据传输才会继续。
7.1.2.3. 主机与从机通信¶
开始标志(S)发出后,主设备会传送一个7位的Slave地址,并且后面跟着一个第8位,称为Read/Write位, R/W位表示主设备是在接受从设备的数据还是在向其写数据。 然后,主设备释放SDA线,等待从设备的应答信号(ACK)。 每个字节的传输都要跟随有一个应答位,应答产生时,从设备将SDA线拉低并且在SCL为高电平时保持低, 数据传输总是以停止标志(P)结束,然后释放通信线路。 然而,主设备也可以产生重复的开始信号去操作另一台从设备,而不发出结束标志。
综上可知,所有的SDA信号变化都要在SCL时钟为低电平时进行,除了开始和结束标志。
7.2. I2C子系统¶
在编写单片机裸机I2C驱动时我们需要根据I2C协议手动配置I2C控制寄存器使其能够输出起始信号、停止信号、数据信息等等。 在Linux系统中则采用了总线、设备驱动模型,我们之前讲解的平台设备也是采用了这种模型,只不过平台总线是一个虚拟的总线
7.2.1. 核心定义¶
Linux内核I2C子系统是一套用于管理I2C总线和I2C设备的标准化框架,其核心目标是“统一接口、简化开发、屏蔽差异”。 它将I2C相关的驱动分为两层:I2C总线驱动和I2C设备驱动,两层通过I2C核心层进行交互,实现总线管理、设备枚举和数据传输的标准化。
I2C子系统的核心逻辑:核心层提供统一的接口,总线驱动负责管理I2C控制器(适配器),实现底层总线的读写操作; 设备驱动负责管理具体的I2C从设备,通过核心层调用总线驱动的接口,实现与从设备的通信。
7.2.2. I2C核心层¶
I2C核心层是I2C子系统的枢纽,由内核源码中的drivers/i2c/i2c-core-base.c、drivers/i2c/i2c-core-of.c等文件实现, 提供了总线驱动和设备驱动的注册、注销接口,以及数据传输、设备枚举等核心功能。核心层的主要职责包括:
管理所有已注册的I2C适配器(总线驱动)和I2C设备(设备驱动)。
提供标准化的数据传输接口(如i2c_transfer),供设备驱动调用。
实现I2C设备的枚举,根据设备树配置或板级信息,匹配总线驱动和设备驱动。
协调总线驱动与设备驱动的交互,屏蔽底层硬件差异。
7.2.3. I2C总线驱动¶
I2C总线驱动也称为I2C适配器驱动,对应物理层面的I2C控制器(如SOC内置的I2C控制器), 由drivers/i2c/busses/目录下的文件实现(如rockchip平台的i2c-rk3x.c)。其核心职责包括:
初始化I2C控制器,配置通信速率、引脚等参数。
实现I2C总线的底层读写操作,响应核心层的数据传输请求。
向核心层注册I2C适配器,供设备驱动调用。
7.2.4. I2C设备驱动¶
I2C设备驱动对应具体的I2C从设备(如EEPROM、RTC、通用传感器),由开发者根据具体设备编写。其核心职责包括:
向核心层注册I2C设备,告知核心层设备的从地址、兼容属性等信息。
实现设备的初始化(如寄存器配置)、数据读取、数据写入等逻辑。
通过核心层提供的接口,调用总线驱动的底层功能,完成与从设备的通信。
7.3. I2C子系统工作流程¶
I2C子系统的完整工作流程可概括为以下步骤,基于通用场景说明:
系统启动时,I2C总线驱动初始化I2C控制器(如I2C1、I2C2),并向核心层注册I2C适配器。
设备树加载时,核心层解析设备树中I2C设备节点(如i2c_mpu6050@68),获取设备的从地址(0x68)、兼容属性(xxx,i2c_mpu6050)等信息。
I2C设备驱动加载时,向核心层注册设备驱动,核心层根据兼容属性,将设备驱动与对应的I2C适配器和I2C设备匹配。
匹配成功后,调用设备驱动的probe函数,完成设备初始化(如寄存器配置)、字符设备注册等操作。
应用层通过字符设备接口(如/dev/i2c_mpu6050)发起数据读写请求,设备驱动通过核心层的i2c_transfer等接口,调用总线驱动的底层功能,与I2C从设备进行通信,完成数据的读取或写入。
驱动卸载时,调用设备驱动的remove函数,释放资源,总线驱动保持运行,可继续为其他I2C设备提供服务。
7.4. I2C子系统核心结构体¶
I2C子系统的核心结构体均由内核定义,开发者只需声明和使用,无需手动定义。
7.4.1. I2C适配器结构体¶
I2C适配器结构体(struct i2c_adapter)用于描述I2C适配器(即I2C控制器)的基本信息和能力, 是总线驱动的核心结构体。
1 2 3 4 5 6 7 8 9 10 11 12 13 | struct i2c_adapter {
struct module *owner; // 驱动模块所有者
unsigned int class; // 适配器支持的设备类型
const struct i2c_algorithm *algo; // 适配器的操作函数集(核心,指向i2c_algorithm结构体)
void *algo_data; // 算法私有数据,供algo中的函数使用
struct rt_mutex bus_lock; // I2C总线锁,防止多进程同时操作总线
int timeout; // 通信超时时间
int retries; // 通信失败后的重试次数
struct device dev; // 继承通用设备结构体,用于设备模型管理
char name[48]; // 适配器名称
int nr; // 适配器编号
// 省略其他辅助成员
};
|
该结构体是I2C适配器的抽象,总线驱动的核心工作就是初始化该结构体,将其注册到核心层,核心层通过该结构体管理I2C控制器的功能和状态。 其中algo成员是核心,指向该适配器支持的操作函数集。
7.4.2. I2C算法结构体¶
I2C算法结构体(struct i2c_algorithm)用于描述I2C适配器的操作函数集,定义了适配器能执行的底层操作,是总线驱动的核心实现。
1 2 3 4 5 6 7 8 9 10 | struct i2c_algorithm {
// 主模式数据传输函数,实现I2C协议的底层读写,返回传输的消息数,失败返回负错误码
int (*master_xfer)(struct i2c_adapter *adap, struct i2c_msg *msgs,
int num);
// 获取适配器的功能,返回适配器支持的功能掩码,如I2C_FUNC_I2C表示支持I2C模式
u32 (*functionality) (struct i2c_adapter *);
// 省略其他不常用成员
};
|
master_xfer函数是总线驱动的核心,芯片厂商需在总线驱动中实现该函数,完成I2C协议的起始、停止、数据收发等底层操作; functionality函数用于告知核心层该适配器支持的功能,如是否支持10位地址、高速模式等。
7.4.3. I2C从设备结构体¶
I2C从设备结构体(struct i2c_client)用于描述I2C从设备的基本信息,每个I2C从设备对应一个该结构体,是设备驱动与核心层交互的核心载体。
1 2 3 4 5 6 7 8 9 10 11 | struct i2c_client {
unsigned short flags; // 设备标志,如I2C_CLIENT_TEN表示使用10位从地址
unsigned short addr; // 从设备地址,7位或10位,低7位有效,高位用于标志位
char name[I2C_NAME_SIZE]; // 从设备名称
struct i2c_adapter *adapter; // 该设备挂载的I2C适配器指针
struct device dev; // 继承通用设备结构体,用于设备模型管理
int init_irq; // 作为从设备时的发送函数
int irq; // 表示该设备生成的中断号
struct list_head detected; // 用于将设备加入适配器的clients链表
// 省略其他辅助成员
};
|
该结构体由核心层根据设备树配置或板级信息自动创建,设备驱动通过该结构体获取从设备的地址、挂载的适配器等信息,实现与从设备的通信。 开发者无需手动创建该结构体,只需在驱动中通过probe函数获取该结构体指针。
7.4.4. I2C设备驱动结构体¶
I2C设备驱动结构体(struct i2c_driver)用于描述I2C设备驱动的基本信息,是设备驱动的核心结构体,用于向核心层注册设备驱动。
1 2 3 4 5 6 7 8 9 10 | struct i2c_driver {
unsigned int class; // 驱动支持的设备类型,与适配器的class对应
int (*probe)(struct i2c_client *, const struct i2c_device_id *); // 设备匹配成功后执行
int (*remove)(struct i2c_client *); // 设备卸载或驱动卸载时执行
void (*shutdown)(struct i2c_client *); // 系统关机时执行
const struct i2c_device_id *id_table; // 设备ID匹配表,用于非设备树场景
const struct of_device_id *of_match_table; // 设备树兼容属性匹配表,用于设备树场景
struct device_driver driver; // 继承通用设备驱动结构体,用于驱动模型管理
// 省略其他辅助成员
};
|
该结构体是设备驱动的抽象,开发者需配置该结构体的probe、remove函数,以及of_match_table(设备树场景)或id_table(非设备树场景), 用于与I2C设备匹配,然后将其注册到核心层。
7.4.5. I2C消息结构体¶
I2C消息结构体(struct i2c_msg)用于描述一次I2C通信的消息(如一次读操作或一次写操作),是数据传输的核心载体,被i2c_transfer等函数使用。
1 2 3 4 5 6 | struct i2c_msg {
__u16 addr; // 从设备地址,7位或10位
__u16 flags; // 消息标志,如I2C_M_RD表示读操作,I2C_M_TEN表示10位地址
__u16 len; // 消息数据长度
__u8 *buf; // 消息数据缓冲区,存储要发送或接收的数据
};
|
一次I2C通信可包含多个消息(如先写寄存器地址,再读寄存器数据),多个消息通过struct i2c_msg数组传递给i2c_transfer函数,由总线驱动的master_xfer函数依次执行。
7.5. I2C总线驱动源码分析¶
I2C总线驱动由芯片厂商提供,如果我们使用rk官方提供的Linux内核,I2C总线驱动已经保存在内核中, 并且默认情况下已经编译进内核。
下面结合源码简单介绍I2C总线的运行机制。
7.5.1. I2C总线定义¶
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驱动的匹配和删除等。
7.5.2. I2C总线注册¶
linux启动之后,默认执行i2c_init。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | 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。
7.5.3. I2C设备和I2C驱动匹配规则¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | 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;
}
|
第7行:设备树匹配方式,比较I2C设备节点的compatible属性和of_device_id中的compatible属性;
第11行:ACPI匹配方式;
第17行:I2C总线传统匹配方式,比较I2C设备名字和I2C驱动的id_table->name字段是否相等。
在I2C总线驱动代码源文件中,我们只简单介绍重要的几个点,如果感兴趣可自行阅读完整的I2C驱动源码。 通常情况下,看驱动程序首先要找到驱动的入口和出口函数,驱动入口和出口位于驱动的末尾,如下所示:
1 2 3 4 5 6 7 8 9 10 | static struct platform_driver rk3x_i2c_driver = {
.probe = rk3x_i2c_probe,
.remove = rk3x_i2c_remove,
.driver = {
.name = "rk3x-i2c",
.of_match_table = rk3x_i2c_match,
.pm = &rk3x_i2c_pm_ops,
},
};
module_platform_driver(rk3x_i2c_driver);
|
我们可以从中得到I2C驱动是一个平台驱动,并且我们知道平台驱动结构体是“rk3x_i2c_driver”,平台驱动结构体如下所示。
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 | static const struct of_device_id rk3x_i2c_match[] = {
{
.compatible = "rockchip,rv1108-i2c",
.data = &rv1108_soc_data
},
{
.compatible = "rockchip,rv1126-i2c",
.data = &rv1126_soc_data
},
{
.compatible = "rockchip,rk3066-i2c",
.data = &rk3066_soc_data
},
{
.compatible = "rockchip,rk3188-i2c",
.data = &rk3188_soc_data
},
{
.compatible = "rockchip,rk3228-i2c",
.data = &rk3228_soc_data
},
{
.compatible = "rockchip,rk3288-i2c",
.data = &rk3288_soc_data
},
{
.compatible = "rockchip,rk3399-i2c",
.data = &rk3399_soc_data
},
{},
};
MODULE_DEVICE_TABLE(of, rk3x_i2c_match);
static struct platform_driver rk3x_i2c_driver = {
.probe = rk3x_i2c_probe,
.remove = rk3x_i2c_remove,
.driver = {
.name = "rk3x-i2c",
.of_match_table = rk3x_i2c_match,
.pm = &rk3x_i2c_pm_ops,
},
};
|
第1-32行:是I2C驱动的匹配表,用于和设备树节点匹配,
第34-42行:是初始化的平台设备结构体,从这个结构体我们可以找到.probe函数,.probe函数的作用我们都很清楚,通常情况下该函数实现设备的基本初始化。
以下是.porbe函数的内容:
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 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 | static int rk3x_i2c_probe(struct platform_device *pdev)
{
struct device_node *np = pdev->dev.of_node;
const struct of_device_id *match;
// RK3X I2C控制器私有数据结构体
struct rk3x_i2c *i2c;
struct resource *mem;
int ret = 0;
u32 value;
int irq;
unsigned long clk_rate;
/* devm_托管内存分配,分配RK3X I2C私有数据结构体 */
i2c = devm_kzalloc(&pdev->dev, sizeof(struct rk3x_i2c), GFP_KERNEL);
if (!i2c)
return -ENOMEM;
/* 设备树匹配,根据驱动匹配表,找到当前设备对应的匹配项 */
match = of_match_node(rk3x_i2c_match, np);
/* 保存芯片专属配置数据,不同RK芯片I2C寄存器/时序不同 */
i2c->soc_data = match->data;
/* 使用通用接口来获取I2C时序属性 */
i2c_parse_fw_timings(&pdev->dev, &i2c->t, true);
/* ===================== 初始化I2C适配器结构体 ===================== */
// 设置I2C适配器名称
strlcpy(i2c->adap.name, "rk3x-i2c", sizeof(i2c->adap.name));
// 驱动所属模块
i2c->adap.owner = THIS_MODULE;
// 绑定I2C总线算法(RK3X专属I2C读写实现)
i2c->adap.algo = &rk3x_i2c_algorithm;
// I2C通信重试次数
i2c->adap.retries = 3;
// 关联设备树节点
i2c->adap.dev.of_node = np;
// 算法私有数据
i2c->adap.algo_data = i2c;
// 父设备为平台设备
i2c->adap.dev.parent = &pdev->dev;
// 关联设备指针
i2c->dev = &pdev->dev;
/* 初始化自旋锁:保护I2C控制器寄存器并发访问 */
spin_lock_init(&i2c->lock);
/* 初始化等待队列:用于I2C传输完成的阻塞等待 */
init_waitqueue_head(&i2c->wait);
/* ===================== 注册系统重启通知器 ===================== */
// 设置重启通知回调函数
i2c->i2c_restart_nb.notifier_call = rk3x_i2c_restart_notify;
// 设置通知器优先级
i2c->i2c_restart_nb.priority = 128;
// 注册预重启通知器,系统重启前执行I2C清理
ret = register_pre_restart_handler(&i2c->i2c_restart_nb);
if (ret) {
dev_err(&pdev->dev, "failed to setup i2c restart handler.\n");
return ret;
}
/* ===================== I2C寄存器地址映射 ===================== */
// 从平台设备中获取I2C控制器的物理内存资源
mem = platform_get_resource(pdev, IORESOURCE_MEM, 0);
// 内存重映射:物理地址转虚拟地址,devm托管释放
i2c->regs = devm_ioremap_resource(&pdev->dev, mem);
if (IS_ERR(i2c->regs))
return PTR_ERR(i2c->regs);
// 省略部分内容
/* ===================== 中断配置 ===================== */
// 从平台设备获取I2C控制器中断号
irq = platform_get_irq(pdev, 0);
if (irq < 0) {
dev_err(&pdev->dev, "cannot find rk3x IRQ\n");
return irq;
}
/* 申请中断 */
ret = devm_request_irq(&pdev->dev, irq, rk3x_i2c_irq,
0, dev_name(&pdev->dev), i2c);
if (ret < 0) {
dev_err(&pdev->dev, "cannot request IRQ\n");
return ret;
}
// 将I2C私有数据保存到平台设备,方便其他接口调用
platform_set_drvdata(pdev, i2c);
// 省略部分内容
ret = i2c_add_adapter(&i2c->adap);
if (ret < 0)
goto err_clk_notifier;
return 0;
}
|
第14行:为rk3x_i2c结构体申请空间,后面会详述这个结构体。
第64行和第66行:获取reg属性,这里使用的是内核提供的“platform_get_resource”它实现的功能和我们使用of函数获取reg属性相同。这里的代码获取得到了i2c的基地址,并且使用“devm_ioremap_resource”将其转化为虚拟地址。
第81-82行:获取中断号,在i2c的设备树节点中定义了中断,这里获取得到的中断号申请中断时会用到,获取函数使用的是内核提供的函数irq_of_parse_and_map。
最终使用i2c_add_adapter函数将平台驱动注册到bus中。
下面我们来看看rk3x_i2c结构体,它是切实用于产商芯片和linux平台关联的桥梁。
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 | struct rk3x_i2c {
struct i2c_adapter adap;
struct device *dev;
const struct rk3x_i2c_soc_data *soc_data;
/* Hardware resources */
void __iomem *regs;
struct clk *clk;
struct clk *pclk;
struct notifier_block clk_rate_nb;
/* Settings */
struct i2c_timings t;
/* Synchronization & notification */
spinlock_t lock;
wait_queue_head_t wait;
bool busy;
/* Current message */
struct i2c_msg *msg;
u8 addr;
unsigned int mode;
bool is_last_msg;
/* I2C state machine */
enum rk3x_i2c_state state;
unsigned int processed;
int error;
unsigned int suspended:1;
struct notifier_block i2c_restart_nb;
bool system_restarting;
};
|
rk3x_i2c结构体成员较多,描述了厂商的I2C控制器信息以及即将注册到总线中的adapter适配器, 通过这个结构体,可以关联linux下的I2C总线模型和产商芯片驱动功能。
adap: 即将注册到总线中的adapter适配器;
irq: 保存I2C的中断号;
clk: clk结构体保存时钟相关信息;
busy: 事件等待的条件。
在前面的probe函数中,初始化rk3x_i2c结构体中的adap成员。
我们重点看probe函数的“i2c->adap.algo = &rk3x_i2c_algorithm”, 它就是用于初始化“访问I2C总线的传输算法”。“st_i2c_algo”定义如下。
1 2 3 4 | static const struct i2c_algorithm rk3x_i2c_algorithm = {
.master_xfer = rk3x_i2c_xfer,
.functionality = rk3x_i2c_func,
};
|
st_i2c_algo结构体内指定了两个函数,它们就是外部访问i2c总线的接口:
函数rk3x_i2c_func只是用于返回I2C总线提供的功能。
函数rk3x_i2c_xfer真正实现访问i2c外设,进行数据传输。
rk3x_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 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 | static int rk3x_i2c_xfer(struct i2c_adapter *adap,
struct i2c_msg *msgs, int num)
{
struct rk3x_i2c *i2c = (struct rk3x_i2c *)adap->algo_data;
unsigned long timeout, flags;
u32 val;
int ret = 0;
int i;
if (i2c->suspended)
return -EACCES;
spin_lock_irqsave(&i2c->lock, flags);
clk_enable(i2c->clk);
clk_enable(i2c->pclk);
i2c->is_last_msg = false;
/*
* Process msgs. We can handle more than one message at once (see
* rk3x_i2c_setup()).
*/
for (i = 0; i < num; i += ret) {
ret = rk3x_i2c_setup(i2c, msgs + i, num - i);
if (ret < 0) {
dev_err(i2c->dev, "rk3x_i2c_setup() failed\n");
break;
}
if (i + ret >= num)
i2c->is_last_msg = true;
rk3x_i2c_start(i2c);
spin_unlock_irqrestore(&i2c->lock, flags);
timeout = wait_event_timeout(i2c->wait, !i2c->busy,
msecs_to_jiffies(WAIT_TIMEOUT));
spin_lock_irqsave(&i2c->lock, flags);
if (timeout == 0) {
dev_err(i2c->dev, "timeout, ipd: 0x%02x, state: %d\n",
i2c_readl(i2c, REG_IPD), i2c->state);
/* Force a STOP condition without interrupt */
rk3x_i2c_disable_irq(i2c);
val = i2c_readl(i2c, REG_CON) & REG_CON_TUNING_MASK;
val |= REG_CON_EN | REG_CON_STOP;
i2c_writel(i2c, val, REG_CON);
i2c->state = STATE_IDLE;
ret = -ETIMEDOUT;
break;
}
if (i2c->error) {
ret = i2c->error;
break;
}
}
rk3x_i2c_disable_irq(i2c);
rk3x_i2c_disable(i2c);
clk_disable(i2c->pclk);
clk_disable(i2c->clk);
spin_unlock_irqrestore(&i2c->lock, flags);
return ret < 0 ? ret : num;
}
|
在编写设备驱动时,如mpu6050的驱动,我们会使用“i2c_transfer”函数执行数据的传输,i2c_transfer函数最终就是调用rk3x_i2c_xfer函数实现具体的收发工作。 届时我们会详细介绍i2c_transfer函数的用法。
在rk3x_i2c_xfer中,实际的收发又是通过rk3x_i2c_setup来完成,函数实现如下:
static int rk3x_i2c_setup(struct rk3x_i2c *i2c, struct i2c_msg *msgs, int num)
{
u32 addr = (msgs[0].addr & 0x7f) << 1;
int ret = 0;
/*
* The I2C adapter can issue a small (len < 4) write packet before
* reading. This speeds up SMBus-style register reads.
* The MRXADDR/MRXRADDR hold the slave address and the slave register
* address in this case.
*/
if (num >= 2 && msgs[0].len < 4 &&
!(msgs[0].flags & I2C_M_RD) && (msgs[1].flags & I2C_M_RD)) {
u32 reg_addr = 0;
int i;
dev_dbg(i2c->dev, "Combined write/read from addr 0x%x\n",
addr >> 1);
/* Fill MRXRADDR with the register address(es) */
for (i = 0; i < msgs[0].len; ++i) {
reg_addr |= msgs[0].buf[i] << (i * 8);
reg_addr |= REG_MRXADDR_VALID(i);
}
/* msgs[0] is handled by hw. */
i2c->msg = &msgs[1];
i2c->mode = REG_CON_MOD_REGISTER_TX;
i2c_writel(i2c, addr | REG_MRXADDR_VALID(0), REG_MRXADDR);
i2c_writel(i2c, reg_addr, REG_MRXRADDR);
ret = 2;
} else {
/*
* We'll have to do it the boring way and process the msgs
* one-by-one.
*/
if (msgs[0].flags & I2C_M_RD) {
addr |= 1; /* set read bit */
/*
* We have to transmit the slave addr first. Use
* MOD_REGISTER_TX for that purpose.
*/
i2c->mode = REG_CON_MOD_REGISTER_TX;
i2c_writel(i2c, addr | REG_MRXADDR_VALID(0),
REG_MRXADDR);
i2c_writel(i2c, 0, REG_MRXRADDR);
} else {
i2c->mode = REG_CON_MOD_TX;
}
i2c->msg = &msgs[0];
ret = 1;
}
i2c->addr = msgs[0].addr;
i2c->busy = true;
i2c->processed = 0;
i2c->error = 0;
rk3x_i2c_clean_ipd(i2c);
return ret;
}
这里就不带大家展开了,操作内容都是比较针对底层外设寄存器的,简单解释参考代码中的注释。
至此我们的I2C平台驱动就给大家分析完了,probe函数完成了I2C的基本初始化并将其添加到了系统中, 驱动中也实现I2C对外接口函数。 我们在初始化i2c_adapter结构体时已经初始化了访问总线算法结构体i2c_algorithm,在前面也介绍过了。
那么总结整个probe函数,主要完成了两个工作:
第一,初始化I2C硬件;
第二,初始化一个可以访问I2C外设的i2c_adapter结构体,并将其添加到系统中。
7.6. I2C子系统核心函数¶
Linux内核为I2C子系统提供了一系列标准化函数,涵盖适配器注册、设备驱动注册、数据传输等核心操作, 以下按功能分类,结合源码讲解。
7.6.1. 总线驱动核心函数¶
7.6.1.1. I2C适配器注册函数(i2c_add_adapter)¶
i2c_add_adapter函数用于将初始化完成的I2C适配器(struct i2c_adapter)注册到I2C核心层,核心层会对适配器进行管理,供设备驱动调用。
函数原型:
1 | int i2c_add_adapter(struct i2c_adapter *adap);
|
参数说明:
adap:指向已初始化完成的struct i2c_adapter结构体指针,包含适配器的名称、算法、超时时间等参数。
返回值:成功返回0;失败返回负整数的错误码。
7.6.1.2. 带编号I2C适配器注册函数(i2c_add_numbered_adapter)¶
与i2c_add_adapter功能一致,区别在于该函数可指定适配器编号(adap->nr),避免编号自动分配导致的冲突。
函数原型:
1 | int i2c_add_numbered_adapter(struct i2c_adapter *adap);
|
参数说明:
adap:指向已初始化完成的struct i2c_adapter结构体指针,需提前设置adap->nr适配器编号。
返回值:成功返回0;失败返回负整数的错误码。
7.6.1.3. I2C适配器注销函数(i2c_del_adapter)¶
i2c_del_adapter函数用于将已注册的I2C适配器从核心层注销,释放适配器占用的资源,终止适配器的工作。
函数原型:
1 | void i2c_del_adapter(struct i2c_adapter *adap);
|
参数说明:
adap:指向已注册的struct i2c_adapter结构体指针,需与注册时的指针一致。
7.6.2. 设备驱动核心函数¶
7.6.2.1. I2C设备驱动注册函数(i2c_add_driver)¶
i2c_add_driver函数用于将初始化完成的I2C设备驱动(struct i2c_driver)注册到I2C核心层, 核心层会根据驱动的匹配表,自动匹配对应的I2C从设备。
函数原型:
1 | int i2c_add_driver(struct i2c_driver *driver);
|
参数说明:
driver:指向已初始化完成的struct i2c_driver结构体指针,包含驱动名称、匹配表、probe/remove函数等。
返回值:成功返回0;失败返回负整数的错误码。
7.6.2.2. I2C设备驱动注销函数(i2c_del_driver)¶
i2c_del_driver函数用于将已注册的I2C设备驱动从核心层注销,释放驱动占用的资源,终止驱动的工作, 同时会调用该驱动所有匹配设备的remove函数。
函数原型:
1 | void i2c_del_driver(struct i2c_driver *driver);
|
参数说明:
driver:指向已注册的struct i2c_driver结构体指针,需与注册时的指针一致。
7.6.3. 数据传输核心函数¶
7.6.3.1. 通用I2C数据传输函数(i2c_transfer)¶
i2c_transfer函数是I2C子系统最核心的数据传输函数,用于实现一次或多次I2C消息的传输, 是设备驱动中最常用的数据传输接口,底层调用总线驱动的master_xfer函数。
函数原型:
1 | int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num);
|
参数说明:
adap:指向I2C适配器结构体指针,即设备挂载的适配器(client->adapter);
msgs:指向struct i2c_msg结构体数组,存储要传输的所有消息,如先写后读;
num:msgs数组的长度,即要传输的消息数量。
返回值:成功返回传输的消息数量;失败返回负整数错误码。
7.6.3.2. I2C写数据函数(i2c_master_send)¶
i2c_master_send函数是简化的I2C写操作函数,用于向I2C从设备发送数据(单次写消息),底层调用i2c_transfer函数。
函数原型:
1 | int i2c_master_send(const struct i2c_client *client, const char *buf, int count);
|
参数说明:
client:指向struct i2c_client结构体指针,即要写入数据的I2C从设备;
buf:指向要发送的数据缓冲区;
count:要发送的数据长度。
返回值:成功返回发送的字节数;失败返回负整数错误码。
7.6.3.3. I2C读数据函数(i2c_master_recv)¶
i2c_master_recv函数是简化的I2C读操作函数,用于从I2C从设备读取数据(单次读消息),底层调用i2c_transfer函数。
函数原型:
1 | int i2c_master_recv(const struct i2c_client *client, char *buf, int count);
|
参数说明:
client:指向struct i2c_client结构体指针,即要读取数据的I2C从设备;
buf:指向存储读取数据的缓冲区;
count:要读取的数据长度)。
返回值:成功返回读取的字节数;失败返回负整数错误码。
7.7. I2C子系统实验¶
本实验基于Linux I2C子系统,实现主机与MPU6050传感器的通信,完成传感器初始化、I2C读写操作, 最终通过应用层读取MPU6050的加速度计、陀螺仪原始数据,理解I2C子系统的核心架构、驱动注册流程、I2C通信协议及设备树配置规范。
本章的示例代码目录为: linux_driver/24_i2c_subsystem
7.7.1. MPU6050传感器简介¶
MPU6050是一款集成加速度计、陀螺仪和温度传感器的I2C从设备,核心特性如下:
I2C从设备地址:默认地址为0x68;
加速度计量程:可配置,原始数据为16位;
陀螺仪量程:可配置,原始数据为16位;
数据读取方式:通过I2C总线读取指定寄存器,连续读取可获取所有传感器数据。
模块参考链接: 野火小智【姿态传感器_MPU6050】模块
重要
MPU6050模块的寄存器说明在以上模块配套资料中有十分详细的说明,本教程不作详细说明,仅从资料中截取关键说明。
野火小智MPU6050六轴姿态模块规格手册:
截取手册关键说明并结合本实验情况整理得:
需配置的寄存器:
配置电源管理1寄存器(0x6B)为0x00,唤醒MPU6050,使用内部8Mhz时钟源;
配置陀螺仪采样率分频寄存器(0x19)为0x07,对应7分频,陀螺仪采样率=陀螺仪输出频率/(1+SMPLRT_DIV)=8KHZ/(1+7)=1KHz;
配置配置寄存器(0x19)为0x06,截止频率是1K,带宽是5K;
配置加速度计配置寄存器(0x1C)为0x00,让加速度传感器工作在±2g模式,不自检;
配置陀螺仪配置寄存器(0x1B)为0x18,选择陀螺仪的满量程为2000deg/s,不自检。
可用于检验的寄存器:
我是谁(ID 号)寄存器(0x75)默认值为0x68,是MPU6050的I2C从设备地址,可用于检验。
需要读取数据的寄存器:
加速度计测量寄存器(0x3B~0x40)存储加速度计X、Y、Z轴的高8位和低8位数据;
温度测量寄存器(0x41~0x42)存储温度高8位和低8位数据;
陀螺仪测量寄存器(0x43~0x48)存储陀螺仪X、Y、Z轴的高8位和低8位数据。
计算公式:
加速度值(m/s²)= ACCEL_OUT / ACCEL_SCALE * GRAVITY
陀螺仪值(deg/s)= GYRO_OUT / GYRO_SCALE
温度(℃)= TEMP_OUT / TEMP_SCALE + TEMP_OFFSET
7.7.2. 设备树插件详解¶
本实验设备树插件(lubancat-i2c-mpu6050-overlay.dts)用于配置I2C适配器和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 | /dts-v1/;
/plugin/;
#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/pinctrl/rockchip.h>
/ {
fragment@0 {
target = <&i2c3>;
__overlay__ {
clock-frequency = <100000>;
pinctrl-names = "default";
pinctrl-0 = <&i2c3m1_xfer>;
#address-cells = <1>;
#size-cells = <0>;
status = "okay";
i2c_mpu6050@68 {
compatible = "fire,i2c_mpu6050";
reg = <0x68>;
status = "okay";
};
};
};
};
|
关键配置说明:
target = &i2c3:指定要修改的I2C适配器为I2C3,需与实际要使用的硬件接口一致;
clock-frequency = <100000>:配置I2C通信速率为100KHz,MPU6050支持标准模式(100KHz)和快速模式(400KHz);
pinctrl-0 = <&i2c3m1_xfer>:此处使用的具体I2C引脚为i2c3m1;
i2c_mpu6050@68:从设备节点名称,@后的0x68是MPU6050的7位I2C地址,必须与传感器实际地址一致(默认0x68);
compatible = “fire,i2c_mpu6050”:驱动匹配的核心标识,需与I2C驱动中of_match_table的属性完全一致,否则驱动无法匹配设备;
reg = <0x68>:明确MPU6050的I2C从设备地址,内核通过该属性识别I2C总线上的具体设备。
注意
以上设备树插件是以I2C3_M1为例,需根据板卡实际接口进行修改!
如果不清楚自己板卡有哪些可用的I2C接口,可在板卡执行以下命令确认:
1 2 3 4 5 6 7 8 9 10 | #查看配置文件可用i2c插件
cat /boot/uEnv/uEnv.txt | grep i2c
#以LubanCat2-V2板卡为例,信息打印如下
#dtoverlay=/dtb/overlay/rk356x-lubancat-i2c3-m0-overlay.dtbo
#dtoverlay=/dtb/overlay/rk356x-lubancat-i2c3-m1-overlay.dtbo
#dtoverlay=/dtb/overlay/rk356x-lubancat-i2c5-m0-overlay.dtbo
#dtoverlay=/dtb/overlay/rk356x-lubancat-i2c3-m0-rtc-overlay.dtbo
#dtoverlay=/dtb/overlay/rk356x-lubancat-i2c3-m1-rtc-overlay.dtbo
#dtoverlay=/dtb/overlay/rk356x-lubancat-i2c5-m0-rtc-overlay.dtbo
|
从可用的设备树插件可以确认,LubanCat2-V2支持i2c3-m0、i2c3-m1、i2c5-m0,从内核源码中找到对应的设备树插件源码如下,以i2c3-m1为例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | #查看i2c3-m1设备树插件源码内容
cat kernel/arch/arm64/boot/dts/rockchip/overlay/rk356x-lubancat-i2c3-m1-overlay.dts
#信息打印如下
/dts-v1/;
/plugin/;
/ {
compatible = "rockchip,rk3568";
fragment@0 {
target = <&i2c3>;
__overlay__ {
status = "okay";
pinctrl-names = "default";
pinctrl-0 = <&i2c3m1_xfer>;
};
};
};
|
可知,如果LubanCat2-V2使用i2c3-m1接口,lubancat-i2c-mpu6050-overlay.dts配置的target和pinctrl-0就是 target = <&i2c3>; 和 pinctrl-0 = <&i2c3m1_xfer>; 。
结合板卡配套的快速使用手册的40pin引脚对照图章节,可确认i2c3-m1接口对应的物理引脚,如下图:
7.7.3. 驱动代码详解¶
本实验驱动代码是I2C子系统从设备驱动,整合了I2C读写、MPU6050初始化、字符设备封装, 核心是通过I2C子系统接口实现与传感器的通信,无需自定义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 | /* MPU6050 寄存器地址定义 */
#define SMPLRT_DIV 0x19 /* 采样率分频寄存器 */
#define CONFIG 0x1A /* 通用配置寄存器(低通滤波/外部同步) */
#define GYRO_CONFIG 0x1B /* 陀螺仪配置寄存器(量程设置) */
#define ACCEL_CONFIG 0x1C /* 加速度计配置寄存器(量程设置) */
#define ACCEL_XOUT_H 0x3B /* 加速度计X轴数据高8位 */
#define ACCEL_XOUT_L 0x3C /* 加速度计X轴数据低8位 */
#define ACCEL_YOUT_H 0x3D /* 加速度计Y轴数据高8位 */
#define ACCEL_YOUT_L 0x3E /* 加速度计Y轴数据低8位 */
#define ACCEL_ZOUT_H 0x3F /* 加速度计Z轴数据高8位 */
#define ACCEL_ZOUT_L 0x40 /* 加速度计Z轴数据低8位 */
#define TEMP_OUT_H 0x41 /* 温度传感器数据高8位 */
#define TEMP_OUT_L 0x42 /* 温度传感器数据低8位 */
#define GYRO_XOUT_H 0x43 /* 陀螺仪X轴数据高8位 */
#define GYRO_XOUT_L 0x44 /* 陀螺仪X轴数据低8位 */
#define GYRO_YOUT_H 0x45 /* 陀螺仪Y轴数据高8位 */
#define GYRO_YOUT_L 0x46 /* 陀螺仪Y轴数据低8位 */
#define GYRO_ZOUT_H 0x47 /* 陀螺仪Z轴数据高8位 */
#define GYRO_ZOUT_L 0x48 /* 陀螺仪Z轴数据低8位 */
#define PWR_MGMT_1 0x6B /* 电源管理寄存器1(唤醒/时钟源) */
#define WHO_AM_I 0x75 /* 器件ID寄存器(默认值0x68) */
#define SlaveAddress 0xD0 /* MPU6050 I2C 写地址(8位) */
#define Address 0x68 /* MPU6050 I2C 7位从设备地址 */
/* 定义字符设备的名称 */
#define DEV_NAME "i2c_mpu6050"
/* 定义字符设备的数量 */
#define DEV_CNT 1
/* 定义 mpu6050 设备结构体 */
struct mpu6050_device {
/* 定义设备号变量 */
dev_t devno;
/* 定义设备类指针 */
struct class *class;
/* 定义设备指针 */
struct device *device;
/* 字符设备结构体 */
struct cdev dev;
/* I2C 客户端指针 */
struct i2c_client *client;
};
/* 单设备驱动,静态定义的实体变量 */
static struct mpu6050_device mpu6050_dev;
|
MPU6050寄存器定义:涵盖电源管理、采样率、量程配置及数据寄存器,是与传感器通信的核心依据;
struct i2c_client *client:I2C从设备的核心结构体,由I2C子系统分配,包含从设备地址、I2C适配器指针等关键信息,驱动通过该结构体与I2C适配器通信;
静态定义mpu6050_dev:本实验为单设备驱动,无需动态分配内存,简化驱动逻辑。
I2C驱动注册与注销
I2C驱动通过i2c_add_driver注册到I2C子系统,当设备树中的从设备与驱动匹配时,执行probe函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | static int __init mpu6050_driver_init(void)
{
int ret;
printk("mpu6050 driver init\n");
/* 向Linux I2C子系统注册MPU6050驱动 */
ret = i2c_add_driver(&mpu6050_driver);
return ret;
}
static void __exit mpu6050_driver_exit(void)
{
printk("mpu6050 driver exit\n");
/* 从Linux I2C核心子系统注销MPU6050驱动,释放驱动资源 */
i2c_del_driver(&mpu6050_driver);
}
module_init(mpu6050_driver_init);
module_exit(mpu6050_driver_exit);
|
驱动初始化
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 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 | static int mpu6050_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
/* 定义返回值变量 */
int ret = 0;
/* 定义主设备号变量 */
int major;
/* 定义次设备号变量 */
int minor;
printk("mpu6050 driver probe\n");
/* 分配设备号 */
ret = alloc_chrdev_region(&mpu6050_dev.devno, 0, DEV_CNT, DEV_NAME);
if (ret < 0) {
/* 打印设备号分配失败信息 */
printk("fail to alloc devno\n");
/* 跳转到错误处理标签 */
goto alloc_err;
}
/* 获取主设备号 */
major = MAJOR(mpu6050_dev.devno);
/* 获取次设备号 */
minor = MINOR(mpu6050_dev.devno);
/* 打印主设备号和次设备号 */
printk("major=%d, minor=%d\n", major, minor);
/* 初始化字符设备 */
cdev_init(&mpu6050_dev.dev, &mpu6050_chr_dev_fops);
mpu6050_dev.dev.owner = THIS_MODULE;
/* 添加字符设备 */
ret = cdev_add(&mpu6050_dev.dev, mpu6050_dev.devno, DEV_CNT);
if (ret < 0) {
/* 打印字符设备添加失败信息 */
printk("fail to add cdev\n");
/* 跳转到错误处理标签 */
goto add_err;
}
/* 创建设备类 */
mpu6050_dev.class = class_create(THIS_MODULE, DEV_NAME);
if (IS_ERR(mpu6050_dev.class)) {
/* 打印设备类创建失败信息 */
printk("fail to create class\n");
ret = PTR_ERR(mpu6050_dev.class);
/* 跳转到错误处理标签 */
goto class_err;
}
/* 创建设备节点 */
mpu6050_dev.device = device_create(mpu6050_dev.class, NULL, mpu6050_dev.devno, NULL, DEV_NAME);
if (IS_ERR(mpu6050_dev.device)) {
/* 打印设备节点创建失败信息 */
printk("fail to create device\n");
ret = PTR_ERR(mpu6050_dev.device);
/* 跳转到错误处理标签 */
goto device_err;
}
/* 保存 I2C 设备信息,赋值到封装结构体的 client 成员 */
mpu6050_dev.client = client;
/* 向 mpu6050 发送配置数据,让mpu6050处于正常工作状态 */
ret = mpu6050_init(mpu6050_dev.client);
if (ret < 0) {
pr_err("mpu6050 hardware init failed\n");
goto init_err;
}
return 0;
init_err:
/* 销毁设备节点 */
device_destroy(mpu6050_dev.class, mpu6050_dev.devno);
device_err:
/* 销毁设备类 */
class_destroy(mpu6050_dev.class);
class_err:
/* 删除字符设备 */
cdev_del(&mpu6050_dev.dev);
add_err:
/* 释放设备号 */
unregister_chrdev_region(mpu6050_dev.devno, DEV_CNT);
alloc_err:
return ret;
}
|
第61行:client是I2C子系统自动传给probe函数的参数,包含I2C设备地址、I2C总线适配器、设备通信必需的所有内核信息,将其传递给我们自己定义的结构体成员,保存这把通信钥匙。
第64行:调用mpu6050_init函数初始化MPU6050模块。
I2C读写核心函数
驱动通过i2c_transfer函数实现I2C总线的读写操作,封装为i2c_write_mpu6050(写寄存器)和i2c_read_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 | static int i2c_write_mpu6050(struct i2c_client *mpu6050_client, u8 address, u8 data)
{
int ret = 0;
/* 定义要发送的数据数组 */
u8 write_data[2];
/* 要发送的数据结构体 */
struct i2c_msg mpu6050_write_msg;
/* 设置要发送的数据 */
write_data[0] = address;
write_data[1] = data;
/* 发送 iic 要写入的地址 reg */
mpu6050_write_msg.addr = mpu6050_client->addr; //mpu6050在iic总线上的地址
mpu6050_write_msg.flags = 0; //标记为发送数据
mpu6050_write_msg.buf = write_data; //数据的首地址
mpu6050_write_msg.len = 2; //reg长度
/* 执行发送 */
ret = i2c_transfer(mpu6050_client->adapter, &mpu6050_write_msg, 1);
if (ret != 1)
{
printk(KERN_ERR "i2c write mpu6050 error\n");
return -1;
}
return 0;
}
static int i2c_read_mpu6050(struct i2c_client *mpu6050_client, u8 address, void *data, u32 length)
{
int ret = 0;
/* 待读取的寄存器地址 */
u8 address_data = address;
/* I2C消息结构体数组 */
struct i2c_msg mpu6050_read_msg[2];
/*设置读取位置msg */
mpu6050_read_msg[0].addr = mpu6050_client->addr; //mpu6050在 iic 总线上的地址
mpu6050_read_msg[0].flags = 0; //标记为发送数据
mpu6050_read_msg[0].buf = &address_data; //写入的首地址
mpu6050_read_msg[0].len = 1; //写入长度
/* 设置读取位置msg */
mpu6050_read_msg[1].addr = mpu6050_client->addr; //mpu6050在 iic 总线上的地址
mpu6050_read_msg[1].flags = I2C_M_RD; //标记为读取数据
mpu6050_read_msg[1].buf = data; //读取得到的数据保存位置
mpu6050_read_msg[1].len = length; //读取长度
/* 执行发送 */
ret = i2c_transfer(mpu6050_client->adapter, mpu6050_read_msg, 2);
if (ret != 2)
{
printk(KERN_ERR "i2c read mpu6050 error\n");
return -1;
}
/* 成功返回实际读取的字节数 */
return length;
}
|
关键说明:
i2c_transfer:I2C子系统提供的核心通信接口,用于执行一个或多个I2C消息传输,返回成功传输的消息数量;
struct i2c_msg:I2C消息结构体,描述一次I2C传输的细节(从设备地址、操作类型、数据缓冲区、长度);
MPU6050通信协议:读寄存器时,需先向传感器写入“要读取的寄存器地址”,再执行读操作,因此需要两个I2C消息;写寄存器时,只需一个消息(寄存器地址+数据)。
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 | static int mpu6050_init(struct i2c_client *client)
{
int ret = 0;
u8 who_am_i = 0;
/* 先读取器件ID寄存器进行检验 */
ret = i2c_read_mpu6050(client, WHO_AM_I, &who_am_i, 1);
if (ret < 0 || who_am_i != 0x68) {
dev_err(&client->dev, "WHO_AM_I check failed! Read: 0x%x\n", who_am_i);
return -ENODEV;
}
/* 配置电源管理寄存器:唤醒MPU6050,使用内部8Mhz时钟源 */
ret = i2c_write_mpu6050(client, PWR_MGMT_1, 0x00);
if (ret < 0) goto init_fail;
/* 配置采样率分频寄存器:陀螺仪采样率,1KHz */
ret = i2c_write_mpu6050(client, SMPLRT_DIV, 0x07);
if (ret < 0) goto init_fail;
/* 配置通用配置寄存器:低通滤波器的设置,截止频率是1K,带宽是5K */
ret = i2c_write_mpu6050(client, CONFIG, 0x06);
if (ret < 0) goto init_fail;
/* 配置加速度计配置寄存器:配置加速度传感器工作在2G模式,不自检 */
ret = i2c_write_mpu6050(client, ACCEL_CONFIG, 0x00);
if (ret < 0) goto init_fail;
/* 配置陀螺仪配置寄存器:陀螺仪自检及测量范围,典型值:0x18(不自检,2000deg/s) */
ret = i2c_write_mpu6050(client, GYRO_CONFIG, 0x18);
if (ret < 0) goto init_fail;
return 0;
init_fail:
/* 初始化错误 */
printk(KERN_ERR "mpu6050 init error \n");
return -1;
}
|
第7行:先读取器件ID寄存器进行检验,读取到的值为0x68再进行后续的初始化操作;
第14行:默认MPU6050是睡眠模式,需配置电源管理寄存器唤醒传感器,使用内部时钟;
第26行:加速度计量程配置,0x00对应2G量程,转换系数为16384 LSB/g(1g=9.8m/s²);
第30行:陀螺仪量程配置,0x18对应2000deg/s量程,转换系数为16.4 LSB/(deg/s)。
字符设备封装
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 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 76 77 78 79 80 | /* 字符设备操作函数集 */
static struct file_operations mpu6050_chr_dev_fops =
{
.owner = THIS_MODULE,
.open = mpu6050_open,
.read = mpu6050_read,
.release = mpu6050_release,
};
static int mpu6050_open(struct inode *inode, struct file *filp)
{
/* 检查设备是否完成初始化 */
if (mpu6050_dev.client == NULL) {
printk(KERN_ERR "mpu6050 not initialized \n");
return -ENODEV;
}
/* 打印设备打开信息 */
printk("mpu6050 open\n");
return 0;
}
static ssize_t mpu6050_read(struct file *filp, char __user *buf, size_t cnt, loff_t *off)
{
int ret;
/* 连续读取14字节 = 加速度6字节 + 温度2字节 + 陀螺仪6字节 */
u8 read_buf[14] = {0};
/* 保存数据,3轴加速度 + 温度 + 3轴陀螺仪,共7个short型数据 */
short mpu6050_result[7] = {0};
/*
* 0x3B~0x48寄存器
* 对应ACCEL_XOUT_H~GYRO_ZOUT_L
* 依次为ACCEL_X、Y、Z、温度、GYRO_X、Y、Z的高8位和低8位共14个寄存器
*/
/* 从 ACCEL_XOUT_H(0x3B) 开始连续读取14字节 (读0x3B~0x48寄存器),一次I2C传输全部读取完 */
ret = i2c_read_mpu6050(mpu6050_dev.client, ACCEL_XOUT_H, read_buf, sizeof(read_buf));
if (ret < 0) {
return -EIO;
}
/*
* 解析数据:提取加速度+温度+陀螺仪
* 0~5 :加速度 X/Y/Z
* 6~7 :温度
* 8~13 :陀螺仪 X/Y/Z
* MPU6050数据为高字节在前,低字节在后,拼接为16位short
*/
mpu6050_result[0] = (read_buf[0] << 8) | read_buf[1]; // ACCEL_X 加速度X
mpu6050_result[1] = (read_buf[2] << 8) | read_buf[3]; // ACCEL_Y 加速度Y
mpu6050_result[2] = (read_buf[4] << 8) | read_buf[5]; // ACCEL_Z 加速度Z
mpu6050_result[3] = (read_buf[6] << 8) | read_buf[7]; // TEMP 温度原始值
mpu6050_result[4] = (read_buf[8] << 8) | read_buf[9]; // GYRO_X 陀螺仪X
mpu6050_result[5] = (read_buf[10] << 8) | read_buf[11]; // GYRO_Y 陀螺仪Y
mpu6050_result[6] = (read_buf[12] << 8) | read_buf[13]; // GYRO_Z 陀螺仪Z
/* 限制拷贝长度,防止用户传入的cnt过小导致越界 */
cnt = min(cnt, sizeof(mpu6050_result));
/* 将读取得到的数据拷贝到用户空间 */
ret = copy_to_user(buf, mpu6050_result, cnt);
/* ret > 0 说明数据没有完整拷贝到用户空间 */
if (ret > 0) return -EFAULT;
return cnt;
}
static int mpu6050_release(struct inode *inode, struct file *filp)
{
printk("mpu6050 release\r\n");
return 0;
}
|
read函数逻辑:通过I2C连续读取14字节数据(加速度3轴+温度+陀螺仪3轴),解析为16位原始数据,拷贝到用户空间,供应用层转换为物理值。
7.7.4. 应用代码详解¶
应用层通过字符设备节点(/dev/i2c_mpu6050)读取MPU6050的原始数据,结合传感器量程配置,将原始数据转换为直观的物理值(加速度:m/s²,陀螺仪:deg/s),完整代码如下:
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 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 | #include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <stdlib.h>
#include <signal.h>
/*
* 计算公式:
* 加速度值(m/s²)= ACCEL_OUT / ACCEL_SCALE * GRAVITY
* 陀螺仪值(deg/s)= GYRO_OUT / GYRO_SCALE
* 温度(℃)= TEMP_OUT / TEMP_SCALE + TEMP_OFFSET
*/
/* 定义单次读取的字节数:7个short类型 = 14字节(加速度6 + 温度2 + 陀螺仪6) */
#define READ_BYTES 14
/* 量程转换系数,与驱动中配置一致 */
#define ACCEL_SCALE 16384.0 // 加速度计±2G量程:16384 LSB/g
#define GYRO_SCALE 16.4 // 陀螺仪±2000deg/s量程:16.4 LSB/(deg/s)
#define TEMP_SCALE 340.0 // 温度传感器灵敏度:340 LSB/°C
#define TEMP_OFFSET 36.53 // 温度传感器零点偏移:36.53°C
#define GRAVITY 9.8 // 重力加速度(m/s²)
/* 全局标志,用于Ctrl+C退出循环读取 */
static int exit_flag = 0;
/* 信号处理函数:捕获Ctrl+C,设置退出标志 */
static void sigint_handler(int sig)
{
if (sig == SIGINT)
{
exit_flag = 1;
printf("\n收到退出信号,即将停止读取...\n");
}
}
int main(int argc, char *argv[])
{
/* 保存收到的mpu6050转换结果数据,依次为:
* AX(加速度X), AY, AZ, TEMP(温度), GX(陀螺仪X), GY, GZ
*/
short receive_data[7] = {0};
/* 物理值变量 */
float ax_ms2, ay_ms2, az_ms2; // 加速度 (m/s²)
float temp_c; // 温度 (°C)
float gx_dps, gy_dps, gz_dps; // 陀螺仪 (deg/s)
int fd = -1;
int ret = -1;
// 注册Ctrl+C信号处理函数
signal(SIGINT, sigint_handler);
// 检查参数,需传入设备节点路径
if (argc != 2) {
printf("Usage: ./i2c_mpu6050_app /dev/i2c_mpu6050\n");
return -1;
}
/* 打开文件 */
fd = open(argv[1], O_RDWR);
if(fd < 0)
{
printf("打开设备文件 %s 失败 !\n", argv[1]);
return -1;
}
printf("设备文件 %s 打开成功,开始持续读取数据(Ctrl+C退出)...\n", argv[1]);
/* 打印物理单位表头 */
printf("AX(m/s²) AY(m/s²) AZ(m/s²) TEMP(°C) GX(°/s) GY(°/s) GZ(°/s)\n");
printf("----------------------------------------------------------------------------------------\n");
/* 循环读取数据,直到捕获Ctrl+C */
while (!exit_flag)
{
/* 复位接收缓冲区,避免脏数据 */
memset(receive_data, 0, sizeof(receive_data));
/* 读取数据,严格读取14字节 */
ret = read(fd, receive_data, READ_BYTES);
if (ret < 0)
{
printf("读取设备文件数据失败 !\n");
close(fd);
return -1;
}
else if (ret != READ_BYTES)
{
/* 处理短读/中断异常,不退出,继续重试 */
printf("读取字节数不匹配,预期%d字节,实际%d字节,重试...\n", READ_BYTES, ret);
usleep(100000);
continue;
}
/* ============ 原始数据转物理值 ============ */
/* 加速度转换:加速度值(m/s²)= ACCEL_OUT / ACCEL_SCALE * GRAVITY */
ax_ms2 = (float)receive_data[0] / ACCEL_SCALE * GRAVITY;
ay_ms2 = (float)receive_data[1] / ACCEL_SCALE * GRAVITY;
az_ms2 = (float)receive_data[2] / ACCEL_SCALE * GRAVITY;
/* 温度转换:温度(℃)= TEMP_OUT / TEMP_SCALE + TEMP_OFFSET */
temp_c = (float)receive_data[3] / TEMP_SCALE + TEMP_OFFSET;
/* 陀螺仪转换:陀螺仪值(deg/s)= GYRO_OUT / GYRO_SCALE */
gx_dps = (float)receive_data[4] / GYRO_SCALE;
gy_dps = (float)receive_data[5] / GYRO_SCALE;
gz_dps = (float)receive_data[6] / GYRO_SCALE;
/* 打印物理值,格式对齐,保留2位小数 */
printf("%-10.2f %-10.2f %-10.2f %-10.2f %-10.2f %-10.2f %-10.2f\n",
ax_ms2, ay_ms2, az_ms2,
temp_c,
gx_dps, gy_dps, gz_dps);
/* 间隔1s时间再读取 */
sleep(1);
}
/* 关闭文件 */
ret = close(fd);
if(ret < 0)
{
printf("关闭设备文件 %s 失败 !\n", argv[1]);
return -1;
}
return 0;
}
|
应用层说明:
设备节点:应用层通过驱动创建的字符设备节点(/dev/i2c_mpu6050)访问MPU6050,无需直接操作I2C总线。
数据转换逻辑:驱动读取的是16位原始数据,需结合量程转换系数转换为物理值,转换公式如下:
加速度值(m/s²)= ACCEL_OUT / ACCEL_SCALE * GRAVITY
陀螺仪值(deg/s)= GYRO_OUT / GYRO_SCALE
温度(℃)= TEMP_OUT / TEMP_SCALE + TEMP_OFFSET
7.7.5. Makefile说明¶
本节实验使用的Makefile如下所示,编写该Makefile时,只需要根据实际情况修改变量KERNEL_DIR、obj-m和test_app即可。
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 | #指定内核路径,可以是相对路径或绝对路径
KERNEL_DIR=../../kernel/
#KERNEL_DIR=/home/guest/LubanCat_Linux_rk356x_SDK/kernel/
#指定目标架构为arm64
ARCH=arm64
#指定交叉编译工具链的前缀
CROSS_COMPILE=aarch64-linux-gnu-
#导出为环境变量
export ARCH CROSS_COMPILE
#指定要编译的内核模块目标文件
obj-m := i2c_mpu6050.o
test_app = i2c_mpu6050_app
#all :默认目标,执行时会编译驱动模块
#$(MAKE) :调用make工具
#-C $(KERNEL_DIR) :指定的内核源码目录
#M=$(CURDIR) :模块的源码位于当前目录
#modules :编译模块
all:
$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules
$(CROSS_COMPILE)gcc -o $(test_app) $(test_app).c
.PHONE:clean
#清理编译生成的文件
clean:
$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) clean
rm $(test_app)
|
7.7.6. 编译设备树和驱动¶
7.7.6.1. 编译设备树¶
修改内核目录/arch/arm64/boot/dts/rockchip/overlays下的Makefile文件,添加我们编辑好的设备树插件,并把设备树插件文件放在和Makefile文件同级目录下,以进行设备树插件的编译。
然后在内核源码顶层目录执行以下命令编译设备树插件:
1 2 3 | #这里以rk356x系列4.19.232内核配置文件为例
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- lubancat2_defconfig
make ARCH=arm64 -j4 CROSS_COMPILE=aarch64-linux-gnu- dtbs
|
提示
其余系列板卡参考 使用内核的构建脚本编译设备树插件 章节进行编译。
7.7.6.2. 加载设备树¶
编译出来的设备树插件位于 内核源码/arch/arm64/boot/dts/rockchip/overlay/lubancat-i2c-mpu6050-overlay.dtbo,
将设备树插件先传到板卡,再拷贝到板卡的 /boot/dtb/overlay/ 目录下。
1 2 3 4 | #先传输到板卡
#再拷贝到板卡的/boot/dtb/overlay/目录下
sudo cp -f lubancat-i2c-mpu6050-overlay.dtbo /boot/dtb/overlay/
|
然后在 /boot/uEnv/uEnv.txt 按照格式添加我们的设备树插件,需要在#overlay_start和#overlay_end之间添加,然后重启开发板,那么系统就会加载我们编译的设备树插件。
重启板卡可以在uboot启动信息中看到设备树插件加载。
7.7.6.3. 编译驱动¶
在实验目录下输入 make 即可编译驱动和应用程序,编译得到内核模块i2c_mpu6050.ko和应用程序i2c_mpu6050_app。
7.7.7. 模块接线说明¶
通过杜邦线连接MPU6050模块和板卡,接线如下:
1 2 3 4 5 6 7 | MPU6050模块 板卡
VCC -------- 3.3V
GND -------- GND
SCL -------- SCL
SDA -------- SDA
// 其余引脚不接
|
7.7.8. 程序运行结果¶
如出现 Permission denied 或类似字样,请注意用户权限,大部分操作硬件外设的功能,几乎都需要root用户权限,简单的解决方案是在执行语句前加入sudo或以root用户运行程序。
7.7.8.1. 实验操作¶
使用以下命令加载驱动和运行测试程序,加载驱动前需确保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 | #先确认设备地址存在,使用i2cdetect命令,其中3表示是i2c3,需要根据实际使用的i2c修改
sudo i2cdetect -a -y 3
#信息打印如下,可以看到68地址
0 1 2 3 4 5 6 7 8 9 a b c d e f
00: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- 68 -- -- -- -- -- -- --
70: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
#加载驱动
sudo insmod i2c_mpu6050.ko
#信息输出如下
[ 824.684434] mpu6050 driver init
[ 824.684722] mpu6050 driver probe
[ 824.684823] major=236, minor=0
#运行测试程序,然后晃动MPU6050模块
sudo ./i2c_mpu6050_app /dev/i2c_mpu6050
#信息打印如下
[ 1876.360222] mpu6050 open
设备文件 /dev/i2c_mpu6050 打开成功,开始持续读取数据(Ctrl+C退出)...
AX(m/s²) AY(m/s²) AZ(m/s²) TEMP(°C) GX(°/s) GY(°/s) GZ(°/s)
----------------------------------------------------------------------------------------
-9.32 -1.46 0.12 28.12 -2.99 0.67 -0.61
-9.31 -1.46 0.12 28.12 -2.87 1.04 -0.67
-9.32 -1.47 0.10 28.13 -2.99 0.67 -0.61
-9.30 -1.46 0.11 28.15 -3.05 0.67 -0.61
-9.32 -1.46 0.10 28.14 -2.99 0.73 -0.61
-9.31 -1.45 0.11 28.15 -2.99 0.67 -0.61
-8.65 -2.43 -0.44 28.14 -10.18 17.13 5.37 //开始晃动模块
-9.34 -0.72 -2.98 28.15 -44.27 -65.24 -0.49
-8.52 -1.27 1.39 28.17 11.65 23.66 5.61
-9.26 -1.82 -0.04 28.18 -6.77 -6.59 -3.35
^C
收到退出信号,即将停止读取...
[ 1886.075376] mpu6050 release
|
7.7.8.2. 数据解析¶
选取晃动前后的各1组数据进行解析:
1 2 3 4 5 | AX(m/s²) AY(m/s²) AZ(m/s²) TEMP(°C) GX(°/s) GY(°/s) GZ(°/s)
----------------------------------------------------------------------------------------
-9.32 -1.46 0.12 28.12 -2.99 0.67 -0.61 // 静止数据
...
-9.34 -0.72 -2.98 28.15 -44.27 -65.24 -0.49 // 晃动数据
|
静止数据解析
加速度解析
驱动配置:±2g量程,转换公式:物理值 = 原始值 / 16384 * 9.8
三轴加速度是重力的分解值,因为模块倾斜放置;
合加速度计算:√((-9.32)^2+(-1.46)^2+(0.12)^2) ≈ 9.44 m/s²
接近标准重力加速度 9.8m/s²,存在误差是传感器安装角度 + 硬件精度导致,因此数据是正常的。
陀螺仪解析
驱动配置:±2000°/s 量程,转换公式:物理值 = 原始值 / 16.4
静止时角速度理想值应为0 °/s,但实际不会绝对为0,因为陀螺仪有零漂特性;
陀螺仪3个轴角速度数值稳定在±6 °/s以内,并无剧烈跳动,因此数据是正常的。
温度解析
温度保持在28℃左右,室温恒定,数值无波动,说明数据正常。
晃动数据解析
加速度解析
晃动时,模块不再仅受重力,三轴数值发生突变,AZ 从 0.12->-2.98;
合加速度计算:√((-9.34)^2+(-0.72)^2+(-2.98)^2) ≈ 9.83 m/s²,
合加速度发生变化,响应运动状态,数据正常。
陀螺仪解析
晃动时传感器发生旋转,角速度瞬间大幅跳变:
GX:-2.99 -> -44.27
GY:0.67 -> -65.24
数值随晃动实时变化,响应旋转动作,数据正常。
温度解析
温度无明显突变说明数据正常。
7.7.9. 实验注意事项¶
I2C地址正确性:MPU6050的7位I2C地址默认是0x68,设备树中reg属性需设置为0x68,若传感器AD0引脚接VCC,地址会变为0x69,需同步修改设备树和驱动;
硬件连接正确性:MPU6050的SDA、SCL引脚需正确连接到开发板的I2C对应的SDA、SCL引脚,同时需接入VCC(3.3V)和GND,避免接反电源损坏传感器;
数据解析顺序:MPU6050的数据为高字节在前、低字节在后,驱动中解析时需按“高字节<<8 | 低字节”的顺序拼接,否则会出现数据错误;
量程转换系数匹配:应用层的转换系数(ACCEL_SCALE、GYRO_SCALE)需与驱动中MPU6050的量程配置一致,否则转换后的物理值会偏差过大;
I2C通信故障排查:若i2cdetect无法识别MPU6050,需检查硬件连接、设备树配置、I2C适配器状态。