1. 程式人生 > >i2c子系統-eeprom例項分析

i2c子系統-eeprom例項分析

兩種訪問eeprom的例子

在Linux應用層訪問eeprom的方法,並給出示例程式碼方便大家理解。第一個方法是通過eeprom的裝置檔案進行訪問。這兩個方法分別對應了i2c裝置驅動的兩個不同的實現,第二個方法是通過sysfs檔案系統對eeprom進行訪問。

第一種通過devfs訪問eeprom的方法是linux i2c提供的一種通用的方法,是有i2c子系統提供的,訪問裝置的能力有限,通過devfs訪問eeprom的方法則需要了解eeprom的讀寫時序。對寫應用程式開發人員的要求較高。

第二種通過sysfs檔案系統的二進位制結點訪問eeprom的方法是由eeprom的裝置驅動實現的,是一種專有的方法。大體來講就是關於時序的操作已經由驅動程式完成了,並以bin_attribute的方式開放到使用者空間,應用程式開發人員只按檔案操作的方式操作裝置;通過sysfs檔案系統訪問eeprom操作簡單,無需瞭解eeprom的硬體特性以及訪問時序。

通過devfs訪問I2C裝置

介紹這個之前,先來看一下:i2c-dev.c 檔案完全可以被看作一個 I2C 裝置驅動,它實現的一個 i2c_client 是虛擬、臨時的,隨著裝置檔案的開啟而產生,並隨裝置檔案的關閉而撤銷,並沒有被新增到 i2c_adapter 的 clients
連結串列中。i2c-dev.c 針對每個 I2C 介面卡生成一個主裝置號為 89 的裝置檔案,實現了i2c_driver 的成員函式以及檔案操作介面,所以 i2c-dev.c 的主體是“i2c_driver 成員函式 + 字元裝置驅動” 。

i2c-dev.c 中提供 i2cdev_read()、i2cdev_write()函式來對應使用者空間要使用的 read()和 write()檔案操作介面,這兩個函式分別呼叫 I2C 核心的 i2c_master_recv()和i2c_master_send()函式來構造一條 I2C 訊息並引發介面卡 algorithm 通訊函式的呼叫,完成訊息的傳輸,對應於如圖 15.7 所示的時序。但是,很遺憾,大多數稍微複雜一點I2C 裝置的讀寫流程並不對應於一條訊息,往往需要兩條甚至更多的訊息來進行一次讀寫週期,這種情況下,在應用層仍然呼叫 read()、write()檔案 API 來讀寫 I2C 裝置,將不能正確地讀寫。許多工程師碰到過類似的問題,往往經過相當長時間的除錯都沒法解決 I2C 裝置的讀寫,連錯誤的原因也無法找到,顯然是對 i2cdev_read()和 i2cdev_write()函式的作用有所誤解。

鑑於上述原因,i2c-dev.c 中 i2cdev_read()和 i2cdev_write()函式不具備太強的通用性,沒有太大的實用價值,只能適用於非 RepStart 模式的情況。對於兩條以上訊息組成的讀寫,在使用者空間需要組織 i2c_msg 訊息陣列並呼叫 I2C_RDWR IOCTL 命令。程式碼清單 15.21 所示為 i2cdev_ioctl()函式的框架,其中詳細列出了 I2C_RDWR 命令的處理過程。

