【STM32】IIC的基本原理(例項:普通IO口模擬IIC時序讀取24C02)

Yngz_Miao發表於2018-05-15

IIC的基本介紹

IIC的簡介

IIC(Inter-Integrated Circuit)匯流排是一種由PHILIPS公司在80年代開發的兩線式序列匯流排,用於連線微控制器及其外圍裝置。它是半雙工通訊方式。

  • IIC匯流排最主要的優點是其簡單性和有效性。由於介面直接在元件之上,因此IIC匯流排佔用的空間非常小,減少了電路板的空間和晶片管腳的數量,降低了互聯成本。匯流排的長度可高達25英尺,並且能夠以10Kbps的最大傳輸速率支援40個元件。
  • IIC匯流排的另一個優點是,它支援多主控(multimastering), 其中任何能夠進行傳送和接收的裝置都可以成為主匯流排。一個主控能夠控制訊號的傳輸和時脈頻率。當然,在任何時間點上只能有一個主控。

IIC序列匯流排一般有兩根訊號線,一根是雙向的資料線SDA,另一根是時鐘線SCL,其時鐘訊號是由主控器件產生。所有接到IIC匯流排裝置上的序列資料SDA都接到匯流排的SDA上,各裝置的時鐘線SCL接到匯流排的SCL上。對於並聯在一條匯流排上的每個IC都有唯一的地址。

一般情況下,資料線SDA和時鐘線SCL都是處於上拉電阻狀態。因為:在匯流排空閒狀態時,這兩根線一般被上面所接的上拉電阻拉高,保持著高電平。

STM32的IIC介面

目前絕大多數的MCU都附帶IIC匯流排介面,STM32也不例外。但是在本文中,我們不使用STM32的硬體IIC來讀取24C02,而是通過軟體的方式來模擬。

原因是因為:STM32的硬體IIC非常複雜,更重要的是它並不穩定,故不推薦使用。

 

IIC協議

IIC匯流排在傳輸資料的過程中一共有三種型別訊號,分別為:開始訊號、結束訊號和應答訊號。這些訊號中,起始訊號是必需的,結束訊號和應答訊號,都可以不要。同時我們還要介紹其空閒狀態、資料的有效性、資料傳輸。

先來看一下IIC匯流排的時序圖:

這可能會比較複雜,可以先看一份簡化了的時序圖:

空閒狀態

當IIC匯流排的資料線SDA和時鐘線SCL兩條訊號線同時處於高電平時,規定為匯流排的空閒狀態。此時各個器件的輸出級場效電晶體均處在截止狀態,即釋放匯流排,由兩條訊號線各自的上拉電阻把電平拉高。 

起始訊號與停止訊號

  • 起始訊號:當時鍾線SCL為高期間,資料線SDA由高到低的跳變;啟動訊號是一種電平跳變時序訊號,而不是一個電平訊號;
  • 停止訊號:當時鍾線SCL為高期間,資料線SDA由低到高的跳變;停止訊號也是一種電平跳變時序訊號,而不是一個電平訊號。

應答訊號

傳送器每傳送一個位元組(8個bit),就在時鐘脈衝9期間釋放資料線,由接收器反饋一個應答訊號。 

  • 應答訊號為低電平時,規定為有效應答位(ACK,簡稱應答位),表示接收器已經成功地接收了該位元組;
  • 應答訊號為高電平時,規定為非應答位(NACK),一般表示接收器接收該位元組沒有成功。 

對於反饋有效應答位ACK的要求是:接收器在第9個時鐘脈衝之前的低電平期間將資料線SDA拉低,並且確保在該時鐘的高電平期間為穩定的低電平。 如果接收器是主控器,則在它收到最後一個位元組後,傳送一個NACK訊號,以通知被控傳送器結束資料傳送,並釋放資料線SDA,以便主控接收器傳送一個停止訊號P。

資料有效性

IIC匯流排進行資料傳送時,時鐘訊號為高電平期間,資料線上的資料必須保持穩定;只有在時鐘線上的訊號為低電平期間,資料線上的高電平或低電平狀態才允許變化。 

