I2C總線及Linux下的I2C體系結構

一.I2C總線

I2C(內置集成電路)總線是由Philips開發的兩線式串行總線,支持多主控模式,任何能夠進行發送和接收的設備都可以成爲主設備。
I2C是philips提出的外設總線。I2C只有兩條線,一條串行數據線SDA,一條是時鐘線SCL ,使用SCL,SDA這兩根信號線就實現了設備之間的數據交互,它方便了工程師的佈線。因此,I2C總線被非常廣泛地應用在EEPROM,實時鐘,小型LCD等設備與CPU的接口中。
總線空閒時,上拉電阻使SDA和SCL線都保持高電平。I2C設備上的串行數據線SDA接口電路是雙向的,輸出電路用於向總線上發送數據,輸入電路用於接收總線上的數據。同樣的,串行時鐘線SCL也是雙向的,作爲控制總線數據傳送的主機要通過SCL輸出電路發送時鐘信號,並檢測總線上SCL上的電平以決定什麼時候發送下一個時鐘脈衝電平;作爲接收主機命令的從設備需按總線上SCL的信號發送或接收SDA上的信號,它也可以向SCL線發送低電平信號以延長總線時鐘信號週期。
起始信號:當SCL爲高期間,SDA由高到低的跳變;啓動信號是一種電平跳變時序信號,而不是一個電平信號。
停止信號:當SCL爲高期間,SDA由低到高的跳變;停止信號也是一種電平跳變時序信號,而不是一個電平信號。
在這裏插入圖片描述
在開始信號之後i2c主設備便會開始發送數據,在發送數據期間,SCL爲高時SDA必須保持穩定無變化,因爲SCL爲高時會採樣數據。若此時SDA變化,可能導致誤髮結束信號而產生終止。也就是說SCL高,SDA保持穩定,則數據有效,SDA的改變只能發生在SCL的低電平期間。當然接收器每接收一個字節數據都會產生一個ACK迴應信號,表示已經收到數據。
開始位和停止位都由I2C主設備產生。在選擇從設備時,如果從設備是採用7位地址,則主設備在發起傳輸過程前,需先發送1字節的地址信息,前7位爲設備地址,最後一位爲讀寫標誌。之後,每次傳輸的數據也是一個字節,從MSB位開始傳輸。每個字節傳輸完後,在SCL的第9個上升沿到來之前,接收方應該發出一個ACK位。SCL上的時鐘脈衝由I2C主控方發出,在第8個時鐘週期之後,主控方應該釋放SDA,I2C總線的時序如下:
在這裏插入圖片描述
在這裏插入圖片描述

二、Linux下的I2C驅動體系結構

在linux系統中,I2C驅動由以下三部分組成:

1.I2C核心:
提供了I2C總線驅動和I2C設備驅動的註冊和註銷方法,I2C通信方法上層的、與具體適配器無關的代碼以及探測設備、檢測設備地址的上層代碼等。
增加/刪除i2c_adapter
int i2c_add_adapter(struct i2c_adapter *adapter)
int i2c_del_adapter(struct i2c_adapter *adap)
int i2c_register_driver(struct module *owner, struct i2c_driver *driver)
void i2c_del_driver(struct i2c_driver *driver)
i2c_client依附/脫離
int i2c_attach_client(struct i2c_client *client)
int i2c_detach_client(struct i2c_client *client)
I2C傳輸,發送和接收
int i2c_master_send(struct i2c_client *client,const char *buf ,int count)
int i2c_master_recv(struct i2c_client *client, char *buf ,int count)
int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num)
2.I2C總線驅動:
I2C總線驅動是對I2C硬件體系結構中適配器端的實現,適配器可由CPU控制,甚至可以集成在CPU內部。I2C總線驅動主要包括i2c_adapter數據結構和i2c_algorithm數據結構以及控制i2c適配器產生通信信號的函數。經由I2C總線驅動的代碼,可以以主控方式產生開始位、停止位、讀寫週期,以及以從設備方式被讀寫、產生ACK等。
一個總線驅動用於支持一條特定的I2C總線的讀寫。一個總線驅動通常需要兩個模塊,struct i2c_adapter和struct i2c_algorithm來描述
I2C_adapter:構造一個對I2C core層接口的數據結構,並通過接口函數向I2C core註冊一個控制器adapter。
i2c_algorithm:主要實現對I2C總線訪問通信的具體算法。

