I2C EEPROM驱动实例分析

2019独角兽企业重金招聘Python工程师标准>>> hot3.png

    上篇分析了Linux Kernel中的I2C驱动框架,本篇举一个具体的I2C设备驱动(eeprom)来对I2C设备驱动有个实际的认识。

    s3c24xx系列集成了一个基于I2C的eeprom设备at24cxx系列。at24cxx系列芯片包含at24c01, at24c02, at24c04, at24c08, at24c16 等,其中xx代表芯片可寻址范围,如01代表1kB,02代表2kB,如此类推。at24xx系列芯片还支持多地址寻址功能,即,支持多个地址芯片。

    直接分析代码:drivers/misc/eeprom/at24.c

    1,驱动注册:

static const struct i2c_device_id at24_ids[] = {    // at24设备驱动程序支持at24cxx全系列驱动
    /* needs 8 addresses as A0-A2 are ignored */
    { "24c00", AT24_DEVICE_MAGIC(128 / 8, AT24_FLAG_TAKE8ADDR) },
    /* old variants can't be handled with this generic entry! */
    { "24c01", AT24_DEVICE_MAGIC(1024 / 8, 0) },
    { "24c02", AT24_DEVICE_MAGIC(2048 / 8, 0) },
    /* spd is a 24c02 in memory DIMMs */
    { "spd", AT24_DEVICE_MAGIC(2048 / 8,
        AT24_FLAG_READONLY | AT24_FLAG_IRUGO) },
    { "24c04", AT24_DEVICE_MAGIC(4096 / 8, 0) },
    /* 24rf08 quirk is handled at i2c-core */
    { "24c08", AT24_DEVICE_MAGIC(8192 / 8, 0) },
    { "24c16", AT24_DEVICE_MAGIC(16384 / 8, 0) },
    { "24c32", AT24_DEVICE_MAGIC(32768 / 8, AT24_FLAG_ADDR16) },
    { "24c64", AT24_DEVICE_MAGIC(65536 / 8, AT24_FLAG_ADDR16) },
    { "24c128", AT24_DEVICE_MAGIC(131072 / 8, AT24_FLAG_ADDR16) },
    { "24c256", AT24_DEVICE_MAGIC(262144 / 8, AT24_FLAG_ADDR16) },
    { "24c512", AT24_DEVICE_MAGIC(524288 / 8, AT24_FLAG_ADDR16) },
    { "24c1024", AT24_DEVICE_MAGIC(1048576 / 8, AT24_FLAG_ADDR16) },
    { "at24", 0 },
    { /* END OF LIST */ }
};

static struct i2c_driver at24_driver = {
    .driver = {
        .name = "at24",
        .acpi_match_table = ACPI_PTR(at24_acpi_ids),
    },
    .probe = at24_probe,    // i2c_bus_type match()之后,调用 i2c_driver的probe()
    .remove = at24_remove,
    .id_table = at24_ids,    // 驱动支持设备id列表
};

static int __init at24_init(void)
{
    if (!io_limit) {
        pr_err("at24: io_limit must not be 0!\n");
        return -EINVAL;
    }

    io_limit = rounddown_pow_of_two(io_limit);
    return i2c_add_driver(&at24_driver);    // 注册at24cxx系列i2c设备驱动
}

    2,设备探测probe

    (1)at24cxx设备平台数据结构

struct at24_platform_data {
    u32        byte_len;        /* size (sum of all addr) */    // 设备支持的容量
    u16        page_size;        /* for writes */    // 设备支持一次读取或写入的大小
    u8        flags;    // 设备特性,如下
#define AT24_FLAG_ADDR16    0x80    /* address pointer is 16 bit */
#define AT24_FLAG_READONLY    0x40    /* sysfs-entry will be read-only */
#define AT24_FLAG_IRUGO        0x20    /* sysfs-entry will be world-readable */
#define AT24_FLAG_TAKE8ADDR    0x10    /* take always 8 addresses (24c00) */

    void        (*setup)(struct memory_accessor *, void *context);
    void        *context;
};
 

    (2)核心数据结构 struct at24_data

struct at24_data {
    struct at24_platform_data chip;    // 芯片平台数据
    struct memory_accessor macc;
    int use_smbus;
    int use_smbus_write;

    /*
     * Lock protects against activities from other Linux tasks,
     * but not from changes by other I2C masters.
     */
    struct mutex lock;
    struct bin_attribute bin;    // 通过sysfs提供给用户的接口

