一、I2C通訊協議簡介
I2C(Inter-Integrated Circuit)匯流排是一種由 PHILIPS 公司開發的兩線式序列匯流排,用於連線微控制器以及其外圍裝置。它是由資料線 SDA 和時鐘線 SCL 構成的序列匯流排,可傳送和接收資料,在 CPU 與被控 IC 之間、IC 與 IC 之間進行雙向傳送。
I2C 匯流排有如下特點:
- 匯流排由資料線 SDA 和時鐘線 SCL 構成的序列匯流排,資料線用來傳輸資料,時鐘線用來同步資料收發。
- 匯流排上每一個器件都有一個唯一的地址識別,所以我們只需要知道器件的地址,根據時序就可以實現微控制器與器件之間的通訊。
- 資料線 SDA 和時鐘線 SCL 都是雙向線路,都透過一個電流源或上拉電阻連線到正的電壓,所以當匯流排空閒的時候,這兩條線路都是高電平。
- 匯流排上資料的傳輸速率在標準模式下可達 100kbit/s 在快速模式下可達 400kbit/s,在高速模式下可達 3.4Mbit/s。
- 匯流排支援裝置連線。在使用 I2C 通訊匯流排時,可以有多個具備 I2C 通訊能力的裝置掛載在上面,同時支援多個主機和多個從機,連線到匯流排的介面數量只由匯流排電容 400pF 的限制決定。
I2C 匯流排掛載多個器件的示意圖,如下圖所示:
二、I2C匯流排時序圖
#define I2C_SCL_GPIO_PORT GPIOB
#define I2C_SCL_GPIO_PIN GPIO_PIN_8
#define I2C_SCL_GPIO_CLK_ENABLE() __HAL_RCC_GPIOB_CLK_ENABLE()
#define I2C_SDA_GPIO_PORT GPIOB
#define I2C_SDA_GPIO_PIN GPIO_PIN_9
#define I2C_SDA_GPIO_CLK_ENABLE() __HAL_RCC_GPIOB_CLK_ENABLE()
#define I2C_SCL(x) do{ x ? \
HAL_GPIO_WritePin(I2C_SCL_GPIO_PORT,I2C_SCL_GPIO_PIN, GPIO_PIN_SET):\
HAL_GPIO_WritePin(I2C_SCL_GPIO_PORT,I2C_SCL_GPIO_PIN, GPIO_PIN_RESET);\
}while(0)
#define I2C_SDA(x) do{ x ? \
HAL_GPIO_WritePin(I2C_SDA_GPIO_PORT, I2C_SDA_GPIO_PIN, GPIO_PIN_SET):\
HAL_GPIO_WritePin(I2C_SDA_GPIO_PORT, I2C_SDA_GPIO_PIN, GPIO_PIN_RESET);\
}while(0)
#define I2C_READ_SDA() HAL_GPIO_ReadPin(I2C_SDA_GPIO_PORT, I2C_SDA_GPIO_PIN)
#define I2C_DELAY() Delay_us(10)
透過宏定義識別符號的方式去定義 SCL 和 SDA 兩個引腳,同時透過宏定義的方式定義了 I2C_SCL() 和 I2C_SDA()設定這兩個管腳可以輸出 0 或者 1,主要還是透過 HAL 庫的 GPIO 操作函式實現的。另外方便在 I2C 操作函式中呼叫讀取 SDA 管腳的資料,這裡直接宏定義 I2C_READ_SDA 實現。
/**
* @brief 模擬I2C初始化函式
*
*/
void I2C_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
// 使能I2C的SCL和SDA引腳對應的GPIO時鐘
I2C_SCL_GPIO_CLK_ENABLE();
I2C_SDA_GPIO_CLK_ENABLE();
GPIO_InitStruct.Pin = I2C_SCL_GPIO_PIN; // I2C SCL引腳
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD; // 開漏輸出模式
GPIO_InitStruct.Pull = GPIO_PULLUP; // 使用上拉電阻
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; // 輸出速度
HAL_GPIO_Init(I2C_SCL_GPIO_PORT, &GPIO_InitStruct);
GPIO_InitStruct.Pin = I2C_SDA_GPIO_PIN; // I2C SCL引腳
HAL_GPIO_Init(I2C_SDA_GPIO_PORT, &GPIO_InitStruct);
// 空閒時,I2C匯流排SCL為高電平,I2C SDA為高電平
I2C_SCL(1);
I2C_SDA(1);
}
開漏模式具有線與特性,即如果有很多開漏模式的引腳連在一起的時候,只有當所有引腳都輸出高阻態,電平才為 1,只要有其中一個為低電平時,就等於接地,使得整條線路都為低電平 0。
另外在開漏輸出模式下,施密特觸發器是開啟的,所以 IO 口引腳的電平狀態會被採集到輸入資料暫存器中,如果對輸入資料暫存器進行讀訪問可以得到 IO 口的狀態。也就是說開漏輸出模式下,我們可以對 IO 口進行讀資料。
①、起始訊號
當 SCL 為高電平期間,SDA 由高到低的跳變。起始訊號是一種電平跳變時序訊號,而不是一個電平訊號。該訊號由主機發出,在起始訊號產生後,匯流排就處於被佔用狀態,準備資料傳輸。
/**
* @brief I2C產生起始訊號函式
*
* @note SCL為高電平期間,SDA從高電平往低電平跳變
*/
void I2C_Start(void)
{
// 1、釋放SDA和SCL,並延遲,空閒狀態
I2C_SDA(1);
I2C_SCL(1);
I2C_DELAY();
// 2、拉低SDA,SDA產生下降沿,並延遲
I2C_SDA(0);
I2C_DELAY();
// 3、鉗住SCL匯流排,準備傳送資料/接收資料,並延時
I2C_SCL(0);
I2C_DELAY();
}
②、停止訊號
當 SCL 為高電平期間,SDA 由低到高的跳變。停止訊號也是一種電平跳變時序訊號,而不是一個電平訊號。該訊號由主機發出,在停止訊號發出後,匯流排就處於空閒狀態。
/**
* @brief I2C產生結束訊號函式
*
* @note SCL為高電平期間,SDA從低電平往高電平跳變
*/
void I2C_Stop(void)
{
// 1、SDA拉低,SCL拉高,並延遲
I2C_SDA(0);
I2C_SCL(1);
I2C_DELAY();
// 2、拉高SDA,產生上升沿,並延遲
I2C_SDA(1);
I2C_DELAY();
}
③、應答訊號
傳送器每傳送一個位元組,就在第 9 個時鐘脈衝釋放資料線,由接收器反饋一個應答訊號。應答訊號為低電平時,規定為有效應答位(ACK 簡稱應答位),表示接收器已經成功地接收了該位元組;應答訊號為高電平時,規定為非應答位(NACK),一般表示接收器接收該位元組沒有成功。
觀察上圖示號 ③ 就可以發現,有效應答的要求是從機在第 9 個時鐘脈衝之前的低電平期間將 SDA 線拉低,並且確保在該時鐘的高電平期間為穩定的低電平。如果接收器是主機,則在它收到最後一個位元組後,傳送一個 NACK 訊號,以通知被控傳送器結束資料傳送,並釋放 SDA 線,以便主機接收器傳送一個停止訊號。
/**
* @brief 主機檢測應答訊號
*
* @return uint8_t 0,接收應答成功;1,接收應答失敗
*/
uint8_t I2C_WaitAck(void)
{
uint8_t waitTime = 0;
uint8_t ack = 0;
// 1、主機釋放SDA資料線並延遲,此時外部器件可以拉低SDA線
I2C_SDA(1);
I2C_DELAY();
// 2、主機拉高SCL,此時從機可以返回ACK
I2C_SCL(1);
I2C_DELAY();
// 3、SCL高電平期間主機讀取SDA狀態,等待應答
while (I2C_READ_SDA())
{
// 4、如果超時的話,就直接產生結束訊號,非應答
waitTime++;
if (waitTime > 250)
{
I2C_Stop();
ack = 1;
break;
}
}
// 5、SCL=0,結束ACK檢查
I2C_SCL(0);
I2C_DELAY();
// 6、返回是否接收到應答訊號
return ack;
}
I2C_WaitAck() 函式主要用在寫時序中,當啟動起始訊號,傳送完 8bit 資料到從機時,我們就需要等待以及處理接收從機傳送過來的響應訊號或者非響應訊號,一般就是在 I2C_SendOneByte() 函式後面呼叫。
首先先釋放 SDA,把電平拉高,延時等待從機操作 SDA 線,然後主機拉高時鐘線並延時,確保有充足的時間讓主機接收到從機發出的 SDA 訊號,這裡使用的是 I2C_READ_SDA 宏定義去讀取,根據 I2C 協議,主機讀取 SDA 的值為低電平,就表示 “應答訊號”;讀到 SDA 的值為高電平,就表示 “非應答訊號”。在這個等待讀取的過程中加入了超時判斷,假如超過這個時間沒有接收到資料,那麼主機直接發出停止訊號,跳出迴圈,返回等於 1 的變數。在正常等待到應答訊號後,主機會把 SCL 時鐘線拉低並延時,返回是否接收到應答訊號。
SDA 為低電平即應答訊號,高電平即非應答訊號,首先先根據返回 “應答” 或者 “非應答” 兩種情況拉低或者拉高 SDA,並延時等待 SDA 電平穩定,然後主機拉高 SCL 線並延時,確保從機能有足夠時間去接收 SDA 線上的電平訊號。然後主機拉低時鐘線並延時,完成這一位資料的傳送。最後把 SDA 拉高,呈高阻態,方便後續通訊用到。
/**
* @brief 傳送應答訊號或非應答訊號
*
* @param ack 0,傳送應答訊號;1,傳送非應答訊號
*/
void I2C_SendAck(uint8_t ack)
{
// 1、拉低SDA,表示應答,拉高SDA,表示非應答,並延遲
I2C_SDA(ack);
I2C_DELAY();
// 2、主機拉高SCL線,並延遲,確保從機能有足夠時間去接收SDA線上的電平訊號
I2C_SCL(1);
I2C_DELAY();
// 3、主機拉低時鐘線並延時,完成這一位資料的傳送
I2C_SCL(0);
I2C_DELAY();
// 4、釋放SDA線,並延遲
I2C_SDA(1);
I2C_DELAY();
}
④、資料有效性
I2C 匯流排進行資料傳送時,時鐘訊號為高電平期間,資料線上的資料必須保持穩定,只有在時鐘線上的訊號為低電平期間,資料線上的高電平或低電平狀態才允許變化。資料在 SCL 的上升沿到來之前就需準備好。並在下降沿到來之前必須穩定。
在 SCL 低電平期間,從機將資料位依次放到 SDA 匯流排上(高位先行),然後釋放 SCL,主機將在 SCL 高電平期間讀取資料位,所以 SCL 高電平期間不允許有資料變化,依次迴圈上過過程 8 次,即可接收一個位元組(主機在接收之前,需要釋放 SDA)。
/**
* @brief I2C讀取一個位元組函式
*
* @param ack 0,傳送應答訊號,1,傳送非應答訊號
* @return uint8_t
*/
uint8_t I2C_ReadOneByte(uint8_t ack)
{
uint8_t receive = 0;
// 1、主機釋放SDA
I2C_SDA(1);
for (uint8_t i = 0; i < 8; i++)
{
// 2、釋放SCL,主機將在SCL高電平期間讀取資料位
I2C_SCL(1);
I2C_DELAY();
// 3、讀取SDA
if (I2C_READ_SDA())
{
receive |= 0x80 >> i;
}
// 4、拉低SCL,從機切換SDA線輸出資料
I2C_SCL(0);
I2C_DELAY();
}
// 5、傳送應答訊號或非應答訊號
I2C_SendAck(ack);
// 6、返回讀取的資料
return receive;
}
首先可以明確的是時鐘訊號是透過主機發出的,而且接收到的資料大小為 1 位元組,但是 I2C 傳輸的單位是 bit,所以就需要執行 8 次迴圈,才能把一位元組資料接收完整。
首先需要一個變數 receive 存放接收到的資料。在每次迴圈的開始的時候,在 SCL 高電平期間加入延時,確保有足夠的時間能讓資料傳送並進行處理,使用宏定義 I2C_READ_SDA() 就可以判斷讀取到的高低電平,假如 SDA 為高電平,會對 0x80 這個數右移當前的迴圈數,然後這個結果會與 receive 進行或運算,將指定位置 1,如果 SDA 是低電平,則不進行處理,當前位仍為 0。當 SCL 線拉低後,需要加入延時,便於從機切換 SDA 線輸出資料。在 8 次迴圈結束後,我們就獲得了 8bit 資料,把它作為返回值返回,然而按照時序圖,作為主機就需要傳送應答或者非應答訊號,去回覆從機。
⑤、資料傳輸
在 I2C 匯流排上傳送的每一位資料都有一個時鐘脈衝相對應(或同步控制),即在 SCL 序列時鐘的配合下,在 SDA 上逐位地序列傳送每一位資料。資料位的傳輸是邊沿觸發。
資料傳輸時序圖如下:
SCL 低電平期間,主機將資料位依次放到 SDA 匯流排上(高位先行),然後釋放 SCL,從機將在 SCL 高電平期間讀取資料位,所以 SCL 高電平期間 SDA 不允許有資料變化,依次迴圈上述過程 8 次,即可傳送一個位元組。
/**
* @brief I2C傳送一個位元組函式
*
* @param data 待傳送的資料
*/
void I2C_SendOneByte(uint8_t data)
{
for (uint8_t i = 0; i < 8; i++)
{
// 1、傳送資料位的高位
I2C_SDA((data & 0x80) >> 7);
I2C_DELAY();
// 2、釋放SCL,從機將在SCL高電平期間讀取資料位
I2C_SCL(1);
I2C_DELAY();
// 3、拉低SCL
I2C_SCL(0);
// 4、資料左移一位,用於下次傳送
data <<= 1;
}
// 5、傳送完成,主機釋放SDA線
I2C_SDA(1);
}
在 I2C 的傳送函式 I2C_SendOneByte() 中,我們把需要傳送的資料作為形參,形參大小為 1 個位元組。在 I2C 匯流排傳輸中,一個時鐘訊號就傳送一個 bit,所以該函式需要迴圈八次,模擬八個時鐘訊號,才能把形參的 8 個位資料都傳送出去。這裡使用的是形參 data 和 0x80 與運算的方式,判斷其最高位的邏輯值,假如為 1 即需要控制 SDA 輸出高電平,否則為 0 控制 SDA 輸出低電平。
經過第一步的 SDA 高低電平的確定後,接著需要延時,確保 SDA 輸出的電平穩定,在 SCL 保持高電平期間,SDA 線上的資料是有效的,此過程也是需要延時,使得從裝置能夠採集到有效的電平。然後準備下一位的資料,所以這裡需要的是把 data 左移一位,等待下一個時鐘的到來,從裝置進行讀取。把上述的操作重複 8 次就可以把 data 的 8 個位資料傳送完畢,迴圈結束後,把 SDA 線拉高,等待接收從裝置傳送過來的應答訊號。
⑥、空閒狀態
I2C 匯流排的 SDA 和 SCL 兩條訊號線同時處於高電平時,規定為匯流排的空閒狀態。此時各個器件的輸出級場效電晶體均處在截止狀態,即釋放匯流排,由兩條訊號線各自的上拉電阻把電平拉高。
三、I2C寫操作時序圖
主機首先在 I2C 匯流排上傳送起始訊號,那麼這時匯流排上的從機都會等待接收由主機發出的資料。主機接著傳送從機地址 +0(寫操作)組成的 8bit 資料,所有從機接收到該 8bit 資料後,自行檢驗是否是自己的裝置的地址,假如是自己的裝置地址,那麼從機就會發出應答訊號。主機在匯流排上接收到有應答訊號後,才能繼續向從機傳送資料。注意:I2C 匯流排上傳送的資料訊號是廣義的,既包括地址訊號,又包括真正的資料訊號。
四、I2C讀操作時序圖
主機向從機讀取資料的操作,一開始的操作與寫操作有點相似,都是由主機發出起始訊號,接著傳送從機地址 +1(讀操作)組成的 8bit 資料,從機接收到資料驗證是否是自身的地址。 那麼在驗證是自己的裝置地址後,從機就會發出應答訊號,並向主機返回 8bit 資料,傳送完之後從機就會等待主機的應答訊號。假如主機一直返回應答訊號,那麼從機可以一直髮送資料,也就是圖中的(n byte + 應答訊號)情況,直到主機發出非應答訊號,從機才會停止傳送資料。
五、硬體I2C的框圖
STM32 有專門用於負責協議的 I2C 協議的外設,只要配置外設,它就會自動根據協議要求產生通訊訊號,收發資料並快取起來,CPU 只要檢測外設的狀態和訪問資料暫存器,就能完成資料收發。這種由硬體外設處理 I2C 協議的方式減輕了 CPU 的工作。
STM32 的 I2C 外設可用作通訊的主機及從機,支援 100kBit/s 的速率,支援 7 位、10 位裝置地址、支援 DMA 資料傳輸,並具有資料校驗功能。它的 I2C 外設還支援 SMBus2.0 協議。
六、I2C通訊過程
使用 I2C 外設通訊時,在通訊的不同階段它會對 “狀態暫存器 (SR1 及 SR2)” 的不同資料位寫入引數,我們透過讀取這些暫存器標誌來了解通訊狀態。一般,我們會把 STM32 作為主裝置。
6.1、主傳送器
圖中的是 “主傳送器” 流程,即作為 I2C 通訊的主機端時,向外傳送資料時的過程。主傳送器傳送流程及事件說明如下:
- 控制產生起始訊號 (S),當發生起始訊號後,它產生事件 “EV5”,並會對 SR1 暫存器的 “SB” 位置 1,表示起始訊號已經傳送;
- 緊接著傳送裝置地址並等待應答訊號,若有從機應答,則產生事件 “EV6” 及 “EV8”,這時 SR1 暫存器的 “ADDR” 位及 “TXE” 位被置 1,ADDR 為 1 表示地址已經傳送,TXE 為 1 表示資料暫存器為空;
- 以上步驟正常執行並對 ADDR 位清零後,我們往 I2C 的 “資料暫存器 DR” 寫入要傳送的資料,這時 TXE 位會被重置 0,表示資料暫存器非空,I2C 外設透過 SDA 訊號線一位位把資料傳送出去後,又會產生 “EV8” 事件,即 TXE 位被置 1,重複這個過程,就可以傳送多個位元組資料了;
- 當我們傳送資料完成後,控制 I2C 裝置產生一個停止訊號 (P),這個時候會產生 EV8_2 事件,SR1 的 TXE 位及 BTF 位都被置 1,表示通訊結束。
假如我們使能了 I2C 中斷,以上所有事件產生時,都會產生 I2C 中斷訊號,進入同一個中斷服務函式,到 I2C 中斷服務程式後,再透過檢查暫存器位來判斷是哪一個事件。
6.2、主接收器
圖中的是 “主接收器” 流程,即作為 I2C 通訊的主機端時,接收從機傳送過來資料時的過程。主接收器傳送流程及事件說明如下:
- 同主傳送流程,起始訊號 (S) 是由主機端產生的,控制發生起始訊號後,它產生事件 “EV5”,並會對 SR1 暫存器的 “SB” 位置 1,表示起始訊號已經傳送;
- 緊接著傳送裝置地址並等待應答訊號,若有從機應答,則產生事件 “EV6” 這時 SR1 暫存器的 “ADDR” 位被置 1,表示地址已經傳送。
- 從機端接收到地址後,開始向主機端傳送資料。當主機接收到這些資料後,會產生 “EV7” 事件,SR1 暫存器的 RXNE 被置 1,表示接收資料暫存器非空,我們讀取該暫存器後,可對資料暫存器清空,以便接收下一次資料。此時我們可以控制 I2C 傳送應答訊號 (ACK) 或非應答訊號 (NACK),若應答,則重複以上步驟接收資料,若非應答,則停止傳輸;
- 傳送非應答訊號後,產生停止訊號 (P),結束傳輸。
七、I2C常用暫存器
7.1、I2C控制暫存器
I2C 控制暫存器 1 的位 10 ACK 應答使能,該位置 0,不產生應答,該位置 1,在接收一個位元組後返回應答。
I2C 控制暫存器 1 的位 9 STOP 在置 1 的情況下,產生結束訊號。
I2C 控制暫存器 1 的位 8 START 在置 1 的情況下,產生起始訊號。
I2C 控制暫存器 1 的位 1 PE 在置 1 的情況下,使能 I2C 外設。
I2C 控制暫存器 2 的位 5:0 FREQ[5:0] 設定 I2C 外設工作的時脈頻率,這個時脈頻率不是主機與從機間的通訊頻率。
7.2、I2C資料暫存器
7.3、I2C狀態暫存器
I2C 狀態暫存器 1 的位 10 AF 檢測應答是否成功,該位為 1,表示應答失敗,該位為 0,表示應答成功。
I2C 狀態暫存器 1 的位 7 TxE 在傳送過程中 DR 為空時該位置 1,傳送裝置地址時不會置位。
I2C 狀態暫存器 1 的位 6 RxNE 接收模式下資料暫存器非空時置 1。RxNE 不會在地址階段置 1。
I2C 狀態暫存器 1 的位 1 ADDR 會在成功傳送裝置地址後置 1。
I2C 狀態暫存器 1 的位 9 SBB 會在成功產生開始訊號後置 1。
7.4、I2C時鐘控制暫存器
該暫存器配置主機與從機通訊的時脈頻率。
7.5、I2C TRISE暫存器
八、IO引腳複用功能
【1】、I2C1 引腳複用及其重對映功能
功能引腳 | 複用引腳 | 重對映引腳 |
---|---|---|
SCL | PB6 | PB8 |
SDA | PB7 | PB9 |
【2】、IC2C 引腳複用及其重對映功能
功能引腳 | 複用引腳 | 重對映引腳 |
---|---|---|
SCL | PB10 | PF1 |
SDA | PB11 | PF0 |
【3】、I2C3 引腳複用及其重對映功能
功能引腳 | 複用引腳 | 重對映引腳 |
---|---|---|
SCL | PA8 | |
SDA | PC9 |
九、I2C配置步驟
9.1、使能對應的時鐘
#define __HAL_RCC_I2C1_CLK_ENABLE() do { \
__IO uint32_t tmpreg = 0x00U; \
SET_BIT(RCC->APB1ENR, RCC_APB1ENR_I2C1EN);\
/* Delay after an RCC peripheral clock enabling */ \
tmpreg = READ_BIT(RCC->APB1ENR, RCC_APB1ENR_I2C1EN);\
UNUSED(tmpreg); \
} while(0U)
#define __HAL_RCC_I2C2_CLK_ENABLE() do { \
__IO uint32_t tmpreg = 0x00U; \
SET_BIT(RCC->APB1ENR, RCC_APB1ENR_I2C2EN);\
/* Delay after an RCC peripheral clock enabling */ \
tmpreg = READ_BIT(RCC->APB1ENR, RCC_APB1ENR_I2C2EN);\
UNUSED(tmpreg); \
} while(0U)
#define __HAL_RCC_I2C3_CLK_ENABLE() do { \
__IO uint32_t tmpreg = 0x00U; \
SET_BIT(RCC->APB1ENR, RCC_APB1ENR_I2C3EN);\
/* Delay after an RCC peripheral clock enabling */ \
tmpreg = READ_BIT(RCC->APB1ENR, RCC_APB1ENR_I2C3EN);\
UNUSED(tmpreg); \
} while(0U)
#define __HAL_RCC_GPIOA_CLK_ENABLE() do { \
__IO uint32_t tmpreg = 0x00U; \
SET_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOAEN);\
/* Delay after an RCC peripheral clock enabling */ \
tmpreg = READ_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOAEN);\
UNUSED(tmpreg); \
} while(0U)
#define __HAL_RCC_GPIOB_CLK_ENABLE() do { \
__IO uint32_t tmpreg = 0x00U; \
SET_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOBEN);\
/* Delay after an RCC peripheral clock enabling */ \
tmpreg = READ_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOBEN);\
UNUSED(tmpreg); \
} while(0U)
#define __HAL_RCC_GPIOC_CLK_ENABLE() do { \
__IO uint32_t tmpreg = 0x00U; \
SET_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOCEN);\
/* Delay after an RCC peripheral clock enabling */ \
tmpreg = READ_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOCEN);\
UNUSED(tmpreg); \
} while(0U)
#define __HAL_RCC_GPIOF_CLK_ENABLE() do { \
__IO uint32_t tmpreg = 0x00U; \
SET_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOFEN);\
/* Delay after an RCC peripheral clock enabling */ \
tmpreg = READ_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOFEN);\
UNUSED(tmpreg); \
} while(0U)
9.2、配置I2C工作引數
要使用一個外設首先要對它進行初始化,I2C 的初始化函式,其宣告如下:
HAL_StatusTypeDef HAL_I2C_Init(I2C_HandleTypeDef *hi2c);
形參 hi2c 是 I2C 的控制代碼,UART_HandleTypeDef 結構體型別,其定義如下:
typedef struct
{
I2C_TypeDef *Instance; // I2C暫存器地址
I2C_InitTypeDef Init; // I2C初始化結構體
uint8_t *pBuffPtr; // I2C傳輸資料緩衝區
uint16_t XferSize; // I2C傳輸資料大小
__IO uint16_t XferCount; // I2C傳輸資料計數器
__IO uint32_t XferOptions; // I2C傳輸選項
__IO uint32_t PreviousState; // I2C上一次狀態
DMA_HandleTypeDef *hdmatx; // IC2傳送引數設定(DMA)
DMA_HandleTypeDef *hdmarx; // I2C接收引數設定(DMA)
HAL_LockTypeDef Lock; // 鎖物件
__IO HAL_I2C_StateTypeDef State; // I2C傳輸狀態結構體
__IO HAL_I2C_ModeTypeDef Mode; // IC2模式
__IO uint32_t ErrorCode; // I2C錯誤狀態
__IO uint32_t Devaddress; // I2C目標地址
__IO uint32_t Memaddress; // I2C目標記憶體地址
__IO uint32_t MemaddSize; // I2C目標記憶體地址大小
__IO uint32_t EventCount; // I2C事件計數
} I2C_HandleTypeDef;
Instance:指向 I2C 暫存器基地址。實際上這個基地址 HAL 庫已經定義好了,可以選擇範圍:I2C1~ I2C3。
#define I2C1 ((I2C_TypeDef *) I2C1_BASE)
#define I2C2 ((I2C_TypeDef *) I2C2_BASE)
#define I2C3 ((I2C_TypeDef *) I2C3_BASE)
Init:I2C 初始化結構體,用於配置通訊引數。
hdmatx,hdmarx:配置 I2C 傳送接收資料的 DMA 具體引數。
Lock:對資源操作增加操作 鎖保護,可選 HAL_UNLOCKED 或者 HAL_LOCKED 兩個引數。
ErrorCode:串列埠錯誤操作資訊。主要用於存放 I2C 操作的錯誤資訊。
I2C_InitTypeDef 這個結構體型別,該結構體用於配置 I2C 的各個通訊引數,具體說明如下:
typedef struct
{
uint32_t ClockSpeed; // SCL時脈頻率
uint32_t DutyCycle; // 時鐘佔空比
uint32_t OwnAddress1; // 本機地址
uint32_t AddressingMode; // 地址模式
uint32_t DualAddressMode; // 雙重地址模式
uint32_t OwnAddress2; // 本機地址2
uint32_t GeneralCallMode; // 指定廣播呼叫模式
uint32_t NoStretchMode; // 禁止時鐘延長模式
} I2C_InitTypeDef;
ClockSpeed:設定 SCL 時脈頻率,此值要低於 40 0000。
DutyCycle:時鐘週期佔空比,可選值如下。
#define I2C_DUTYCYCLE_2 0x00000000U
#define I2C_DUTYCYCLE_16_9 I2C_CCR_DUTY
OwnAddress1:STM32 的 I2C 裝置自身地址 1。每個連線到 I2C 匯流排上的裝置都要有一個自己的地址,作為主機也不例外。地址可設定為 7 位或 10 位(受下面 AddressingMode 成員決定),只要該地址是 I2C 匯流排上唯一的即可。STM32 的 I2C 外設可同時使用兩個地址,即同時對兩個地址作出響應,這個結構成員 OwnAddress1 配置的是預設的、OAR1 暫存器儲存的地址,若需要設定第二個地址暫存器 OAR2,可使用 DualAddressMode 成員使能,然後設定 OwnAddress2 成員即可,OAR2 不支援 10 位地址。
AddressingMode:地址模式,我們可以設定為 7 位地址模式 或 10 位地址模式。這需要根據實際連線到 I2C 匯流排上裝置的地址進行選擇,這個成員的配置也影響到 OwnAddress1 成員,只有這裡設定成 10 位模式時,OwnAddress1 才支援 10 位地址。
#define I2C_ADDRESSINGMODE_7BIT 0x00004000U
#define I2C_ADDRESSINGMODE_10BIT (I2C_OAR1_ADDMODE | 0x00004000U)
DualAddressMode:雙重地址模式。STM32 的 I2C 外設可同時使用兩個地址,即同時對兩個地址作出響應。
#define I2C_DUALADDRESS_DISABLE 0x00000000U
#define I2C_DUALADDRESS_ENABLE I2C_OAR2_ENDUAL
OwnAddress2:STM32 的 I2C 裝置自身地址 2。
#define UART_HWCONTROL_NONE 0x00000000U
#define UART_HWCONTROL_RTS ((uint32_t)USART_CR3_RTSE)
#define UART_HWCONTROL_CTS ((uint32_t)USART_CR3_CTSE)
#define UART_HWCONTROL_RTS_CTS ((uint32_t)(USART_CR3_RTSE | USART_CR3_CTSE))
NoStretchMode:本成員是關於 I2C 禁止時鐘延長模式設定,用於在從模式下禁止時鐘延長。它在主模式下必須保持關閉。
#define UART_OVERSAMPLING_16 0x00000000U
#define UART_OVERSAMPLING_8 ((uint32_t)USART_CR1_OVER8)
該函式的返回值是 HAL_StatusTypeDef 列舉型別的值,有 4 個,分別是 HAL_OK 表示 成功,HAL_ERROR 表示 錯誤,HAL_BUSY 表示 忙碌,HAL_TIMEOUT 表示 超時。
typedef enum
{
HAL_OK = 0x00U, // 成功
HAL_ERROR = 0x01U, // 錯誤
HAL_BUSY = 0x02U, // 忙碌
HAL_TIMEOUT = 0x03U // 超時
} HAL_StatusTypeDef;
9.3、I2C底層初始化
HAL 庫中,提供 HAL_GPIO_Init() 函式用於配置 GPIO 功能模式,初始化 GPIO。該函式的宣告如下:
void HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init);
該函式的第一個形參 GPIOx 用來 指定埠號,可選值如下:
#define GPIOA ((GPIO_TypeDef *) GPIOA_BASE)
#define GPIOB ((GPIO_TypeDef *) GPIOB_BASE)
#define GPIOC ((GPIO_TypeDef *) GPIOC_BASE)
#define GPIOF ((GPIO_TypeDef *) GPIOF_BASE)
第二個引數是 GPIO_InitTypeDef 型別的結構體變數,用來 設定 GPIO 的工作模式,其定義如下:
typedef struct
{
uint32_t Pin; // 引腳號
uint32_t Mode; // 模式設定
uint32_t Pull; // 上下拉設定
uint32_t Speed; // 速度設定
uint32_t Alternate; // 複用功能設定
}GPIO_InitTypeDef;
成員 Pin 表示 引腳號,範圍:GPIO_PIN_0 到 GPIO_PIN_15。
#define GPIO_PIN_0 ((uint16_t)0x0001) /* Pin 0 selected */
#define GPIO_PIN_1 ((uint16_t)0x0002) /* Pin 1 selected */
#define GPIO_PIN_2 ((uint16_t)0x0004) /* Pin 2 selected */
#define GPIO_PIN_3 ((uint16_t)0x0008) /* Pin 3 selected */
#define GPIO_PIN_4 ((uint16_t)0x0010) /* Pin 4 selected */
#define GPIO_PIN_5 ((uint16_t)0x0020) /* Pin 5 selected */
#define GPIO_PIN_6 ((uint16_t)0x0040) /* Pin 6 selected */
#define GPIO_PIN_7 ((uint16_t)0x0080) /* Pin 7 selected */
#define GPIO_PIN_8 ((uint16_t)0x0100) /* Pin 8 selected */
#define GPIO_PIN_9 ((uint16_t)0x0200) /* Pin 9 selected */
#define GPIO_PIN_10 ((uint16_t)0x0400) /* Pin 10 selected */
#define GPIO_PIN_11 ((uint16_t)0x0800) /* Pin 11 selected */
#define GPIO_PIN_12 ((uint16_t)0x1000) /* Pin 12 selected */
#define GPIO_PIN_13 ((uint16_t)0x2000) /* Pin 13 selected */
#define GPIO_PIN_14 ((uint16_t)0x4000) /* Pin 14 selected */
#define GPIO_PIN_15 ((uint16_t)0x8000) /* Pin 15 selected */
成員 Mode 是 GPIO 的 模式選擇,有以下選擇項:
#define GPIO_MODE_AF_OD 0x00000002U // 開漏式複用
成員 Pull 用於 配置上下拉電阻,有以下選擇項:
#define GPIO_NOPULL 0x00000000U // 無上下拉
#define GPIO_PULLUP 0x00000001U // 上拉
#define GPIO_PULLDOWN 0x00000002U // 下拉
成員 Speed 用於 配置 GPIO 的速度,有以下選擇項:
#define GPIO_SPEED_FREQ_LOW 0x00000000U // 低速
#define GPIO_SPEED_FREQ_MEDIUM 0x00000001U // 中速
#define GPIO_SPEED_FREQ_HIGH 0x00000002U // 高速
#define GPIO_SPEED_FREQ_VERY_HIGH 0x00000003U // 極速
成員 Alternate 用於 配置具體的複用功能,不同的 GPIO 口可以複用的功能不同,具體可參考資料手冊。
#define GPIO_AF4_I2C1 ((uint8_t)0x04) /* I2C1 Alternate Function mapping */
#define GPIO_AF4_I2C2 ((uint8_t)0x04) /* I2C2 Alternate Function mapping */
#define GPIO_AF4_I2C3 ((uint8_t)0x04) /* I2C3 Alternate Function mapping */
9.4、I2C傳送資料
HAL 庫提供 HAL_I2C_Master_Transmit()
函式主機向從機傳送資料。其宣告如下:
HAL_StatusTypeDef HAL_I2C_Master_Transmit(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size, uint32_t Timeout);
形參 hi2c 是 I2C_HandleTypeDef 結構體指標型別的 I2C 控制代碼。形參 DevAddress 是 從機裝置的地址。形參 pData 是 要傳送的資料緩衝區的指標。形參 Size 是 要傳送的資料大小,以位元組為單位。
該函式的返回值是 HAL_StatusTypeDef 列舉型別的值,有 4 個,分別是 HAL_OK 表示 成功,HAL_ERROR 表示 錯誤,HAL_BUSY 表示 忙碌,HAL_TIMEOUT 表示 超時。
HAL 庫提供 HAL_I2C_Mem_Write()
函式主機向從機指定記憶體地址中寫入資料。其宣告如下:
HAL_StatusTypeDef HAL_I2C_Mem_Write(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint16_t MemAddress, uint16_t MemAddSize, uint8_t *pData, uint16_t Size, uint32_t Timeout)
形參 hi2c 是 I2C_HandleTypeDef 結構體指標型別的 I2C 控制代碼。形參 DevAddress 是 從機裝置地址。形參 MemAddress 是 從機裝置的記憶體地址。形參 MemAddSize 是 從機裝置記憶體地址的長度。形參 pData 是 要寫入的資料緩衝區的指標。形參 Size 是 要寫入的資料大小,以位元組為單位。
該函式的返回值是 HAL_StatusTypeDef 列舉型別的值,有 4 個,分別是 HAL_OK 表示 成功,HAL_ERROR 表示 錯誤,HAL_BUSY 表示 忙碌,HAL_TIMEOUT 表示 超時。
形參 DevAddress 是裝置的地址,呼叫前必須左移;
9.5、I2C讀取資料
HAL 庫提供了 HAL_I2C_Master_Receive()
函式主機接收從機發來的資料。該函式的宣告如下:
HAL_StatusTypeDef HAL_I2C_Master_Receive(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size, uint32_t Timeout);
形參 hi2c 是 I2C_HandleTypeDef 結構體指標型別的 I2C 控制代碼。形參 DevAddress 是 從機的裝置地址。形參 pData 是 要儲存資料的緩衝區指標。形參 Size 是 要讀取的資料大小,以位元組為單位。形參 Timeout 設定 超時時間,以毫秒為單位。
該函式的返回值是 HAL_StatusTypeDef 列舉型別的值,有 4 個,分別是 HAL_OK 表示 成功,HAL_ERROR 表示 錯誤,HAL_BUSY 表示 忙碌,HAL_TIMEOUT 表示 超時。
HAL 庫還提供了 HAL_I2C_Mem_Read()
函式主機從指定的指定記憶體地址中讀取資料。該函式的宣告如下:
HAL_StatusTypeDef HAL_I2C_Mem_Read(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint16_t MemAddress, uint16_t MemAddSize, uint8_t *pData, uint16_t Size, uint32_t Timeout)
形參 hi2c 是 I2C_HandleTypeDef 結構體指標型別的 I2C 控制代碼。形參 DevAddress 是 從機的裝置地址。形參 MemAddress 是 從機裝置的記憶體地址。形參 MemAddSize 是 從機裝置記憶體地址的長度。形參 pData 是 要儲存資料的緩衝區指標。形參 Size 是 要讀取的資料大小,以位元組為單位。形參 Timeout 設定 超時時間,以毫秒為單位。
該函式的返回值是 HAL_StatusTypeDef 列舉型別的值,有 4 個,分別是 HAL_OK 表示 成功,HAL_ERROR 表示 錯誤,HAL_BUSY 表示 忙碌,HAL_TIMEOUT 表示 超時。
形參 DevAddress 是裝置的地址,呼叫前必須左移;