1. 程式人生 > >深入解讀微控制器IO口模擬IIC程式設計

深入解讀微控制器IO口模擬IIC程式設計

在微控制器的開發過程中,經常會使用IIC介面連線外部感測器獲得相應的資料。一旦我們的IIC介面數目較多而微控制器固有的IIC介面不夠的情況,這時一個微控制器普通IO口模擬IIC的做法可以解決我們的尷尬。這篇部落格詳細的介紹STM32F103的IO口模擬IIC的詳細做法。

首先,我們需要認真分析下IIC協議。

IIC協議是需要很嚴格的劃分一個主機和從機。在實際使用過程中,通常控制器為主機,各種感測器為從機。如果是兩個控制器之間採用IIC進行資料傳輸,那麼一定要進行主從機的分配,以免因為主從機狀態不確定而導致通訊不能正常。

IIC協議規定採用IIC協議進行資料的傳輸需要兩條訊號線,一條是時鐘時鐘訊號線,也就是我們常說的SCLK,一條是資料訊號線SDA。主從機之間的資料傳輸完全依靠這兩個訊號的配合。同時,只有主機才能進行時鐘訊號的生成,其實這樣是為了防止由於時鐘的導致資料不能進行傳輸。

 在IIC協議中,從機有唯一的地址,如果從機為一個感測器,通常該地址分為兩部分:第一部分為感測器固定好的高四位,第二部分為自己靈活配置的三位A0 ,A1和A2和讀寫確定位,通過對這三個管腳的配置可實現8個地址的分配和對從機的讀寫操作。具體怎麼實現我們下文分析。     IIC協議是一個真正的多主機匯流排如果兩個或更多主機同時初始化資料傳輸可以通過沖突檢測和仲裁防止資料被破壞,至於其傳送速度, 序列的8 位雙向資料傳輸位速率在標準模式下可達100kbit/s 快速模式下可達400kbit/s 高速模式下可達3.4Mbit/s,完全可以滿足常規設計需求。

在IIC協議中,需要注意以下四點:

  1、開始訊號,在時鐘高電平期間,資料由高變低時就是為協議開始的訊號。

  2、結束訊號,在時鐘高電平期間,資料由低變高時就是為協議結束的訊號。

開始和結束訊號如下圖所示。

stp.jpg

                                    圖一

  3、應答/非應答訊號。當主機發送一個位元組後從機需要進行一個應答訊號,即我們所謂的ASK/NASK訊號,以此來判斷訊號是否完成了傳輸。

  4、資料何時儲存何時傳送

   IIC匯流排是以序列方式傳輸資料,從資料位元組的最高位開始傳送,每一個數據位在SCL上都有一個時鐘脈衝相對應。在時鐘線高電平期間資料線上必須保持穩定的邏輯電平狀態,高電平為資料1,低電平為資料0。只有在時鐘線為低電平時,才允許資料線上的電平狀態變化,如下圖所示:

mul.jpg

              圖二

為深入理解C語言編寫的微控制器IO模擬IIC程式,利用stm32f103驅動24C256進行說明。

在主機方面,微控制器首先要完成管腳的配置:

在巨集定義中,由於數字訊號用0和1表示數字資訊,因此SCL和SDA在數值上只表現為0和1.設定如下。

#define I2C_SCL_0    GPIO_ResetBits(GPIOB,GPIO_Pin_15)

#define I2C_SCL_1    GPIO_SetBits(GPIOB,GPIO_Pin_15)

#define I2C_SDA_0     GPIO_ResetBits(GPIOB,GPIO_Pin_14)

#define I2C_SDA_1     GPIO_SetBits(GPIOB,GPIO_Pin_14)

 當SDA為輸入的時候,程式需要讀取IO口的狀態,因此使用GPIO_ReadInputDataBit來讀取微控制器IO口的狀態。

#define RD_I2C_IO             GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_14)

 同時,為了程式在執行過程中狀態的穩定,需要設定一個延時的引數,巨集定義如下

#define DF_I2C_TCY    2