#include <stdio.h>
#include <linux/types.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h> #include <sys/types.h> #include <sys/ioctl.h> #include <errno.h> #define I2C_RETRIES 0x0701 #define I2C_TIMEOUT 0x0702 #define I2C_RDWR 0x0707 #define I2C_ADDR 0x50 struct i2c_msg { unsigned short addr; unsigned short flags; #define I2C_M_TEN 0x0010 #define I2C_M_RD 0x0001 unsigned short len; unsigned char *buf; }; struct i2c_rdwr_ioctl_data { struct i2c_msg *msgs; int nmsgs; }; /***********main***********/ int main(){ int fd,ret; struct i2c_rdwr_ioctl_data e2prom_data; //disable WP system("echo 103 > /sys/class/gpio/export"); system("echo \"out\" > /sys/class/gpio/gpio103/direction"); system("echo 0 > /sys/class/gpio/gpio103/value"); fd=open("/dev/i2c-0",O_RDWR);//開啟eeprom裝置檔案結點 if(fd<0){ perror("open error"); } e2prom_data.nmsgs=2; e2prom_data.msgs=(struct i2c_msg*)malloc(e2prom_data.nmsgs*sizeof(struct i2c_msg)); if(!e2prom_data.msgs) { perror("malloc error"); exit(1); } ioctl(fd,I2C_TIMEOUT,1);/*超時時間*/ ioctl(fd,I2C_RETRIES,2);/*重複次數*/ /***write data to e2prom**/ e2prom_data.nmsgs=1;//由前面eeprom讀寫分析可知,寫eeprom需要一條訊息 (e2prom_data.msgs[0]).len=5;//此訊息的長度為5個位元組,第一,二個位元組是要寫入資料的地址,第三,四,五,位元組是要寫入的資料 (e2prom_data.msgs[0]).addr=I2C_ADDR;//e2prom 裝置地址 (e2prom_data.msgs[0]).flags=0; //write (e2prom_data.msgs[0]).buf=(unsigned char*)malloc(5); (e2prom_data.msgs[0]).buf[0]=0x00;// e2prom addr[15:8] (e2prom_data.msgs[0]).buf[1]=0x00;//e2prom addr[7:0] (e2prom_data.msgs[0]).buf[2]=0x55;//the data to write byte0 (e2prom_data.msgs[0]).buf[3]=0x66;//the data to write byte1 (e2prom_data.msgs[0]).buf[4]=0x77;//the data to write byte2 ret=ioctl(fd,I2C_RDWR,(unsigned long)&e2prom_data);//通過ioctl進行實際寫入操作 if(ret<0) { perror("ioctl error1"); } sleep(1); /******read data from e2prom*******/ e2prom_data.nmsgs=2;//讀eeprom需要兩條訊息 (e2prom_data.msgs[0]).len=1;//第一條訊息實際是寫eeprom,需要告訴eeprom需要讀資料的地址,因此長度為1個位元組 (e2prom_data.msgs[0]).addr=I2C_ADDR; // e2prom 裝置地址 (e2prom_data.msgs[0]).flags=0;//write (e2prom_data.msgs[0]).buf=(unsigned char*)malloc(2); (e2prom_data.msgs[0]).buf[0]=0x00;// e2prom addr[15:8] (e2prom_data.msgs[0]).buf[1]=0x00;//e2prom addr[7:0] (e2prom_data.msgs[1]).len=6; (e2prom_data.msgs[1]).addr=I2C_ADDR; (e2prom_data.msgs[1]).flags=I2C_M_RD; (e2prom_data.msgs[1]).buf=(unsigned char*)malloc(6); (e2prom_data.msgs[1]).buf[0]=0; (e2prom_data.msgs[1]).buf[1]=0; (e2prom_data.msgs[1]).buf[2]=0; (e2prom_data.msgs[1]).buf[3]=0; (e2prom_data.msgs[1]).buf[4]=0; (e2prom_data.msgs[1]).buf[5]=0; ret=ioctl(fd,I2C_RDWR,(unsigned long)&e2prom_data);//通過ioctl進行實際的讀操作 if(ret<0) { perror("ioctl error2"); } printf("Read back:0x%02X\n\r",(e2prom_data.msgs[1]).buf[0]); printf("Read back:0x%02X\n\r",(e2prom_data.msgs[1]).buf[1]); printf("Read back:0x%02X\n\r",(e2prom_data.msgs[1]).buf[2]); close(fd); system("echo 103 > /sys/class/gpio/unexport"); return 0; }

通過 通過sysfs檔案系統訪問I2C裝置

eeprom的裝置驅動在/sys/devices/44000000.ocp/44e0b000.i2c/i2c-0/0-0050/eeprom目錄下把eeprom裝置對映為一個二進位制節點,檔名為eeprom。對這個eeprom檔案的讀寫就是對eeprom進行讀寫。

