天天看點

I2C、I2S、SPI、GPIO模拟I2C學習筆記

1.  什麼是 I2C總線

NXP 半導體(原 Philips半導體)于 20 多年前發明了一種簡單的雙向二線制串行通信總線,這個總線被稱為 Inter-IC 或者 I2C 總線。目前 I2C 總線已經成為業界嵌入式應用的标準解決方案,被廣泛地應用在各式各樣基于微控器的專業、消費與電信産品中,作為控制、診斷與電源管理總線。多個符合 I2C 總線标準的器件都可以通過同一條 I2C總線進行通信,而不需要額外的位址譯碼器。由于 I2C是一種兩線式串行總線,是以簡單的操作特性成為它快速崛起成為業界标準的關鍵因素。 

2. I2C總線的衆多優秀特點 

    總線僅由 2根信号線組成   由此帶來的好處有:節省晶片 I/O、節省 PCB 面積、節省線材成本,等等。 

    總線協定簡單   I2C 總線的協定原文有好幾十頁,如果直接讓初學者來看确實頭大,但是并不意為着 I2C 總線協定本身就複雜。 本文撰寫的目的就是服務于廣大初學者,僅數頁的正式内容,圖文并茂,容易入門。相信讀者認真看過之後,就能基本上掌握 I2C 總線的要領,為進一步操控具體的器件打下良好的基礎。 

    協定容易實作   得益于簡單的協定規範,在晶片内部,以硬體的方法實作 I2C 部件的邏輯是很容易的。對應用工程師來講,即使 MCU内部沒有硬體的 I2C總線接口,也能夠友善地利用開漏的 I/O(如果沒有,可用準雙向 I/O代替)來模拟實作。 

    支援的器件多   NXP 半導體最早提出 I2C 總線協定,目前包括半導體巨頭德州儀器(TI) 、 美國國家半導體 (National Semi) 、 意法半導體 (ST) 、 美信半導體 (Maxim-IC)等都有大量器件帶有 I2C 總線接口,這為應用工程師設計産品時選擇合适的 I2C 器件提供了廣闊的空間。 

    總線上可同時挂接多個器件   同一條 I2C 總線上可以挂接很多個器件,一般可達數十個以上,甚至更多。器件之間是靠不同的編址來區分的,而不需要附加的 I/O 線或位址譯碼部件。 

    總線可裁減性好   在原有總線連接配接的基礎上可以随時新增或者删除器件。用軟體可以很容易實作 I2C 總線的自檢功能,能夠及時發現總線上的變動。 

    總線電氣相容性好   I2C 總線規定器件之間以開漏 I/O 互聯,這樣,隻要選取适當的上拉電阻就能輕易實作 3V/5V邏輯電平的相容,而不需要額外的轉換。 

    支援多種通信方式   一主多從是最常見的通信方式。此外還支援雙主機通信、多主機通信以及廣播模式等等。 

    通信速率高   I2C 總線标準傳輸速率為 100kbps(每秒 100k 位) 。在快速模式下為400kbps。按照後來修訂的版本,位速率可高達 3.4Mbps。 

    兼顧低速通信   I2C總線的通信速率也可以低至幾kbps以下, 用以支援低速器件 (比如軟體模拟的實作)或者用來延長通信距離。 

    有一定的通信距離   一般情況下,I2C 總線通信距離有幾米到十幾米。通過降低傳輸速率等辦法,通信距離可延長到數十米乃至數百米以上。

3. I2C總線的信号線 

I2C 總線隻需要由兩根信号線組成, 一根是串行資料線 SDA, 另一根是串行時鐘線 SCL。在系統中,I2C 總線的典型接法如圖 1 所示,注意連接配接時需要共地。 

