1. 程式人生 > >詳解Linux-I2C驅動

詳解Linux-I2C驅動

目錄

一、LinuxI2C驅動--概述

1.1 寫在前面

本人學生一枚,之前沒有詳細的接觸過linux驅動,只是讀過宋寶華的《Linux裝置驅動開發詳解》,這段時間想靜下心來學習下linux i2c驅動,在網上找了很多資料,前輩們寫的文章讓我受益匪淺,但是一開始上手真的很痛苦,基本上大家都是從linux i2c體系結構的三大組成談起:i2c核心,i2c匯流排驅動,i2c裝置驅動,好抽象。所以我才想寫這個文章,從一個新人的角度分享下我學習linux i2c驅動的心得,寫的不對的地方歡迎大家批評指正。

因為對Linux裝置模型還不是很熟悉,所以我按照如何去實現一個i2c傳輸來講述,對於平臺匯流排、裝置與匯流排如何去匹配等暫時忽略。

當然很多東西都是我從網上搜刮而來的,也請大家原諒。我會把一些有用的博文連結放在後面,希望對大家有用。

1.2 I2C

I2C匯流排是由Philips公司開發的兩線式序列匯流排,這兩根線為時鐘線(SCL)和雙向資料線(SDA)。由於I2C匯流排僅需要兩根線,因此在電路板上佔用的空間更少,帶來的問題是頻寬較窄。I2C在標準模式下傳輸速率最高100Kb/s,在快速模式下最高可達400kb/s。屬於半雙工。

在嵌入式系統中,I2C應用非常廣泛,大多數微控制器中集成了I2C匯流排,一般用於和RTC,EEPROM,智慧電池電路,感測器,LCD以及其他類似裝置之間的通訊。

1.3 硬體

開發板:飛凌OK210

CPU型號:Samsung S5PV210

EEPROM型號:AT24C01A

linux-i2c-1.3.png

1.4 軟體

linux版本:Linux 2.6.35.7

I2C匯流排驅動:drivers/i2c/busses/i2c-s3c2410.c

eeprom驅動:drivers/misc/eeprom/at24.c

1.5 參考

二、LinuxI2C驅動--I2C匯流排

本節分析下I2C匯流排協議,因為我的開發板是三星s5pv210晶片,所以就以此為例。

2.1 I2C匯流排物理結構

linux-i2c-2.1.png

I2C匯流排在物理連線上非常簡單,分別由SDA(序列資料線)和SCL(序列時鐘線)及上拉電阻組成。通訊原理是通過對SCL和SDA線高低電平時序的控制,來產生I2C匯流排協議所需要的訊號進行資料的傳遞。在匯流排空閒狀態時,這兩根線一般被上面所接的上拉電阻拉高,保持著高電平。

2.2 I2C匯流排特性

  • 每個連線到匯流排的器件都可以通過唯一的地址和一直存在的簡單的主機/從機關係來軟體設定地址
  • 多主機匯流排,如果兩個或者更多的主機同時初始化資料傳輸,可以通過仲裁防止資料被破壞。
  • 序列8位雙向資料傳輸
  • 標準模式傳輸速率為100kbits/s
  • 快速模式傳輸速率為400kbits/s
  • 7位地址模
  • 支援主機發、主機收,從機發、從機收

2.3 開始和停止條件

當SCL是高電平時,SDA線由高電平向低電平切換,表示開始;當SCL是高電平時,SDA線由低電平向高電平切換,表示停止。如下圖所示。

linux-i2c-2.3.png

2.4 資料傳輸格式

傳送到SDA線上的每個位元組必須為8位,每次傳輸可以傳送的位元組數不受限制,但是每個位元組後面必須跟一個響應位。

linux-i2c-2.4.1.png

linux-i2c-2.4.2.png

2.5 響應

資料傳輸必須帶響應,響應時鐘脈衝由主機產生,在SCL的第9個時鐘脈衝上,前8個時鐘脈衝用來傳輸8位即1byte的資料。

當傳送端收到響應時鐘脈衝的時候就會拉高SDA從而釋放SDA線,而接收端通過拉低SDA先來表示收到資料,即SDA在響應期間保持低電平。