#define EEPROM_DEVICE "/sys/devices/44000000.ocp/44e0b000.i2c/i2c-0/0-0050/eeprom"
#define TEST_STR "eeprom write/read test!"

int main(void)
{
    int fd;
    struct stat eeprom_stat;
    char read_buf[32] = { '\0' };

    fd = open(EEPROM_DEVICE, O_RDWR);
    if (fd < 0) {
        printf("open eeprom unsuccess\n");
        return;
    }

    if (stat(EEPROM_DEVICE, &eeprom_stat) < 0) {
        perror("failed to get eeprom status");
    } else {
        printf("\neeprom size: %d KB\n\n", eeprom_stat.st_size/1024);
    }

    printf("write \'%s\' to eeprom\n", TEST_STR);
    if (write(fd, TEST_STR, strlen(TEST_STR)) < 0) {
        perror("write to eeprom failed\n");
        return;
    }

    lseek(fd, 0, SEEK_SET);
    if (read(fd, read_buf, strlen(TEST_STR)) < 0) {
        printf("read back failed\n");
        return;
    }

    printf("\nget the following string from eeprom:\n%s\n\n", read_buf);

    return 0;
}

eeprom驅動分析

在drivers/misc/eeprom/at24.c中可以看到eeprom驅動對i2c_driver結構的例項化。

static struct i2c_driver at24_driver = {
    .driver = {
        .name = "at24",
        .owner = THIS_MODULE,
    },
    .probe = at24_probe,
    .remove = at24_remove,
    .id_table = at24_ids,
};

其中probe和remove會在模組初始化和解除安裝的時候被呼叫。

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);
}
module_init(at24_init);

static void __exit at24_exit(void)
{
    i2c_del_driver(&at24_driver);
}

