【嵌入式】STM32和I2C通信

一、简介

I2C(Inter IC Bus)是有飞利浦公司开发的一种通用数据总线,主要通过两个通信线SCL和SDA进行通信,其中SCL(Serial Clock)是时钟线,用于收发双方同步数据,SDA(Serial Data)是数据线,用于传输数据。是一种同步半双工的数据总线,其有数据应答功能,支持在总线上挂载多个设备。 不少的设备,比如说常用的0.42寸的OLED显示器,MPU6050加速度传感器,AT24C02存储器模块,DS3231实时时钟模块等,都是用I2C协议

二、硬件电路分析

I2C典型电路如下:

  • 所有I2C设备的SCL连在一起,SDA连在一起
  • 设备的SCL和SDA都需要设置为开漏输出模式
  • SCL和SDA各配置一个上拉电阻,阻值一般为4.7k欧姆
    在这里插入图片描述

在I2C中,支持总线挂载多设备,主要有一主多从,多主多从两个模式。在主从控制中,CPU作为总线的主机的权利很大,一般掌握着SCL和SDA的控制,而从机只有在申请发送数据或做出应答的时候,才能从主机中获取到SDA的控制权,而SCL的控制权任何时刻都是在主机手里的。

由于主机拥有SCL的绝对控制权,主机的SC应该L配置成推挽输出,而从机的SCL设置为浮空输入或者上拉输入,时钟信号由主机发送,所有从机负责接收并且对齐时钟信号。 对于SDA,主机和从机都可能会在输入和输出之间切换,而如果时钟同步没做好,可能会出现两个设备同时对SDA进行操作的情况,如果其中一个输出高电平,另外一个输出低电平,则会导致线路短路,另外,协调电路中各个设备,使得只有一个设备在发送信息,是一件很麻烦的事情。为了避免这种情况,I2C禁止所有设备输出强上拉的高电平,采用外置的弱上拉电阻加开漏输出的电路结构,这也是上述典型电路中的1、2点。

对于开漏输出,实际上的引脚的内部结构是只有下拉接负极的开关管,而没有接正极的上拉开关管,因此引脚只能输出0电平或者浮空,而由于外置了上拉电阻,则在浮空的时候,SDA会被外置电阻弱上拉为高电平,从而避免了同时有强下拉和强上拉的情况。这样子就杜绝了电源短路的情况。

并且这个设计开漏加弱上拉的模式,同时兼具了输入和输出的功能:如果需要输出,则直接使用开漏输出进行操作,而想要输入的时候,则不做操作,直接观察电平高低。另外,这个设计还有一个“线与”的性质,也就是只要有任意电路输出低电平,SDA总线就会处于低电平,这可以让CPU执行多主机模式下的时钟同步和总线仲裁,这是多主多从模式的基础。同时,如果发送SDA总线处于低电平状态,则表明有人正在占用SDA

总之,这个设计有三个优点:

  1. 避免线路短路
  2. 兼顾输入和输出
  3. 可实现多主多从

三、I2C时序基本单元

3.1 发送和接收数据

I2C规定,I2C的起始条件和终止条件如下:
在这里插入图片描述
起始和终止都是由主机控制,空闲的时候所有从设备都需要保持端口浮空。

发送一个字节:
SCL低电平期间,主机将数据一次放在SDA总线上,然后释放SCL,从机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次重复八次,则发送一个字节。比如说首先主机拉低SCL,然后将SDA也拉低,表示发送一个0,然后保持SDA低电平,释放SCL,此时各个从设备负责读取SDA电平状态0,以此往复。如果主机进中断了,停止操作SCL和SDA,那么SCL和SDA则会保持当前状态,传输暂停,不会导致传输丢失的情况,这是同步时序的好处。
在这里插入图片描述

接收一个字节:
SCL低电平期间,从机将数据一次放在SDA线上,然后主机释放SCL,主机会在SCL高电平期间读取数据。所以SCL高电平期间SDA不允许有数据变化,依次重复八次,则主机接收一个字节