linux-i2c-2.5.png

2.6 匯流排仲裁

當兩個主機在總線上產生競爭時就需要仲裁。

SDA線低電平的優先順序高於高電平。當一個主機首先產生低電平,而緊接著另一個主機產生高電平,但是由於低電平的優先順序高於高電平,所以匯流排成低電平,也就是發低電平的主機佔有匯流排而發高電平的主機不佔有匯流排。如果兩個主機都是傳送低電平,那麼繼續比較下一個時鐘週期的電平來決定誰佔有匯流排,以此類推。

linux-i2c-2.6.png

三、LinuxI2C驅動--解析EEPROM的讀寫

本節介紹eeprom的讀寫時序,參考的是AT24C01A的datasheet。

3.1 概述

AT24C01A的儲存大小是1K,頁大小是8個位元組。

linux-i2c-3.1.png

3.2 裝置地址

linux-i2c-3.2.png

7位地址,前四位是1010,後三位由晶片引腳決定,由原理圖可知後三位是000,也就是裝置地址為0x50,因為資料傳輸是8位的,最後一位決定是讀還是寫。

3.3 讀eeprom

linux-i2c-3.3.png

讀任意地址eeprom的資料,首先第一個位元組得先在SDA上發出eeprom的裝置地址,也就是0x50,並且8位資料的最後一位是低電平表示寫裝置,然後第二個位元組是要讀的資料在eeprom內的地址,這樣以後再產生開始條件,第三個位元組在SDA上發出裝置地址,此時的最後一位是高電平,表示讀裝置,第四個位元組的資料就是讀eeprom的對應地址的資料。

可以看到,讀eeprom需要兩個開始條件,也就是2條訊息,第一條訊息寫eeprom確定讀的位置,大小為2個位元組,第二條訊息才是真正的讀eeprom。

3.4 寫eeprom

linux-i2c-3.4.png

寫eeprom就相對簡單,只需一個開始條件,第一個位元組發出裝置地址和置最低位為低電平表示寫eeprom,第二個位元組發出要讀資料在eerpom的地址,第三個位元組讀到的資料就對應地址在eeprom上的資料

四、LinuxI2C驅動--從兩個訪問eeprom的例子開始

本小節介紹兩個在linux應用層訪問eeprom的方法,並給出示例程式碼方便大家理解。第一個方法是通過sysfs檔案系統對eeprom進行訪問,第二個方法是通過eeprom的裝置檔案進行訪問。這兩個方法分別對應了i2c裝置驅動的兩個不同的實現,在後面的小結會詳細的分析。

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

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

我們可以先用cat命令來看下eeprom的內容。

[[email protected]]# cat eeprom                                                                      
�����������X�����������������������������������������������

發現裡面都是亂碼,然後用echo命令把字串“test”輸入給eeprom檔案,然後再cat出來。

就會發現字串test已經存在eeprom裡面了,我們知道sysfs檔案系統斷電後就沒了,也無法對資料進行儲存,為了驗證確實把“test”字串儲存在了eeprom,可以把系統斷電重啟,然後cat eeprom,會發現test還是存在的,證明確實對eeprom進行了寫入操作。

當然,因為eeprom已經對映為一個檔案了,我們還可以通過檔案I/O寫應用程式對其進行簡單的訪問測試。比如以下程式對特定地址(0x40)寫入特定資料(Hi,this is an eepromtest!),然後再把寫入的資料在此地址上讀出來。

#include<stdio.h>#include<stdlib.h>#include<sys/types.h>#include<sys/stat.h>#include<fcntl.h>#include<string.h>int main(void){int fd, size, len, i;char buf[50]={0};char*bufw="Hi,this is an eepromtest!";//要寫入的資料

  len=strlen(bufw);//資料長度
  fd= open("/sys/bus/i2c/devices/0-0050/eeprom",O_RDWR);//開啟檔案if(fd<0){
      printf("####i2c test device open failed####/n");return(-1);}//寫操作
  lseek(fd,0x40,SEEK_SET);//定位地址,地址是0x40if((size=write(fd,bufw, len))<0)//寫入資料{
      printf("write error\n");return1;}
  printf("writeok\n");//讀操作
  lseek(fd,0x40, SEEK_SET);//準備讀,首先定位地址,因為前面寫入的時候更新了當前檔案偏移量,所以這邊需要重新定位到0x40.if((size=read(fd,buf,len))<0)//讀資料{
      printf("readerror\n");return1;}
  printf("readok\n");for(i=0; i< len; i++)
      printf("buff[%d]=%x\n",i, buf[i]);//列印資料
  close(fd);return0;}

