STM32:深度解析 I2C 总线:从基础到代码实现

知识点1【I2C总线的概述】

I2C:集成电路之间(Inter-Integrated Circuit),是一种硬件接口——总线

是一种由Philips公司,开发的双向二线制同步串行总线协议,广泛用于低速设备间的短距离通信。

可以实现一对多的通信。

核心概念:允许多个 ”从设备“ 通过仅仅两条共享的总线信号线SDA,SCL)实现 ”一对多“或”多对多“的通信。

特点:

1、有两根通信线:SCL,SDA

2、串行

3、同步

4、半双工

5、带数据应答

6、支持挂在多设备一主多从,多主多从)

我们下面介绍的都是一主多从模式下的

知识点2【硬件要求】

  • 所有设备的SCL连在一起,SDA连在一起
  • 设备的SCL和SDA均要配置为开漏输出模式
    • 配置成开漏输出的原因:

      如果不用开漏输出,SCL很好处理:主机的SCL配置为推挽输出,从机均配置为浮空输入

      但是SDA却很难处理:主机和从机共享一条SDA线(半双工),需要在发送和接收之间来回切换,配置起来很麻烦,因此这里不太建议使用推挽输出。

    • 开漏输出就能够很好的地解决这个问题:外部有一个上拉电阻,弱上拉,当输出的时候,进行输入输出即可;输入的时候即观察电平的变化即可;当不操作的时候,默认处于一个若上拉不影响电路的状态(默认状态处于一个高电平)。并且此模式下有一个”线与“的特性:只要有一个设备处于低电平,总线就处于低电平——主要利用的也就是这个特性。

知识点3【I2C时序分析】

对于SCL线,控制权永远在主机

对于SDA,从机不允许主动发送获取SDA的控制权,只有在主动发出读取从机数据,或者从机应答的时候,才能够短暂地获取到SDA的控制权

1、起始条件 和 终止条件

与其他的区别,都是在SCL高电平期间,SDA发生电平的跳变的

条件标志
起始条件SCL高电平期间,SDA高→低
终止条件SCL高电平期间,SDA低变高

2、发送一个字节

SCL低电平期间,主机将数据位依次放在SDA线上(高位先行),然后释放SCL,从机将在SCL高电平期间读取数据SCL高电平期间,SDA不允许有电平变化(与起始和终止位区分开),循环8次,即可发送一个字节

在这个阶段中,SDA和SCL全部 由 主机掌握

3、接受一个字节

SCL低电平期间,从机将数据位依次放在SDA线上(高位先行),然后释放SCL,主机将在SCL高点平期间读取数据,SCL高电平期间,SDA不允许变化,循环8次,即可接收一个字节

主机在接收数据前,需要释放SDA总线

(虚线是从机控制,实线是主机控制)

4、接收应答

主机在发送完一个字节后,在下一个时钟接收一位数据,判断从机是否应答:0表示应答,1(默认电平)表示没有应答。

5、发送应答

主机接收完一个字节后,在下一个时钟发送一位数据:0表示应答,1表示未应答。

注意:

应答中的发送和接收,都是相对于主机而言的。即 发送应答:主机发送应答接收应答:主机接收应答

因此这两张图的区分就是看,在高电平期间,谁掌握了SDA线的控制权

同步的好处:不过于依赖于外部电路,允许在发送过程中进入中断,可用软件模拟时序

异步的好处:节省一根时钟线,节省资源,对时间要求严格,对硬件依赖程度高

知识点5【时序模块的拼接】

0、概念补充

(1)一主多从模式中,主机是如何识别不同的从机呢

每个从机都有一个专属地址,主机通过从机的地址去确定要与那个从机进行通信。因此我们发送数据前需要一个识别的过程。

(2)模块不同,地址不同。那如果同一模块我们要连接多个,该如何识别呢?

地址位的某一位或者几位是可以变化的,以此达到此目的。

如MPU6050的ADD位,就是用来达到此目的的。

1、指定地址写

在指定设备(Slave Address)的指定地址(Reg Address),写入指定数据(Data)

这张图大家自行分析,一定要分析,按照我们上面所讲的 时序介绍。

依次是:

起始标志→主机发送从机地址→ 接收应答 → 主机发送指定地址 → 接收应答 → 发送数据 → 终止标志

2、指定地址读

在指定设备(Slave Address)的指定地址(Reg Address),读取指定数据(Data)

依次是:

起始标志→主机发送从机地址→ 接收应答 → 主机发送指定地址 → 接收应答

这一部分我们称为识别部分

Sr:是一个起始标志,重新开始即正式开始接收

主机释放SDA,主机发送数据 → 发送应答 → 接收数据 → 终止标志

知识点6【I2C的配置流程】

我们使用标准库进行配置,流程简单但是内部设计到的状态,需要准确记忆(bilibili:江协科技中有严格按照时序图配置的流程,大家感兴趣的可以去看看在10.3节)

我利用的I2C1,SCL对应PB6,SDA对应PB7

1、状态信息补充

(1)Flag

I2C_FLAG_BUSY :SDA总线正在被其他设备占用时,这位会被置位

I2C_FLAG_AF

(2)Event

I2C_EVENT_MASTER_MODE_SELECT :主设备请求控制总线,表示起始条件发送成功

        关键事件:SCL高电平期间,SDA有高变低

