[STM32]从零开始的IIC协议讲解与设备驱动

一、什么是IIC协议?

IIC(Inter-Integrated Circuit)协议,也称为I²C,是一种广泛使用的串行通信协议,主要用于在短距离内连接微控制器、传感器和其他集成电路(IC)。IIC协议由飞利浦公司在1980年代开发,具有以下特点:

  1. 双线制

IIC协议使用两条线进行通信:

  • SDA(Serial Data Line):串行数据线,用于传输数据。
  • SCL(Serial Clock Line):串行时钟线,用于同步数据传输。
  1. 主从架构

IIC协议采用主从架构,其中一个设备(主设备)控制总线上的通信,多个设备(从设备)可以连接到同一总线。主设备发起数据传输,并控制时钟信号。

  1. 地址寻址

每个从设备在总线上都有一个唯一的地址,主设备通过发送该地址来选择要通信的从设备。地址通常为7位或10位。

  1. 数据传输

IIC支持双向数据传输。主设备可以发送数据到从设备,也可以从从设备接收数据。数据以字节为单位传输,传输结束时,接收设备会发送一个应答信号(ACK)。

  1. 速度

IIC协议的标准传输速率有以下几种:

  • 标准模式:最高100 kbps
  • 快速模式:最高400 kbps
  • 高速模式:最高3.4 Mbps
  1. 应用

IIC协议广泛应用于各种电子设备中,如传感器、存储器、显示器等,特别适合于需要多种设备连接到同一总线的应用场合。

  1. 优点
  • 连接简单,线路少。
  • 可连接多个设备,减少了引脚占用。
  • 适合短距离通信。
  1. 缺点
  • 数据传输速度较低。
  • 在长距离或高干扰环境下性能下降。

二、哪些常见的模块会使用到IIC?

在嵌入式学习中,我们常用的OLED屏幕,MPU6050,AT24C02模块,它们使用的都是IIC协议,当然,这里也只是列举了一部分常见的模块,在实际的开发过程中,还有许多模块也使用到了IIC协议,如果我们想让我们两个芯片之间进行通信,同时又不需要很高的速度并且需要节省引脚时,我们就可以采用IIC通信。这篇文章中,我会为大家介绍IIC的协议电平以及编写代码使用IIC驱动一个MPU6050。

三、IIC的总线结构与数据有效性

IIC使用两根信号线进行通信,要求两根线都使用 开漏输出接上拉电阻 的配置,以此实现总线上所有节点SDA、SCL信号的 线与 逻辑关系。

在这种模式下,当我们的控制IC不去拉总线时,总线持续被上拉电阻拉为高,当我们的控制IC将总线拉低时,整个总线都为低。IIC的总线结合了线与的特性,可以使得我们的总线在被一方拉着时我们另一方也可以读取。

IIC 的数据读取动作都在 SCL为高 时产生,SCL为低时是数据改变的时期,无论SDA如何变化都不影响读取。所以,传输数据的过程中,当SCL为高时,数据应当保持稳定,避免数据的采集出错。

四、IIC的协议电平

在IIC通信中,我们会使用到下面几种信号:

1.起始信号

起始信号要求SCL为高时,SDA从高到低的跳变产生开始信号,下面是我使用逻辑分析仪抓出来的IIC起始信号:

我们可以看到,通道0为我们的IIC的SCL线,通道1为我们IIC的SDA线,我们为了发送起始信号,我们首先将SCL和SDK都拉为1,随后在SCL为1的前提下将SDK拉低,这样我们起始信号就已经完成了,是不是非常简单。

2.停止信号

SCL为高时,SDA从低到高的跳变产生停止信号,我们继续使用逻辑分析仪抓出波形:

这里可以看到,我们的SCL本来就为高,然后SDA由低变为高。

3.重复开始信号

在结束时不给出STOP信号,而以一个时钟周期内再次给出开始信号作为替代。

4.字节格式

