软件模拟I2C

本文详细介绍了I2C通信协议的工作原理,包括起始和停止条件、数据有效性、应答信号等,并提供了基于STM32的GPIO模拟I2C通信的C代码实现,包括GPIO配置、字节读写、应答信号处理等功能。通过这些代码,读者可以了解如何在嵌入式系统中实现I2C通信。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

I2C(Inter-Integrated Circuit)协议是一种用于同步、半双工、串行总线(由SCL时钟线、SDA线组成)上的协议。规定了总线空闲状态、起始条件、停止条件、数据有效性、字节格式、响应Ack信号、从设备地址选择、数据方向。有主从机之分,主机master就是产生时钟的一方,并且起始信号和停止信号由主机发送。

协议说明(之前有博客介绍I2C协议)

总线空闲时钟:SCL、SDA均为高电平,此时主从设备都不控制总线(主从设备的SDA和SCL引脚为输入或者开漏输出),由外部上拉电阻将总线拉高。(I2C的开漏输出是总线的关键)

起始条件:SCL在高电平的时候,SDA出现下降沿,如下图:
在这里插入图片描述

停止信号:SCL在高电平的时候,SDA出现上升沿,如下图:
在这里插入图片描述
 数据有效性:什么时候读取数据(SDA)有效,在SCL为高电平的时候,读数据有效,什么时候写数据(改变SDA),在SCL为低电平时,写数据有效。

在这里插入图片描述

字节格式:发送到SDA线上的每个字节必须为8位,首先传输的是字节的高八位(MSB) .

响应(ACK):为确保发送数据被对方可靠的接收,接收方必须发出ACK信号表示已成功接收数据。这个ACK信号就是接收方在接收完一个字节数据后,SCL的第一个下降沿处拉低SDA。发送方如何接受这个ACK信号?在发送完一个字节数据之后,SCL的第一个高电平处读取SDA线(SDA引脚处于输入状态不会使SDA拉低),如果读出为低,则表示对方成功接收到数据,在发送方读完ACK后,接收方需要释放SDA,即在SCL的第二个下降沿处将SDA设置为开漏输出,释放SDA总线。

从设备地址选择和数据方向:I2C是根据设备地址进行寻址的,在发送完一个起始信号之后,紧接着发送一个字节,有7位和10位地址之分,一般用7位地址,最后一位表示数据传输方向,组成一个字节,0表示写方向,1表示读方向。

模拟IO设置

#ifndef __BSP_I2C_GPIO_H
#define __BSP_I2C_GPIO_H

#include "stm32f10x.h"

#define EEPROM_I2C_SDA_PORT		GPIOB
#define EEPROM_I2C_SDA_PIN		GPIO_Pin_7
#define EEPROM_I2C_SDA_CLK		RCC_APB2Periph_GPIOB
#define EEPROM_I2C_SCL_PORT		GPIOB
#define EEPROM_I2C_SCL_PIN		GPIO_Pin_6
#define EEPROM_I2C_SCL_CLK		RCC_APB2Periph_GPIOB

#define EEPROM_I2Cx			I2C1
#define EEPROM_I2Cx_CLK		RCC_APB1Periph_I2C1
#define EEPROM_I2C_WR				0xFE
#define EEPROM_I2C_RD				0x01
#define STM32_OWN_ADDRESS		0x0A
#define EEPROM_ADDRESS			0xA0

#define I2C_SDA_HIGH()		GPIO_SetBits(EEPROM_I2C_SDA_PORT,EEPROM_I2C_SDA_PIN);
#define I2C_SDA_LOW()		  GPIO_ResetBits(EEPROM_I2C_SDA_PORT,EEPROM_I2C_SDA_PIN);
#define I2C_SCL_HIGH()		GPIO_SetBits(EEPROM_I2C_SCL_PORT,EEPROM_I2C_SCL_PIN);
#define I2C_SCL_LOW()		  GPIO_ResetBits(EEPROM_I2C_SCL_PORT,EEPROM_I2C_SCL_PIN);
#define I2C_SDA_READ()		GPIO_ReadInputDataBit(EEPROM_I2C_SDA_PORT,EEPROM_I2C_SDA_PIN)

void I2C_Ack(void);
uint8_t I2C_CheckDevice(void);
void I2C_GPIO_Config(void);
void I2C_NAck(void);
void I2C_NAck(void);
void I2C_SendByte(uint8_t data);
uint8_t I2C_ReadByte(void);
void I2C_Start(void);
void I2C_Stop(void);
uint8_t I2C_WaitAck(void);

#endif

GPIO配置开漏输出

