故事背景来源于一个丑陋的Fixup。
故事详情是项目中使用到一个温度传感器TMP75,这个东西使用的如此广泛以至于内核中早就有他的支持了,当然名字不叫tmp75.c,而是lm75.c。直接选为module,然后编译,加载,发现不能正常注册上去。
将I2C的Debug选项打开,看到设备与控制器的交互过程,在lm75_detect()中读取TMP75的4号寄存器,控制器返回"Error: no response!"的错误。 打开手册,没有看到有这个寄存器,于是将不存在的这些寄存器访问都注销掉。Driver正常注册,然后通过/sys/class/hwmon/hwmon0/能访问到温度值,读取的值确认也是正确的。

故事到这里才刚刚开始,调试时直接通过将其注销掉的,现在回去看看这地方的为什么会有这么"二"的访问呢。
代码中这个地方有注释,位置在drivers/hwmon/lm75.c中的lm75_detect(),大体意思就是LM75芯片没有ID寄存器,不能通过ID确认在这个地址上的是不是这款芯片。于是想了两个蛋疼的测试来间接确认这款芯片:

  • 第一个测试为读取芯片的内部寄存器4,5,6,7会返回上一次有效读取的值,
  • 第二个测试就是内部寄存器号的bit7-bit3变化不会影响到可用寄存器的访问。 (补注:新版本的内核已经有说明除了LM75和LM75A,其他兼容的芯片不支持这种方式的探测)

看了TMP75的手册,从来就没有这样的描述。只能确定这个是一个hack的处理办法,而悲摧的事情是TMP75配合SB710的SMBus控制器不是这样的结果。只能简单的将这段代码使用#if 0 ..... #endif的方式注销掉。 提交patch的时候,看到这样的修改真的蛮恶心的。想到在ARM平台的时候I2C设备都是通过名字进行匹配,看看是否能够修改成这种方式进行处理。于是将I2C的子系统看了一遍,确定应该能够通过这种方式处理的!

在I2C的整个子系统中,定义了四种软件结构:

  • 一种是描述I2C Master的,称为Adapter
  • 一种是描述关于I2C Slave设备,称为i2c_board_info
  • 一种是控制设备驱动,称为I2C Driver
  • 一种是将I2C Master以及I2C Slave设备信息整合在一起用于操作的结构,称为I2C Client

Adapter实现master设备的操作,即完成Driver的读取,写入slave设备的请求。
i2c_board_info描述了i2c slave设备使用的地址,设备名称,如果有中断,还包括了中断号等等
Driver中完成对设备功能的实现,如TMP75,Driver实现对温度的读取以及将结果通过sysfs导出到用户空间
I2C Client主要是将设备以及该设备对因的Adapter连接在一起,供Driver使用的结构;

本来将I2C匹配看完就差不多能够完成功能,但是既然题目是发生在Kernel中的故事,那么还得有头有尾才行,不然就成了挖坑工了。
如果你看过二十本武侠小说,大致能够理出的一个故事套路就是: 主角在前十几或者二十几年中苦练绝技,功成后开始游历江湖,然后碰到很多很多人,做了很多很多行侠仗义的事情,展开了一场波澜壮阔的人生画卷,最后几乎都是左拥右抱钱袋鼓的样子结束。套用到我们这里来就是:

  • 先期我们得构建好几个数据结构I2C Driver,I2C Client,相关的I2C Adapter,这就是打基础。
  • 将这些结构通过相关的register方法,注册到设备模型中去,这就是出山。
  • 然后想办法让I2C Device与I2C Adapter相遇,构成一个I2C Client;
  • 然后还得让I2C Client与I2C Driver相遇,然后能够在I2C Driver中使用I2C Client处理设备读取相关的东西,这就是故事重要部分;
  • 最后确定这个Driver只是工作在Kernel Mode还是需要用户空间程序去控制,收尾。
  • 如果你要展示英雄迟暮的悲凉,你还得关注unregister以及remove,module_exit()等等东西。