at24_probe()

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

    //獲取板級裝置資訊
    if (client->dev.platform_data) {
        chip = *(struct at24_platform_data *)client->dev.platform_data;
    } else {
        if (!id->driver_data) {
            err = -ENODEV;
            goto err_out;
        }
        magic = id->driver_data;
        chip.byte_len = BIT(magic & AT24_BITMASK(AT24_SIZE_BYTELEN));
        magic >>= AT24_SIZE_BYTELEN;
        chip.flags = magic & AT24_BITMASK(AT24_SIZE_FLAGS);
        /*
         * 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;

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

    //檢查引數,必須為2的冪
    if (!is_power_of_2(chip.byte_len))
        dev_warn(&client->dev,
            "byte_len looks suspicious (no power of 2)!\n");
    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. */
    //檢查是否支援I2C協議,如果不支援則檢查是否支援SMBUS
    if (!i2c_check_functionality(client->adapter, I2C_FUNC_I2C)) {
        if (chip.flags & AT24_FLAG_ADDR16) {
            err = -EPFNOSUPPORT;
            goto err_out;
        }
        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 {
            err = -EPFNOSUPPORT;
            goto err_out;
        }
    }

    if (chip.flags & AT24_FLAG_TAKE8ADDR)//檢查時候使用8個地址
        num_addresses = 8;
    else
        num_addresses = DIV_ROUND_UP(chip.byte_len,//AT24C01使用一個地址
            (chip.flags & AT24_FLAG_ADDR16) ? 65536 : 256);

    at24 = kzalloc(sizeof(struct at24_data) +
        num_addresses * sizeof(struct i2c_client *), GFP_KERNEL);//為at24_data分配記憶體,同時根據地址個數分配i2c_client
    if (!at24) {
        err = -ENOMEM;
        goto err_out;
    }

    mutex_init(&at24->lock);
    //初始化at24_data,也就是填充此結構體
    at24->use_smbus = use_smbus;
    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)
     */
    //以二進位制結點的形式呈現eeprom的資料
    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;//繫結讀函式
    at24->bin.size = chip.byte_len;

    at24->macc.read = at24_macc_read;

    //判斷是否可寫
    writable = !(chip.flags & AT24_FLAG_READONLY);
    if (writable) {//如果可寫
        if (!use_smbus || i2c_check_functionality(client->adapter,
                I2C_FUNC_SMBUS_WRITE_I2C_BLOCK)) {

            unsigned write_max = chip.page_size;

            at24->macc.write = at24_macc_write;

            at24->bin.write = at24_bin_write;//繫結寫函式
            at24->bin.attr.mode |= S_IWUSR;//檔案擁有者可寫

            if (write_max > io_limit)//一次最多寫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 = kmalloc(write_max + 2, GFP_KERNEL);//分配緩衝區,多餘兩個位元組用於儲存暫存器地址
            if (!at24->writebuf) {
                err = -ENOMEM;
                goto err_struct;
            }
        } else {
            dev_warn(&client->dev,
                "cannot write due to controller restrictions.");
        }
    }

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

    //向sysfs檔案系統註冊二進位制結點
    err = sysfs_create_bin_file(&client->dev.kobj, &at24->bin);
    if (err)
        goto err_clients;

    //儲存驅動資料
    i2c_set_clientdata(client, at24);

    dev_info(&client->dev, "%zu byte %s EEPROM %s\n",
        at24->bin.size, client->name,
        writable ? "(writable)" : "(read-only)");
    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");
    }
    dev_dbg(&client->dev,
        "page_size %d, num_addresses %d, write_max %d, use_smbus %d\n",
        chip.page_size, num_addresses,
        at24->write_max, use_smbus);

    /* 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]);

    kfree(at24->writebuf);
err_struct:
    kfree(at24);
err_out:
    dev_dbg(&client->dev, "probe error %d\n", err);
    return err;
}
如果你正在開發的裝置驅動程式中需要與使用者層的介面,一般可選的方法有:

註冊虛擬的字元裝置檔案,以這個虛擬裝置上的 read/write/ioctl 等介面與使用者互動;但 read/write 一般只能做一件事情, ioctl 可以根據 cmd 引數做多個功能(使用i2c-dev.c中的函式就是這個道理),但其缺點是很明顯的: ioctl 介面無法直接在 Shell 指令碼中使用,為了使用 ioctl 的功能,還必須編寫配套的 C語言的虛擬裝置操作程式, ioctl 的二進位制資料介面也是造成大小端問題 (big endian與little endian)、32位/64位不可移植問題的根源;
註冊 proc 介面,接受使用者的 read/write/ioctl 操作;同樣的,一個 proc 項通常使用其 read/write/ioctl 介面,它所存在的問題與上面的虛擬字元裝置的的問題相似;
註冊 sysfs 屬性;
最重要的是,新增虛擬字元裝置支援和註冊 proc 介面支援這兩者所需要增加的程式碼量都並不少,最好的方法還是使用 sysfs 屬性支援,一切在使用者層是可見的透明,且增加的程式碼量是最少的,可維護性也最好;

at24_probe()函式主要的工作是在sys目錄在建立bin結點檔案,也就是前面通過sysfs檔案系統訪問i2c裝置中提到的/sys/bus/i2c/devices/0-0050/eeprom檔案,使用者可以用此檔案來操作eeprom,提供讀/寫操作方法,在probe裡面讀寫操作已經與二進位制結點繫結,讀操作函式是at24_bin_read(),寫操作函式是at24_bin_write()。

其中有個重要的結構體:
 struct at24_data {
     struct at24_platform_data chip;
     struct memory_accessor macc;
     int use_smbus;

     /*
      * Lock protects against activities from other Linux tasks,
      * but not from changes by other I2C masters.
      */
     struct mutex lock;
     struct bin_attribute bin;//二進位制結點

     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[];
 };

at24_data是此驅動的一些私有資料的封裝,包括二進位制結點,以及寫緩衝區。

而對at24_bin_read(),at24_bin_write()的進一步追蹤可知:at24_bin_read()—->at24_read—->at24_eeprom_read;at24_bin_write—->at24_write—->at24_eeprom_write—->2c_transfer i2c_transfer