void I2C_GPIO_Config(void)
{
	GPIO_InitTypeDef GPIO_InitStruct;
	RCC_APB2PeriphClockCmd(EEPROM_I2C_SDA_CLK|EEPROM_I2C_SCL_CLK,ENABLE);
	
	GPIO_InitStruct.GPIO_Mode	=	GPIO_Mode_Out_OD;
	GPIO_InitStruct.GPIO_Pin	=	EEPROM_I2C_SDA_PIN;
	GPIO_InitStruct.GPIO_Speed	=	GPIO_Speed_50MHz;
	GPIO_Init(EEPROM_I2C_SDA_PORT,&GPIO_InitStruct);
	
	GPIO_InitStruct.GPIO_Mode	=	GPIO_Mode_Out_OD;
	GPIO_InitStruct.GPIO_Pin	=	EEPROM_I2C_SCL_PIN;
	GPIO_InitStruct.GPIO_Speed	=	GPIO_Speed_50MHz;
	GPIO_Init(EEPROM_I2C_SCL_PORT,&GPIO_InitStruct);

	I2C_SCL_HIGH();
    I2C_SDA_HIGH();    //总线空闲
}

延时函数(随便,我这是软件延时,不准确,只要不超出400kbit/s即可,I2C最大速率3.4Mbit/s,一般都只支持快速模式) 

static void I2C_Delay(void)
{
	uint16_t i;
	for(i=0;i<10;i++);

}

 起始信号

void I2C_Start(void)
{
	I2C_SDA_HIGH();	
	I2C_SCL_HIGH();
	I2C_Delay();
	I2C_SDA_LOW();
	I2C_Delay();
	I2C_SCL_LOW();		//一般低电平结束表示一个时钟周期
	I2C_Delay();
}

停止信号

void I2C_Stop(void)
{
	I2C_SDA_LOW();
	I2C_SCL_HIGH();
	I2C_Delay();
	I2C_SDA_HIGH();
	
}

应答信号

void I2C_Ack(void)
{
	I2C_SDA_LOW();
	I2C_Delay();
	I2C_SCL_HIGH();
	I2C_Delay();
	I2C_SCL_LOW();
	I2C_Delay();
	I2C_SDA_HIGH();	 //前面提到的,应答之后需要释放SDA总线

}

非应答信号(这就不需要释放总线)

void I2C_NAck(void)
{
	I2C_SDA_HIGH();
	I2C_Delay();
	I2C_SCL_HIGH();
	I2C_Delay();
	I2C_SCL_LOW();
	I2C_Delay();
}

等待应答信号

uint8_t I2C_WaitAck(void)
{
	uint8_t r;
	I2C_SDA_HIGH();		//等待应答,主机需要释放SDA总线,由从机产生应答
	I2C_Delay();		
	I2C_SCL_HIGH();
	I2C_Delay();
	if(I2C_SDA_READ())
	{
		r=1;
	}
	else
	{
		r=0;
	}
	I2C_SCL_LOW();
	I2C_Delay();
	return r;
}

读取字节函数

uint8_t I2C_ReadByte(void)
{
	uint8_t i=0;
	uint8_t value=0;
	for(i=0;i<8;i++)
	{
		value<<=1;
		I2C_SCL_HIGH();
		I2C_Delay();
		if(I2C_SDA_READ())
		{
			value++;
		}
		I2C_SCL_LOW();
		I2C_Delay();
	}
	return value;
}

写入字节函数

void I2C_SendByte(uint8_t data)
{
	uint8_t i = 0;
	for(i=0;i<8;i++)
	{
		if(data&0x80)
		{
			I2C_SDA_HIGH();
		}
		else
		{
			I2C_SDA_LOW();
		}
		I2C_Delay();
		I2C_SCL_HIGH();
		I2C_Delay();
		I2C_SCL_LOW();
		if(i==7)
		{
			I2C_SDA_HIGH();		//数据发送完,释放SDA总线
			
		}
		data<<=1;
		I2C_Delay();
	}

}

写到这里,可以先进行测试,直接发送设备地址,看EEPROM是否有应答产生,能否进行后续更为复杂的读写测试

uint8_t I2C_CheckDevice(void)
{
	uint8_t Ack ;
	
	I2C_GPIO_Config();
	I2C_Start();
	I2C_SendByte(EEPROM_ADDRESS&EEPROM_I2C_WR);
	Ack	=	I2C_WaitAck();
	I2C_Stop();
	return Ack;
	
}

读取任意字节函数