    u8 *writebuf;
    unsigned write_max;
    unsigned num_addresses;    // 芯片支持的地址数量

    /*
     * Some chips tie up multiple I2C addresses; dummy devices reserve
     * them for us, and we'll use them with SMBus calls.
     */
    struct i2c_client *client[];    // 多地址芯片实例
};

static int at24_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
    struct at24_platform_data chip;
    kernel_ulong_t magic = 0;
    bool writable;
    int use_smbus = 0;
    int use_smbus_write = 0;
    struct at24_data *at24;
    int err;
    unsigned i, num_addresses;

    // 获取eeprom设备平台相关数据,如

    if (client->dev.platform_data) {

        // 设备树或者平台相关代码已经初始化好平台相关数据
        chip = *(struct at24_platform_data *)client->dev.platform_data;
    } else {

        // 没有预先配置好,从驱动支持列表的 magic 中解析平台相关数据,如容量,特性等
        if (id) {
            magic = id->driver_data;
        } else {
            const struct acpi_device_id *aid;

            aid = acpi_match_device(at24_acpi_ids, &client->dev);
            if (aid)
                magic = aid->driver_data;
        }
        if (!magic)
            return -ENODEV;

        chip.byte_len = BIT(magic & AT24_BITMASK(AT24_SIZE_BYTELEN));    // 从magic解析eeprom设备容量
        magic >>= AT24_SIZE_BYTELEN;
        chip.flags = magic & AT24_BITMASK(AT24_SIZE_FLAGS);    // 从magic解析eeprom设备特性
        /*
         * This is slow, but we can't know all eeproms, so we better
         * play safe. Specifying custom eeprom-types via platform_data
         * is recommended anyhow.
         */
        chip.page_size = 1;

        /* update chipdata if OF is present */
        at24_get_ofdata(client, &chip);

        chip.setup = NULL;
        chip.context = NULL;
    }

    if (!is_power_of_2(chip.byte_len))
        dev_warn(&client->dev,
            "byte_len looks suspicious (no power of 2)!\n");
    if (!chip.page_size) {
        dev_err(&client->dev, "page_size must not be 0!\n");
        return -EINVAL;
    }
    if (!is_power_of_2(chip.page_size))
        dev_warn(&client->dev,
            "page_size looks suspicious (no power of 2)!\n");

    /* Use I2C operations unless we're stuck with SMBus extensions. */
    if (!i2c_check_functionality(client->adapter, I2C_FUNC_I2C)) {
        if (chip.flags & AT24_FLAG_ADDR16)
            return -EPFNOSUPPORT;

        if (i2c_check_functionality(client->adapter,
                I2C_FUNC_SMBUS_READ_I2C_BLOCK)) {
            use_smbus = I2C_SMBUS_I2C_BLOCK_DATA;
        } else if (i2c_check_functionality(client->adapter,
                I2C_FUNC_SMBUS_READ_WORD_DATA)) {
            use_smbus = I2C_SMBUS_WORD_DATA;
        } else if (i2c_check_functionality(client->adapter,
                I2C_FUNC_SMBUS_READ_BYTE_DATA)) {
            use_smbus = I2C_SMBUS_BYTE_DATA;
        } else {
            return -EPFNOSUPPORT;
        }
    }

    /* Use I2C operations unless we're stuck with SMBus extensions. */
    if (!i2c_check_functionality(client->adapter, I2C_FUNC_I2C)) {
        if (i2c_check_functionality(client->adapter,
                I2C_FUNC_SMBUS_WRITE_I2C_BLOCK)) {
            use_smbus_write = I2C_SMBUS_I2C_BLOCK_DATA;
        } else if (i2c_check_functionality(client->adapter,
                I2C_FUNC_SMBUS_WRITE_BYTE_DATA)) {
            use_smbus_write = I2C_SMBUS_BYTE_DATA;
            chip.page_size = 1;
        }
    }

    if (chip.flags & AT24_FLAG_TAKE8ADDR)    // 明确指定支持芯片支持8地址
        num_addresses = 8;
    else
        num_addresses =    DIV_ROUND_UP(chip.byte_len,
            (chip.flags & AT24_FLAG_ADDR16) ? 65536 : 256);    // 根据芯片容量计算支持的地址数量

    at24 = devm_kzalloc(&client->dev, sizeof(struct at24_data) +
        num_addresses * sizeof(struct i2c_client *), GFP_KERNEL);    // 分配核心结构对象 at24_data

    if (!at24)
        return -ENOMEM;

    mutex_init(&at24->lock);
    at24->use_smbus = use_smbus;
    at24->use_smbus_write = use_smbus_write;
    at24->chip = chip;    // 初始化芯片平台相关数据
    at24->num_addresses = num_addresses;    // 初始化芯片支持的地址数量

    /*
     * Export the EEPROM bytes through sysfs, since that's convenient.
     * By default, only root should see the data (maybe passwords etc)
     */

    // sysfs接口初始化
    sysfs_bin_attr_init(&at24->bin);
    at24->bin.attr.name = "eeprom";
    at24->bin.attr.mode = chip.flags & AT24_FLAG_IRUGO ? S_IRUGO : S_IRUSR;
    at24->bin.read = at24_bin_read;    // sysfs读取接口
    at24->bin.size = chip.byte_len;    // 芯片容量,即,sysfs文件大小

    at24->macc.read = at24_macc_read;

    writable = !(chip.flags & AT24_FLAG_READONLY);    // 设备特性是否可写
    if (writable) {
        if (!use_smbus || use_smbus_write) {

            unsigned write_max = chip.page_size;

            at24->macc.write = at24_macc_write;

            at24->bin.write = at24_bin_write;    // sysfs写入接口
            at24->bin.attr.mode |= S_IWUSR;

            if (write_max > io_limit)
                write_max = io_limit;
            if (use_smbus && write_max > I2C_SMBUS_BLOCK_MAX)
                write_max = I2C_SMBUS_BLOCK_MAX;
            at24->write_max = write_max;

            /* buffer (data + address at the beginning) */
            at24->writebuf = devm_kzalloc(&client->dev,
                write_max + 2, GFP_KERNEL);    // 分配写入buffer

            if (!at24->writebuf)
                return -ENOMEM;
        } else {
            dev_warn(&client->dev,
                "cannot write due to controller restrictions.");
        }
    }

    // 对于支持多地址芯片,每个地址范围分配一个i2c_client实例

    at24->client[0] = client;    // 默认地址范围client

    /* use dummy devices for multiple-address chips */
    for (i = 1; i < num_addresses; i++) {
        at24->client[i] = i2c_new_dummy(client->adapter,
                    client->addr + i);    // 多地址,其他地址范围每个分配一个dummy client

        if (!at24->client[i]) {
            dev_err(&client->dev, "address 0x%02x unavailable\n",
                    client->addr + i);
            err = -EADDRINUSE;
            goto err_clients;
        }
    }

    err = sysfs_create_bin_file(&client->dev.kobj, &at24->bin);    // 创建sysfs文件,给用户提供访问接口
    if (err)
        goto err_clients;

    i2c_set_clientdata(client, at24);    // 方便通过 client 获取 at24 核心私有结构

    dev_info(&client->dev, "%zu byte %s EEPROM, %s, %u bytes/write\n",
        at24->bin.size, client->name,
        writable ? "writable" : "read-only", at24->write_max);
    if (use_smbus == I2C_SMBUS_WORD_DATA ||
        use_smbus == I2C_SMBUS_BYTE_DATA) {
        dev_notice(&client->dev, "Falling back to %s reads, "
               "performance will suffer\n", use_smbus ==
               I2C_SMBUS_WORD_DATA ? "word" : "byte");
    }

    /* export data to kernel code */
    if (chip.setup)
        chip.setup(&at24->macc, chip.context);

    return 0;