4.2 通過devfs訪問I2C裝置

linux的i2c驅動會針對每個i2c介面卡在/dev/目錄下生成一個主裝置號為89的裝置檔案,簡單的來說,對於本例的eeprom驅動,/dev/i2c/0就是它的裝置檔案,因此接下來的eeprom的訪問就變為了對此裝置檔案的訪問。

我們需要用到兩個結構體i2cmsg和i2crdwrioctldata。

struct i2c_msg {//i2c訊息結構體,每個i2c訊息對應一個結構體
 __u16 addr;/* 從裝置地址,此處就是eeprom地址,即0x50 */
 __u16 flags;/* 一些標誌,比如i2c讀等*/
 __u16 len;/* i2c訊息的長度 */
 __u8 *buf;/* 指向i2c訊息中的資料 */};struct i2c_rdwr_ioctl_data {struct i2c_msg __user *msgs;/* 指向一個i2c訊息 */
 __u32 nmsgs;/* i2c訊息的數量 */};

對一個eeprom上的特定地址(0x10)寫入特定資料(0x58)並在從此地址讀出寫入資料的示例程式如下所示。

#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>#include<linux/i2c.h>#include<linux/i2c-dev.h>int main(){int fd,ret;struct i2c_rdwr_ioctl_data e2prom_data;
    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);/*重複次數*//*寫eeprom*/
    e2prom_data.nmsgs=1;//由前面eeprom讀寫分析可知,寫eeprom需要一條訊息(e2prom_data.msgs[0]).len=2;//此訊息的長度為2個位元組,第一個位元組是要寫入資料的地址,第二個位元組是要寫入的資料(e2prom_data.msgs[0]).addr=0x50;//e2prom 裝置地址(e2prom_data.msgs[0]).flags=0;//寫(e2prom_data.msgs[0]).buf=(unsignedchar*)malloc(2);(e2prom_data.msgs[0]).buf[0]=0x10;// e2prom 寫入目標的地址(e2prom_data.msgs[0]).buf[1]=0x58;//寫入的資料
    ret=ioctl(fd,I2C_RDWR,(unsignedlong)&e2prom_data);//通過ioctl進行實際寫入操作,後面會詳細分析if(ret<0){
        perror("ioctl error1");}
    sleep(1);/*讀eeprom*/
    e2prom_data.nmsgs=2;//讀eeprom需要兩條訊息(e2prom_data.msgs[0]).len=1;//第一條訊息實際是寫eeprom,需要告訴eeprom需要讀資料的地址,因此長度為1個位元組(e2prom_data.msgs[0]).addr=0x50;// e2prom 裝置地址(e2prom_data.msgs[0]).flags=0;//先是寫(e2prom_data.msgs[0]).buf[0]=0x10;//e2prom上需要讀的資料的地址(e2prom_data.msgs[1]).len=1;//第二條訊息才是讀eeprom,(e2prom_data.msgs[1]).addr=0x50;// e2prom 裝置地址 (e2prom_data.msgs[1]).flags=I2C_M_RD;//然後是讀(e2prom_data.msgs[1]).buf=(unsignedchar*)malloc(1);//存放返回值的地址。(e2prom_data.msgs[1]).buf[0]=0;//初始化讀緩衝,讀到的資料放到此緩衝區
    ret=ioctl(fd,I2C_RDWR,(unsignedlong)&e2prom_data);//通過ioctl進行實際的讀操作if(ret<0){
        perror("ioctl error2");}

    printf("buff[0]=%x\n",(e2prom_data.msgs[1]).buf[0]);/***列印讀出的值,沒錯的話,就應該是前面寫的0x58了***/
    close(fd