關于軟件模擬IIC協議GPIO究竟使用開漏還是推挽輸出(附代碼)
軟件模擬IIC的一些疑問
一、為什么選擇軟件模擬?
現在很多的開發板都支持硬件IIC,尤其筆者經常的接觸的STM32,直接調用相關函數就可以完成數據傳輸而且問題少,不明白為什么不直接使用硬件,反而要自己寫,不是會很麻煩。后來換了開發板,遇到一些bug就漸漸明白了原因了
1.可移植性強
并非所有開發板硬件IIC使用方法均一致,不同廠商之間的硬件IIC使用是不同的,比如TI開發板和STM開發板,在需要快速上手開發時軟件模擬無疑是最好的選擇,不論是什么型號的MCU均可以快速移植。
2.易于拓展
雖然一條IIC總線支持掛載多個主機與從機,但也有數量限制,掛載設備過多會導致信號噪聲,嚴重影響數據傳輸。若硬件IIC少,就必須依靠軟件模擬IIC擴容
二、推挽還是開漏?(SDA盡可能使用開漏)
網絡上有很多模擬IIC的開源代碼,但筆者十分疑惑為什么有人使用開漏輸出,有人使用推挽輸出。筆者早期學習的時候記得很清楚要使用開漏輸出,但在實際操作中發現推挽輸出也是可以進行軟件IIC的。
1.開漏
開漏輸出無法真正輸出高電平,即高電平時沒有驅動能力,必須進行外部上拉,需要借助外部上拉電阻完成對外驅動(開漏引腳不連接外部的上拉電阻時,只能輸出低電平)。
開漏輸出時,P-MOS截止,N-MOS導通,當輸出數據寄存器輸出0時,經過輸出控制變為邏輯1,導通Vss,輸出高電平。反之N-MOS截至,I/O引腳呈現高阻態。很顯然可以看出芯片內部是無法輸出高電平的,只能進行外接上拉電阻依靠外部電平輸出高電平。
值得注意的是,開漏是不需要對GPIO口進行輸入輸出模式切換的,本身就可直接讀取電平狀況
開漏輸出有一個優點,通過改變上拉電源的電壓,便可以改變傳輸電平,比如STM32用3.3V供電,將GPIO設置為開漏輸出模式,同時引腳外部接上拉電阻到5V,則高電平時可以拉到5V,不需要接特殊的電平轉換電路或芯片
2.推挽
這里類比開漏簡單介紹,推挽不需要依靠外部即可直接輸出高低電平,但是推挽輸出不能在輸出狀態下直接讀取引腳狀態,必須進行模式切換,轉換為輸入模式
PS:為什么呢,明明推挽輸出時施密特觸發器仍在工作?
解答:推挽輸出是強輸出電流模式,在此模式下的輸出通道上的推挽結構MOS管,屬于強上拉和強下拉的,這會影響讀取輸入數據寄存器的值,強上拉意味著會將來自外部的低電平輸入強制置高,強下拉意味著會將來自外部的高電平輸入強制置低
3.推挽可以模擬IIC嗎
通過查閱資料以及實際操作,推挽是可以用作模擬IIC的,然而需要注意以下問題,并且實際使用體驗較差,可能導致通信緩慢。
(1)無法實現線與,只能固定連接一個IIC設備
將多個開漏輸出的IO口,連接到一條線上。通過一只上拉電阻,在不增加任何器件的情況下,形成“與邏輯”關系。這也是I2C,SMBus等總線判斷總線占用狀態的原理。
(2)注意連接的IIC設備輸出電平大小,與IO口電平容忍情況
由于使用推挽時判斷從機應答需要從機拉低/拉高SDA,也就是向IO輸入電平,如果從機輸出5V,而該IO口不能容忍5V很大概率會損壞IO口甚至MCU。
(3)多設備時序沖突問題
I2C被設計運用在多主機多從機的場景,所以不可避免地會遇到一個問題——總線沖突。
若因某種原因時序混亂導致一個IO口輸出高電平,一個輸出低電平,那么就會出現短路現象,有燒壞設備的風險。
4.注意事項
對于模擬IIC,可以SCL使用推挽輸出進行模擬,SDA使用開漏輸出,板載資源并非十分短缺則沒有必要SDA使用推挽輸出。尤其值得注意,只有單主-單從,即整條線上只有一個主機、一個從機,SCL不存在爭搶控制權的情況下,使用推挽輸出模擬SDA才沒有問題。
5.簡單介紹IIC
這里筆者還是簡單介紹一下IIC,主要是加深印象。