即:資料在時鐘線SCL的上升沿到來之前就需準備好。並在在下降沿到來之前必須穩定。

資料的傳達

在IIC匯流排上傳送的每一位資料都有一個時鐘脈衝相對應(或同步控制),即在SCL序列時鐘的配合下,在SDA上逐位地序列傳送每一位資料。資料位的傳輸是邊沿觸發。

延時時間

 

IIC匯流排的資料傳送

IIC匯流排上的每一個裝置都可以作為主裝置或者從裝置,而且每一個裝置都會對應一個唯一的地址(地址通過物理接地或者拉高),主從裝置之間就通過這個地址來確定與哪個器件進行通訊,在通常的應用中,我們把CPU帶I2C匯流排介面的模組作為主裝置,把掛接在匯流排上的其他裝置都作為從裝置。

也就是說,主裝置在傳輸有效資料之前要先指定從裝置的地址,地址指定的過程和上面資料傳輸的過程一樣,只不過大多數從裝置的地址是7位的,然後協議規定再給地址新增一個最低位用來表示接下來資料傳輸的方向,0表示主裝置向從裝置寫資料,1表示主裝置向從裝置讀資料。

  • 主裝置往從裝置中寫資料。資料傳輸格式如下:

淡藍色部分表示資料由主機向從機傳送,粉紅色部分則表示資料由從機向主機傳送。

寫用0來表示(高電平),讀用1來表示(低電平)。

  • 主裝置從從裝置中讀資料。資料傳輸格式如下:

在從機產生響應時,主機從傳送變成接收,從機從接收變成傳送。之後,資料由從機傳送,主機接收,每個應答由主機產生,時鐘訊號仍由主機產生。若主機要終止本次傳輸,則傳送一個非應答訊號,接著主機產生停止條件。

  •  主裝置往從裝置中寫資料,然後重啟起始條件,緊接著從從裝置中讀取資料;或者是主裝置從從裝置中讀資料,然後重啟起始條件,緊接著主裝置往從裝置中寫資料。資料傳輸格式如下:

在多主的通訊系統中,匯流排上有多個節點,它們都有自己的定址地址,可以作為從節點被別的節點訪問,同時它們都可以作為主節點向其它的節點傳送控制位元組和傳送資料。但是如果有兩個或兩個以上的節點都向匯流排上傳送啟動訊號並開始傳送資料,這樣就形成了衝突。要解決這種衝突,就要進行仲裁的判決,這就是I2C匯流排上的仲裁。

I2C匯流排上的仲裁分兩部分:SCL線的同步和SDA線的仲裁。

這部分就暫時不介紹了,想要了解:可以參考連結淺談I2C匯流排I2C匯流排協議圖解

 

IIC底層驅動程式分析

現擬採用PB6、PB7來模擬IIC時序,其中:PB6為時鐘線,PB7為資料線。

首先進行一些必要的巨集定義:

//IO方向設定
#define SDA_IN()  {GPIOB->CRL&=0X0FFFFFFF;GPIOB->CRL|=(u32)8<<28;}
#define SDA_OUT() {GPIOB->CRL&=0X0FFFFFFF;GPIOB->CRL|=(u32)3<<28;}

//IO操作函式	 
#define IIC_SCL    PBout(6) //SCL
#define IIC_SDA    PBout(7) //SDA	 
#define READ_SDA   PBin(7)  //輸入SDA 

//IIC所有操作函式
void IIC_Init(void);                //初始化IIC的IO口				 
void IIC_Start(void);				//傳送IIC開始訊號
void IIC_Stop(void);	  			//傳送IIC停止訊號
void IIC_Send_Byte(u8 txd);			//IIC傳送一個位元組
u8 IIC_Read_Byte(unsigned char ack);//IIC讀取一個位元組
u8 IIC_Wait_Ack(void); 				//IIC等待ACK訊號
void IIC_Ack(void);					//IIC傳送ACK訊號
void IIC_NAck(void);				//IIC不傳送ACK訊號

由於IIC是半雙工通訊方式,因而資料線SDA可能會資料輸入,也可能是資料輸出,需要定義IIC_SDA來進行輸出、READ_SDA來進行輸入,與此同時就要對IO口進行模式配置:SDA_IN()和SDA_OUT()。