一般具有 I2C總線的器件其 SDA和 SCL 管腳都是漏極開路 (或集電極開路) 輸出結構。是以實際使用時,SDA 和 SCL 信号線都必須要加上拉電阻 Rp(Pull-Up Resistor) 。上拉電阻一般取值 3~10KΩ。開漏結構的好處是: 

    當總線空閑時,這兩條信号線都保持高電平,不會消耗電流。 

    電氣相容性好。上拉電阻接 5V電源就能與 5V邏輯器件接口,上拉電阻接 3V電源又能與 3V邏輯器件接口。 

    因為是開漏結構,是以不同器件的 SDA 與 SDA 之間、SCL 與 SCL 之間可以直接相連,不需要額外的轉換電路。 

4. I2C總線的基本概念 

    發送器(Transmitter) :發送資料到總線的器件 

    接收器(Receiver) :從總線接收資料的器件 

    主機(Master) :初始化發送、産生時鐘信号和終止發送的器件 

    從機(Slave) :被主機尋址的器件 

I2C 總線是雙向傳輸的總線,是以主機和從機都可能成為發送器和接收器。如果主機向從機發送資料,則主機是發送器,而從機是接收器;如果主機從從機讀取資料,則主機是接收器,而從機是發送器。不論主機是發送器還是接收器,時鐘信号 SCL 都要由主機來産生。  

 5. I2C總線資料傳送速率 

I2C 總線的通信速率受主機控制,能快能慢。但是最高速率是有限制的,I2C 總線上資料的傳輸速率在标準模式(Standard-mode)下為 100kbps(每秒 100k 位) ,在快速模式下為400kbps。按照後來修訂的版本,位速率最高可達 3.4Mbps。

6. I2C總線上資料的有效性(Data validity) 

    資料線 SDA 的電平狀态必須在時鐘線 SCL 處于高電平期間保持穩定不變。SDA 的電平狀态隻有在 SCL 處于低電平期間才允許改變。但是在 I2C總線的起始和結束時例外。 

注:某些其它的串行總線協定,如 SPI,可能規定資料在時鐘信号的邊沿(上升沿或下降沿)有效,而I2C總線則是低電平有效。 

7.  起始條件和停止條件(START and STOP conditions) 

    起始條件 當 SCL 處于高電平期間時,SDA 從高電平向低電平跳變時産生起始條件。總線在起始條件産生後便處于忙的狀态。起始條件常常簡記為 S。 

    停止條件 當 SCL 處于高電平期間時,SDA 從低電平向高電平跳變時産生停止條件。總線在停止條件産生後處于空閑狀态。停止條件簡記為 P。 

8.  從機位址(Slave Address) 

    I2C 總線不需要額外的位址譯碼器和片選信号。多個具有 I2C 總線接口的器件都可以連接配接到同一條 I2C 總線上,它們之間通過器件位址來區分。主機是主要器件,它不需要器件位址,其它器件都屬于從機,要有器件位址。必須保證同一條 I2C 總線上所有從機的位址都是唯一确定的,不能有重複,否則 I2C總線将不能正常工作。一般從機位址由 7 位位址位和一位讀寫标志 R/W 組成,7位位址占據高 7 位,讀寫位在最後。讀寫位是 0,表示主機将要向從機寫入資料;讀寫位是 1,則表示主機将要從從機讀取資料。 

9.  資料是按位元組傳輸的 

    I2C 總線總是以位元組(Byte)為機關收發資料。每次傳輸的位元組數量沒有嚴格限制。首先傳輸的是資料的最高位(MSB,第 7 位) ,最後傳輸的是最低位(LSB,第 0 位) 。另外,每個位元組之後還要跟一個響應位,稱為應答。 

10.  應答(Acknowledge) 

    在 I2C 總線傳輸資料過程中,每傳輸一個位元組,都要跟一個應答狀态位。接收器接收資料的情況可以通過應答位來告知發送器。應答位的時鐘脈沖仍由主機産生,而應答位的資料狀态則遵循“誰接收誰産生”的原則,即總是由接收器産生應答位。主機向從機發送資料時,應答位由從機産生;主機從從機接收資料時,應答位由主機産生。I2C總線标準規定:應答位為 0 表示接收器應答(ACK) ,常常簡記為 A;為 1 則表示非應答(NACK) ,常常簡記為A。發送器發送完 LSB 之後,應當釋放 SDA 線(拉高 SDA,輸出半導體截止) ,以等待接收器産生應答位。 

    如果接收器在接收完最後一個位元組的資料,或者不能再接收更多的資料時,應當産生非應答來通知發送器。發送器如果發現接收器産生了非應答狀态,則應當終止發送。 

 11.  基本的資料傳輸格式 