主机接收和发送的区别是,主机接受之前需要释放SDA,而发送之前需要拉低SDA

3.2 发送和接收应答

发送应答:主机接受完一个字节之后,在下一个时钟发送一位数据,数据0表示应答,1表示非应答
接收应答:主机在发送完一个字节之后,在下一个时钟接收一位数据,判断从机是否应答,数据0表示应答,数据1表示非应答。主机在接受前需要释放SDA

左边为发送应答,右边为接收应答

3.3 数据帧分析

有了上面六个切片,我们就可以将它们组成一个完整的数据帧了。

指定地址写
主机首先需要发送一个字节确定发送的对象,该字节是从机的地址。从机地址分为7位地址和10位地址,其中7位地址是厂商按芯片型号赋值的,而后三位则是可变地址,使得在挂载多个相同型号的芯片的时候可以做出区分。这个就是指定从机步骤。而一个字节有8个位,其中7位用于标识从机,另外一位是读写位,用于表示主机要进行读操作还是写操作。

主机发送指定从机字节后,目标从机会立马对主机进行应答,也就是主机发送完指定从机字节之后,主机会释放SDA,此时目标从机立马下拉SDA,表示应答,而主机在下一个SCL高电平的时候读SDA,发现自己释放SDA后,SDA仍是低电平,根据线与设计,证明电路中有设备对SDA进行了下拉操作,也就是有设备做出了应答。

一般来说,指定地址写在从机应答后,主机写的第一个字节是指定写入从机的哪一个寄存器,接着再下一个字节是写入的值

指定地址读
接下来,则根据读写位,进行主机的读写操作。如果主机发送主机读命令,那么在指定从机之后,会立马进入主机读状态,但是此时还不知道主机需要读的是从机哪一个寄存器中的值,这怎么办呢?在支持I2C的设备中,寄存器地址一般都是线性紧挨排布的。而其中会有一个地址指针(假设位于0x19),用于指向寄存器,每次读一次,指针就会自增一次。因此比如要读取0xAA的寄存器,那么首先对指定从机发送一个写请求,将0x19的内存单元写为0xA9,然后终止。再发起一次主机读,然后指定从机接收到请求后,0x19自增1,变成0xAA,然后读出0xAA寄存器中的值。这个操作被称为**“指定地址读”**

四、I2C的软件实现

以MPU6050为示例,尝试使用I2C软件实现来完成对MPU6050的操控。抽象是I2C_Soft.c文件,记录了软件I2C的实现方法

#include "stm32f10x.h"      // Device header
#include "delay.h"

#define SCL_PORT GPIOB
#define SCL_PIN GPIO_Pin_10
#define SDA_PIN GPIO_Pin_11

// SCL读
void I2C_Soft_W_SCL(uint8_t BitValue){
	GPIO_WriteBit(GPIOB, GPIO_Pin_10, (BitAction)BitValue);
	delay_ms(10);
}
// SDA读
void I2C_Soft_W_SDA(uint8_t BitValue){
	GPIO_WriteBit(GPIOB, GPIO_Pin_11, (BitAction)BitValue);
	delay_ms(10);
}
// SDA写
uint8_t I2C_Soft_R_SDA(void){
	uint8_t BitValue;
	BitValue = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11);
	delay_ms(10);
	return BitValue;
}

// I2C初始化
void I2C_Soft_Init(void){
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
	
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11;
	GPIO_InitStructure.GPIO_Speed =  GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &GPIO_InitStructure);
	
	GPIO_SetBits(GPIOB, GPIO_Pin_10|GPIO_Pin_11);
}

// 开始I2C通信
// pull SCL first and then pull SDA
void I2C_Soft_Start(void){
	I2C_Soft_W_SDA(1);
	I2C_Soft_W_SCL(1);
	I2C_Soft_W_SCL(0);
	I2C_Soft_W_SDA(0);
}

