STM32的I2C通信(软件读取MPU6050)

在进一步学习STM32后,I2C通信可以说是一个转折点。不同的时序图到底表示什么,软件和硬件读写到底有什么区别,如何才能高效理解并运用I2C成为了一个难题。说实在的,第一次接触I2C时,跟着一步步看时序图其实并不是很困难。但最让人头疼的环节,我觉得是把时序图和代码结合起来,保证在之后的运用中不会出错,这也是初学者必需迈过的一道坎。于是我也对此进行了一个梳理。

在此之前,先弄懂单工,半双工和全双工的区别。

类型双向通信?同时发送/接收?举例
单工(Simplex)只能一个方向电视广播(只能接收)
半双工(Half Duplex)双向但不能同时一次一个方向对讲机、一些串口通信
全双工(Full Duplex)双向并且同时同时双向收发电话通话、UART串口通信等

1.软件 I2C 的底层本质是什么?

就是你用普通 GPIO 来控制两个信号线的高低电平(SDA 数据线、SCL 时钟线),手动模拟 I2C 协议的电平波形。I2C 是一个基于开漏输出 + 上拉电阻的协议,总线空闲时是高电平,所有设备只能拉低,不能主动输出高电平。

名称引脚说明
SDA数据线(Data)双向传输数据
SCL时钟线(Clock)主设备输出的时钟信号

特点:

主从结构:主机(比如 STM32)发起通信,从机响应。

每个设备有唯一地址(7位或10位地址)。

支持多个从机,靠地址识别。

通信以“起始位 - 地址 - 数据 - 停止位”流程进行。

2.理解 GPIO 模拟 I2C 的五个基本动作(你必须掌握)

我们要实现下面 5 个函数,它们组合起来构成整个 I2C 协议的通信过程:

函数含义在协议中代表什么
MyI2C_Start()起始条件SDA下降,SCL高电平
MyI2C_Stop()停止条件SDA上升,SCL高电平
MyI2C_SendByte()发送1字节(主机→从机)发送地址、寄存器、数据
MyI2C_ReceiveByte()接收1字节(从机→主机)读取寄存器数据
MyI2C_ReceiveAck() / MyI2C_SendAck()接收/发送ACK第9位响应信号
void MyI2C_W_SCL(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOB, GPIO_Pin_10, (BitAction)BitValue);
	Delay_us(10);
}

void MyI2C_W_SDA(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOB, GPIO_Pin_11, (BitAction)BitValue);
	Delay_us(10);
}

uint8_t MyI2C_R_SDA(void)
{
	uint8_t BitValue;
	BitValue = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11);
	Delay_us(10);
	return BitValue;
}
函数名作用引脚备注
MyI2C_W_SCL输出 SCL 信号高/低GPIOB Pin 10控制时钟线电平,延时 10us
MyI2C_W_SDA输出 SDA 信号高/低GPIOB Pin 11控制数据信号电平,延时 10us
MyI2C_R_SDA读取 SDA 信号电平GPIOB Pin 11读取数据线当前电平,延时 10us

3.发送起始信号和停止信号

void MyI2C_Start(void)
{
	MyI2C_W_SDA(1);
	MyI2C_W_SCL(1);
	MyI2C_W_SDA(0);
	MyI2C_W_SCL(0);
}
void MyI2C_Stop(void)
{
	MyI2C_W_SDA(0);
	MyI2C_W_SCL(1);
	MyI2C_W_SDA(1);
}

首先起始状态,先把SCL和SDA保持高电平顺序无所谓,接着把SDA置低电平,然后SCL再置低。

接着是停止状态,先把SDA置低电平,再把SCL拉高,再把SDA拉高。这里有个问题,为什么SCL一开始不用拉低。

停止必须满足两个条件:

SCL == 1(时钟线为高)

SDA: 0 → 1(数据线在高时钟上升)

也就是说:

SCL 要先高,再 SDA 上升

而之后SCL为什么还要拉高,这里我的理解,可以把它看成一个高电平的状态,不管之前是否已经拉高。在这必须保持SCL是高电平才能进行后续的SDA拉高。

4.发送字节,接收字节

void MyI2C_SendByte(uint8_t Byte)
{
	uint8_t i;
	for (i = 0; i < 8; i ++)
	{
		MyI2C_W_SDA(Byte & (0x80 >> i));
		MyI2C_W_SCL(1);
		MyI2C_W_SCL(0);
	}
}