3.I2C設備驅動:
I2C設備驅動是對I2C硬件體系結構中設備端的實現,I2C設備掛接在受CPU控制的I2C適配器上,通過I2C適配器與CPU交換數據。I2C設備驅動主要包括數據結構i2c_driver和i2c_client,這需要具體設備來實現其中的成員函數。

i2c_driver結構對應一套具體的驅動方法,例如:probe、remove、suspend等,需要自己申明。i2c_client數據結構由內核根據具體的設備註冊信息自動生成,設備驅動根據硬件具體情況填充。具體使用下面介紹。
I2C 設備驅動具體實現放在在/drivers/i2c目錄下chips文件夾。
i2c-dev.c:提供了用戶層對I2C設備的訪問,包括open,read,write,ioctl,release等常規文件操作,我們可以通過open函數打開 I2C的設備文件,通過ioctl函數設定要訪問從設備的地址,然後可以通過 read和write或者ioctl函數完成對I2C設備的讀寫操作。
I2C_client:對應於真實的物理設備,結構體中包含了芯片地址,設備名稱,設備使用的中斷號,設備所依附的控制器,設備所依附的驅動等內容。

在這裏插入圖片描述
架構層次分類  
第一層:提供i2c adapter的硬件驅動,探測、初始化i2c_adapter(如申請i2c的io地址和中斷號),驅動soc控制的i2c_adapter在硬件上產生信號(start、stop、ack)以及處理i2c中斷。覆蓋圖中的硬件實現層。
第二層:提供i2c adapter的algorithm,用具體適配器的xxx_xfer()函數來填充i2c_algorithm的master_xfer函數指針,並把賦值後的i2c_algorithm再賦值給i2c_adapter的algo指針。覆蓋圖中的訪問抽象層、i2c核心層。
第三層:實現i2c設備驅動中的i2c_driver接口,用具體的i2c device設備的attach_adapter()、detach_adapter()方法賦值給i2c_driver的成員函數指針。實現設備與總線(或者叫adapter)的掛接。覆蓋圖中的driver驅動層。
第四層:實現i2c設備所對應的具體device的驅動,i2c_driver只是實現設備與總線的掛接,而掛接在總線上的設備則是千差萬別的,所以要實現具體設備device的write()、read()、ioctl()等方法,賦值給file_operations,然後註冊字符設備(多數是字符設備)。覆蓋圖中的driver驅動層。
第一層和第二層又叫i2c總線驅動(bus),第三層和第四層屬於i2c設備驅動(device_driver)。

linux下I2C驅動體系結構文件介紹:
i2c-core.c這個文件實現了I2C核心的功能以及/proc/bus/i2c*接口。
i2c-dev.c實現了I2C適配器設備文件的功能,每一個I2C適配器都被分配一個設備。通過適配器訪問設備時的主設備號都爲89,次設備號爲0-255。I2c-dev.c並沒有針對特定的設備而設計,只是提供了通用的read(),write(),和ioctl()等接口,應用層可以借用這些接口訪問掛接在適配器上的I2C設備的存儲空間或寄存器,並控制I2C設備的工作方式。
busses文件夾這個文件中包含了一些I2C總線的驅動,如針對S3C2410,S3C2440,S3C6410等處理器的I2C控制器驅動爲i2c-s3c2410.c。
algos文件夾實現了一些I2C總線適配器的algorithm。

