GPIO模擬I2C通訊協議(一)
1 I2C協議簡介
I2C匯流排是由Philips公司在上世紀80年代開發的一種簡單、雙向二線制同步序列匯流排。它只需要兩根線即可在連線於總線上的器件之間傳送資訊。(本段來源於百度)
I2C既是一種匯流排,也是一種通訊協議。匯流排和通訊協議之間的關係類似於硬體和基於此硬體的軟體,同一種總線上可以跑多種協議,如在RS485總線上可以跑莫迪康的MODBUS,松下的MEWTOCOL,西門子的profibus/DP等協議;同樣地,同一種協議也可以跑在不同的總線上,如上述協議還可以跑在乙太網上。一言以蔽之,匯流排涉及的是物理層的硬體,而協議可以認為是在物理層上傳遞資訊的約定或規則。
或者也可以這麼說。在嵌入式開發中,通訊協議可分為兩層:物理層和協議層。物理層是資料在物理媒介傳輸的保障;協議層主要規定通訊邏輯,如同一收發雙方的資料打包、解包標準。打個比方,物理層相當於現實中的公路,而協議層則是交通規則,汽車可以在路上行駛,但是需要交通規則對行駛規則進行約束,不然將出現危險,也就是資料傳輸紊亂、丟包。(本段來源於部落格)
1.1 物理層
I2C匯流排協議只需要2根訊號線即可完成資料的傳輸,這兩根線分別是時鐘線SCL和訊號線SDA。I2C線上有且只有1個主裝置Master和若干個從裝置Slave,區別Master和Slave的標準是SCL,即誰是SCL的提供者,誰就是Master,而與SDA無關。這點尤其需要注意,傳送SDA不能作為區別Master和Slave的標準。I2C通訊系統連線示意如圖1所示:
關於I2C匯流排再作以下說明:
1-兩條匯流排SDA和SCL都必須接上拉電阻,這是為了確保兩條匯流排在空閒時都是高電平,上拉電阻的經典取值是10kΩ; 2-I2C總線上可以掛載多個主機和多個從機,但是同一時間只允許1個主機和1個從機進行通訊; 3-總線上每一個裝置都有一個獨立的地址,通過該地址實現通訊; 4-總線上的裝置是“線與”的關係,即任一裝置的管腳輸出低電平都可以將該管腳所在匯流排的電平拉低,線與關係是時鐘同步和匯流排仲裁的硬體基礎; 5-I2C傳輸速度有標準速度模式(SS Mode)、快速模式(FS Mode)和高速模式(HS Mode)三種,資料傳輸速率分別為100kbps、400kbps和3.4Mbps。
1.2 協議層
協議層規約了通訊的起始停止訊號、資料有效性、響應、匯流排仲裁、時鐘同步、地址廣播等內容。
1.2.1 匯流排空閒與訊號起始終止
I2C協議規定SDA和SCL都為高電平時匯流排空閒(not busy)。匯流排空閒如圖2的(A)部所示。
I2C協議規定SCL保持高電平、SDA由高變低為起始訊號(start),所有命令和資料的傳輸必須以起始訊號為首。起始訊號如圖2的(B)部所示。
I2C協議規定SCL保持高電平、SDA由低變高為終止訊號(stop)。所有命令和資料的傳輸必須以終止訊號為尾。起始訊號如圖2的©部所示。
1.2.2 資料有效
I2C協議規定在總線上出現起始訊號start後,若SCL在高電平期間SDA保持電平不變,則SDA的狀態表示有效資料(data valid)。在傳輸資料時SDA的改變必須只能發生在SCL為低電平期間,每一bit資料有1個時鐘脈衝時長。資料有效如圖2的(D)部所示。
1.2.3 應答和非應答
I2C協議規定每個被定址裝置在接收1位元組資料後都必須向傳送位元組的裝置傳送應答(ACK)訊號,確認的器件必須在應答時鐘脈衝期間下拉SDA線,使得SDA線在應答相關時鐘脈衝SCL為高電平期間穩定為低電平。
I2C協議規定與ACK訊號相反的訊號為非應答(not ACK)訊號。在主器件從從器件中讀取資料時,主器件必須在讀取的最後1位元組資料後在SDA總線上產生not ACK訊號以示意從器件停止傳送資料。not ACK訊號是在SCL為高電平期間保持SDA也為高電平。
1.2.4 地址廣播
地址廣播是I2C協議規定的定址方式。它是指主裝置在產生start訊號後,各個從裝置開始關注匯流排SDA訊號,此時主裝置在總線上生成需接受/傳送資料的從裝置的地址(Address),相當於向總線上所有從裝置廣播了這一地址。每個從裝置將總線上的地址與自己的地址相對比,不一致的退出接收,一致的繼續接收,直到8bit地址資料廣播完畢,仍然留下的那一個從裝置就是主裝置的定址目標。
1.2.5 匯流排仲裁
匯流排仲裁解決的是多個主裝置競爭使用同一匯流排的問題。下面舉例說明:
假設主控器1要傳送的資料DATA1為“101 ……”;主控器2要傳送的資料DATA2為“1001 ……”匯流排被啟動後兩個主控器在每傳送一個數據位時都要對自己的輸出電平進行檢測,只要檢測的電平與自己發出的電平一致,他們就會繼續佔用匯流排。在這種情況下匯流排還是得不到仲裁。當主控器1傳送第3位資料“1”時(主控器2傳送“0” ),由於“線與”的結果SDA上的電平為“0”,這樣當主控器1檢測自己的輸出電平時,就會測到一個與自身不相符的“0”電平。這時主控器1只好放棄對匯流排的控制權;因此主控器2就成為匯流排的唯一主宰者。(例項來自部落格)
從中可以得出:參與仲裁的所有主控器都不會丟失資料;參與仲裁的所有主控器沒有固定的優先級別,而是遵循低電平優先的原則。
1.2.6 時鐘同步
時鐘同步是用來解決中控器和被控器的資料傳輸速率不相同的問題。
被控器可以通過將SCL主動拉低並延長其低電平時間的方法來通知主控器,當主控器在準備下一次傳送時發現SCL為低電平,就會等待,直至被控器完成操作並釋放SCL線的控制控制權。這樣,主控器實際上受到被控器的時鐘同步控制。由此可見,SCL線上的低電平是由時鐘低電平最長的器件決定,高電平的時間由高電平時間最短的器件決定。
需要說明的是,不管是匯流排仲裁還是時鐘同步,它們得以實現的基礎是SDA匯流排的“線與”性質,而這是由I2C匯流排獨特的IO結構決定的。另外,匯流排仲裁和時鐘同步之間並不存在特定的先後關係,它們往往同時發生。
2 I2C協議的C程式碼實現
下面將用C語言實現第1章所描述的I2C匯流排協議的各個動作,並將這些分散的動作整合起來實現位元組的讀寫操作。
首先需要在微控制器上定義2個IO口以連線兩根匯流排SDA和SCL,這個可隨意取,只要是IO口都行。我的取值如下:
#define SDA IO_CONFIG_PB0
#define SCL IO_CONFIG_PB1
2.1 單個動作
2.1.1 初始化
初始化的效果是SDA和SCL總線上全部呈現高電平,由於兩根線都已連線上拉電阻,且埠的IO是開漏極,因此只需要將它們的方向都設為input即可。
void i2c_init(void)
{
io_func_config(SDA, IO_FUNC_GPIO); // choose IO_CONFIG_PB0 as GPIO
io_func_config(SCL, IO_FUNC_GPIO); // choose IO_CONFIG_PB1 as GPIO
io_input(SDA); // SDA input
io_input(SCL); // SCL input
delay_us(20);
}
2.1.2 起始訊號
用GPIO模擬起始訊號,SCL保持高電平、SDA由高變低。
void i2c_start(void)
{
io_output(SDA); // SDA output
io_output(SCL); // SCL output
io_set_high(SDA); // SDA=1
io_set_high(SCL); // SCL=1
delay_us(5);
io_set_low(SDA); // SDA=0
delay_us(5);
}
2.1.3 終止訊號
用GPIO模擬起始訊號,SCL保持高電平、SDA由低變高。
void i2c_stop(void)
{
io_output(SDA); // set SDA as input
//io_output(SCL); // set SCL as input
if (io_get(SCL) == 1)
io_set_low(SCL); // SDA=0
if (io_get(SDA) == 1)
io_set_low(SDA); // SDA=0
delay_us(5);
io_set_high(SCL); // SCL=1
delay_us(5);
io_set_high(SDA); // SDA=1
delay_us(5);
// release SDA and SCL
io_input(SDA); // set SDA as input
io_input(SCL); // set SCL as input
}
2.1.4 主控器讀取ACK
微控制器讀取ACK對應的IO口處的電平。
uint8_t i2c_read_ack(void)
{
uint8_t ack;
io_input(SDA); // SDA input
io_set_high(SCL); // SCL=1
ack = io_get(SDA);
delay_us(5);
io_set_low(SCL); // SCL=0
delay_us(5);
return ack;
}
2.1.5 主控器傳送ACK
微控制器向ACK對應的IO口傳送低電平。
void i2c_send_ack(void)
{
io_output(SDA);
io_set_low(SCL); // SCL=0
io_set_low(SDA); // SDA=0
delay_us(5);
io_set_high(SCL); // SCL=1
delay_us(5);
// TAKE CAREFULLY!!!These two orders below must be included
// to pull down the SCL for the following opreations.
io_set_low(SCL); // SCL=0
delay_us(5);
}
2.1.6 主控器傳送not ACK
微控制器向ACK對應的IO口傳送高電平。
void i2c_send_nack(void)
{
io_output(SDA);
io_set_low(SCL); // SCL=0
io_set_high(SDA); // SDA=1
delay_us(5);
io_set_high(SCL); // SCL=1
delay_us(5);
io_set_low(SCL); // SCL=0
delay_us(5);
}
2.1.7 主控器檢查是否接收到ACK
主控器在發完第1個地址位元組後,按規定被控期需要向主控器回覆一個ACK訊號,主控器如果能在總線上檢測到這個ACK,就繼續向被控器傳送資料,否則視為本次資料傳送失敗。
uint8_t i2c_ack_check(uint8_t ctrl_byte)
{
i2c_start();
i2c_write_single_byte(ctrl_byte);
if(i2c_read_ack() == 0)
{
//sl_printf("i2c_read_ack() == 0.\n");
// time delay here is not necessary, just to make waveforms more readable
delay_us(30);
//i2c_stop();
io_input(SDA); // set SDA as input
io_input(SCL); // set SCL as input
return 0;
}
else
{
//sl_printf("i2c_read_ack() == 1.\n");
// time delay here is to save computing resource
delay_us(100);
//io_input(SDA); // set SDA as input
//io_input(SCL); // set SCL as input
return 1;
}
}
以上就是所謂的“單個動作”,是形成I2C功能的最基本的單元。上述動作經過各種組合即可實現I2C的基本操作:單位元組的讀寫。
2.2 組合動作:位元組讀寫
如2.1節所述,本節介紹2種最基本的“組合動作”——單位元組的讀寫
2.2.1 單位元組讀
單位元組讀的程式碼如下:
uint8_t i2c_read_single_byte(void)
{
uint8_t i=8;
uint8_t i2c_buff = 0x0;
// 每次read,最開始總是高電平,即使MSB is low 也要先高再低(在SCL=0期間有一個小凸起)
// Is that because of setting SDA as input, making it HIGH at the very beginning? Maybe so.
delay_us(5);
io_input(SDA); // SDA input
delay_us(5);
while (i--)
{
i2c_buff = i2c_buff<<1;
io_set_high(SCL); // SCL=1
//
if(io_get(SDA)==1)
i2c_buff |= 0x01; // Write 1 to LSB of i2c_buff
else
i2c_buff &= 0xFE; // Write 0 to LSB of i2c_buff
delay_us(5);
io_set_low(SCL); // SCL=0
delay_us(5);
//i2c_buff = i2c_buff<<1; // move to the next MSB(from MSB to LEB)
}
//sl_printf("i2cbuf=%d\n", i2c_buff);
return i2c_buff;
}
它的邏輯是:
初始化—主控器傳送起始訊號—主控器逐bit讀取SDA線上訊號。
2.2.2 單位元組寫
單位元組寫的程式碼如下:
void i2c_write_single_byte(uint8_t i2c_buff)
{
uint8_t i=8;
io_output(SDA); // SDA output
io_output(SCL); // SCL output
while (i--)
{
io_set_low(SCL); // SCL=0
delay_us(5);
if(i2c_buff & 0x80) // MSB(i2c_buff)==1
io_set_high(SDA); // SDA=1
else
io_set_low(SDA); // SDA=0
io_set_high(SCL); // SCL=1
delay_us(5);
i2c_buff = i2c_buff<<1; // move to the next MSB(from MSB to LEB)
}
// After transfer, release the SCL line
io_set_low(SCL); // SCL=0
delay_us(5);
}
它的邏輯是:
初始化—主控器傳送起始訊號—主控器逐bit往SDA線上寫資料。
3 小結
本次內容只介紹了GPIO模擬I2C協議的最底層,即在總線上完成訊號讀寫的時序。有以下幾點需要注意:
1-程式碼是從主控器的角度寫的; 2-這裡沒有ACK是因為這還遠不是主從裝置之間的資料傳輸; 3-總線上單個位元組的讀寫不等於主從裝置之間單個位元組的傳輸,主從裝置之間單個位元組的傳輸比這複雜,將在下節講到。
從驅動開發的角度來看,今天完成的正是驅動開發的最底層:完全關注於協議本身的時序邏輯而不涉及具體器件。比如主微控制器通過I2C連線從裝置諸如Flash、E2PROM、ADC、DAC、SRAM和從微控制器等等,這些屬於更高層的API,下次部落格會講到。
最後需要提到的一點是所謂“分層的思想”,在整個實習過程中我感到這在驅動開發工作中是一個非常重要的思想。分層即封裝,將一個完整的驅動設定為若干層,每一層只關注本層的內容,實現本層計劃實現的功能,並給更高層的驅動程式碼提供API。這樣一個複雜的驅動開發工作就會變得簡單,並且也更利於多人協作。
4 後記
部落格貼上的程式碼中提到的函式諸如io_set_low()、io_set_high()、io_output()、io_input()等都是對IO的基本操作,基本是顧名思義的。這些操作在每款開發板中都會有提供,或是位操作、或是暫存器操作、或是函式操作等等。有趣的是,這些提供的IO操作函式本身也屬於API。
下篇部落格將涉及E2PROM作為I2C匯流排的被控器並實現E2PROM與主控器(微控制器)之間的資料傳輸。
轉載時務必註明來源及作者。尊重智慧財產權從我做起。