err_clients:
    for (i = 1; i < num_addresses; i++)
        if (at24->client[i])
            i2c_unregister_device(at24->client[i]);

    return err;
}
 

    3,sysfs读取操作

        at24cxx系列i2c芯片读取协议:先发送写命令,写入要访问eeprom的目标地址(8位或者16位,由芯片平台数据flag决定),再发送读数据命令,读取目标地址数据。因此,读取操作需要两个消息,一个写入消息,一个读取消息。

static ssize_t at24_bin_read(struct file *filp, struct kobject *kobj,
        struct bin_attribute *attr,
        char *buf, loff_t off, size_t count)
{
    struct at24_data *at24;

    at24 = dev_get_drvdata(container_of(kobj, struct device, kobj));
    return at24_read(at24, buf, off, count);
}

static ssize_t at24_read(struct at24_data *at24,
        char *buf, loff_t off, size_t count)
{
    ssize_t retval = 0;

    if (unlikely(!count))
        return count;

    /*
     * Read data from chip, protecting against concurrent updates
     * from this host, but not from other I2C masters.
     */
    mutex_lock(&at24->lock);

    while (count) {
        ssize_t    status;

        status = at24_eeprom_read(at24, buf, off, count);    // 实际读取操作,返回读取的字节数
        if (status <= 0) {
            if (retval == 0)
                retval = status;
            break;
        }
        buf += status;    // 空闲buffer首地址更新
        off += status;    // 要读取的eeprom地址更新
        count -= status;    // 空闲buffer大小更新
        retval += status;    // 累计读取的大小更新
    }

    mutex_unlock(&at24->lock);

    return retval;
}
 