在圖 4 和圖 5 中,各種符号的意義為: 

    S:起始位(START) 

SA:從機位址(Slave Address) ,7 位從機位址 

W:寫标志位(Write) ,1 位寫标志 

R:讀标志位(Read) ,1位讀标志 

A:應答位(Acknowledge) ,1 位應答 

A:非應答位(Not Acknowledge) ,1位非應答 

D:資料(Data) ,每個資料都必須是 8 位 

P:停止位(STOP) 

陰影:主機産生的信号 

無陰影:從機産生的信号 

  應當注意的是,與圖 5 中的情況不同的是,在圖 4 中,主機向從機發送最後一個位元組的資料時,從機可能應答也可能非應答,但不管怎樣主機都可以産生停止條件。如果主機在向從機發送資料(甚至包括從機位址在内)時檢測到從機非應答,則應當及時停止傳輸。 

12.  傳輸一個位元組資料的時序圖 

為了更清楚地了解I2C總線的基本資料傳輸過程, 下面畫出了隻傳輸1個位元組的時序圖,這是最基本的傳輸方式。在圖 6 和圖 7 中,SDA信号線被畫成了兩條,一個是主機産生的,另一個是從機産生的。 實際上主機和從機的 SDA 信号線總是連接配接在一起的, 是同一根 SDA。畫成兩個 SDA有助于深刻了解在 I2C總線上主機和從機的不同行為。 

13.  傳輸多個位元組資料的時序圖 

主機連續向從機發送或從從機接收多個位元組資料的情況也很容易了解, 下面直接給出相關時序圖。 

14.  重複起始條件(Repeated START condition) 

主機與從機進行通信時,有時需要切換資料的收發方向。例如,通路某一具有 I2C總線接口的 E2PROM 存儲器時,主機先向存儲器輸入存儲單元的位址資訊(發送資料) ,然後再讀取其中的存儲内容(接收資料) 。 

在切換資料的傳輸方向時,可以不必先産生停止條件再開始下次傳輸,而是直接再一次産生開始條件。I2C 總線在已經處于忙的狀态下,再一次直接産生起始條件的情況被稱為重複起始條件。重複起始條件常常簡記為 Sr。 

正常的起始條件和重複起始條件在實體波形上并沒有什麼不同,差別僅僅是在邏輯方面。在進行多位元組資料傳輸過程中,隻要資料的收發方向發生了切換,就要用到重複起始條件。 

   圖 10 給出了帶有重複起始條件的多位元組資料傳輸格式示意圖。要特别注意圖中重複起始條件 Sr 的用法。如果讀者有興趣的話,可以自行畫出其對應的時序圖。 

15.  子位址 

帶有 I2C 總線的器件除了有從機位址(Slave Address)外,還可能有子位址。從機位址是指該器件在 I2C 總線上被主機尋址的位址, 而子位址是指該器件内部不同部件或存儲單元的編址。例如,帶 I2C 總線接口的 E2PROM 就是擁有子位址器件的典型代表。 

某些器件(隻占少數)内部結構比較簡單,可能沒有子位址,隻有必須的從機位址。 與從機位址一樣,子位址實際上也是像普通資料那樣進行傳輸的,傳輸格式仍然是與資料相統一的,區分傳輸的到底是位址還是資料要靠收發雙方具體的邏輯約定。子位址的長度必須由整數個位元組組成,可能是單位元組(8 位子位址) ,也可能是雙位元組(16 位子位址) ,還可能是 3 位元組以上,這要看具體器件的規定。

_____________________________________________________________________________________________________________________________

