[技術] 用GPIO實做I2C介面(Bit-Banging)

Written on 12:00 上午 by Yu Lai

最近工作上需要去讀寫I2C介面的IC,
是顆用來量測環境溫度的IC-LM75。
I2C是由2支腳-SDA和SCL所組成的並聯介面,
所有I2C的IC都接在這2支腳上,
I2C相關資訊可以自行到Googlewiki找找。

而我們的板子的CPU本身沒有I2C的介面,
它是透過2支GPIO腳來連接LM75,
所以就必須以軟體的方式來控制GPIO來模擬I2C的Signal,
以上的實做也被稱做Bit-Banging。

照著LM75的Datasheet和網路上相關的資料,
還蠻順利的完成整個Bit-Banging的實做。

以下就是相關實做的source code。

#define SCL_PORT  6
#define SDA_PORT 23

#define ADDR_LM75 0x94 /* 1001 010 x */
#define I2C_DELAY_TIME 10 /* us */

#define ACK 1
#define NO_ACK 0

首先是會用到的#define。
SCL腳位是使用GPIO6,SDA是使用GPIO23。
而LM75使用的Address則是固定的1001配上
板子上A2,A1,A0腳位所接的電位的010當前7個bit。
另外,由於I2C算是比較慢的介面,所以要有個delay time。
void i2c_init(void) {

/* Enable SDA & SCL gpio port */
gpio_enable(SDA_PORT);
gpio_enable(SCL_PORT);

/* Set SDA & SCL to High */
gpio_set(SDA_PORT, __HIGH__);
gpio_set(SCL_PORT, __HIGH__);

}

這裡是將GPIO腳位設定啟動,並將SDA和SCL電位設成HIGH,
完成Initial的動作。
void i2c_start(void) {

/* I2C start sequence is defined as
* a High to Low Transition on the data
* line as the CLK pin is high */

gpio_set(SDA_PORT, __HIGH__); /* SDA: High */
gpio_set(SCL_PORT, __HIGH__); /* SCL: High */
HAL_DELAY_US(I2C_DELAY_TIME);

gpio_set(SDA_PORT, __LOW__); /* SDA: Low */
gpio_set(SCL_PORT, __LOW__); /* SCL: Low */
HAL_DELAY_US(I2C_DELAY_TIME);

}

void i2c_stop(void) {

/* I2C stop sequence is defined as
* data pin is low, then CLK pin is high,
* finally data pin is high. */

gpio_set(SDA_PORT, __LOW__); /* SDA: Low */
gpio_set(SCL_PORT, __HIGH__); /* SCL: High */
gpio_set(SDA_PORT, __HIGH__); /* SDA: High */

}

如source code裡我comment所寫的,
I2C介面溝通的start就是SDA和SCL都是由本來保持的High變成Low所開始。
而stop就是SDA和SCL由High-Low傳遞資料之間變為High持續下去。
void i2c_write(unsigned char data) {

/* An I2C output byte is bits 7-0
* (MSB to LSB). Shift one bit at a time
* to the MDO output, and then clock the
* data to the I2C Slave */

unsigned char i;

/* Write to slave */
for(i = 0; i < 8; i++) {
gpio_set(SDA_PORT, (data&0x80)?1:0); /* Send data bit */
data <<= 1; /* Shift one bit */
gpio_set(SCL_PORT, __HIGH__); /* SCL: High */
HAL_DELAY_US(I2C_DELAY_TIME);
gpio_set(SCL_PORT, __LOW__); /* SCL: Low */
HAL_DELAY_US(I2C_DELAY_TIME);
}

/* Read ACK bit from slave */
gpio_get(SDA_PORT);
gpio_set(SCL_PORT, __HIGH__); /* SCL: High */
HAL_DELAY_US(I2C_DELAY_TIME);
gpio_set(SCL_PORT, __LOW__); /* SCL: Low */
HAL_DELAY_US(I2C_DELAY_TIME);

}

unsigned char i2c_read(unsigned char send_ack) {

unsigned char i, data;

data = 0x00;

/* Read from slave */
for(i = 0; i < 8; i++) {
data <<= 1; /* Shift one bit */
data |= gpio_get(SDA_PORT); /* Read data bit */
gpio_set(SCL_PORT, __HIGH__); /* SCL: High */
HAL_DELAY_US(I2C_DELAY_TIME);
gpio_set(SCL_PORT, __LOW__); /* SCL: Low */
HAL_DELAY_US(I2C_DELAY_TIME);
}

/* Send ACK bit to slave */
if(send_ack)
gpio_set(SDA_PORT, __LOW__); /* SDA: Low */
else
gpio_set(SDA_PORT, __HIGH__); /* SDA: High */
gpio_set(SCL_PORT, __HIGH__); /* SCL: High */
HAL_DELAY_US(I2C_DELAY_TIME);
gpio_set(SCL_PORT, __LOW__); /* SCL: Low */
HAL_DELAY_US(I2C_DELAY_TIME);

return data;

}