uint8_t I2C_BufferRead(uint8_t *pBuffer,uint8_t addr,uint16_t numToRead)
{
	uint16_t i;
	I2C_Start();                                   //起始信号
	I2C_SendByte(EEPROM_ADDRESS&EEPROM_I2C_WR);   //发送设备地址
	if(I2C_WaitAck())                            //等待应答
	{
		I2C_Stop();
		return 0;
	}
	I2C_SendByte(addr);                            //发送要读取的内存地址
	if(I2C_WaitAck())                            //等待应答
	{
		I2C_Stop();
		return 0;
	}
	I2C_Start();                        //起始信号
	I2C_SendByte(EEPROM_ADDRESS|EEPROM_I2C_RD);        //设备地址
	if(I2C_WaitAck())
	{
		I2C_Stop();
		return 0;
	}
	for(i=0;i<numToRead;i++)            //读取数据
	{
		pBuffer[i] = I2C_ReadByte();
		
		if(i!=(numToRead-1))        //最后一个字节要发送非应答信号
		{
			I2C_Ack();
		}
		else
		{
			I2C_NAck();
		}
	}
	I2C_Stop();
	return 1;

}

页写入函数

uint8_t I2C_PageWrite(uint8_t *pBuffer,uint8_t addr,uint16_t numToWrite)
{
	uint16_t i;
	//I2C_Stop();
	I2C_Start();
	I2C_SendByte(EEPROM_ADDRESS&EEPROM_I2C_WR);
	if(I2C_WaitAck())
	{
		I2C_Stop();
		return 0;
	}
	I2C_SendByte(addr);
	if(I2C_WaitAck())
	{
	  I2C_Stop();
		return 0;
	}
   while(numToWrite--)
	 {
		I2C_SendByte(*pBuffer);
		if(I2C_WaitAck())
		{
			I2C_Stop();
			return 0;
		}
		pBuffer++;
	 }
	I2C_Stop();
	
	return 1;
}

 任意字节读取函数

uint8_t I2C_BufferWrite(uint8_t *pBuffer,uint8_t addr,uint16_t numToWrite)
{
	uint16_t count,numPage,numSingle,usAddr;
	usAddr = addr%8;
	count = 8-usAddr;
	numPage	= numToWrite/8;
	numSingle=numToWrite%8;
	if(usAddr==0)
	{
		if(numPage==0)
		{
			I2C_PageWrite(pBuffer,addr,numToWrite);
			Delay(0xFFFF);
		}
		else
		{
			while(numPage--)
			{
				I2C_PageWrite(pBuffer,addr,8);
				Delay(0xFFFF);
				pBuffer+=8;
				addr+=8;
			}
			if(numSingle!=0)
			{
			I2C_PageWrite(pBuffer,addr,numSingle);
			Delay(0xFFFF);
			}
		}
	
	}
	else
	{
		if(numPage==0)
		{
			if(numSingle>count)
			{
			I2C_PageWrite(pBuffer,addr,count);
			Delay(0xFFFF);
			pBuffer+=count;
			addr+=count;
			I2C_PageWrite(pBuffer,addr,(numSingle-count));
			Delay(0xFFFF);
			}
			else
			{
				I2C_PageWrite(pBuffer,addr,numSingle);
				Delay(0xFFFF);
			}
		}
		else
		{
			numToWrite -= count;
			numPage=numToWrite/8;
			numSingle=numToWrite%8;
			I2C_PageWrite(pBuffer,addr,count);
			Delay(0xFFFF);
		  pBuffer+=count;
			addr+=count;
			while(numPage--)
			{
				I2C_PageWrite(pBuffer,addr,8);
				Delay(0xFFFF);
				pBuffer+=8;
				addr+=8;
			}
			if(numSingle!=0)
			{
					I2C_PageWrite(pBuffer,addr,numSingle);
					Delay(0xFFFF);
			}
		}
	
	}
	return 1;
}

参考野火的STM32例程,这里把页写入和任意字节写入归为一个函数

uint8_t I2C_Write(uint8_t *pBuffer,uint8_t addr,uint16_t numToWrite)
{
	uint16_t i,m;        
	uint16_t usAddr;
	usAddr = addr;
	for(i=0;i<numToWrite;i++)
	{
		if((i==0)||((usAddr%8)==0))        //每当是第一个字节或者新的一页,要重新发起始信号
		{
			I2C_Stop();            //停止信号,内部时序
			for(m=0;m<1000;m++)    //用于超时处理
			{
				I2C_Start();        //起始信号
				I2C_SendByte(EEPROM_ADDRESS&EEPROM_I2C_WR);    //设备地址
				if(I2C_WaitAck() == 0)        //应答则跳出循环
				{
					break;
				}
				
			}    
			if(m==1000)        //无应答,跳出函数
			{
				I2C_Stop();
				return 0;
			}
			I2C_SendByte(usAddr);        //要写入的内存地址
			if(I2C_WaitAck()!=0)
			{
				I2C_Stop();
				return 0;
			}
		}
		I2C_SendByte(pBuffer[i]);        //写入数据
		  if(I2C_WaitAck()!=0)
			{
				I2C_Stop();
				return 0;
			}
			usAddr++;                    //地址递增
	}
	I2C_Stop();
	return 1;
}

效果如下:

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值