文章目錄
- I2C 物理層
- I2C 協議層
- 1. 數據有效性
- 2. 起始和停止信號
- 3. 應答響應
- 4. 總線的尋址方式
- 5. 數據傳輸
- 5.1 主機向從機發送數據
- 5.2 主機由從機中讀數據
- 5.3 I2C通信復合格式
- I2C 驅動編寫
- 1. 配置 SCL 和 SDA
- 2. I2C起始信號和停止信號
- 3. 等待從設備應答
- 4. 主機發送ACK和NACK信號
- 5. 發送一個Byte
- 6. 接收一個Byte
- 7. 函數聲明
I2C 物理層
兩條信號線:
-
SCL (Serial Clock Line): 時鐘信號線,連接主設備MCU 和所有從設備Device A, B, C;主設備控制SCL時鐘的生成和頻率。
-
SDA (Serial Data Line): 數據信號線,與SCL平行,連接主設備和所有從設備;所有的數據(地址、命令、實際數據)都通過這條線在設備間雙向傳輸。
共享總線架構:
- 多個設備共享物理連接:所有通信通過這兩條線進行,無論MCU與哪個設備通信,都使用SDA和SCL這兩條線;設備通過唯一地址被尋址。
開漏輸出 (Open-Drain):
每一個連接到SDA或SCL線的點,其輸出驅動器都是一個開漏結構,這意味著:
-
器件只能主動拉低信號線(將信號導通到GND)。
-
器件無法主動驅動信號線為高電平,它只能釋放總線(關閉MOS管),讓信號線自然上浮。
上拉電阻 (Pull-up Resistors):
由于總線上所有設備都使用開漏輸出,它們無法主動輸出高電平,上拉電阻為總線提供高電平。
-
當所有設備都不主動拉低總線時,上拉電阻會將SDA和SCL線拉至高電平 ≈Vcc,這是總線的空閑狀態(Idle State)。
-
當某個設備需要發送低電平時,它只需激活其開漏輸出,將總線拉低到GND 0V,上拉電阻限制了此時流過該MOS管的電流。
I2C 協議層
1. 數據有效性
-
SCL 高電平期間:要求數據穩定,接收端設備是在SCL時鐘的上升沿或其前后很短時間窗內對SDA線上的數據進行采樣讀取的,此時數據必須穩定,才能確保接收方采樣到正確無誤的值。
-
SCL 低電平期間:允許數據變化,此時SCL為低,所有設備都知道當前不是采樣時刻,發送端可以切換電平來設置下一個比特(0或1)。
2. 起始和停止信號
-
?起始信號 (Start Condition - S):當 SCL 線處于穩定的 高電平 狀態時,SDA 線發生一個從 高電平 到 低電平 的下降沿跳變。
-
終止信號 (Stop Condition - P):當 SCL 線處于穩定的 高電平 狀態時,SDA 線發生一個從 低電平 到 高電平 的上升沿跳變。?
3. 應答響應
每當發送器件傳輸完一個字節的數據后,后面必須緊跟一個校驗位,這個校驗位是接收端通過控制SDA(數據線)來實現的,以提醒發送端數據我這邊已經接收完成,數據傳送可以繼續進行。這個校驗位其實就是數據或地址傳輸過程中的響應。
響應包括 應答(ACK) 和 非應答(NACK) 兩種信號。
作為數據接收端時,當設備(無論主從機)接收到I2C傳輸的一個字節數據或地址后,若希望對方繼續發送數據,則需要向對方發送 應答(ACK) 信號即低電平脈沖,發送方會繼續發送下一個數據;若接收端希望結束數據傳輸,則向對方發送 非應答(NACK) 信號即高電平脈沖,發送方接收到該信號后會產生一個停止信號,結束信號傳輸。
每一個字節必須保證是8位長度。數據傳送時,先傳送最高位(MSB),每一個被傳送的字節后面都必須跟隨一位應答位(即一幀共有9位)。
4. 總線的尋址方式
D7~D1位組成從機的地址。D0位是數據傳送方向位,為 0 時表示主機向從機寫數據,為 1 時表示主機由從機讀數據。
當主機發送了一個地址后,總線上的每個器件都將頭7位與它自己的地址比較,如果一樣,器件會判定它被主機尋址,其他地址不同的器件將被忽略后面的數據信號。至于是從機接收器還是從機發送器,都由R/W位決定。
5. 數據傳輸
I2C總線上傳送的數據信號是廣義的,既包括地址信號,又包括真正的數據信號。在起始信號后必須傳送一個從機的地址(7位),第8位是數據的傳送方向位(R/W),用“0”表示主機發送(寫)數據(W),“1”表示主機接收數據(R)。每次數據傳送總是由主機產生的終止信號結束。但是,若主機希望繼續占用總線進行新的數據傳送,則可以不產生終止信號,馬上再次發出起始信號對另一從機進行尋址。
5.1 主機向從機發送數據
IIC主機向從機寫數據的過程為:主機先發起始信號S,接著發送含寫標志位(0)的從機地址,若從機回應答A ,主機持續發送數據幀(每幀后從機應答A );最后一幀數據發送后,從機回A / A ̄\overline{A}A ,主機再發終止信號P ,完成寫操作。
5.2 主機由從機中讀數據
IIC主機從從機讀數據的過程為:主機先發起始信號S,接著發送含讀標志位(1)的從機地址,從機回應答A后,從機開始發送數據幀;主機對中間數據幀回應答A ,最后一幀數據傳輸時主機回非應答A ̄\boldsymbol{\overline{A}}A ,隨后主機發送終止信號P ,完成讀數據操作。
5.3 I2C通信復合格式
IIC 傳送過程中需改變方向時,主機先按初始方向(如寫,方向位為 0 )發起始信號 S 、發送從機地址 + 方向位,完成該方向數據傳輸;隨后重發起始信號 S ,再次發送同一從機地址,且讀 / 寫方向位與初始方向反相(如改為讀,方向位為 1 ),從而實現傳送方向切換,繼續后續數據交互。
I2C 驅動編寫
我們使用的是軟件模擬I2C,所以在配置管腳輸出類型時,選擇推挽輸出即可,如果配置為開漏輸出也是可以的,但是需要引腳外接上拉電阻。
1. 配置 SCL 和 SDA
使用PB8作為SCL,PB9作為SDA
iic.h
#ifndef _IIC_H
#define _IIC_H#include "stm32f4xx_hal.h"
#include <stdint.h>// =====================================================
// SCL時鐘線配置
#define IIC_SCL_PORT GPIOB // SCL端口
#define IIC_SCL_PIN GPIO_PIN_8 // SCL引腳// SDA數據線配置
#define IIC_SDA_PORT GPIOB // SDA端口
#define IIC_SDA_PIN GPIO_PIN_9 // SDA引腳
// =====================================================// IO操作宏
#define IIC_SCL_HIGH() HAL_GPIO_WritePin(IIC_SCL_PORT, IIC_SCL_PIN, GPIO_PIN_SET) // SCL置高
#define IIC_SCL_LOW() HAL_GPIO_WritePin(IIC_SCL_PORT, IIC_SCL_PIN, GPIO_PIN_RESET) // SCL置低
#define IIC_SDA_HIGH() HAL_GPIO_WritePin(IIC_SDA_PORT, IIC_SDA_PIN, GPIO_PIN_SET) // SDA置高
#define IIC_SDA_LOW() HAL_GPIO_WritePin(IIC_SDA_PORT, IIC_SDA_PIN, GPIO_PIN_RESET) // SDA置低
#define READ_SDA() HAL_GPIO_ReadPin(IIC_SDA_PORT, IIC_SDA_PIN) // 讀取SDA狀態// 時序參數宏:定義IIC通信時序延遲(單位:微秒)
#define IIC_DELAY() delay_us(2) // 標準時序延遲(適用于數據位變化)
#define IIC_START_DELAY() delay_us(5) // 起始/停止信號額外延遲(確保信號穩定)#endif
iic.c
#include "iic.h"
#include "SysTick.h" // 確保包含延時函數頭文件static void SDA_OUT(void); // 配置SDA為輸出模式
static void SDA_IN(void); // 配置SDA為輸入模式/*** @brief IIC總線初始化* @note 配置SCL和SDA引腳為推挽輸出模式,并釋放總線*/
void IIC_Init(void) {GPIO_InitTypeDef GPIO_InitStruct = {0};// 使能GPIOB時鐘__HAL_RCC_GPIOB_CLK_ENABLE();// 配置SCL和SDA引腳GPIO_InitStruct.Pin = IIC_SCL_PIN | IIC_SDA_PIN; // 同時配置兩個引腳GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; // 推挽輸出模式GPIO_InitStruct.Pull = GPIO_PULLUP; // 內部上拉(確保總線空閑時為高電平)GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; // 高速模式HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);// 初始狀態:總線釋放(SCL和SDA均為高電平)IIC_SDA_HIGH();IIC_SCL_HIGH();
}/*** @brief 配置SDA為輸出模式* @note 在主機需要控制SDA線時調用(發送數據/地址時)*/
static void SDA_OUT(void) {GPIO_InitTypeDef GPIO_InitStruct = {.Pin = IIC_SDA_PIN,.Mode = GPIO_MODE_OUTPUT_PP, // 推挽輸出.Pull = GPIO_PULLUP, // 上拉電阻使能.Speed = GPIO_SPEED_FREQ_HIGH // 高速模式};HAL_GPIO_Init(IIC_SDA_PORT, &GPIO_InitStruct);
}/*** @brief 配置SDA為輸入模式* @note 在主機需要讀取SDA線時調用(接收數據/ACK時)*/
static void SDA_IN(void) {GPIO_InitTypeDef GPIO_InitStruct = {.Pin = IIC_SDA_PIN,.Mode = GPIO_MODE_INPUT, // 輸入模式.Pull = GPIO_PULLUP, // 上拉電阻使能.Speed = GPIO_SPEED_FREQ_HIGH // 高速模式};HAL_GPIO_Init(IIC_SDA_PORT, &GPIO_InitStruct);
}
2. I2C起始信號和停止信號
/*** @brief 產生IIC起始信號* @note 時序:SCL高電平時,SDA從高變低*/
void IIC_Start(void) {SDA_OUT(); // 確保SDA為輸出模式IIC_SDA_HIGH(); // SDA置高IIC_SCL_HIGH(); // SCL置高IIC_START_DELAY(); // 保持起始條件建立時間IIC_SDA_LOW(); // START信號:SCL高時SDA從高變低IIC_DELAY(); // 保持SDA低電平時間IIC_SCL_LOW(); // 鉗住總線,準備發送數據
}/*** @brief 產生IIC停止信號* @note 時序:SCL高電平時,SDA從低變高*/
void IIC_Stop(void) {SDA_OUT(); // 確保SDA為輸出模式IIC_SCL_LOW(); // 確保SCL為低(數據穩定期)IIC_SDA_LOW(); // SDA置低(準備停止信號)IIC_DELAY(); // 延遲IIC_SCL_HIGH(); // SCL置高IIC_START_DELAY(); // 保持停止條件建立時間IIC_SDA_HIGH(); // STOP信號:SCL高時SDA從低變高IIC_DELAY(); // 確保總線釋放
}
3. 等待從設備應答
/*** @brief 等待從設備應答* @retval 0: 收到ACK應答* 1: 未收到ACK(超時)*/
uint8_t IIC_Wait_Ack(void) {uint8_t wait_time = 0;SDA_IN(); // 配置SDA為輸入模式IIC_SDA_HIGH(); // 釋放SDA線(由上拉電阻拉高)IIC_DELAY(); // 短暫延時IIC_SCL_HIGH(); // 主機拉高SCL,從機應在此時拉低SDAIIC_DELAY(); // 等待從機響應// 檢測SDA是否為低電平(ACK信號)while(READ_SDA() == GPIO_PIN_SET) {if(++wait_time > 250) { // 超時檢測(約500us)IIC_Stop(); // 終止通信return 1; // 超時失敗}delay_us(2); // 微小延時避免忙等}IIC_SCL_LOW(); // SCL置低,結束ACK檢測return 0; // 成功收到ACK
}
4. 主機發送ACK和NACK信號
/*** @brief 產生ACK應答信號* @note 主機在接收數據后發送ACK(繼續接收)*/
void IIC_Ack(void) {IIC_SCL_LOW(); // SCL置低(數據變化期)SDA_OUT(); // 控制SDA線IIC_SDA_LOW(); // SDA置低(ACK信號)IIC_DELAY(); // 數據建立時間IIC_SCL_HIGH(); // SCL置高(ACK有效)IIC_DELAY(); // 保持ACK時間IIC_SCL_LOW(); // SCL置低,結束ACK
}/*** @brief 產生NACK非應答信號* @note 主機在接收數據后發送NACK(結束接收)*/
void IIC_NAck(void) {IIC_SCL_LOW(); // SCL置低(數據變化期)SDA_OUT(); // 控制SDA線IIC_SDA_HIGH(); // SDA置高(NACK信號)IIC_DELAY(); // 數據建立時間IIC_SCL_HIGH(); // SCL置高(NACK有效)IIC_DELAY(); // 保持NACK時間IIC_SCL_LOW(); // SCL置低,結束NACK
}
5. 發送一個Byte
/*** @brief IIC發送一個字節* @param txd: 要發送的字節數據* @note 高位先發,每個時鐘周期發送1位*/
void IIC_Send_Byte(uint8_t txd) {SDA_OUT(); // 確保SDA為輸出模式IIC_SCL_LOW(); // SCL置低,開始數據傳輸// 循環發送8位數據(MSB first)for(uint8_t i = 0; i < 8; i++) {// 根據數據最高位設置SDA(txd & 0x80) ? IIC_SDA_HIGH() : IIC_SDA_LOW();txd <<= 1; // 左移準備下一位IIC_DELAY(); // 數據建立時間IIC_SCL_HIGH(); // 產生上升沿,從機采樣數據IIC_DELAY(); // 保持高電平時間IIC_SCL_LOW(); // 產生下降沿,準備下一位IIC_DELAY(); // 數據保持時間}
}
6. 接收一個Byte
/*** @brief IIC讀取一個字節* @param ack: 是否發送應答(1=ACK, 0=NACK)* @retval 讀取到的字節數據* @note 高位先收,每個時鐘周期讀取1位*/
uint8_t IIC_Read_Byte(uint8_t ack) {uint8_t receive = 0;SDA_IN(); // 配置SDA為輸入模式// 循環讀取8位數據(MSB first)for(uint8_t i = 0; i < 8; i++) {IIC_SCL_LOW(); // 確保SCL為低(數據變化期)IIC_DELAY(); // 從機準備數據時間IIC_SCL_HIGH(); // 產生上升沿,主機采樣數據receive <<= 1; // 左移接收寄存器if(READ_SDA()) receive |= 0x01; // 讀取SDA狀態并存儲IIC_DELAY(); // 保持高電平時間}// 根據參數發送ACK/NACKack ? IIC_Ack() : IIC_NAck();return receive;
}
7. 函數聲明
// IIC操作函數聲明
// =====================================================
void IIC_Init(void); // IIC總線初始化
void IIC_Start(void); // 發送IIC起始信號
void IIC_Stop(void); // 發送IIC停止信號
void IIC_Send_Byte(uint8_t txd); // IIC發送一個字節
uint8_t IIC_Read_Byte(uint8_t ack); // IIC讀取一個字節(ack=1發送ACK,ack=0發送NACK)
uint8_t IIC_Wait_Ack(void); // 等待從設備應答(0=成功,1=失敗)
void IIC_Ack(void); // 發送ACK應答信號
void IIC_NAck(void); // 發送NACK非應答信號
// =====================================================