1. 程式人生 > >I2C EEPROM驅動例項分析

I2C EEPROM驅動例項分析

    上篇分析了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匯流排的操作基礎之上,實現各自的功能。