uint8_t MyI2C_ReceiveByte(void)
{
	uint8_t i, Byte = 0x00;
	MyI2C_W_SDA(1);
	for (i = 0; i < 8; i ++)
	{
		MyI2C_W_SCL(1);
		if (MyI2C_R_SDA() == 1){Byte |= (0x80 >> i);}
		MyI2C_W_SCL(0);
	}
	return Byte;
}

第一步:先写 SDA,送第 i 位(从高位到低位)

第二步:拉高 SCL(数据在 SCL 高电平时被采样)

第三步:拉低 SCL(准备下一位)

第一步,释放 SDA,让从机驱动数据

第二步,提供时钟,准备采样

第三步,如果为高,则置对应位

第四步,拉低 SCL,准备下一位

//假设从机 SDA 输出 = 10100101

i=0: Byte |= 0x80 >> 0 = 0x80   → Byte = 0x00 | 0x80 = 0x80  
i=1: Byte |= 0x80 >> 1 = 0x40   → 不执行(SDA为0)
i=2: Byte |= 0x80 >> 2 = 0x20   → Byte = 0x80 | 0x20 = 0xA0  
i=3: Byte |= 0x80 >> 3 = 0x10   → 不执行(SDA为0)
i=4: Byte |= 0x80 >> 4 = 0x08   → 不执行(SDA为0)
i=5: Byte |= 0x80 >> 5 = 0x04   → Byte = 0xA0 | 0x04 = 0xA4  
i=6: Byte |= 0x80 >> 6 = 0x02   → 不执行(SDA为0)
i=7: Byte |= 0x80 >> 7 = 0x01   → Byte = 0xA4 | 0x01 = 0xA5
 

5.发送和接收应答

void MyI2C_SendAck(uint8_t AckBit)
{
	MyI2C_W_SDA(AckBit);  // 设置 SDA:0=ACK,1=NACK
	MyI2C_W_SCL(1);       // 拉高 SCL → 从机在此时读取确认位
	MyI2C_W_SCL(0);       // 拉低 SCL → 结束这 1 位传输
}
uint8_t MyI2C_ReceiveAck(void)
{
	uint8_t AckBit;
	MyI2C_W_SDA(1);       // 释放 SDA,让从机驱动确认位
	MyI2C_W_SCL(1);       // 提供时钟,上升沿准备读取
	AckBit = MyI2C_R_SDA();  // 读取 SDA 电平:0=ACK,1=NACK
	MyI2C_W_SCL(0);       // 拉低 SCL,结束 ACK 位
	return AckBit;
}
名称SDA 说明意义
ACKSDA = 0确认,表示“我收到了”
NACKSDA = 1非确认,表示“我不要了”或“出错了”

场景ACK发出方操作细节对应代码函数
主机写数据,等待ACK从机主机释放SDA,SCL高时检测SDA电平MyI2C_ReceiveAck()
主机读数据,发送ACK主机主机控制SDA拉低或释放,SCL高时有效MyI2C_SendAck()

6.完整写入流程

1.发送起始信号(Start)

2.发送设备地址 + 写(0xD0)

3.发送寄存器地址(如 0x3B)

4.读取 1 字节数据

5.发送停止信号(Stop)

void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data)
{
	MyI2C_Start();
	MyI2C_SendByte(MPU6050_ADDRESS);
	MyI2C_ReceiveAck();
	MyI2C_SendByte(RegAddress);
	MyI2C_ReceiveAck();
	MyI2C_SendByte(Data);
	MyI2C_ReceiveAck();
	MyI2C_Stop();
}

7.完整读取流程

1.发送起始信号(Start)

2.发送设备地址 + 写(0xD0)

3.发送寄存器地址(如 0x3B)

4.再次发送起始信号(Repeated Start)

5.发送设备地址 + 读(0xD1)

6.读取 1 字节数据

7.主机发送 NACK

8.发送停止信号(Stop)

uint8_t MPU6050_ReadReg(uint8_t RegAddress)
{
	uint8_t Data;
	
	MyI2C_Start();
	MyI2C_SendByte(MPU6050_ADDRESS);
	MyI2C_ReceiveAck();
	MyI2C_SendByte(RegAddress);
	MyI2C_ReceiveAck();
	
	MyI2C_Start();
	MyI2C_SendByte(MPU6050_ADDRESS | 0x01);
	MyI2C_ReceiveAck();
	Data = MyI2C_ReceiveByte();
	MyI2C_SendAck(1);
	MyI2C_Stop();
	
	return Data;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值