static ssize_t at24_eeprom_read(struct at24_data *at24, char *buf,
        unsigned offset, size_t count)
{
    struct i2c_msg msg[2];
    u8 msgbuf[2];
    struct i2c_client *client;
    unsigned long timeout, read_time;
    int status, i;

    memset(msg, 0, sizeof(msg));

    /*
     * REVISIT some multi-address chips don't rollover page reads to
     * the next slave address, so we may need to truncate the count.
     * Those chips might need another quirk flag.
     *
     * If the real hardware used four adjacent 24c02 chips and that
     * were misconfigured as one 24c08, that would be a similar effect:
     * one "eeprom" file not four, but larger reads would fail when
     * they crossed certain pages.
     */

    /*
     * Slave address and byte offset derive from the offset. Always
     * set the byte address; on a multi-master board, another master
     * may have changed the chip's "current" address pointer.
     */
    client = at24_translate_offset(at24, &offset);    // 多地址芯片,计算目标地址落在哪个client,以及更新client地址范围内偏移offset

    if (count > io_limit)
        count = io_limit;

    if (at24->use_smbus) {
        /* Smaller eeproms can work given some SMBus extension calls */
        if (count > I2C_SMBUS_BLOCK_MAX)
            count = I2C_SMBUS_BLOCK_MAX;
    } else {
        /*
         * When we have a better choice than SMBus calls, use a
         * combined I2C message. Write address; then read up to
         * io_limit data bytes. Note that read page rollover helps us
         * here (unlike writes). msgbuf is u8 and will cast to our
         * needs.
         */
        i = 0;
        if (at24->chip.flags & AT24_FLAG_ADDR16)
            msgbuf[i++] = offset >> 8;    // 如果是16位目标地址,先发送高8位

        msgbuf[i++] = offset;    // 16位目标地址的低8位,或者8位目标地址

        // msg[0]是写命令,写入eeprom的目标地址

        msg[0].addr = client->addr;    // client->addr是eeprom的I2C地址,通常是0x50
        msg[0].buf = msgbuf;
        msg[0].len = i;

        // msg[1]是读命令,读取目标地址的数据

        msg[1].addr = client->addr;
        msg[1].flags = I2C_M_RD;
        msg[1].buf = buf;
        msg[1].len = count;

    }

    /*
     * Reads fail if the previous write didn't complete yet. We may
     * loop a few times until this one succeeds, waiting at least
     * long enough for one entire page write to work.
     */
    timeout = jiffies + msecs_to_jiffies(write_timeout);
    do {
        read_time = jiffies;
        if (at24->use_smbus) {
            status = i2c_smbus_read_i2c_block_data_or_emulated(client, offset,
                                       count, buf);
        } else {
            status = i2c_transfer(client->adapter, msg, 2);    // 调用i2c框架传输接口,执行实际数据传输操作
            if (status == 2)    // i2c_transfer返回执行成功的msg个数
                status = count; 
        }
        dev_dbg(&client->dev, "read %zu@%d --> %d (%ld)\n",
                count, offset, status, jiffies);

        if (status == count)
            return count;    // 返回实际读取的数据长度

        /* REVISIT: at HZ=100, this is sloooow */
        msleep(1);
    } while (time_before(read_time, timeout));

    return -ETIMEDOUT;
}
 

    4,sysfs写入操作

        at24cxx系列i2c芯片写入协议:先发送写命令,写入要访问eeprom的目标地址(8位或者16位,由芯片平台数据flag决定),再发送写数据命令,读取目标地址数据。因此,写入操作只需要一个写入消息就可以了。