SDA数据线上的每个字节必须是8位 ,对于每次传输的字节数没有限制 。每个字节(8位)数据传送完后紧跟着应答信号(ACK,第9位)。数据的先后顺序为:高位在前 。在字节格式下,IIC规定在SCL为高电平时接收或者发送数据,在SCL为低电平时变换SDA的数据。下面我们同样的来看看发送一个字节的波形:

上面发送的数据为0x55,是不是觉得非常杂乱,下面我们来框选一下:

大家可以看到,每次都是我们的SDA先变化到指定电平然后再去等待SCL变为高。在SCL变为高时IIC设备就读取SDA上的数据。

5.应答信号

协议规定数据传输过程必须包含应答(ACK)。接收器通过应答告知发送的字节已被成功接收,之后发送器可以进行下一个字节的传输。主机产生数据传输过程中的所有时钟,包括用于应答的第9个时钟。发送器在应答时钟周期内释放对SDA总线的控制,这样接收器可以通过将SDA线拉低告知发送器:数据已被成功接收。

应答信号分为两种:

1.当第9位(应答位)为 低电平 时,为 ACK  (Acknowledge)   信号

2.当第9位(应答位)为 高电平 时,为 NACK(Not Acknowledge)信号

主机发送数据,从机接收时,ACK信号由从机发出。当在SCL第9位时钟高电平信号期间,如果SDA仍然保持高电平,则主机可以直接产生STOP条件终止以后的传输或者继续ReSTART开始一个新的传输

从机发送数据,主机读取数据时,ACK信号由主机给出。主机响应ACK表示还需要再接收数据,而当主机接收完想要的数据后,通过发送NACK告诉从机读取数据结束、释放总线。随后主机发送STOP命令,将总线释放,结束读操作。

因为这里的应答信号是紧跟字节后的一个位,我们这里就不使用逻辑分析仪展示了。

五、七位地址设备一个完整的IIC通信

1.向七位设备的指定寄存器写入指定值

假如我们想向一个七位的设备的指定寄存器中写入一个指定的值,我们需要做下面的步骤,首先,我们需要在IIC总线中发送七位设备的地址,不出意外并且地址正确的话,设备会产生一个应答,应答成功以后就表示这个设备在我们的IIC总线中,注意,发送地址这个步骤是紧跟起始信号之后的,随后我们需要发送第二个数据,第二个数据是我们指定设备的指定寄存器地址,第二个数据发送完以后,我们会发送第三个数据,第三个数据表示的就是我们想向指定的设备的指定寄存器中写入的值,下面我们还是使用逻辑分析仪来抓一下波形:

这里我们可以看到波形没有任何规律,让我们将波形拆开来看,我们可以看到,在最开始的地方,SDA在SCL为高时被拉低,这就是我们的起始信号:

在跟着起始信号以后,我们紧跟着八个位的数据,也就是一个字节,这个字节的数据为0x68,并且是加上读写位以后的地址,这里我们是对0xD0地址的设备进行了写操作:

在一个字节的数据之后,我们可以看到了一个应答,这里可以看到,在第九位数据位时,我们的SDA在SCL为高电平时时为低电平,表示接收从机已经给了我们应答。当从设备应答以后,我们的一个完整的数据就已经发送完了。当我们发送完第一个字节以后并且被我们呼叫的设备给了我们应答,这就表示,这个IIC设备正在我们的IIC总线中。下面我将发送我们想要操作的寄存器地址,这里同样的是一个八位的地址,这里的地址表示我们操作的IIC设备内的寄存器地址,这里我们设备的寄存器地址为0x6B:

最后一组数据就是我们想对这个寄存器写入的值了,我们这里向0x6B寄存器写入0x01的值:

在最后,我们的SDA在SCL为高时从低变到高,发送停止信号:

这样,我们就完成了一次对指定设备指定寄存器的写入操作。

2.读取七位设备指定寄存器值