I2S(Inter-IC Sound Bus)是飛利浦公司為數字音頻裝置之間的音頻 資料傳輸而制定的一種總線标準。

I2S有3個主要信号:1.串行時鐘SCLK,也叫位時鐘(BCLK),即對應數字音頻的每一位資料,SCLK都有1個脈沖。SCLK的頻率=2×采樣頻率×采樣位數  2.幀時鐘LRCK,用于切換左右聲道的資料。LRCK為“1”表示正在傳輸的是左聲道的資料,為“0”則表示正在傳輸的是右聲道的資料。LRCK的頻率等于采樣頻率。3.串行資料SDATA,就是用二進制補碼表示的音頻資料。I2S(Inter-IC Sound Bus)是飛利浦公司為數字音頻裝置之間的音頻資料傳輸而制定的一種總線标準。在飛利浦公司的I2S标準中,既規定了硬體接口規範,也規定了數字音頻資料的格式。I2S有3個主要信号:1.串行時鐘SCLK,也叫位時鐘(BCLK),即對應數字音頻的每一位資料,SCLK都有1個脈沖。SCLK的頻率=2×采樣頻率×采樣位數  2. 幀時鐘LRCK,用于切換左右聲道的資料。LRCK為“1”表示正在傳輸的是左聲道的資料,為“0”則表示正在傳輸的是右聲道的資料。LRCK的頻率等于采樣頻率。3.串行資料SDAT就是用二進制補碼表示的音頻資料。

一個典型的I2S信号見圖3。

     I2S格式的信号無論有多少位有效資料,資料的最高位總是出現在LRCK變化(也就是一幀開始)後的第2個SCLK脈沖處。這就使得接收端與發送端的有效位數可以不同。如果接收端能處理的有效位數少于發送端,可以放棄資料幀中多餘的低位資料;如果接收端能處理的有效位數多于發送端,可以自行補足剩餘的位。這種同步機制使得數字音頻裝置的互連更加友善,而且不會造成資料錯位。

______________________________________________________________________________________________________________________________

SPI:串行外圍裝置接口SPI(serial peripheral interface)總線技術是Motorola公司推出的一種同步串行接口,Motorola公司生産的絕大多數MCU(微控制器)都配有SPI硬體接口,如68系列MCU。SPI用于CPU與各種外圍器件進行全雙工、同步串行通訊。SPI可以同時發出和接收串行資料。它隻需四條線就可以完成MCU與各種外圍器件的通訊,這四條線是:串行時鐘線(CSK)、主機輸入/從機輸出資料線(MISO)、主機輸出/從機輸入資料線(MOSI)、低電平有效從機選擇線CS。這些外圍器件可以是簡單的TTL移位寄存器,複雜的LCD顯示驅動器,A/D、D/A轉換子系統或其他的MCU。當SPI工作時,在移位寄存器中的資料逐位從輸出引腳(MOSI)輸出(高位在前),同時從輸入引腳(MISO)接收的資料逐位移到移位寄存器(高位在前)。發送一個位元組後,從另一個外圍器件接收的位元組資料進入移位寄存器中。主SPI的時鐘信号(SCK)使傳輸同步。其典型系統框圖如下圖所示。

圖2示出SPI總線工作的四種方式,其中使用的最為廣泛的是SPI0和SPI3方式(實線表示):、

SPI子產品為了和外設進行資料交換,根據外設工作要求,其輸出串行同步時鐘極性和相位可以進行配置,時鐘極性(CPOL)對傳輸協定沒有重大的影響。如果 CPOL=0,串行同步時鐘的空閑狀态為低電平;如果CPOL=1,串行同步時鐘的空閑狀态為高電平。時鐘相位(CPHA)能夠配置用于選擇兩種不同的傳輸協定之一進行資料傳輸。如果CPHA=0,在串行同步時鐘的第一個跳變沿(上升或下降)資料被采樣;如果CPHA=1,在串行同步時鐘的第二個跳變沿(上升或下降)資料被采樣。SPI主子產品和與之通信的外設音時鐘相位和極性應該一緻。SPI總線接口時序如圖所示。

