1、IIC簡介
? ? ? ? I2C,即Inter IC Bus。是由Philips公司開發的一種串行通用數據總線,主要用于近距離、低速的芯片之間的通信;有兩根通信線:SCL(Serial Clock)用于通信雙方時鐘的同步、SDA(Serial Data)用于收發數據;具有同步,半雙工,帶數據應答,支持總線掛載多設備(一主多從、多主多從)等特點。
? ? ? ? IIC總線是一種多主機總線,連接在IIC總線上的器件分為主機和從機,主機有權發起和結束一次通信,而從機只能被主機呼叫;當總線上有多個主機同時啟用總線時,IIC也具備沖突檢測和仲裁的功能來防止錯誤產生;每個連接到IIC總線上的器件都有一個唯一的地址(一般是7bit),且每個器件都可以作為主機也可以作為從機(同一時刻只能有一個主機),總線上的器件增加和刪除不影響其它器件正常工作;IIC總線在通信時,總線上發送數據的器件為發送器,接收數據的器件為接收器。
? ? ? ? 所有I2C設備的SCL連在一起,SDA連在一起;設備的SCL和SDA均要配置成開漏輸出模式;SCL和SDA各添加一個上拉電阻,阻值一般為4.7KΩ左右。
? ? ? ? 在STM32內部集成了硬件I2C收發電路,可以由硬件自動執行時鐘生成、起始終止條件生成、應答位收發、數據收發等功能,減輕CPU的負擔;支持多主機模型;支持7位/10位地址模式;支持不同的通訊速度,標準速度(高達100 kHz),快速(高達400 kHz);支持DMA;兼容SMBus協議。
????????STM32F103C8T6 硬件I2C資源:I2C1、I2C2
? ? ? ? 對于串口這樣的異步時序來說,軟件實現非常麻煩,硬件實現非常簡單,所以串口的實現基本全都倒向硬件了;而對IIC這樣的同步時序來說,軟件實現反而簡單靈活,硬件實現,相比之下,不能完全讓人省心,所以IIC的實現,軟件模擬的情況還是比較多的。
? ? ? ? 考慮到硬件IIC也有很多獨有的優勢,比如執行效率比較高,可以節省軟件資源,功能比較強大,可以實現完整的多主機通信模型,時序波形規整,通信速率快等,所以硬件IIC也是有相應的應用場景的。
2、IIC結構圖
????????以下結構圖基于STM32F103xxx
?????????這里的數據收發的核心部分是數據寄存器和數據移位寄存器,當我們需要發送數據時,可以把一個字節的數據寫到數據寄存器DR,當移位寄存器沒有數據移位時,這個數據寄存器的值就是進一步轉到移位寄存器這里,在移位的過程中,我們就可以直接把下一個數據放在數據寄存器里等著了,一旦數據發送完成,下一個數據就可以無縫連接,繼續發送。當數據由數據寄存器轉到移位寄存器時,就會置狀態寄存器的值TXE位為1,表示發送寄存器為空。
? ? ? ? 在接收時,也是這一路,輸入的數據,一位一位的從引腳移入到移位寄存器里,當一個字節的數據收齊之后,數據就整體從移位寄存器轉到數據寄存器,同時置標志位RXNE,表示接收寄存器非空,這時就可以把數據從數據寄存器讀出來了。
? ? ? ? 基本框圖
3、IIC時序
3.1?IIC時序基本單元
????????起始條件:SCL高電平期間,SDA從高電平切換到低電平
????????終止條件:SCL高電平期間,SDA從低電平切換到高電平
? ? ? ? 發送一個字節:SCL低電平期間,主機將數據位依次放到SDA線上(高位先行),然后釋放SCL,從機將在SCL高電平期間讀取數據位,所以SCL高電平期間SDA不允許有數據變化,依次循環上述過程8次,即可發送一個字節?。
? ? ? ? 接收一個字節:SCL低電平期間,從機將數據位依次放到SDA線上(高位先行),然后釋放SCL,主機將在SCL高電平期間讀取數據位,所以SCL高電平期間SDA不允許有數據變化,依次循環上述過程8次,即可接收一個字節(主機在接收之前,需要釋放SDA)?
????????發送應答:主機在接收完一個字節之后,在下一個時鐘發送一位數據,數據0表示應答,數據1表示非應答
????????接收應答:主機在發送完一個字節之后,在下一個時鐘接收一位數據,判斷從機是否應答,數據0表示應答,數據1表示非應答(主機在接收之前,需要釋放SDA)?
3.2?IIC時序
3.2.1?指定地址寫
? ? ? ? 對于指定設備(Slave Address),在指定地址(Reg Address)下,寫入指定數據(Data)
? ? ? ? 這里這個指定設備,通過從機地址來確定,這里這個指定地址就是某個設備內部的寄存器地址。
3.2.2? 當前地址讀
? ? ? ? 對于指定設備(Slave Address),在當前地址指針指示的地址下,讀取從機數據(Data)
? ? ? ? 在這個時序圖中,主機發送第一個字節,指定讀之后,第2個字節讀寫的方向就要反過來了,控制權交給從機,由從機來發送數據,這時主機無法去指定是由從機哪個寄存器發出的數據,那么這里這個當前地址指針指示的地址就很重要了。在從機中,所有的寄存器都被分配到了一個線性區域中,并且會有一個單獨的指針變量指示著其中一個寄存器,這個指針上電一般默認0地址,并且每寫入和讀出一個字節后,這個指針就會自動自增一次,移動到下一個位置,那么在調用當前地址讀的時序時,主機沒有指定要讀哪個地址,從機就會返回當前指針指向的寄存器的值。
3.2.3?指定地址讀
? ? ? ? 對于指定設備(Slave Address),在指定地址(Reg Address)下,讀取從機數據(Data)
? ? ? ? 這里先指定從機地址是1101000,讀寫標志位是0,代表我要進行寫的操作。經常從機應答之后,再發送一個字節,第二個字節用來指定地址,這個數據就寫入到了從機的地址指針中了,也就是從機接受到這個數據之后,他的寄存器指針就指向了0x19這個位置,之后再重復一個起始條件,因為指定讀寫標志位只能是跟著起始條件的第一個字節,如果想要切換讀寫方向,只能再來個起始條件,然后起始條件后,重新尋址并且指定讀寫標志位,此時讀寫標志位是1,代表我要開始讀了,這時候接收到的就是0x19下的數據。
? ? ? ? 寫入的地址會存在地址指針里面,所以這個地址并不會因為時序的停止而消失。
4、操作流程
4.1?主機發送
? ? ? ? 指定地址寫:首先初始化之后,總線默認空閑狀態,STM32默認是從模式,為了產生一個起始條件,STM32需要寫入控制寄存器(這個要看一下手冊的寄存器描述),之后STM32由從模式轉為主模式,控制完硬件電路之后,要檢查標志位,來看看硬件有沒有達到我們想要的狀態,在這里起始條件之后會發生EV5事件,這個EV5事件就可以把它當成標志位(這里使用EV幾事件,而不寫具體標志位,是因為有的事件會產生多個標志位,這里的EV幾事件就是包含了多個標志位的大標志位,在庫函數中也會有對應),檢查到起始條件已發送的情況下就可以發送一個字節的從機地址了,從機地址需要寫到數據寄存器DR中,寫入DR后,硬件電路會把這個字節發送到移位寄存器中,再把這一個字節發送到IIC總線上,之后硬件會自動接收應答并判斷,如果沒有應答,硬件會置應答失敗的標志位,然后這個標志位可以申請中斷來提醒我們,在尋址完成之后,會發生EV6事件(代表主模式下地址發送結束),EV6事件結束之后是EV8_1事件(TXE標志位=1,移位寄存器空,數據寄存器空),這時需要我們寫入數據寄存器DR進行數據發送了,一旦寫入數據寄存器之后,因為移位寄存器也是空,所以DR會立刻轉到移位寄存器進行發送,這時就是EV8事件(移位寄存器非空,數據寄存器空),這時就是移位寄存器正在發數據的狀態,所以流程這里,數據1的時序就發生了,之后應該是寫入了下一個數據,數據2此刻應該被寫入到數據寄存器里等著了,然后接收應答位之后,數據2就轉入移位寄存器進行發送,此時的狀態是移位寄存器非空,數據寄存器空,所以這是EV8事件又發生了,之后重復該過程,一旦我們檢測要EEV8事件,就可以寫入下一個數據了,最后當我們想要發送的數據寫完之后,這時就沒有新的數據寫入數據寄存器了,當移位寄存器當前的數據移位完成時,此時就是移位寄存器空,數據寄存器也空的狀態,這個事件就是這里的EV8_2事件,當檢測到EV8_2時,就可以產生終止條件了,產生終止條件在控制寄存器中有相應的位可以控制,到這里,一個完整的時序就發送完成了。
4.2?主機接收?
? ? ? ? 從七位主接收來看,起始,從機地址+讀,接收應答,然后就是,接收數據,發送應答,最后一個數據給非應答,之后終止。從這個時序看,這是當前地址讀的一個時序。
5、示例代碼
5.1?軟件讀寫IIC
#include "stm32f10x.h" // Device header
#include "Delay.h" //#define SCL_PORT GPIOB
//#define SCL_PIN GPIO_Pin_10//對端口和引腳的封裝,方便后續修改和移植
void MyI2C_W_SCL(uint8_t BitValue)
{GPIO_WriteBit(GPIOB, GPIO_Pin_10, (BitAction)BitValue);//I2C時序可以稍微慢一點,但是如果快了,那就要看一下手冊對時序時間的要求Delay_us(10);}void MyI2C_W_SDA(uint8_t BitValue)
{GPIO_WriteBit(GPIOB, GPIO_Pin_11, (BitAction)BitValue);//I2C時序可以稍微慢一點,但是如果快了,那就要看一下手冊對時序時間的要求Delay_us(10);}uint8_t MyI2C_R_SDA(void)
{uint8_t BitValue;BitValue = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11);Delay_us(10);return BitValue;
}void MyI2C_Init(void)
{//軟件讀取I2C只要gpio的庫函數就可以了,I2C的庫函數就不用看了//任務一,將SCL和SDA都初始化為開漏輸出模式//任務二,將SCL和SDA都置高電平RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);//配置端口//先定義一個結構體變量GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD; //開漏輸出,開漏輸出模式仍然可以輸入GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //速度50MHzGPIO_Init(GPIOB, &GPIO_InitStructure);//釋放總線,SCL和SDA處于高電平,此時I2C總線處于空閑狀態GPIO_SetBits(GPIOB, GPIO_Pin_10 | GPIO_Pin_11);}void MyI2C_Start(void)
{//根據I2C時序要求,這里兼顧了開始時序和Sr期間時序MyI2C_W_SCL(1);MyI2C_W_SDA(1);MyI2C_W_SDA(0);MyI2C_W_SCL(0);
} void MyI2C_Stop(void)
{MyI2C_W_SDA(0);MyI2C_W_SCL(1);MyI2C_W_SDA(1);
}void MyI2C_SendByte(uint8_t Byte)
{
// MyI2C_W_SDA(Byte & 0x80); //取出數據的最高位,SDA是高位先行
// //釋放SCL,讀走放在SDA的數據
// MyI2C_W_SCL(1);
// //再拉低SCL,就可以放下一位數據了
// MyI2C_W_SCL(0);uint8_t i;for (i = 0; i < 8; i ++){MyI2C_W_SDA(Byte & (0x80 >> i)); //0x80 >> i,表示0x80右移i位MyI2C_W_SCL(1);MyI2C_W_SCL(0);}
}uint8_t MyI2C_ReceiveByte(void)
{uint8_t i, Byte = 0x00;//主機釋放SDA,從機把數據放到SDAMyI2C_W_SDA(1);for (i = 0; i < 8; i ++){//主機釋放SCL,SCL高電平,主機就能讀取數據了MyI2C_W_SCL(1);if (MyI2C_R_SDA() == 1){Byte |= (0x80 >> i);}//再次拉低SCL,這時從機就會把數據放在SDA上MyI2C_W_SCL(0);}return Byte;}void MyI2C_SendAck(uint8_t AckBit)
{//函數進來時,SCL低電平,主機把AckBit放到SDA上MyI2C_W_SDA(AckBit); //SCL高電平,從機讀取應答MyI2C_W_SCL(1);//SCL低電平,進入下一個時序單元MyI2C_W_SCL(0);
}uint8_t MyI2C_ReceiveAck(void)
{uint8_t AckBit;//函數進來時,SCl低電平//主機釋放SDA,防止從機干擾,同時從機應答位放到SDAMyI2C_W_SDA(1);//SCL高電平,主機讀取應答位MyI2C_W_SCL(1);AckBit = MyI2C_R_SDA();//SCL低電平,進入下一個時序單元MyI2C_W_SCL(0);return AckBit;}
5.2?硬件讀寫IIC
//MyI2C_Init();//用硬件來配置I2C外設,對I2C2外設進行初始化,來替換之前用軟件實現的MyI2C_Init(); //第一步,開啟I2C外設和對應GPIO口的時鐘RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2, ENABLE);RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);//第二步,把I2C外設對應的GPIO口初始化為復用開漏模式GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD; //復用開漏輸出,開漏是I2C硬件要求,復用就是GPIO的控制權要交給硬件外設GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //速度50MHzGPIO_Init(GPIOB, &GPIO_InitStructure);//第三步,使用結構體,對整個I2C進行配置I2C_InitTypeDef I2C_InitStructure;I2C_InitStructure.I2C_Mode = I2C_Mode_I2C; //I2C的模式,這里選擇是I2CI2C_InitStructure.I2C_ClockSpeed = 50000; //配置SCL的時鐘頻率,數值越大,SCL頻率越高,數據傳輸就越快,這里寫的是50kHzI2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2; //時鐘占空比,只有在時鐘頻率大于100kHz,也就是進入到快速狀態時才有用,在小雨100kHz的標準速度下,占空比是標準的1:1I2C_InitStructure.I2C_Ack = I2C_Ack_Enable; //應答位配置,這里給enable,默認是給應答的I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit; //這里是指定STM32作為從機,可以響應幾位的地址,這里選擇7位地址I2C_InitStructure.I2C_OwnAddress1 = 0x00; //自身地址1,這個也是stm32作為從機使用的,用于指定stm32的自身地址,方便別的主機呼叫它,這里暫時不需要做從機被別人使喚,隨便給一個,只要不和總線上其它設備的地址重復就可以了I2C_Init(I2C2, &I2C_InitStructure);//第四步,I2C_Cmd,使能I2CI2C_Cmd(I2C2, ENABLE);
//封裝指定地址寫和指定地址讀的時序
//指定地址寫寄存器
void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data)
{
// MyI2C_Start();
// MyI2C_SendByte(MPU6050_ADDRESS); //從機地址+讀寫位
// MyI2C_ReceiveAck();
// //發送指定寄存器地址
// MyI2C_SendByte(RegAddress);
// MyI2C_ReceiveAck();
// //發送指定要寫入指定寄存器地址下的數據
// MyI2C_SendByte(Data);
// MyI2C_ReceiveAck();
// //終止時序
// MyI2C_Stop();uint32_t Timeout;//控制外設電路,實現指定地址寫的時序,來替換上面的WriteRegI2C_GenerateSTART(I2C2, ENABLE); //生成起始條件//對于非阻塞的程序,在函數結束之后,都要等待相應的標志位,來確保這個函數的操作執行到位了//對照PPT流程圖,等待EV5的到來,stm32默認為從機,發送起始條件后變為主機Timeout = 10000;while (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT) != SUCCESS) //監測EV5事件是否發生了//在程序中如果while死循環等待用多了,一旦總線出問題了,就很容易造成整個程序卡死,還要設計一個超時退出的機制{Timeout --;if (Timeout == 0){break; //使用break跳出這個循環,使用return跳出整個函數//在實際項目中,如果想讓代碼更加完善,這里不能只是簡單的break了//這里還應該做一些相應的錯誤處理操作,比如說打印錯誤日志、進行系統復位//或者說,如果項目設計危險的機械結構,就要評估一下,是不是應該進行緊急停機的操作}}
// MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT); //這一句就等同與上面的等待事件和超時退出的結合I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter); //發送從機地址,第三個參數是方向,也就是從機地址的最低位,讀寫位//在這個庫函數中,發送數據都自帶了接收應答的過程,同樣,接收數據也自帶了發送應答的過程,如果應答錯誤,硬件會通過中斷和標志位來提示我們,所以這里發送地址后,應答位就不需要處理了//等待EV6事件while (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED) != SUCCESS);//寫入DR,發送數據I2C_SendData(I2C2, RegAddress);//等待EV8事件while (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTING) != SUCCESS);//發送數據I2C_SendData(I2C2, Data);//等待事件,這里這個是最后一個字節,要等待EV8_2事件while (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED) != SUCCESS);I2C_GenerateSTOP(I2C2, ENABLE);
}//指定地址讀
uint8_t MPU6050_ReadReg(uint8_t RegAddress)
{uint8_t Data;
// MyI2C_Start();
// MyI2C_SendByte(MPU6050_ADDRESS); //從機地址+讀寫位
// MyI2C_ReceiveAck();
// //發送指定寄存器地址
// MyI2C_SendByte(RegAddress);
// MyI2C_ReceiveAck();
//
// //轉入讀的時序,就必須重新指定讀寫位,就必須重新起始
// MyI2C_Start();
// MyI2C_SendByte(MPU6050_ADDRESS | 0x01); //原從機地址,讀寫位為1
// MyI2C_ReceiveAck(); //接收應答后,總線控制權就正式交給從機了
// Data = MyI2C_ReceiveByte();
// //主機接收后,要給從機發送一個應答
// //參數給0,就是給從機應答,給1,就是不給從機應答;想繼續讀多個字節,那就要給應答,從機收到應答后就會繼續發送數據
// MyI2C_SendAck(1);
// MyI2C_Stop();//控制外設電路,來實現指定地址讀的時序,來替換上面的ReadRegI2C_GenerateSTART(I2C2, ENABLE); //生成起始條件while (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT) != SUCCESS); //監測EV5事件是否發生了//在程序中如果while死循環等待用多了,一旦總線出問題了,就很容易造成整個程序卡死,還要設計一個超時退出的機制I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter); //發送從機地址,第三個參數是方向,也就是從機地址的最低位,讀寫位while (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED) != SUCCESS);//寫入DR,發送數據I2C_SendData(I2C2, RegAddress);while (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED) != SUCCESS);I2C_GenerateSTART(I2C2, ENABLE); //重復生成起始條件while (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT) != SUCCESS); //監測EV5事件是否發生了I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Receiver); //第三個參數改為Receiver之后,函數內部就會自動把MPU6050_ADDRESS這個地址的最低位置1了,就不需要手動來改了while (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED) != SUCCESS); //在接收最后一個字節之前,也就是EV7_1事件那里,需要提前把ACK置0,STOP置1,如果只需要讀取一個字節,那在EV6事件之后就要立刻ACK置0,STOP置1,要是設置晚了,時序上就會多一個字節出來I2C_AcknowledgeConfig(I2C2, DISABLE);I2C_GenerateSTOP(I2C2, ENABLE);//等待EV7事件,等EV7事件產生后,一個字節的數據就已經在DR里面了,我們讀取DR即可拿出這一個字節while (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_BYTE_RECEIVED) != SUCCESS);Data = I2C_ReceiveData(I2C2);//ack再次置1,我們的想法是,默認狀態下ACK就是1,給從機應答,在接收最后一個字節之前,臨時把ACK置0,給非應答。//所以在接收函數的最后,要回復默認的ACk = 1,這個流程是為了方便指定地址收多個字節I2C_AcknowledgeConfig(I2C2, ENABLE);return Data;
}