下面我们来看看寄存器的读取操作,IIC协议规定,在读取IIC设备的寄存器时,应先发送设备的地址并且读写位为0,随后发送想要读取的寄存器地址,随后重新开启一次通信,重新开启的通信我们仍然需要先发送要读取的设备的设备地址,随后将SDA的控制权交给从机,主机只控制时钟,从机跟着主机的时钟发送数据,主机读取数据。这里从机的发送速度不能太慢,如果从机一直在这里磨磨唧唧主机是不会等你的,下面我们来看看主机读取一次数据的时序图,同样是用逻辑分析仪抓出来的:

看不懂没关系,我们挨个来分析,首先,SDA在SCL是高的时候从高变到低,发出了一个起始信号:

随后我们就发送了从机的地址,注意,这里第一次发送读写位为0表示写入:

随后重新发送一次起始信号,这里可以停止了再起始,也可以直接起始:

随后,再次发送一次从机的地址,这次的读写位位1,表示读取:

随后,SDA的控制权就交给从机,从机根据主机的时钟控制SDA,主机读取SDA的数据:

最后,SDA在SCL为高时从低变到高,产生了停止信号:

至此就完成了一次IIC设备寄存器的读取操作。

六、软件IIC与硬件IIC

在STM32中,IIC作为一个外设存在,我们可以操作这个外设直接向别的IIC设备读取或者写入数据。在现在大部分的IC中,都集成IIC的外设,我们可以通过函数或者操作相关寄存器直接使用,可以说是非常方便了。这种被集成在芯片中我们可以直接操作使其发送或者读取数据的IIC外设被我们称为硬件IIC。因为IIC是一种同步通信协议,并且通信速率不快,我们可以使用STM32的普通GPIO引脚模拟IIC协议,因为时钟被我们自己控制,所以不管我们时序多慢都行。用引脚模拟IIC的方式被我们称为软件IIC。

七、使用软件IIC读取MPU6050的数据

这里为了让大家将IIC协议理解得更深刻,我们这里使用软件模拟的方式去读取MPU6050的数据。这里我会做代码讲解,逐一分析代码内容。在下面的代码中我会开始写IIC的协议,中途如果产生了错误我也会记录其中。我编写好的代码例程和资料在下方的网盘中:

IIC相关代码和资料:https://pan.baidu.com/s/1jFnza_MaRSSPc9hm9Ubrig?pwd=clxm

提取码:clxm

首先我们需要复制一份STM32F103C8T6的工程模板过来,并且打开:

这里我首先创建两个文件来存放IIC的代码,这里分别创建IIC.h和IIC.c。我创建在了工程目录的HOME目录下:

将其导入到keil中:

随后将.h文件中的标准格式写好:

因为这里是使用软件IIC,所以现在我们到.c中封装一个GPIO的初始化函数,注意,这里要将GPIO初始化为开漏输出模式,这里一定要将GPIO初始化为开漏输出:

cpp 复制代码
void IIC_GpioInit(void)
{
    GPIO_InitTypeDef GPIO_InitStructTypedef;
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
	GPIO_InitStructTypedef.GPIO_Mode = GPIO_Mode_Out_OD;
	GPIO_InitStructTypedef.GPIO_Pin = GPIO_Pin_10|GPIO_Pin_11;
	GPIO_InitStructTypedef.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB,&GPIO_InitStructTypedef);
}

下面我们来封装两个函数,用于控制SCL和SDA,这样我们就可以通过传入的参数直接控制SCL和SDA了:

cpp 复制代码
void IIC_W_SCL(uint8_t val)
{
	GPIO_WriteBit(GPIOB,GPIO_Pin_10,(BitAction)val);
}

void IIC_W_SDA(uint8_t val)
{
	GPIO_WriteBit(GPIOB,GPIO_Pin_11,(BitAction)val);
}

写好这两个函数以后,我们直接在主函数中测试一下这两个函数,编写这种通信协议类的代码,建议大家购买一个逻辑分析仪来抓取波形,不然很有可能编写逻辑不正确但是却找不出问题,如果你会看示波器当然效果是一样的。我们在主函数中写入以下代码:

我们编译并且下载,不出意外的话,我们的SCL和SDA的电平应该是500毫秒翻转一次,我们同样使用逻辑分析仪来查看:

这里可以看到,我们SCL和SDA的电平都是正常的。说明我们的初始化和控制相关的函数没有问题,这里没有问题以后,我们开始进行下一步。

我们再定义一个读取SDA的函数,大致道理是和上面一样的,我们这里就不需要传入参数了,我们将收到的数据作为函数的返回值就行了:

cpp 复制代码
uint8_t IIC_R_SDA(void)
{
	return GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_11);
}

这样我们读取SDA的函数就封装好了,函数很简单,甚至可以不封装,但是为了让代码具有可读性,我们还是选择封装一层。

下面我们开始封装IIC的起始函数,起始函数要求SDA在SCL为高时从高变到低,我们按照要求开始封装起始信号的函数:

cpp 复制代码
void IIC_Start(void)
{
	IIC_W_SCL(1);
	IIC_W_SDA(1);
	IIC_W_SDA(0);
	IIC_W_SCL(0);
}

下面我们来解释一下代码,在代码中,先将SCL和SDA都拉高,然后我们再拉低SDA,这就已经满足了IIC的起始信号的条件。这里我们再将SCL拉低,准备发送数据。下面使用逻辑分析仪看一下这个函数的电平变化:

从波形可以看到,我们这里的SDA在SCL为高时变化为低,说明我们的起始信号已经成功发出了。

下面我们再来编写一个停止信号的函数,停止信号要求SDA在SCL为高时从高变到低,我们来封装一个停止信号的函数:

cpp 复制代码
void IIC_Stop(void)
{
	IIC_W_SCL(1);
	IIC_W_SDA(0);
	IIC_W_SDA(1);
}

同样的,来解释一下代码,这里我们首先保证SCL为高,随后再保证SDA为低,最后我们在SCL为高时将SDA拉高。

我们再用逻辑分析仪来观察一下:

可以看到,SDA在SCL为高时从低变到高,满足停止信号的要求。

下面我们来封装一个使用IIC发送一个字节的函数,如下图:

cpp 复制代码
void IIC_SendData(uint8_t data)
{
	uint8_t i;
	IIC_W_SCL(0);
	for(i=0;i<8;i++)
	{
		if(data<<i&0x80)
			IIC_W_SDA(1);
		else
			IIC_W_SDA(0);
		IIC_W_SCL(1);
		IIC_W_SCL(0);
	}
	IIC_W_SDA(1);
}

现在来解释一下代码,这里我们将要发送的字节作为传入的参数,然后将SCL拉低,随后进入for循环,这里在for循环中要判断data的第i位是否为高,如果为高就拉高SDA,如果为低就拉低SDA,在完成SDA的跳变以后让SCL跳变一次实现从机读取一次数据。下面我们来解释一下这里的左移,因为涉及位操作,所以请确保自己的C语言基础过关。这里的左移说起来也很简单,当i为0时,data向左移0位,等于没有左移,这时候再与0x80相与,这里的0x80转换为二进制为1000 000 这里就实现了将data的最高位取出来,如果data的最高位为一,那么就拉高SDA,如果data的最高位为零,那就拉低SDA,再次进入循环,当i为1时,data向左移一位,也就是将data的第六位取出来随后再重复上面的操作,直到最后,将data的每一位都取出来并且发送,就完成了一个字节的发送。在最后,我们将SDA拉高,确保我们放开SDA总线,准备接收应答信号。

下面我们同样使用逻辑分析仪来看看我们的发送函数是否正确,在主函数中,我们可以将发送一个字节的函数写在引脚初始化函数的后面,这里我们发送"0x68",转换为二进制就是"0110 1000":

使用逻辑分析仪来看看:

我们可以看到,这里SCL被拉高了八次,正好对应八次数据,我们在每次SCL被拉高时去看SDA的数据,也符合最开始的预期,这也完全符合了IIC协议中SCL被拉高时读取数据,SCL低时变换数据:

至此,我们发送一个字节的函数就已经写完了。

下面我们再来编写一个接收应答的函数,应答信号会产生在SCL的第九个时钟周期,在这期间,我们要确保我们自己释放了SDA线,如果我们一直将SDA拉着,从机拉不动SDA,就不能准确的接收应答引号了,这里我们编写一个函数来接收应答信号:

cpp 复制代码
uint8_t IIC_ReadAck(void)
{
	uint8_t num;
	IIC_W_SCL(0);
    IIC_W_SDA(1);
	IIC_W_SCL(1);
	num = IIC_R_SDA();
	IIC_W_SCL(0);
	return num;
}

这里的接收应答的函数就很简单了,首先定义一个变量用来存放应答,然后将SCL拉低,这里的拉低是为了确保SCL是低以便产生上升沿,将SDA拉高,确保释放了SDA总线,最后将SCL拉高,随后读取SDA的数据,最后将SCL拉低并且返回读取到的应答。

现在既然我们已经写好了起始,停止,发送一个字节,接收一个应答的函数,我们就可以做一些测试了。我们将STM32与MPU6050都插在面包板上,将PB10接到MPU的SCL,将PB11接到MPU的SDA,并且给MPU通电,将逻辑分析仪接到SCL和SDA检测数据。我们在keil中可以这样写函数:

首先,这里发送一个起始信号,随后发送0XD0的数据,这里的0xD0是0x68在左移一位后在第0位补零得到的,还记得一开始提到的IIC的协议标准吗,对于七位设备的读写,我们将七位地址左移一位以后,用第0位表示读还是写。这里在第0位补0表示写数据。最后接收一个应答。我们将代码烧录以后,看逻辑分析仪:

这里可以看到,我们顺利的发送了起始信号以及数据,并且从设备给出了应答。说明我们前面编写的这些函数是没有问题的。

这里的时序图大家一定要学会自己看,后面很多涉及时序逻辑问题的代码都需要使用逻辑分析仪来看。

下面再来写一个接收一个字节的函数,如图:

cpp 复制代码
uint8_t IIC_ReadData(void)
{
	char i;
	uint8_t data=0;
	IIC_W_SCL(0);
	IIC_W_SDA(1);
	for(i=8;i>0;i--)
	{
		IIC_W_SCL(1);
		data|=IIC_R_SDA()<<(i-1);
		IIC_W_SCL(0);
	}
	return data;
}

如果理解了发送的话,这里的接收数据的代码也很简单了。我们首先定义一个变量来存接收到的数据,随后将SCL拉低,确保等会儿产生上升沿。随后释放SDA,注意,这里一定要释放SDA,不然是接收不到从机的数据的。下面就进入了for循环,这里的for会循环8次,我们还是依次来看。当第一次循环时,i为8,我们将SCL拉高,这时候就可以读取数据了,下面再将我们读到的数据向左移i-1位,下面我来解释一下这里的i-1,因为我们IIC发送数据遵循高位在前的原则,所以说这里我们第一次接收到的数据是最高位。我们将其左移i-1位也就是8-1,也就是第七位,即一个八位变量的最高位。下面再将其与data相或,这样我们接收到的最高位的值就存入了data中。以此类推,我们将接收到的每一位都存入data中,直到第0位。至此,我们一个字节的数据就接收完成了。最后将data作为函数的返回值,这样,一个接收一个字节的函数就写好了。

在接收了一个字节以后,我们需要给从机一个应答,这里我们还需要写一个发送应答的函数,如图:

cpp 复制代码
void IIC_SendAck(uint8_t Ack)
{
	IIC_W_SCL(0);
	if(Ack == 1)
		IIC_W_SDA(1);
	else
		IIC_W_SDA(0);
	IIC_W_SCL(1);
	IIC_W_SDA(1);
	IIC_W_SCL(0);
}

