超详细讲解:STM32软件模拟硬件I2C,模拟标准库的函数(标准库)

前言:STM32F103系列的硬件I2C还是有点问题的,一旦外设资源占用多的话,硬件I2C通讯还是有时会出点小毛病,比如数据不准确,或者偶尔I2C初始化失败,所以对于I2C接口设备,在项目占用外设资源不是很大的话,一般都会用I/O口来模拟I2C通讯(软件I2C)

需要I2C协议手册中文版链接:https://share.weiyun.com/2OLJiuLi

看不懂下面源码每一步都会有注释

I2C传输一次数据DATA(即8位一个字节)的过程:

I2C每次传输一个字节即8bit,首先发送起始信号,再发送从机地址(7位)和读写信号(1/0),. 从机发送应答信号,主设备发送要传输的数据字节(8位),从机发送应答信号,.主设备发送停止条件。(SCL时钟线高电平时表示此时数据有效,低电平表示无效,SDA数据线需要外界上拉电组,即默认时高电平表示空闲,这里我们使用软件I/O口来模拟I2C通讯,我们都通过软件来写入电平信号来实现),以下i2c_Delay()延时都是延时几微秒

I2C.H:

头文件函数和宏定义:

#ifndef _BSP_I2C_GPIO_H
#define _BSP_I2C_GPIO_H


#include <inttypes.h>


#define EEPROM_I2C_WR	0		/* 写控制bit */
#define EEPROM_I2C_RD	1		/* 读控制bit */


/* 定义I2C总线连接的GPIO端口, 用户只需要修改下面4行代码即可任意改变SCL和SDA的引脚 */
#define EEPROM_GPIO_PORT_I2C	GPIOB			/* GPIO端口 */
#define EEPROM_RCC_I2C_PORT 	RCC_APB2Periph_GPIOB		/* GPIO端口时钟 */
#define EEPROM_I2C_SCL_PIN		GPIO_Pin_6			/* 连接到SCL时钟线的GPIO */
#define EEPROM_I2C_SDA_PIN		GPIO_Pin_7			/* 连接到SDA数据线的GPIO */


#define EEPROM_I2C_SCL_1()  GPIO_SetBits(EEPROM_GPIO_PORT_I2C, EEPROM_I2C_SCL_PIN)		/* SCL = 1 */
#define EEPROM_I2C_SCL_0()  GPIO_ResetBits(EEPROM_GPIO_PORT_I2C, EEPROM_I2C_SCL_PIN)		/* SCL = 0 */
	
#define EEPROM_I2C_SDA_1()  GPIO_SetBits(EEPROM_GPIO_PORT_I2C, EEPROM_I2C_SDA_PIN)		/* SDA = 1 */
#define EEPROM_I2C_SDA_0()  GPIO_ResetBits(EEPROM_GPIO_PORT_I2C, EEPROM_I2C_SDA_PIN)		/* SDA = 0 */
	
#define EEPROM_I2C_SDA_READ()  GPIO_ReadInputDataBit(EEPROM_GPIO_PORT_I2C, EEPROM_I2C_SDA_PIN)	

//函数声明
void i2c_Start(void);
void i2c_Stop(void);
void i2c_SendByte(uint8_t _ucByte);
uint8_t i2c_ReadByte(void);
uint8_t i2c_WaitAck(void);
void i2c_Ack(void);
void i2c_NAck(void);
uint8_t i2c_CheckDevice(uint8_t _Address);


#endif

软件模拟I2C函数:

void i2c_Start(void);
void i2c_Stop(void);
void i2c_SendByte(uint8_t _ucByte);
uint8_t i2c_ReadByte(void);
uint8_t i2c_WaitAck(void);
void i2c_Ack(void);
void i2c_NAck(void);
uint8_t i2c_CheckDevice(uint8_t _Address);

i2c_Delay延时函数

作用:因为STM32f103系列主频为72M,CPU执行的速度很快,而电平转换的事件没那么快,需要延迟几微秒,以便CPU能够检测到 I/O 口信号变化

/*
*********************************************************************************************************
*	函 数 名: i2c_Delay
*	功能说明: I2C总线位延迟,最快400KHz
*	形    参:无
*	返 回 值: 无
*********************************************************************************************************
*/
static void i2c_Delay(void)
{
	uint8_t i;

	/* 
	 	下面的时间是通过逻辑分析仪测试得到的。
    工作条件:CPU主频72MHz ,MDK编译环境,1级优化
  
		循环次数为10时,SCL频率 = 205KHz 
		循环次数为7时,SCL频率 = 347KHz, SCL高电平时间1.5us,SCL低电平时间2.87us 
	 	循环次数为5时,SCL频率 = 421KHz, SCL高电平时间1.25us,SCL低电平时间2.375us 
	*/
	for (i = 0; i < 10; i++);
}

GPIO模拟I2C的配置