1 构造数据结构

1.1 struct i2c_adapter

下面是该结构的定义,在include/liunux/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;
        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;
};

owner

可以直接赋为THIS_MODULE, 这个域能够避免模块在操作的时候被卸载掉。  

class

如果Adapter需要支持自动扫描的I2C Driver,这个就需要进行设置,I2C Driver只能使用同种CLASS的Adapter进行自动探测。可选的值有:
#define I2C_CLASS_HWMON         (1<<0)  /* lm_sensors, ... */
#define I2C_CLASS_DDC           (1<<3)  /* DDC bus on graphics adapters */
#define I2C_CLASS_SPD           (1<<7)  /* Memory modules */

algo

实现了具体的数据传输的结构体指针。struct i2c_algorithm结构如下
struct i2c_algorithm {
        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 *);
};
一般实现smbus_xfer即可,如果有一些特定的I2C操作可能就需要实现master_xfer的操作。    
functionality也是一个比较重要的函数,他返回主设备能够完成哪些I2C操作以及支持的特性。在I2C Driver中,我们需要判断Adapter支持哪种传输方式,然后能够挑选一个比较优化的方式进行传输。部分值
#define I2C_FUNC_SMBUS_BYTE_DATA        (I2C_FUNC_SMBUS_READ_BYTE_DATA | \
                                         I2C_FUNC_SMBUS_WRITE_BYTE_DATA)
#define I2C_FUNC_SMBUS_WORD_DATA        (I2C_FUNC_SMBUS_READ_WORD_DATA | \
                                         I2C_FUNC_SMBUS_WRITE_WORD_DATA)
#define I2C_FUNC_SMBUS_BLOCK_DATA       (I2C_FUNC_SMBUS_READ_BLOCK_DATA | \
                                         I2C_FUNC_SMBUS_WRITE_BLOCK_DATA)

name

定义了adapter的名字。

nr

用于创建bus号的,即在/sys/bus/i2c/devices/i2c-xx(xx代表数值从0开始递增) 看到的数值。

例子:
drivers/i2c/busses/i2c-piix4.c piix4_adapter 定义

1.2 struct i2c_board_info

结构体如下 include/linux/i2c.h

struct i2c_board_info { 
        char            type[I2C_NAME_SIZE]; 
        unsigned short  flags; 
        unsigned short  addr; 
        void            *platform_data; 
        struct dev_archdata     *archdata; 
        struct device_node *of_node; 
        int             irq; 
};

type

slave设备名称,会被赋值给client的name字段,也用于匹配I2C Driver中id_table 必须的  

flags

标志,会拷贝到i2c_client.flags中,这个可以标志一些设备特性比如是否需要10地址支持等 可选  

addr

slave设备的I2C Address   必须的   

platfrom_data

会将其拷贝到i2c_client.dev.platfrom_data 可选  

archdata

会将其拷贝到i2c_client.dev.archdata 可选   

irq

如果有中断,可填上中断号 可选 

例子:
arch/mips/alchemy/board-gpr.c gpr_i2c_info[]

1.3 struct i2c_client

看看struct i2c_client的数据结构 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 i2c_driver *driver;      /* and our access routines      */
        struct device dev;              /* the device structure         */
        int irq;                        /* irq issued by device         */
        struct list_head detected;
};

flags

与i2c_board_info中的flags相同  

addr

与i2c_board_info中addr相同    

name

与i2c_board_info中type相同,slave设备的名字  

adapter

对应的I2C Master Driver   

driver

对应的I2C Driver   

例子:
参考i2c_new_devices()的实现,一般来说我们不会直接会填充一个i2c_client的结构体的

1.4 struct i2c_driver

再看看struct i2c_driver的实现

