1. 程式人生 > >i2c抽象/模擬i2c

i2c抽象/模擬i2c

1.寫在前面

i2c匯流排是由PHILIPS公司開發的一種簡單、雙向二線制同步序列匯流排。關於i2c的使用,並不陌生,C51、ARM、MSP430等,都基本整合硬體i2c,或者不整合i2c的,可以根據匯流排時序圖使用普通IO口翻轉模擬一根i2c匯流排。對於當下流行的stm32飽受詬病的硬體i2c,相信很多人都是使用模擬i2c。模擬i2c的原始碼比較多,大多都是大同小異,對於各類例程,提供的模擬i2c似乎都不是太規範(個人見解),特別是一根i2c匯流排掛多個外設、模擬多根i2c匯流排、以及更換一個i2c外設時,都需要大幅度修改原始碼、複製原始碼、重新除錯時序等重複的工作。在看過RT-Thread的驅動框架(包括其提供的模擬i2c)、Linux裝置驅動框架,覺得分層就特別好,完美解決了上述提及的問題。參考RT-Thread的模擬i2c,整理修改在裸機上使用。下面“從上到下”講述基本實現。

2.Linux、RT-Thread裝置驅動模型

1)  匯流排驅動與外設驅動分離,方便一根匯流排掛多個外設,方便移植;

2)  底層(與硬體相關)與上層分離,方便新增匯流排及移植到不同處理器,移植到其他處理器,只需重新實現硬體相關的“暫存器”層即可;

3.MCU下裸機形式i2c匯流排抽象

此部分實現原始碼為:i2c_core.c  i2c_core.h

1)i2c匯流排抽象對外介面(API)

“i2c_bus_xfer”為i2c封裝對外的API,函式原型如下,提供一個函式模型,具體需要例項化函式指標。

int i2c_bus_xfer(struct i2c_dev_device *dev,struct i2c_dev_message msgs[],unsigned int num)
{
	int size;
	
	size = dev->xfer(dev,msgs,num);	
	return size;
}

a)此函式即作為驅動外設的對外介面,所有操作通過此函式介面,與底層匯流排實現分離,如EEPROM、RTC、溫度感測器等;

b)一個對外函式已經實現90%的情況使用,對應一些特殊情況,後期再完善或增加API。

a)struct i2c_dev_device *i2c_dev

2)i2c匯流排抽象API引數

a)i2c_dev:i2c裝置指標,型別為“struct i2c_dev_device”,驅動一個i2c外設時,首先要對此指標裝置初始化;

b)msgs:i2c一幀資料,傳送資料及存放返回資料的快取;

c)num:資料幀數量。

3)struct i2c_dev_device

該結構體為關鍵,呼叫API驅動外設時,首先對此初始化(類似於Linux/RT-Thread註冊裝置)。完整的裝置包括兩部分,資料操作函式和i2c相關資訊(如硬體i2c或者模擬i2c)。因此“struct i2c_dev_device”的原型為:

struct i2c_dev_device
{
        int (*xfer)(struct i2c_dev_device *dev,struct i2c_dev_message msgs[],unsigned int num);
        void *i2c_phy;
};

a)第一個引數是函式指標,資料收發通過此函式指標呼叫實體函式實現;

b)第二個引數是一個void指標,初始化時指向我們使用的物理i2c(硬體/模擬),使用時可強制轉換為對應的型別。

4)xfer

該函式與i2c匯流排裝置對外介面函式“i2c_bus_xfer”具有相同的引數,形參引數參考此項的第2點,初始化時例項化指向實體函式。

5)struct i2c_dev_message

“struct i2c_dev_message”為i2c匯流排訪問外設的一幀資料資訊,包括髮送資料、外設從地址、訪問標識等。原型如下:

struct i2c_dev_message
{
	unsigned short 	addr;
	unsigned short	flags;
	unsigned short	size;
	unsigned char	*buff;
	unsigned char   retries;		
};

a)addr:i2c外設從機地址,常用為7位,10位較少用;

b)flags:標識,傳送、接收、應答、地址位選擇等標識;幾種標識如下:

#define I2C_BUS_WR             0x0000
#define I2C_BUS_RD             (1u << 0)
#define I2C_BUS_ADDR_10BIT     (1u << 2)
#define I2C_BUS_NO_START      (1u << 4)
#define I2C_BUS_IGNORE_NACK    (1u << 5)
#define I2C_BUS_NO_READ_ACK    (1u << 6)

c)size:傳送的資料大小,或者接收的快取大小;

d)buff:快取區;

e)retries:i2c啟動失敗時,重啟的次數。

4.模擬i2c抽象

對於模擬i2c,在以往的實現方式中,基本是時序圖和外設程式碼混合在一起,增加外設或者使用新的i2c外設時,需要對模擬2ic程式碼進行較大工作量的修改,或者以“複製”的方式實現一套新的i2c匯流排。但同理,可以把模擬i2c時序部分程式碼抽象出來,以“複用”程式碼的形式實現。此部分實現原始碼為:i2c_bitops.c  i2c_bitops.h

1)模擬i2c抽象對外介面

根據上述封裝的對外API,使用時,首先需要實現入口引數“i2c_dev”例項化,用模擬i2c即是呼叫模擬i2c相關介面。

int i2c_bitops_bus_xfer(struct ops_i2c_dev *i2c_bus,struct i2c_dev_message msgs[],unsigned long num)
{
	struct i2c_dev_message *msg;
	unsigned long i;
	unsigned short ignore_nack;
	int ret;
	
	ignore_nack = msg->flags & I2C_BUS_IGNORE_NACK;
	i2c_bitops_start(i2c_bus);							
	for (i = 0; i < num; i++)
    	{
		msg = &msgs[i];
       		if (!(msg->flags & I2C_BUS_NO_START))
       		{
            		if (i)
            		{
                		i2c_bitops_restart(i2c_bus);	
            		}
			ret = i2c_bitops_send_address(i2c_bus,msg);
			if ((ret != 0) && !ignore_nack)
				goto out;
		}
		if (msg->flags & I2C_BUS_RD)
       		{//read
			ret = i2c_bitops_bus_read(i2c_bus,msg);
			if(ret < msg->size)
			{
				ret = -1;
				goto out;
			}
		}
		else
		{//write
			ret = i2c_bitops_bus_write(i2c_bus,msg);
			if(ret < msg->size)
			{
				ret = -1;
				goto out;
			}
		}
	}
	ret = i;
out:
	i2c_bitops_stop(i2c_bus);
		
	return ret;
}
int ops_i2c_bus_xfer(struct i2c_dev_device *i2c_dev,struct i2c_dev_message msgs[],unsigned int num)
{
	return(i2c_bitops_bus_xfer((struct ops_i2c_dev*)(i2c_dev->i2c_phy),msgs,num));
}

a)模擬一根i2c匯流排時,對外的操作函式都通過上訴函式;i2c資訊幀相關引數由上層呼叫傳遞進入,此處主要增加“struct ops_i2c_dev”的封裝;

b)該函式使用到的函,其中入口引數為“struct ops_i2c_dev”型別的都是模擬i2c相關;

d)模擬i2c封裝實現主要針對“struct ops_i2c_dev”原型的例項化。

2)struct ops_i2c_dev

“struct ops_i2c_dev”原型如下:

struct ops_i2c_dev
{
        void (*set_sda)(int8_t state);
        void (*set_scl)(int8_t state);
        int8_t (*get_sda)(void);
        int8_t (*get_scl)(void);
        void (*delayus)(uint32_t us);
};

a)set_sda:資料線輸出;

b)set_scl:時鐘線輸出;

c)get_sda:資料線輸入(捕獲);

d)get_scl:時鐘線輸入(捕獲);

e)delayus:延時函式;

要實現一個模擬i2c,只需將上訴函式指標的實體實現即可,具體看後面描述。

3)模擬i2c時序

以產生i2c起始訊號函式為例子,簡要分析:

static voidi2c_bitops_start(struct ops_i2c_dev *i2c_bus)
{
    i2c_bus->set_sda(0);                                          
    i2c_bus->delayus(3);
    i2c_bus->set_scl(0);                                                       
}        

