1.1 前言
在使用I2C通信時,一般會用到軟件模擬I2C。目前網絡上能搜索到的軟件模擬I2C一般都是模擬I2C主機,很少有模擬I2C從機的例程。由于I2C主機在進行數據收發時,有明確的可預見性,也就是主機明確知道什么時候要進行數據的收發操作,而且I2C的同步時鐘信號也是由主機產生的,所以實現起來相對來說比較簡單。而I2C從機的通信受制于主機,即什么時候需要進行數據的收發都是由主機發起的,數據收發的發起時機具有隨機性,所以實現方法不能參照軟件模擬I2C主機那樣使用單純的軟件查詢狀態的方法。由于實際使用時,MCU的固件還會執行其他的操作,所以如果單純使用軟件查詢的方法來判斷I2C通信的起始信號不太現實。這里提供一種軟件模擬I2C從機的實現方法,考慮使用GPIO中斷的方法來及時接收I2C通信的起始信號,并進行數據的收發。
1.2 測試平臺
這里使用的開發環境和相關硬件如下。
- 操作系統:Ubuntu 20.04.2 LTS x86_64(使用uname -a命令查看)
- 集成開發環境(IDE):Eclipse IDE for Embedded C/C++ Developers,Version: 2021-06 (4.20.0)
- 硬件開發板:STM32F429I-DISCO
- 本文對應的例程代碼鏈接如下。
https://download.csdn.net/download/goodrenze/85272480
1.3 軟件模擬I2C從機實現方法
這里結合開發板STM32F429I-DISCO上的STM32F429ZI的單片機來演示軟件模擬I2C從機的實現方法。
I2C通信的時序圖如下圖1所示。
圖1 I2C通信時序圖
I2C通信的時序中關鍵的幾個點如下。
1)START和ReSTART信號:用于標識I2C通信的開始,時序特點是SCL為高電平的時候,SDA從高電平變成低電平。
2)STOP信號:用于標識I2C通信的結束,時序特點是SCL為高電平的時候,SDA從低電平變成高電平。
3)應答信號:I2C通信每傳輸完8個比特的數據位后,緊接著需要傳輸應答標志位,當該位為0時,是ACK應答信號,該位為1時,是NACK無應答信號。應答信號在SCL的第9個時鐘周期的位置。
4)數據采集時刻:I2C通信的數據在SCL的上升沿進行采集確認,所以在SCL的高電平期間,數據必須保持不變,防止數據采集出錯。當然,START信號和STOP信號的時序在SCL高電平期間是特殊情況,具有專門的含義。
5)數據更新時刻:I2C通信的數據更新需要在SCL為低電平的時候進行。
通過以上幾個關鍵點,軟件模擬I2C從機的基本思路就有了。由于各個關鍵點基本都發生在SCL或SDA的上升沿或者下降沿的地方,所以可以將用于模擬I2C通信引腳的GPIO口配置成邊沿中斷,這樣就可以通過中斷實時抓取邊沿信號,并在中斷中進行及時的數據處理。使用GPIO的邊沿中斷來模擬I2C從機的好處是可以實時獲取到START和STOP信號,I2C主機發過來的數據可以通過中斷得到及時處理,而且程序主流程無需關心模擬I2C從機的相關處理,可以處理其他事務。
因為是I2C從機,所以SCL引腳直接固定成輸入引腳即可,而SDA信號由于是雙向的,所以需要根據I2C通信中的各個狀態來設置輸入或輸出方向。另外,由于GPIO中斷只在GPIO配置成輸入時才會產生,所以默認情況下,SDA必須設置成輸入引腳。
程序的具體設計思路如下。
1)將SCL和SDA引腳設置成GPIO的邊沿中斷模式,默認為輸入引腳。I2C通信狀態機設置成默認的IDLE狀態。SCL的中斷用于處理數據的收發,SDA的中斷只用于START/ReSTART/STOP這些特殊信號的判斷。
2)SDA引腳中斷處理思路:發生下降沿中斷,并且SCL為高電平,則收到START信號,狀態機更新成START狀態;發生上升沿中斷,并且SCL為高電平,則收到STOP信號,緊接著I2C通信就應該處于空閑狀態,所以這里直接將狀態機設置成IDLE狀態。
3)SCL引腳中斷處理思路:
A. 發生下降沿中斷時
A1. 如果狀態機為START狀態,則I2C通信正式開始,準備開始接收設備地址,狀態機更新成DATA狀態。
A2. 如果狀態機為DATA狀態,SCL下降沿計數小于8時,如果是主機讀取數據,則更新SDA的位數據輸出。SCL下降沿計數等于8時,進入應答階段,狀態機更新成ACK狀態;如果是主機寫入數據,并且是設備地址數據,則判斷設備地址是否匹配,如果設備地址匹配,則將SDA設置成輸出,并輸出ACK信號,否則如果地址不匹配,則SDA保持為輸入狀態,不輸出ACK信號;如果是主機讀取數據,將SDA設置成輸入,準備接收主機的應答信號。
A3. 如果狀態機為ACK狀態,這時應答信號已經傳輸完畢,狀態機更新成DATA狀態,準備繼續接收或發送數據。如果是主機寫入數據,將SDA設置成輸入,繼續接收后續數據;如果是主機讀取數據,將SDA設置成輸出,繼續發送后續數據。
A4. 如果狀態機為NACK狀態,說明緊接著I2C通信將停止或重新啟動,準備接收STOP或者ReSTART信號,所以需要將SDA設置成輸入。此時狀態機狀態保持不變。
B. 發生上升沿中斷時
B1. 如果狀態機為DATA狀態,I2C通信處于數據階段,如果是主機寫入數據,則采集主機通過SDA發送過來的位數據。
B2. 如果狀態機為ACK狀態,I2C通信處于應答階段,如果是主機讀取數據,則采集主機的應答信號,如果主機應答信號為1,說明主機發送了NACK的應答,狀態機需要更新成NACK狀態,準備接收停止或重新啟動信號。
1.4 軟件模擬I2C從機的代碼實現
根據上面的程序思路,可以開始進行程序代碼的設計,步驟如下。
1)設計I2C從機通信對應的結構體,I2C通信狀態定義,I2C通信相關的宏定義的聲明。部分代碼如下。
// ...
#define SW_SLAVE_ADDR 0xA2
#define SW_SLAVE_SCL_CLK_EN() __HAL_RCC_GPIOB_CLK_ENABLE()
#define SW_SLAVE_SDA_CLK_EN() __HAL_RCC_GPIOB_CLK_ENABLE()
#define SW_SLAVE_SCL_PRT GPIOB
#define SW_SLAVE_SCL_PIN GPIO_PIN_6
#define SW_SLAVE_SDA_PRT GPIOB
#define SW_SLAVE_SDA_PIN GPIO_PIN_7
#define GPIO_MODE_MSK 0x00000003U
#define I2C_STA_IDLE 0
#define I2C_STA_START 1
#define I2C_STA_DATA 2
#define I2C_STA_ACK 3
#define I2C_STA_NACK 4
#define I2C_STA_STOP 5
#define I2C_READ 1
#define I2C_WRITE 0
#define GPIO_DIR_IN 0
#define GPIO_DIR_OUT 1
// ...
typedef struct _SwSlaveI2C_t
{
uint8_t State; // I2C通信狀態
uint8_t Rw; // I2C讀寫標志:0-寫,1-讀
uint8_t SclFallCnt; // SCL下降沿計數
uint8_t Flag; // I2C狀態標志,BIT0:0-地址無效,1-地址匹配
uint32_t StartMs; // I2C通信起始時間,單位ms,用于判斷通信是否超時
uint8_t* RxBuf; // 指向接收緩沖區的指針
uint8_t* TxBuf; // 指向發送緩沖區的指針
uint8_t RxIdx; // 接收緩沖區數據寫入索引,最大值255
uint8_t TxIdx; // 發送緩沖區數據讀取索引,最大值255
}SwSlaveI2C_t;
extern SwSlaveI2C_t SwSlaveI2C;
// ...
2)I2C通信引腳SCL/SDA對應的GPIO的初始化。這里使用PB6/PB7引腳。代碼如下。
void InitSwSlaveI2C(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
/* Enable I2C GPIO clock */
SW_SLAVE_SCL_CLK_EN();
SW_SLAVE_SDA_CLK_EN();
/* Configure SCL GPIO pin */
GPIO_InitStructure.Pin = SW_SLAVE_SCL_PIN;
GPIO_InitStructure.Mode = GPIO_MODE_OUTPUT_OD;
GPIO_InitStructure.Pull = GPIO_PULLUP;
GPIO_InitStructure.Speed = GPIO_SPEED_FAST;
HAL_GPIO_Init(SW_SLAVE_SCL_PRT, &GPIO_InitStructure);
/* Configure SDA GPIO pin */
GPIO_InitStructure.Pin = SW_SLAVE_SDA_PIN;
HAL_GPIO_Init(SW_SLAVE_SDA_PRT, &GPIO_InitStructure);
/* Configure SCL GPIO pin as input interruption with pull up */
GPIO_InitStructure.Pin = SW_SLAVE_SCL_PIN;
GPIO_InitStructure.Mode = GPIO_MODE_IT_RISING_FALLING;
HAL_GPIO_Init(SW_SLAVE_SCL_PRT, &GPIO_InitStructure);
/* Configure SDA GPIO pin as input interruption with pull up */
GPIO_InitStructure.Pin = SW_SLAVE_SDA_PIN;
HAL_GPIO_Init(SW_SLAVE_SDA_PRT, &GPIO_InitStructure);
/* Enable and set EXTI Line9_5 Interrupt to the highest priority */
HAL_NVIC_SetPriority(EXTI9_5_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(EXTI9_5_IRQn);
}
3)由于SCL/SDA引腳被設置成中斷引腳,需要實現GPIO的中斷處理函數。中斷處理函數中已經包含了軟件模擬I2C從機的所有功能。代碼如下。
void EXTI9_5_IRQHandler(void)
{
I2cGpioIsr();
}
void I2cGpioIsr(void)
{
uint32_t temp;
// 處理SCL的上下沿中斷
if(__HAL_GPIO_EXTI_GET_IT(SW_SLAVE_SCL_PIN) != RESET)
{
__HAL_GPIO_EXTI_CLEAR_IT(SW_SLAVE_SCL_PIN);
// 更新通信起始時間
SwSlaveI2C.StartMs = HAL_GetTick();
// SCL的下降沿事件處理,此時需要更新要傳輸的數據
if((SW_SLAVE_SCL_PRT->IDR & SW_SLAVE_SCL_PIN) == (uint32_t)GPIO_PIN_RESET)
{
switch(SwSlaveI2C.State)
{
case I2C_STA_START: // 起始信號的下降沿,初始化相關參數并轉到接收比特數據狀態
SwSlaveI2C.SclFallCnt = 0;
SwSlaveI2C.RxIdx = 0;
SwSlaveI2C.TxIdx = 0;
SwSlaveI2C.Flag = 0; // 默認地址不匹配
SwSlaveI2C.RxBuf[SwSlaveI2C.RxIdx] = 0;
SwSlaveI2C.Rw = I2C_WRITE; // 第1字節為設備地址,一定是寫入
SwSlaveI2C.State = I2C_STA_DATA;
break;
case I2C_STA_DATA:
SwSlaveI2C.SclFallCnt++;
if(8 > SwSlaveI2C.SclFallCnt)
{
// 如果是主機讀取數據,則在SCL低電平時更新比特數據
if(SwSlaveI2C.Rw == I2C_READ)
{
if(SwSlaveI2C.TxBuf[SwSlaveI2C.TxIdx] & (1 << (7 - SwSlaveI2C.SclFallCnt)))
{
SET_SDA_PIN();
}
else
{
CLR_SDA_PIN();
}
}
}
else if(8 == SwSlaveI2C.SclFallCnt)
{
if(SwSlaveI2C.Rw == I2C_WRITE)
{
// 從第一個地址字節中獲取讀寫標志位,并判斷地址是否匹配
if(SwSlaveI2C.RxIdx == 0)
{
if((SwSlaveI2C.RxBuf[0] & 0xFE) == SW_SLAVE_ADDR)
{
SwSlaveI2C.Flag = 1; // 地址匹配
SwSlaveI2C.Rw = SwSlaveI2C.RxBuf[0] & 0x01;
}
}
if(SwSlaveI2C.Flag)
{
// 如果是主機寫入數據,且地址匹配,則接收完8比特數據后,需要發送ACK信號進行應答
SET_SDA_DIR(temp, GPIO_DIR_OUT);
CLR_SDA_PIN();
}
}
else
{
// 如果是主機讀取數據,需要將SDA設置成輸入以便判斷應答標志位狀態
SET_SDA_DIR(temp, GPIO_DIR_IN);
// 如果是主機讀取數據,準備發送下一個字節的數據
SwSlaveI2C.TxIdx++;
}
// 接收或發送完8比特數據后,準備發送或接收應答信號
SwSlaveI2C.State = I2C_STA_ACK;
}
break;
case I2C_STA_ACK:
SwSlaveI2C.SclFallCnt = 0;
if(SwSlaveI2C.Rw == I2C_WRITE)
{
// 如果是主機寫入數據,且ACK發送完畢,則SDA設置成輸入,繼續接收數據
SET_SDA_DIR(temp, GPIO_DIR_IN);
SwSlaveI2C.RxIdx++;
SwSlaveI2C.RxBuf[SwSlaveI2C.RxIdx] = 0;
}
else
{
// 如果是主機讀取數據,且ACK接收完畢,則SDA設置成輸出,繼續發送數據
SET_SDA_DIR(temp, GPIO_DIR_OUT);
if(SwSlaveI2C.TxBuf[SwSlaveI2C.TxIdx] & 0x80)
{
SET_SDA_PIN();
}
else
{
CLR_SDA_PIN();
}
}
SwSlaveI2C.State = I2C_STA_DATA;
break;
case I2C_STA_NACK: // 如果收到了NACK,則后面將是STOP或者ReSTART信號,需要將SDA設置成輸入
SwSlaveI2C.SclFallCnt = 0;
SET_SDA_DIR(temp, GPIO_DIR_IN);
break;
}
}
// SCL的上升沿事件處理,此時需要采集數據,而且在數據階段,SCL高電平時數據必須保持不變
else
{
switch(SwSlaveI2C.State)
{
case I2C_STA_DATA: // 數據階段,如果是主機寫入數據,則采集比特數據
if((I2C_WRITE == SwSlaveI2C.Rw) && (8 > SwSlaveI2C.SclFallCnt))
{
if(SW_SLAVE_SDA_PRT->IDR & SW_SLAVE_SDA_PIN)
{
SwSlaveI2C.RxBuf[SwSlaveI2C.RxIdx] |= (1 << (7 - SwSlaveI2C.SclFallCnt));
}
}
break;
case I2C_STA_ACK: // 應答階段,如果是主機讀取數據,則判斷ACK/NACK信號,默認狀態是ACK
if((SwSlaveI2C.Rw == I2C_READ) && (SW_SLAVE_SDA_PRT->IDR & SW_SLAVE_SDA_PIN))
{
SwSlaveI2C.State = I2C_STA_NACK;
}
break;
}
}
}
else if(__HAL_GPIO_EXTI_GET_IT(SW_SLAVE_SDA_PIN) != RESET)
{
__HAL_GPIO_EXTI_CLEAR_IT(SW_SLAVE_SDA_PIN);
if((SW_SLAVE_SDA_PRT->IDR & SW_SLAVE_SDA_PIN) == (uint32_t)GPIO_PIN_RESET)
{
// SCL為高電平時,SDA從高變低,說明是起始信號
if(SW_SLAVE_SCL_PRT->IDR & SW_SLAVE_SCL_PIN)
{
SwSlaveI2C.State = I2C_STA_START;
}
}
else
{
// SCL為高電平時,SDA從低變高,說明是停止信號,一次I2C通信結束,直接將狀態設置成空閑
if(SW_SLAVE_SCL_PRT->IDR & SW_SLAVE_SCL_PIN)
{
SwSlaveI2C.State = I2C_STA_IDLE;
}
}
}
}
4)為了確保模擬I2C從機通信的可靠性,額外設計了I2C通信超時處理函數。在I2C通信進行的過程中,如果通信出現了中斷,則通過超時判斷來重置I2C從機狀態,確保出現通信異常時可以從異常狀態中自動恢復。該函數需要在主流程中調用。代碼如下。
void CheckSwSlaveI2cTimeout(void)
{
uint32_t TimeMs, TimeCurMs;
if(SwSlaveI2C.State != I2C_STA_IDLE)
{
TimeCurMs = HAL_GetTick();
if(TimeCurMs >= SwSlaveI2C.StartMs)
{
TimeMs = TimeCurMs - SwSlaveI2C.StartMs;
}
else
{
TimeMs = ~(SwSlaveI2C.StartMs - TimeCurMs) + 1;
}
if(500 <= TimeMs)
{
// I2C通信超時的話,重置狀態機,并把SDA設置成輸入
SwSlaveI2C.State = I2C_STA_IDLE;
SET_SDA_DIR(TimeMs, GPIO_DIR_IN);
}
}
}
5)軟件模擬I2C從機相關功能驗證代碼。這里需要借助STM32的另外一個I2C主機進行配合測試。這里將PF0/PF1對應的引腳配置成I2C主機,主機直接使用STM32的硬件I2C實現。PF0/PF1分別和PB7/PB6連接,然后驗證數據收發的正確性。具體代碼參見上面的工程鏈接。這里只展示最終的測試結果數據。如下圖所示。
軟件模擬I2C從機狀態
I2C主機發送數據
軟件模擬I2C從機接收數據
圖2 軟件模擬I2C從機數據接收驗證結果
軟件模擬I2C從機狀態
軟件模擬I2C從機發送數據
I2C主機接收數據
圖3 軟件模擬I2C從機數據發送驗證結果
1.5 軟件模擬I2C從機的注意事項
本例程中,對于400kbps速率的I2C通信,在進行代碼編譯鏈接時,需要使用-Ofast的優化方式,以提高中斷處理函數的執行速度,使程序能正確執行。如果使用默認的無優化配置,會造成程序無法正確運行。
對于主頻比較低的MCU,使用這里提供的軟件模擬I2C從機進行I2C通信時,建議使用100kpbs以下的通信速率,并且注意使用可以提高代碼執行速度的代碼優化配置。
另外,建議將用于模擬SDA/SCL的GPIO引腳中斷優先級設置成最高,以便能及時響應I2C通信時序的中斷。
-
mcu
+關注
關注
146文章
17017瀏覽量
350363 -
主機
+關注
關注
0文章
985瀏覽量
35070 -
I2C
+關注
關注
28文章
1482瀏覽量
123344 -
軟件模擬
+關注
關注
0文章
8瀏覽量
7220 -
從機
+關注
關注
0文章
3瀏覽量
855
發布評論請先 登錄
相關推薦
評論