struct i2c_driver {
    unsigned int class;
    int (*attach_adapter)(struct i2c_adapter *) __deprecated;
    int (*detach_adapter)(struct i2c_adapter *) __deprecated;
    int (*probe)(struct i2c_client *, const struct i2c_device_id *);
    int (*remove)(struct i2c_client *);
    void (*shutdown)(struct i2c_client *);
    int (*suspend)(struct i2c_client *, pm_message_t mesg);
    int (*resume)(struct i2c_client *);
    void (*alert)(struct i2c_client *, unsigned int data);
    int (*command)(struct i2c_client *client, unsigned int cmd, void *arg);
    struct device_driver driver;
    const struct i2c_device_id *id_table;
    int (*detect)(struct i2c_client *, struct i2c_board_info *);
    const unsigned short *address_list;
    struct list_head clients;
};

class

与Adapter中class含义一样,这里代表Driver属于哪个类别的。
如果Driver要求自动探测的话,则需要对应的Adapter class含有这个类别。

attach_adapter

用于寻找对应的adapter,然后生成i2c_client。
这个API的局限性就是需要预先知道slave设备将使用哪个Adapter。  

detach_adapter

用于解除对应的引用 可以看到这两个API已经标志了__deprecated,所以我们写的时候需要避免试用这两个API  

probe, remove

标准的设备驱动模型,如果用匹配的device,则会调用probe函数,注销的时候会调用remove  

driver

这个域中name代表驱动名,如果与模块名一致,那么系统能够通过Dbus动态加载它  

id_table

支持的slave设备名称列表,用于标准驱动设备模型中设备匹配  

detect

这个函数用于自动匹配。

address_list

slave设备可能用到的地址,用于自动匹配的  

例子: drivers/hwmon/lm75.c 中lm75_driver 的定义

2 匹配的种种情况

接下来看看设备与驱动的绑定情况。
I2C子系统中设备与驱动的匹配大体有两种情况。

2. 1 通过Linux设备驱动模型进行匹配

通过注册的设备名称与Driver中声明支持的设备名称(id_tables中定义的)是否匹配来完成的。这适合预先已经知道系统中有什么设备的情况。 通过两种方法能够实现,其主要的思路就是想法构建一个合适的i2c_client结构体
一种编程模型:

Example (from omap2 h4):

static struct i2c_board_info __initdata h4_i2c_board_info[] = {
        {
                I2C_BOARD_INFO("isp1301_omap", 0x2d),
                .irq            = OMAP_GPIO_IRQ(125),
        },
        {       /* EEPROM on mainboard */
                I2C_BOARD_INFO("24c01", 0x52),
                .platform_data  = &m24c01,
        },
        {       /* EEPROM on cpu card */
                I2C_BOARD_INFO("24c01", 0x57),
                .platform_data  = &m24c01,
        },
};

static void __init omap_h4_init(void)
{
        (...)
        i2c_register_board_info(1, h4_i2c_board_info,
                        ARRAY_SIZE(h4_i2c_board_info));
        (...)
}

通过i2c_board_info 声明了三个I2C slave设备,然后通过i2c_register_board_info()会生成三个对应的i2c_clinet,需要注意到 i2c_register_board_info()中第一个参数是指的bus num,这就要求对应的Adapter需要固定注册到这个位置。这在嵌入式系统中是比较常见。 这种情况适合即知道I2C Slave设备又知道I2C Master设备情况

第二种编程模型为:

Example (from the sfe4001 network driver):

static struct i2c_board_info sfe4001_hwmon_info = {
        I2C_BOARD_INFO("max6647", 0x4e),
};

int sfe4001_init(struct efx_nic *efx)
{
        (...)
        efx->board_info.hwmon_client =
                i2c_new_device(&efx->i2c_adap, &sfe4001_hwmon_info);

        (...)
}

这种情况适合我们不能提前知道bus num的情况,但是知道有什么设备。还有一种改进型的编程模型

Example (from the nxp OHCI driver):

static const unsigned short normal_i2c[] = { 0x2c, 0x2d, I2C_CLIENT_END };