//初始化IIC,在開始傳輸時保持時序的穩定,需要將SCL和SDA都設定為高電平,其中FM24C256的SCL 與PB15 連線, SDA與PB14連線。C程式如下:

GPIO_InitTypeDef GPIO_InitStruct_I2C;

void IIC_Init(void)

{                                                   

GPIO_InitTypeDef GPIO_InitStructure;

RCC_APB2PeriphClockCmd(    RCC_APB2Periph_GPIOB, ENABLE );      

GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14|GPIO_Pin_15;

GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD ;   //推輓輸出

GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;

GPIO_Init(GPIOB, &GPIO_InitStructure);

GPIO_SetBits(GPIOB,GPIO_Pin_14|GPIO_Pin_15);       //PB14,PB15 輸出高

}

當配置好之後,需要設定時鐘和資料的方向問題,因為時鐘只是主機輸出,而資料SDA需要具有輸出和輸入功能的切換,C預言具體實施如下:

配置SCL為輸出的C語言格式:

void I2C_SCL_OUT(void)

{

   GPIO_InitStruct_I2C.GPIO_Pin   = GPIO_Pin_15;

   GPIO_InitStruct_I2C.GPIO_Mode  = GPIO_Mode_Out_OD;

 GPIO_InitStruct_I2C.GPIO_Speed = GPIO_Speed_50MHz;

   GPIO_Init(GPIOB,&GPIO_InitStruct_I2C);

}

配置 SDA為輸出的C語言格式:

void I2C_SDA_OUT(void)

{

   GPIO_InitStruct_I2C.GPIO_Pin   = GPIO_Pin_14;

   GPIO_InitStruct_I2C.GPIO_Mode  = GPIO_Mode_Out_OD;

 GPIO_InitStruct_I2C.GPIO_Speed = GPIO_Speed_50MHz;

   GPIO_Init(GPIOB,&GPIO_InitStruct_I2C);

}

而SDA在讀從機的時候,需要將SDA設定為為輸入模式,因此該管腳需要設定為輸入模式,C語言程式如下:

void I2C_SDA_IN(void)

{

   GPIO_InitStruct_I2C.GPIO_Pin   = GPIO_Pin_14;

   GPIO_InitStruct_I2C.GPIO_Mode  = GPIO_Mode_IN_FLOATING;

 GPIO_InitStruct_I2C.GPIO_Speed = GPIO_Speed_50MHz;

   GPIO_Init(GPIOB,&GPIO_InitStruct_I2C);

}

設定好上面的引數後,IIC的傳輸功能函式基本完成,現在開始進入協議構建階段。

在圖一中,我們看出IIC起始條件為SCL為高電平的時候,SDA由1突變為0;C語言實現程式如下:

void Start_I2C(void)

{

   I2C_SDA_OUT();

   I2C_SCL_OUT();

   I2C_SDA_1;

   I2C_SCL_1;

   delay_us(DF_I2C_TCY);

   I2C_SDA_0;

   delay_us(DF_I2C_TCY);

   I2C_SCL_0;

}

 完成IIC啟動後,將SCL保持低電平,用於SDA資料的載入,在協議中,只有在SCL為低電平階段,才能進行SDA資料的變換。

結束IIC傳送資料,在SCL高電平階段,SDA由0變為1.而SDA資料的載入需要在SCL為低電平0階段進行。

void Stop_I2C(void)

{

   I2C_SDA_OUT();

   I2C_SCL_OUT();

   I2C_SCL_0;

   I2C_SDA_0;

   delay_us(DF_I2C_TCY);

   I2C_SCL_1;

   delay_us(DF_I2C_TCY);

   I2C_SDA_1;

   I2C_SDA_IN();  

}

在IIC程式設計中,都是以8bit為基礎進行資料的傳輸。在主機發送時候,也是每次傳送一位元組,這個模組的設計流程為移位操作,具體的C語言程式設計如下:

void SendByte_I2C(u8 shu)