____________________________________________________________________________________________________________________________

以下為用兩個GPIO口模拟I2C總線的程式:

#ifndef I2C_H

#define I2C_H

// Proto definition

void i2cStart(void);

void i2cStop (void);

bool i2cGetAck(void);

void i2cPutAck(bool isACK);

void i2cSendByte(u8 data);

u8   i2cGetByte (void);

bool i2c_Open(u8 scl,u8 sda);

void i2c_Close(void);

bool i2c_SendData(u8 Addr, u8 *dataBuffer, u8 Size);

bool i2c_GetData(u8 Addr, u8 *dataBuffer, u8 Size);

#endif // I2C_H

gpio_i2c.c

#include "drv_i2c.h"

#if 0

#include "io.h"

#define i2c_fprintf(x) sxs_fprintf x

#else

#define i2c_fprintf(x)

#endif

// Global variant

u32 I2C_SDA;

u32 I2C_SCL;

u32 I2C_BUS;

// Delay Time (Max I2C = 400 KHz)

// 20 ---> 195 KHz

// 10 ---> 355 KHz

#define DELAY 20

void i2cDelay(u16 cnt)

{

    u16 i;

    for (i=0;i<cnt;i++);

}

// I2C Extern Interface API

bool i2c_Open(u8 scl,u8 sda)

{

 i2c_fprintf((TSTDOUT, "Open I2C Bus."));

 I2C_SCL = 1 << scl;

 I2C_SDA = 1 << sda;

 I2C_BUS = I2C_SCL | I2C_SDA;

    //if ((I2C_SDA & USED_GPIO)==0 || (I2C_SCL & USED_GPIO)==0)

    //{

    //    i2c_fprintf((TSTDOUT, "I2C GPIO Used Wrong in Board Config."));

    //    return(FALSE);

    //}

    //else

    {

        //i2c_fprintf((TSTDOUT, "I2C GPIO Used Right in Board Config."));

        i2c_fprintf((TSTDOUT, "I2C_SDA is GPIO 0x%x", I2C_SDA));

        i2c_fprintf((TSTDOUT, "I2C_SCL is GPIO 0x%x", I2C_SCL));

     // Set GPIO as output and set output=1

     hal_gpio_SetOut(I2C_BUS); // Pay attention that SCL is always output and

     hal_gpio_SetBit(I2C_BUS); // SDA can be output or input. So please make sure

                               // the SDA pin direction first before used.

        return(TRUE);

    }

}

void i2c_Close(void)

{

    hal_gpio_SetOut(I2C_BUS);

    hal_gpio_SetBit(I2C_BUS);

}

// The Format of I2C Write Telgram

// |Mast |  M->S   |M->S |S->M |     M->S       |S->M |     M->S      |S->M |Mast|

// |Start| 7 bits  | R/W | ACK |     8 bits     | ACK |     8 bits    |N/ACK|STOP|

// |Start|SlaveAddr|WRITE| ACK |DATA - HIGH BYTE| ACK |DATA - LOW BYTE|N/ACK|STOP|

bool i2c_SendData(u8 Addr, u8 *dataBuffer, u8 Size)

{

    u8  bufSize = Size;

    i2cStart();

    i2cSendByte(Addr<<1);

    if (!i2cGetAck())

    {

        i2cStop();

        return FALSE;

    }

    do

    {

        i2cSendByte(*dataBuffer++);

        if (!i2cGetAck())

        {

         i2cStop();

         return FALSE;

        }

    }

    while((--bufSize)!=0);

    i2cStop();

    return TRUE;

}

// The Format of I2C Write Telgram

// |Mast |  M->S   |M->S|S->M |     S->M       |M->S |     S->M      |M->S|Mast|

// |Start| 7 bits  |R/W | ACK |     8 bits     | ACK |     8 bits    |NACK|STOP|

// |Start|SlaveAddr|READ| ACK |DATA - HIGH BYTE| ACK |DATA - LOW BYTE|NACK|STOP|