i2c_adapter與i2c_algorithm  
i2c_adapter對應與物理上的一個適配器,而i2c_algorithm對應一套通信方法,一個i2c適配器需要i2c_algorithm中提供的(i2c_algorithm中的又是更下層與硬件相關的代碼提供)通信函數來控制適配器上產生特定的訪問週期。缺少i2c_algorithm的i2c_adapter什麼也做不了,因此i2c_adapter中包含其使用的i2c_algorithm的指針。

i2c_driver和i2c_client  
i2c_driver對應一套驅動方法,其主要函數是attach_adapter()和detach_adapter()
i2c_client對應真實的i2c物理設備,每個i2c設備都需要一個i2c_client來描述
i2c_driver與i2c_client的關係是一對多。一個i2c_driver上可以支持多個同等類型的i2c_client
.
i2c_adapter和i2c_client  
i2c_adapter和i2c_client的關係與i2c硬件體系中適配器和設備的關係一致,即i2c_client依附於i2c_adapter,由於一個適配器上可以連接多個i2c設備,所以i2c_adapter中包含依附於它的i2c_client的鏈表。
從i2c驅動架構圖中可以看出,linux內核對i2c架構抽象了一個叫核心層core的中間件,它分離了設備驅動device driver和硬件控制的實現細節(如操作i2c的寄存器),core層不但爲上面的設備驅動提供封裝後的內核註冊函數,而且還爲下面的硬件事件提供註冊接口(也就是i2c總線註冊接口),可以說core層起到了承上啓下的作用。

i2c-core對應的源文件爲i2c-core.c,位於內核目錄/driver/i2c/i2c-core.c,i2c-core.c中主要的函數有以下幾個:
i2c_transfer()函數:i2c_transfer()函數本身並不具備驅動適配器物理硬件完成消息交互的能力,它只是尋找到i2c_adapter對應的i2c_algorithm,並使用i2c_algorithm的master_xfer()函數真正的驅動硬件流程。

int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num)
{
    unsigned long orig_jiffies;
    int ret, try;
    
    if (adap->algo->master_xfer) {
#ifdef DEBUG
        for (ret = 0; ret < num; ret++) {
            dev_dbg(&adap->dev, "master_xfer[%d] %c, addr=0x%02x, "
                "len=%d%s\n", ret, (msgs[ret].flags & I2C_M_RD)
                ? 'R' : 'W', msgs[ret].addr, msgs[ret].len,
                (msgs[ret].flags & I2C_M_RECV_LEN) ? "+" : "");
        }
#endif

        if (in_atomic() || irqs_disabled()) {
            ret = i2c_trylock_adapter(adap);
            if (!ret)
                /* I2C activity is ongoing. */
                return -EAGAIN;
        } else {
            i2c_lock_adapter(adap);
        }
        /* Retry automatically on arbitration loss */
        orig_jiffies = jiffies;
        for (ret = 0, try = 0; try <= adap->retries; try++) {
            ret = adap->algo->master_xfer(adap, msgs, num);
            if (ret != -EAGAIN)
                break;
            if (time_after(jiffies, orig_jiffies + adap->timeout))
                break;
        }
        i2c_unlock_adapter(adap);
        return ret;
    } else {
        dev_dbg(&adap->dev, "I2C level transfers not supported\n");
        return -EOPNOTSUPP;
    }
}
EXPORT_SYMBOL(i2c_transfer);

i2c_add_adapter函數和i2c_del_adapter函數則用於在i2c_core.c 中向下層的硬件事件提供註冊和註銷接口。
i2c_register_driver和i2c_del_driver提供設備驅動的註冊和註銷。

buses文件夾下的i2c_s3c2410.c的與硬件相關的總線驅動代碼。I2C適配器控制硬件發送接收信號的i2c_algorithm結構體中的函數指針在與硬件相關的代碼中被賦值。

/* i2c bus registration info */
static const struct i2c_algorithm s3c24xx_i2c_algorithm = {
    .master_xfer        = s3c24xx_i2c_xfer,    //
    .functionality      = s3c24xx_i2c_func,
};