而時鐘線SCL一直是輸出的,所以就沒有資料線SDA麻煩了。

//初始化IIC
void IIC_Init(void)
{					     
	GPIO_InitTypeDef GPIO_InitStructure;
	RCC_APB2PeriphClockCmd(	RCC_APB2Periph_GPIOB, ENABLE );	//使能GPIOB時鐘
	   
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6|GPIO_Pin_7;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP ;   //推輓輸出
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &GPIO_InitStructure);
	GPIO_SetBits(GPIOB,GPIO_Pin_6|GPIO_Pin_7); 	//PB6,PB7 輸出高,空閒狀態
}
//產生IIC起始訊號
void IIC_Start(void)
{
	SDA_OUT();     //sda線輸出
	IIC_SDA=1;	  	  
	IIC_SCL=1;
	delay_us(4);
 	IIC_SDA=0;    //START:when CLK is high,DATA change form high to low 
	delay_us(4);
	IIC_SCL=0;    //鉗住I2C匯流排,準備傳送或接收資料 
}	  
//產生IIC停止訊號
void IIC_Stop(void)
{
	SDA_OUT();    //sda線輸出
	IIC_SCL=0;
	IIC_SDA=0;    //STOP:when CLK is high DATA change form low to high
 	delay_us(4);
	IIC_SCL=1; 
	IIC_SDA=1;    //傳送I2C匯流排結束訊號
	delay_us(4);							   	
}
//傳送資料後,等待應答訊號到來
//返回值:1,接收應答失敗,IIC直接退出
//        0,接收應答成功,什麼都不做
u8 IIC_Wait_Ack(void)
{
	u8 ucErrTime=0;
	SDA_IN();      //SDA設定為輸入  
	IIC_SDA=1;delay_us(1);	   
	IIC_SCL=1;delay_us(1);	 
	while(READ_SDA)
	{
		ucErrTime++;
		if(ucErrTime>250)
		{
			IIC_Stop();
			return 1;
		}
	}
	IIC_SCL=0;    //時鐘輸出0 	   
	return 0;  
} 
//產生ACK應答
void IIC_Ack(void)
{
	IIC_SCL=0;
	SDA_OUT();
	IIC_SDA=0;
	delay_us(2);
	IIC_SCL=1;
	delay_us(2);
	IIC_SCL=0;
}
//不產生ACK應答		    
void IIC_NAck(void)
{
	IIC_SCL=0;
	SDA_OUT();
	IIC_SDA=1;
	delay_us(2);
	IIC_SCL=1;
	delay_us(2);
	IIC_SCL=0;
}					 				     
//IIC傳送一個位元組
//返回從機有無應答
//1,有應答
//0,無應答			  
void IIC_Send_Byte(u8 txd)
{                        
    u8 t;   
	SDA_OUT(); 	    
    IIC_SCL=0;            //拉低時鐘開始資料傳輸
    for(t=0;t<8;t++)
    {              
        //IIC_SDA=(txd&0x80)>>7;
		if((txd&0x80)>>7)
			IIC_SDA=1;
		else
			IIC_SDA=0;
		txd<<=1; 	  
		delay_us(2);       //對TEA5767這三個延時都是必須的
		IIC_SCL=1;
		delay_us(2); 
		IIC_SCL=0;	
		delay_us(2);
    }	 
} 	    
//讀1個位元組,ack=1時,傳送ACK,ack=0,傳送nACK   
u8 IIC_Read_Byte(unsigned char ack)
{
	unsigned char i,receive=0;
	SDA_IN();        //SDA設定為輸入
    for(i=0;i<8;i++ )
	{
        IIC_SCL=0; 
        delay_us(2);
		IIC_SCL=1;
        receive<<=1;
        if(READ_SDA)receive++;   
		delay_us(1); 
    }					 
    if (!ack)
        IIC_NAck();        //傳送nACK
    else
        IIC_Ack();         //傳送ACK   
    return receive;
}

這裡是通過普通IO口(PB6、PB7)來模擬IIC時序的程式,其實本質上都是嚴格按照IIC的時序圖進行的,認真讀,仔細對比,應該是沒有什麼困難的。