入口引數為struct ops_i2c_dev *i2c_bus,其實就是i2c_bitops_bus_xfer應用層函式傳入的引數,最終是在此呼叫,底層需要實現的就是io模擬的輸入/輸出狀態函式。

其他函式,如

static void i2c_bitops_restart(struct ops_i2c_dev *i2c_bus)
static chari2c_bitops_wait_ack(struct ops_i2c_dev *i2c_bus)
static int i2c_bitops_send_byte(struct ops_i2c_dev*i2c_bus,unsigned char data)

等等,入口引數都是i2c_bus,時序實現與常規裸機程式設計是一致的,不同的是函式指標的分離呼叫,具體看附件原始碼。

4)標識位

在以往的模擬i2c或者硬體i2c中,操作外設時都有各類情況,如讀和寫方向的切換、連續操作(不需啟動i2c匯流排,如寫EEPROM,先寫地址再寫資料)等。對於這類情況,我們處理辦法是選擇相關的巨集標識即可,具體實現由“中間層”實現,讓i2c外設驅動起來更簡單!以上述對外函式為例:

a)通過標識位判斷是讀還是寫狀態

if (msg->flags & I2C_BUS_RD)
{//read
        ret = i2c_bitops_bus_read(i2c_bus,msg);
        if(ret < msg->size)
        {
                ret = -1;
                goto out;
        }
}

b)應答狀態標識

ignore_nack = msg->flags & I2C_BUS_IGNORE_NACK;

5)讀寫函式

讀寫函式最終是通過io口1bit的翻轉模擬出時序,從而獲得資料,這部分與常規模擬i2c一致,通過函式指標方式操作。主要實現介面函式:

static unsigned long i2c_bitops_bus_write(struct ops_i2c_dev *i2c_bus,struct i2c_dev_message *msg);
static unsigned long i2c_bitops_bus_read(struct ops_i2c_dev *i2c_bus,struct i2c_dev_message *msg);

5.模擬i2c匯流排實現

此部分實現原始碼為:i2c_hw.c  i2c_hw.h

以stm32f1為硬體平臺,採用上述模擬i2c封裝,實現一根模擬i2c匯流排。

1)實現struct ops_i2c_dev函式實體

除了“delayus”函式外,其餘為io翻轉,以“set_sda”和“delayus”為例,實現如下:

static void gpio_set_sda(int8_t state)
{
        if (state)
                I2C1_SDA_PORT->BSRR = I2C1_SDA_PIN;
        else
                I2C1_SDA_PORT->BRR = I2C1_SDA_PIN;
}
static void gpio_delayus(uint32_t us)
{
#if 0  
        volatile int32_t i;
	
        for (; us > 0; us--)
        {
                i = 30;  //mini 17
                while(i--);
        }
#else
        Delayus(us);
#endif
}

a)為例提高速率,上訴程式碼採用暫存器方式操作,可以用庫函式操作io口;

b)延時可以用硬體定時器延時,或者軟體延時,具體根據cpu時鐘計算;

c)其他原始碼看附件中“i2c_hw.c”

2)初始化一根模擬i2c匯流排

void stm32f1xx_i2c_init(void)
{
	GPIO_InitTypeDef GPIO_InitStructure;										
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);		

	GPIO_InitStructure.GPIO_Pin = I2C1_SDA_PIN | I2C1_SCL_PIN;
  	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;	   		
  	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;	   		
  	GPIO_Init(I2C1_SDA_PORT, &GPIO_InitStructure);								   				
	I2C1_SDA_PORT->BSRR = I2C1_SDA_PIN;												
	I2C1_SCL_PORT->BSRR = I2C1_SCL_PIN;
	
	//device init
	ops_i2c1_dev.set_sda = gpio_set_sda;
	ops_i2c1_dev.get_sda = gpio_get_sda;
	ops_i2c1_dev.set_scl = gpio_set_scl;
	ops_i2c1_dev.get_scl = gpio_get_scl;
	ops_i2c1_dev.delayus = gpio_delayus;
		
	i2c1_dev.i2c_phy 	 = &ops_i2c1_dev;
	i2c1_dev.xfer 		 = ops_i2c_bus_xfer; 
}