注意:GPIO工作模式一定要设置为:开漏输出(Open-Drain)模式在开漏模式下,GPIO引脚只能主动将总线拉低到低电平,而释放时则依赖外部上拉电阻将总线拉回高电平。这样设计的好处是避免多个设备同时驱动总线导致电流冲突或损坏设备。如果使用推挽输出(Push-Pull)模式,当两个设备同时驱动总线到不同的电平时,会产生短路电流,可能损坏引脚或设备。

/*
*********************************************************************************************************
*	函 数 名: i2c_CfgGpio
*	功能说明: 配置I2C总线的GPIO,采用模拟IO的方式实现
*	形    参:无
*	返 回 值: 无

*********************************************************************************************************
*/
static void i2c_CfgGpio(void)  
{
	GPIO_InitTypeDef GPIO_InitStructure;

	RCC_APB2PeriphClockCmd(EEPROM_RCC_I2C_PORT, ENABLE);	/* 打开GPIO时钟 */

	GPIO_InitStructure.GPIO_Pin = EEPROM_I2C_SCL_PIN | EEPROM_I2C_SDA_PIN;  
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;  	/* 开漏输出, */
	GPIO_Init(EEPROM_GPIO_PORT_I2C, &GPIO_InitStructure);

	/* 给一个停止信号, 复位I2C总线上的所有设备到待机模式 */
	i2c_Stop();
}

I2C启动信号:


I2C_Start启动函数:void I2C_Start(void):

//起始信号
void I2C_Start(void)
{
    EEPROM_I2C_SDA_1();;//拉高SDA
	EEPROM_I2C_SCL_1();//拉高SLA
	i2c_Delay();  //因为STM32103主频很快为72M,执行速度很快,需要延时几微秒,从而让单片机能识别到
	EEPROM_I2C_SDA_0(); //拉低SDA
	i2c_Delay();  //延时
	EEPROM_I2C_SCL_0();//拉低SCL
	i2c_Delay();//延时
}

 i2c_Stop停止函数:

    
 


/*
*********************************************************************************************************
*	函 数 名: i2c_Stop
*	功能说明: CPU发起I2C总线停止信号
*	形    参:无
*	返 回 值: 无
*********************************************************************************************************
*/
void i2c_Stop(void)
{
	/* 当SCL高电平时,SDA出现一个上跳沿表示I2C总线停止信号 */
	EEPROM_I2C_SDA_0();  //拉低SDA
	EEPROM_I2C_SCL_1();  //拉高SCL
	i2c_Delay();  //延时
	EEPROM_I2C_SDA_1();  //拉高SDA释放数据总线
}

i2c_SendByte发送字节函数

思路:首先,每次只能发送一位/bit数据(先发送最高位,第8位),所以我们需要循环8次,然后,判断发送的一字节的数据     |(与上)  0x80(10000000)是否为1,即取最高位,若数据最高位为1,则此时将SDA数据线拉高,表示发送高电平信号1,否则拉低数据线SDA,发送低电平信号0,然后延时几微秒/us,让CPU能够识别到信号,然后拉高SCL时钟线表示此时数据有效,延时,此时要传入的数据左移一位,开始读第七位数据,延时几微秒,如此循环8次

/*
*********************************************************************************************************
*	函 数 名: i2c_SendByte
*	功能说明: CPU向I2C总线设备发送8bit数据
*	形    参:_ucByte : 等待发送的字节
*	返 回 值: 无
*********************************************************************************************************
*/
void i2c_SendByte(uint8_t _ucByte)
{
	uint8_t i;

	/* 先发送字节的高位bit7 */
	for (i = 0; i < 8; i++)  //每次只能发送一位/bit数据,循环8次从而发送一个字节
	{		
		if (_ucByte & 0x80)  //判断发送的一字节的数据是否为1
		{
			EEPROM_I2C_SDA_1();//数据最高位为1,则此时将SDA数据线拉高,表示发送高电平信号1
		}
		else
		{
			EEPROM_I2C_SDA_0();//否则拉低数据线SDA,发送低电平信号0
		}
		i2c_Delay();  //延时
		EEPROM_I2C_SCL_1();  //SCL拉高数据有效
		i2c_Delay();	//延时
		_ucByte <<= 1;	/* 左移一个bit */
		i2c_Delay();    //延时
	}
}

i2c_ReadByte读字节函数

思路:也是循环8次,每次只能读一位/bit数据,从读取一个字节,首先要声明个变量value初始化为0用来表示数据(8位二进制形式),及一个字节,先将value左移一位,让出最低位用来存储后面读到的数据,然后拉高SCL时钟线,表示后面的SDA数据线数据有效,判断此时SDA是否为真及高电平1,若为则value++,然后SCL时钟线拉低表示一bit读取完毕,延时,准备下一次循环读取,如此反复