接著就是傳送的過程了,
一般I2C都是由高位先寫入(這個不一定,要看Datasheet)。
在寫入時,Master(指CPU)先把SDA設成寫入的bit,
再把SCL依照我們delay的時間來做clock signal的產生(low->high->low->...)。
接著重複上面的動作依序把整個byte都寫入。
寫完整個byte後要讀取一下Slave(指LM75)所回傳的ACK bit。

而在讀取時,一樣先讀入SDA的電位當成data的bit,
再把SCL產生clock signal給Slave,
Slave在收到clock signal後會在SDA上變動電位把資料依序傳出來。
所以同樣的重複上面的動作就可以把組合出整個byte的資料。
當Master讀完byte後也要有回傳ACK的動作來告知是否繼續有下一個byte要讀取。
int get_lm75_temp(void) {

unsigned char msb = 0x00, lsb = 0x00;

i2c_start();
i2c_write(ADDR_LM75); /* Ask LM75 write */
i2c_write(0x00);

i2c_start();

i2c_write(ADDR_LM75+1); /* Ask LM75 read */
msb = i2c_read(ACK);
lsb = i2c_read(NO_ACK);
i2c_stop();

return (msb << 8) | lsb;

}

最後,照著LM75的Datasheet,
Master先傳送address byte給Slave,
Slave在收到address後會先比對自己A2,A1,A0,
若符合再依照address byte的最後一個bit來回應之後的動作,
若為0則是write動作,若為1則是read動作。

所以我們要讀取現在溫度資料時,
CPU先把address byte+0給寫到LM75,告知接下來要寫入register設定,
接著把0x00寫入表示要讀取第0個register,也就是現在的溫度。
再來就是重新開始先寫入address byte+1給LM75,告知要讀取資料。
接著就把溫度資料讀出,共2個byte,所以讀了2次囉。
最後就是把資料組合起來return出去囉。

If you enjoyed this post Subscribe to our feed

6 Comments

  1. 宇傑 |

    您好,我最近在試著用C,透過I2C寫入程式到IC裡,參考了您的程式後,有幾個問題想請問:
    (1)msb = i2c_read(ACK);lsb = i2c_read(NO_ACK);
    (2)gpio_get(SDA_PORT);
    想請問這兩段程式,如果要用C來執行的話,應該寫成怎麼樣子?
    麻煩您了,謝謝~

     
  2. Yu Lai |

    宇傑你好,

    關於(1)中的i2c_read()相關的C Code我也有一起貼在文章中啊.
    而(2)的gpio_get(SDA_PORT);這個會這樣寫的原因是因為不同的CPU會有不同取GPIO腳位的input值的方法, 有可能是Port mapping I/O, 也有可能是memory mapping I/O. 我只好寫成function, 避免誤導大家. 至於你所選用的CPU是如何去取GPIO的值, 這可能就要去翻該CPU相關的data sheet才能得知了. 以下是我選用的CPU的實做內容, 給您參考一下吧.
    int gpio_get(int port) {

    unsigned int tmp;

    HAL_WRITE_UINT32(PCI_ADDR, (port > 31)? GPIO_DATA2 : GPIO_DATA1);
    HAL_READ_UINT32(PCI_DATA, tmp);
    tmp |= PORT2BIT(port);
    HAL_WRITE_UINT32(PCI_DATA, tmp);
    HAL_READ_UINT32(PCI_DATA, tmp);
    tmp &= PORT2BIT(port);

    return (tmp)? __HIGH__:__LOW__;
    }

     
  3. now |

    在int get_lm75_temp(void)沒有看到void i2c_init(void) 的使用,請問這個initial function 只要在system開機後執行一次就好了嗎?

     
  4. Yu Lai |

    是的.

     
  5. stanley |

    請問這個function,是否有要求須要在何種環境下執行(Kernel Space 或User Space , 還是兩者皆可呢 謝謝

     
  6. Yu Lai |

    基本上兩者皆可, 但還是要看你的gpio_get和gpio_set怎麼實做.

     

張貼留言