就提一下:IIC_Read_Byte()函式,這個函式的參數列示讀取一個位元組之後,需要給對方應答訊號或非應答訊號。

 

普通IO口模擬IIC時序讀取24C02

24C02晶片介紹

EEPROM (Electrically Erasable Programmable read only memory),帶電可擦可程式設計只讀儲存器——一種掉電後資料不丟失的儲存晶片。 

24Cxx晶片是EEPROM晶片的一種,它是基於IIC匯流排的儲存器件,遵循二線制協議,由於其具有介面方便,體積小,資料掉電不丟失等特點,在儀器儀表及工業自動化控制中得到大量的應用。24Cxx在電路的作用主要是在掉電的情況下儲存資料。

本文使用的是24C02晶片,總容量是2k個bit(256個位元組)。這裡晶片名稱裡的02代表著總容量。

24C02晶片的引腳分佈和具體的作用見下圖:

 

24C02晶片的引腳說明
引腳名稱 說明
A0-A2 地址輸入線
SDA 資料線
SCL 時鐘線
WP 防寫
GND、VCC 提供電源

下圖是本文中24C02和STM32的引腳連線圖:

從圖中可以看出:A0、A1、A2都為0。

對於並聯在一條IIC匯流排上的每個IC都有唯一的地址。那麼看一下從器件地址,可以看出對於不同大小的24Cxx,具有不同的從器件地址。由於24C02為2k容量,也就是說只需要參考圖中第一行的內容:

根據圖中的內容:如果是寫24C02的時候,從器件地址為10100000(0xA0);讀24C02的時候,從器件地址為10100001(0xA1)。

24C02晶片的時序圖

這部分的內容應結合上文:I2C匯流排的資料傳送的內容一起理解。

24C02位元組寫時序

對24C02晶片進行寫位元組操作的時候,步驟如下:

  1. 開始位,後面緊跟從器件地址位(0xA0),等待應答,這是為了在IIC匯流排上確定24C02的從地址位置;
  2. 確定操作24C02的地址,等待應答,也就是將位元組寫入到24C02中256個位元組中的位置;
  3. 確定需要寫入24C02晶片的位元組,等待應答,停止位。

24C02位元組讀時序

對24C02晶片進行讀位元組操作的時候,步驟如下:

  1. 開始位,後面緊跟從器件地址位(0xA0),等待應答,這是為了在IIC匯流排上確定24C02的從地址位置;
  2. 確定操作24C02的地址,等待應答,也就是從24C02中256個位元組中讀取位元組的位置;
  3. 再次開始位,後面緊跟從器件地址位(0xA1),等待應答;
  4. 獲取從24C02晶片中讀取的位元組,發出非應答訊號,停止位。

讀取24C02晶片程式