/*實現i2c數據的發送和接收的處理過程,也實現i2c適配器和i2c core的連接*/
static int s3c24xx_i2c_xfer(struct i2c_adapter *adap,struct i2c_msg *msgs, int num)
{
    struct s3c24xx_i2c *i2c = (struct s3c24xx_i2c *)adap->algo_data;
    int retry;
    int ret;
    clk_enable(i2c->clk);
    for (retry = 0; retry < adap->retries; retry++) {
        ret = s3c24xx_i2c_doxfer(i2c, msgs, num);//推進i2c消息的傳輸,分析此函數可以知道,是通過中斷處理函數推進的
        if (ret != -EAGAIN) {
            clk_disable(i2c->clk);
            return ret;
        }
        dev_dbg(i2c->dev, "Retrying transmission (%d)\n", retry);
        udelay(100);
    }
    clk_disable(i2c->clk);
    return -EREMOTEIO;
}

編寫具體的I2C驅動時,工程師需要處理的主要工作如下:
  1).提供I2C適配器的硬件驅動,探測,初始化I2C適配器(如申請I2C的I/O地址和中斷號),驅動CPU控制的I2C適配器從硬件上產生。
  2).提供I2C控制的algorithm, 用具體適配器的xxx_xfer()函數填充i2c_algorithm的master_xfer指針,並把i2c_algorithm指針賦給i2c_adapter的algo指針。
  3).實現I2C設備驅動中的i2c_driver接口,用具體yyy的yyy_probe(),yyy_remove(),yyy_suspend(),yyy_resume()函數指針和i2c_device_id設備ID表賦給i2c_driver的probe,remove,suspend,resume和id_table指針。
  4).實現I2C設備所對應類型的具體驅動,i2c_driver只是實現設備與總線的掛接。
  上面的工作中前兩個屬於I2C總線驅動,後面兩個屬於I2C設備驅動。

s3c2440中設備調用總線驅動的入口就是i2c_core.c 中的i2c_transfer函數。我們從設備調用總線驅動的入口處開始分析:
在i2c-core.c中的i2c_transfer函數中,會有語句:ret = adap->algo->master_xfer(adap, msgs, num);來實現數據傳遞,實際此處就是I2C總線驅動執行的入口。在s3c2440.c的總線驅動中,i2c_algorithm結構體中的master_xfer函數指針則指向了 s3c24xx_i2c_xfer。在s3c24xx_i2c_xfer函數中,調用上才s3c244xx_i2c_doxfer函數,s3c24xx_i2c_doxfer函數完成的功能有:
1.將s3c24xx的I2C適配器設置位I2C主設備。
2.初始化s3c24xx_i2c結構體。
3.使能i2c中斷。
4.啓動i2c消息的傳輸。

s3c24xx_i2c_doxfer函數調用s3c24xx_i2c_message_start()函數啓動了I2C消息數組的傳輸週期,並沒有實現完整的master_transfer傳輸流程,實現完整的傳輸流程需要藉助i2c適配器上的中斷來步步推進。主要包括函數s3c24xx_i2c_irq()和其依賴的i2c_s3c_irq_nextbyte(i2c, status)。
那麼,總線驅動是在何時調用i2c適配器的中斷處理函數s3c24xx_i2c_irq呢?
在總線驅動的s3c24xx-i2c-probe函數中,會調用該中斷處理函數,而調用s3c24xx-i2c-probe的前提則是當一個適合的i2c設備被探測到時,便會調用該函數。
那麼s3c24xx_i2c_probe函數會在何時被調用呢?
該函數被賦值給s3c24xx_i2c_driver中的probe函數指針,由以下代碼可知,當初始化一個adapter或exit一個適配器時,就會調用s3c24xx_i2c_driver結構體,並根據該結構體中的函數指針執行不同的函數。