// I2C通信停止信号
void I2C_Soft_Stop(void){
	I2C_Soft_W_SDA(0);
	I2C_Soft_W_SCL(1);
	I2C_Soft_W_SDA(1);
}

// I2C发送一个字节
void I2C_Soft_SendByte(uint8_t byte){
	// BitAction会自动将0x00或者0x80这种八位二进制转化为0或者1
	int i;
	for (i=0; i<8; i++){
		I2C_Soft_W_SDA(byte & (0x80 >> i));
		I2C_Soft_W_SCL(1);
		I2C_Soft_W_SCL(0);
	}
}

// I2C接受一个字节
uint8_t I2C_Soft_ReceiveByte(void){
	uint8_t byte = 0x00;
	I2C_Soft_W_SDA(1);
	int i;
	for (i=0; i<8; i++){
		I2C_Soft_W_SCL(1);
		if (I2C_Soft_R_SDA() == 1){byte |= (0x80 >> i);}
		I2C_Soft_W_SCL(0);
	}
	return byte;
}

// I2C发送确认帧
void I2C_Soft_SendAck(uint8_t AckBit){
	I2C_Soft_W_SDA(AckBit);
	I2C_Soft_W_SCL(1);		
	I2C_Soft_W_SCL(0);	
}

// I2C接受确认帧
uint8_t I2C_Soft_ReceiveAck(void){
	uint8_t AckBit;
	I2C_Soft_W_SDA(1);
	I2C_Soft_W_SCL(1);
	AckBit = I2C_Soft_R_SDA();
	I2C_Soft_W_SCL(0);
	return AckBit;
}

接着是MPU6050.c文件,封装对MPU6050封装的操作,

#include "stm32f10x.h"                  // Device header
#include "I2C_Soft.h"

#define MPU6050_ADDR 0xD0

void MPU6050_WriteReg(uint8_t RegAddr, uint8_t Data){
	I2C_Soft_Start();
	I2C_Soft_SendByte(MPU6050_ADDR);
	I2C_Soft_ReceiveAck();
	I2C_Soft_SendByte(RegAddr);
	I2C_Soft_ReceiveAck();
	I2C_Soft_SendByte(Data);
	I2C_Soft_ReceiveAck();
	I2C_Soft_Stop();
}	

uint8_t MPU6050_ReadReg(uint8_t RegAddr){
	I2C_Soft_Start();
	I2C_Soft_SendByte(MPU6050_ADDR);
	I2C_Soft_ReceiveAck();
	I2C_Soft_SendByte(RegAddr);
	I2C_Soft_ReceiveAck();
	
	I2C_Soft_Start();
	I2C_Soft_SendByte(MPU6050_ADDR | 0x01); // Read Command
	uint8_t data = I2C_Soft_ReceiveByte();
	I2C_Soft_SendAck(1);
	I2C_Soft_Stop();
	return data;
}

void MPU6050_Init(){
	I2C_Soft_Init();
}

五、I2C硬件实现

在这里插入图片描述

这里我们主要实现七位地址,I2C一主多从的硬件实现。 STM32内部I2C外设的结构图
在这里插入图片描述
其中SDA和SCL是通过GPIO口复用的引脚。比如I2C2的SDA和SCL是复用了PB10和PB11端口,I2C1复用了PB6和PB7。数据发送的核心是数据移位寄存器,写的时候,数据先从DATA REGISTER移动到数据移位寄存器,然后数据移位寄存器一位一位将数据发送到SDA上。而读SDA则刚好相反。

下面有比较器、自身地址寄存器,双地址寄存器,PEC寄存器和PEC计算等模块。比较器和自身地址寄存器和双地址寄存器主要是用于STM作为从机的时候使用,自身地址寄存器存储着从机模式下STM32的地址,在这不详细展开。PEC寄存器则可以自动进行CRC校验,然后将校验位加到帧后面,用于数据校验,详见计算机网络

另外,STM32中还有独立的控制寄存器,用于控制I2C硬件电路的运行。将控制寄存器中的起始位置为一,I2C就会自动发送起始信号

  • 13
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值