14、I2C通信协议原理和MPU6050

一、串口通讯

  • 只能在两个设备之间进行
  • 若要三台设备两两通信,则每个设备得需要两组窗口,为3组相互独立的窗口通讯
  • 为解决这个问题:设计了总线通讯,有多种,I2C为其中一种

img

二、I2C通信

(1)特点

1、同步、半双工

2、带数据应答

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

4、可以是软件IC和硬件IC

(2)时钟线和数据线

img

1、主机:任何情况下,主机完全掌握SCL线,在空闲状态下,主机可以主动发起对SDA控制,只有在从机发送数据和从机应答时,主机才会转交SDA的控制权给从机

2、SCL时钟线在任何时刻都只能被动的读取,从机不允许控制SCL线,
3、SDA数据线,从机不允许主动发起对SDA的控制,只有在主机发送读取从机的命令后,从机应答的时候,从机才能短暂的获取SDA的控制权

4、主机拥有SCL的绝对控制权,所以主机的SCL可以配置成推挽输出,所有从机的SCL都配置成浮空输入或者上拉输入

5、为了实现输出输入半双工,避免总线没协调好导致电源短路,I2C规定禁止所有设备输出强上拉的高电平,采用外置若上拉电阻加开漏输出的电路

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

(3)SCL和SDA的状态

  • 当SCL和SDA都为高电平,为空闲状态时(起始和终止都是由主机产生的,故空闲时,从机始终放开)
  • 当SCL为高电平,SDA为下降沿的的状态时,为开始发送数据,起始发送数据完成

  • 当SCL为高电平,SDA为上升沿的的状态时,为数据发送完成

发送数据的过程为下图

img

当时钟线为高电平时,数据线上的数据必须保持稳定,比如时钟线为高时,数据线上的数据始终为高,完成逻辑1的传输,保持低电平则为0。(主机在接受之前,需要释放SDA,释放SDA相当于切换成输入模式,所有设备和主机都处于输入模式,当主机需要发送时,就可以主动去拉低SDA,而主机在接收的时候,必须主动释放SDA)

(4)例子:单片机向从设备写信息

img

假设上述是主机给24C02发送数据:
由上图可知:
24C02的设备地址位1010 000,主机在24C02的 0000 0000的存储位置写入0000 1111的数据

(1)读写数据位:读数据置1,写数据置0

(2)第一个应答信号:信号时由从机发送给主机,如果从机收到之前的信息,回复0,没有收到或者(主机)读取接收完成回复1

(3)第二个应答信号:单片机需要存储器返回一个应答信号

(4)第三个应答信号:发送完数据后,需要再给主机发送应答信号0,告诉主机写入成功

(5)最后写入停止位:SCL为高电平,SDA为上升沿

(5)读数据帧

img

  • 前半部分:指定地址写(但没来得及写),后半部分,指定地址读

  • 首先写入设备地址,然后写数据,

  • 接下来写寄存器的地址,在收到从机的应答信号之后,主机再发送一个起始号,

  • 再发送一遍设备地址,然后才能发送读数据,接下来,存储器会把寄存器中的数据发送给单片机。

  • 最后一部分的数据可以多来几个,就可以写多个数据,地址指针在读后会自增,就可以连续读出一片区域的寄存器,效率也会变高。

  • 主机给应答:从机就会继续发,主机给非应答,从机不会再法发,交出SDA的控制权,从机控制SDA发送一个字节的权力,开始于读写标志位1,结束于主机给应答位为1

(6)开漏输出和推挽输出

img

任何设备在任何时刻都可以输入,但在输出部分,采用的是开漏输出
推挽输出:上面一个开关管接到正极,下面一个开关管接到负极,上面导通输出高电平,下面导通输出低电平,因为这是通过开关管直接接到正极和负极的,所以这个是强上拉和强下拉的模式
开漏输出:去掉强上拉的开关管,输出低电平时下面导通是强下拉,输出高电平时,下面断开但是没有上管了,引脚浮空,故所有设备只能输出低电平而不能输出高电平

img

  • 为了避免高电平造成的引脚浮空,需要在总线外面SCL和SDA各置一个上拉电阻,弱上拉

  • 开漏加弱上拉模式:同时兼具的输入和输出的功能

  • 要输出时,就去拉杆子或者放手操作杆子变化就行了
  • 要输入时,就直接放手然后观察杆子高低就行了
  • 因为开路模式下,输出高电平就相当于断开硬件,所以在输入之前可以直接输出高电平,不需要再切换成输入模式
  • 第三就是这个模式会有个“线与”的现象,只要有任意一个或多个设备,输出了低电平,,总线就处于低电平
  • 只有所有的设备都输出高电平,总线才处于高电平

(7)两个实验

1、介绍协议规则,用软件模拟的形式实现协议(AT24C02存储器模块)

  • 通过数据线,实现单排年纪读写外挂模块寄存器的功能

  • 在指定的位置写寄存器,对外挂模块进行配置

  • 在指定的位置度寄存器,获取外挂模块的数据,读出的数据会显示显示屏上

