【STM32】I2C通信

基本的任务是:通过通信线,实现单片机读写外挂模块寄存器的功能。其中至少要实现在指定位置写寄存器和在指定的位置读寄存器这两个功能。

异步时序的优点:省一根时钟线,节约资源;缺点:对事件要求严格,对硬件电路依赖严重

同步时序反过来。

1 I2C通信

I2C(Inter IC Bus)是由Philips公司开发的一种通用数据总线

两根通信线:SCLSerial Clock)、SDASerial Data

同步,半双工

带数据应答

支持总线挂载多设备(一主多从、多主多从)

一主多从:一个单片机作为主机,挂载一个或者多个模块作为从机。

多主多从:多个主机,多个从机(但是同一时刻只能有一个主机控制)

1.1 硬件电路

所有I2C设备的SCL连在一起,SDA连在一起

设备的SCL和SDA均要配置成开漏输出模式

SCL和SDA各添加一个上拉电阻,阻值一般为4.7KΩ左右

左边的CPU就是单片机,作为总线的主机,主机的权利很大,包括对SCL线的完全控制,任何时候都是主机完全掌控SCL线。空闲状态下,主机可以主动发起对SDA的控制,只有在从机发送数据和从机应答时候,主机才会转交SDA的控制权给从机。下面都是被控制IC,也就是挂载在I2C上总线上的从机,这些从机可以是姿态传感器、OELD、存储器、时钟模块等。从机的权利比较小,对于SCL时钟线,在任何时刻都只能被动的读取,从机不允许控制SCL线。对于SDA数据线,从机不允许主动发起对SDA的控制,只有在主机发送读取从机的命令后或者从机应答的时候,从机才能短暂的取得SDA的控制权,这就是一主多从模型。

主机的SCL是输出,没问题,主机和从机的SDA在输入和输出之间变化,

左边是SCL的结构,右边是SDA的结构。

首先引脚的信号进来,都可以通过一个数据缓冲器或者施密特触发器,进行输入,因为输入对电路没有任何影响。在输出这部分使用的是开漏输出的配置(输出低电平,开关管导通,引脚直接接地,是强下拉;输出高电平,这个开关管断开,引脚什么都不接,处于浮空状态,所有的设备只能输出低电平而不能输出高电平,为了避免高电平造成的引脚浮空,这时需要在总线外面SCL和SDA各外置一个上拉电阻,弱上拉)

好处:

(1)完全杜绝了电源短路现象,保证了电路的安全;

(2)避免了引脚模式的频繁切换,开漏加弱上拉的模式,同时兼具了输入和输出的功能。开漏模式下,输出高电平就相当断开引脚,所以在输入之前,可以直接输出高电平,不用切换输入模式了;

(3)这个模式会有一个“线与”的现象,就是只要有任意一个或多个设备输出了低电平,总线就处于低电平。利用这个特性执行多主机模式下的时钟同步和总线仲裁。

1.2 I2C时序基本单元

(1)起始条件:SCL高电平期间,SDA从高电平切换到低电平

(2)终止条件:SCL高电平期间,SDA从低电平切换到高电平

在I2C处于空闲时,SCL和SDA都处于高电平;SCL高电平期间,SDA从高电平切换到低电平,之后主机要再把SCL拽下来,一方面是占用这个总线,另一方面也是为了这些基本单元的拼接。(低电平开始,低电平结束)

终止条件:SCL先回弹到高电平,之后,SDA再回弹至高电平,这个上升沿触发终止条件。

起始和终止都是由主机产生的。所以在总线空闲状态时,从机必须双手放开,不允许主动跳出来碰总线(允许的话是多主机模型了)

(3)发送一个字节:SCL低电平期间,主机将数据位依次放到SDA线上(高位先行),然后释放SCL(SCL成高电平),从机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可发送一个字节

起始开始后,第一个字节也必须是主机发送的,最开始SCL是低电平,如果主机想发送0,就拉低SDA到低电平;如果想发送1,就放手,SDA回弹至高电平。在SCL低电平期间,允许改变SDA的电平。当这一位放好后,主机松手时钟线,SCL回弹到高电平,在高电平期间是从机读取SDA的时候,所以高电平期间,SDA不允许变化;SCL处于高电平之后,从机需要尽快的读取SDA,一般是在上升沿这个时刻,从机就已经读取完成了。主机在放手SCL一段时间后,就可以继续拉低SCL了,传输下一位,主机也需要在SCL下降沿之后尽快把数据放到SDA上;数据放完之后,主机再松手SCL,SCL到达高电平,从机读,以此类推。主机拉低SCL,把数据放在SDA上,主机松开SCL,从机读取SDA上的数据,在SCL的同步下依次进行主机的发送和从机的接收。高位先行,所以第一位是第一个字节的最高位B7。SCL和SDA全程由主机掌控,从机只能被动读取。

