I2C協議(下)——GPIO模擬I2C的簡單讀寫實驗

上一篇介紹了硬體I2C的簡單讀寫實驗,但stm32的硬體I2C存在一點小小的不穩定,所以這裡介紹下用GPIO模擬I2C讀寫。在CPU資源不緊張的情況下,很多人一般會選擇GPIO模擬I2C。

用GPIO模擬I2C,即通過內核來控制SDA和SCL兩條線的電平狀態,來產生信號,如起始信號、停止信號等,嚴格遵循I2C匯流排協議來實現通訊。換句話說,當你把軟體模擬I2C的程序弄懂後,你對I2C匯流排的信號時序便瞭然於胸了。

其實這篇文章就是對上一篇里硬體I2C操作的各個方法進行相應時序的實現,嚴格遵循I2C協議,說白了,就是控制SDA和SCL兩條線不同時刻的不同電平狀態。如產生起始信號的I2C_GenerateSTART()函數。

產生起始、停止信號

I2C協議(上)——基礎介紹里說過,當SCL高電平時,SDA由高電平向低電平轉換,即可產生起始信號,這便是起始信號產生的時序。如圖15-1為這一時序的圖解。

圖15-1

從圖中我們可以看出,當SCL置高時,SDA出現了一個下降沿。將此時序翻譯成程序代碼便可以是如下函數。

#define I2C_SDA_1() GPIOB->BSRR = GPIO_Pin_7 // SDA線置高電平n#define I2C_SDA_0() GPIOB->BRR = GPIO_Pin_7 // SDA線置低電平nn#define I2C_SCL_1() GPIOB->BSRR = GPIO_Pin_6 // SCL線置高電平n#define I2C_SCL_0() GPIOB->BRR = GPIO_Pin_6 // SCL線置低電平nn/**n * @brief 產生起始條件n * @param 無 n * @retval 無n */nvoid i2c_Start(void)n{n I2C_SDA_1();n I2C_SCL_1();n Delay();n I2C_SDA_0();n Delay();n I2C_SCL_0();n Delay();n}n

從函數結構看出,先是把SDA和SCL電平都拉高,延時後將SDA電平拉低,使其出現下降沿,再延時後SCL電平也置低。函數嚴格遵循了起始條件產生的時序。

同理,依葫蘆畫瓢,停止條件的產生也按時序完成。

/**n * @brief 產生停止條件n * @param 無 n * @retval 無n */nvoid i2c_Stop(void)n{n I2C_SDA_0();n I2C_SCL_1();n Delay();n I2C_SDA_1();n}n

在SCL高電平時,SDA出現一個上升沿,即產生停止信號。

產生應答、非應答信號

如下圖紅框所示,在SCL置高時,SDA被拉低則為應答信號,SDA被拉高為非應答信號。

圖15-2