这里的应答函数将想发送的应答作为传入参数,首先将SCL拉低,确保等会儿产生高电平,随后判断传入的应答是1还是0来拉SDA总线。SDA拉完以后,一定要将SCL拉高,这里一定要将SCL拉高,只有这样从机才会读取应答。最后将SDA释放。SCL拉低。注意,这里一定将SCL拉低,这一步很重要。

至此,IIC的全部要使用的函数我们都封装好了,下面我们就来测试一下这些函数,这里作为测试的话,我们可以选择去读取一下MPU6050的ID寄存器。MPU6050的ID寄存器地址为0x75。那我们就可以大致梳理出函数的步骤了。首先发送起始信号,随后发送MPU6050的地址加上读写位为写,再接收MPU6050的应答,随后再次发送起始信号,再发送MPU6050的地址加上读写位为读,接收MPU6050的应答,最后接收一个字节的数据,这个字节的数据就是读取到的MPU6050ID寄存器的数据,如下面的代码:

cpp 复制代码
#include "delay.h"
uint8_t ID;
int main(void)
{
	IIC_GpioInit();
	IIC_Start();
	IIC_SendData(0xD0);
	IIC_ReadAck();
	IIC_SendData(0x75);
	IIC_ReadAck();
	IIC_Start();
	IIC_SendData(0xD0|1);
	IIC_ReadAck();
	ID = IIC_ReadData();
	IIC_SendAck(1);
    IIC_Stop();
	while(1)
	{

	}
}

因为考虑到大家可能不怎么会,我们还是对代码进行依次解释。

首先是"IIC_GpioInit();"用于初始化相关引脚。

随后"IIC_Start();"发送了IIC的起始信号。

随后"IIC_SendData(0xD0);"发送了MPU6050的地址,并且读写位为0,表示写。

随后"IIC_ReadAck();"接收了一个应答。

随后"IIC_SendData(0x75);"发送了MPU6050 ID寄存器的地址。

随后"IIC_ReadAck();"接收了一个应答。

随后"IIC_Start();"发送重复起始信号。

随后"IIC_SendData(0xD0|1);"发送了MPU6050的地址,并且读写位为1,这里将0xD0与1相或这里的读写位为1表示读。

随后"IIC_ReadAck();"接收了一个应答。

随后"ID = IIC_ReadData();"接收了一个字节,并且将接收到的数据存放再ID变量中。

随后"IIC_SendAck(1);"发送了一个NAck,表示非应答。

最后"IIC_Stop();"停止了本次通信。

我们进入调试模式中,可以看到,我们的ID变量在代码运行以后已经有值被放入其中:

这也说明了我们编写的所有IIC函数没有问题。同样的打开逻辑分析仪就可以看到我们的代码逻辑。

如果你的代码到这里运行的结果和我一样,那么可喜可贺,如果你的代码运行以后,ID变量并没有读到值,那就要考虑是不是有地方的时序弄错了,建议跟着上面的代码进行分段调试。如果没有逻辑分析仪建议购买一个逻辑分析仪。在调试通信协议时会方便许多。记住一点,优秀并且实用的代码是通过反复调试出来的,一定不要只盯着代码看来找问题,通过调试发现问题才是我们解决代码问题的最优解决办法。

在上面的IIC代码没有问题以后,我们已经解决了基本的与MPU6050的通信问题,这一来确实挺不容易了,现在我们要基于这的基础上向上封装MPU6050的驱动。

下面在HOME中创建"MPU6050.c","MPU6050.h":

在keil中写好头文件格式:

下面我们先来写一个读MPU6050 ID的函数,这里我们可以将上面我们写好的函数直接封装,因为我们的测试函数就是在读取MPU6050的ID寄存器,如图:

cpp 复制代码
uint8_t MPU6050_GetID(void)
{
	uint8_t ID;
	IIC_GpioInit();
	IIC_Start();
	IIC_SendData(0xD0);
	IIC_ReadAck();
	IIC_SendData(0x75);
	IIC_ReadAck();
	IIC_Start();
	IIC_SendData(0xD0|1);
	IIC_ReadAck();
	ID = IIC_ReadData();
	IIC_SendAck(1);
	IIC_Stop();
	return ID;
}

