深入解讀單片機IO口模擬IIC程序設計
在單片機的開發過程中,經常會使用IIC接口連接外部傳感器獲得相應的數據。一旦我們的IIC接口數目較多而單片機固有的IIC接口不夠的情況,這時一個單片機普通IO口模擬IIC的做法可以解決我們的尷尬。這篇博客詳細的介紹STM32F103的IO口模擬IIC的詳細做法。
首先,我們需要認真分析下IIC協議。
IIC協議是需要很嚴格的劃分一個主機和從機。在實際使用過程中,通??刂破鳛橹鳈C,各種傳感器為從機。如果是兩個控制器之間采用IIC進行數據傳輸,那么一定要進行主從機的分配,以免因為主從機狀態不確定而導致通訊不能正常。
IIC協議規定采用IIC協議進行數據的傳輸需要兩條信號線,一條是時鐘時鐘信號線,也就是我們常說的SCLK,一條是數據信號線SDA。主從機之間的數據傳輸完全依靠這兩個信號的配合。同時,只有主機才能進行時鐘信號的生成,其實這樣是為了防止由于時鐘的導致數據不能進行傳輸。
在IIC協議中,從機有唯一的地址,如果從機為一個傳感器,通常該地址分為兩部分:第一部分為傳感器固定好的高四位,第二部分為自己靈活配置的三位A0 ,A1和A2和讀寫確定位,通過對這三個管腳的配置可實現8個地址的分配和對從機的讀寫操作。具體怎么實現我們下文分析。
IIC協議是一個真正的多主機總線如果兩個或更多主機同時初始化數據傳輸可以通過沖突檢測和仲裁防止數據被破壞,至于其傳送速度, 串行的8 位雙向數據傳輸位速率在標準模式下可達100kbit/s 快速模式下可達400kbit/s 高速模式下可達3.4Mbit/s,完全可以滿足常規設計需求。
在IIC協議中,需要注意以下四點:
1、開始信號,在時鐘高電平期間,數據由高變低時就是為協議開始的信號。
2、結束信號,在時鐘高電平期間,數據由低變高時就是為協議結束的信號。
開始和結束信號如下圖所示。
圖一
3、應答/非應答信號。當主機發送一個字節后從機需要進行一個應答信號,即我們所謂的ASK/NASK信號,以此來判斷信號是否完成了傳輸。
4、數據何時存儲何時發送
IIC總線是以串行方式傳輸數據,從數據字節的最高位開始傳送,每一個數據位在SCL上都有一個時鐘脈沖相對應。在時鐘線高電平期間數據線上必須保持穩定的邏輯電平狀態,高電平為數據1,低電平為數據0。只有在時鐘線為低電平時,才允許數據線上的電平狀態變化,如下圖所示:
圖二
為深入理解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協議有深刻的理解,才能將程序完好的寫出來。希望通過這篇博文,能讓你明白其中的道理。
*博客內容為網友個人發布,僅代表博主個人觀點,如有侵權請聯系工作人員刪除。