/**n * @brief 產生應答信號ACKn * @param 無n * @retval 無n */nvoid i2c_Ack(void)n{n I2C_SDA_0();n Delay();n I2C_SCL_1();n Delay();n I2C_SCL_0();n Delay();n I2C_SDA_1();t// CPU釋放SDAn}n

同理,非應答信號類似。

/**n * @brief 產生非應答信號NACKn * @param 無n * @retval 無n */nvoid i2c_NAck(void)n{n I2C_SDA_1();n Delay();n I2C_SCL_1();n Delay();n I2C_SCL_0();n Delay();n}n

當然了,還需要一個函數來讀取應答信號。

#define I2C_SDA_READ() (GPIOB->IDR & GPIO_Pin_7) // 讀取SDA電平狀態nn/**n * @brief CPU產生一個時鐘,讀取應答信號ACKn * @param 無n * @retval 返回0表示應答信號,返回1表示非應答信號n */nuint8_t i2c_WaitAck(void)n{n uint8_t re;nn I2C_SDA_1();t// CPU釋放SDA匯流排n Delay();n I2C_SCL_1();t// CPU拉高SCL電平,發送一個時鐘, 此時會返回ACK應答n Delay();n if (I2C_SDA_READ() != 0)t// 判斷讀取的SDA電平狀態n {ntre = 1;n }n elsen {ntre = 0;n }n I2C_SCL_0();n Delay();n n return re;n}n

發送(寫)位元組數據

I2C協議中指出,發送的每個位元組必須為8位,並且從最高位開始傳輸。我們只要通過SDA將該位元組一位一位發送出去即可。

/**n * @brief 發送一個位元組的數據n * @param n * @arg byte:等待發送的位元組n * @retval 無n */nvoid i2c_SendByte(uint8_t byte)n{n uint8_t i;nn /* 先發送位元組的最高位 */n for (i=0; i<8; i++)n {ttn if (byte & 0x80) // 判斷最高位為1或0nt{nt I2C_SDA_1();nt}ntelsent{nt I2C_SDA_0();nt}nntDelay();ntI2C_SCL_1();ntDelay();tntI2C_SCL_0();ntif(i == 7)nt{nt I2C_SDA_1(); // 釋放匯流排nt}ntbyte <<= 1;t // 左移1位,準備發送下一位ntDelay();n }n}n

函數i2c_SendByte()模擬的就是庫函數I2C_SendData(),將位元組數據逐位發送到數據寄存器。發送到數據寄存器後,就需要把位元組數據寫入EEPROM了。

/**n * @brief 在一定的延時時間內等待應答n * @paramn * @arg delay:延時時間 n * @retval 無n */nvoid i2c_WaitAck_Delay(delay)n{n uint8_t ack_status;nn while(delay--)n {n ack_status = i2c_WaitAck();n if(ack_status) // 如果是非應答,主機需要產生停止信號終止傳輸n {n i2c_Stop();nt return 0;n }n elsen {n break;n }n }n}nn/**n * @brief 寫一個位元組到EEPROMn * @param n * @arg WriteAddr:要寫入的內存地址 n * @arg pBuffer:緩衝區指針n * @retval 無n */nuint32_t I2C_ByteWrite(u8 WriteAddr, u8 pBuffer)n{tttn i2c_Start();nn i2c_SendByte(0xA0);t // 發送設備地址n i2c_WaitAck_Delay(1000);nttn i2c_SendByte(WriteAddr); // 發送要寫入的內存地址n i2c_WaitAck_Delay(1000);nn i2c_SendByte(pBuffer); // 開始寫入數據n i2c_WaitAck_Delay(1000);nn i2c_Stop();n return 1;n}n

接收(讀)位元組數據

讀取位元組數據也是逐位完成的,讀到的第一位是最高位。

/**n * @brief 接收一個位元組的數據n * @param 無n * @retval 接收到的數據n */nuint8_t i2c_ReadByte(void)n{n uint8_t i;n uint8_t value = 0;nn for (i=0; i<8; i++)n {ntvalue <<= 1;ntI2C_SCL_1();ntDelay();ntif(I2C_SDA_READ())nt{nt value++;nt}ntI2C_SCL_0();ntDelay();n }nn return value;n}n

從EEPROM讀取數據,只是多了個起始條件,需要先將設備地址和數據內存地址寫入,然後才開始讀取。

/**n * @brief 讀取位元組數據到EEPROMn * @param n * @arg WriteAddr:要寫入的內存地址 n * @arg pBuffer:緩衝區指針n * @arg numByteToRead:讀取的位元組數n * @retval 無n */nuint32_t I2C_Read(u8 WriteAddr, u8* pBuffer, u8 numByteToRead)n{n /* 第一次起始條件 */n i2c_Start();nn i2c_SendByte(0xA0);t // 此處是寫指令n i2c_WaitAck_Delay(1000);nn i2c_SendByte(WriteAddr);n i2c_WaitAck_Delay(1000);nn /* 第二次起始條件 */n i2c_Start();nn i2c_SendByte(0xA1);t // 此處是讀指令n i2c_WaitAck_Delay(1000);tnn while(numByteToRead)n {nt*pBuffer = i2c_ReadByte();nttnt/* 每讀完1個位元組後,需要發送Ack,最後一個位元組發Nack */ntif (numByteToRead != 1)nt{nt i2c_Ack();nt}ntelsent{nt i2c_NAck();nt}n n pBuffer++;n numByteToRead--;n }nn i2c_Stop();n return 0;n}n

前面說了很多次了,用GPIO模擬I2C,需要我們熟悉I2C協議,協議里會詳細說明各個時序,我們只要嚴格按照時序編程即可。

-----------------------------------------------------------------------

文章首發於知乎專欄 - stm32,轉載請私信,並註明原文出處。


推薦閱讀:

DMA數據傳輸
可不可以用動態圖像(動態二維碼)傳輸數據,具體怎麼實現?
SPI(中)——通訊過程時序

TAG:通信 | 数据传输 |