这里让我们在主函数中调用:

同样在调试模式中调试,查看ID变量的值:

下面我们来封装一个向MPU6050写寄存器的函数,这里给MPU6050发送数据或者是读取寄存器数据的函数不会写的话,可以参考资料中的"MPU-6000.6050中文资料.pdf":

这里让我们打开中文资料,滑到写MPU6050寄存器相关的地方:

这里我们的代码按照逻辑进行就可以了,如图:

cpp 复制代码
void MPU6050_SetREG(uint8_t REG,uint8_t Data)
{
	IIC_Start();
	IIC_SendData(MPU6050_ADDRES);
	IIC_ReadAck();
	IIC_SendData(REG);
	IIC_ReadAck();
	IIC_SendData(Data);
	IIC_ReadAck();
	IIC_Stop();	
}

这里我么可以将代码和手册中的逻辑对照着看,大家写代码时也可以这样,避免时序逻辑上的错误。这里我们将想要写入的寄存器的地址和想要写的数据作为函数的参数,下面的"MPU6050_ADDRES"表示MPU6050的地址,我们直接进行一个宏定义"#define MPU6050_ADDRES 0xD0"。因为这里在以后MPU6050的地址会经常用到,所以进行了宏定义。

写完这个函数以后,我们再写一个读MPU6050寄存器的函数,这里读寄存器大家应该是太熟了吧,之前读MPU6050的ID寄存器不就是在读寄存器吗,我们可以直接参照读MPU6050的函数和手册中给出的时序来写一个读寄存器的函数:

这里读寄存器的函数我们就可以用刚才的ID寄存器来试一下,我们在主函数中这样写:

同样的在调试模式中看ID变量的值:

如果你这里没有读到值,可以考虑使用逻辑分析仪抓一下波形,看看时序,一定要将调试和逻辑分析仪结合起来,这样才能更快的发现问题,下图是使用函数"MPU6050_GetREG"读取ID寄存器时产生的正确时序:

下面我们来使用向MPU6050寄存器写入值的函数来配置MPU6050,这里我们就需要打开资料中的"MPU-6050寄存器映射(中文).pdf":

这里对MPU6050的寄存器配置只是做一个简单的讲解,如果你不会看寄存器手册不知道怎么配置寄存器的值,直接跟着我配就行了。

首先我们需要写一个MPU6050的初始化函数,用于MPU6050的初始化配置:

这里在函数中,我们先将IIC的相关引脚初始化了以便后面使用。

随后打开寄存器手册,翻到4.30处:

我们首先要配置MPU6050的电源管理器,

这里我们需要配置电源管理寄存器,需要让MPU6050处于唤醒模式,然后以X轴作为基准时钟。所以这里我们配置电源管理寄存器为0x01,这里我们可以看到这个寄存器的地址为0x6b,所以在初始化函数中,我们可以这样写:

然后我们开始配置电源管理寄存器2:

这里要让所有轴都处于工作模式,并且MPU6050不处于休眠模式,所以这里配置电源管理寄存器2为0x00,这里电源管理寄存器2的地址为0x6C,所以在代码中这样写:

根据上面的步骤,我们依次配置"采样率分频寄存器","MPU6050配置寄存器","陀螺仪配置寄存器","加速度计配置寄存器",代码如下:

cpp 复制代码
void MPU6050_Init(void)
{
	IIC_GpioInit();
    MPU6050_SetREG(0x6B,0x01);
	MPU6050_SetREG(0x6C,0x00);
	MPU6050_SetREG(0x19,0x09);
	MPU6050_SetREG(0x1A,0x06);
	MPU6050_SetREG(0x1B,0x18);
	MPU6050_SetREG(0x1C,0x18);
}

这里对寄存器的操作是直接采用寄存器地址的形式,为了增加代码的可读性,不推荐大家直接写寄存器地址,建议单独写一个头文件将其宏定义。