#define AT24C01		127
#define AT24C02		255
#define AT24C04		511
#define AT24C08		1023
#define AT24C16		2047
#define AT24C32		4095
#define AT24C64	    8191
#define AT24C128	16383
#define AT24C256	32767  
//Mini STM32開發板使用的是24c02,所以定義EE_TYPE為AT24C02
#define EE_TYPE AT24C02
//初始化IIC介面
void AT24CXX_Init(void)
{
	IIC_Init();
}
//在AT24CXX指定地址讀出一個資料
//ReadAddr:開始讀數的地址  
//返回值  :讀到的資料
u8 AT24CXX_ReadOneByte(u16 ReadAddr)
{				  
	u8 temp=0;		  	    																 
        IIC_Start();  
	if(EE_TYPE>AT24C16)            //為了相容24Cxx中其他的版本
	{
		IIC_Send_Byte(0XA0);	   //傳送寫命令
		IIC_Wait_Ack();
		IIC_Send_Byte(ReadAddr>>8);    //傳送高地址
		IIC_Wait_Ack();		 
	}else      IIC_Send_Byte(0XA0+((ReadAddr/256)<<1));   //傳送器件地址0XA0,寫資料 	 

	IIC_Wait_Ack(); 
        IIC_Send_Byte(ReadAddr%256);   //傳送低地址
	IIC_Wait_Ack();	    
	IIC_Start();  	 	   
	IIC_Send_Byte(0XA1);           //進入接收模式			   
	IIC_Wait_Ack();	 
        temp=IIC_Read_Byte(0);	    //讀一個位元組,非應答訊號訊號	   
        IIC_Stop();        //產生一個停止條件	    
	return temp;
}
//在AT24CXX指定地址寫入一個資料
//WriteAddr  :寫入資料的目的地址    
//DataToWrite:要寫入的資料
void AT24CXX_WriteOneByte(u16 WriteAddr,u8 DataToWrite)
{				   	  	    																 
        IIC_Start();  
	if(EE_TYPE>AT24C16)
	{
		IIC_Send_Byte(0XA0);	    //傳送寫命令
		IIC_Wait_Ack();
		IIC_Send_Byte(WriteAddr>>8);    //傳送高地址
 	}else
	{
		IIC_Send_Byte(0XA0+((WriteAddr/256)<<1));   //傳送器件地址0XA0,寫資料 
	}	 
	IIC_Wait_Ack();	   
        IIC_Send_Byte(WriteAddr%256);   //傳送低地址
	IIC_Wait_Ack(); 	 										  		   
	IIC_Send_Byte(DataToWrite);     //傳送位元組							   
	IIC_Wait_Ack();  		    	   
        IIC_Stop();    //產生一個停止條件 
	delay_ms(10);	 
}
//在AT24CXX裡面的指定地址開始寫入長度為Len的資料
//該函式用於寫入16bit或者32bit的資料.
//WriteAddr  :開始寫入的地址  
//DataToWrite:資料陣列首地址
//Len        :要寫入資料的長度2,4
void AT24CXX_WriteLenByte(u16 WriteAddr,u32 DataToWrite,u8 Len)
{  	
	u8 t;
	for(t=0;t<Len;t++)
	{
		AT24CXX_WriteOneByte(WriteAddr+t,(DataToWrite>>(8*t))&0xff);
	}												    
}

//在AT24CXX裡面的指定地址開始讀出長度為Len的資料
//該函式用於讀出16bit或者32bit的資料.
//ReadAddr   :開始讀出的地址 
//返回值     :資料
//Len        :要讀出資料的長度2,4
u32 AT24CXX_ReadLenByte(u16 ReadAddr,u8 Len)
{  	
	u8 t;
	u32 temp=0;
	for(t=0;t<Len;t++)
	{
		temp<<=8;
		temp+=AT24CXX_ReadOneByte(ReadAddr+Len-t-1); 	 				   
	}
	return temp;												    
}
//檢查AT24CXX是否正常
//這裡用了24XX的最後一個地址(255)來儲存標誌字.
//如果用其他24C系列,這個地址要修改
//返回1:檢測失敗
//返回0:檢測成功
u8 AT24CXX_Check(void)
{
	u8 temp;
	temp=AT24CXX_ReadOneByte(255);//避免每次開機都寫AT24CXX			   
	if(temp==0X55)return 0;		   
	else//排除第一次初始化的情況
	{
		AT24CXX_WriteOneByte(255,0X55);
	    temp=AT24CXX_ReadOneByte(255);	  
		if(temp==0X55)return 0;
	}
	return 1;											  
}