bool i2c_GetData(u8 Addr, u8 *dataBuffer, u8 Size)

{

    u8 bufSize = Size;

    i2cStart();

    i2cSendByte((Addr<<1)|0x1);

    if (!i2cGetAck())

    {

        i2cStop();

        return FALSE;

    }

    do

    {

     *dataBuffer++ = i2cGetByte();

     i2cPutAck(TRUE);

    }

    while((--bufSize)!=1);

    *dataBuffer = i2cGetByte();

    i2cPutAck(FALSE);

    i2cStop();

    return (TRUE);

}

// I2C Internal API

void i2cStart(void)

{

    hal_gpio_SetOut(I2C_SDA);

    hal_gpio_SetBit(I2C_SDA);

    hal_gpio_SetBit(I2C_SCL);

    i2cDelay(DELAY);

    hal_gpio_ClrBit(I2C_SDA);

    i2cDelay(DELAY/2);

    hal_gpio_ClrBit(I2C_SCL);

    i2cDelay(DELAY/2);

}

void i2cStop(void)

{

    hal_gpio_SetOut(I2C_SDA);

    hal_gpio_ClrBit(I2C_SDA);

    i2cDelay(DELAY/2);

    hal_gpio_SetBit(I2C_SCL);

    i2cDelay(DELAY/2);

    hal_gpio_SetBit(I2C_SDA);

    // After STOP, I2C_BUS goes into IDLE state.

}

void i2cSendByte(u8 data)

{

    u8 cnt = 8;

    hal_gpio_SetOut(I2C_SDA);

    do

    {

        if (data & 0x80)

        {

            hal_gpio_SetBit(I2C_SDA);

        }

        else

        {

            hal_gpio_ClrBit(I2C_SDA);

        }

        data<<=1;

        i2cDelay(DELAY/2);

        hal_gpio_SetBit(I2C_SCL);

        i2cDelay(DELAY);

        hal_gpio_ClrBit(I2C_SCL);

        i2cDelay(DELAY/2);

    }

    while (--cnt != 0);

}

u8 i2cGetByte(void)

{

    u8 cnt = 8;

    u8 data = 0;

//    hal_gpio_SetOut(I2C_SDA);

//    hal_gpio_SetBit(I2C_SDA); // Should pull up SDA before read.

//    i2cDelay(DELAY);

    hal_gpio_SetIn(I2C_SDA); // Set GPIO as input

    i2cDelay(DELAY/2);

    do

    {

     hal_gpio_SetBit(I2C_SCL);

     i2cDelay(DELAY/2);

     data<<=1;

     if (hal_gpio_GetVal(I2C_SDA)) data++; // Get input data bit

     i2cDelay(DELAY/2);

     hal_gpio_ClrBit(I2C_SCL);

     i2cDelay(DELAY);

    }

    while (--cnt != 0);

    return(data);

}

bool i2cGetAck(void)

{

    bool isACK;

    hal_gpio_SetIn(I2C_SDA);

    hal_gpio_ClrBit(I2C_SCL);

    i2cDelay(DELAY/2);

    hal_gpio_SetBit(I2C_SCL);

    i2cDelay(DELAY/2);

    // Read Line at the middle of input, it is stable.

    if (hal_gpio_GetVal(I2C_SDA)) isACK = FALSE;

    else                          isACK = TRUE;

    i2cDelay(DELAY/2);

    hal_gpio_ClrBit(I2C_SCL);

    i2cDelay(DELAY/2);

    return isACK;

}

void i2cPutAck(bool isACK)

{

    hal_gpio_SetOut(I2C_SDA);

    if (isACK) hal_gpio_ClrBit(I2C_SDA);

    else       hal_gpio_SetBit(I2C_SDA);

    i2cDelay(DELAY/2);

    hal_gpio_SetBit(I2C_SCL);

    i2cDelay(DELAY);

    hal_gpio_ClrBit(I2C_SCL);

    i2cDelay(DELAY/2);

}

參考:衆發米業www.hubeidami.sinaapp.com