本节课程主要有两个代码
1、软件I2C读写MPU6050
2、硬件I2C读写MPU6050
代码实现的效果是一样的
软件I2C读写MPU6050的程序现象

  • 通过软件I2C协议对MPU6050(在本实验中ID号为0x68)芯片内部的寄存器进行读写
  • 写入到配置寄存器,可以对外挂的模块及进行配置
  • 读出数据寄存器,可以获取外挂的数据
  • 最终显示在屏幕上
  • 最上面:id号
  • 左边:加速度传感器的输出数据(x轴y轴和z轴的加速度)
  • 右边:陀螺仪传感器的输出数据(x轴y轴和z轴的角速度)
  • 改变MPU6050传感器的姿态,6个数据就会对应变化

(8)异步时序和同步时序
a、异步时序

1、好处:省一根时钟线,节省资源

2、坏处:对时钟要求严格,发送方和接收方时钟不能由过大的偏差

传输过程中,单片机进中断,发送方时序暂停,接受方仍会按照约定的速率读取,传输出错

故异步时序的缺点:非常依赖硬件外设的支持,必须有USART电路才能方便的使用,否则很难用软件模拟。

b、同步时序(时钟要求不严格,对电路依赖度低)

1、设计时钟线,则对传输的时间要求变低

2、在单方面暂停传输时,时钟线也暂停,传输双方都能定格在暂停的时刻,可过段时间再来继续,

3、极大的降低单片机对硬件电路的依赖,没有硬件电路的支持,也可以很方便的用软件手动翻转电平来实现通信

三、MPU6050

(1)MPU6050简介

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

  • 3 轴加速度计( Accelerometer ):测量 X 、 Y 、 Z 轴的加速度

  • 3 轴陀螺仪传感器( Gyroscope ):测量 X 、 Y 、 Z 轴的角速度

(2)MPU6050参数

  • 16 位 ADC 采集传感器的模拟信号,量化范围: -32768~32767
  • 加速度计满量程选择: ±2 、 ±4 、 ±8 、 ±16 ( g )
  • 陀螺仪满量程选择: ±250 、 ±500 、 ±1000 、 ±2000 ( °/sec )
  • 可配置的数字低通滤波器
  • 可配置的时钟源
  • 可配置的采样分频
  • I2C从机地址:1101000(AD0=0)1101001(AD0=1)

例如从机地址为0x68,(0x68<<1)|1(或者(0x68<<1)|0)当作从机地址,因为传输的时候是8个字节,前七个是地址,最后一位是数据读写位

  • 若物体运动剧烈,选择大量程,防止加速度或角速度超出了量程
  • 若物体运动平缓,选择小量程,则测量的分辨率比较大
  • 满量程越大测量范围就越广
  • 满量程越小测量分辨率就越高
  • 满量程和加速度是线性关系
  • 可配置数字低通滤波器:配置寄存器对输出数据进行低通滤波,消除数据抖动,使数据输出平缓
  • 时钟源通过分频器的分频,可以为AD转换,给内部其他电路提供时钟
  • 控制分频系数就可以控制AD转换的快慢

(3)MPU6050硬件电路

img

img

a、左上角LDO

  • 为低压差线性稳压器,输入端可以是3.3V到5V,经过后输出稳定3.3V的电压

    b、右下角J1 CON1模块

  • 左下角XCL和XDA通常就是用于外接磁力计或者气压计(在无人机需要定高飞行需要增加气压计的时候使用)

  • 当接上磁力计或气压计之后,MPU6050的主机接口可以直接访问这些扩展芯片的数据
  • 把这些扩展芯片的数据读取到MPO6050里面,在MPO6050里面会有DMP单元,进行数据融合和姿态解算
  • 若不需要MPO6050的解算功能,可以把磁力计或气压计直接挂载在SCL和SDA总线上
  • AD0引脚:是从机地址的最低位,接低电平时候,7为从机地址为110 1000,接高电平时,7位从机地址是110 1001,AD0接了一个电阻,故悬空是低电平状态,若想要接高电平,可以把AD0直接引导VCC上,强上拉至高电平
  • INT:中断输出引脚,可以配置芯片内部的一些事件来触发中断引脚的输出
  • 比如数据准备好了、I2C主机错误等

四、硬件I2C读写MPU6050代码

1、接线图

img

SDA接在B11,SCL接在B10 ,软件IIC的两个引脚可以任意更改的,因为都是开漏输出,硬件接在哪个引脚上,程序中就对应操作哪个引脚

但是硬件IIC,通信引脚是不可以任意指定的,查表,由于PB6、PB7被OLED应用,所以用PB10、PB11,软件IIC的代码在此处也适用

img

img

2、软件IIC改为硬件IIC

  • 第一步:开启I2C外设,对I2C2外设进行初始化,以替换MyI2C_Init

  • 第二步:控制外设电路,实现指定地址写的时序,以替换WriteReg

  • 第三步:控制外设电路,实现指定地址读的时序,以替换ReadReg