static struct platform_driver s3c24xx_i2c_driver = {
    .probe      = s3c24xx_i2c_probe,
    .remove     = s3c24xx_i2c_remove,
    .id_table   = s3c24xx_driver_ids,
    .driver     = {
        .owner  = THIS_MODULE,
        .name   = "s3c-i2c",
        .pm = S3C24XX_DEV_PM_OPS,
    },
};

i2c_dev.c文件
i2c-dev.c文件完全可以看作一個i2c設備驅動,不過,它實現的一個i2c_client是虛擬、臨時的,隨着設備文件的打開而產生,並隨設備文件的關閉而撤銷,並沒有被添加i2c_addapter的client鏈表中。i2cdev_read()、i2cdev_write()函數來對應用戶空間要使用的read()和write()文件操作接口,這兩個函數會調用i2c核心的i2c_master_recv()和i2c_master_send函數來構造一個i2c消息並引發適配器algorithm通信函數的調用,完成消息的傳輸。
需要注意的是,大多數I2C設備的讀寫流程並不對應於一條消息,往往需要兩條甚至更多的消息來進行一次讀寫週期,這種情況下,在應用層仍然調用read、write來讀寫I2C設備,將不能正確的讀寫。
因此,i2c-dev.c中的i2cdev_read()和i2cdev_write()函數並不具備太強的通用性,只能適用於非RepStart模式的情況。對於兩條以上消息組成的讀寫,在用戶空間需要組織i2c_msg消息數組並調用I2C_RDWR_IOCTL命令。
在linux下的I2C驅動體系結構中,通過i2c適配器來控制i2c從設備,而在內核中消息的傳遞則會涉及到內核中相關的結構體,並且必須遵循一定的讀寫時序:
我們在調用ioctl之前,需要組織好i2c_msg消息結構體和nmsgs成員。在i2c_msg消息結構體成員包括:從設備地址、讀寫標誌(默認爲寫入)、消息長度(其單位爲字節)、指向消息內容的指針。 這些成員我們看完之後會發現它大致符合先給設備地址,然後給寫信號以及數據的時序。其實但我們寫代碼的時候並不一定是addr非得定義在flags前面,因爲內核會自動幫助我們完成這些具體的時序操作。但有一點,我們必須要填充好nmsgs以及i2c_msg中的成員。

/* This is the structure as used in the I2C_RDWR ioctl call */
struct i2c_rdwr_ioctl_data {
    struct i2c_msg __user *msgs;    /* pointers to i2c_msgs */
    __u32 nmsgs;            /* number of i2c_msgs */
};  


struct i2c_msg {
    __u16 addr; /* slave address            */
    __u16 flags; /*默認爲寫入*/
    __u16 len;      /* msg length               */
    __u8 *buf;      /* pointer to msg data          */
};

可以這樣理解,當我們在用戶空間調用ioctl函數時,會在i2c_dev.c文件中找相應的ioctl函數,我們再來看下i2c_dev.c中的ioctl函數是如何實現消息的交換的。

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,
};