a)i2c io初始化;

b)i2c裝置例項化,其中“ops_i2c1_dev”和“i2c1_dev”即是我們定義的匯流排裝置,後面使用該匯流排時主要通過“i2c1_dev”實現對底層的呼叫。

6.驅動EEPROM(AT24C16)

此部分實現原始碼為:24clxx.c  24clxx.h

上面匯流排完成後,驅動一個i2c外設可以說就是信手拈來的事情了,而且模擬i2c匯流排抽象出來後,不需在做重複除錯時序的工作。

假設初始化的i2c裝置為i2c1_dev。

1)  寫EEPROM。寫一個位元組,頁寫演算法詳細見原始碼附件(24clxx.c):

char ee_24clxx_writebyte(u16 addr,u8 data)
{
     struct i2c_dev_message ee24_msg[1];
     u8	buf[3];
     u8  slave_addr;
     if(EEPROM_MODEL > 16)
     {					  
	  slave_addr =EE24CLXX_SLAVE_ADDR;
	  buf[0] = (addr >>8)& 0xff;   
	  buf[1] = addr & 0xff;
	  buf[2] = data;
          ee24_msg[0].size  = 3;
     }
     else
     {
          slave_addr = EE24CLXX_SLAVE_ADDR | (addr>>8);
	  buf[0] = addr & 0xff;
	  buf[1] = data;
	  ee24_msg[0].size = 2;
     }
     ee24_msg[0].addr = slave_addr;
     ee24_msg[0].flags = I2C_BUS_WR;
     ee24_msg[0].buff = buf;
     i2c_bus_xfer(&i2c1_dev,ee24_msg,1);
		
     return 0;
}

2)讀EEPROM

voidee_24clxx_readbytes(u16 read_ddr, char* pbuffer, u16 read_size)
{ 
     struct i2c_dev_message ee24_msg[2];
     u8     buf[2];
     u8     slave_addr;
     if(EEPROM_MODEL > 16)
     {
          slave_addr =EE24CLXX_SLAVE_ADDR;
          buf[0] = (read_ddr>>8)& 0xff;
          buf[1] = read_ddr& 0xff;
          ee24_msg[0].size  = 2;
     }
     else
     {
          slave_addr =EE24CLXX_SLAVE_ADDR | (read_ddr>>8);
          buf[0] = read_ddr & 0xff;
          ee24_msg[0].size  = 1;
     }
     ee24_msg[0].buff  = buf;
     ee24_msg[0].addr  = slave_addr;
     ee24_msg[0].flags = I2C_BUS_WR;
     ee24_msg[1].addr  = slave_addr;
     ee24_msg[1].flags = I2C_BUS_RD;
     ee24_msg[1].buff  = (u8*)pbuffer;
     ee24_msg[1].size  = read_size;
     i2c_bus_xfer(&i2c1_dev,ee24_msg,2);
}

3)注意事項

驅動一個外設相對容易了,注意的事項就是標識位部分。

a)此處外設地址(addr),是實際地址,不含讀寫位(7bit),比如AT24C16外設地址為0x50,可能大家平常用的是0xA0,因為包括讀寫位;

b)寫資料時,如果以2幀i2c_dev_message訊息傳送,需要注意“I2C_BUS_NO_START”巨集,此巨集標識意思是不需要再次啟動i2c了,一般看i2c外設手冊時序圖可知道。如寫EEPROM是先寫地址,然後寫資料這個過程是連續的,此時就需用到“I2C_BUS_NO_START”標識。程式可改成這樣:

char ee_24clxx_writebyte(u16 addr,u8 data)
{
     struct i2c_dev_message ee24_msg[2];
     u8     buf[2];
     u8 slave_addr;
     if(EEPROM_MODEL > 16)
     {                                   
          slave_addr =EE24CLXX_SLAVE_ADDR;
          buf[0] = (addr>>8)& 0xff;  
          buf[1] = addr &0xff;
          ee24_msg[0].size  = 2;
     }
     else
     {
           slave_addr =EE24CLXX_SLAVE_ADDR | (addr>>8);
           buf[0] = addr &0xff;
           ee24_msg[0].size  = 1;
     }
     ee24_msg[0].addr = slave_addr;
     ee24_msg[0].flags = I2C_BUS_WR;
     ee24_msg[0].buff  = buf;
     ee24_msg[1].addr = slave_addr;
     ee24_msg[1].flags = I2C_BUS_WR |I2C_BUS_NO_START;
     ee24_msg[1].buff  = &data;
     ee24_msg[1].size  = 1;
     i2c_bus_xfer(&i2c1_dev,ee24_msg,2);
          
     return 0;
}