I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED :指定目标从设备,表示从机地址已发送并收到从机的应答,主设备可以发送数据

I2C_EVENT_MASTER_BYTE_TRANSMITTED :表示1byte数据以完整发送并接收到从机的应答。

        可用于 指定地址 和 数据 发送完成的标志

2、代码演示

#include "i2c.h"

int i2c_timeout;

void I2C_Config(void)
{
	GPIO_InitTypeDef GPIO_InitTSources={0};
	I2C_InitTypeDef  I2C_InitSources;
	//打开时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1,ENABLE);
	
	GPIO_InitTSources.GPIO_Pin=GPIO_Pin_6|GPIO_Pin_7;
	GPIO_InitTSources.GPIO_Mode=GPIO_Mode_AF_OD;
	GPIO_InitTSources.GPIO_Speed=GPIO_Speed_10MHz;
	//PB6  PB7
	GPIO_Init(GPIOB,&GPIO_InitTSources);
	//i2c配置
	
	I2C_InitSources.I2C_ClockSpeed=100000;//100KHZ
	I2C_InitSources.I2C_Mode=I2C_Mode_I2C;//IIC模式
	I2C_InitSources.I2C_OwnAddress1=0x00;
	I2C_InitSources.I2C_Ack=I2C_Ack_Enable;//使能应答
	I2C_InitSources.I2C_AcknowledgedAddress=I2C_AcknowledgedAddress_7bit;//7bit从机地址
	I2C_InitSources.I2C_DutyCycle=I2C_DutyCycle_16_9;
	
	I2C_Init(I2C1,&I2C_InitSources);
	I2C_Cmd(I2C1,ENABLE);
}

u8 IIC_Send_Data(uint8_t sla_addr,uint8_t reg_addr,uint8_t data)
{
	//防止抢占冲突
	i2c_timeout = I2C_MAX_TIMEOUT_MS;
	while(I2C_GetFlagStatus(I2C1,I2C_FLAG_BUSY) && i2c_timeout--)
	{
		//延时1ms
		Delay_ms(1);
		if(i2c_timeout == 0)
		{
			//返回超时信息
			return I2C_TRANS_TIMEOUT;
		}
	}
	
	//I2C起始 加 防超时操作
	i2c_timeout = I2C_MAX_TIMEOUT_MS;
	I2C_GenerateSTART(I2C1,ENABLE);
	while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_MODE_SELECT) && i2c_timeout--)
	{
		Delay_ms(1);
		if(!i2c_timeout)
		{
			I2C_GenerateSTOP(I2C1,ENABLE);
			return I2C_TRANS_TIMEOUT;
		}
	}
	
	
	//发送从设备地址,判断是否被选中
	i2c_timeout = I2C_MAX_TIMEOUT_MS;
	I2C_Send7bitAddress(I2C1,sla_addr,I2C_Direction_Transmitter);
	while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED) && i2c_timeout--)
	{
		Delay_ms(1);
		if(!i2c_timeout)
		{
			I2C_GenerateSTOP(I2C1,ENABLE);
			return I2C_TRANS_TIMEOUT;
		}
	}
	
	//判断接收应答 是否正确
	if(I2C_GetFlagStatus(I2C1,I2C_FLAG_AF))
	{
		//清楚标志位
		I2C_ClearFlag(I2C1, I2C_FLAG_AF);
		//关闭I2C
		I2C_GenerateSTOP(I2C1,ENABLE);
		//返回值
		return I2C_TRANS_OTHERERR;
	}
	
	//发送从设备的寄存器地址
	i2c_timeout = I2C_MAX_TIMEOUT_MS;
	I2C_SendData(I2C1,reg_addr);
	while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_BYTE_TRANSMITTED) && i2c_timeout--)
	{
		Delay_ms(1);
		if(!i2c_timeout)
		{
			I2C_GenerateSTOP(I2C1,ENABLE);
			return I2C_TRANS_TIMEOUT;
		}
	}
	
	//发送数据
	i2c_timeout = I2C_MAX_TIMEOUT_MS;
	I2C_SendData(I2C1,data);
	while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_BYTE_TRANSMITTED) && i2c_timeout--)
	{
		Delay_ms(1);
		if(!i2c_timeout)
		{
			I2C_GenerateSTOP(I2C1,ENABLE);
			return I2C_TRANS_TIMEOUT;
		}
	}
	
	//停止
	I2C_GenerateSTOP(I2C1,ENABLE);
	return I2C_TRANS_SUCCESS;
}
#ifndef _I2C_H_
#define _I2C_H_

#include "stm32f10x.h"
#include "delay.h"

#define I2C_MAX_TIMEOUT_MS 1000
#define I2C_TRANS_OTHERERR 2
#define I2C_TRANS_TIMEOUT 1
#define I2C_TRANS_SUCCESS 0

u8 IIC_Send_Data(uint8_t sla_addr,uint8_t reg_addr,uint8_t data);
void I2C_Config(void);

#endif

结束

代码重在练习!

代码重在练习!

代码重在练习!

今天的分享就到此结束了,希望对你有所帮助,如果你喜欢我的分享,请点赞收藏加关注,谢谢大家!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值