I2C是广泛应用于计算机中的串行总线,用于处理器和其外设之间的通信。
I2C硬件基本概念
- I2C总线由两根传递数据的双向信号线与一根地线组成,半双工、主从方式通信。
- Serial Clock Line (SCL)
- Serial Data Address (SDA)每个设备都有一个唯一设备地址,一次传输8bit,高位在前,低位在后。一次完整的I2C通信需要经历一个完整的时序,I2C总线通信完整时序如下图。一般在驱动中无需关心具体时序,只需操作SoC中的I2C控制器即可,只有在裸机下需要用GPIO模拟I2C通信时才需用到,所以笔者在本文不阐述I2C时序(其实就是懒 O__O “…)。
- 总线速度有三种模式
- 标准模式 100kbps
- 快速模式 400kbps
- 高速模式 3.4Mbps
I2C子系统框架
- I2C设备驱动层:drivers/i2c/i2c-dev.c (通用型) 或者为特定设备定制的设备驱动(比如E2PROM驱动)
- I2C核心层: drivers/i2c/i2c-coere.c
- I2C总线驱动层(主机控制器驱动层):drivers/i2c/busses/i2c-s3c2410.c
I2C设备驱动层
- 是I2C从机的驱动程序
- 给用户提供调用接口
- 内核提供两种方式来实现设备驱动:
- 第一种是内核默认实现的通用型的I2C设备驱动,位于
drivers/i2c/i2c-dev.c
中。 这种方式仅仅只是封装了I2C的基本操作,相当于只是封装了I2C的基本时序,向应用层只提供了I2C基本操作的接口,该接口通用于所有的I2C设备。具体设备相关的操作,需要开发者在应用层根据硬件特性来完成对设备的操作。该方式的优点就是通用,而缺点也很明显,封装的不够彻底,需要应用开发人员对硬件有一定程度的了解。 - 第二种是根据特定设备来编写的特定的I2C设备驱动, 该方式彻底封装了硬件的操作,提供给应用层的接口彻底屏蔽I2C的通信细节。该方式的优点就是应用开发人员无需关心硬件。
- 第一种是内核默认实现的通用型的I2C设备驱动,位于
I2C核心层
- 注册I2C总线
- 由内核开发人员编写的,不涉及具体硬件
- 给驱动编程人员提供编程接口
I2C总线驱动层
- 是I2C主机适配器的驱动程序
- 初始化I2C适配器(控制器)
- 实现操作方法:根据I2C操作时序进行操作I2C控制器实现收发数据
源码分析
源码中会涉及到一部分SMBus相关内容,SMBus是Intel在I2C的基础上开发的类似I2C的总线,本文不探讨SMBus相关内容(其实说白了,还是懒QAQ)。笔者会大体上对I2C子系统的源码进行分析,如若分析的有出入,还望指出。
I2C核心层
I2C核心层的实现位于drivers/i2c/i2c-core.c
中,笔者从i2c_init
函数开始分析。
static int __init i2c_init(void)
{
int retval;
retval = bus_register(&i2c_bus_type); // 注册I2C总线
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驱动
if (retval)
goto class_err;
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;
}
该函数先是调用了bus_register
函数注册了I2C总线,随后调用i2c_add_driver
函数来注册了一个虚假的I2C驱动。
先对注册的I2C总线i2c_bus_type
进行分析
struct bus_type i2c_bus_type = {
.name = "i2c",
.match = i2c_device_match,
.probe = i2c_device_probe,
.remove = i2c_device_remove,
.shutdown = i2c_device_shutdown,
.pm = &i2c_device_pm_ops,
};
根据Linux设备驱动模型的原理,I2C总线下会挂载两条链表,分别为设备链和驱动链,只要其中一个链表有结点插入,即会通过i2c_device_match
函数来遍历另一条链表去匹配设备与驱动,一旦匹配上则会调用i2c_device_probe
函数,而i2c_device_probe
函数又会调用i2c_driver的probe
函数。进到i2c_device_match
和i2c_device_probe
进行分析。
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;
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;
}
可以看到, i2c_device_match
函数调用的是i2c_match_id
函数来进行匹配。从源码中可见,需要注意的是I2C总线匹配方式不同于Platform总线,I2C总线只匹配**id_table
**中的name,并不会去匹配driver中的name。
static int i2c_device_probe(struct device *dev)
{
struct i2c_client *client = i2c_verify_client(dev);
struct i2c_driver *driver;
int status;
if (!client)
return 0;
driver = to_i2c_driver(dev->driver);
if (!driver->probe || !driver->id_table)
return -ENODEV;
client->driver = driver;
if (!device_can_wakeup(&client->dev))
device_init_wakeup(&client->dev,
client->flags & I2C_CLIENT_WAKE);
dev_dbg(dev, "proben");
/* 调用driver中的probe函数 */
status = driver->probe(client, i2c_match_id(driver->id_table, client));
if (status) {
client->driver = NULL;
i2c_set_clientdata(client, NULL);
}
return status;
}
可以看到,的确是调用driver->probe
来进行真正的probe。需要注意的是**if (!driver->probe || !driver->id_table) return -ENODEV;
**中对**id_table
**进行了非空判断,所以如果采用设备树方式进行匹配也需要对**.id_table
**进行有效赋值,否则会出现match上了但probe函数不会调用的奇怪现象,个人感觉这应该是个bug,毕竟这个核心层在设备树出现之前就已经存在了。
回到i2c_init
函数,然后注册了一个空的名为dummy
的i2c_driver。
static int dummy_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
return 0;
}
static int dummy_remove(struct i2c_client *client)
{
return 0;
}
static struct i2c_driver dummy_driver = {
.driver.name = "dummy",
.probe = dummy_probe,
.remove = dummy_remove,
.id_table = dummy_id,
};
可以看到这是一个完全空的虚假驱动,而I2C核心层为何要注册一个假的驱动不得而知,笔者查阅了网上资料也没法得知,但是/sys/bus/i2c/drivers/dummy
确实存在,所以笔者猜测应该纯粹是开发该层次调试用的。
核心层还提供了一系列函数接口供驱动开发者注册和注销驱动:
- i2c_add_adapter 注册I2C主机适配器驱动 (动态分配总线号)
- i2c_add_numbered_adapter 注册I2C主机适配器驱动 (静态指定总线号)
- i2c_del_adapter 注销I2C主机适配器驱动
- i2c_add_driver 注册I2C从机设备驱动
- i2c_del_driver 注销I2C从机设备驱动
其他函数暂不分析,在分析其他层的时候调用时再进行分析。
I2C设备驱动层
笔者先从内核提供的通用驱动开始分析,最后在文末给出特定驱动的分析。内核提供了一个通用于所有设备的I2C设备驱动,用户可以在应用层实现对I2C的驱动,其实现位于drivers/i2c/i2c-dev.c
中。同样从init函数开始,笔者从i2c_dev_init
函数开始分析。
static int __init i2c_dev_init(void)
{
int res;
printk(KERN_INFO "i2c /dev entries drivern");
/* 将通用驱动注册为字符设备驱动,并提供file_operations 操作方法 */
res = register_chrdev(I2C_MAJOR, "i2c", &i2cdev_fops);
if (res)
goto out;
/* 创建类 */
i2c_dev_class = class_create(THIS_MODULE, "i2c-dev");
if (IS_ERR(i2c_dev_class)) {
res = PTR_ERR(i2c_dev_class);
goto out_unreg_chrdev;
}
/* 注册I2C从机设备驱动 */
res = i2c_add_driver(&i2cdev_driver);
if (res)
goto out_unreg_class;
return 0;
out_unreg_class:
class_destroy(i2c_dev_class);
out_unreg_chrdev:
unregister_chrdev(I2C_MAJOR, "i2c");
out:
printk(KERN_ERR "%s: Driver Initialisation failedn", __FILE__);
return res;
}
i2c_dev_init
函数先是调用了register_chrdev
函数注册了一个字符设备驱动,并提供了一个file_operations。由此可见,是将通用驱动实现为字符设备驱动,并由其file_operations结构体的方法为应用层提供通用接口。然后调用class_create
创建了一个类,但是可以看到并没有调用device_create
在该类下创建设备,所以注意在这里并没有生成设备节点。最后调用i2c_add_driver
注册了一个I2C从机设备驱动i2cdev_driver
。i2cdev_driver
定义如下。
static struct i2c_driver i2cdev_driver = {
.driver = {
.name = "dev_driver",
},
.attach_adapter = i2cdev_attach_adapter,
.detach_adapter = i2cdev_detach_adapter,
};
从上可以看到并没有对id_table
进行赋值,从上文在I2C核心层分析可知,I2C总线是根据id_table
进行匹配,所以这里并不会按照常规的Linux驱动模型进行match后probe,况且这个驱动里也没有probe方法。所以这到底是什么情况?别慌,虽然没有id_table和probe,但是它单独提供了两个方法attach_adapter
和detach_adapter
。这里先埋个伏笔,不做分析,到I2C总线驱动层分析后自然会柳暗花明。
I2C总线驱动层
笔者使用的SoC是S5PV210,其控制器跟S3C2410基本一致,所以三星的驱动开发者并没有再去写一份S5PV210的主机适配器驱动,而是使用了S3C2410的主机适配器驱动,其位于drivers/i2c/busses/i2c-s3c2410.c
中。
从i2c_adap_s3c_init
函数开始分析。
static int __init i2c_adap_s3c_init(void)
{
return platform_driver_register(&s3c24xx_i2c_driver);
}
可以看到其作为平台设备驱动而实现,注册了s3c24xx_i2c_driver
驱动。
static struct platform_device_id s3c24xx_driver_ids[] = {
{
.name = "s3c2410-i2c",
.driver_data = TYPE_S3C2410,
}, {
.name = "s3c2440-i2c",
.driver_data = TYPE_S3C2440,
}, { },
};
MODULE_DEVICE_TABLE(platform, s3c24xx_driver_ids);
static struct platform_driver s3c24xx_i2c_driver = {
.probe = s3c24xx_i2c_probe,
.remove = s3c24xx_i2c_remove,
.id_table = s3c24xx_driver_ids,
.driver = {
.owner = THIS_MODULE,
.name = "s3c-i2c",
.pm = S3C24XX_DEV_PM_OPS,
},
};
根据平台总线的原理,很容易得知在arch/arm/mach-s5pv210/mach-x210.c
中对其驱动对应的设备进行了注册,其注册的设备定义位于dev-i2c0.c
,这是I2C的资源文件。其定义的资源如下。
static struct resource s3c_i2c_resource[] = {
[0] = {
.start = S3C_PA_IIC,
.end = S3C_PA_IIC SZ_4K - 1,
.flags = IORESOURCE_MEM,
},
[1] = {
.start = IRQ_IIC,
.end = IRQ_IIC,
.flags = IORESOURCE_IRQ,
},
};
struct platform_device s3c_device_i2c0 = {
.name = "s3c2410-i2c",
.id = 0,
.num_resources = ARRAY_SIZE(s3c_i2c_resource),
.resource = s3c_i2c_resource,
};
由name可知,与s3c24xx_i2c_driver
是匹配的。除此之外,还定义了平台数据default_i2c_data0
和default_i2c_data0
函数。其相关的调用还是在arch/arm/mach-s5pv210/mach-x210.c
中进行的,在mach-x210.c
中的smdkc110_machine_init
函数中进行了如下调用
/* i2c */
// 设置I2C平台数据 NULL表示设置默认的平台数据
s3c_i2c0_set_platdata(NULL);
s3c_i2c1_set_platdata(NULL);
s3c_i2c2_set_platdata(NULL);
现在进到s3c_i2c0_set_platdata
函数进行分析。
static struct s3c2410_platform_i2c default_i2c_data0 __initdata = {
.flags = 0,
.slave_addr = 0x10, // I2C控制器作为从设备时使用的地址
.frequency = 400*1000, // 400kbps
.sda_delay = S3C2410_IICLC_SDA_DELAY15 | S3C2410_IICLC_FILTER_ON, // 间隔时间
};
void __init s3c_i2c0_set_platdata(struct s3c2410_platform_i2c *pd)
{
struct s3c2410_platform_i2c *npd;
if (!pd) // 参数为NULL则使用该函数上面定义的默认的平台数据
pd = &default_i2c_data0;
npd = kmemdup(pd, sizeof(struct s3c2410_platform_i2c), GFP_KERNEL);
if (!npd)
printk(KERN_ERR "%s: no memory for platform datan", __func__);
else if (!npd->cfg_gpio)
npd->cfg_gpio = s3c_i2c0_cfg_gpio; // GPIO初始化方法
// 设置为平台数据
s3c_device_i2c0.dev.platform_data = npd;
}
可以看到传递NULL则使用了默认的平台数据, 将s3c_i2c0_cfg_gpio
函数设置到了平台数据cfg_gpio
方法中,最后将平台数据挂接到s3c_device_i2c0
这个设备上。
void s3c_i2c0_cfg_gpio(struct platform_device *dev)
{
s3c_gpio_cfgpin(S5PV210_GPD1(0), S3C_GPIO_SFN(2)); // 设置控制寄存器为I2C0_SDA模式
s3c_gpio_setpull(S5PV210_GPD1(0), S3C_GPIO_PULL_NONE);
s3c_gpio_cfgpin(S5PV210_GPD1(1), S3C_GPIO_SFN(2)); // 设置控制寄存器为I2C0_SCL模式
s3c_gpio_setpull(S5PV210_GPD1(1), S3C_GPIO_PULL_NONE);
}
可以看到s3c_i2c0_cfg_gpio
函数只是对I2C控制器两根通信线的GPIO初始化。
接下去回到I2C总线驱动层i2c-s3c2410.c
中, 进入到s3c24xx_i2c_probe
函数进行分析。 probe函数的代码比较多,分段进行分析。
struct s3c24xx_i2c *i2c;
struct s3c2410_platform_i2c *pdata;
struct resource *res;
int ret;
// 获取I2C平台数据
pdata = pdev->dev.platform_data;
if (!pdata) {
dev_err(&pdev->dev, "no platform datan");
return -EINVAL;
}
i2c = kzalloc(sizeof(struct s3c24xx_i2c), GFP_KERNEL);
if (!i2c) {
dev_err(&pdev->dev, "no memory for staten");
return -ENOMEM;
}
strlcpy(i2c->adap.name, "s3c2410-i2c", sizeof(i2c->adap.name));
i2c->adap.owner = THIS_MODULE;
i2c->adap.algo = &s3c24xx_i2c_algorithm; // I2C主机控制器的操作方法
i2c->adap.retries = 2;
i2c->adap.class = I2C_CLASS_HWMON | I2C_CLASS_SPD;
i2c->tx_setup = 50;
三星采用struct s3c24xx_i2c
结构体来对SoC的控制器进行抽象,该结构体继承于struct i2c_adapter
。该段代码先是从device中获取了平台数据,该平台数据即是上文调用s3c_i2c0_set_platdata
函数时设置的。然后对i2c->adap
进行了相关赋值,关键部分是i2c->adap.algo = &s3c24xx_i2c_algorithm;
,adap.algo
表示I2C主机控制器的操作方法,将该SoC的操作方法挂接到了适配器上。s3c24xx_i2c_algorithm
定义了两个操作方法,主要是master_xfer
方法,用来发送消息。代码如下。
static const struct i2c_algorithm s3c24xx_i2c_algorithm = {
.master_xfer = s3c24xx_i2c_xfer,
.functionality = s3c24xx_i2c_func,
};
s3c24xx_i2c_xfer
涉及到对具体控制器的操作,不进行展开,但是注意的是其内部调用的是s3c24xx_i2c_doxfer
,在s3c24xx_i2c_doxfer
函数内部发送完数据后,调用wait_event_timeout
函数来进行睡眠等待从机响应。因此可知内核中I2C的等待从机的ACK信号是通过中断实现的,即主机发送完数据后进入睡眠等待从机,从机响应后通过中断通知主机后唤醒。
probe函数接着做了获取时钟和使能时钟,相关代码如下。
代码语言:javascript复制// 获取时钟
i2c->clk = clk_get(&pdev->dev, "i2c");
if (IS_ERR(i2c->clk)) {
dev_err(&pdev->dev, "cannot get clockn");
ret = -ENOENT;
goto err_noclk;
}
dev_dbg(&pdev->dev, "clock source %pn", i2c->clk);
// 使能时钟
clk_enable(i2c->clk);
紧接着对具体IO和IRQ进行操作。
代码语言:javascript复制// 获取I2C平台资源(IO内存地址、IRQ)
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (res == NULL) {
dev_err(&pdev->dev, "cannot find IO resourcen");
ret = -ENOENT;
goto err_clk;
}
i2c->ioarea = request_mem_region(res->start, resource_size(res),
pdev->name);
if (i2c->ioarea == NULL) {
dev_err(&pdev->dev, "cannot request IOn");
ret = -ENXIO;
goto err_clk;
}
// 将物理地址映射为虚拟地址
i2c->regs = ioremap(res->start, resource_size(res));
if (i2c->regs == NULL) {
dev_err(&pdev->dev, "cannot map IOn");
ret = -ENXIO;
goto err_ioarea;
}
dev_dbg(&pdev->dev, "registers %p (%p, %p)n",
i2c->regs, i2c->ioarea, res);
/* setup info block for the i2c core */
i2c->adap.algo_data = i2c;
i2c->adap.dev.parent = &pdev->dev;
/* initialise the i2c controller */
// 初始化I2C控制器
ret = s3c24xx_i2c_init(i2c);
if (ret != 0)
goto err_iomap;
// 获取IRQ资源
i2c->irq = ret = platform_get_irq(pdev, 0);
if (ret <= 0) {
dev_err(&pdev->dev, "cannot find IRQn");
goto err_iomap;
}
// 申请IRQ (裸机一般使用查询法来判断从机的响应,而内核一般采用中断方式等待从机响应)
ret = request_irq(i2c->irq, s3c24xx_i2c_irq, IRQF_DISABLED,
dev_name(&pdev->dev), i2c);
把关注点放在初始化I2C控制器的s3c24xx_i2c_init
函数和申请IRQ上。
static int s3c24xx_i2c_init(struct s3c24xx_i2c *i2c)
{
unsigned long iicon = S3C2410_IICCON_IRQEN | S3C2410_IICCON_ACKEN;
struct s3c2410_platform_i2c *pdata;
unsigned int freq;
/* get the plafrom data */
pdata = i2c->dev->platform_data;
/* inititalise the gpio */
if (pdata->cfg_gpio)
pdata->cfg_gpio(to_platform_device(i2c->dev)); // 设置I2C对应的管脚
/* write slave address */
// 设置I2C控制器作为从设备时的地址
writeb(pdata->slave_addr, i2c->regs S3C2410_IICADD);
dev_dbg(i2c->dev, "slave address 0xxn", pdata->slave_addr);
writel(iicon, i2c->regs S3C2410_IICCON); // 使能 Tx/Rx Interrupt 和 ACK信号
/* we need to work out the divisors for the clock... */
// 配置I2C的时钟频率
if (s3c24xx_i2c_clockrate(i2c, &freq) != 0) {
writel(0, i2c->regs S3C2410_IICCON);
dev_err(i2c->dev, "cannot meet bus frequency requiredn");
return -EINVAL;
}
/* todo - check that the i2c lines aren't being dragged anywhere */
dev_dbg(i2c->dev, "bus frequency set to %d KHzn", freq);
dev_dbg(i2c->dev, "S3C2410_IICCON=0xlxn", iicon);
dev_dbg(i2c->dev, "S3C2440_IICLC=xn", pdata->sda_delay);
writel(pdata->sda_delay, i2c->regs S3C2440_IICLC);
return 0;
}
可以看到设置I2C对应的管脚是调用平台数据中的cfg_gpio
,其实看到这里如果还有印象的话就能反应出来这是在调用s3c_i2c0_set_platdata
中设置的。该函数还设置了I2C控制器的从地址,该地址用来在控制器作为从地址时使用,但是这种情况的出现微乎其微。除此之外使能Tx/Rx Interrupt和ACK信号,配置了I2C的时钟频率。
注意从前一段分析中得知,内核中I2C采用中断方式等待从机响应,所以probe函数这一段代码中申请了IRQ并绑定了中断处理函数s3c24xx_i2c_irq
。
static irqreturn_t s3c24xx_i2c_irq(int irqno, void *dev_id)
{
struct s3c24xx_i2c *i2c = dev_id;
unsigned long status;
unsigned long tmp;
// 获取I2CSTAT寄存器的值
status = readl(i2c->regs S3C2410_IICSTAT);
if (status & S3C2410_IICSTAT_ARBITR) { // I2C总线仲裁失败
/* deal with arbitration loss */
dev_err(i2c->dev, "deal with arbitration lossn");
}
if (i2c->state == STATE_IDLE) {
dev_dbg(i2c->dev, "IRQ: error i2c->state == IDLEn");
tmp = readl(i2c->regs S3C2410_IICCON);
tmp &= ~S3C2410_IICCON_IRQPEND;
writel(tmp, i2c->regs S3C2410_IICCON);
goto out;
}
/* pretty much this leaves us with the fact that we've
* transmitted or received whatever byte we last sent */
// 处理I2C的收发数据
i2c_s3c_irq_nextbyte(i2c, status);
out:
return IRQ_HANDLED;
}
具体也不展开分析了,但是要注意的是有这么一条线:该中断处理函数调用了i2c_s3c_irq_nextbyte
,然后内部调用了s3c24xx_i2c_stop
,再内部调用了s3c24xx_i2c_master_complete
,最后再内部执行了一个关键代码wake_up(&i2c->wait);
,这就是通过中断方式唤醒之前在发送数据时进行的睡眠等待。
回到probe函数,最后分析重头戏。
代码语言:javascript复制ret = i2c_add_numbered_adapter(&i2c->adap);
if (ret < 0) {
dev_err(&pdev->dev, "failed to add bus to i2c coren");
goto err_cpufreq;
}
该代码将I2C适配器注册到了内核中。i2c_add_numbered_adapter
函数由核心层提供,其定义位于I2C核心层drivers/i2c/i2c-core.c
中,用来注册I2C适配器。其实在内核中提供了两个adapter注册接口,分别为i2c_add_adapter
和i2c_add_numbered_adapter
由于在系统中可能存在多个adapter, 所以将每一条I2C总线(控制器)对应一个编号,这个总线号(可以称这个编号为总线号码)与PCI中的总线号不同。它和硬件无关, 只是软件上便于区分而已。对于i2c_add_adapter
而言, 它使用的是动态总线号, 即由系统给其分配一个总线号, 而i2c_add_numbered_adapter
则是自己指定总线号, 如果这个总线号非法或者是被占用, 就会注册失败。不管哪个注册接口,其核心都是调用i2c_register_adapter
函数来进行真正的注册。取出i2c_register_adapter
函数的关键部分进行分析。
res = device_register(&adap->dev);
if (adap->nr < __i2c_first_dynamic_bus_num)
i2c_scan_static_board_info(adap);
dummy = bus_for_each_drv(&i2c_bus_type, NULL, adap,
__process_new_adapter);
device_register(&adap->dev);
表示主机适配器adapter的注册。
i2c_scan_static_board_info(adap);
内部先遍历__i2c_board_list
取出板卡信息(描述的是板子上的I2C外设的信息,即I2C从机的信息),该链表的生成是在arch/arm/mach-s5pv210/mach-x210.c
中进行的,在mach-x210.c
中的smdkc110_machine_init
函数中进行了除之前分析的调用s3c_i2c0_set_platdata
外,还调用了i2c_register_board_info
对板卡信息进行了注册。
int __init
i2c_register_board_info(int busnum,
struct i2c_board_info const *info, unsigned len)
{
int status;
down_write(&__i2c_board_lock);
/* dynamic bus numbers will be assigned after the last static one */
// __i2c_first_dynamic_bus_num为全局未显式初始化变量,所以第一次进到这个函数,值为0
if (busnum >= __i2c_first_dynamic_bus_num)
__i2c_first_dynamic_bus_num = busnum 1;
for (status = 0; len; len--, info ) {
struct i2c_devinfo *devinfo;
devinfo = kzalloc(sizeof(*devinfo), GFP_KERNEL);
if (!devinfo) {
pr_debug("i2c-core: can't register boardinfo!n");
status = -ENOMEM;
break;
}
devinfo->busnum = busnum;
devinfo->board_info = *info;
list_add_tail(&devinfo->list, &__i2c_board_list); // 将board_info用链表管理起来
}
up_write(&__i2c_board_lock);
return status;
}
板卡信息的描述,主要对其设备名和从地址进行赋值,示例如下
代码语言:javascript复制#define I2C_BOARD_INFO(dev_type, dev_addr)
.type = dev_type, .addr = (dev_addr)
#ifdef CONFIG_TOUCHSCREEN_GSLX680
{
I2C_BOARD_INFO("gslX680", 0x40), // 主要对其设备名和从地址进行赋值
},
#endif
然后在i2c_scan_static_board_info
内部利用板卡信息作为原料调用i2c_new_device
来创建了client,表示从机设备,并将adapter挂接到了client结构体内部的指针上。i2c_scan_static_board_info
代码如下。
static void i2c_scan_static_board_info(struct i2c_adapter *adapter)
{
struct i2c_devinfo *devinfo;
down_read(&__i2c_board_lock);
// __i2c_board_list在调用i2c_register_board_info时链接起来的
list_for_each_entry(devinfo, &__i2c_board_list, list) {
if (devinfo->busnum == adapter->nr
&& !i2c_new_device(adapter,
&devinfo->board_info))
dev_err(&adapter->dev,
"Can't create device at 0xxn",
devinfo->board_info.addr);
}
up_read(&__i2c_board_lock);
}
创建完client后,回到i2c_register_adapter
函数,最后执行了dummy = bus_for_each_drv(&i2c_bus_type, NULL, adap, __process_new_adapter);
,该函数是遍历在I2C总线上已经注册的driver,通过回调**__process_new_adapter
**函数的方式,遍历到i2c-dev这个通用驱动后就会用其**i2cdev_attach_adapter
**方法来挂接到在i2c-dev中注册的字符设备驱动,并使用这个字符设备驱动的主设备号和adapter中的总线号(作为次设备号)来创建名为i2c-x的设备节点,应用层访问这个设备节点后即可调用在i2c-dev中注册的file_operations中的操作方法,从操作方法源码知,最终读写调用的是adapter中的读写方法(即在本平台中为i2c-s3c2410.c中定义的方法)。下面对其进行验证。
__process_new_adapter
展开如下
static int i2c_do_add_adapter(struct i2c_driver *driver,
struct i2c_adapter *adap)
{
/* Detect supported devices on that bus, and instantiate them */
i2c_detect(adap, driver);
/* Let legacy drivers scan this bus for matching devices */
if (driver->attach_adapter) {
/* We ignore the return code; if it fails, too bad */
driver->attach_adapter(adap); // 调用i2c-dev中的i2cdev_attach_adapter方法
}
return 0;
}
static int __process_new_adapter(struct device_driver *d, void *data)
{
return i2c_do_add_adapter(to_i2c_driver(d), data);
}
可以看到driver->attach_adapter(adap);
,的确是调用I2C总线下的驱动中的attach_adapter
方法,到了这里在I2C设备驱动层埋下的悬念终于要水落石出了(不容易啊啊啊啊啊啊),穿越回到I2C设备驱动层进行分析,进入drivers/i2c/i2c-dev.c
分析i2cdev_attach_adapter
方法。
static int i2cdev_attach_adapter(struct i2c_adapter *adap)
{
struct i2c_dev *i2c_dev;
int res;
i2c_dev = get_free_i2c_dev(adap);
if (IS_ERR(i2c_dev))
return PTR_ERR(i2c_dev);
/* register this i2c device with the driver core */
/* 使用主设备号和adapter中的总线号(作为次设备号)来创建名为i2c-x的设备节点 */
i2c_dev->dev = device_create(i2c_dev_class, &adap->dev,
MKDEV(I2C_MAJOR, adap->nr), NULL,
"i2c-%d", adap->nr);
if (IS_ERR(i2c_dev->dev)) {
res = PTR_ERR(i2c_dev->dev);
goto error;
}
res = device_create_file(i2c_dev->dev, &dev_attr_name);
if (res)
goto error_destroy;
pr_debug("i2c-dev: adapter [%s] registered as minor %dn",
adap->name, adap->nr);
return 0;
error_destroy:
device_destroy(i2c_dev_class, MKDEV(I2C_MAJOR, adap->nr));
error:
return_i2c_dev(i2c_dev);
return res;
}
i2c_dev->dev = device_create(i2c_dev_class, &adap->dev, MKDEV(I2C_MAJOR, adap->nr), NULL, "i2c-%d", adap->nr);
使用主设备号和adapter中的总线号(作为次设备号)来创建名为i2c-x的设备节点。
static ssize_t i2cdev_write(struct file *file, const char __user *buf,
size_t count, loff_t *offset)
{
int ret;
char *tmp;
// 取出i2c_client
struct i2c_client *client = file->private_data;
if (count > 8192)
count = 8192;
tmp = kmalloc(count, GFP_KERNEL);
if (tmp == NULL)
return -ENOMEM;
// 拷贝用户数据到内核空间
if (copy_from_user(tmp, buf, count)) {
kfree(tmp);
return -EFAULT;
}
pr_debug("i2c-dev: i2c-%d writing %zu bytes.n",
iminor(file->f_path.dentry->d_inode), count);
// 发送I2C数据
ret = i2c_master_send(client, tmp, count);
kfree(tmp);
return ret;
}
以write函数为例,可以看到写数据通过ret = i2c_master_send(client, tmp, count);
完成的。
int i2c_master_send(struct i2c_client *client, const char *buf, int count)
{
int ret;
// 获取I2C适配器
struct i2c_adapter *adap = client->adapter;
struct i2c_msg msg;
// 封装I2C数据包
msg.addr = client->addr;
msg.flags = client->flags & I2C_M_TEN; // 发送标志位
msg.len = count;
msg.buf = (char *)buf;
// 发送I2C数据包
ret = i2c_transfer(adap, &msg, 1);
/* If everything went ok (i.e. 1 msg transmitted), return #bytes
transmitted, else error code. */
return (ret == 1) ? count : ret;
}
可以看到,经过I2C数据包的封装后,真正的最终写数据通过ret = i2c_transfer(adap, &msg, 1);
完成的。进入到i2c_transfer
函数,截取关键部分。
for (ret = 0, try = 0; try <= adap->retries; try ) {
// 调用具体的SoC的I2C总线驱动的发送方法
ret = adap->algo->master_xfer(adap, msgs, num);
if (ret != -EAGAIN)
break;
if (time_after(jiffies, orig_jiffies adap->timeout))
break;
}
山回路转不见君,雪上空留马行处。
adap->algo->master_xfer(adap, msgs, num);
终于回到了原点见到了I2C总线驱动层中定义的操作方法。
可以看到过程的确如上文所说,表现为从I2C总线驱动层自底向上后又由自顶向下的调用流程,简直一跃千里后又倾泻而下。
I2C特定设备驱动分析
笔者以S5PV210的E2PROM驱动为例讲解, 源码见github链接。
代码语言:javascript复制struct e2prom_device {
struct i2c_client *at24c02_client; /* I2C client(从设备) */
/* class和device用来自动创建设备节点 */
struct class *at24c02_class;
struct device *at24c02_device;
};
struct e2prom_device *e2prom_dev;
封装一个e2prom_device
结构体表示对E2PROM的抽象,其中包含I2C client(用来表示I2C从设备)以及class和device(这两者单纯是用来自动创建设备节点的)。
struct i2c_device_id e2prom_table[] = {
[0] = {
.name = "24c02",
.driver_data = 0,
},
[1] = {
.name = "24c08",
.driver_data = 0,
},
};
/* I2C设备驱动 */
struct i2c_driver e2prom_driver = {
.probe = e2prom_probe,
.remove = e2prom_remove,
.id_table = e2prom_table,
.driver = {
.name = "e2prom",
},
};
static int __init e2prom_init(void)
{
return i2c_add_driver(&e2prom_driver); /* 注册I2C设备驱动 */
}
先是调用i2c_add_driver
注册I2C设备驱动。根据上文在I2C核心层的源码分析可知,会通过在核心层中注册的i2c_bus_type
下的i2c_device_match
函数来匹配设备与驱动,一旦匹配上则会调用其i2c_device_probe
函数,而i2c_device_probe
函数又会调用i2c_driver的probe函数。注意如上文分析所知,client生成的原料为board_info,所以要使这个驱动成功匹配,需要在arch/arm/mach-s5pv210/mach-x210.c
中使用i2c_register_board_info
来注册board_info。接下去直奔prob函数进行分析。
struct file_operations e2prom_fops = {
.owner = THIS_MODULE,
.open = e2prom_open,
.write = e2prom_write,
.read = e2prom_read,
};
static int e2prom_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
int ret;
printk(KERN_INFO "e2prom probe!n");
e2prom_dev = kmalloc(sizeof(struct e2prom_device), GFP_KERNEL);
if (!e2prom_dev) {
printk(KERN_ERR "malloc failed!n");
return -ENOMEM;
}
e2prom_dev->at24c02_client = client;
/* 注册为字符设备驱动 */
ret = register_chrdev(E2PROM_MAJOR, "e2prom_module", &e2prom_fops);
if (ret < 0) {
printk(KERN_ERR "malloc failedn");
ret = -ENOMEM;
goto err0;
}
/* 创建类 */
e2prom_dev->at24c02_class = class_create(THIS_MODULE, "e2prom_class");
if (IS_ERR(e2prom_dev->at24c02_class)) {
printk(KERN_ERR "class create failed!n");
ret = PTR_ERR(e2prom_dev->at24c02_class);
goto err1;
}
/* 在类下创建设备 */
e2prom_dev->at24c02_device = device_create(e2prom_dev->at24c02_class, NULL, MKDEV(E2PROM_MAJOR, 0), NULL, "at24c08");
if (IS_ERR(e2prom_dev->at24c02_device)) {
printk(KERN_ERR "class create failed!n");
ret = PTR_ERR(e2prom_dev->at24c02_device);
goto err1;
}
return 0;
err1:
unregister_chrdev(E2PROM_MAJOR, "e2prom_module");
err0:
kfree(e2prom_dev);
ret
在probe函数中调用register_chrdev
函数来将E2PROM驱动注册为了字符设备驱动,并绑定了fops。然后调用class_create
和device_create
自动生成设备节点。
static int e2prom_open(struct inode *inode, struct file *file)
{
return 0;
}
open方法为空,以write方法为例讲解具体的操作,read方法类似。
代码语言:javascript复制static ssize_t e2prom_write(struct file *file, const char __user *buf,
size_t size, loff_t *offset)
{
int ret = 0;
char *tmp;
tmp = kmalloc(size, GFP_KERNEL);
if (tmp == NULL) {
printk(KERN_ERR "mallo failed!n");
return -ENOMEM;
}
/* 将用户空间数据拷贝到内核空间 */
ret = copy_from_user(tmp, buf, size);
if (ret) {
printk("copy data faile!n");
goto err0;
}
/* I2C write */
ret = i2c_write_byte(tmp, size);
if (ret) {
printk(KERN_ERR "wrtie byte failed!n");
goto err0;
}
kfree(tmp);
return size;
err0:
kfree(tmp);
return -EINVAL;
}
可以看到真正的操作I2C在i2c_write_byte
函数。
static int i2c_write_byte(char *buf, int count)
{
int ret = 0;
struct i2c_msg msg;
/* 封装I2C数据包 */
msg.addr = e2prom_dev->at24c02_client->addr; /* I2C从设备地址 */
msg.flags = 0; /* write flag */
msg.len = count; /* 数据长度 */
msg.buf = buf; /* 写入的数据 */
/* 调用I2C核心层提供的传输函数,其本质还是调用的I2C总线驱动(主机控制器驱动)层下实现的algo->master_xfe方法 */
ret = i2c_transfer(e2prom_dev->at24c02_client->adapter, &msg, 1);
if (ret < 0) {
printk(KERN_ERR "i2c transfer failed!n");
return -EINVAL;
}
return ret;
}
可以看到是调用在I2C核心层提供的传输函数,其本质还是在传输函数内部调用了跟具体SoC相关的I2C主机控制器操作方法中的传输方法。该函数接口需要提供一个i2c_msg
,所以对其进行了创建并填充,注意msg.flags = 0;
中0表示写,1表示读。
终了,撒花!!!✿✿✿ ~
本文作者: Ifan Tsai (菜菜)
本文链接: https://cloud.tencent.com/developer/article/2164591
版权声明: 本文采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。转载请注明出处!