static long i2cdev_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
    struct i2c_client *client = file->private_data;
    unsigned long funcs;


    dev_dbg(&client->adapter->dev, "ioctl, cmd=0x%02x, arg=0x%02lx\n",
        cmd, arg);


    switch (cmd) {
    case I2C_SLAVE:
    case I2C_SLAVE_FORCE:
    
    if ((arg > 0x3ff) ||
            (((client->flags & I2C_M_TEN) == 0) && arg > 0x7f))
            return -EINVAL;
        if (cmd == I2C_SLAVE && i2cdev_check_addr(client->adapter, arg))
            return -EBUSY;
        /* REVISIT: address could become busy later */
        client->addr = arg;
        return 0;
    case I2C_TENBIT:
        if (arg)
            client->flags |= I2C_M_TEN;
        else
            client->flags &= ~I2C_M_TEN;
        return 0;
     case I2C_PEC:
        if (arg)
            client->flags |= I2C_CLIENT_PEC;
        else
            client->flags &= ~I2C_CLIENT_PEC;
        return 0;
    case I2C_FUNCS:
        funcs = i2c_get_functionality(client->adapter);
        return put_user(funcs, (unsigned long __user *)arg);


    case I2C_RDWR:
        return i2cdev_ioctl_rdrw(client, arg);

    case I2C_SMBUS:
        return i2cdev_ioctl_smbus(client, arg);

    case I2C_RETRIES:
        client->adapter->retries = arg;
        break;

    case I2C_TIMEOUT:
        /* For historical reasons, user-space sets the timeout
         * value in units of 10 ms.
         */
        client->adapter->timeout = msecs_to_jiffies(arg * 10);
        break;
    default:
        /* NOTE:  returning a fault code here could cause trouble
         * in buggy userspace code.  Some old kernel bugs returned
         * zero in this case, and userspace code might accidentally
         * have depended on that bug.
         */
        return -ENOTTY;
    }
    return 0;
}

i2c_dev.c 中的ioctl函數中有一個switch,case語句,會根據用戶空間的ioctl函數指定的參數來選擇執行不同的動作,當在用戶空間指定I2C_RDWR時,就會調用i2cdev_ioctl函數中的i2cdev_ioctl_rdrw函數,繼續追蹤該函數,它會調用copy_from_user或者copy_to_user以及i2c_transfer函數,如果繼續深入瞭解,i2c_transfer函數是在i2c_core.c中實現的函數,i2c_core.c則是連接了i2c設備驅動和總線驅動,i2c_transfer中的函數調用adap->algo->master_xfer則是i2c總線驅動的入口。而在i2c總線驅動中,例如i2c-s3c2410.c中,則將master_xfer函數指針指向s3c24xx_i2c_xfer函數,s3c24xx_i2c_xfer函數則會繼續調用s3c24xx_i2c_doxfer函數繼續傳輸消息的推進。
linux i2c驅動體系結構的圖解:
在這裏插入圖片描述
AT24C02
在這裏插入圖片描述
在這裏插入圖片描述
AT24C02的存儲容量爲2K bit,內容分成32頁,每頁8Byte,共256Byte,操作時有兩種尋址方式:芯片尋址和片內子地址尋址。
(1)芯片尋址:AT24C02的芯片地址爲1010,其地址控制字格式爲1010A2A1A0R/W。其中A2,A1,A0可編程地址選擇位。A2,A1,A0引腳接高、低電平後得到確定的三位編碼,與1010形成7位編碼,即爲該器件的地址碼。R/W爲芯片讀寫控制位,該位爲0,表示芯片進行寫操作。
(2)片內子地址尋址:芯片尋址可對內部256B中的任一個進行讀/寫操作,其尋址範圍爲00~FF,共256個尋址單位。
at24c02: AT24C02是一個2K位串行CMOS E2PROM, 內部含有256個8位字節,AT24C02有一個8字節頁寫緩衝器。該器件通過IIC總線接口進行操作,有一個專門的寫保護功能。
根據AT24C02的datasheet:
在這裏插入圖片描述
在這裏插入圖片描述

由於E2PROM的半導體工藝特性,對E2PROM的寫入時間要5~10ms,但AT24CXX系列串行E2PROM芯片內部設置了一個具有SRAM性質的輸入緩衝器,稱爲頁寫緩衝器。CPU對該芯片寫操作時,AT24CXX系列芯片先將CPU輸入的數據暫存頁寫緩衝器內,然後,慢慢寫入E2PROM中。因此,CPU對AT24CXX系列E2PROM一次寫入的數據,受到該芯片頁寫緩衝器容量的限制。頁寫緩衝器的容量:AT24C01A/02爲8B,AT24C04/08/16爲16B,AT24C32/64爲32B。

參考博客:https://blog.csdn.net/wangpengqi/article/details/17711165
https://blog.csdn.net/u010944778/article/details/46807737

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章