linux设备模型____I2C具体实现

2024-05-09 22:38

本文主要是介绍linux设备模型____I2C具体实现,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

结合一个实际板子的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驱动真正具体实现的部分,将在后续继续探究。
 

这篇关于linux设备模型____I2C具体实现的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/974704

相关文章

Redis客户端连接机制的实现方案

《Redis客户端连接机制的实现方案》本文主要介绍了Redis客户端连接机制的实现方案,包括事件驱动模型、非阻塞I/O处理、连接池应用及配置优化,具有一定的参考价值,感兴趣的可以了解一下... 目录1. Redis连接模型概述2. 连接建立过程详解2.1 连php接初始化流程2.2 关键配置参数3. 最大连

Python实现网格交易策略的过程

《Python实现网格交易策略的过程》本文讲解Python网格交易策略,利用ccxt获取加密货币数据及backtrader回测,通过设定网格节点,低买高卖获利,适合震荡行情,下面跟我一起看看我们的第一... 网格交易是一种经典的量化交易策略,其核心思想是在价格上下预设多个“网格”,当价格触发特定网格时执行买

Oracle数据库定时备份脚本方式(Linux)

《Oracle数据库定时备份脚本方式(Linux)》文章介绍Oracle数据库自动备份方案,包含主机备份传输与备机解压导入流程,强调需提前全量删除原库数据避免报错,并需配置无密传输、定时任务及验证脚本... 目录说明主机脚本备机上自动导库脚本整个自动备份oracle数据库的过程(建议全程用root用户)总结

Linux如何查看文件权限的命令

《Linux如何查看文件权限的命令》Linux中使用ls-R命令递归查看指定目录及子目录下所有文件和文件夹的权限信息,以列表形式展示权限位、所有者、组等详细内容... 目录linux China编程查看文件权限命令输出结果示例这里是查看tomcat文件夹总结Linux 查看文件权限命令ls -l 文件或文件夹

idea的终端(Terminal)cmd的命令换成linux的命令详解

《idea的终端(Terminal)cmd的命令换成linux的命令详解》本文介绍IDEA配置Git的步骤:安装Git、修改终端设置并重启IDEA,强调顺序,作为个人经验分享,希望提供参考并支持脚本之... 目录一编程、设置前二、前置条件三、android设置四、设置后总结一、php设置前二、前置条件

python设置环境变量路径实现过程

《python设置环境变量路径实现过程》本文介绍设置Python路径的多种方法:临时设置(Windows用`set`,Linux/macOS用`export`)、永久设置(系统属性或shell配置文件... 目录设置python路径的方法临时设置环境变量(适用于当前会话)永久设置环境变量(Windows系统

Python对接支付宝支付之使用AliPay实现的详细操作指南

《Python对接支付宝支付之使用AliPay实现的详细操作指南》支付宝没有提供PythonSDK,但是强大的github就有提供python-alipay-sdk,封装里很多复杂操作,使用这个我们就... 目录一、引言二、准备工作2.1 支付宝开放平台入驻与应用创建2.2 密钥生成与配置2.3 安装ali

Spring Security 单点登录与自动登录机制的实现原理

《SpringSecurity单点登录与自动登录机制的实现原理》本文探讨SpringSecurity实现单点登录(SSO)与自动登录机制,涵盖JWT跨系统认证、RememberMe持久化Token... 目录一、核心概念解析1.1 单点登录(SSO)1.2 自动登录(Remember Me)二、代码分析三、

C#中的Converter的具体应用

《C#中的Converter的具体应用》C#中的Converter提供了一种灵活的类型转换机制,本文详细介绍了Converter的基本概念、使用场景,具有一定的参考价值,感兴趣的可以了解一下... 目录Converter的基本概念1. Converter委托2. 使用场景布尔型转换示例示例1:简单的字符串到

PyCharm中配置PyQt的实现步骤

《PyCharm中配置PyQt的实现步骤》PyCharm是JetBrains推出的一款强大的PythonIDE,结合PyQt可以进行pythion高效开发桌面GUI应用程序,本文就来介绍一下PyCha... 目录1. 安装China编程PyQt1.PyQt 核心组件2. 基础 PyQt 应用程序结构3. 使用 Q