一、I2C協定簡介
1、I2C實體層
1、在一個 I2C 通訊總線中,可連接配接多個 I2C 通訊裝置,支援多個通訊主機及多個通訊從機。
2、一個 I2C 總線隻使用兩條總線線路,一條雙向串行資料線(SDA) ,一條串行時鐘線(SCL)。
3、每個連接配接到總線的裝置都有一個獨立的位址,主機可以利用這個位址進行不同裝置之間的通路。
4、總線通過上拉電阻接到電源。當 I2C 裝置空閑時,會輸出高阻态,而當所有裝置都空閑,都輸出高阻态時,由上拉電阻把總線拉成高電平。
2、I2C協定層
1、I2C讀寫過程
主機寫資料到從機:
若配置的方向傳輸位為“寫資料”方向,廣播完位址,接收到應答信号後, 主機開始正式向從機傳輸資料,資料包的大小為 8 位,主機每發送完一個位元組資料,都要等待從機的應答信号(ACK),重複這個過程,可以向從機傳輸 N 個資料。當資料傳輸結束時,主機向從機發送一個停止傳輸信号(P),表示不再傳輸資料。
主機由從機中讀資料:
若配置的方向傳輸位為“讀資料”方向, 廣播完位址,接收到應答信号後, 從機開始向主機傳回資料,資料包大小也為 8 位,從機每發送完一個資料,都會等待主機的應答信号(ACK),重複這個過程,可以傳回 N 個資料,這個 N 也沒有大小限制。當主機希望停止接收資料時,就向從機傳回一個非應答信号(NACK),則從機自動停止資料傳輸。
複合通訊:
該傳輸過程有兩次起始信号(S)。一般在第一次傳輸中,主機通過 SLAVE_ADDRESS 尋找到從裝置後,發送一段“資料”,這段資料通常用于表示從裝置内部的寄存器或存儲器位址(注意區分它與 SLAVE_ADDRESS 的差別);在第二次的傳輸中,對該位址的内容進行讀或寫。也就是說,第一次通訊是告訴從機讀寫位址,第二次則是讀寫的實際内容。
2、通訊的起始和停止信号
當 SCL 線是高電平時 SDA 線從高電平向低電平切換,這個情況表示通訊的起始。當 SCL 是高電平時 SDA線由低電平向高電平切換,表示通訊的停止。起始和停止信号一般由主機産生。
3、位址及資料方向
I2C 協定規定裝置位址可以是 7 位或 10 位,實際中 7 位的位址應用比較廣泛。緊跟裝置位址的一個資料位用來表示資料傳輸方向資料方向位,第 8 位或第 11 位。資料方向位為“1”時表示主機由從機讀資料,該位為“0”時表示主機向從機寫資料。讀資料方向時,主機會釋放對 SDA 信号線的控制,由從機控制 SDA 信号線,主機接收信号,寫資料方向時, SDA 由主機控制,從機接收信号。
4、響應
I2C 的資料和位址傳輸都帶響應。響應包括“應答(ACK)”和“非應答(NACK)”兩種信号。作為資料接收端時,當裝置(無論主從機)接收到 I2C 傳輸的一個位元組資料或位址後,若希望對方繼續發送資料,則需要向對方發送“應答(ACK)”信号,發送方會繼續發送下一個資料;若接收端希望結束資料傳輸,則向對方發送“非應答(NACK)”信号,發送方接收到該信号後會産生一個停止信号,結束信号傳輸。傳輸時主機産生時鐘,在第 9 個時鐘時,資料發送端會釋放 SDA 的控制權,由資料接
收端控制 SDA,若 SDA 為高電平,表示非應答信号(NACK),低電平表示應答信号(ACK)。
二、 I2C架構
引腳:
時鐘控制邏輯:
SCL 線的時鐘信号,由 I2C 接口根據時鐘控制寄存器(CCR)控制。
1、可選擇 I2C 通訊的“标準/快速”模式,這兩個模式分别 I2C 對應 100/400Kbit/s 的通訊速率。
2、在快速模式下可選擇 SCL 時鐘的占空比,可選 Tlow/Thigh=2 或 Tlow/Thigh=16/9模式。
3、STM32的I2C外設挂載在APB1總線上,使用APB1的時鐘源PCLK1。
SCL信号線輸出時鐘計算:
通訊過程 :
1、主發送器
2、主接收器
三、I2C初始化結構體
四、讀寫EEPROM
EEPROM晶片的裝置位址一共有 7 位,其中高 4 位固定為:1010 b,低 3 位則由 A0/A1/A2 信号線的電平決定,R/W 是讀寫方向位,與位址無關。
按照我們此處的連接配接, A0/A1/A2均為 0,是以 EEPROM的7位裝置位址是: 101 0000b ,即 0x50。由于 I2C 通訊時常常是位址跟讀寫方向連在一起構成一個 8 位數,且當 R/W 位為0 時,表示寫方向,是以加上 7 位位址,其值為“0xA0”,常稱該值為 I2C 裝置的“寫位址”;當 R/W 位為 1 時,表示讀方向,加上 7 位位址,其值為“0xA1”,常稱該值為“讀位址”。
EEPROM 晶片中還有一個 WP 引腳,具有寫保護功能,當該引腳電平為高時,禁止寫入資料,當引腳為低電平時,可寫入資料,我們直接接地,不使用寫保護功能。
1、 初始化I2C的GPIO
static void I2C_GPIO_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
/* 使能與 I2C 有關的時鐘 */
RCC_APB1PeriphClockCmd ( RCC_APB1Periph_I2C1, ENABLE );
RCC_APB2PeriphClockCmd ( RCC_APB2Periph_GPIOB, ENABLE );
/* I2C_SCL、 I2C_SDA*/
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD; // 開漏輸出
GPIO_Init(GPIOB, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD; // 開漏輸出
GPIO_Init(GPIOB, &GPIO_InitStructure);
}
2 、配置I2C模式
static void I2C_Mode_Configu(void)
{
I2C_InitTypeDef I2C_InitStructure;
/* I2C 配置 */
I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;
/* 高電平資料穩定,低電平資料變化 SCL 時鐘線的占空比 */
I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;
//I2C主機位址,随機
I2C_InitStructure.I2C_OwnAddress1 =0x5F;
I2C_InitStructure.I2C_Ack = I2C_Ack_Enable ;
/* I2C 的尋址模式 */
I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
/* 通信速率 */
I2C_InitStructure.I2C_ClockSpeed = 400000;
/* I2C 初始化 */
I2C_Init(I2C1, &I2C_InitStructure);
/* 使能 I2C */
I2C_Cmd(I2C1, ENABLE);
}
3、向EEPROM寫入一個位元組的資料
EEPROM 單位元組寫入時序:
EPROM 的單位元組時序規定,向它寫入資料的時候,第一個位元組為記憶體位址,第二個位元組是要寫入的資料内容。
//EEPROM記憶體256K,記憶體位址設定為8位uint8_t
void EEPROM_Byte_Write(uint8_t addr,uint8_t data)
{
//産生起始信号
I2C_GenerateSTART(I2C1, ENABLE);
while(I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)==ERROR);
//EV5時間被檢測到,發送裝置位址
I2C_Send7bitAddress(I2C1, 0xA0, I2C_Direction_Transmitter);
while(I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)==ERROR);
//EV6事件被檢測到,發送要操作的存儲單元位址
I2C_SendData(I2C1, addr);
while(I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTING)==ERROR);
//EV8事件被檢測到,發送要存儲的資料
I2C_SendData(I2C1, data);
while(I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)==ERROR);
//資料傳輸完成
I2C_GenerateSTOP(I2C1, ENABLE);
}
4、EEPROM的頁寫入
EEPROM 頁寫入時序:
向連續位址寫入多個資料的時候,隻要告訴 EEPROM 第一個記憶體位址 address1,後面的資料按次序寫入到 address2、 address3… 這樣可以節省通訊的時間,加快速度。根據頁寫入時序,第一個資料被解釋為要寫入的記憶體位址 address1,後續可連續發送 n個資料,這些資料會依次寫入到記憶體中。其中 AT24C02 型号的晶片頁寫入時序最多可以一次發送 8 個資料(即 n = 8 ),該值也稱為頁大小。
//向EEPROM寫入多個位元組(頁寫入),每次寫入不能超過8個
//EEPROM記憶體256K,記憶體位址設定為8位uint8_t
void EEPROM_Page_Write(uint8_t addr,uint8_t *data,uint8_t numByteToWrite)
{
//産生起始信号
I2C_GenerateSTART(I2C1, ENABLE);
//檢測EV5事件
while(I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)==ERROR);
//發送裝置位址
I2C_Send7bitAddress(I2C1, 0xA0, I2C_Direction_Transmitter);
//檢測EV6事件
while(I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)==ERROR);
//發送要操作的存儲單元位址
I2C_SendData(I2C1, addr);
//檢測EV8事件
while(I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTING)==ERROR);
while(numByteToWrite)
{
//發送要存儲的資料
I2C_SendData(I2C1, *data);
//檢測EV8事件
while(I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)==ERROR);
data++;
numByteToWrite--;
}
//資料傳輸完成
I2C_GenerateSTOP(I2C1, ENABLE);
}
5、利用頁寫入的方式快速寫入多位元組
首位址對齊到頁時的情況:
首位址未對齊到頁時的情況
#define I2C_PageSize 8
/**
* @brief 将緩沖區中的資料寫到 I2C EEPROM 中
* @param
* @arg pBuffer:緩沖區指針
* @arg WriteAddr:寫位址
* @arg NumByteToWrite:寫的位元組數
* @retval 無
*/
void I2C_EE_BufferWrite(u8* pBuffer, u8 WriteAddr,u16 NumByteToWrite)
{
uint8_t NumOfPage=0,NumOfSingle=0,Addr =0,count=0;
/*mod 運算求餘,若 writeAddr 是 I2C_PageSize 整數倍,運算結果 Addr 值為 0*/
Addr = WriteAddr % I2C_PageSize;
/*差 count 個資料值,剛好可以對齊到頁位址*/
count = I2C_PageSize - Addr;
/*計算出要寫多少整數頁*/
NumOfPage = NumByteToWrite / I2C_PageSize;
/*mod 運算求餘,計算出剩餘不滿一頁的位元組數*/
NumOfSingle = NumByteToWrite % I2C_PageSize;
// Addr=0,則 WriteAddr 剛好按頁對齊 aligned
// 這樣就很簡單了,直接寫就可以,寫完整頁後
// 把剩下的不滿一頁的寫完即可
if (Addr == 0) {
/* 如果 NumByteToWrite < I2C_PageSize */
if (NumOfPage == 0) {
I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle);
I2C_EE_WaitForWritingEnd();
}
/* 如果 NumByteToWrite > I2C_PageSize */
else {
/*先把整數頁都寫了*/
while (NumOfPage--) {
I2C_EE_PageWrite(pBuffer, WriteAddr, I2C_PageSize);
I2C_EE_WaitForWritingEnd();
WriteAddr += I2C_PageSize;
pBuffer += I2C_PageSize;
}
/*若有多餘的不滿一頁的資料,把它寫完*/
if (NumOfSingle!=0) {
I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle);
I2C_EE_WaitForWritingEnd();
}
}
}
// 如果 WriteAddr 不是按 I2C_PageSize 對齊
// 那就算出對齊到頁位址還需要多少個資料,然後
// 先把這幾個資料寫完,剩下開始的位址就已經對齊
// 到頁位址了,代碼重複上面的即可
else {
/* 如果 NumByteToWrite < I2C_PageSize */
if (NumOfPage== 0) {
if (NumOfSingle > count) {
I2C_EE_PageWrite(pBuffer, WriteAddr,NumOfSingle);
I2C_EE_WaitForWritingEnd();
WriteAddr += count;
pBuffer += count;
}
}
/* 如果 NumByteToWrite > I2C_PageSize */
else {
/*位址不對齊多出的 count 分開處理,不加入這個運算*/
NumByteToWrite -= count;
NumOfPage = NumByteToWrite / I2C_PageSize;
NumOfSingle = NumByteToWrite % I2C_PageSize;
/*先把 WriteAddr 所在頁的剩餘位元組寫了*/
if (count != 0) {
I2C_EE_PageWrite(pBuffer, WriteAddr, count);
I2C_EE_WaitForWritingEnd();
/*WriteAddr 加上 count 後,位址就對齊到頁了*/
WriteAddr += count;
pBuffer += count;
}
/*把整數頁都寫了*/
while (NumOfPage--) {
I2C_EE_PageWrite(pBuffer, WriteAddr, I2C_PageSize);
I2C_EE_WaitForWritingEnd();
WriteAddr += I2C_PageSize;
pBuffer += I2C_PageSize;
}
/*若有多餘的不滿一頁的資料,把它寫完*/
if (NumOfSingle != 0) {
I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle);
I2C_EE_WaitForWritingEnd();
}
}
}
}
6、從EEPROM讀取資料
EEPROM 讀取時序:
從 EEPROM 讀取資料是一個複合的 I2C 時序,它實際上包含一個寫過程和一個讀過程。讀時序的第一個通訊過程中,使用 I2C 發送裝置位址尋址(寫方向),接着發送要讀取的“記憶體位址”;第二個通訊過程中,再次使用 I2C 發送裝置位址尋址,但這個時候的資料方向是讀方向;在這個過程之後, EEPROM 會向主機傳回從“記憶體位址”開始的資料,一個位元組一個位元組地傳輸,隻要主機的響應為“應答信号”,它就會一直傳輸下去,主機想結束傳輸時,就發送“非應答信号”,并以“停止信号”結束通訊,作為從機的 EEPROM也會停止傳輸。
/**
* @brief 從EEPROM 裡面讀取一塊資料
* @param data:存放從 EEPROM 讀取的資料的緩沖區指針
* @param addr:接收資料的 EEPROM 的位址
* @param numByteToRead:要從 EEPROM 讀取的位元組數
*/
void EEPROM_Read(uint8_t addr,uint8_t *data,uint8_t numByteToRead)
{
//産生起始信号
I2C_GenerateSTART(I2C1, ENABLE);
//檢測EV5事件
while(I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)==ERROR);
//發送裝置位址
I2C_Send7bitAddress(I2C1, 0xA0, I2C_Direction_Transmitter);
//檢測EV6事件
while(I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)==ERROR);
//發送要操作的存儲單元位址
I2C_SendData(I2C1, addr);
//檢測EV8事件
while(I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTING)==ERROR);
//第二次起始信号
I2C_GenerateSTART(I2C1, ENABLE);
//檢測EV5事件
while(I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)==ERROR);
//發送裝置位址 最後一位為讀 方向為接收
I2C_Send7bitAddress(I2C1, 0xA1, I2C_Direction_Receiver);
//檢測EV6事件
while(I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED)==ERROR);
while(numByteToRead)
{
if(numByteToRead==1)
{
//如果為最後一個位元組
I2C_AcknowledgeConfig(I2C1, DISABLE);
}
//EV7事件被檢測到,即資料寄存器有新的有效資料
while(I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED)==ERROR);
//接收資料
*data=I2C_ReceiveData(I2C1);
data++;
numByteToRead--;
}
//資料傳輸完成
I2C_GenerateSTOP(I2C1, ENABLE);
//使能應答,友善下次I2C傳輸
I2C_AcknowledgeConfig(I2C1, DISABLE);
}
7、狀态等待函數
void I2C_EE_WaitForWritingEnd()
{
do
{
I2C_GenerateSTART(I2C1, ENABLE);
//檢查EV5事件
while(I2C_GetFlagStatus(I2C1, I2C_FLAG_SB)==RESET);
I2C_Send7bitAddress(I2C1, 0xA0, I2C_Direction_Transmitter);
}
//檢查EV6事件
while(I2C_GetFlagStatus(I2C1, I2C_FLAG_ADDR)==RESET);
I2C_AcknowledgeConfig(I2C1, DISABLE);
}