IIC協議的數據傳輸主要依靠SCL和SDA兩根線,其中SDA的數據(電平信號變化)僅在SCL高電平期間有效。需要注意,數據的傳遞與接收均為高位在前,低位在后。
- 開始信號(START/S): SCL為高時,SDA從高到低的跳變產生開始信號
- 結束信號(STOP/P): SCL為高時,SDA從低到高的跳變產生結束信號
并且在完成數據接收與發送后主機或者從機都要進行應答(第9位),確認接收成功。
??應答信號分為兩種:
????1)當第9位(應答位)為 低電平 時,為 ACK???信號
????2)當第9位(應答位)為 高電平 時,為 NACK 信號
值得注意的是,現在很多IIC設備均具有連續讀取寫入功能,即主機或從機發送 ACK信號后,被寫入或讀取的從機會進行地址自增,從而不在重新發送開始信號,很快的提高了效率。
四、IIC通信中的問題
對于想要查找IIC通信問題,必須借助邏輯分析儀查看,不然根本無法確認發送的信號是否正確。
地址錯誤(常見硬件IIC)
這種問題常見于OLED,例如OLED的IIC地址為0x78,但事實上使用HAL庫的硬件IIC發送地址應該是0x3C | (讀寫位),原因在于IIC設備地址由8位兩部分組成,前七位為固定地址,最后一位為讀寫位。而HAL庫自帶的IIC默認只需要固定地址,讀寫位HAL庫會自動左移添加。所以我們需要人工右移IIC地址來補償HAL庫代碼。
五、附代碼
筆者通過定義相應結構體,允許不同的IIC接口調用同一套代碼,減少重復工作。需要注意的是,GPIO_PinState為HAL庫自帶的枚舉結構,需要自己根據實際修改
/**
* @file iic_gpio.h
* @brief 模擬I2C的功能實現
* @author 瀚海浮萍
* @date 8/16/2025
*/
#include "iic_gpio.h"
// 定義引腳和端口(SDA SCL)
Temp_IIC iic1 = {GPIOA,GPIO_PIN_9,GPIOA,GPIO_PIN_8};
/*-------------------------GPIO引入()-------------------------------*/
/* 若使用CubeMX配置完成則不需要初始化 */
void I2C_GPIO_Init(Temp_IIC *iic)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
// 初始化SCL引腳
GPIO_InitStruct.Pin = iic->T_SCL_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(iic->GPIO_SCL, &GPIO_InitStruct);
// 初始化SDA引腳
GPIO_InitStruct.Pin = iic->T_SDA_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(iic->GPIO_SDA, &GPIO_InitStruct);
// 將SCL和SDA拉高
HAL_GPIO_WritePin(iic->GPIO_SCL, iic->T_SCL_Pin, GPIO_PIN_SET);
HAL_GPIO_WritePin(iic->GPIO_SCL, iic->T_SDA_Pin, GPIO_PIN_SET);
}
static void Set_High_SCL(Temp_IIC *iic)
{
HAL_GPIO_WritePin(iic->GPIO_SCL, iic->T_SCL_Pin, GPIO_PIN_SET);
}
static void Set_Low_SCL(Temp_IIC *iic)
{
HAL_GPIO_WritePin(iic->GPIO_SCL, iic->T_SCL_Pin, GPIO_PIN_RESET);
}
static void Set_High_SDA(Temp_IIC *iic)
{
HAL_GPIO_WritePin(iic->GPIO_SDA, iic->T_SDA_Pin, GPIO_PIN_SET);
}
static void Set_Low_SDA(Temp_IIC *iic)
{
HAL_GPIO_WritePin(iic->GPIO_SDA, iic->T_SDA_Pin, GPIO_PIN_RESET);
}
static GPIO_PinState Read_SDA(Temp_IIC *iic)
{
return HAL_GPIO_ReadPin(iic->GPIO_SDA, iic->T_SDA_Pin);
}
static void Delay_us(volatile uint32_t delay)
{
int last, curr, val;
int temp;
while (delay != 0)
{
temp = delay > 900 ? 900 : delay;
last = SysTick->VAL;
curr = last - CPU_FREQUENCY_MHZ * temp;
if (curr >= 0)
{
do
{
val = SysTick->VAL;
}
while ((val < last) && (val >= curr));
}
else
{
curr += CPU_FREQUENCY_MHZ * 1000;
do
{
val = SysTick->VAL;
}
while ((val <= last) || (val > curr));
}
delay -= temp;
}
}
/*-------------------------協議基礎代碼實現(除引腳枚舉外,無需用戶修改)-------------------------------*/
// 設置SCL線電平
static void I2C_Set_SCL(Temp_IIC *iic,GPIO_PinState state)
{
if(state)
Set_High_SCL(iic);
else
Set_Low_SCL(iic);
}
// 設置SDA線電平
static void I2C_Set_SDA(Temp_IIC *iic,GPIO_PinState state)
{
if(state)
Set_High_SDA(iic);
Set_Low_SDA(iic);
}
// 讀取SDA線電平
static GPIO_PinState I2C_Read_SDA(Temp_IIC *iic)
{
return Read_SDA(iic);
}
void I2C_Delay(void)
{
Delay_us(8);
}
//開始信號
void I2C_Start(Temp_IIC *iic)
{
I2C_Set_SDA(iic,GPIO_PIN_SET);
I2C_Set_SCL(iic,GPIO_PIN_SET);
I2C_Delay();
I2C_Set_SDA(iic,GPIO_PIN_RESET);
I2C_Delay();
I2C_Set_SCL(iic,GPIO_PIN_RESET);
I2C_Delay();
}
//結束信號
void I2C_Stop(Temp_IIC *iic)
{
I2C_Set_SDA(iic,GPIO_PIN_RESET);
I2C_Set_SCL(iic,GPIO_PIN_SET);
I2C_Delay();
I2C_Set_SDA(iic,GPIO_PIN_SET);
I2C_Delay();
}
// 發送一個字節,返回ACK狀態
IIC_StatusTypeDef I2C_Send_Byte(Temp_IIC *iic,uint8_t byte)
{
for (int8_t i = 7; i >= 0; i--)
{
GPIO_PinState bit = (byte & (1 << i)) ? GPIO_PIN_SET : GPIO_PIN_RESET;
I2C_Set_SDA(iic,bit);
I2C_Delay();
I2C_Set_SCL(iic,GPIO_PIN_SET);
I2C_Delay();
I2C_Set_SCL(iic,GPIO_PIN_RESET);
I2C_Delay();
}
// 接收ACK
I2C_Delay();
I2C_Set_SCL(iic,GPIO_PIN_SET);
I2C_Delay();
GPIO_PinState ack = I2C_Read_SDA(iic); // 讀取ACK信號
I2C_Set_SCL(iic,GPIO_PIN_RESET);
I2C_Delay();
return (ack == GPIO_PIN_RESET) ? IIC_EOK : IIC_ERR;
}
// 讀取一個字節,并發送ACK或NACK
uint8_t I2C_Read_Byte(Temp_IIC *iic,GPIO_PinState ack)
{
uint8_t byte = 0;
I2C_Delay();
for (int8_t i = 7; i >= 0; i--)
{
I2C_Set_SCL(iic,GPIO_PIN_SET);
I2C_Delay();
if (I2C_Read_SDA(iic) == GPIO_PIN_SET)
{
byte |= (1 << i);
}
I2C_Set_SCL(iic,GPIO_PIN_RESET);
I2C_Delay();
}
// 發送ACK或NACK
I2C_Set_SDA(iic,ack); // 發送ACK或NACK
I2C_Delay();
I2C_Set_SCL(iic,GPIO_PIN_SET);
I2C_Delay();
I2C_Set_SCL(iic,GPIO_PIN_RESET);
I2C_Delay();
return byte;
}
/*-------------------------IIC代碼實現(無需用戶修改,直接調用即可)-------------------------------*/
/**
* @brief I2C向指定地址寫入一字節
* @param iic:軟件IIC接口 Addr:設備地址 pData:寫入數據
* @retval 無
**/
IIC_StatusTypeDef I2C_Write_Byte(Temp_IIC *iic, uint8_t Addr, uint8_t pData)
{
I2C_Start(iic);
if (I2C_Send_Byte(iic,Addr << 1) != IIC_EOK)
{
I2C_Stop(iic);
return IIC_ERR;
}
if (I2C_Send_Byte(iic,pData) != IIC_EOK)
{
I2C_Stop(iic);
return IIC_ERR;
}
I2C_Stop(iic);
return IIC_EOK;
}
/**
* @brief 模擬I2C寫入函數,類似HAL_I2C_Mem_Write,僅支持8位地址
* @param iic:軟件IIC接口 devAddr:設備地址 memAddr:內存地址 pData:寫入數據 size:寫入字節數
* @retval 無
**/
IIC_StatusTypeDef G_I2C_Mem_Write(Temp_IIC *iic, uint8_t devAddr, uint8_t memAddr, uint8_t *pData, uint16_t size)
{
I2C_Start(iic);
if (I2C_Send_Byte(iic,devAddr << 1) != IIC_EOK)
{
I2C_Stop(iic);
return IIC_ERR;
}
if (I2C_Send_Byte(iic,memAddr) != IIC_EOK)
{
I2C_Stop(iic);
return IIC_ERR;
}
for (uint16_t i = 0; i < size; i++)
{
if (I2C_Send_Byte(iic,pData[i]) != IIC_EOK)
{
I2C_Stop(iic);
return IIC_ERR;
}
}
I2C_Stop(iic);
return IIC_EOK;
}
/*
* @brief 模擬I2C讀取函數,類似HAL_I2C_Mem_Read,僅支持8位地址
* @param iic:軟件IIC接口 devAddr:設備地址 memAddr:內存地址 pData:讀出數據存儲 size:讀出字節數
* @retval 無
**/
IIC_StatusTypeDef G_I2C_Mem_Read(Temp_IIC *iic, uint8_t devAddr, uint8_t memAddr, uint8_t *pData, uint16_t size)
{
I2C_Start(iic);
if (I2C_Send_Byte(iic, devAddr << 1) != IIC_EOK)
{
I2C_Stop(iic);
return IIC_ERR;
}
if (I2C_Send_Byte(iic, memAddr) != IIC_EOK)
{
I2C_Stop(iic);
return IIC_ERR;
}
I2C_Start(iic); // 重新啟動信號
if (I2C_Send_Byte(iic,(devAddr << 1) | 0x01) != IIC_EOK)
{
I2C_Stop(iic);
return IIC_ERR;
}
for (uint16_t i = 0; i < size; i++)
{
pData[i] = I2C_Read_Byte(iic,(i == size - 1) ? I2C_NACK : I2C_ACK);
}
I2C_Stop(iic);
return IIC_EOK;
}
/*
* @brief 模擬I2C寫入函數,僅支持16位地址
* @param iic:軟件IIC接口 devAddr:設備地址 memAddr:內存地址 pData:寫入數據 size:寫入字節數
* @retval 無
**/
IIC_StatusTypeDef I2C_Mem_Write(Temp_IIC *iic, uint8_t devAddr, uint16_t memAddr, uint8_t *pData, uint16_t size)
{
I2C_Start(iic);
if (I2C_Send_Byte(iic,devAddr << 1) != IIC_EOK)
{
I2C_Stop(iic);
return IIC_ERR;
}
// 發送高位地址字節
if (I2C_Send_Byte(iic,(uint8_t)(memAddr >> 8)) != IIC_EOK)
{
I2C_Stop(iic);
return IIC_ERR;
}
// 發送低位地址字節
if (I2C_Send_Byte(iic,(uint8_t)(memAddr & 0xFF)) != IIC_EOK)
{
I2C_Stop(iic);
return IIC_ERR;
}
// 發送數據
for (uint16_t i = 0; i < size; i++)
{
if (I2C_Send_Byte(iic,pData[i]) != IIC_EOK)
{
I2C_Stop(iic);
return IIC_ERR;
}
}
I2C_Stop(iic);
return IIC_EOK;
}
/*
* @brief 模擬I2C讀取函數,僅支持16位地址
* @param iic:軟件IIC接口 devAddr:設備地址 memAddr:內存地址 pData:讀出數據 size:讀出字節數
* @retval 無
**/
IIC_StatusTypeDef I2C_Mem_Read(Temp_IIC *iic,uint8_t devAddr, uint16_t memAddr, uint8_t *pData, uint16_t size)
{
I2C_Start(iic);
if (I2C_Send_Byte(iic,devAddr << 1) != IIC_EOK)
{
I2C_Stop(iic);
return IIC_ERR;
}
// 發送高位地址字節
if (I2C_Send_Byte(iic,(uint8_t)(memAddr >> 8)) != IIC_EOK)
{
I2C_Stop(iic);
return IIC_ERR;
}
// 發送低位地址字節
if (I2C_Send_Byte(iic,(uint8_t)(memAddr & 0xFF)) != IIC_EOK)
{
I2C_Stop(iic);
return IIC_ERR;
}
I2C_Start(iic); // 重新啟動信號
if (I2C_Send_Byte(iic,(devAddr << 1) | 0x01) != IIC_EOK)
{
I2C_Stop(iic);
return IIC_ERR;
}
// 接收數據
for (uint16_t i = 0; i < size; i++)
{
pData[i] = I2C_Read_Byte(iic,(i == size - 1) ? I2C_NACK : I2C_ACK);
}
I2C_Stop(iic);
return IIC_EOK;
}
/**
* @file iic_gpio.h
* @brief 模擬I2C的功能實現
* @author 瀚海浮萍
* @date 8/16/2025
*/
#ifndef _IIC_GPIO_H_
#define _IIC_GPIO_H_
/*-----------------------根據實況進行修改-------------------------------*/
#include "stm32f1xx_hal.h"
#define CPU_FREQUENCY_MHZ 72 // STM32時鐘主頻
/*-----------------------以下無需進行修改-------------------------------*/
// 定義 I2C ACK 和 NACK
#define I2C_ACK GPIO_PIN_RESET
#define I2C_NACK GPIO_PIN_SET
/* IIC結構定義 */
typedef struct{
GPIO_TypeDef *GPIO_SDA;
uint16_t T_SDA_Pin;
GPIO_TypeDef *GPIO_SCL;
uint16_t T_SCL_Pin;
}Temp_IIC;
typedef enum
{
IIC_EOK = 0x00U,
IIC_ERR = 0x01U
}IIC_StatusTypeDef;
/*-----------------------用戶直接外部調用-------------------------------*/
extern Temp_IIC iic1;
void I2C_GPIO_Init(Temp_IIC *iic);
void I2C_Delay(void);
void I2C_Start(Temp_IIC *iic);
void I2C_Stop(Temp_IIC *iic);
uint8_t I2C_Read_Byte(Temp_IIC *iic,GPIO_PinState ack);
IIC_StatusTypeDef I2C_Send_Byte(Temp_IIC *iic,uint8_t byte);
/*-------------------------用戶API調用-------------------------------*/
// I2C API函數聲明(向指定地址寫入一字節 僅支持8位地址)
IIC_StatusTypeDef I2C_Write_Byte(Temp_IIC *iic, uint8_t Addr, uint8_t pData);
// I2C API函數聲明(僅支持16位地址)
IIC_StatusTypeDef I2C_Mem_Write(Temp_IIC *iic ,uint8_t devAddr, uint16_t memAddr, uint8_t *pData, uint16_t size);
IIC_StatusTypeDef I2C_Mem_Read(Temp_IIC *iic ,uint8_t devAddr, uint16_t memAddr, uint8_t *pData, uint16_t size);
// I2C API函數聲明(僅支持8位地址)
IIC_StatusTypeDef G_I2C_Mem_Write(Temp_IIC *iic, uint8_t devAddr, uint8_t memAddr, uint8_t *pData, uint16_t size);
IIC_StatusTypeDef G_I2C_Mem_Read(Temp_IIC *iic, uint8_t devAddr, uint8_t memAddr, uint8_t *pData, uint16_t size);
#endif /* __IIC_GPIO_H */

浙公網安備 33010602011771號