软件I2C
注意:
SDA(串行数据线)和SCL(串行时钟线)都是双向I/O线,接口电路为开漏输出。需通过上拉电阻接电源VCC。
软件I2C说明
说明,有的单片机没有硬件I2C的功能,或者因为电路设计失误不得不使用软件I2C。
软件I2C虽然引脚设定灵活,但效果自然是比不上硬件I2C的……
本人按照STM32的硬件I2C风格编写了软件I2C,这样便于移植更换
具体来说:
虽然软件I2C(也称为Bit-banging I2C)提供了极大的灵活性,允许开发者在任意GPIO引脚上实现I2C通信,但它也有几个明显的缺点:
-
CPU占用率高:由于所有的I2C协议处理(如时序控制、信号的高低电平转换等)都由CPU直接管理,而不是专用硬件模块完成,因此会占用大量的CPU资源。这意味着,在进行I2C通信时,CPU不能执行其他任务,除非通过中断或DMA等方式来部分缓解这个问题。
-
速度受限:由于需要依靠CPU指令来精确地控制时序,软件I2C的速度通常比硬件I2C慢得多。特别是在高速模式下(例如I2C Fast-mode Plus,速度可达1MHz),准确地生成和检测信号变得更加困难,可能无法达到标准要求的速度。
-
时序精度问题:不同CPU架构和运行频率下的指令周期时间不同,这使得编写可移植性好的软件I2C代码变得复杂。此外,中断或其他系统活动可能会干扰软件I2C的时序,导致通信失败。
-
可靠性较低:与硬件I2C相比,软件I2C更容易受到电磁干扰(EMI)和其他电气噪声的影响,因为其不具备硬件级别的过滤和错误纠正能力。同时,软件实现难以完全保证严格的I2C协议时序要求,尤其是在复杂的系统环境中。
-
不支持高级特性:许多现代微控制器的硬件I2C接口支持诸如多主控、时钟扩展(clock stretching)、中断驱动传输、DMA支持等高级特性。而这些特性通常很难或者不可能通过软件I2C来实现。
-
开发和维护成本:虽然对于简单的应用来说,软件I2C可以快速实现,但对于更复杂的应用场景,如需要支持多种速率、处理各种异常情况等,则需要更多的开发工作,并且调试起来也更加困难。
综上所述,尽管软件I2C在某些特定情况下(比如当硬件I2C引脚已被占用,或者需要在多个不同的GPIO引脚上实现I2C通信)非常有用,但考虑到性能、稳定性和功能完整性等方面,硬件I2C通常是更好的选择。如果条件允许,使用硬件I2C可以减少开发时间和提高系统的整体性能。
软件I2C代码
注意,根据自己使用的单片机修改IO口输入输出的相关实现!
SDA(串行数据线)和SCL(串行时钟线)都是双向I/O线,接口电路为开漏输出。需通过上拉电阻接电源VCC。
实现软件微秒级延时的代码
微秒级延时头文件
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
//移植时请根据自身单片机修改
/**
* @brief 软件微秒级延时
*
* @param us 微秒数
*
* @note 需要根据系统频率简单修改
* @note 不怎么准,但一般不需要很准
* @note 准确的延时需要借助硬件定时器等
*/
void softDelayMicro(uint8_t us);
#ifdef __cplusplus
}
#endif
微秒级延时源文件
#include "soft_delay.h"
// 定义每微秒需要执行的大约循环次数
// 注意:这个值可能需要根据实际情况进行调整
// 例如我的stm32f405频率168MHz
#define DELAY_MICROSECOND_LOOP_COUNT (168 / 4)
#pragma GCC push_options //禁止编译器优化这段代码
#pragma GCC optimize ("O0")
void softDelayMicro(uint8_t us)
{
while (us--) {
volatile uint32_t counter = DELAY_MICROSECOND_LOOP_COUNT;
while (counter--);
}
}
#pragma GCC pop_options
软件I2C头文件
/**
* @file soft_I2C.h
* @author your name (you@domain.com)
* @brief 软件I2C
* @version 0.1
* @date 2025-05-12
*
* @copyright Copyright (c) 2025
*
* @note //根据你的单片机修改IO口的输入输出的相关内容!
* @note SDA(串行数据线)和SCL(串行时钟线)都是双向I/O线,接口电路为开漏输出。
*/
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
#include <stdbool.h>
#include "gpio.h"//根据你的单片机修改IO口的输入输出的相关内容!
#include "soft_delay.h"
/**
* @brief 保存某个I2C总线使用的端口引脚信息
*
* @note SDA(串行数据线)和SCL(串行时钟线)都是双向I/O线,接口电路为开漏输出。
*/
typedef struct
{
GPIO_TypeDef* SCL_Port; //时钟线端口
uint32_t SCL_Pin; //时钟线引脚
GPIO_TypeDef* SDA_Port; //数据线端口
uint32_t SDA_Pin; //数据线引脚
}Soft_I2C_Handle;
/**
* @brief 设置软件I2c使用的端口和引脚
*
* @param si2c 指向Soft_I2C_Handle结构体的指针,该结构体包含指定软件I2C的配置信息。
* @param scl_port 时钟线端口
* @param scl_pin 时钟线引脚
* @param sda_port 数据线端口
* @param sda_pin 数据线引脚
*
* @note 请先完成对应端口的初始化配置,然后再使用这个函数
* @note SDA(串行数据线)和SCL(串行时钟线)都是双向I/O线,接口电路为开漏输出。
*/
void Soft_I2C_Config(Soft_I2C_Handle* si2c, GPIO_TypeDef* scl_port, uint32_t scl_pin, GPIO_TypeDef* sda_port, uint32_t sda_pin);
/**
* @brief 软件I2C以主模式发送一定数量的数据。
*
* @param si2c 指向Soft_I2C_Handle结构体的指针,该结构体包含指定软件I2C的配置信息。
* @param DevAddress 目标设备地址:在调用接口之前,必须将数据表中的设备7位地址值左移1位
* @param pData 指向数据缓冲区的指针
* @param Size 要发送的数据量
* @return true 成功
* @return false 失败
*/
bool Soft_I2C_Master_Transmit(Soft_I2C_Handle* si2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size);
/**
* @brief 软件I2C以主模式接收一定数量的数据。
*
* @param si2c 指向Soft_I2C_Handle结构体的指针,该结构体包含指定软件I2C的配置信息。
* @param DevAddress 目标设备地址:在调用接口之前,必须将数据表中的设备7位地址值左移1位
* @param pData 指向数据缓冲区的指针
* @param Size 要接收的数据量
* @return true 成功
* @return false 失败
*/
bool Soft_I2C_Master_Receive(Soft_I2C_Handle* si2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size);
/** @defgroup I2C_Memory_Address_Size I2C Memory Address Size
* @{
*/
#define SOFT_I2C_MEMADD_SIZE_8BIT 0x00000001U
#define SOFT_I2C_MEMADD_SIZE_16BIT 0x00000010U
/**
* @brief 软件I2C以阻塞方式从特定内存地址读取一定数量的数据
*
* @param si2c 指向Soft_I2C_Handle结构体的指针,该结构体包含指定软件I2C的配置信息。
* @param DevAddress 目标设备地址:在调用接口之前,必须将数据表中的设备7位地址值左移1位
* @param MemAddress 目标设备内部存储器地址
* @param MemAddSize 目标设备内部存储器地址的大小,只能是SOFT_I2C_MEMADD_SIZE_8BIT或者SOFT_I2C_MEMADD_SIZE_16BIT
* @param pData 指向数据缓冲区的指针
* @param Size 要接收的数据量
* @return true 成功
* @return false 失败
*
*/
bool Soft_I2C_Mem_Read(Soft_I2C_Handle *si2c, uint16_t DevAddress, uint16_t MemAddress, uint16_t MemAddSize, uint8_t *pData, uint16_t Size);
/**
* @brief 软件I2C以阻塞方式将一定数量的数据写入特定的内存地址
*
* @param si2c 指向Soft_I2C_Handle结构体的指针,该结构体包含指定软件I2C的配置信息。
* @param DevAddress 目标设备地址:在调用接口之前,必须将数据表中的设备7位地址值左移1位
* @param MemAddress 目标设备内部存储器地址
* @param MemAddSize 目标设备内部存储器地址的大小,只能是SOFT_I2C_MEMADD_SIZE_8BIT或者SOFT_I2C_MEMADD_SIZE_16BIT
* @param pData 指向数据缓冲区的指针
* @param Size 要发送的数据量
* @return true 成功
* @return false 失败
*
*/
bool Soft_I2C_Mem_Write(Soft_I2C_Handle *si2c, uint16_t DevAddress, uint16_t MemAddress, uint16_t MemAddSize, uint8_t *pData, uint16_t Size);
#ifdef __cplusplus
}
#endif
软件I2C源文件
/**
* @file soft_I2C.c
* @author your name (you@domain.com)
* @brief 软件I2C
* @version 0.1
* @date 2025-05-12
*
* @copyright Copyright (c) 2025
*
*/
#include "soft_I2C.h"
/****************************** 1. 配置函数 *****************************************/
//移植时请根据自身单片机修改
void Soft_I2C_Config(Soft_I2C_Handle* si2c, GPIO_TypeDef* scl_port, uint32_t scl_pin, GPIO_TypeDef* sda_port, uint32_t sda_pin)
{
si2c->SCL_Port = scl_port;
si2c->SCL_Pin = scl_pin;
si2c->SDA_Port = sda_port;
si2c->SDA_Pin = sda_pin;
}
/****************************** 2. 辅助宏/函数定义 *****************************************/
#define I2C_PIN_HIGH GPIO_PIN_SET
#define I2C_PIN_LOW GPIO_PIN_RESET
//移植时请根据自身单片机修改
static inline void SCL_Low(Soft_I2C_Handle* si2c) {
HAL_GPIO_WritePin(si2c->SCL_Port, si2c->SCL_Pin, GPIO_PIN_RESET);
}
static inline void SCL_High(Soft_I2C_Handle* si2c) {
HAL_GPIO_WritePin(si2c->SCL_Port, si2c->SCL_Pin, GPIO_PIN_SET);
}
static inline void SDA_Low(Soft_I2C_Handle* si2c) {
HAL_GPIO_WritePin(si2c->SDA_Port, si2c->SDA_Pin, GPIO_PIN_RESET);
}
static inline void SDA_High(Soft_I2C_Handle* si2c) {
HAL_GPIO_WritePin(si2c->SDA_Port, si2c->SDA_Pin, GPIO_PIN_SET);
}
static inline GPIO_PinState SDA_Read(Soft_I2C_Handle* si2c) {
return HAL_GPIO_ReadPin(si2c->SDA_Port, si2c->SDA_Pin);
}
/**
* @brief 设置SDA引脚为输入
*
* @param si2c
*/
static void SDA_SetInput(Soft_I2C_Handle* si2c)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = si2c->SDA_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(si2c->SDA_Port, &GPIO_InitStruct);
}
/**
* @brief 设置SDA引脚为输出
*
* @param si2c
*/
static void SDA_SetOutput(Soft_I2C_Handle* si2c)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = si2c->SDA_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(si2c->SDA_Port, &GPIO_InitStruct);
}
/****************************** 3. I2C 协议辅助函数 *****************************************/
/******************** 为了内部实现I2C功能而创造,通常不需要直接调用 ******************************/
/**
* @brief 产生IIC起始信号
*
* @param si2c
*
* @note SCL为高电平时,SDA由高变低表示起始信号;
*/
void I2C_Start(Soft_I2C_Handle *si2c);
/**
* @brief 产生IIC停止信号
*
* @param si2c
*
* @note SCL为高电平时,SDA由低变高表示停止信号;
*/
void I2C_Stop(Soft_I2C_Handle *si2c);
/**
* @brief 发送一字节数据
*
* @param si2c
* @param byte
*
* @note 先传送最高位,后传送低位
*/
void I2C_SendByte(Soft_I2C_Handle *si2c, uint8_t byte);
/**
* @brief 等待应答信号到来
*
* @param si2c
* @return true 接收应答成功
* @return false 接收应答失败
*
* @note 在SCL为高电平期间,若检测到SDA引脚为低电平,则接收设备响应正常
* @note 发送器发送完一个字节数据后接收器必须发送1位应答位来回应发送器
*
*/
bool I2C_Wait_Ack(Soft_I2C_Handle *si2c);
/**
* @brief 接收1字节数据
*
* @param si2c
* @return uint8_t
*/
uint8_t I2C_ReceiveByte(Soft_I2C_Handle *si2c);
/**
* @brief 产生ACK应答
*
* @param si2c
*
* @note 在SCL为高电平期间,SDA引脚输出为低电平,产生应答信号
*/
void I2C_Ack(Soft_I2C_Handle *si2c);
/**
* @brief 不产生ACK应答
*
* @param si2c
*
* @note 在SCL为高电平期间,若SDA引脚为高电平,产生非应答信号
*/
void I2C_Nack(Soft_I2C_Handle *si2c);
/**
* @brief 发送一字节数据,并等待应答
*
* @param si2c
* @param byte
* @return true 应答
* @return false 无应答
*/
bool I2C_TransmitByte(Soft_I2C_Handle* si2c, uint8_t byte);
/**
* @brief 向目标设备发送内存地址
*
* @param si2c 指向Soft_I2C_Handle结构体的指针
* @param MemAddress 目标设备地址:在调用接口之前,必须将数据表中的设备7位地址值左移1位
* @param MemAddSize 目标设备内部存储器地址的大小
*
* @note 根据 MemAddSize 决定发送长度 只能是SOFT_I2C_MEMADD_SIZE_8BIT或者SOFT_I2C_MEMADD_SIZE_16BIT
*/
bool I2C_TransmitMemoryAddress(Soft_I2C_Handle* si2c, uint16_t MemAddress, uint16_t MemAddSize);
void I2C_Start(Soft_I2C_Handle* si2c) {
SDA_SetOutput(si2c);
SDA_High(si2c);
SCL_High(si2c);
softDelayMicro(10);
SDA_Low(si2c);
softDelayMicro(10);
SCL_Low(si2c);//钳住I2C总线,准备发送或接收数据
}
void I2C_Stop(Soft_I2C_Handle* si2c) {
SDA_SetOutput(si2c);
SCL_Low(si2c);
SDA_Low(si2c);
softDelayMicro(10);
SCL_High(si2c);
SDA_High(si2c);//发送I2C总线结束信号
softDelayMicro(10);
}
void I2C_SendByte(Soft_I2C_Handle* si2c, uint8_t byte) {
SDA_SetOutput(si2c);
for (int i = 0; i < 8; i++) {
if (byte & 0x80)
SDA_High(si2c);
else
SDA_Low(si2c);
softDelayMicro(10);
SCL_High(si2c);
softDelayMicro(10);
SCL_Low(si2c);
softDelayMicro(10);
byte <<= 1;//左移准备发下一位
}
}
bool I2C_Wait_Ack(Soft_I2C_Handle* si2c) {
SDA_High(si2c);
SDA_SetInput(si2c);
softDelayMicro(10);
SCL_High(si2c);
softDelayMicro(10);
uint8_t cycleTimes = 0;
while(SDA_Read(si2c) != I2C_PIN_LOW)// 从机拉低表示 ACK
{
if(cycleTimes > 5)
{
// I2C_Stop(si2c);
SCL_Low(si2c);
softDelayMicro(10);
return false;
}
cycleTimes++;
softDelayMicro(10);
}
SCL_Low(si2c);
softDelayMicro(10);
return true;
}
bool I2C_TransmitByte(Soft_I2C_Handle* si2c, uint8_t byte)
{
I2C_SendByte(si2c, byte);
return I2C_Wait_Ack(si2c);
}
uint8_t I2C_ReceiveByte(Soft_I2C_Handle* si2c) {
uint8_t byte = 0;
SDA_SetInput(si2c);
for (int i = 0; i < 8; i++) {
SCL_Low(si2c);
softDelayMicro(10);
SCL_High(si2c);
byte <<= 1;
if (SDA_Read(si2c) == I2C_PIN_HIGH)
byte |= 0x01;
softDelayMicro(10);
}
return byte;
}
void I2C_Ack(Soft_I2C_Handle* si2c) {
SDA_SetOutput(si2c);
SCL_Low(si2c);
softDelayMicro(10);
SDA_Low(si2c);
softDelayMicro(10);
SCL_High(si2c);
softDelayMicro(10);
SCL_Low(si2c);// SCL输出低时,SDA应立即拉高,释放总线
SDA_High(si2c);
softDelayMicro(10);
}
void I2C_Nack(Soft_I2C_Handle* si2c) {
SDA_SetOutput(si2c);
SCL_Low(si2c);
softDelayMicro(10);
SDA_High(si2c);
softDelayMicro(10);
SCL_High(si2c);
softDelayMicro(10);
SCL_Low(si2c);
softDelayMicro(10);
}
bool I2C_TransmitMemoryAddress(Soft_I2C_Handle* si2c, uint16_t MemAddress, uint16_t MemAddSize)
{
if (MemAddSize == SOFT_I2C_MEMADD_SIZE_8BIT)
{
if(!I2C_TransmitByte(si2c, (uint8_t)(MemAddress & 0xFF))) return false;
}
else if (MemAddSize == SOFT_I2C_MEMADD_SIZE_16BIT)
{
if(!I2C_TransmitByte( si2c, (uint8_t)((MemAddress >> 8) & 0xFF) ) ) return false;
if(!I2C_TransmitByte( si2c, (uint8_t)(MemAddress & 0xFF) ) ) return false;
}
return true;
}
/****************** 4. 软件I2C读写函数实现 ***************/
bool Soft_I2C_Master_Transmit(Soft_I2C_Handle* si2c, uint16_t DevAddress, uint8_t* pData, uint16_t Size) {
I2C_Start(si2c);// 启动IIC通信
if(!I2C_TransmitByte(si2c, DevAddress | 0x00)) return false;//发送写数据指令
for (uint16_t i = 0; i < Size; i++) {
if(!I2C_TransmitByte(si2c, pData[i])) return false;
}
I2C_Stop(si2c);
return true;
}
bool Soft_I2C_Master_Receive(Soft_I2C_Handle* si2c, uint16_t DevAddress, uint8_t* pData, uint16_t Size) {
I2C_Start(si2c);
if(!I2C_TransmitByte(si2c, DevAddress | 0x01)) return false;//发送读数据指令
for (uint16_t i = 0; i < Size; i++) {
pData[i] = I2C_ReceiveByte(si2c);
if (i < Size - 1)
I2C_Ack(si2c);
else
I2C_Nack(si2c);
}
I2C_Stop(si2c);
return true;
}
bool Soft_I2C_Mem_Read(Soft_I2C_Handle* si2c, uint16_t DevAddress, uint16_t MemAddress, uint16_t MemAddSize, uint8_t *pData, uint16_t Size)
{
// 第一阶段:发送设备地址 + 内存地址
I2C_Start(si2c); // 假设启动信号总是成功
I2C_SendByte(si2c, DevAddress | 0x00); // 写模式
if (!I2C_Wait_Ack(si2c)) return false;
//向目标设备发送内存地址
if( !I2C_TransmitMemoryAddress(si2c, MemAddress, MemAddSize) ) return false;
// 第二阶段:重复启动,切换为读模式
I2C_Stop(si2c); // 停止信号
I2C_Start(si2c); // 重新启动
if(!I2C_TransmitByte( si2c, DevAddress | 0x01 ) ) return false;// 读模式
// 第三阶段:接收数据
for (uint16_t i = 0; i < Size; i++) {
pData[i] = I2C_ReceiveByte(si2c);
if (i < Size - 1)
I2C_Ack(si2c);
else
I2C_Nack(si2c);
}
I2C_Stop(si2c);
return true;
}
bool Soft_I2C_Mem_Write(Soft_I2C_Handle *si2c, uint16_t DevAddress, uint16_t MemAddress, uint16_t MemAddSize, uint8_t *pData, uint16_t Size)
{
I2C_Start(si2c); // 假设启动信号总是成功
if(!I2C_TransmitByte( si2c, DevAddress | 0x00 ) ) return false;// 写模式
//向目标设备发送内存地址
if( !I2C_TransmitMemoryAddress(si2c, MemAddress, MemAddSize) ) return false;
// 发送数据
for (uint16_t i = 0; i < Size; i++) {
if(!I2C_TransmitByte( si2c, pData[i] ) ) return false;
}
I2C_Stop(si2c);
return true;
}
代码使用说明
1、根据自己的单片机修改相关内容
2、创造句柄并配置引脚(和硬件I2C类似)
3、进行读写操作(和硬件I2C类似)
例
以温湿度传感器AHT20为例
aht20.h
#ifndef __DHT20_H__
#define __DHT20_H__
#ifdef __cplusplus
extern "C" {
#endif
#include "main.h"
#include "soft_I2C.h"
// 初始化AHT20
void AHT20_Init();
// 获取温度和湿度
void AHT20_Read(float *Temperature, float *Humidity);
#ifdef __cplusplus
}
#endif
#endif
aht20.c
#include "aht20.h"
#define AHT20_ADDRESS 0x70
uint8_t readBuffer[6] = {0};
Soft_I2C_Handle AHT20_I2C;
/**
* @brief 初始化AHT20
*/
void AHT20_Init()
{
Soft_I2C_Config(&AHT20_I2C, AHT20_SCL_GPIO_Port, AHT20_SCL_Pin, AHT20_SDA_GPIO_Port, AHT20_SDA_Pin);
uint8_t readBuffer;
HAL_Delay(40);
Soft_I2C_Master_Receive(&AHT20_I2C, AHT20_ADDRESS, &readBuffer, 1);
if ((readBuffer & 0x08) == 0x00)
{
uint8_t sendBuffer[3] = {0xBE, 0x08, 0x00};
Soft_I2C_Master_Transmit(&AHT20_I2C, AHT20_ADDRESS, sendBuffer, 3);
}
}
/**
* @brief 获取温度和湿度
* @param Temperature: 存储获取到的温度
* @param Humidity: 存储获取到的湿度
*/
void AHT20_Read(float *Temperature, float *Humidity)
{
uint8_t sendBuffer[3] = {0xAC, 0x33, 0x00};
uint8_t readBuffer[6] = {0};
Soft_I2C_Master_Transmit(&AHT20_I2C, AHT20_ADDRESS, sendBuffer, 3);
HAL_Delay(75);
Soft_I2C_Master_Receive(&AHT20_I2C, AHT20_ADDRESS, readBuffer, 6);
if ((readBuffer[0] & 0x80) == 0x00)
{
uint32_t data = 0;
data = ((uint32_t)readBuffer[3] >> 4) + ((uint32_t)readBuffer[2] << 4) + ((uint32_t)readBuffer[1] << 12);
*Humidity = data * 100.0f / (1 << 20);
data = (((uint32_t)readBuffer[3] & 0x0F) << 16) + ((uint32_t)readBuffer[4] << 8) + (uint32_t)readBuffer[5];
*Temperature = data * 200.0f / (1 << 20) - 50;
}
}
参考资料
https://blog.csdn.net/zhangduang_KHKW/article/details/121953275
https://baike.baidu.com/item/I2C%E6%80%BB%E7%BA%BF/918424