结合一个实际板子的I2C驱动,进一步加深对linux设备模型的理解。此外,I2C子系统本身也是linux驱动部分的一个重点,本文也会描述。
这个板子是以一个marvell芯片为CPU的一个路由器,CPU已集成了硬件的I2C控制器。对于I2C设备驱动部分,它的步骤基本如下:
1、 将包括了marvell的I2C控制器参数内容的设备注册在platform总线下;I2C控制器参数包括物理寄存器地址及范围、中断号、频率、超时值等一些参数;
2、 将相应的marvell的I2C控制器的驱动注册在platform总线下;驱动的重要内容就是其probe函数,创建该驱动后会触发这个probe函数,它将用第1步注册的设备的I2C控制器参数进行物理地址<--->内核虚拟地址映射、配置寄存器初始化、挂中断等初始化操作,最后会在I2C总线下创建I2C适配器(i2c-0)设备;
3、 之后内核将创建字符设备,并创建一个叫i2c-dev的类,然后在I2C总线下创建一个驱动,该驱动会触发其attach_adapter成员函数,这个函数会使用第2步创建的I2C适配器的参数,在刚创建的class下创建一个设备i2c-0;
4、 至此,I2C驱动的创建部分全部完成,用户态可以通过访问/dev/i2c-0的字符设备,通过该字符设备的fops提供的open、close、read、write、ioctl方法,通过linux的I2C驱动机制,再控制I2C适配器来间接地控制I2C硬件设备。
下面是具体步骤描述:
1、 platform_device_register(&kw_i2c);
这一步是I2C适配器的设备的注册,下面是全局变量kw_i2c的内容:
因为要注册到platform总线下,所以kw_i2c是platform_device型变量,它有resource来承载I2C适配器的一些硬件参数,而有些硬件参数无法以struct resource结构描述时,使用struct device结构的platform_data成员承载,这里也用到了这种方法;
static struct platform_device kw_i2c = {
//宏MV64XXX_I2C_CTLR_NAMEdefine为“mv64xxx_i2c”
.name = MV64XXX_I2C_CTLR_NAME,
.id = 0,
.num_resources = ARRAY_SIZE(kw_i2c_resources),
.resource = kw_i2c_resources,
.dev = {
.platform_data = &kw_i2c_pdata,
},
};
可以用struct resource结构描述的I2C适配器参数,包括寄存器地址及范围和中断;
static struct resource kw_i2c_resources[] =
{
{
.name = "i2c base",
.start = INTER_REGS_BASE + MV_TWSI_SLAVE_REGS_OFFSET(0),
.end = INTER_REGS_BASE + MV_TWSI_SLAVE_REGS_OFFSET(0) + 0x20 -1,
.flags = IORESOURCE_MEM,
},
{
.name = "i2c irq",
.start = TWSI_IRQ_NUM(0),
.end = TWSI_IRQ_NUM(0),
.flags = IORESOURCE_IRQ,
},
};
无法用struct resource结构描述的I2C适配器参数,需要借用struct device结构体的platform_data成员;
static struct mv64xxx_i2c_pdata kw_i2c_pdata =
{
.freq_m = 8, /* assumes 166 MHz TCLK */
.freq_n = 3,
.timeout = 1000, /* Default timeout of 1 second */
};
通过函数platform_device_register把这个名字叫mv64xxx_i2c.0的设备(因为kw_i2c的id成员值为0,所以名字为mv64xxx_i2c.0,详见platform_device_register函数调用的platform_device_add函数的实现,只有在id成员值为-1时才不改原名字)注册到platform总线,此时,在sysfs下可以看到/sys/bus/platform/devices/mv64xxx_i2c.0/目录。
该设备在注册设备期间是找不到驱动的,即bus_probe_device将返回,这是因为这时候还没有任何匹配的驱动注册在platform总线下,I2C驱动作为module将在之后才加载,而kw_i2c设备的加载是在内核初始化时完成。
2、 platform_driver_register(&mv64xxx_i2c_driver);
这一步是I2C适配器的驱动的注册,但是现在必须说明的是,这个驱动并不是以后内核态真正使用的I2C设备驱动,它只是执行硬件的初始化,然后利用linux设备模型的机制,把第1步的I2C适配器硬件参数信息转移到将要在第3、4步的内容中;
platform_driver_register(&mv64xxx_i2c_driver);
通过把I2C适配器的驱动注册到platform总线之后,会触发该驱动的probe函数,该驱动内容如下:
static struct platform_driver mv64xxx_i2c_driver = {
.probe = mv64xxx_i2c_probe,
.remove = __devexit_p(mv64xxx_i2c_remove),
.driver = {
.owner = THIS_MODULE,
//宏MV64XXX_I2C_CTLR_NAMEdefine为“mv64xxx_i2c”
.name = MV64XXX_I2C_CTLR_NAME,
},
};
宏MV64XXX_I2C_CTLR_NAME也是define为“mv64xxx_i2c”,即和第1步的设备名字一样,这就会触发它的probe函数执行,这个probe函数不仅会从第1步注册的设备获取到全部硬件参数,还最终会在I2C总线下建立一个适配器设备,这个是最关键的地方;它的probe函数是:
mv64xxx_i2c_probe(struct platform_device *pd)
{
struct mv64xxx_i2c_data *drv_data;
//由第1步注册的设备,获取到其struct device结构的platform_data参数
struct mv64xxx_i2c_pdata *pdata = pd->dev.platform_data;
int rc;
if ((pd->id != 0) || !pdata)
return -ENODEV;
drv_data = kzalloc(sizeof(struct mv64xxx_i2c_data), GFP_KERNEL);
if (!drv_data)
return -ENOMEM;
//物理内容<--->内核虚拟地址
if (mv64xxx_i2c_map_regs(pd, drv_data)) {
rc = -ENODEV;
goto exit_kfree;
}
//设置即将要在I2C总线建立的适配器设备的name成员值为“mv64xxx_i2c adapter”,注意:这个名字不是要创建的设备的kobject的名字!这个name成员只是它的一个属性
strlcpy(drv_data->adapter.name, MV64XXX_I2C_CTLR_NAME " adapter",
sizeof(drv_data->adapter.name));
init_waitqueue_head(&drv_data->waitq);
spin_lock_init(&drv_data->lock);
//配置获取到的struct device结构的platform_data参数
drv_data->freq_m = pdata->freq_m;
drv_data->freq_n = pdata->freq_n;
//由第1步注册的设备,获取到中断号
drv_data->irq = platform_get_irq(pd, 0);
if (drv_data->irq < 0) {
rc = -ENXIO;
goto exit_unmap_regs;
}
//设置I2C适配器的设备的父亲为第1步创建的设备即/sys/platform/mv64xxx_i2c.0
drv_data->adapter.dev.parent = &pd->dev;
//设置I2C适配器的算法成员,这个是linux的I2C驱动的核心
drv_data->adapter.algo = &mv64xxx_i2c_algo;
drv_data->adapter.owner = THIS_MODULE;
drv_data->adapter.class = I2C_CLASS_HWMON | I2C_CLASS_SPD;
drv_data->adapter.timeout = msecs_to_jiffies(pdata->timeout);
//设置I2C适配器的次设备号成员,为0
drv_data->adapter.nr = pd->id;
platform_set_drvdata(pd, drv_data);
i2c_set_adapdata(&drv_data->adapter, drv_data);
//初始化,配置硬件寄存器
mv64xxx_i2c_hw_init(drv_data);
//挂I2C中断
if (request_irq(drv_data->irq, mv64xxx_i2c_intr, 0,
MV64XXX_I2C_CTLR_NAME, drv_data)) {
dev_err(&drv_data->adapter.dev,
"mv64xxx: Can't register intr handler irq: %d\n",
drv_data->irq);
rc = -EINVAL;
goto exit_unmap_regs;
}
//最重要的地方,在I2C总线下创建设备
else if ((rc = i2c_add_numbered_adapter(&drv_data->adapter)) != 0) {
dev_err(&drv_data->adapter.dev,
"mv64xxx: Can't add i2c adapter, rc: %d\n", -rc);
goto exit_free_irq;
}
return 0;
exit_free_irq:
free_irq(drv_data->irq, drv_data);
exit_unmap_regs:
mv64xxx_i2c_unmap_regs(drv_data);
exit_kfree:
kfree(drv_data);
return rc;
}
下面开始分析函数i2c_add_numbered_adapter,它主要是调用i2c_register_adapter函数,参数就是刚才的适配器结构(&drv_data->adapter),该函数主要操作是:
//所要创建的I2C适配器设备的kobject的名字为i2c-0
dev_set_name(&adap->dev, "i2c-%d", adap->nr);
//它的设备所属总线是I2C总线(i2c_bus_type),设备类型为适配器型(i2c_adapter_type);
adap->dev.bus = &i2c_bus_type;
adap->dev.type = &i2c_adapter_type;
//在I2C总线下注册该设备
res = device_register(&adap->dev);
至此,在I2C总线上注册了一个名字叫i2c-0的设备,但它有可能找不到驱动,因为这时内核在I2C总线上自动创建的驱动很可能还未创建;
注意,前两步是驱动编写者需要完成的工作;之所以要在I2C总线上创建I2C适配器的设备,是为了把第1步创建的I2C适配器的参数传递到I2C总线,后面内核会自动创建一个驱动,它将利用I2C适配器的参数创建最终用户态访问的设备;这就达到了用户态可以间接的访问I2C适配器的效果;
3、 i2c_dev_init (void) //在内核源码的/driver/i2c/i2c-dev.c文件中
这一步是要在I2C总线创建一个驱动,第2步已经在I2C总线上创建了名字为i2c-0的设备,这就会触发这个驱动的attach_adapter函数,该函数将取得i2c-0设备的适配器相关硬件参数,然后创建一个由用户态访问的设备,达到用户态代码访问内核时,内核代码能够调用到第1步创建的I2C适配器的相关函数;
res = register_chrdev(I2C_MAJOR, "i2c", &i2cdev_fops);
创建一个字符设备,这是为了给用户态代码访问/dev/i2c-0设备使用的,它的fops也由内核代码提供,有完整的open、release、read、write、ioctl方法;但需注意,它的主设备号I2C_MAJOR值为89,这个值是由内核提供的,也就是永远为89,这一点和后面在类下建立设备还会有关系,很重要!这个字符设备是通过后面的class_creat和device_create实现在/dev/下注册,而不是手动mknod;
static const struct file_operations i2cdev_fops = {
.owner = THIS_MODULE,
.llseek = no_llseek,
.read = i2cdev_read,
.write = i2cdev_write,
.unlocked_ioctl = i2cdev_ioctl,
.open = i2cdev_open,
.release = i2cdev_release,
};
然后是建立一个叫i2c-dev的class;这是为了后面能够在/dev/下注册字符设备;
i2c_dev_class = class_create(THIS_MODULE, "i2c-dev");
最后是注册一个驱动,这个驱动就是为了触发该驱动的attach_adapter函数,进而创建给用户态访问用的/dev/的设备文件;
res = i2c_add_driver(&i2cdev_driver);
全局变量i2cdev_driver如下所示,它的名字叫dev_driver;
static struct i2c_driver i2cdev_driver = {
.driver = {
.name = "dev_driver",
},
.attach_adapter = i2cdev_attach_adapter,
.detach_adapter = i2cdev_detach_adapter,
};
i2c_add_driver函数将实际调用i2c_register_driver,分析这个函数,
int i2c_register_driver(struct module *owner, struct i2c_driver *driver)
{
int res;
/* Can't register until after driver model init */
if (unlikely(WARN_ON(!i2c_bus_type.p)))
return -EAGAIN;
/* add the driver to the list of i2c drivers in the driver core */
driver->driver.owner = owner;
//这个驱动也挂在I2C总线
driver->driver.bus = &i2c_bus_type;
/* When registration returns, the driver core
* will have called probe() for all matching-but-unbound devices.
*/
//该驱动将被注册到I2C总线下,因为它的名字是dev_driver,所以它这时匹配不到任何设备,它的匹配在后面
res = driver_register(&driver->driver);
if (res)
return res;
pr_debug("i2c-core: driver [%s] registered\n", driver->driver.name);
INIT_LIST_HEAD(&driver->clients);
/* Walk the adapters that are already present */
mutex_lock(&core_lock);
//它是在这里匹配设备的,注意,这里的匹配方式不依靠任何名字,而是直接匹配,这样将立即找到第2步在I2C总线创建的设备i2c-0;
bus_for_each_dev(&i2c_bus_type, NULL, driver, __attach_adapter);
mutex_unlock(&core_lock);
return 0;
}
可以看看函数__attach_adapter的实现,它的匹配方式就是一个:设备的类型必须是适配器类型i2c_adapter_type,这将匹配到第2步创建的设备i2c-0;
static int __attach_adapter(struct device *dev, void *data)
{
struct i2c_adapter *adapter;
struct i2c_driver *driver = data;
//匹配方式:设备的类型必须是适配器类型i2c_adapter_type
if (dev->type != &i2c_adapter_type)
return 0;
adapter = to_i2c_adapter(dev);
i2c_detect(adapter, driver);
/* Legacy drivers scan i2c busses directly */
//调用该驱动的attach_adapter函数
if (driver->attach_adapter)
driver->attach_adapter(adapter);
return 0;
}
这个驱动的attach_adapter函数如下:它将在第3步开始建立的名字为i2c-dev的class下建立设备,这将在/dev/目录下建立设备文件,注意看device_create还是的最后两个参数,它们是用来设置该设备的名称的,最后一个参数前面赋值为0,说明该设备的名称还是i2c-0,另外,主设备号依然是宏I2C_MAJOR,和前面的字符设备注册的值是一样的,这样将在/sys/class/i2c-dev/和/dev/目录下建立设备文件i2c-0,主次设备号分别为89、0;/dev/目录下的i2c-0文件将用于用户态代码访问字符设备使用;
static int i2cdev_attach_adapter(struct i2c_adapter *adap)
{
struct i2c_dev *i2c_dev;
int res;
//动态分配一个struct i2c_dev类型的变量
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 */
//在类i2c-dev下建立设备,主次设备号为89、0,设备名字为i2c-0,这还会在/dev/目录下创建一个同名字的设备文件,这个文件将用于用户态代码访问字符设备使用;
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 %d\n",
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/i2c-0的字符设备,通过该字符设备的fops提供的open、close、read、write、ioctl方法,通过linux的I2C驱动机制,再控制I2C适配器来间接地控制I2C硬件设备
4、 具体I2C访问方式:
现在可以看看,用户态是怎么访问I2C硬件设备的,下面是个用户态代码:
file = open("/dev/i2c-0", O_RDWR)
首先打开/dev/下设备文件;
i2c_smbus_read_byte(file)
这是一个用户态上层函数,它的实现是:
static inline __s32 i2c_smbus_read_byte(int file)
{
union i2c_smbus_data data;
//参数2、4的宏值均为1
if (i2c_smbus_access(file, I2C_SMBUS_READ, 0, I2C_SMBUS_BYTE, &data))
return -1;
else
return 0x0FF & data.byte;
}
再看看函数i2c_smbus_access的实现,
static inline __s32 i2c_smbus_access(int file, char read_write, __u8 command,
int size, union i2c_smbus_data *data)
{
struct i2c_smbus_ioctl_data args;
args.read_write = read_write; //值为I2C_SMBUS_READ,为1
args.command = command; //值为0
args.size = size; //值为I2C_SMBUS_BYTE,为1
args.data = data; //用于返回值
return ioctl(file,I2C_SMBUS,&args);//ioctl方法为I2C_SMBUS
}
现在看看设备i2c-0的fops提供的ioctl方法中的I2C_SMBUS的case的情况:
case I2C_SMBUS:
return i2cdev_ioctl_smbus(client, arg);
函数i2cdev_ioctl_smbus是由内核提供,其实现很长,涉及本次操作的是:
i2c_smbus_xfer(client->adapter, client->addr, client->flags,
data_arg.read_write, data_arg.command, data_arg.size, &temp);
注意,上面的client,是由文件系统通过用户态代码打开操作的文件得到的参数:
static long i2cdev_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
struct i2c_client *client = (struct i2c_client *)file->private_data;
…………….
}
client变量的结构类型如下,由此我们可知,文件系统通过用户态打开的文件,得到的client变量,包含内容:i2c-0设备的适配器指针adapter、i2c-0设备的驱动driver、芯片主地址addr、i2c-0设备的设备指针dev、flag(含义暂未知);这说明,内核能知道打开的这个设备文件背后的重要参数!
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 i2c_driver *driver; /* and our access routines */
struct device dev; /* the device structure */
int irq; /* irq issued by device */
struct list_head detected;
};
继续看函数i2c_smbus_xfer的情况,注意它是内核提供的函数,它的函数实现是:
s32 i2c_smbus_xfer(struct i2c_adapter *adapter, u16 addr, unsigned short flags,
char read_write, u8 command, int protocol,
union i2c_smbus_data *data)
{
unsigned long orig_jiffies;
int try;
s32 res;
flags &= I2C_M_TEN | I2C_CLIENT_PEC;
//存在驱动编写者编写的“算法”函数的条件下,这个“算法”函数在第2步挂在了适配器指针中,即全局变量mv64xxx_i2c_algo
if (adapter->algo->smbus_xfer) {
mutex_lock(&adapter->bus_lock);
/* Retry automatically on arbitration loss */
orig_jiffies = jiffies;
for (res = 0, try = 0; try <= adapter->retries; try++) {
//这里将调用第2步挂的“算法”函数,即全局变量mv64xxx_i2c_algo下的函数,这是驱动编写者实现的函数,可见,内核让用户态代码和实际的适配器驱动联系起来了!
res = adapter->algo->smbus_xfer(adapter, addr, flags,
read_write, command,
protocol, data);
if (res != -EAGAIN)
break;
if (time_after(jiffies,
orig_jiffies + adapter->timeout))
break;
}
mutex_unlock(&adapter->bus_lock);
} else
res = i2c_smbus_xfer_emulated(adapter,addr,flags,read_write,
command, protocol, data);
return res;
}
上面是一个用户态的例子,再如read、write等,都是一样的道理。
至此说明,至少对于上面的这种方式的I2C驱动,驱动编写者需要编写的是,I2C适配器的驱动,把它的设备和驱动都挂在platform总线上即可,编写重点其实是“算法”函数,在这里就是全局变量mv64xxx_i2c_algo,因为最后内核部分调用的是这里的成员函数。
5、 总结:
这种方式的I2C驱动,如下图所示:虚线内的由驱动编写者实现,右半部分是内核自己完成,其实驱动编写者只需编写适配器驱动和用户态代码,驱动代码的核心是“算法”的函数实现,具体I2C传输的算法部分,是I2C驱动真正具体实现的部分,将在后续继续探究。