至此MPU6050的初始化就已经完成了,下面来写一个读取数据的函数,这里的加速度计和角速度计的数据为16位数据,在MPU6050的寄存器中数据是按高八位和第八位存储的,我们需要将数据读取出来以后再拼接为16位的。代码如下:

cpp 复制代码
void MPU6050_GetData(int16_t *AccX, int16_t *AccY, int16_t *AccZ, 
						int16_t *GyroX, int16_t *GyroY, int16_t *GyroZ)
{
	uint8_t DataH, DataL;								
	
	DataH = MPU6050_GetREG(0x3B);		
	DataL = MPU6050_GetREG(0x3C);		
	*AccX = (DataH << 8) | DataL;						
	
	DataH = MPU6050_GetREG(0x3D);		
	DataL = MPU6050_GetREG(0x3E);		
	*AccY = (DataH << 8) | DataL;						
	
	DataH = MPU6050_GetREG(0x3F);		
	DataL = MPU6050_GetREG(0X40);		
	*AccZ = (DataH << 8) | DataL;						
	
	DataH = MPU6050_GetREG(0x43);		
	DataL = MPU6050_GetREG(0x44);		
	*GyroX = (DataH << 8) | DataL;						
	
	DataH = MPU6050_GetREG(0x45);		
	DataL = MPU6050_GetREG(0x46);		
	*GyroY = (DataH << 8) | DataL;						
	
	DataH = MPU6050_GetREG(0x47);		
	DataL = MPU6050_GetREG(0x48);		
	*GyroZ = (DataH << 8) | DataL;						
}

如果大家这里的代码看不懂的话,直接复制即可。这里的代码参考了bilibili的"江协科技"。

在函数中,要求传入int16_t的指针类型,所以在使用的时候也需要传入变量的地址:

随后我们进入调试模式看看数据:

可以看到,这几个变量应该是持续变化的,在逻辑分析仪中的波形也是比较有规律的:

如果你这里读取不到数据,可以考虑查看底层的函数是否有误,将底层的函数结合逻辑分析仪调整正确以后在封装上层函数。

至此,我们MPU6050的数据读取就已经完成了。

八、结语

在这篇文章中,我教了大家如何从零开始手写软件IIC协议。手写IIC协议有助于提高我们写代码的逻辑能力,也能让我们对IIC有更进一步的认识。记住一点,编写通信协议相关的代码时要结合逻辑分析仪进行反复调试,直到代码产生的逻辑和协议逻辑对应。不要只是盯着代码看,这样不能找出问题。优秀的代码是从反复的试验和调试中产生出来的。最后,感谢大家的观看!

相关推荐
wenchm17 分钟前
细说STM32单片机DMA中断收发RTC实时时间并改善其鲁棒性的另一种方法
stm32·单片机·嵌入式硬件
编码追梦人1 小时前
如何实现单片机的安全启动和安全固件更新
单片机
电子工程师UP学堂2 小时前
电子应用设计方案-16:智能闹钟系统方案设计
单片机·嵌入式硬件
飞凌嵌入式2 小时前
飞凌嵌入式T113-i开发板RISC-V核的实时应用方案
人工智能·嵌入式硬件·嵌入式·risc-v·飞凌嵌入式
blessing。。3 小时前
I2C学习
linux·单片机·嵌入式硬件·嵌入式
嵌新程5 小时前
day03(单片机高级)RTOS
stm32·单片机·嵌入式硬件·freertos·rtos·u575
Lin2012305 小时前
STM32 Keil5 attribute 关键字的用法
stm32·单片机·嵌入式硬件
电工小王(全国可飞)5 小时前
STM32 RAM在Memory Map中被分为3个区域
stm32·单片机·嵌入式硬件
maxiumII5 小时前
Diving into the STM32 HAL-----DAC笔记
笔记·stm32·嵌入式硬件
美式小田8 小时前
单片机学习笔记 9. 8×8LED点阵屏
笔记·单片机·嵌入式硬件·学习