static int __devinit usb_hcd_nxp_probe(struct platform_device *pdev)
{
        (...)
        struct i2c_adapter *i2c_adap;
        struct i2c_board_info i2c_info;

        (...)
        i2c_adap = i2c_get_adapter(2);
        memset(&i2c_info, 0, sizeof(struct i2c_board_info));
        strlcpy(i2c_info.type, "isp1301_nxp", I2C_NAME_SIZE);
        isp1301_i2c_client = i2c_new_probed_device(i2c_adap, &i2c_info,
                                                   normal_i2c, NULL);
        i2c_put_adapter(i2c_adap);
        (...)
}

这种情况适合不知道设备地址在哪个上面的情况,有可能是0x2c,也有可能是0x2d的。i2c_new_probed_device()只会创建一个有效的client或者返回NULL。这跟后面第2种方式的detect有区别。

2.2 通过对设备驱动模型扩展的方式进行匹配

这种方法主要是解决不知道系统中是否有这样的设备而设计的,比如对PC上SMBus Monitor设备支持。实现方法就是在I2C Driver中实现Address_list以及Detect方法。在调用i2c_add_driver()函数中会创建一个临时的I2C Client,然后使用Adress_list定义的地址以SMBus字节读方式挨着访问一遍,如果正常回应则是有设备,然后再通过Detect方法,进一步判断是否为支持的设备,如果返回成功,则就是有这样的设备了。这个时候开始构造一个I2C Client,作为Driver中I2C操作的Client。在这个模型中如果有多个地址有效,则会创建多个i2c_client的。

Example : 
drivers/hwmon/lm75.c lm75_detect()

这种模型中,在detect方法中i2c_client结构体是一个临时的,而probe中的传进来的i2c_client就能够用于整个通信了。

这不仅仅是一场的约会,而是一生的相守。

3 工作方式,Kernel Mode还是需要用户空间去控制

如果是Kernel Mode话仅仅调用smbus_read_byte_data()就可以了。 如果要提供用户态的控制就得挑选方法,或者通过sysfs,procfs,或者ioctl,read,write甚至是netlink方式。

4 后面的一切

其实看到这里,你会发现全篇有点扯淡的感觉。为了不至于太浪费生命,后面的unregister以及remove就省略了。看看例子就能解决了。

5 故事的结局

因为知道系统中确实有这样的设备,故通过第一种方式能够实现匹配,而不用去修改lm75.c中代码了。最后修正的样子如下:

static int __devinit tmp75_probe(struct platform_device *dev)
{
    struct i2c_adapter *adapter = NULL;
    struct i2c_board_info info;
    int i = 0, found = 0;

    memset(&info, 0, sizeof(struct i2c_board_info));

    adapter = i2c_get_adapter(i++);
    while (adapter) {
        if (strncmp(adapter->name, "SMBus PIIX4", 11) == 0) {
            found = 1;
            break;
        }

        adapter = i2c_get_adapter(i++);
    }

    if (!found)
        goto fail;

    info.addr = TMP75_SMB_ADDR;
    info.platform_data = "TMP75 Temprature Sensor";
    /* name should match drivers/hwmon/lm75.c id_table */
    strncpy(info.type, "tmp75", I2C_NAME_SIZE);

    tmp75_client = i2c_new_device(adapter, &info);
    if (tmp75_client == NULL) {
        printk(KERN_ERR "failed to attach tmp75 sensor\n");
        goto fail;
    }

    printk(KERN_INFO "Success to attach TMP75 sensor\n");

    return 0;
fail:
    printk(KERN_ERR "Fail to fount smbus controller attach TMP75 sensor\n");

    return 0;
}

6 最好的文档

这篇文章主要是个人学习的一个总结。其实关于I2C的文档,内核源码中 Documentation/i2c/下面就有,写的也非常详细了,如果本文与他们有冲突的话,请以内核文档为准。并能够来信提醒我。

用xelatex制作了一个PDF版本,点这里下载



blog comments powered by Disqus