4)其他

理解之後,或者使用過Linux、RT-Thread的驅動框架的,再驅動其他i2c外設,就是很容易的事情了,剩下的就是配置暫存器、應用演算法的問題了。

7.總結

1)整體思路比較易理解,本質就是函式指標,將與硬體底層無關的部分抽象出來,相關聯的地方分層明確,通過函式指標的方式進行呼叫。

2)事務分離,通用、重複的事情交給匯流排處理,特殊任務留給外設驅動。

8.相關例子

1)LM75A溫度感測器使用:

2)LP55231 LED驅動使用:

9.原始碼

10.參考

相關推薦

i2c抽象/模擬i2c

1.寫在前面 i2c匯流排是由PHILIPS公司開發的一種簡單、雙向二線制同步序列匯流排。關於i2c的使用,並不陌生,C51、ARM、MSP430等,都基本整合硬體i2c,或者不整合i2c的,可以根據匯流排時序圖使用普通IO口翻轉模擬一根i2c匯流排。對於當下流行的stm3

STM32F103利用模擬I2C驅動ADS1115

ADS1115通過模擬I2C驅動:(部分程式碼借鑑了網路上的幾個,並且根據引腳進行了配置,都沒有執行成功,今天調了一天,終於在晚上調了出來) 注意:本部分程式碼需要只是ADS1115的部分程式(一些用到的陣列在此沒有寫),模擬II2C的各程式並未給出,大家根據需要進行裁剪,程式碼完全開源,希

微控制器GPIO軟體模擬I2C通訊程式

文章原始地址:http://feotech.com/?p=98 本程式主要用於使用可程式設計晶片自身的IO管腳,模擬I2C通訊的協議,實現I2C匯流排資料的傳輸. /** ****************************************************

GPIO實現I2C協議模擬(1)

最近需要用GPIO模擬I2C協議,如果是在Linux下面比較簡單,但在Windows下面,是否有沒Linux那麼簡單了。 索性自己對I2C協議還有一些瞭解,翻了SPEC並結合示波器量出的實際訊號分析,總算有些成就,在這裡以做記錄 I2C是什麼,這裡不做介紹,網上資料太多. 要用GPIO模擬I2C協議,就是控制

GPIO模擬I2C通訊協議(一)

概要: 從本節開始,我將用3-5篇部落格的篇幅對我為期3個月的本科實習做總結。本節將首先介紹I2C協議的基本時序,然後給出用GPIO模擬實現I2C功能的C程式碼。最後介紹驅動開發的一些思路。 關鍵字

linux核心GPIO模擬I2C例項(轉)