//在AT24CXX裡面的指定地址開始讀出指定個數的資料
//ReadAddr :開始讀出的地址 對24c02為0~255
//pBuffer  :資料陣列首地址
//NumToRead:要讀出資料的個數
void AT24CXX_Read(u16 ReadAddr,u8 *pBuffer,u16 NumToRead)
{
	while(NumToRead)
	{
		*pBuffer++=AT24CXX_ReadOneByte(ReadAddr++);	
		NumToRead--;
	}
}  
//在AT24CXX裡面的指定地址開始寫入指定個數的資料
//WriteAddr :開始寫入的地址 對24c02為0~255
//pBuffer   :資料陣列首地址
//NumToWrite:要寫入資料的個數
void AT24CXX_Write(u16 WriteAddr,u8 *pBuffer,u16 NumToWrite)
{
	while(NumToWrite--)
	{
		AT24CXX_WriteOneByte(WriteAddr,*pBuffer);
		WriteAddr++;
		pBuffer++;
	}
}
//要寫入到24c02的字串陣列
const u8 TEXT_Buffer[]={"WarShipSTM32 IIC TEST"};
#define SIZE sizeof(TEXT_Buffer)	
	
 int main(void)
 {	 
	u8 key;
	u16 i=0;
	u8 datatemp[SIZE];
	delay_init();	    	 //延時函式初始化	  
        NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//設定中斷優先順序分組為組2:2位搶佔優先順序,2位響應優先順序
	uart_init(115200);	 	//串列埠初始化為115200
	LED_Init();		  		//初始化與LED連線的硬體介面
	LCD_Init();			   	//初始化LCD 	
	KEY_Init();				//按鍵初始化		 	 	
	AT24CXX_Init();			//IIC初始化 

 	POINT_COLOR=RED;//設定字型為紅色 
	LCD_ShowString(30,50,200,16,16,"WarShip STM32");	
	LCD_ShowString(30,70,200,16,16,"IIC TEST");	
	LCD_ShowString(30,90,200,16,16,"ATOM@ALIENTEK");
	LCD_ShowString(30,110,200,16,16,"2015/1/15");	
	LCD_ShowString(30,130,200,16,16,"KEY1:Write  KEY0:Read");	//顯示提示資訊		
 	while(AT24CXX_Check())//檢測不到24c02
	{
		LCD_ShowString(30,150,200,16,16,"24C02 Check Failed!");
		delay_ms(500);
		LCD_ShowString(30,150,200,16,16,"Please Check!      ");
		delay_ms(500);
		LED0=!LED0;//DS0閃爍
	}
	LCD_ShowString(30,150,200,16,16,"24C02 Ready!");    
 	POINT_COLOR=BLUE;//設定字型為藍色	  
	while(1)
	{
		key=KEY_Scan(0);
		if(key==KEY1_PRES)//KEY_UP按下,寫入24C02
		{
			LCD_Fill(0,170,239,319,WHITE);//清除半屏    
 			LCD_ShowString(30,170,200,16,16,"Start Write 24C02....");
			AT24CXX_Write(0,(u8*)TEXT_Buffer,SIZE);
			LCD_ShowString(30,170,200,16,16,"24C02 Write Finished!");//提示傳送完成
		}
		if(key==KEY0_PRES)//KEY1按下,讀取字串並顯示
		{
 			LCD_ShowString(30,170,200,16,16,"Start Read 24C02.... ");
			AT24CXX_Read(0,datatemp,SIZE);
			LCD_ShowString(30,170,200,16,16,"The Data Readed Is:  ");//提示傳送完成
			LCD_ShowString(30,190,200,16,16,datatemp);//顯示讀到的字串
		}
		i++;
		delay_ms(10);
		if(i==20)
		{
			LED0=!LED0;//提示系統正在執行	
			i=0;
		}		   
	}
}

 

IIC總結

  1. 進行資料傳送時,在SCL為高電平期間,SDA線上電平必須保持穩定,只有SCL為低時,才允許SDA線上電平改變狀態。並且每個位元組傳送時都是高位在前;
  2. 對於應答訊號,ACK=0時為有效應答位,說明從機已經成功接收到該位元組,若為1則說明接受不成功;
  3. 如果從機需要延遲下一個資料位元組開始傳送的時間,可以通過把SCL電平拉低並保持來強制主機進入等待狀態;
  4. 主機完成一次通訊後還想繼續佔用匯流排在進行一次通訊,而又不釋放匯流排,就要利用重啟動訊號。它既作為前一次資料傳輸的結束,又作為後一次傳輸的開始;
  5. 匯流排衝突時,按“低電平優先”的仲裁原則,把匯流排判給在資料線上先傳送低電平的主器件;
  6. 在特殊情況下,若需禁止所有發生在I2C匯流排上的通訊,可採用封鎖或關閉匯流排,具體操作為在匯流排上的任一器件將SCL鎖定在低電平即可;
  7. SDA仲裁和SCL時鐘同步處理過程沒有先後關係,而是同時進行的。

 

相關文章