3、配置IIC外设初始化配置

  • 第一步:开启IIC外设和对应GPIO口的时钟

  • 第二步:把IIC外设和对应的GPIO口初始化为复用开漏模式

  • 第三步:使用结构体,对整个IIC进行配置
  • 第四步:I2C_Cmd,使能I2C

4、MPU6050.c代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
#include "stm32f10x.h"                  // Device header
#include "MPU6050_Reg.h"

#define MPU6050_ADDRESS 0xD0

void MPU6050_WaitEvent(I2C_TypeDef* I2Cx, uint32_t I2C_EVENT)
{
uint32_t Timeout;
Timeout = 10000;
while (I2C_CheckEvent(I2Cx, I2C_EVENT) != SUCCESS)
{
Timeout --;
if (Timeout == 0)
{
break;
}
}
}

void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data)
{
I2C_GenerateSTART(I2C2, ENABLE);//生成起始条件,实际是在操作CR1寄存器,置1,产生起始条件
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);//等待EV5事件的到来

//发送从机地址,接收应答,直接向DR寄存器写入一个字节即可
I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter);//I2C2、从机地址、发送(即最低地址清0)
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);

I2C_SendData(I2C2, RegAddress);
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTING);//字节正在发送中

I2C_SendData(I2C2, Data);
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED);//字节已经发送完毕transmitted

I2C_GenerateSTOP(I2C2, ENABLE);//生成终止条件
}

uint8_t MPU6050_ReadReg(uint8_t RegAddress)
{
uint8_t Data;

I2C_GenerateSTART(I2C2, ENABLE);
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);

I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter);
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);

I2C_SendData(I2C2, RegAddress);
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED);

I2C_GenerateSTART(I2C2, ENABLE);
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);

I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Receiver);
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED);

I2C_AcknowledgeConfig(I2C2, DISABLE);
I2C_GenerateSTOP(I2C2, ENABLE);

MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_RECEIVED);
Data = I2C_ReceiveData(I2C2);

I2C_AcknowledgeConfig(I2C2, ENABLE);

return Data;
}

void MPU6050_Init(void)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2, ENABLE);//使能IIC
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);//使能GPIOB口

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);

I2C_InitTypeDef I2C_InitStructure;
I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;
I2C_InitStructure.I2C_ClockSpeed = 50000;//50KHz,在0~100KHZ范围内,标准速度,100KHZ~400kHZ快速
//速度快的时候,低电平的时间需要多一些,因为低电平数据可能翻转,需要时间,高电平时,读取数据
I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;//低电平时间和高电平时间是2:1
I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;//配置ACK应答位,默认是给应答的
I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;//stm32作为从机,可以响应多少位的地址
I2C_InitStructure.I2C_OwnAddress1 = 0x00;//stm32暂时不作为从机,可以随便给个地址,只要不跟别的主机冲突即可
I2C_Init(I2C2, &I2C_InitStructure);

I2C_Cmd(I2C2, ENABLE);

MPU6050_WriteReg(MPU6050_PWR_MGMT_1, 0x01);
MPU6050_WriteReg(MPU6050_PWR_MGMT_2, 0x00);
MPU6050_WriteReg(MPU6050_SMPLRT_DIV, 0x09);
MPU6050_WriteReg(MPU6050_CONFIG, 0x06);
MPU6050_WriteReg(MPU6050_GYRO_CONFIG, 0x18);
MPU6050_WriteReg(MPU6050_ACCEL_CONFIG, 0x18);
}

uint8_t MPU6050_GetID(void)
{
return MPU6050_ReadReg(MPU6050_WHO_AM_I);
}

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);
DataL = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L);
*AccX = (DataH << 8) | DataL;

DataH = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_H);
DataL = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_L);
*AccY = (DataH << 8) | DataL;

DataH = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_H);
DataL = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_L);
*AccZ = (DataH << 8) | DataL;

DataH = MPU6050_ReadReg(MPU6050_GYRO_XOUT_H);
DataL = MPU6050_ReadReg(MPU6050_GYRO_XOUT_L);
*GyroX = (DataH << 8) | DataL;

DataH = MPU6050_ReadReg(MPU6050_GYRO_YOUT_H);
DataL = MPU6050_ReadReg(MPU6050_GYRO_YOUT_L);
*GyroY = (DataH << 8) | DataL;

DataH = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_H);
DataL = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_L);
*GyroZ = (DataH << 8) | DataL;
}

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "MPU6050.h"

uint8_t ID;
int16_t AX, AY, AZ, GX, GY, GZ;

int main(void)
{
OLED_Init();
MPU6050_Init();

OLED_ShowString(1, 1, "ID:");
ID = MPU6050_GetID();
OLED_ShowHexNum(1, 4, ID, 2);

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, GY, 5);
OLED_ShowSignedNum(4, 8, GZ, 5);
}
}