{

   u8 i;

   I2C_SDA_OUT();

   I2C_SCL_OUT();

   for(i=0;i<8;i++)

   {

      I2C_SCL_0;

   if(shu&0x80)

   {

      I2C_SDA_1;

   }

   else

   {

      I2C_SDA_0;

   }

   shu = shu<<1;

   delay_us(DF_I2C_TCY);

   I2C_SCL_1;

   delay_us(DF_I2C_TCY);

   }

   I2C_SCL_0;

}

     基本的思路為:SCL在為0時,可以進行SDA資料的配置,當SCL為1時,SDA資料一定要鎖定。其次為資料的移位,將待發送資料與0x80進行與運算,獲得最高位的資料,通過8次迴圈完成1byte的資料傳送。

     在IIC接收接收一位元組的程式中,也是以移位的方式進行,注意此時需要將SDA埠設定為輸入模式,讀取微控制器IO口的狀態進行資料的獲取。具體C語言程式設計如下:

u8 RcvByte_I2C(void)

{

   u8 c;

   u8 i;

   c = 0x00;

   I2C_SCL_OUT();

   I2C_SDA_IN();

   I2C_SDA_1;

   for(i=0;i<8;i++)

   {

      I2C_SCL_0;

   delay_us(DF_I2C_TCY);

   I2C_SCL_1;

   delay_us(DF_I2C_TCY);

   c = c<<1;

   if(RD_I2C_IO)

   {

      c = c + 0x01;

   }

   I2C_SCL_0;

   delay_us(DF_I2C_TCY);

   }

   I2C_SCL_OUT();

   I2C_SCL_0;

   return(c);

}

 當IIC進行主機獲取數值時,主機需要等待從機的應答訊號,以此來判斷從機是否完成了資料的接收。從主機方看,為IIC等待ASK函式,具體C語言程式設計如下:

IIC_Wait_Ack(void)

{

u8 ucErrTime=0;

 I2C_SDA_IN();     //SDA資料輸入

 I2C_SCL_OUT();

  delay_us(DF_I2C_TCY);           

I2C_SCL_1;

delay_us(DF_I2C_TCY);  

 I2C_SDA_IN();

while(RD_I2C_IO)

{

           ucErrTime++;

           if(ucErrTime>255)

           {

                    Stop_I2C();

                    return 1;

           }

}

I2C_SCL_0;

  I2C_SDA_IN();

return 0; 

}

在該函式中,通過 延時等待從機的ACK是否傳送出來,如果傳送出來,則函式返回0,主機可繼續傳送資料,如果返回1,則從機沒有應答,此時需要停止IIC資料傳輸。防止出現錯誤資料。

    由於IIC為雙向資料通訊,當從機發送完資料,主機也需要傳送應答訊號來說我接收到你的資訊了,此時從機才可變為接收狀態,接收來自主機的資料。C語言程式如下:

void ACK_I2C(void)

{

   u16 t;

   t = 255;

   I2C_SDA_IN();

   I2C_SCL_OUT();

   I2C_SDA_1;

   delay_us(DF_I2C_TCY);

   I2C_SCL_1;

   delay_us(DF_I2C_TCY);

   I2C_SDA_IN();

   while(RD_I2C_IO)

   {

      t--;

   if(t==0)

   {

      Stop_I2C();

              return;

   }

   }  

   I2C_SDA_IN();

   I2C_SCL_0;

}

 當IIC程式執行到主機讀取從機資料完成,需要停止此次資料傳輸時,主機發送一個發出主無應答訊號,從機接收到後就停止傳送資料,之後主機即可傳送停止訊號,停止此次資料的傳輸。C語言程式設計如下:

void NACK_I2C(void)

{

   I2C_SDA_OUT();

   I2C_SCL_OUT();

   I2C_SDA_1;

   delay_us(DF_I2C_TCY);

   I2C_SCL_1;

   delay_us(DF_I2C_TCY);

   I2C_SCL_0;

   I2C_SDA_IN();

}

 在IIC程式設計中,需要向從機的某個地址進行資料的寫入,該函式通常將將寫入的地址和寫入的資料作為引數,通過上面IIC功能模組函式完成資料一個位元組的寫入,C語言程式設計如下:

void WR_ByteI2C(u16 add,u8 shu)

{

   u8 cHByte;

   u8 cLByte;

   u8 cmd;

   cmd = 0xa0;//地址資訊,從機的地址,本例程中為eeprom的從機地址

cHByte = add/256;

   cLByte = add%256;

   Start_I2C();//啟動

   SendByte_I2C(cmd);//發寫命令

   ACK_I2C();

   SendByte_I2C(cHByte);//發寫地址

   ACK_I2C();

   SendByte_I2C(cLByte);//發寫地址

   ACK_I2C();

   SendByte_I2C(shu);

   ACK_I2C();

   Stop_I2C();

}

對於IIC讀取從機一個地址的資料,需要將從機待讀取地址作為引數,返回為讀取到的資料,具體C語言程式如下:

u8 RD_ByteI2C(u16 add)

{

   u8 c;

   u8 cHByte;

   u8 cLByte;

   u8 cmd;

   cmd = 0xa0;

   cHByte = add/256;

   cLByte = add%256;

   Start_I2C();//啟動

   SendByte_I2C(cmd);//發寫命令

   ACK_I2C();

   SendByte_I2C(cHByte);//發寫地址

   ACK_I2C();

   SendByte_I2C(cLByte);//發寫地址

   ACK_I2C();

   Start_I2C();//啟動

   SendByte_I2C(cmd+1);//

   ACK_I2C();

   c = RcvByte_I2C();

   NACK_I2C();

   Stop_I2C();

   return(c);

}

從機方面,FM24c256為儲存器,在硬體電路上設定好地址後,呼叫前面寫好的函式即可實現資料的讀寫:

具體的從AT24CXX指定地址讀出一個數據,具體C語言程式設計如下(該過程需要對IIC協議有明確的認識,單位元組傳送,多位元組傳送):

u8 AT24CXX_ReadOneByte(u16 ReadAddr)

{                                                                                                                                                                                                                                                                                                                            

  u8 temp=0;                                                                                                                                                                            

    Start_I2C(); 

  SendByte_I2C(0XA0);          //傳送寫命令1010 000 R/W

  IIC_Wait_Ack();

           SendByte_I2C(ReadAddr>>8);//傳送高地址

           IIC_Wait_Ack();

     SendByte_I2C(ReadAddr%256);   //傳送低地址

        IIC_Wait_Ack(); 

        Start_I2C();              

  SendByte_I2C(0XA1);           //進入接收模式                      

    IIC_Wait_Ack();

    temp=RcvByte_I2C();                   

    Stop_I2C();//產生一個停止條件        

return temp;

}

對於在AT24CXX指定的地址寫入一個數據,只要一句IIC的協議操作,即可完成,     void AT24CXX_WriteOneByte(u16 WriteAddr,u8 DataToWrite)

{                                                                                                                                                                                                           

     Start_I2C(); 

if(EE_TYPE>AT24C16)

{

           SendByte_I2C(0XA0);           //傳送寫命令

           IIC_Wait_Ack();

 SendByte_I2C(WriteAddr>>8);//傳送高地址

         }else

{

            SendByte_I2C(0XA0+((WriteAddr/256)<<1));   //傳送器件地址0XA0,寫資料

}         

IIC_Wait_Ack();          

     SendByte_I2C(WriteAddr%256);   //傳送低地址

IIC_Wait_Ack();                                                                                                                     

 SendByte_I2C(DataToWrite);     //傳送位元組                                                             

IIC_Wait_Ack();                              

   Stop_I2C();//產生一個停止條件

delay_ms(10);  

}

其實,對於微控制器IO口模擬IIC介面是一個古老而又常見的問題,在IIC介面不夠的情況下,模擬IIC介面是常用的方法。

不論是什麼情況,IO口模擬也好,直接使用特定IIC介面也好,只有對IIC協議有深刻的理解,才能將程式完好的寫出來。希望通過這篇博文,能讓你明白其中的道理。