static ssize_t at24_bin_write(struct file *filp, struct kobject *kobj,
        struct bin_attribute *attr,
        char *buf, loff_t off, size_t count)
{
    struct at24_data *at24;

    at24 = dev_get_drvdata(container_of(kobj, struct device, kobj));
    return at24_write(at24, buf, off, count);
}
 

static ssize_t at24_write(struct at24_data *at24, const char *buf, loff_t off,
              size_t count)
{
    ssize_t retval = 0;

    if (unlikely(!count))
        return count;

    /*
     * Write data to chip, protecting against concurrent updates
     * from this host, but not from other I2C masters.
     */
    mutex_lock(&at24->lock);

    while (count) {
        ssize_t    status;

        status = at24_eeprom_write(at24, buf, off, count);    // 实际写入操作,返回写入的字节数
        if (status <= 0) {
            if (retval == 0)
                retval = status;
            break;
        }

        // 同读取操作类似
        buf += status;
        off += status;
        count -= status;
        retval += status;

    }

    mutex_unlock(&at24->lock);

    return retval;
}
 

static ssize_t at24_eeprom_write(struct at24_data *at24, const char *buf,
        unsigned offset, size_t count)
{
    struct i2c_client *client;
    struct i2c_msg msg;
    ssize_t status = 0;
    unsigned long timeout, write_time;
    unsigned next_page;

    /* Get corresponding I2C address and adjust offset */
    client = at24_translate_offset(at24, &offset);    // 多地址芯片,计算目标地址落在哪个client,以及更新client地址范围内偏移offset

    /* write_max is at most a page */
    if (count > at24->write_max)
        count = at24->write_max;

    /* Never roll over backwards, to the start of this page */
    next_page = roundup(offset + 1, at24->chip.page_size);
    if (offset + count > next_page)
        count = next_page - offset;    // 保证每次写入不会跨page

    /* If we'll use I2C calls for I/O, set up the message */
    if (!at24->use_smbus) {
        int i = 0;

        msg.addr = client->addr;
        msg.flags = 0;

        /* msg.buf is u8 and casts will mask the values */
        msg.buf = at24->writebuf;
        if (at24->chip.flags & AT24_FLAG_ADDR16)
            msg.buf[i++] = offset >> 8;    // 16位地址高8位

        msg.buf[i++] = offset;    // 16位地址低8位或者8位地址
        memcpy(&msg.buf[i], buf, count);    // 拷贝实际要写入的数据
        msg.len = i + count;    // 实际消息的长度: eeprom目标地址大小+数据大小
    }

    /*
     * Writes fail if the previous one didn't complete yet. We may
     * loop a few times until this one succeeds, waiting at least
     * long enough for one entire page write to work.
     */
    timeout = jiffies + msecs_to_jiffies(write_timeout);
    do {
        write_time = jiffies;
        if (at24->use_smbus_write) {
            switch (at24->use_smbus_write) {
            case I2C_SMBUS_I2C_BLOCK_DATA:
                status = i2c_smbus_write_i2c_block_data(client,
                        offset, count, buf);
                break;
            case I2C_SMBUS_BYTE_DATA:
                status = i2c_smbus_write_byte_data(client,
                        offset, buf[0]);
                break;
            }

            if (status == 0)
                status = count;
        } else {
            status = i2c_transfer(client->adapter, &msg, 1);    // 调用i2c框架传输接口,执行实际数据传输操作
            if (status == 1)    // i2c_transfer返回执行成功的msg个数
                status = count;
        }
        dev_dbg(&client->dev, "write %zu@%d --> %zd (%ld)\n",
                count, offset, status, jiffies);

        if (status == count)
            return count;    // 返回实际写入的数据长度

        /* REVISIT: at HZ=100, this is sloooow */
        msleep(1);
    } while (time_before(write_time, timeout));

    return -ETIMEDOUT;
}
 

    I2C eeprom的设备驱动的读取和写入的协议都算是比较简单的。在实际中,也可能遇到更加复杂的i2c设备驱动,这就需要我们熟悉具体设备的业务协议,通过sysfs或者字符设备等,提供给用户操作的api接口。

    从这个例子中,我们看到了kernel中随处可见的抽象思想:I2C核心框架抽象出一套平台无关的接口,I2C adapter实现具体总线的操作algorithm,而各个具体的i2c设备又在此I2C总线的操作基础之上,实现各自的功能。

转载于:https://my.oschina.net/yepanl/blog/2987080

你可能感兴趣的:(I2C EEPROM驱动实例分析)