I2C總線協議的軟件模擬實現方法
在上一篇博客中已經講過I2C總線通信協議,本文講述I2C總線協議的軟件模擬實現方法。
1. 簡述
所謂的I2C總線協議的軟件模擬實現方法,就是用軟件控制GPIO的輸入、輸出和高低電平變化,來模擬I2C總線通訊過程中SCL、SDA的電平變化來實現的。
2. I2C總線的封裝
每個處理器對應的GPIO操作都有差異,即使是同一款處理器,不同的人也會有不同的GPIO封裝風格,就以我個人習慣用的GPIO方法為例來進行講解。我習慣上將GPIO的組和位封裝為一個結構體,這樣使用方便,看起來也更直觀。
typedef struct {
unsigned char group;
unsigned char bit;
} gpio_t;
將I2C總線中使用的SCL和SDA的GPIO進一步進行封裝。
typedef struct {
gpio_t scl;
gpio_t sda;
} i2c_gpio_t;
將I2C總線軟件模擬部分當做驅動程序中的一個模塊來使用,定義一個結構體來封裝I2C模塊中的一些全局變量,如:GPIO、鎖等等。本文中的鎖只是為了保證I2C的一個操作步驟是原子的,所有鎖的使用可以忽略,如果想要了解更過關於鎖的使用方法,請關注另外一篇博客(還沒來得及寫,以后會補充)。
typedef struct {
i2c_gpio_t gpio;
spinlock_t lock;
struct mutex i2c_mutex;
} i2c_info_t;
3. 軟件模擬實現
3.1 I2C總線的初始化
1)先初始化I2C總線,具體要做的內容是,先把外部調用I2C模塊時要使用的GPIO引腳,作為參數傳遞到I2C模塊,用來進行一系列的操作。在這里將GPIO作為參數傳遞到I2C模塊后,保存在全局變量的結構體中。
2)再初始化I2C總線的GPIO引腳,即將用來代替模擬I2C總線中SCL、SDA引腳的GPIO設置為輸出,並輸出高電平,因為兩條線上都接有上拉電阻,I2C總線空閑時默認SCL、SDA都處於高電平,也就是空閑狀態。
3)如果要使用鎖機制,需要在這一步中將鎖初始化。
// I2C模塊初始化
int i2c_init(i2c_gpio_t *gpio)
{
i2c_debug("i2c_init");
// 初始化鎖
spin_lock_init(&i2c_info.lock);
mutex_init(&i2c_info.i2c_mutex);
// 初始化全局變量中I2C的GPIO
i2c_info.gpio.scl = gpio->scl;
i2c_info.gpio.sda = gpio->sda;
i2c_gpio_init();
return 0;
}
// I2C的GPIO初始化
static void i2c_gpio_init(void)
{
i2c_debug("i2c_gpio_init");
i2c_sda_init();
i2c_scl_init();
}
// I2C的SCL初始化
static void i2c_scl_init(void)
{
i2c_debug("scl init");
SET_SCL_OUT;
SET_SCL_HIGH;
}
// I2C的SDA初始化
static void i2c_sda_init(void)
{
i2c_debug("sda init");
SET_SDA_OUT;
SET_SDA_HIGH;
}
3.2 I2C總線的起始位
I2C總線在開始通信時要先發送一個起始位標志,起始位是在SCL為高電平時,SDA由高電平變為低電平。
// I2C總線的起始位
int i2c_start(void)
{
mutex_lock(&i2c_info.i2c_mutex);
SET_SDA_OUT;
udelay(I2C_DELAY);
SET_SDA_HIGH;
udelay(I2C_DELAY);
SET_SCL_HIGH;
udelay(I2C_DELAY);
SET_SDA_LOW;
udelay(I2C_DELAY);
SET_SCL_LOW;
udelay(I2C_DELAY);
mutex_unlock(&i2c_info.i2c_mutex);
return 0;
}
3.3 I2C總線的結束位
I2C總線在數據傳輸完成后,需要發送一個結束位,來結束I2C通訊,並釋放I2C總線,結束位是在SCL為高電平時,SDA由低電平變為高電平
// I2C總線的結束位
int i2c_stop(void)
{
mutex_lock(&i2c_info.i2c_mutex);
SET_SDA_OUT;
udelay(I2C_DELAY);
SET_SCL_LOW;
udelay(I2C_DELAY);
SET_SDA_LOW;
udelay(I2C_DELAY);
SET_SCL_HIGH;
udelay(I2C_DELAY);
SET_SDA_HIGH;
udelay(I2C_DELAY);
mutex_unlock(&i2c_info.i2c_mutex);
return 0;
}
3.4 I2C總線的應答
為了統一管理和使用方便,將I2C總線的等待應答、發送應答信號、發送非應答信號封裝在一起進行管理。
1)I2C總線的等待應答
在I2C總線通訊時,主設備給從設備發送一個字節的數據后,要等待從設備的一個應答信號,這時候主設備處於等待應答狀態,需要檢測從設備的應答信號是否到來,如果從設備的應答信號到來,主設備就繼續給從設備發送下一個字節的數據,或者發送停止位結束I2C通訊;如果在主設備等待超時后,從設備的應答信號時鍾不到來,就說明I2C總線通訊中出現問題,主設備跳出等待,直接發送結束位,以結束I2C總線通訊。
// I2C總線的等待應答
static int i2c_wait_ack(void)
{
int ack_times = 0;
int ret = 0;
mutex_lock(&i2c_info.i2c_mutex);
SET_SDA_OUT;
udelay(I2C_DELAY);
SET_SDA_HIGH;
udelay(I2C_DELAY);
SET_SDA_IN;
udelay(I2C_DELAY);
SET_SCL_LOW;
udelay(I2C_DELAY);
SET_SCL_HIGH;
udelay(I2C_DELAY);
ack_times = 0;
// 檢測從設備應答信號
while (GET_SDA_VAL) {
ack_times++;
// 判斷等待是否超時
if (ack_times == 10) {
ret = 1;
i2c_error("i2c ack error, no ack");
break;
}
}
SET_SCL_LOW;
mutex_unlock(&i2c_info.i2c_mutex);
return ret;
}
2)I2C總線的發送應答
在I2C總線通信的時候,主設備每次接收到從設備發送的一個字節數據后,要給從設備發送應答信號(ACK)以繼續接收從設備的數據,或者給從設備發送非應答信號(NOACK)以結束接收從設備的數據。
應答信號(ACK)就是先拉低SDA線,並在SCL為高電平期間保持SDA線為低電平
// I2C總線發送應答信號
static int i2c_send_ack(void)
{
i2c_debug("i2c_send_ack");
mutex_lock(&i2c_info.i2c_mutex);
SET_SDA_OUT;
udelay(I2C_DELAY);
SET_SCL_LOW;
udelay(I2C_DELAY);
SET_SDA_LOW;
udelay(I2C_DELAY);
SET_SCL_HIGH;
udelay(I2C_DELAY);
SET_SCL_LOW;
udelay(I2C_DELAY);
mutex_unlock(&i2c_info.i2c_mutex);
return 0;
}
非應答信號(NOACK)就是不要拉低SDA線(此時SDA線為高電平),並在SCL為高電平期間保持SDA線為高電平。
// I2C總線發送非應答信號
static int i2c_send_noack(void)
{
i2c_debug("i2c_send_noack");
mutex_lock(&i2c_info.i2c_mutex);
SET_SDA_OUT;
udelay(I2C_DELAY);
SET_SCL_LOW;
udelay(I2C_DELAY);
SET_SDA_HIGH;
udelay(I2C_DELAY);
SET_SCL_HIGH;
udelay(I2C_DELAY);
SET_SCL_LOW;
udelay(I2C_DELAY);
mutex_unlock(&i2c_info.i2c_mutex);
return 0;
}
3.5 I2C總線的寫操作
// I2C總線的寫操作
int i2c_write_byte(u8 data)
{
unsigned long flag = 0;
u8 i = 0;
local_irq_save(flag);
preempt_disable();
mutex_lock(&i2c_info.i2c_mutex);
SET_SDA_OUT;
udelay(I2C_DELAY);
for (i = 0; i < 8; i++) {
if (data & 0x80) {
SET_SDA_HIGH;
} else {
SET_SDA_LOW;
}
udelay(I2C_DELAY);
SET_SCL_HIGH;
udelay(I2C_DELAY);
SET_SCL_LOW;
udelay(I2C_DELAY);
data <<= 0x1;
}
mutex_unlock(&i2c_info.i2c_mutex);
preempt_enable();
local_irq_restore(flag);
return 0;
}
int i2c_write_byte_with_ack(u8 data)
{
i2c_write_byte(data);
if (i2c_ack(I2C_WAIT_ACK)) {
i2c_error("wait ack failed, no ack");
i2c_stop();
return -1;
}
return 0;
}
3.6 I2C總線的讀操作
// I2C總線的讀操作
int i2c_read_byte(u8 *data)
{
unsigned long flag = 0;
u8 ret = 0;
u8 i = 0;
local_irq_save(flag);
preempt_disable();
mutex_lock(&i2c_info.i2c_mutex);
SET_SDA_IN;
udelay(I2C_DELAY);
for (i = 0; i < 8; i++) {
SET_SCL_HIGH;
udelay(I2C_DELAY);
ret <<= 1;
if (GET_SDA_VAL) {
ret |= 0x01;
}
SET_SCL_LOW;
udelay(I2C_DELAY);
}
mutex_unlock(&i2c_info.i2c_mutex);
preempt_enable();
local_irq_restore(flag);
*data = ret;
return 0;
}