(4)接收一个字节:SCL低电平期间,从机将数据位依次放到SDA线上(高位先行),然后释放SCL,主机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可接收一个字节(主机在接收之前,需要释放SDA

释放SDA就相当于切换成输入模式,所有设备包括主机都处于输入模式,当主机需要发送的时候,就可以主动拉低SDA,而主机在被动接收的时候,就必须先释放SDA(总线是线与的特征)。

接收一个字节和发送一个字节非常相似。

区别是:发送一个字节是低电平主机放数据,高电平从机读数据;

           而接收一个字节是低电平从机放数据,高电平主机读数据,

主机在接收数据之前要先释放SDA线,然后这是从机取得SDA的控制权,从机需要发送0,就把SDA拉低;从机需要发送1,就放手SDA线,SDA回弹至高电平。然后同样的,低电平变换数据,高电平读取数据。实线表示主机控制的电平,虚线表示从机控制的电平。SCL全程由主机控制,SDA主机在接收前要释放,交由从机控制,从机的数据变化都是贴着SCL下降沿进行的,而主机可以在SCL任意高电平时刻读取数据,这就是接收一个字节的时序。

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

在接收一个字节后,需要给从机一个应答位,发送应答位的目的是告诉从机,是不会还要继续发,如果从机发送一个数据后,得到了主机的应答,那从机就会继续发送;如果从机没有得到主机的应答,那从机就会认为,自己发送了一个数据,主机不理,可能主机不想要了,这时从机就会释放SDA,交出SDA的控制权,防止干扰主机的操作。

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

在调用发送一个字节之后,就要紧跟着调用接收应答的时序,用来判断从机有没有收到刚才给它的数据。如果从机收到了,那在应答位这里,主机释放SDA的时候,从机就应该立刻把SDA拉下来,然后在SCL高电平期间,主动读取应答;如果应答位为0,就说明从机确实收到了。这个场景就是主机刚发送一个字节,问有没有人收到,现在把SDA放手了,如果有人收到的话,就把SDA拽下来,然后主机高电平读取数据,发现确实有人拽下来了,说明有人收到了数据;如果主机发送松手后,SDA跟着回弹到高电平,说明没人回应,没人收到或者收到没回应。

1.3 I2C时序

从机有唯一的设备地址。(7位地址)MPU6050: 1101 000

(1)指定地址写

对于指定设备(Slave Address),在指定地址(Reg Address)下(设备内部地址,寄存器),写入指定数据(Data

SAD和SDL都处于高电平,开始的时候,拉低SDA,产生起始条件,在起始条件之后,紧跟着的时序必须是发送一个字节的时序,字节的内容必须是从机地址+读写位,从机地址7位,读写位1位, 加起来是一个字节8位,发送从机地址就是确定通信对象,发送读写位就是确定接下来是读出还是写入。具体:低电平期间,SDA变换数据,高电平期间,从机读取SDA。绿色的竖线表示从机读的数据。主机寻找的地址是1101 000(MPU6050的地址),后面的0表示之后的时序主机要进行写入操作,1表示之后的时序要进行读出操作。目前主机是发送一个字节,字节的内容转换成16进制,高位先行,就是0xD0,然后根据协议规定,紧跟着的就是接收从机的应答位(Receive ACK,RA),在这个时刻,主机要释放SDA。

如果单看主机的波形,释放SDA之后,引脚电平回弹到高电平(黄线),但是根据协议规定,从机要在这个时候拉低SDA(绿线),综合两者的波形,在主机释放SDA之后,由于SDA也被从机拽住了,所以主机松手后,SDA并没有回弹高电平,这个过程就表示从机产生了应答,最终高电平期间,主机读SDA,发现是0,就说明进行寻址时,有人给我应答了,传输没问题;如果主机读取SDA发现是1,就说明寻址在应答期间,我松手了,但是没人拽住它,没人给我应答,就直接产生停止条件。后面的上升沿就是应答结束后,从机释放SDA产生的,从机交出SDA的控制权,因为从机要在低电平期间尽快变换数据,所以这个上升沿和SCL的下降沿几乎是同时发生的。继续往后,读写位给了0,所以应答结束后,要继续发送一个字节,同样的时序,再来一遍,第二个字节就可以送到指定设备的内部了,从机设备可以自己定义第二个字节和后续字节的用途,一般第二个字节可以是寄存器地址或者指令控制字等。这里是0x19就表示要操作0x19地址下的寄存器了。

接着同样是从机应答,主机释放SDA,从机拽住SDA,SDA表现为低电平,主机接收到应答位0,表示收到了从机的应答。同样的流程再来一遍,主机再发送一个字节,这里表示要在0x19地址下写入0xAA,最后是接收应答位。如果主机不需要再继续传输了,就可以产生停止条件,在停止条件之前,先拉低SDA,为后续的SDA上升沿做准备,然后释放SCL,再释放SDA,这样就产生了SCL高电平期间,SDA的上升沿。

总结:这个数据帧的目的就是对于指定从机地址为1101 0000的设备,在其内部0x19的寄存器中写入0xAA这个数据。

(2)当前地址读

对于指定设备(Slave Address),在当前地址指针指示的地址下,读取从机数据(Data

如果主机想要读取从机的数据,就可以执行这个时序。最开始还是SCL高电平期间,拉低SDA,产生起始条件,起始条件开始后,主机必须首先调用发送一个字节,来进行从机的寻址和指定读写标志位,图中表示本次寻址的目标是1101 000的设备,最后一位读写标志位为1,表示主机读取数据,紧跟着,发送一个字节之后,接收一下从机的应答,从机应答0,表示从机接收到了第一个字节,在从机应答之后,在这里开始,数据的传输方向就要反过来了。

主机刚才发出了读的指令,所以在这之后,主机就不能继续发送了,要把SDA的控制权给从机,主机调用接收一个字节的时序,进行接收操作,之后,从机得到主机的允许,可以在SCL低电平期间写入SDA,然后主机在SCL高电平期间读取SDA,最终,主机在SCL高电平期间依次读取8位,就接收到从机发送的一个字节的数据,0000 1111,也就是0x0F,这里没有指定地址环节,这里需要用到当前地址指针了,在从机中,所有寄存器被分配到一个线性区域中,并且会有一个单独的指针变量,指示着其中一个寄存器,一般默认为0地址,每写入和读出一个字节后,这个指针就会自动自增一次,主机没有指定地址的话,从机就会返回当前指针指向的寄存器的值。

(3)指定地址读

对于指定设备(Slave Address),在指定地址(Reg Address)下,读取从机数据(Data

前面一部分是指定地址的时序,把后面写数据的部分去掉,然后把前面这一部分设置地址,还没有指定写什么数据的时序,追加到当前地址读的时序前,就得到了指定地址读的时序(复合格式)

前面部分是指定地址写,只指定了地址,还没来得及写;后面的部分是当前地址读,加起来就是指定地址读了。

前面依然是启动条件,然后发送一个字节进行寻址,指定从机地址是1101 000,读写标标志位是0,表示进行写操作,经过从机应答后,再发送一个字节,第二个字节用来指定地址,这个数据就写入到从机的地址指针里了,也就是说从机接收到这个数据之后,它的寄存器指针就指向0x19这个位置了,之后要写入的数据,不给它发,直接再来个起始条件(start repeat),然后重新寻址并且指定读的标志位,此时读写标志位是1,表示要开始读了,接着主机接收一个字节,这个字节就是0x19地址下的数据,

进阶版本就是读写多个字节。

1.4 MPU6050简介

MPU6050是一个6轴姿态传感器,可以测量芯片自身XYZ轴的加速度、角速度参数,通过数据融合,可进一步得到姿态角,常应用于平衡车、飞行器等需要检测自身姿态的场景

3轴加速度计(Accelerometer):测量XYZ轴的加速度。具有静态稳定性,不具有动态稳定性

3轴陀螺仪传感器(Gyroscope):测量XYZ轴的角速度。具有动态稳定性,不具有静态稳定性

6轴 = 3轴加速度 + 3轴角速度

9轴 = 3轴加速度 + 3轴角速度 + 3轴磁场强度

10轴 = 3轴加速度 + 3轴角速度 + 3轴磁场强度 + 1气压强度

1.4.1 MPU6050参数

16ADC采集传感器的模拟信号,量化范围:-32768~32767

加速度计满量程选择:±2±4±8±16g

陀螺仪满量程选择: ±250±500±1000±2000°/sec

可配置的数字低通滤波器

可配置的时钟源

可配置的采样分频

I2C从机地址:110 1000AD0=0)(0x68需要左移,融入读写位:0xD0写地址,0xD1读地址)

                        110 1001(AD0=1)

1.4.2 硬件电路

左边是MPU6050的芯片,左下角是8针的排阵;左上角是LDO低压差线性稳压器。

XCL和XDA是扩展用的;

AD0接低电平,7位从机地址就是1101 000;接高电平的话,7位从机地址就是1101 001。(弱下拉)

这个MPU6050芯片的MDD供电是2.375-3.46V,加了稳压器,就可以在3.3~5V

 

引脚

功能

VCCGND

电源

SCLSDA

I2C通信引脚

XCLXDA

主机I2C通信引脚

AD0

从机地址最低位

INT

中断信号输出

1.4.3 MPU6050框图

左上角是时钟系统,有时钟输入脚和时钟输出脚,一般使用内部时钟。灰色的是传感器。

自测:自测响应。

手册

产品说明书

寄存器映像

采样频率分频器、配置寄存器、陀螺仪配置寄存器、加速度计配置寄存器

分别是:加速度计XYZ轴、温度传感器、陀螺仪XYZ轴的数据。_L表示低8位;_H表示高8位

分别是:电源管理寄存器1/2、器件ID号

采样频率分频器:分频越小,内部AD的转换就越快,数据寄存器刷新就越快,反之越慢。

配置寄存器

陀螺仪配置寄存器:高3位是XYZ自测使能位,中间两位是满量程位

自测响应范围

加速度计配置寄存器:高3位是XYZ自测使能位,中间两位是满量程位,后三位配置高通滤波器

加速度计的数据寄存器,直接读取数据寄存器即可,读出高8位和低8位,高8位左移8位或上低8位即可。

器件ID号

这个芯片上电默认是睡眠模式。

2 软件I2C读写MPU6050

完成软件I2C协议的时序和基于I2C协议读写寄存器来控制MPU6050。模块内部自带上拉电阻。

2.1 接线图

目前STM32是主机,MPU6050是从机。AD0修改从机地址的最低位(程序中调试)

框架:首先建立I2C通信层的.c和.h模块,在通信层,写好I2C底层的GPIO初始化和6个时序基本单元(起始,终止,发送一个字节,接收一个字节,发送应答,接收应答);再建立MPU6050的.c和.h模块,在这一层,基于I2C通信的模块,来实现指定地址的读和指定地址写,再实现写寄存器对芯片进行配置,读寄存器得到传感器数据。最后在main.c中调用MPU6050模块,初始化拿到数据,显示数据

2.2 模块封装

2.2.1 MyI2C

(1)I2C初始化函数

// I2C初始化
void MyI2C_Init(void)
{// 软件I2C只需要用GPIO的读写就可以了,不看库函数// 1把SCL和SDA初始化成开漏输出模式// 2把SCL和SDA置高电平/*开启时钟*/RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);		// 开启GPIOA的时钟/*GPIO初始化*/GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;			// 将PA1和PA2引脚初始化为开漏输出(输出低电平+浮空输入)GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOB, &GPIO_InitStructure);						/*设置GPIO初始化后的默认电平*/GPIO_SetBits(GPIOB, GPIO_Pin_10 | GPIO_Pin_11);				// 设置PA1和PA2引脚为高电平
}

(2)封装SCL和SD读写的函数

// 封装SCL的写函数
void MyI2C_W_SCL(uint8_t bitValue)
{GPIO_WriteBit(GPIOB, GPIO_Pin_10, (BitAction)bitValue);Delay_us(10);
}// 封装SDA的写函数
void MyI2C_W_SDA(uint8_t bitValue)
{GPIO_WriteBit(GPIOB, GPIO_Pin_11, (BitAction)bitValue);Delay_us(10);
}// 封装SDA的读函数
uint8_t MyI2C_R_SDA(void)
{uint8_t bitValue;bitValue = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11);Delay_us(10);return bitValue;
}

(3)起始条件

起始条件:SCL高电平期间,SDA从高电平切换到低电平。在I2C处于空闲时,SCL和SDA都处于高电平;SCL高电平期间,SDA从高电平切换到低电平,之后主机要再把SCL拽下来,一方面是占用这个总线,另一方面也是为了这些基本单元的拼接。(低电平开始,低电平结束)

// 起始条件
void MyI2C_Start(void)
{// 确保SCL和SDA都释放,先拉低SDA,再拉低SCLMyI2C_W_SDA(1);			// 释放SDA(在前)MyI2C_W_SCL(1);			// 释放SCLMyI2C_W_SDA(0);			// 先拉低SDAMyI2C_W_SCL(0);			// 再拉低SCL
}

(4)终止条件

终止条件:SCL高电平期间,SDA从低电平切换到高电平。终止条件:SCL先回弹到高电平,之后,SDA再回弹至高电平,这个上升沿触发终止条件。

// 终止条件
void MyI2C_Stop(void)
{// 先拉低SDA,再释放SCL,再释放SDAMyI2C_W_SDA(0);			// 先拉低SDAMyI2C_W_SCL(1);			// 释放SCLMyI2C_W_SDA(1);			// 释放SDA
}

(5)发送一个字节

主机拉低SCL,把数据放在SDA上,主机松开SCL,从机读取SDA上的数据,在SCL的同步下依次进行主机的发送和从机的接收。高位先行,所以第一位是第一个字节的最高位B7。SCL和SDA全程由主机掌控,从机只能被动读取。

// 发送一个字节
void MyI2C_SendByte(uint8_t byte)
{// 发送一个字节开始时,SCL是低电平// 趁SCL是低电平,先把byte的最高位放在SDA线上
//	MyI2C_W_SDA(byte & 0x80);	// 取决byte的最高位
//	MyI2C_W_SCL(1);				// 释放SCL
//	MyI2C_W_SCL(0);				// 拉低SCL
//	
//	MyI2C_W_SDA(byte & 0x40);	// 取决byte的次高位
//	MyI2C_W_SCL(1);				// 释放SCL
//	MyI2C_W_SCL(0);				// 拉低SCLuint8_t i = 0;for (i = 0; i < 8; i++){MyI2C_W_SDA(byte & (0x80 >> i));	// 取决byte的i高位MyI2C_W_SCL(1);						// 释放SCLMyI2C_W_SCL(0);						// 拉低SCL}
}

(6)接收一个字节

SCL低电平期间,从机将数据位依次放到SDA线上(高位先行),然后释放SCL,主机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可接收一个字节(主机在接收之前,需要释放SDA

// 接收一个字节
uint8_t MyI2C_ReceiveByte(void)
{uint8_t byte = 0x00,i = 0;// SCL低电平,SDA写数据,SCL高电平,读SDA的数据MyI2C_W_SDA(1);						// 释放SDA
//	MyI2C_W_SCL(1);						// 释放SCL
//	// 读数据
//	if (MyI2C_R_SDA() == 1)
//	{
//		byte |= 0x80;					// 把最高位置1
//	}
//	MyI2C_W_SCL(0);						// 拉低SCLfor (i = 0; i < 8; i++){MyI2C_W_SCL(1);						// 释放SCL// 读数据if (MyI2C_R_SDA() == 1){byte |= (0x80 >> i);			// 把高位置1}MyI2C_W_SCL(0);						// 拉低SCL}return byte;
}

(7)发送应答(发送一个字节的简化版)

// 发送应答(发送一个字节的简化版)
void MyI2C_SendAck(uint8_t ackByte)
{MyI2C_W_SDA(ackByte);			// 取决ackByteMyI2C_W_SCL(1);					// 释放SCLMyI2C_W_SCL(0);					// 拉低SCL
}

(8)接收应答(接收一个字节的简化版)

// 接收应答(接收一个字节的简化版)
uint8_t MyI2C_ReceiveAck(void)
{uint8_t ackByte;MyI2C_W_SDA(1);						// 释放SDAMyI2C_W_SCL(1);						// 释放SCLackByte = MyI2C_R_SDA();			// 读数据MyI2C_W_SCL(0);						// 拉低SCLreturn ackByte;
}

最终的MyI2C.c

#include "stm32f10x.h"                  // Device header
#include "Delay.h"// 封装SCL的写函数
void MyI2C_W_SCL(uint8_t bitValue)
{GPIO_WriteBit(GPIOB, GPIO_Pin_10, (BitAction)bitValue);Delay_us(10);
}// 封装SDA的写函数
void MyI2C_W_SDA(uint8_t bitValue)
{GPIO_WriteBit(GPIOB, GPIO_Pin_11, (BitAction)bitValue);Delay_us(10);
}// 封装SDA的读函数
uint8_t MyI2C_R_SDA(void)
{uint8_t bitValue;bitValue = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11);Delay_us(10);return bitValue;
}// I2C初始化
void MyI2C_Init(void)
{// 软件I2C只需要用GPIO的读写就可以了,不看库函数// 1把SCL和SDA初始化成开漏输出模式// 2把SCL和SDA置高电平/*开启时钟*/RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);		// 开启GPIOA的时钟/*GPIO初始化*/GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;			// 将PA1和PA2引脚初始化为开漏输出(输出低电平+浮空输入)GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOB, &GPIO_InitStructure);						/*设置GPIO初始化后的默认电平*/GPIO_SetBits(GPIOB, GPIO_Pin_10 | GPIO_Pin_11);				// 设置PA1和PA2引脚为高电平
}// 起始条件
void MyI2C_Start(void)
{// 确保SCL和SDA都释放,先拉低SDA,再拉低SCLMyI2C_W_SDA(1);			// 释放SDA(在前)MyI2C_W_SCL(1);			// 释放SCLMyI2C_W_SDA(0);			// 先拉低SDAMyI2C_W_SCL(0);			// 再拉低SCL
}// 终止条件
void MyI2C_Stop(void)
{// 先拉低SDA,再释放SCL,再释放SDAMyI2C_W_SDA(0);			// 先拉低SDAMyI2C_W_SCL(1);			// 释放SCLMyI2C_W_SDA(1);			// 释放SDA
}// 发送一个字节
void MyI2C_SendByte(uint8_t byte)
{// 发送一个字节开始时,SCL是低电平// 趁SCL是低电平,先把byte的最高位放在SDA线上
//	MyI2C_W_SDA(byte & 0x80);	// 取决byte的最高位
//	MyI2C_W_SCL(1);				// 释放SCL
//	MyI2C_W_SCL(0);				// 拉低SCL
//	
//	MyI2C_W_SDA(byte & 0x40);	// 取决byte的次高位
//	MyI2C_W_SCL(1);				// 释放SCL
//	MyI2C_W_SCL(0);				// 拉低SCLuint8_t i = 0;for (i = 0; i < 8; i++){MyI2C_W_SDA(byte & (0x80 >> i));	// 取决byte的i高位MyI2C_W_SCL(1);						// 释放SCLMyI2C_W_SCL(0);						// 拉低SCL}
}// 接收一个字节
uint8_t MyI2C_ReceiveByte(void)
{uint8_t byte = 0x00,i = 0;// SCL低电平,SDA写数据,SCL高电平,读SDA的数据MyI2C_W_SDA(1);						// 释放SDA
//	MyI2C_W_SCL(1);						// 释放SCL
//	// 读数据
//	if (MyI2C_R_SDA() == 1)
//	{
//		byte |= 0x80;					// 把最高位置1
//	}
//	MyI2C_W_SCL(0);						// 拉低SCLfor (i = 0; i < 8; i++){MyI2C_W_SCL(1);						// 释放SCL// 读数据if (MyI2C_R_SDA() == 1){byte |= (0x80 >> i);			// 把高位置1}MyI2C_W_SCL(0);						// 拉低SCL}return byte;
}// 发送应答(发送一个字节的简化版)
void MyI2C_SendAck(uint8_t ackByte)
{MyI2C_W_SDA(ackByte);			// 取决ackByteMyI2C_W_SCL(1);					// 释放SCLMyI2C_W_SCL(0);					// 拉低SCL
}// 接收应答(接收一个字节的简化版)
uint8_t MyI2C_ReceiveAck(void)
{uint8_t ackByte;MyI2C_W_SDA(1);						// 释放SDAMyI2C_W_SCL(1);						// 释放SCLackByte = MyI2C_R_SDA();			// 读数据MyI2C_W_SCL(0);						// 拉低SCLreturn ackByte;
}

测试从机是否给应答

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "MyI2C.h"int main()
{OLED_Init();								// 初始化OLED// 测试从机是否给应答MyI2C_Init();// 指定地址写MyI2C_Start();							// 起始条件// 测试总线上是否有从机
//	MyI2C_SendByte(0xD0);					// 1101 000 0  寻址
//	//	MyI2C_SendByte(0xA1);					// 不存在,因此发送应答是001
//	uint8_t ack = MyI2C_ReceiveAck();		// 000/001
//	OLED_ShowHexNum(1, 1, ack, 3);// 测试AD0引脚改名// 飞线AD0接到VCC(1101 0010)上,0xD0就不应答了(001),MyI2C_SendByte(0xD0);uint8_t ack = MyI2C_ReceiveAck();OLED_ShowHexNum(1, 1, ack, 3);while (1){}
}
2.2.2 MPU6050

这个模块建立在MyI2C之上的

MPU6050.c

#include "stm32f10x.h"                  // Device header
#include "MyI2C.h"
#include "MPU6050_Reg.h"#define MPU6050_ADDRESS 	0xD0// 指定地址写
// 参数是8位地址和8位数据
void MPU6050_WriteReg(uint8_t regAddress, uint8_t data)
{MyI2C_Start();								// i2c起始MyI2C_SendByte(MPU6050_ADDRESS);			// 发送MPU6050的地址MyI2C_ReceiveAck();							// 接收应答MyI2C_SendByte(regAddress);					// 发送指定寄存器地址MyI2C_ReceiveAck();							// 接收应答MyI2C_SendByte(data);						// 指定写入寄存器的数据 MyI2C_ReceiveAck();							// 接收应答MyI2C_Stop();								// 终止
}// 指定地址读
uint8_t MPU6050_ReadReg(uint8_t regAddress)
{uint8_t data;MyI2C_Start();								// i2c起始MyI2C_SendByte(MPU6050_ADDRESS);			// 发送MPU6050的地址MyI2C_ReceiveAck();							// 接收应答MyI2C_SendByte(regAddress);					// 发送指定寄存器地址MyI2C_ReceiveAck();							// 接收应答MyI2C_Start();								// i2c起始MyI2C_SendByte(MPU6050_ADDRESS | 0x01);		// 读数据 1101 0000写/1101 0001读MyI2C_ReceiveAck();							// 接收应答data = MyI2C_ReceiveByte();					// 总线控制权给从机,从机开始发送一个字节MyI2C_SendAck(0);							// 发送应答MyI2C_Stop();								// 终止return data;
}// 初始化MPU6050
void MPU6050_Init(void)
{MyI2C_Init();								// 初始化I2C
}

先小小的测试一下

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "MPU6050.h"int main()
{OLED_Init();								// 初始化OLEDMPU6050_Init();// 读取芯片的ID号
//	uint8_t id = MPU6050_ReadReg(0x75);			// 地址是0x75
//	OLED_ShowHexNum(1, 1, id, 2);				// ID号0x68// 写寄存器需要解除芯片的睡眠模式MPU6050_WriteReg(0x6B, 0x00);				// 电源管理寄存器1MPU6050_WriteReg(0x19, 0xAA);				// 采样率分频寄存器,地址是0x19,值是0XAAuint8_t id = MPU6050_ReadReg(0x19);OLED_ShowHexNum(2, 1, id, 2);				// 0xAAwhile (1){}
}

宏定义寄存器地址

#ifndef __MPU6050_REG_H__
#define __MPU6050_REG_H__#define	MPU6050_SMPLRT_DIV		0x19
#define	MPU6050_CONFIG			0x1A
#define	MPU6050_GYRO_CONFIG		0x1B
#define	MPU6050_ACCEL_CONFIG	0x1C#define	MPU6050_ACCEL_XOUT_H	0x3B
#define	MPU6050_ACCEL_XOUT_L	0x3C
#define	MPU6050_ACCEL_YOUT_H	0x3D
#define	MPU6050_ACCEL_YOUT_L	0x3E
#define	MPU6050_ACCEL_ZOUT_H	0x3F
#define	MPU6050_ACCEL_ZOUT_L	0x40
#define	MPU6050_TEMP_OUT_H		0x41
#define	MPU6050_TEMP_OUT_L		0x42
#define	MPU6050_GYRO_XOUT_H		0x43
#define	MPU6050_GYRO_XOUT_L		0x44
#define	MPU6050_GYRO_YOUT_H		0x45
#define	MPU6050_GYRO_YOUT_L		0x46
#define	MPU6050_GYRO_ZOUT_H		0x47
#define	MPU6050_GYRO_ZOUT_L		0x48#define	MPU6050_PWR_MGMT_1		0x6B
#define	MPU6050_PWR_MGMT_2		0x6C
#define	MPU6050_WHO_AM_I		0x75#endif

继续完善MPU6050.c

#include "stm32f10x.h"                  // Device header
#include "MyI2C.h"
#include "MPU6050_Reg.h"#define MPU6050_ADDRESS 	0xD0// 指定地址写
// 参数是8位地址和8位数据
void MPU6050_WriteReg(uint8_t regAddress, uint8_t data)
{MyI2C_Start();								// i2c起始MyI2C_SendByte(MPU6050_ADDRESS);			// 发送MPU6050的地址MyI2C_ReceiveAck();							// 接收应答MyI2C_SendByte(regAddress);					// 发送指定寄存器地址MyI2C_ReceiveAck();							// 接收应答MyI2C_SendByte(data);						// 指定写入寄存器的数据 MyI2C_ReceiveAck();							// 接收应答MyI2C_Stop();								// 终止
}// 指定地址读
uint8_t MPU6050_ReadReg(uint8_t regAddress)
{uint8_t data;MyI2C_Start();								// i2c起始MyI2C_SendByte(MPU6050_ADDRESS);			// 发送MPU6050的地址MyI2C_ReceiveAck();							// 接收应答MyI2C_SendByte(regAddress);					// 发送指定寄存器地址MyI2C_ReceiveAck();							// 接收应答MyI2C_Start();								// i2c起始MyI2C_SendByte(MPU6050_ADDRESS | 0x01);		// 读数据 1101 0000写/1101 0001读MyI2C_ReceiveAck();							// 接收应答data = MyI2C_ReceiveByte();					// 总线控制权给从机,从机开始发送一个字节MyI2C_SendAck(1);							// 发送应答// 读取最后一个字节给非应答MyI2C_SendAck(0),之前都给应答MyI2C_Stop();								// 终止return data;
}// 初始化MPU6050
// 配置的是:解除睡眠,选择陀螺仪时钟,6个轴均不待机,采样分频位10,滤波参数最大
// 陀螺仪和加速度计都选择最大量程
void MPU6050_Init(void)
{MyI2C_Init();													// 初始化I2CMPU6050_WriteReg(MPU6050_PWR_MGMT_1, 0x01);						// 配置电源管理寄存器1,结合数据手册写MPU6050_WriteReg(MPU6050_PWR_MGMT_2, 0x00);						// 配置电源管理寄存器2,结合数据手册写MPU6050_WriteReg(MPU6050_SMPLRT_DIV, 0x09);						// 采样率分频寄存器,10分频MPU6050_WriteReg(MPU6050_CONFIG, 0x06);							// 配置寄存器MPU6050_WriteReg(MPU6050_GYRO_CONFIG, 0x18);					// 陀螺仪配置寄存器MPU6050_WriteReg(MPU6050_ACCEL_CONFIG, 0x18);					// 加速度计寄存器	
}// 获取数据寄存器,返回6个数据,形参是指针
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_ReadReg(MPU6050_ACCEL_XOUT_H);							// 读取加速度寄存器X轴的高8位dataL = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L);							// 读取加速度寄存器X轴的低8位*accX = (dataH << 8) | dataL;											// 	通过指针返回回去dataH = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_H);							// 读取加速度寄存器y轴的高8位dataL = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_L);							// 读取加速度寄存器y轴的低8位*accY = (dataH << 8) | dataL;											// 	通过指针返回回去dataH = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_H);							// 读取加速度寄存器z轴的高8位dataL = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_L);							// 读取加速度寄存器z轴的低8位*accZ = (dataH << 8) | dataL;											// 	通过指针返回回去dataH = MPU6050_ReadReg(MPU6050_GYRO_XOUT_H);							// 读取陀螺仪寄存器x轴的高8位dataL = MPU6050_ReadReg(MPU6050_GYRO_XOUT_L);							// 读取陀螺仪寄存器x轴的低8位*GyroX = (dataH << 8) | dataL;											// 	通过指针返回回去dataH = MPU6050_ReadReg(MPU6050_GYRO_YOUT_H);							// 读取陀螺仪寄存器y轴的高8位dataL = MPU6050_ReadReg(MPU6050_GYRO_YOUT_L);							// 读取陀螺仪寄存器y轴的低8位*GyroY = (dataH << 8) | dataL;											// 	通过指针返回回去dataH = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_H);							// 读取陀螺仪寄存器z轴的高8位dataL = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_L);							// 读取陀螺仪寄存器z轴的低8位*GyroZ = (dataH << 8) | dataL;											// 	通过指针返回回去
}// 获取ID号
uint8_t MPU6050_GetID(void)
{return MPU6050_ReadReg(MPU6050_WHO_AM_I);
}

2.3 主函数

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "MPU6050.h"int16_t AX, AY, AZ, GX, GY, GZ;int main()
{OLED_Init();								// 初始化OLEDMPU6050_Init();OLED_ShowString(1, 1, "ID:");OLED_ShowHexNum(1, 4, MPU6050_GetID(), 6);while (1){MPU6050_GetData(&AX, &AY, &AZ, &GX, &GY, &GZ);OLED_ShowSignedNum(2, 1, AX, 5);OLED_ShowSignedNum(3, 1, AY, 5);OLED_ShowSignedNum(4, 1, AZ, 5);OLED_ShowSignedNum(2, 8, GX, 5);OLED_ShowSignedNum(3, 8, GX, 5);OLED_ShowSignedNum(4, 8, GX, 5);}
}

现象:

读取的数据x / 32768 = x / 满量程

x = 1913/32768*16=0.93408203125g/m2。重力加速度。其他的数据也是如此计算。

3 I2C通信外设

简单应用使用软件I2C,对性能有要求,使用硬件I2C

3.1 I2C外设简介

STM32内部集成了硬件I2C收发电路,可以由硬件自动执行时钟生成、起始终止条件生成、应答位收发、数据收发等功能,减轻CPU的负担

支持多主机模型(STM32使用的是可变多主机,谁要做主机就跳出来,一视同仁

支持7/10位地址模式

7位地址的模式就是前文中的,起始条件之后,紧跟着的一个字节必须是7位地址+读写位。只有128位,数量少。

I2C起始的第一个字节必须是寻址+读写位。

10位地址:起始之后的前两个字节,都作为寻址。第一个字节有7个空位,第二个字节有8个空位,加起来15个空位,多出的5位做标志位了(11110)。如果第二个字节也是寻址,那第一个字节的前5位就必须是11110,此时第一个字节剩下的2位+第二个字节的8位都作为寻址,就是10位寻址了。​​​​​​​

支持不同的通讯速度,标准速度(高达100 kHz),快速(高达400 kHz)

支持DMA

兼容SMBus协议(system management bus,系统管理总线,主要用于电源管理中)

STM32F103C8T6 硬件I2C资源:I2C1、I2C2

3.1.1 I2C外设框图

坐标是这个外设的通信引脚SDA和SCL,下面的SMBALERT是SMBus使用的。这个引脚一般都是借用GPIO的复用模式与外界相连的。

接着看I2C的功能框图

上面是数据控制部分,其中核心的是数据寄存器和数据移位寄存器。当需要发送数据的时候,可以把一个字节的数据写到数据寄存器DR中,当移位寄存器没有数据移位时,数据寄存器的值就会进一步转到移位寄存器里,在移位的过程中就可以直接把下一个数据放到数据寄存器里了,一旦前一个数据移位完成,下一个数据就可以无缝衔接,继续发送。当数据由数据寄存器转到移位寄存器的时候,就会置状态寄存器TXE位为1,表示发送寄存器位空,这是发送的流程。在接收的时候,也是这一路,输入的数据一位一位的从引脚移入到移位寄存器,当一个字节的数据收齐之后,数据就整体从移位寄存器转到数据寄存器,同时置标志位RXNE,表示接收寄存器非空,这时就可以把数据从数据寄存器读出来。(与串口不同,这里是半双工,串口是全双工)。需要写入控制寄存器的对应位来应对何时读何时写。

STM32作为从机的时候使用,被主机召唤,自身需要地址。

下面是SCL时钟控制。

3.1.2 I2C基本模型图

首先移位寄存器和数据寄存器的相互配合是通信的核心部分,I2C是高位先行,所以这个移位寄存器是向左移位。一个SCL时钟移位一次,移位8次就移一个字节。接收的时候,数据通过GPIO口从右边以此移进来,最终移8次,一个字节的接收就完成了。GPIO口需要配置成复用开漏输出模式(复用就是GPIO的状态交由片上外设来控制的,开漏输出就是I2C协议要求的端口配置模式)。

3.1.3 主机发送

当STM32想要执行指定地址写的时候,就按照这个图来进行。这里有7位地址的主发送和10位地址的主发送,区别是:7位地址起始条件之后的一个字节是寻址,10位地址起始条件后的两个字节是寻址(其中前一个字节这里写的是帧头,内容是5位的标志位11110+2位地址+1位读写位),后一个字节的内容就是纯粹的8位地址了,两个字节加一起构成了10位寻址。主要关注7位

7位地址:流程是起始、从机地址、应答,后面是数据1,应答,数据2,应答,......,数据N,应答,终止。(MPU6050规定的是数据1是指定寄存器地址,数据2是指定寄存器地址下的数据)。

这是一个典型的指定地址写的时序流程。首先初始化之后,总线默认是空闲状态,STM32默认是从模式,为了产生一个起始条件,STM32要写入控制寄存器(查手册);

之后STM32由从模式转为主模式,也就是多主机模型下STM32有数据要发送,就要跳出来。控制玩硬件电路之后,检查标志位,看看硬件有没有达到想要的状态。把EV5事件当作标志位;

检测起始条件已经发送时,就可以发送一个字节的从机地址了,从机地址需要写到数据寄存器DR中,写入DR之后,硬件电路就会自动地把这一个字节转到移位寄存器里,再把这一个字节发送到IIC总线上,之后硬件会自动接收应答位并判断,如果没有应答,硬件就会置应答失败的标志位,这个标志位可以申请中断来提醒我们。在寻址完成之后,会发生EV6事件,EV8_1事件,

3.1.4 主机接收

当前地址读的格式

3.1.5 软件/硬件波形对比

4 硬件I2C读写MPU6050

4.1 接线图

和软件I2C读写MPU6050一样

4.2 模块封装

1配置I2C外设,对I2C2外设进行初始化
(1)开启I2C外设和对应GPIO的时钟
(2)把I2C外设对应的GPIO口初始化为复用开漏模式
(3)使用结构体对整个I2C进行配置
(4)I2C_Cmd,使能I2

2控制外设电路,实现指定地址写的时序

3控制外设电路,实现指定地址读的时序

库函数

void I2C_DeInit(I2C_TypeDef* I2Cx);
void I2C_Init(I2C_TypeDef* I2Cx, I2C_InitTypeDef* I2C_InitStruct);
void I2C_StructInit(I2C_InitTypeDef* I2C_InitStruct);
void I2C_Cmd(I2C_TypeDef* I2Cx, FunctionalState NewState);// I2C生成起始条件/生成终止条件
void I2C_GenerateSTART(I2C_TypeDef* I2Cx, FunctionalState NewState);
void I2C_GenerateSTOP(I2C_TypeDef* I2Cx, FunctionalState NewState);
// 配置在收到一个字节后,是否给从机应答
void I2C_AcknowledgeConfig(I2C_TypeDef* I2Cx, FunctionalState NewState);
// 发送数据,写数据到数据寄存器DR中
void I2C_SendData(I2C_TypeDef* I2Cx, uint8_t Data);
// 接收数据,读取DR寄存器的数据
uint8_t I2C_ReceiveData(I2C_TypeDef* I2Cx);
// 发送7位地址
void I2C_Send7bitAddress(I2C_TypeDef* I2Cx, uint8_t Address, uint8_t I2C_Direction);
// 状态监控
I2C_CheckEvent();

MPU6050.c

#include "stm32f10x.h"                  // Device header
#include "MPU6050_Reg.h"#define MPU6050_ADDRESS 	0xD0// 封装I2C_CheckEvent函数,加上超时,防止卡死
void MPU6050_CheckEvent(I2C_TypeDef* I2Cx, uint32_t I2C_EVENT)
{uint32_t timeOut = 10000;while (I2C_CheckEvent(I2Cx, I2C_EVENT) != SUCCESS){timeOut--;if (timeOut == 0){break;}}
}// 指定地址写
// 参数是8位地址和8位数据
void MPU6050_WriteReg(uint8_t regAddress, uint8_t data)
{// 2控制外设电路,实现指定地址写的时序I2C_GenerateSTART(I2C2, ENABLE);						// 生成起始条件// 等待EV5事件
//	while (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT) != SUCCESS);MPU6050_CheckEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);// 发送从机地址,自带接收应答,同样接收数据也自带发送应答I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter);// 等待EV6事件MPU6050_CheckEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);I2C_SendData(I2C2, regAddress);			// 直接写入DR,发送数据// 等待EV8事件MPU6050_CheckEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTING);I2C_SendData(I2C2, data);				// 直接写入DR,发送数据// 连续发送过程等待EV8,最后一位等待EV8_2事件MPU6050_CheckEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED);I2C_GenerateSTOP(I2C2, ENABLE);				// 生成终止条件
}// 指定地址读
uint8_t MPU6050_ReadReg(uint8_t regAddress)
{// 3控制外设电路,实现指定地址读的时序I2C_GenerateSTART(I2C2, ENABLE);						// 生成起始条件// 等待EV5事件MPU6050_CheckEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);// 发送从机地址,自带接收应答,同样接收数据也自带发送应答I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter);// 等待EV6事件MPU6050_CheckEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);I2C_SendData(I2C2, regAddress);			// 直接写入DR,发送数据// 等待EV8事件MPU6050_CheckEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED);I2C_GenerateSTART(I2C2, ENABLE);						// 生成起始条件// 等待EV5事件MPU6050_CheckEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);// 接收数据也自带发送应答I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Receiver);// 等待EV6事件MPU6050_CheckEvent(I2C2, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED);I2C_AcknowledgeConfig(I2C2, DISABLE);					// 配置ACK位I2C_GenerateSTOP(I2C2, ENABLE);							// 生成终止条件// 等待EV7事件MPU6050_CheckEvent(I2C2, I2C_EVENT_MASTER_BYTE_RECEIVED);uint8_t data = I2C_ReceiveData(I2C2);I2C_AcknowledgeConfig(I2C2, ENABLE);					// 配置ACK位return data;
}// 初始化MPU6050
// 配置的是:解除睡眠,选择陀螺仪时钟,6个轴均不待机,采样分频位10,滤波参数最大
// 陀螺仪和加速度计都选择最大量程
void MPU6050_Init(void)
{
//	MyI2C_Init();													// 初始化I2C// 1配置I2C外设,对I2C2外设进行初始化// (1)开启I2C外设和对应GPIO的时钟RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2, ENABLE);RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);// (2)把I2C外设对应的GPIO口初始化为复用开漏模式GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOB, &GPIO_InitStructure);	// (3)使用结构体对整个I2C进行配置I2C_InitTypeDef I2C_InitStructure;I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;						// I2C的模式I2C_InitStructure.I2C_ClockSpeed = 50000;						// 时钟速度I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2; 				// 时钟占空比,小于于100KHz是1:1I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;						// ACK应答位I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;		// 7位地址I2C_InitStructure.I2C_OwnAddress1 = 0x00;						// 自身地址,用不到I2C_Init(I2C2, &I2C_InitStructure);// (4)I2C_Cmd,使能I2CI2C_Cmd(I2C2, ENABLE);MPU6050_WriteReg(MPU6050_PWR_MGMT_1, 0x01);						// 配置电源管理寄存器1,结合数据手册写MPU6050_WriteReg(MPU6050_PWR_MGMT_2, 0x00);						// 配置电源管理寄存器2,结合数据手册写MPU6050_WriteReg(MPU6050_SMPLRT_DIV, 0x09);						// 采样率分频寄存器,10分频MPU6050_WriteReg(MPU6050_CONFIG, 0x06);							// 配置寄存器MPU6050_WriteReg(MPU6050_GYRO_CONFIG, 0x18);					// 陀螺仪配置寄存器MPU6050_WriteReg(MPU6050_ACCEL_CONFIG, 0x18);					// 加速度计寄存器	
}// 获取数据寄存器,返回6个数据,形参是指针
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_ReadReg(MPU6050_ACCEL_XOUT_H);							// 读取加速度寄存器X轴的高8位dataL = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L);							// 读取加速度寄存器X轴的低8位*accX = (dataH << 8) | dataL;											// 	通过指针返回回去dataH = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_H);							// 读取加速度寄存器y轴的高8位dataL = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_L);							// 读取加速度寄存器y轴的低8位*accY = (dataH << 8) | dataL;											// 	通过指针返回回去dataH = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_H);							// 读取加速度寄存器z轴的高8位dataL = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_L);							// 读取加速度寄存器z轴的低8位*accZ = (dataH << 8) | dataL;											// 	通过指针返回回去dataH = MPU6050_ReadReg(MPU6050_GYRO_XOUT_H);							// 读取陀螺仪寄存器x轴的高8位dataL = MPU6050_ReadReg(MPU6050_GYRO_XOUT_L);							// 读取陀螺仪寄存器x轴的低8位*GyroX = (dataH << 8) | dataL;											// 	通过指针返回回去dataH = MPU6050_ReadReg(MPU6050_GYRO_YOUT_H);							// 读取陀螺仪寄存器y轴的高8位dataL = MPU6050_ReadReg(MPU6050_GYRO_YOUT_L);							// 读取陀螺仪寄存器y轴的低8位*GyroY = (dataH << 8) | dataL;											// 	通过指针返回回去dataH = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_H);							// 读取陀螺仪寄存器z轴的高8位dataL = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_L);							// 读取陀螺仪寄存器z轴的低8位*GyroZ = (dataH << 8) | dataL;											// 	通过指针返回回去
}// 获取ID号
uint8_t MPU6050_GetID(void)
{return MPU6050_ReadReg(MPU6050_WHO_AM_I);
}

4.3 主函数

同2.3,现象也和2.3一样

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/229708.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

20231228在Firefly的AIO-3399J开发板的Android11的挖掘机的DTS配置单前置摄像头ov13850

20231228在Firefly的AIO-3399J开发板的Android11的挖掘机的DTS配置单前置摄像头ov13850 2023/12/28 10:42 【碰到一个很神奇的问题】&#xff1a; 昨天晚上前置摄像头怎么也点不亮&#xff01;改了巨多的地方&#xff01;晚上睡觉之前把开发板彻底断电了&#xff01;今天开电脑…

深度生成模型之GAN基础 ->(个人学习记录笔记)

文章目录 深度生成模型之GAN基础生成对抗网络1. 生成对抗网络如何生成数据2. 生成对抗原理3. GAN的核心优化目标4. D的优化5. GAN的理想状态6. GAN的训练7. 梯度不稳定与模式崩塌(collapse mode)问题8. 梯度消失问题 深度生成模型之GAN基础 生成对抗网络 1. 生成对抗网络如何…

深入研究矫正单应性矩阵用于立体相机在线自标定

文章&#xff1a;Dive Deeper into Rectifying Homography for Stereo Camera Online Self-Calibration 作者&#xff1a;Hongbo Zhao, Yikang Zhang, Qijun Chen,, and Rui Fan 编辑&#xff1a;点云PCL 欢迎各位加入知识星球&#xff0c;获取PDF论文&#xff0c;欢迎转发朋…

【Java并发】深入浅出 synchronized关键词原理-上

一个问题的思考 建设我们有两个线程&#xff0c;一个进行5000次的相加操作&#xff0c;另一个进行5000次的减操作。那么最终结果是多少 package com.jia.syn;import java.util.concurrent.TimeUnit;/*** author qxlx* date 2024/1/2 10:08 PM*/ public class SynTest {privat…

稳部落 – 新浪微博备份导出工具

稳部落 稳部落是新浪微博备份导出工具&#xff0c;可以帮助用户非常方便的导出备份新浪微博的数据&#xff0c;让我们可以永久保存这些微博数据。它支持新浪微博、微博私信、微博评论的导出&#xff0c;并可以备份包含图片、视频的完整微博内容。用户只需登录微博账号&#xf…

Java学习,一文掌握Java之SpringBoot框架学习文集(2)

&#x1f3c6;作者简介&#xff0c;普修罗双战士&#xff0c;一直追求不断学习和成长&#xff0c;在技术的道路上持续探索和实践。 &#x1f3c6;多年互联网行业从业经验&#xff0c;历任核心研发工程师&#xff0c;项目技术负责人。 &#x1f389;欢迎 &#x1f44d;点赞✍评论…

【计算机毕业设计】SSM游戏点评网站

项目介绍 本项目分为前后台&#xff0c;前台为普通用户登录&#xff0c;后台为管理员登录&#xff1b; 管理员角色包含以下功能&#xff1a; 管理员登录,管理员管理,网站用户管理,游戏资讯管理,游戏类型管理,城市信息管理,竞技场管理,游戏信息管理,游戏评价信息管理等功能。…

双侧电源系统距离保护MATLAB仿真模型

微❤关注“电气仔推送”获得资料&#xff08;专享优惠&#xff09; 系统原始数据 双侧电源系统模型如图所示&#xff1a; 仿真模型搭建 将线路AB分成Line1和Line2&#xff0c;将线路BC分成Line3和Line4&#xff0c;用三相电压电流测量模块作为系统母线&#xff0c;根据系统已…

Java基础-----集合类(一)

文章目录 1.集合类简介2. 自定义集合类 1.集合类简介 集合和数组一样&#xff0c;都是用来存储多个数据的结构&#xff0c;也可以称作容器。 数组长度是不可变化的&#xff0c;一旦在初始化数组时指定了数组长度&#xff0c;这个长度就不可变。如果需要处理数量变化的数据&am…

Flutter 混合开发 - 动态下发 libflutter.so libapp.so

背景 最近在做包体积优化&#xff0c;在完成代码混淆、压缩&#xff0c;裁剪ndk支持架构&#xff0c;以及资源压缩&#xff08;如图片转webp、mp3压缩等&#xff09;后发现安装包的中占比较大的仍是 so 动态库依赖。 具体查看发现 libflutter.so 和 libapp.so 的体积是最大的&…

探索Java的魅力

从本篇文章开始&#xff0c;小编准备写一个关于java基础学习的系列文章&#xff0c;文章涉及到java语言中的基础组件、实现原理、使用场景、代码案例。看完下面一系列文章&#xff0c;希望能加深你对java的理解。 本篇文章作为本系列的第一篇文章&#xff0c;主要介绍一些java…

【数据库原理】(6)关系数据库的关系操作集合

基本关系操作 关系数据操作的对象都是关系,其操作结果仍为关系,即集合式操作。关系数据库的操作可以分为两大类&#xff1a;数据查询和数据更新。这些操作都是基于数学理论&#xff0c;特别是集合理论。下面是对这些基本操作的解释和如何用不同的关系数据语言来表达这些操作的…

STM32入门教程-2023版【3-2】推挽输出和开漏输出驱动问题

关注 点赞 不错过精彩内容 大家好&#xff0c;我是硬核王同学&#xff0c;最近在做免费的嵌入式知识分享&#xff0c;帮助对嵌入式感兴趣的同学学习嵌入式、做项目、找工作! 二、正式点亮一个LED灯 &#xff08;4&#xff09;推挽输出和开漏输出驱动问题 把LED的正负极对换&…

react useEffect 内存泄漏

componentWillUnmount() {this.setState (state, callback) > {return;};// 清除reactionthis.reaction();}useEffect 使用AbortController useEffect(() > { let abortController new AbortController(); // your async action is here return () > { abortCo…

008、所有权

所有权可以说是Rust中最为独特的一个功能了。正是所有权概念和相关工具的引入&#xff0c;Rust才能够在没有垃圾回收机制的前提下保障内存安全。 因此&#xff0c;正确地了解所有权概念及其在Rust中的实现方式&#xff0c;对于所有Rust开发者来讲都是十分重要的。在本文中&…

添加 Android App Links

添加 Android App Links功能 介绍一个简单的效果Android配置Add Url intent filtersAdd logic to handle the intentAssociate website 搭建网页支持AppLinks 介绍 Android App Links 是指将用户直接转到 Android 应用内特定内容的 HTTP 网址。Android App Links 可为您的应用带…

计算机网络--作业

作业一 1、比较电路交换、报文交换和分组报文交换优缺点 电路交换 电路交换是以电路连接为目的的交换方式&#xff0c;通信之前要在通信双方之间建立一条被双方独占的物理通道&#xff08;由通信双方之间的交换设备和链路逐段连接而成&#xff09;。 优点&#xff1a; ①由于…

SpringSecurity-2.7中跨域问题

SpringSecurity-2.7中跨域问题 访问测试 起因 写这篇的起因是会了解到 SSM(CrosOrigin)解决跨域,但是会在加入SpringSecurity配置后,这个跨域解决方案就失效了,而/login这个请求上是无法添加这个注解或者通过配置(WebMvcConfig)去解决跨域,所以只能使用SpringSecurity提供的.c…

MySQL的安装网络配置

目录 一. MySQL5.7的安装 二. MySQL8.0的安装 三. 配置网络访问 思维导图 一. MySQL5.7的安装 1. 解压 2. 将my.ini文件放入到解压文件中 3. 编辑my.ini文件&#xff0c;将路径改为当前路径 4. 进到bin目录下&#xff0c;以管理员身份打开cmd命令窗口 5. 安装MySQL服务 my…

C++Qt6 哈夫曼编码求解 数据结构课程设计 | JorbanS

一、 问题描述 在进行程序设计时&#xff0c;通常给每一个字符标记一个单独的代码来表示一组字符&#xff0c;即编码。在进行二进制编码时&#xff0c;假设所有的代码都等长&#xff0c;那么表示 n 个不同的字符需要 位&#xff0c;称为等长编码。如果每个字符的使用频率相等&…