/*
*********************************************************************************************************
*	函 数 名: i2c_ReadByte
*	功能说明: CPU从I2C总线设备读取8bit数据
*	形    参:无
*	返 回 值: 读到的数据
*********************************************************************************************************
*/
uint8_t i2c_ReadByte(void)
{
	uint8_t i;
	uint8_t value;

	/* 读到第1个bit为数据的bit7 */
	value = 0;
	for (i = 0; i < 8; i++)
	{
		value <<= 1;//先左一位,空出value最低位,读取传输的数据的最高位
		EEPROM_I2C_SCL_1();  //拉高SCL表示此时数据有效
		i2c_Delay();  
		if (EEPROM_I2C_SDA_READ())  //判断SDA总线是否是高电平,若高电平则+1
		{
			value++;
		}
		EEPROM_I2C_SCL_0();  //SCL拉点表示一位/bit数据(一字节)读取完毕
		i2c_Delay();
	}
	return value;
}

i2c_WaitAck等待从机应答函数

思路:就像是检测等待EV事件是否响应,发送完从机地址(7位)+读写信号(1/0)和一个字节数据data都要检测从机适应信号,此时从机会控制SDA数据线的高低电平来发送应答信号,1为无器件响应,0表示正确应答。首先我们声明个变量用来存储后面SDA数据线的信号,我们需要将数据线SDA拉高,来释放总线,表示总线空闲,以便从机控制数据线SDA,然后延时,再拉高时钟线SCL表示此时数据有效,判断此时SDA数据线是否为0并赋值给之前声明的变量,若为0则表示从机正确应答并赋值给之前声明的变量,此时用声明变量否则表示无器件应答。后面拉低时钟线SCL表示等待应答结束,延时,结束等待应答。

/*
*********************************************************************************************************
*	函 数 名: i2c_WaitAck,主机CPU等待从机应答信号
*	功能说明: CPU产生一个时钟,并读取器件的ACK应答信号
*	形    参:无
*	返 回 值: 返回0表示正确应答,1表示无器件响应
*********************************************************************************************************
*/
uint8_t i2c_WaitAck(void)
{
	uint8_t re;

	EEPROM_I2C_SDA_1();	/* CPU释放SDA总线,总线空闲 */
	i2c_Delay();
	EEPROM_I2C_SCL_1();	/* CPU驱动SCL = 1,用于等待有效位 此时器件会返回ACK应答 */
	i2c_Delay();
	if (EEPROM_I2C_SDA_READ())	/* CPU读取SDA口线状态 */
	{
		re = 1;  //1表示无器件响应
	}
	else
	{
		re = 0;  //0表示正确应答
	}
	EEPROM_I2C_SCL_0();  //SCL拉低表示等待应答结束

	i2c_Delay();
	return re;
}

CPU应答信号过程

思路:条件判断:ACK:SCL=1   SDA=0    NACK: SCL  SDA=1

i2c_Ack   CPU产生一个应答信号函数

/*
*********************************************************************************************************
*	函 数 名: i2c_Ack,CPU产生一个应答信号
*	功能说明: CPU产生一个ACK信号,用于从机向CPU主机通信产生应答信号
*	形    参:无
*	返 回 值: 无
*********************************************************************************************************
*/
void i2c_Ack(void)
{
	EEPROM_I2C_SDA_0();	/* CPU驱动SDA = 0 */
	i2c_Delay();
	EEPROM_I2C_SCL_1();	/* CPU产生1个时钟 */
	i2c_Delay();  //cpu结束应答
	EEPROM_I2C_SCL_0(); /*SCL低电平无效,释放总线*/
	i2c_Delay();
	EEPROM_I2C_SDA_1();	/* CPU释放SDA总线 */
}

.. i2c_NAckCPU结束产生一个非应答信号

/*
*********************************************************************************************************
*	函 数 名: i2c_NAck,CPU产生一个不应答信号
*	功能说明: CPU产生1个NACK信号,用于从机向CPU主机通信产生不应答信号
*	形    参:无
*	返 回 值: 无
*********************************************************************************************************
*/
void i2c_NAck(void)
{
	EEPROM_I2C_SDA_1();	/* CPU驱动SDA = 1 */
	i2c_Delay();
	EEPROM_I2C_SCL_1();	/* CPU产生1个时钟 */
	i2c_Delay();  //延时,CPU结束产生一个非应答信号
	EEPROM_I2C_SCL_0();  /*SCL低电平无效,释放总线*/
	i2c_Delay();	
}

i2c_CheckDevice   检测I2C总线设备,CPU向发送设备地址,然后读取设备应答来判断该设备是否存在

/*
*********************************************************************************************************
*	函 数 名: i2c_CheckDevice
*	功能说明: 检测I2C总线设备,CPU向发送设备地址,然后读取设备应答来判断该设备是否存在
*	形    参:_Address:设备的I2C总线地址
*	返 回 值: 返回值 0 表示正确, 返回1表示未探测到
*********************************************************************************************************
*/
uint8_t i2c_CheckDevice(uint8_t _Address)
{
	uint8_t ucAck;

	i2c_CfgGpio();		/* 配置GPIO */

	
	i2c_Start();		/* 发送启动信号 */

	/* 发送设备地址+读写控制bit(0 = w, 1 = r) bit7 先传 */
	i2c_SendByte(_Address | EEPROM_I2C_WR);
	ucAck = i2c_WaitAck();	/* 检测设备的ACK应答 */

	i2c_Stop();			/* 发送停止信号 */

	return ucAck;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值