前言: 在許多情況下,我們並沒有足夠的I2C匯流排,本文主在介紹如何利用Linux核心中的i2c-gpio模組,利用2條GPIO線模擬i2c匯流排,並掛載裝置。 思路: 先通過對i2c-gpio所定義的結構體初始化(包括初始化i2c的2條線,頻率

開啟linux核心自帶的模擬i2c-gpio模組過程

首先我們要知道核心的makefile是根據配置檔案,也就是kconfig來決定是否編譯一個檔案的。 如果沒有配置核心編譯它,就不會生成.o檔案。 自然就不會開啟這個模組了。 如下是Kconfig 檔

STM32F1 模擬I2C驅動DAC(LTC2605)程式

由於在電路設計上將SCL、SDA線拉太長導致硬體I2C時常出問題,所以考慮用模擬I2C解決,這裡參考別人的程式碼然後自己應用到24位DAC(LTC2605)上,已移植成功。 (1).simulate_i2c.c #include "stm32f10x.h" #include "simulat

STM8 模擬I2C驅動0.91寸OLED屏

#define OLED_SCLK_Clr() GPIO_ResetBits(I2C_PORT, I2C_SDA_PIN )//SDA IIC介面的時鐘訊號 #define OLED_SCLK_Set() GPIO_SetBits(I2C_PORT, I2C_SDA_PIN

STM32 IO口模擬I2C+驅動MPU6050

之後運用MPU6050做了一個平衡小車,可以通過藍芽控制,下載連結: diy平衡小車 一、MPU6050 1. MPU6050介紹 MPU6050 是 InvenSense 公司推出的全球首款整合性 6 軸運動處理元件,相較於多元件 方案,免除

STM32模擬I2C程式

修改自cleanflight /******************************************************************************* 測試平臺:STM32F103ZET6最小系統 *******

GPIO模擬I2C操作除錯注意事項

I2C作為板級序列資料匯流排,其規格相對簡單,但除錯過程中的一些細節問題容易被忽視,產生意想不到的時序錯誤。寫這邊文章為了記錄我在除錯I2C過程中遇到的問題,以便今後查閱並作為經驗與大家分享。 關於I2C匯流排的規格可以參考I2C的規格書,描述準確詳細,此不贅述。 使用M

Linux使用模擬I2C

由於專案除錯過程需要使用I2C介面,但是在使用硬體I2C過程中,總是發現主機與從機通訊不穩定,想使用模擬I2C來除錯。因為沒做過Linux下的模擬I2C,心裡沒底,但是還是強迫自己去實踐。實踐證明:Linux的驅動框架真的很完善了,使能模擬I2C只需要往平臺匯流排新增一個平

模擬i2c實現流程

首先,在理論上,先分析一下I2C具體都包含了那些內容,後面再結合例項分析一下模擬I2C實現的具體流程 第一,I2C的具體包括以下幾部分內容 1、I2C協議 2條雙向序列線,一條資料線SDA,一條時鐘線SCL。 SDA傳輸資料是大端傳輸,每次

STM32 模擬I2C (STM32F051)

/** ****************************************************************************** * @file i2c simu.c * @brief simulation

計算模擬I2C的傳輸速率

在編寫I2C器件的驅動時,經常會看到手冊中提及該器件的最高傳輸速率,如: 而在配置暫存器實現I2C中也是需要配置I2C的傳輸速率,如: 但模擬I2C的速率該如何計算?首先一般I2C速率的單位是kbit/s,I2C傳輸速率的定義:每秒傳輸的位元位數。先把程式碼貼出來(這裡

linux gpio模擬i2c的使用/用GPIO模擬I2C匯流排-3

這個結構專門用於資料傳輸相關的addr為I2C裝置地址,flags為一些標誌位,len為資料的長度,buf為資料。這裡巨集定義的一些標誌還是需要了解一下。 I2C_M_TEN表示10位裝置地址 I2C_M_RD讀標誌 I2C_M_NOSTART無起始訊號標誌 I2

Android 平臺上 使用 i2c-tools除錯i2c

下載原始碼 編譯 在i2c-tools-3.1.1下新建Android.mk檔案,內容如下 LOCAL_PATH:= $(call my-dir)   include $(CLEAR_VARS)   LOCAL_MODULE_TAGS := optional LO

linux I2C 驅動之----i2c驅動的註冊過程(i2c_register_driver->driver_register(&driver->driver)->driver_find)

Linux下i2c驅動的載入過程,分為i2c裝置層、i2c adapter層與i2c核心層 i2c裝置驅動層也就是我們為特定i2c裝置編寫的驅動,下面是我自己理解的i2c驅動的註冊過程 在我們寫的i2c裝置驅動中,我們會呼叫i2c_add_driver()開始i2c裝

amlogic平臺android 系統linux核心中新增i2c裝置實現i2c的讀寫

上一篇,我介紹瞭如何在uboot中新增i2c裝置,以及移植i2c的讀寫介面。簡單來說uboot階段使用i2c裝置和平臺關聯性比較大,但不同平臺套路是差不多的。你可以將uboot階段看作是引導androi