前言
I2C子系统详解,本篇博客从内核源码的角度来看I2C子系统。
嵌入式驱动学习专栏将详细记录博主学习驱动的详细过程,未来预计四个月将高强度更新本专栏,喜欢的可以关注本博主并订阅本专栏,一起讨论一起学习。现在关注就是老粉啦!
I2C介绍
I2C支持一主多从,各设备地址独立,标准模式传输速率为100kbit/s,快速模式为400kbit/s。总线通过上拉电阻连接到电源,当I2C设备空闲时,会输出高阻态,而当所有设备都空闲,都输出高阻态时,由上拉电阻把电线拉成高电平。
I2C物理总线使用两条总线线路,SCL(时钟线,数据收发同步)和SDA(数据线,传输具体数据)。
I2C通信协议
起始信号和停止信号
在 SCL 为高电平的时候,SDA 出现下降沿就表示为起始位。
和起始位的功能相反。在 SCL 位高电平的时候,SDA出现上升沿就表示为停止位。
应答信号
I2C主机发送完8位数据后会将SDA设置为输入状态,等待I2C从机应答,即等到I2C从机告诉主机它接收到数据了。应答信号是由从机发出的,主机需要提供应答信号所需的时钟,主机发送完 8 位数据以后紧跟着的一个时钟信号就是给应答信号使用的。从机通过将 SDA 拉低来表示发出应答信号,表示通信成功,否则表示通信失败。
数据传输
I2C的数据字节定义为8-bits长度,对每次传送的总字节数量没有限制,但对每一次传输必须伴有应答信号。I2C 总线在数据传输的时候要保证在 SCL 高电平期间,SDA 上的数据稳定,因此 SDA 上的数据变化只能在 SCL 低电平期间发生。
主机与从机通信
- 开始标志(S)发出后,主设备会传送一个7位的Slave地址,并且后面跟着一个第8位,称为Read/Write位。R/W位表示主设备是在接受从设备的数据还是在向其写数据。
- 然后,主设备释放SDA线,等待从设备的应答信号(ACK)每个字节的传输都要跟随有一个应答位。应答产生时,从设备将SDA线拉低并且在SCL为高电平时保持低。
- 数据传输总是以停止标志(P)结束,然后释放通信线路。 然而,主设备也可以产生重复的开始信号去操作另一台从设备,而不发出结束标志。综上可知,所有的SDA信号变化都要在SCL时钟为低电平时进行,除了开始和结束标志
先介绍几个缩写:
简写 | 含义 |
---|---|
S | 起始信号 |
P | 结束信号 |
AD | 地址数据 |
W | 写标志 |
R | 读标志 |
ACK | 应答信号 |
NACK | 拒绝应答信号 |
RA | 要写入或读取的寄存器地址 |
DATA | 传输的数据 |
单字节写数据:
多字节写数据:
举个通俗的例子来理解这个过程,现在老师和学生要打电话取得沟通:
老师开始通信——主机发送S,表示开始
老师找到学生的微信,并点击音频通话按钮——主机发送AD+W
学生接通电话——从机发送ACK
老师问:是xxx嘛——主机发送RA
学生说:是我——从机发送ACK
然后老师开始说:两件事情,第一件事xxxx——主机发送DATA
学生说:好的老师——从机发送ACK
然后老师说:第二件事xxx——主机发送DATA
学生说:好的老师——从机发送ACK
老师挂断电话——主机发送停止信号P
单字节读数据(第一个是W是因为后面需要向设备写入我要读取哪个寄存器):
多字节读数据:
这个再举老师的例子不合适了,因为老师作为主机举出来的例子很奇怪,但是也不能把学生当做主机,因为在I2C中,只有主机能发S信号,如果把学生作为主机,就会产生一种错觉,就是设备与主机之间能相互做主机,显然是很不对的。因此换一个里子,现在王先生去参加一个学术会议,会议的会务要找他获取一些报名信息:
会务想要找王先生沟通——主机发送S信号
会务找到王先生的电话并拨打过去——主机发送AD+W信号
王先生接到电话——从机发送ACK
会务说:您好,我们这边需要向您获取一些报名信息——主机发送RA,表示要读取信号的位置
王先生说:好的——从机发送ACK
会务说:那我们开始了哈——主机发送开始信号
会务说:再次确认,您是王xx先生吗,是的话请您告诉我们所在单位,发票抬头——主机发送地址、读取标志
王先生:好的——从机发送ACK
王先生:我的单位是xxx——从机发送data
会务说:好的——主机发送ACK
王先生:发票抬头是xxx——从机发送data
会务说:好的,我们这边登记完成——主机发送NACK,表示数据读取完成
会务挂断电话——主机发送P
I2C驱动框架
在裸机开发I2C驱动时,需要根据I2C协议手动配置I2C控制寄存器能够输出起始信号、停止信号、数据信息等。但是Linux中采用的是总线——设备——驱动模型。基于此想法,我们需要两个驱动,一个是I2C驱动,一个是设备驱动,比如用I2C读取mpu6050数据的时候,就需要一个mpu6050驱动。
i2c驱动框架包括i2c总线驱动、具体某个设备的驱动。
I2C总线又包括I2C设备和I2C驱动,当向Linux中注册设备或驱动的时候,按照I2C总线匹配规则进行匹配,配对成功的话,就可以通过I2C_driver
中的probe
函数创建具体的设备驱动。I2C设备不需要手动创建,而是利用设备树,设备树节点与platform总线相配合使用,所以需要先对I2C总线包装一层platform总线,当设备树节点转换为平台总线设备时,再进一步将其转换为I2C设备,注册到I2C总线中。
关键数据结构
i2c_adapter
这是一个I2C控制器,用于标识物理I2C总线以及访问它所需的访问算法的结构,其定义在 include/linux/i2c.h
文件内
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;
};
这里面有两个很重要:
const struct i2c_algorithm *algo
:这是一个i2c_algorithm结构体,访问总线的算法;struct device dev
:表明这是一个设备
struct 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
:该函数表示作为从设备时的发送函数
struct i2c_client
该结构体表示I2C从设备,即MPU6050等,其同样定义在include/linux/i2c.h
中:
struct i2c_client {
unsigned short flags; /* div., see below */
unsigned short addr; /* chip address - NOTE: 7bit */
/* addresses are stored in the */
/* _LOWER_ 7 bits */
char name[I2C_NAME_SIZE];
struct i2c_adapter *adapter; /* the adapter we sit on */
struct device dev; /* the device structure */
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_CLIENT_PEC表示它使用SMBus数据包错误检查;addr
:连接到I2C总线上的地址name
:表示设备的类型,通常是芯片名adapter
:struct i2c_adapter 结构体,管理托管这个I2C设备的总线段dev
:Driver model设备节点init_irq
:作为从设备时的发送函数irq
:表示该设备生成的中断号detected
:一个列表,因为I2C是多从机,这些从机的管理可以用一个链表来管理slave_cb
:使用适配器的I2C从模式时回调。适配器调用它来将从属事件传递给从属驱动程序。i2c_client识别连接到i2c总线的单个设备(即芯片)。
struct i2c_driver
I2C设备驱动,其在内核中的定义同样在include/linux/i2c.h
中:
struct i2c_driver {
unsigned int class;
/* Notifies the driver that a new bus has appeared. You should avoid
* using this, it will be removed in a near future.
*/
int (*attach_adapter)(struct i2c_adapter *) __deprecated;
/* Standard driver model interfaces */
int (*probe)(struct i2c_client *, const struct i2c_device_id *);
int (*remove)(struct i2c_client *);
/* driver model interfaces that don't relate to enumeration */
void (*shutdown)(struct i2c_client *);
/* Alert callback, for example for the SMBus alert protocol.
* The format and meaning of the data value depends on the protocol.
* For the SMBus alert protocol, there is a single bit of data passed
* as the alert response's low bit ("event flag").
*/
void (*alert)(struct i2c_client *, unsigned int data);
/* a ioctl like command that can be used to perform specific functions
* with the device.
*/
int (*command)(struct i2c_client *client, unsigned int cmd, void *arg);
struct device_driver driver;
const struct i2c_device_id *id_table;
/* Device detection callback for automatic device creation */
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总线的运行机制:
- 注册I2C总线
- 将I2C驱动添加到I2C总线的驱动链表中
- 遍历I2C总线上的链表,根据
i2c_device_match
函数进行匹配,如果匹配调用i2c_device_probe
函数 i2c_device_probe
函数会调用I2C驱动的probe函数
I2C总线定义在drivers/i2c/i2c-core.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
,其函数定义在drivers/i2c/i2c-core.c
中:
static int __init i2c_init(void)
{
int retval;
...
retval = bus_register(&i2c_bus_type); // 注册总线I2C_bus_type
if (retval)
return retval;
#ifdef CONFIG_I2C_COMPAT
i2c_adapter_compat_class = class_compat_register("i2c-adapter");
if (!i2c_adapter_compat_class) {
retval = -ENOMEM;
goto bus_err;
}
#endif
retval = i2c_add_driver(&dummy_driver); // 注册I2C驱动dummy_driver
if (retval)
goto class_err;
if (IS_ENABLED(CONFIG_OF_DYNAMIC))
WARN_ON(of_reconfig_notifier_register(&i2c_of_notifier));
return 0;
class_err:
#ifdef CONFIG_I2C_COMPAT
class_compat_unregister(i2c_adapter_compat_class);
bus_err:
#endif
bus_unregister(&i2c_bus_type);
return retval;
}
i2c设备和i2c驱动匹配规则
其定义在drivers/i2c/i2c-core.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;
if (!client)
return 0;
/* Attempt an OF style match */
if (of_driver_match_device(dev, drv))
return 1;
/* Then ACPI style match */
if (acpi_driver_match_device(dev, drv))
return 1;
driver = to_i2c_driver(drv);
/* match on an id table if there is one */
if (driver->id_table)
return i2c_match_id(driver->id_table, client) != NULL;
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驱动入口函数定义在drivers/i2c/busses/i2c-imx.c
中:
static int __init i2c_adap_imx_init(void)
{
return platform_driver_register(&i2c_imx_driver);
}
subsys_initcall(i2c_adap_imx_init);
static void __exit i2c_adap_imx_exit(void)
{
platform_driver_unregister(&i2c_imx_driver);
}
module_exit(i2c_adap_imx_exit);
入口函数和出口函数都在外面封了一层platform驱动,接下来来看这个platform驱动结构体i2c_imx_driver
:
static struct platform_driver i2c_imx_driver = {
.probe = i2c_imx_probe,
.remove = i2c_imx_remove,
.driver = {
.name = DRIVER_NAME,
.owner = THIS_MODULE,
.of_match_table = i2c_imx_dt_ids,
.pm = IMX_I2C_PM,
},
.id_table = imx_i2c_devtype,
};
匹配列表为i2c_imx_dt_ids
,用于和设备树节点匹配:
static const struct of_device_id i2c_imx_dt_ids[] = {
{ .compatible = "fsl,imx1-i2c", .data = &imx1_i2c_hwdata, },
{ .compatible = "fsl,imx21-i2c", .data = &imx21_i2c_hwdata, },
{ .compatible = "fsl,vf610-i2c", .data = &vf610_i2c_hwdata, },
{ /* sentinel */ }
};
接下来看probe
函数:
static int i2c_imx_probe(struct platform_device *pdev)
{
const struct of_device_id *of_id = of_match_device(i2c_imx_dt_ids,
&pdev->dev);
struct imx_i2c_struct *i2c_imx;
struct resource *res;
struct imxi2c_platform_data *pdata = dev_get_platdata(&pdev->dev);
void __iomem *base;
int irq, ret;
dma_addr_t phy_addr;
dev_dbg(&pdev->dev, "<%s>\n", __func__);
irq = platform_get_irq(pdev, 0); // 获取中断号
if (irq < 0) {
dev_err(&pdev->dev, "can't get irq number\n");
return irq;
}
res = platform_get_resource(pdev, IORESOURCE_MEM, 0); // 获取reg属性,该函数实现的功能与of函数获取reg属性相同
base = devm_ioremap_resource(&pdev->dev, res); // 获取I2C的基地址后,用该函数转为虚拟地址
if (IS_ERR(base))
return PTR_ERR(base);
phy_addr = (dma_addr_t)res->start;
i2c_imx = devm_kzalloc(&pdev->dev, sizeof(*i2c_imx), GFP_KERNEL); // 为i2c_imx申请内存空间
if (!i2c_imx)
return -ENOMEM;
if (of_id)
i2c_imx->hwdata = of_id->data;
else
i2c_imx->hwdata = (struct imx_i2c_hwdata *)
platform_get_device_id(pdev)->driver_data;
/* Setup i2c_imx driver structure */
strlcpy(i2c_imx->adapter.name, pdev->name, sizeof(i2c_imx->adapter.name)); // 这部分是初始化i2c_imx结构体,在程序中代表一个实际的i2c总线。
i2c_imx->adapter.owner = THIS_MODULE;
i2c_imx->adapter.algo = &i2c_imx_algo; // 特别需要关心的是该成员,其代表了访问总线的算法
i2c_imx->adapter.dev.parent = &pdev->dev;
i2c_imx->adapter.nr = pdev->id;
i2c_imx->adapter.dev.of_node = pdev->dev.of_node;
i2c_imx->base = base;
/* Get I2C clock */
i2c_imx->clk = devm_clk_get(&pdev->dev, NULL);
if (IS_ERR(i2c_imx->clk)) {
dev_err(&pdev->dev, "can't get I2C clock\n");
return PTR_ERR(i2c_imx->clk);
}
ret = clk_prepare_enable(i2c_imx->clk);
if (ret) {
dev_err(&pdev->dev, "can't enable I2C clock\n");
return ret;
}
/* Request IRQ */
ret = devm_request_irq(&pdev->dev, irq, i2c_imx_isr,
IRQF_NO_SUSPEND, pdev->name, i2c_imx);
if (ret) {
dev_err(&pdev->dev, "can't claim irq %d\n", irq);
goto clk_disable;
}
/* Init queue */
init_waitqueue_head(&i2c_imx->queue);
/* Set up adapter data */
i2c_set_adapdata(&i2c_imx->adapter, i2c_imx);
/* Set up clock divider */
i2c_imx->bitrate = IMX_I2C_BIT_RATE;
ret = of_property_read_u32(pdev->dev.of_node,
"clock-frequency", &i2c_imx->bitrate);
if (ret < 0 && pdata && pdata->bitrate)
i2c_imx->bitrate = pdata->bitrate;
/* Set up chip registers to defaults */
imx_i2c_write_reg(i2c_imx->hwdata->i2cr_ien_opcode ^ I2CR_IEN,
i2c_imx, IMX_I2C_I2CR);
imx_i2c_write_reg(i2c_imx->hwdata->i2sr_clr_opcode, i2c_imx, IMX_I2C_I2SR);
/* Add I2C adapter */
ret = i2c_add_numbered_adapter(&i2c_imx->adapter);
if (ret < 0) {
dev_err(&pdev->dev, "registration failed\n");
goto clk_disable;
}
/* Set up platform driver data */
platform_set_drvdata(pdev, i2c_imx);
clk_disable_unprepare(i2c_imx->clk);
dev_dbg(&i2c_imx->adapter.dev, "claimed irq %d\n", irq);
dev_dbg(&i2c_imx->adapter.dev, "device resources: %pR\n", res);
dev_dbg(&i2c_imx->adapter.dev, "adapter name: \"%s\"\n",
i2c_imx->adapter.name);
dev_info(&i2c_imx->adapter.dev, "IMX I2C adapter registered\n");
/* Init DMA config if supported */
i2c_imx_dma_request(i2c_imx, phy_addr);
return 0; /* Return OK */
clk_disable:
clk_disable_unprepare(i2c_imx->clk);
return ret;
}
接下来看probe
函数,整个.prob
函数完成了两个主要工作:
- 第一,初始化i2c1硬件
- 第二,初始化一个“代表i2c1”的i2c_adapter结构体,并将其添加到系统中。
.probe函数完成了i2c的基本初始化并将其添加到了系统中。i2c总线驱动的另外一个重要工作就是实现i2c对外接口函数。 我们在初始化i2c_adapter结构体时已经初始化了访问总线算法结构体i2c_adapter->i2c_algorithm
。 具体代码为 i2c_imx->adapter.algo = &i2c_imx_algo
。
i2c_imx
是一个imx_i2c_struct
类型的结构体,该结构体大多数是用于保存i2c硬件信息,其定义如下:
struct imx_i2c_struct {
struct i2c_adapter adapter;
struct clk *clk;
void __iomem *base;
wait_queue_head_t queue;
unsigned long i2csr;
unsigned int disable_delay;
int stopped;
unsigned int ifdr; /* IMX_I2C_IFDR */
unsigned int cur_clk;
unsigned int bitrate;
const struct imx_i2c_hwdata *hwdata;
struct imx_i2c_dma *dma;
};
- clk: clk结构体保存时钟相关信息
- bitrate: 保存i2c的波特率
- dma: struct imx_i2c_dma 结构体 dam相关信息等等
上面probe函数中,定义i2c_imx结构体的时候,也定义了访问i2c的算法,其定义如下:
static struct i2c_algorithm i2c_imx_algo = {
.master_xfer = i2c_imx_xfer,
.functionality = i2c_imx_func,
};
i2c_imx_algo结构体内指定了两个函数,它们就是外部访问i2c总线的接口:
- 函数i2c_imx_func只是用于返回当前所处状态。
- 函数i2c_imx_xfer真正实现外部访问i2c总线。
/*
* @description : 实现具体的收发工作
*/
static int i2c_imx_xfer(struct i2c_adapter *adapter, struct i2c_msg *msgs, int num)
至此,我们知道i2c总线驱动完成了i2c的硬件初始化、将i2c总线添加到系统、并提供外界访问i2c总线的接口函数。 我们的i2c设备驱动只需要根据特定设备使用这些接口函数即可。
i2c设备驱动核心函数
#define i2c_add_driver(driver) // 这个宏调用的下面这个函数
/*
* @description : 注册一个i2c驱动
* @param-owner : 一般是THIS_MODULE
* @param-driver: 要注册的i2c_driver
* @return : 0表示成功,负数表示失败
*/
int i2c_register_driver(struct module *owner, struct i2c_driver *driver)
/*
* @description : 注册一个i2c驱动
* @param-adap : 收发消息所使用的i2c适配器
* @param-msgs : struct i2c_msg 结构体,i2c要发送的一个或多个消息
* @param-num : 消息数量,也就是msgs的数量
* @return : 成功的话返回发送msgs的数量,失败的话返回负数
*/
int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num)
i2c_msg
定义在include/uapi/linux/i2c.h
中:
struct i2c_msg {
__u16 addr; /* slave address */
__u16 flags;
#define I2C_M_TEN 0x0010 /* this is a ten bit chip address */
#define I2C_M_RD 0x0001 /* read data, from slave to master */
#define I2C_M_STOP 0x8000 /* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_NOSTART 0x4000 /* if I2C_FUNC_NOSTART */
#define I2C_M_REV_DIR_ADDR 0x2000 /* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_IGNORE_NAK 0x1000 /* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_NO_RD_ACK 0x0800 /* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_RECV_LEN 0x0400 /* length will be first received byte */
__u16 len; /* msg length */
__u8 *buf; /* pointer to msg data */
};
addr
: iic设备地址flags
: 消息传输方向和特性。I2C_M_RD:表示读取消息;0:表示发送消息。len
: 消息数据的长度buf
: 字符数组存放消息,作为消息的缓冲区
// 发送一个i2c消息
int i2c_master_send(const struct i2c_client *client, const char *buf, int count)
// 接收一个i2c消息
int i2c_master_recv(const struct i2c_client *client, char *buf, int count)
总结
简单来说,i2c_client设备有一个属性是name,i2c_driver中有个id_table,然后将client中的name与table中的值进行对比匹配,匹配成功则进入probe函数。而I2C_adapter
在Linux系统中起着桥梁作用,连接主机系统与I2C总线。
参考资料
[1] 【正点原子】I.MX6U嵌入式Linux驱区动开发指南 第二十六章、第六十一章
[2] I2C子系统–mpu6050驱动实验