首页 > 其他分享 >十一、i²c通信

十一、i²c通信

时间:2024-02-21 22:45:17浏览次数:18  
标签:十一 i2c 通信 MPU6050 地址 寄存器 I2C 数据

九、I²C通信

I²C通信协议

i2c简介

  • I2C(Inter IC Bus)是由Philips公司开发的一种通用数据总线
  • 两根通信线:SCL(Serial Clock)、SDA(Serial Data)
  • 同步,半双工
  • 带数据应答
  • 支持总线挂载多设备(一主多从、多主多从)

image

i2c硬件电路

  • 所有I2C设备的SCL连在一起,SDA连在一起
  • 设备的SCL和SDA均要配置成开漏输出模式
  • SCL和SDA各添加一个上拉电阻,阻值一般为4.7KΩ左右

image

主机和从机都采用开漏输出,也就是高电平无效,由外部的弱上拉电阻拉高电平

i2c基本时序单元

起始和终止条件

  • 起始条件:SCL高电平期间,SDA从高电平切换到低电平
  • 终止条件:SCL高电平期间,SDA从低电平切换到高电平

image

发送数据

SCL低电平期间主机把数据位按从高位到低位的顺序依次放到SDA的线上,然后将SCL切换到高电平从机开始根据SDA的电平读数据,当SDA为高电平时读出来的数据为1,当SDA为低电平的的时候读出来的数据为0,读完一个位再把SCL切换到低电平

在SCL高电平期间SDA不允许有数据变化,依次循环上面的过程8次,即可发送一个字节

image

接收数据

SCL低电平期间从机把数据位按从高位到低位的顺序依次放到SDA的线上,然后将SCL切换到高电平主机开始开始根据SDA的电平读数据,当SDA为高电平时读出来的数据为1,当SDA为低电平的的时候读出来的数据为0,读完一个位再把SCL切换到低电平

在SCL高电平期间SDA不允许有数据变化,依次循环上面的过程8次,即可接受一个字节(注意:主机在接受数据前要释放SDA)

image

发送和接收应答

  • 发送应答:在接受完一个字节后,主机下一个时钟发送一位数据,判断主机是否应答,数据0表示应答,数据1表示没有应答
  • 接收应答:在发送完一个字节后,主机下一个时钟再接收一位数据,判断从机是否应答,数据0表示应答,数据1表示没有应答(主机在接受之前,需要释放SDA)
  • 因为上拉电阻的存在,当主机释放SDA后,SDA默认为高电平,所以用数据1表示没有应答
  • 可以把这个当作第九位数据,类似校验位

image

i2c时序

指定地址写

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

时序单元拼接:起始条件--->从机地址+写指令--->接收从机应答位(应答)--->主机指定从机的寄存器地址--->接收从机应答位(应答)--->主机写入的数据--->接收从机应答位(应答)--->停止条件

image

当前地址读

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

  • 当前地址指针:

    在从机中,所有的寄存器都被分配到一个线性区域中,其中会有个单独的指针变量指向其中的一个寄存器,该指针复位默认指向0地址,并且每写入一个字节和读出一个字节后都会自动自增一次。

    在调用当前地址读时序时,主机没有指定要读取的寄存器地址,从机就会发送当前地址指针指向的寄存器的值

  • 当前地址指针的作用:

    i2c规定,一旦读写标志位给1,下一个字节则立刻转为读的时序,主机无法指定要读取的寄存器,所以没有指定从机寄存器的时序。因此从机会发送当前地址指针指向的寄存器中的数据

时序单元拼接:起始条件--->从机地址+读指令--->接收从机应答位(应答)--->主机读取从机发送的数据--->主机发送应答位(不应答)--->停止条件

发送应答时,如果主机应答从机,则从机继续发送下一个地址的寄存器中的数据

一般不使用该时序

image

指定地址读

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

该时序结合了指定地址写和当前地址读的时序,可以随时读取指定的从机寄存器中的数据

先发送指定地址写的时序的起始条件、从机地址写、接收应答,再发送一次起始条件,最后调用完整的当前地址读的时序,就完成了指定地址读的时序

在发送写指令和存放数据的寄存器地址后,当前地址指针的指向就是该寄存器,所以再调用当前地址读就是读取发送的地址中的数据

时序单元拼接:起始条件--->从机地址+写指令--->从机接受应答--->指定从机寄存器的地址--->从机接收应答--->起始条件--->从机地址+读指令--->从机接收应答--->从机发送数据--->发送应答,主机不应答--->结束条件

image

MPU6050六轴姿态传感器

MPU6050简介

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

image

9轴姿态传感器

3轴加速度计,3轴陀螺仪传感器

3轴磁场传感器:测量X、Y、Z轴的磁场强度

10轴姿态传感器

3轴加速度计,3轴陀螺仪传感器,3轴磁场传感器

气压传感器:根据气压强度测量垂直地面的高度信息

加速度计

加速度

加速度是速度对于时间的变化率,用于描述物体速度变化的快慢

加速度分为重力加速度和运动加速度,如果此时芯片运动起来了,测量的角度就会受运动加速度的影响。

总结:加速度计具有静态稳定性,不具有动态稳定性

陀螺仪传感器

一般来说,陀螺仪可以测量角速度和旋转角度,但是该芯片无法测量旋转角度,只能测量角速度

角速度

角速度是描述物体绕轴旋转的速度的物理量,通常用符号ω表示。它的单位是弧度/秒,表示物体每秒绕轴旋转的弧度数。

总结:当物体静止时,角速度值会因为噪声无法完全归零 , 所以陀螺仪具有动态稳定性,不具有静态稳定性

姿态角

姿态角,或者叫做欧拉角。以飞机为例,欧拉角就是飞机机身相对于初始 3 个轴的夹角,飞机机头下倾或者上仰,这个轴的夹角叫做俯仰,Pitch;飞机机身左翻滚或者右翻滚,这个轴的夹角叫做滚转,Roll;飞机机身保持水平,机头向左转向或者向右转向,这个轴的夹角叫做偏航,Yaw,简单来说,欧拉角就表达了飞机此时的姿态。飞机是上仰了还是下倾了,飞机向左倾斜还是向右倾斜,通过欧拉角都能清晰地表示出来。

陀螺仪是动态稳定,静态不稳定,加速度计是静态稳定,动态不稳定。这两种传感器的特性正好互补,所以我们取长补短,进行一下互补滤波,就能融合得到静态和动态都稳定的姿态角了。

MPU6050参数

16位ADC采集传感器,量化范围:-32768~32767

  • 加速度计满量程:-2 ~ +2、-4 ~ +4、-8 ~ +8、-16 ~ +16(g)

  • 陀螺仪满量程:-250 ~ +250、-500 ~ +500、-1000 ~ +1000、-2000 ~ +2000( °/sec )

    这里满量程的意思就相当于ADC外设的VREF参考电压,当AD值达到最大时对应的电压是3.3V还是5V

  • 可配置的数字低通滤波器

可以配置寄存器来选择对输出数据进行低通滤波;如果你觉得输出数据抖动太厉害,就可以加一点低通滤波,这样输出数据就会平缓一些。

  • 可配置的时钟源

  • 可配置的采样分频

    这两个参数是配合使用,时钟源经过这个分频器的分频,可以为 AD 转换和内部其他电路提供时钟,控制分频系数,就可以控制 AD 转换的快慢

  • i2c从机地址

    1101 000(AD0=0)

    1101 001(AD0=1)

    AD0:是芯片引出来的一个引脚,可以控制7位从机地址的最低位,防止在挂载多个设备时出现相同地址的从机

    要对从机进行读写就需要将从机地址整体向左移一位,此时最低位,也就是第0位是读写位,第1位是AD0

    也可以直接将读写位都算到从机地址中,也就是0xD1是对该从机读操作,0xD0是对该从机写操作

硬件电路

从图中可以看到,MUP6050的SDA和SCL都接了弱上拉电阻,就不需要我们再接电阻了,直接将SDA和SCL接到GPIO口即可

image

引脚定义

VCC、GND 电源
SCL、SDA I2C通信引脚
XCL、XDA 主机I2C通信引脚
AD0 从机地址最低位
INT 中断信号输出

XCL、XDA:MPU6050只是一个6轴的姿态传感器,如果想进行9轴或者10轴的姿态测量,只需要将磁力计和气压计当作MPU6050的从机,XCL和XDA就是MPU6050作为主机时的引脚

INT: 可以配置芯片内部的一些事件,来触发中断引脚的输出,比如数据准备好了、I2C 主机错误等,另外芯片内部还内置了一些实用的小功能,比如自由落体检测、运动检测、零运动检测等。这些信号都可以出发 INT 引脚产生电平跳变

AD0:MPU6050已经给我们接了一个弱下拉电阻,当引脚悬空时,默认输出低电平

内部框图

image

  • 时钟源

    1. 内部晶振,可以作为系统时钟。
    2. XYZ 轴的陀螺仪,它们也都会有个晶振,因为陀螺仪内部需要高精度时钟的支持,所以陀螺仪内部也有独立的时钟,这 3 个时钟也可以输出,作为系统时钟。
    3. 通过外部的 CLKIN 引脚,输入 32.768 KHz 的方波,或者 19.2 MHz 的方波,作为系统时钟。(不过这个外部时钟还需要额外的电路,比较麻烦,所以如果不是特别要求的话,一般用内部晶振或者内部陀螺仪的晶振,作为系统时钟,这个是时钟源的选择问题)
  • 自测单元

    这部分是用来验证芯片好坏的,当启动自测后,芯片内部会模拟一个外力施加在传感器上,这个外力导致传感器数据会比平时大一些,那如何进行自测呢?
    我们可以先使能自测,读取数据,再失能自测,读取数据,两个数据一相减,得到的数据叫自测响应,这个自测响应,芯片手册里给出了一个范围,如果自测响应在这个范围内,就说明芯片没问题,如果不在,就说明芯片可能坏了,使用的话就要小心点,这个是自测的功能

  • 电荷泵

    或者叫充电泵,CPOUT 引脚需要外接一个电容,是一种升压电路

    电荷泵的升压原理(简单描述,了解一下即可):

    • 比如我有个电池,电压是 5V,然后再来个电容,首先电池和电容并联,电池给电容充电,充满之后,电容是不是也相当于一个 5V 的电池了。然后呢,关键部分来了,我再修改电路的接法,把电池和电容串联起来,电池 5V,电容也是 5V,这样输出就是 10V 的电压了,就把电池电压升高至两倍了
    • 不过由于这个电容电荷比较少,用一下就不行了,所以这个并联、串联的切换速度要快,趁电容还没放电完,就要及时并联充电,这样一直持续,并联充电,串联放电,并联充电,串联放电,然后后续再加一个电源滤波,就能进行平稳的升压了,这就是电荷泵的升压原理。
    • 那这里,由于陀螺仪内部是需要一个高电压支持的,所以这里设计了一个电荷泵进行升压,当然这个升压过程是自动的,不需要我们管,了解一下即可。
  • 中断状态寄存器:可以控制内部的哪些事件到中断引脚的输出

  • 先入先出寄存器:对数据流进行缓存

  • 配置寄存器:对内部的各个电路进行配置

  • 数据寄存器:也就是传感器寄存器,存储了各个传感器的数据

  • 工厂校准:这个意思就是内部的传感器都进行了校准

  • 数字运动处理器:简称 DMP,是芯片内部自带的一个姿态解算的硬件算法,配合官方的 DMP 库,可以进行姿态解算

  • 接口旁路选择器:

    就是一个开关,如果拨到上面,辅助的 I2C 引脚就和正常的 I2C 引脚接到一起,这样两路总线就合在一起了,STM32 可以控制所有设备,这时 STM32 就是主机,MPU6050 和它的扩展设备都是 STM32 的从机;
    如果拨到下面,辅助的 I2C 引脚就由 MPU6050 控制,两条 I2C 总线独立分开,这时 STM32 是 MPU6050 的主机,MPU6050 又是它的扩展设备的主机

寄存器

常用寄存器

地址(HEX) 寄存器名称 寄存器翻译 读写权限
0x19 SMPLRT_DIV 采样频率分频器 R/W
0x1A CONFIG 配置寄存器 R/W
0x1B GYRO_CONFIG 陀螺仪配置寄存器 R/W
0x1C ACCEL_CONFIG 加速度计配置寄存器 R/W
0x3B ACCEL_XOUT_H 加速度计数据寄存器 R
0x3C ACCEL_XOUT_L 加速度计数据寄存器 R
0x3D ACCEL_YOUT_H 加速度计数据寄存器 R
0x3E ACCEL_YOUT_L 加速度计数据寄存器 R
0x3F ACCEL_ZOUT_H 加速度计数据寄存器 R
0x40 ACCEL_ZOUT_L 加速度计数据寄存器 R
0x41 TEMP_OUT_H 温度数据寄存器 R
0x42 TEMP_OUT_L 温度数据寄存器 R
0x43 GYRO_XOUT_H 陀螺仪数据寄存器 R
0x44 GYRO_XOUT_L 陀螺仪数据寄存器 R
0x45 GYRO_YOUT_H 陀螺仪数据寄存器 R
0x46 GYRO_YOUT_L 陀螺仪数据寄存器 R
0x47 GYRO_ZOUT_H 陀螺仪数据寄存器 R
0x48 GYRO_ZOUT_L 陀螺仪数据寄存器 R
0x6B PWR_MGMT_1 电源管理寄存器 1 R/W
0x6C PWR_MGMT_2 电源管理寄存器 2 R/W
0x75 WHO_AM_I 器件 ID 号 R
  • 采样频率分频器

    里面的 8 位表示一个整数,作为分频值,这个寄存器可以配置采样频率的分频系数。简单来说就是,分频越小,内部的 AD 转换就越快,数据寄存器的刷新就越快,反之就越慢。这里有个公式:采样频率(你可以理解为数据刷新率) = 陀螺仪输出时钟频率 /(1 + 分频值)

  • 配置寄存器

    内部有两部分,外部同步设置和低通滤波器配置。

    • 外部同步用不上
    • 低通滤波器:配置这些位可以选择手册这里的各种滤波参数(0~7),这个低通滤波器可以让输入数据更加平滑,配置滤波器参数越大,输出数据抖动就越小,0 是不使用低通滤波器,陀螺仪时钟为 8KHz,之后使用了滤波器,陀螺仪时钟就是 1KHz,这个刚才说过,然后这个最大的参数,是保留位,没有用到
  • 陀螺仪配置寄存器

    高 3 位是 XYZ 轴的自测使能位,中间2位是满量程选择位,后面 3 位没用到

    • 自测:内部框图的自测单元也有介绍。手册这里有个公式:自测响应 = 自测使能时的数据 - 自测失能时的数据。我们上电后,先使能自测,读取数据,再失能自测,读取数据,两者相减,得到自测响应,然后在产品说明书手册的电气特性表里找一下自测响应的范围,如果在这个范围里,芯片就通过了自测,之后正常使用即可
    • 满量程选择: 可以选择四种(0~3),在MPU6050参数中有介绍,量程越大,范围越广;量程越小,分辨率越高
  • 加速度计配置寄存器

    和陀螺仪配置寄存器基本一样 高 3 位是自测使能位,中间2位是满量程选择位,后面 3 位配置高通滤波器

    高通滤波器:刚才说过,就是内置小功能,运动检测用到的,对数据输出没有影响,我们暂时用不到

  • 数据寄存器

    包括加速度计 XYZ 轴,温度传感器、陀螺仪 XYZ 轴的数据,这里 L 表示低 8 位,H 表示高 8 位

    • 加速度计的数据寄存器,我们想读取数据的话,直接读取数据寄存器就行了,这是一个 16 位的有符号数,以二进制补码的方式存储,我们读出高 8 位和低 8 位,高位左移 8 次,或上低位的数据,最后再存在一个 int16_t 的变量里,这样就可以得到数据了。

    下面温度传感器数据、陀螺仪数据,都是一样的操作方法

  • 电源管理寄存器1和2

    电源管理寄存器1

    • 第8位:设备复位,这位写1所有寄存器恢复到默认值
    • 第7位:睡眠模式,这位写1芯片睡眠不工作,进入低功耗
    • 第6位:循环模式,这位写1设备进入低功耗,过一段时间启动一次,唤醒频率由电源管理寄存器2的第8位和第7位决定
    • 第4位:温度传感器失能,写1后禁用内部的温度传感器
    • 低三位(0、1、2位):选择系统时钟源。 分别是:内部晶振(0),XYZ轴陀螺仪晶振(1~3),外部引脚的两个方波(4、5)。 一般我们选择内部晶振或者陀螺仪晶振,不过手册里非常建议我们选择陀螺仪晶振,因为陀螺仪的晶振更加精确

    电源管理寄存器2

    • 高两位(7、8位):上面说过,控制循环模式的唤醒频率
    • 剩下的6位可以分别控制6个轴进入待机模式, 如果你只需要部分轴的数据,可以让其他轴待机,这样比较省电
  • 器件ID号

    该寄存器是只读的。中间六位固定位110100,最高位和最低位如果读出来的话其实都是0,也就是0x68

    AD0是外部引脚引出来的,并不能影响该寄存器的最低位。也就是哪怕AD0置高电平,芯片的i2c地址为:1101 001,但是读该寄存器还是0x68

所有的寄存器上电默认值都是 0x00,除了 107 号寄存器,上电默认 0x40,117 号寄存器,上电默认 0x68。
117 号,就是 ID 号,默认 0x68
107 号,是电源管理寄存器 1,默认 0x40,也就是次高位为 1,这里次高位是 SLEEP,所以这个芯片上电默认就是睡眠模式,我们在操作它之前,要先记得解除睡眠。否则操作其他寄存器是无效的

全部寄存器

标有黄线的是上面的常用寄存器

image

image

image

image

image

案例:软件i2c读写MPU6050

接线图

image

i2c代码封装

i2c.c

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

#define i2c_Port GPIOB			// i2c通信的GPIO端口
#define i2c_SCL GPIO_Pin_10		// SCL对应的引脚
#define i2c_SDA GPIO_Pin_11		// SDA对应的引脚

// 操作SCL
void i2c_W_SCL(uint8_t state)
{
	// BitAction:可以将1、0强转为Bit_SET、Bit_RESET
	GPIO_WriteBit(i2c_Port, i2c_SCL, (BitAction)state);
	Delay_us(10);	// 防止单片机主频过快,从机反应不过来
}
// 操作SDA
void i2c_W_SDA(uint8_t state)
{
	// BitAction:可以将1、0强转为Bit_SET、Bit_RESET
	GPIO_WriteBit(i2c_Port, i2c_SDA, (BitAction)state);
	Delay_us(10);	// 防止单片机主频过快,从机反应不过来
}
// 读取SDA
uint8_t i2c_R_SDA()
{
	uint8_t BitVale = GPIO_ReadInputDataBit(i2c_Port, i2c_SDA);
	Delay_us(10);
	return BitVale;
}

// 初始化用于i2c通信的GPIO口
void i2c_Init()
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
	
	GPIO_InitTypeDef GPIO_InitStruct;
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_OD;	// 开漏输出,i2c通信规定高电平无效,由弱上拉置1
	GPIO_InitStruct.GPIO_Pin = i2c_SCL | i2c_SDA;
	GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(i2c_Port, &GPIO_InitStruct);
}

// 起始条件
void i2c_Start()
{
	i2c_W_SDA(1);
	i2c_W_SCL(1);
	i2c_W_SDA(0);
	i2c_W_SCL(0);
}

// 结束条件
void i2c_End()
{
	i2c_W_SDA(0);
	i2c_W_SCL(1);
	i2c_W_SDA(1);
}

// 主机发送一个字节
void i2c_Write_Byte(uint8_t Byte)
{
	for(uint8_t i = 0; i < 8; i++)
	{
		i2c_W_SDA(Byte & (0x80 >> i));
		i2c_W_SCL(1);
		i2c_W_SCL(0);
	}
}

// 主机读取一个字节
uint8_t i2c_Read_Byte()
{
	uint8_t Byte = 0x00;
	i2c_W_SDA(1);
	for(uint8_t i = 0;i < 8; i++)
	{
		i2c_W_SCL(1);
		if(i2c_R_SDA() == 1){Byte |= (0x80 >> i);}
		i2c_W_SCL(0);
	}
	return Byte;
}

// 接收应答
uint8_t i2c_Read_ACK()
{
	i2c_W_SDA(1);
	i2c_W_SCL(1);
	uint8_t ACK = i2c_R_SDA();
	i2c_W_SCL(0);
	return ACK;
}

// 发送应答
void i2c_Write_ACK(uint8_t ACK)
{
	i2c_W_SDA(ACK);
	i2c_W_SCL(1);
	i2c_W_SCL(0);
}

i2c.h

#ifndef __i2c_H
#define __i2c_H

// 初始化用于i2c通信的GPIO口
void i2c_Init(void);
// 起始条件
void i2c_Start(void);
// 结束条件
void i2c_End(void);
// 主机读取一个字节
uint8_t i2c_Read_Byte(void);
// 主机发送一个字节
void i2c_Write_Byte(uint8_t Byte);
// 接收应答
uint8_t i2c_Read_ACK(void);
// 发送应答
void i2c_Write_ACK(uint8_t ACK);

#endif

封装MPU6050

MPU6050.c

#include "stm32f10x.h"                  // Device header
#include "MPU6050_Reg.h"
#include "i2c.h"

#define MPU_Addr 0xD0

/**
  * 函    数:写一个字节的数据到寄存器中
  * 参    数:Reg_Addr 寄存器地址
  * 参    数:Byte 要写入的字节
  * 返 回 值:无
  */
void MPU6050_Write_Reg(uint8_t Reg_Addr, uint8_t Byte)
{
	i2c_Start();				// 开始条件
	i2c_Write_Byte(MPU_Addr);	// MPU写地址
	i2c_Read_ACK();				// 接收应答
	i2c_Write_Byte(Reg_Addr);	// MPU寄存器地址
	i2c_Read_ACK();				// 接收应答
	i2c_Write_Byte(Byte);		// 写入数据
	i2c_Read_ACK();				// 接收应答
	i2c_End();					// 结束条件
}

/**
  * 函    数:从指定的寄存器中读一个字节的数据
  * 参    数:Reg_Addr 寄存器地址
  * 返 回 值:从寄存器中读出的数据(0~255)
  */
uint8_t MPU6050_Read_Reg(uint8_t Reg_Addr)
{
	uint8_t Data;						// 存放读出的数据
	
	i2c_Start();						// 开始条件
	i2c_Write_Byte(MPU_Addr);			// MPU写地址
	i2c_Read_ACK();						// 接收应答
	i2c_Write_Byte(Reg_Addr);			// MPU寄存器地址
	i2c_Read_ACK();						// 接收应答
	
	i2c_Start();						// 开始条件
	i2c_Write_Byte(MPU_Addr | 0x01);	// MPU读地址
	i2c_Read_ACK();						// 接收应答
	Data = i2c_Read_Byte();				// 读一个字节
	i2c_Write_ACK(1);					// 发送应答不应答
	i2c_End();							// 结束条件
	
	return Data;
}

// 存放数据寄存器中的值从0~7分别为:X、Y、Z轴的加速度、温度、X、Y、Z轴的角速度
int16_t MPU6050_Data[7];

int16_t *MPU6050_Read_ALL_Data()
{
	// MPU6050将数据的高位和低位分开放的,一共7个可测量的数据,有14个数据寄存器
	uint16_t Byte[14];
	
	i2c_Start();							// 起始条件
	i2c_Write_Byte(MPU_Addr);				// 从机地址,写指令
	i2c_Read_ACK();							// 接收应答
	i2c_Write_Byte(MPU6050_ACCEL_XOUT_H);	// 第一个数据寄存器的地址,每读一次数据,当前地址指针会自增一次
	i2c_Read_ACK();							// 接收应答
	
	i2c_Start();							// 重复起始
	i2c_Write_Byte(MPU_Addr | 0x01);		// 从机地址,读指令
	i2c_Read_ACK();							// 接收应答
	
	// 连续接收14个数据寄存器的数据,将读出来的数据存放到Byte数组中
	for(uint8_t i = 0; i < 14; i++)
	{
		Byte[i] = i2c_Read_Byte();			// 读一个字节
		if(i == 13){i2c_Write_ACK(1);}		// 没有读到14次时主机给发送应答,表示我还要读数据
		else{i2c_Write_ACK(0);}				// 当14个数据寄存器的数据读完后主机不给发送应答,表示数据接收完毕
	}
	
	i2c_End();								// 结束条件
	
	// 将高8位的数和低8位的数据组合起来放到MPU6050数组中返回出去
	for(uint8_t i = 0; i < 7; i++)
	{
		// 高位数据和低位数据是相邻的,高位在低位的前一个地址
		MPU6050_Data[i] = (int16_t)(Byte[i*2] << 8) | (Byte[i*2+1]);
	}
	return MPU6050_Data;
}

void MPU6050_Init()
{
	i2c_Init();
	
	/*MPU6050寄存器初始化,需要对照MPU6050手册的寄存器描述配置,此处仅配置了部分重要的寄存器*/
	MPU6050_Write_Reg(MPU6050_PWR_MGMT_1, 0x01);		//电源管理寄存器1,取消睡眠模式,选择时钟源为X轴陀螺仪
	MPU6050_Write_Reg(MPU6050_PWR_MGMT_2, 0x00);		//电源管理寄存器2,保持默认值0,所有轴均不待机
	MPU6050_Write_Reg(MPU6050_SMPLRT_DIV, 0x09);		//采样率分频寄存器,配置采样率
	MPU6050_Write_Reg(MPU6050_CONFIG, 0x06);			//配置寄存器,配置DLPF
	MPU6050_Write_Reg(MPU6050_GYRO_CONFIG, 0x18);		//陀螺仪配置寄存器,选择满量程为±2000°/s
	MPU6050_Write_Reg(MPU6050_ACCEL_CONFIG, 0x18);		//加速度计配置寄存器,选择满量程为±16g
}

MPU6050_Reg.h

MPU6050寄存器的地址

#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	// X轴的加速度高8位数据
#define	MPU6050_ACCEL_XOUT_L	0x3C	// X轴的加速度低8位数据
#define	MPU6050_ACCEL_YOUT_H	0x3D	// Y轴的加速度高8位数据
#define	MPU6050_ACCEL_YOUT_L	0x3E	// Y轴的加速度低8位数据
#define	MPU6050_ACCEL_ZOUT_H	0x3F	// Z轴的加速度高8位数据
#define	MPU6050_ACCEL_ZOUT_L	0x40	// Z轴的加速度低8位数据
#define	MPU6050_TEMP_OUT_H		0x41	// 温度数据的高8位数据
#define	MPU6050_TEMP_OUT_L		0x42	// 温度数据的低8位数据
#define	MPU6050_GYRO_XOUT_H		0x43	// X轴的角速度高8位数据
#define	MPU6050_GYRO_XOUT_L		0x44	// X轴的角速度低8位数据
#define	MPU6050_GYRO_YOUT_H		0x45	// Y轴的角速度高8位数据
#define	MPU6050_GYRO_YOUT_L		0x46	// Y轴的角速度低8位数据
#define	MPU6050_GYRO_ZOUT_H		0x47	// Z轴的角速度高8位数据
#define	MPU6050_GYRO_ZOUT_L		0x48	// Z轴的角速度低8位数据

#define	MPU6050_PWR_MGMT_1		0x6B	// 电源管理寄存器1
#define	MPU6050_PWR_MGMT_2		0x6C	// 电源管理寄存器2
#define	MPU6050_WHO_AM_I		0x75	// 器件ID号

#endif

MPU6050.h

#ifndef __MPU6050_H
#define __MPU6050_H

// 初始化
void MPU6050_Init(void);

// 写单个寄存器
void MPU6050_Write_Reg(uint8_t Reg_Addr, uint8_t Byte);

// 读取全部数据寄存器
int16_t *MPU6050_Read_ALL_Data(void);

// 读单个寄存器
uint8_t MPU6050_Read_Reg(uint8_t Reg_Addr);

#endif

主函数

#include "stm32f10x.h"                  // Device header
#include "OLED.h"
#include "MPU6050.h"

int main()
{
	OLED_Init();
	MPU6050_Init();
	
	// 接收返回的数组
	int16_t *MPU_Data;
	while(1)
	{
		// 读取全部数据寄存器
		MPU_Data = MPU6050_Read_ALL_Data();
		
		OLED_ShowSignedNum(1,1,MPU_Data[0],5);
		OLED_ShowSignedNum(2,1,MPU_Data[1],5);
		OLED_ShowSignedNum(3,1,MPU_Data[2],5);
		
		OLED_ShowSignedNum(1,8,MPU_Data[4],5);
		OLED_ShowSignedNum(2,8,MPU_Data[5],5);
		OLED_ShowSignedNum(3,8,MPU_Data[6],5);
		
		OLED_ShowSignedNum(4,3,MPU_Data[3],5);
	}
}

i2c外设

i2c外设简介

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

  • 支持多主机模型

    • 固定多主机

    总线上有两个及以上固定的主机,从机和主机设备都是固定的,不可变化

    • 可变多主机

    总线上没有固定的主机和从机,任何一个设备都可以在总线空闲时跳出来当主机,指定其他任何一个设备进项通信,通信完成后主机就又要回到从机的位置。当多个设备要成为主机时就需要进行仲裁,仲裁失败的让出总线控制权。STM32的i2c外设使用的就是可变多主机模型

  • 支持7位/10位地址模式

    10位地址

    如果总线上的设备超过128后7位地址就不够了,所以有10位地址的模式。

    • 7位地址的设备寻址时只需要发送一个字节即可,10位地址的设备寻址就需要两个字节
    • 第一个字节的高五位必须是11110,也就是10位地址寻址的标志位,第一个字节的最低位依然是读写位,第一个字节剩下的2个位加上第二个字节的8个位就组成10位地址设备的地址

    而11110作为10位地址模式的标志位,在7位地址模式下的高五位是不会出现

    • 示例:

    例如有一个10位地址的设备地址为:1001 1100 11写模式。那么i2c的前两个字节就要发送:11110 10 0 01110011

    15位~11位:11110,标志位

    10位~9位:10,设备地址的高两位

    8位:0,写指令

    7位~0位:01110011,设备地址的低8位

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

  • 支持DMA

    在多字节传输的时候可以提高传输效率

  • 兼容SMBus协议

    这个 SMBus(System Management Bus),是系统管理总线,SMBus 是基于 I2C 总线改进而来的,主要用于电源管理系统中,SMBus 和 I2C 非常像,所以 STM32 的 I2C 外设就顺便兼容了一下 SMBus

  • STM32F103C8T6 硬件I2C资源:I2C1、I2C2

内部框图

image

  • 数据收发的部分

    数据控制部分,这里数据收发的核心部分,是这里的数据寄存器和数据移位寄存器。

    • 发送数据:可以把一个字节数据写到数据寄存器 DR;当移位寄存器没有数据移位时,这个数据寄存器的值就会进一步转到移位寄存器里,在移位的过程中,我们就可以直接把下一个数据放到数据寄存器里等着了,一旦前一个数据移位完成,下一个数据就可以无缝衔接,继续发送,当数据由数据寄存器转到移位寄存器时,就会置状态寄存器的 TXE 位为 1,表示发送寄存器为空,这就是发送的流程
    • 接收数据: 也是这一路,输入的数据,一位一位地从引脚移入到移位寄存器里,当一个字节的数据收齐之后,数据就整体从移位寄存器转到数据寄存器,同时置标志位 RXNE,表示接收寄存器非空,这时我们就可以把数据从数据寄存器读出来了

    与串口的区别:串口是全双工,发送数据和接收数据可以同时进行,所以串口有四个寄存器:数据接收寄存器、数据接收移位寄存器、数据发送寄存器、数据发送移位寄存器

  • 地址比较的部分

    比较器和地址寄存器这是从机模式使用的, STM32 的 I2C 是基于可变多主机模型设计的,STM32 不进行通信的时候,就是从机,既然作为从机,它就应该可以被别人召唤。

    我们可以自定一个从机地址,写到这个寄存器,当 STM32 作为从机,在被寻址时,如果收到的寻址通过比较器判断,和自身地址相同,那 STM32 就作为从机,响应外部主机的召唤

    并且这个 STM32 支持同时响应两个从机地址,所以就有自身地址寄存器和双地址寄存器

  • 数据校验部分

    这是 STM32 设计的一个数据校验模块,当我们发送一个多字节的数据帧时,在这里硬件可以自动执行 CRC 校验计算

    CRC 是一种很常见的数据校验算法,它会根据前面这些数据,进行各种数据运算,然后会得到一个字节的校验位附加在这个数据帧后面,在接收到这一帧数据后,STM32 的硬件也可以自动执行校验的判定,如果数据在传输过程中出错了,CRC 校验算法就通不过,硬件就会置校验错误标志位,告诉你数据错了

i2c的基本结构

image

移位寄存器和数据寄存器 DR 的配合是通信的核心部分,这里因为 I2C 是高位先行,所以移位寄存器是向左移位,在发送的时候,最高位先移出去,然后是次高位,等等。一个 SCL 时钟,移位一次,移位 8 次,这样就能把一个字节,由高位到低位,依次放到 SDA 线上了,那在接收的时候呢,数据通过 GPIO 口从右边依次移进来,最终移 8 次,一个字节就接收完成了

注意: 这两个对应的 GPIO 口,都要配置成复用开漏输出的模式

主模式数据的收发

也就是STM32作为主机

发送数据,指定地址写

image

以7位主发送为例的详细流程解释

  • 起始条件S:在控制寄存器 CR1 中,有个 START 位,在这一位写 1,就可以产生起始条件了,当起始条件发出后,这一位可以由硬件清除,所以,只要在这一位写 1,STM32 就自动产生起始条件了。之后,STM32 由从模式转为主模式,也就是多主机模型下,STM32 有数据要发,就要跳出来

  • EV5: 起始条件之后,会发生 EV5 事件,可以把它当成是标志位。 EV5 事件就是 SB(Start Bit)标志位为 1,SB 是状态寄存器SR1的一个位,这一位置 1,代表起始条件已发送,软件读取 SR1 寄存器后,也就是查看了这一位,然后写数据寄存器的操作将清除该位,写数据寄存器 DR,就是我们接下来的操作,所以按照正常的流程来,这个状态寄存器是不需要手动清除的。

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

  • EV6:在寻址完成之后,会发生 EV6 事件,下面解释了 EV6 事件,就是 ADDR 标志位为 1,在手册中可以找到,ADDR 标志位在主模式状态下就代表地址发送结束。

  • EV8_1:EV6 事件结束后,是 EV8_1 事件,下面解释,EV8_1 事件就是 TxE 标志位 = 1,移位寄存器空,数据寄存器空,这时需要我们写入数据寄存器 DR 进行数据发送了

  • EV8:一旦写入 DR数据寄存之后,因为移位寄存器也是空,所以 DR 会立刻转到移位寄存器进行发送,这时就是 EV8 事件,移位寄存器非空,数据寄存器空,这时就是 移位寄存器正在发数据的状态。当数据1发送完成后,接收到从机的应答前,在数据寄存器中写入数据2,此时EV8事件消失。然后接收到了从机的应答,数据寄存器DR转移数据到移位寄存器中进行发送,此时EV8事件又出现。在移位寄存器发送的过程中,数据3写入DR数据寄存器中,EV8事件消失。如此循环连续的发送数据

  • EV8_2:最后,当我们想要发送的数据写完之后,这时就没有新的数据可以写入到数据寄存器了。当移位寄存器当前的数据移位完成时,此时就是移位寄存器空,数据寄存器也空的状态,这个事件就是这里的 EV8_2,下面解释,EV8_2 是 TxE = 1,也就是数据寄存器空,BTF(Byte Transfer Finished),这个是字节发送结束标志位,手册里可以看一下解释:在发送时,当一个新数据将被发送且数据寄存器还未被写入新的数据时,BTF 标志位置 1,这个意思就是,当前的移位寄存器已经移完了,该找数据寄存器要下一个数据了,但是一看,数据寄存器没有数据,这就说明主机不想发了,这时就代表字节发送结束,是时候停止了。所以当检测到 EV8_2 时,就可以产生终止条件了

  • 停止条件P: 控制寄存器 CR1 中有一位 STOP,写 1,就会在当前字节传输,或在当前起始条件发出后产生停止条件

接收数据,当前地址读

想要指定地址读的时序需要自己拼接

image

这里有 7 位主接收和 10 位主接收,从这个 7 位主接收的时序看,这里时序的流程是:起始,从机地址+读,接收应答,然后就是,接收数据,发送应答,接收数据,发送应答,最后一个数据,给非应答,之后终止。可以看出,这个时序应该是当前地址读的时序,指定地址读的复合格式,这里没有给,得需要我们自己组合一下,然后下面 10 位地址的当前地址读就复杂一些了。这里是,起始,发送帧头,这个帧头里的读写位,应该还是写的,因为后面还要跟着发送第二个字节的地址,之后继续发送第二个字节的 8 位地址,这样才能进行寻址,然后要想转入读的时序,必须再发送重复起始条件,发送帧头,这次帧头的读写位就是读的了,因为发送读的指令之后,必须要立刻转入读的时序,所以这第二个字节的地址就没有了,直接转入接收数据的时序,这是 10 位地址的操作流程,稍微复杂一些,当然我们主要还是看 7 位地址的就行。

首先写入控制寄存器的 START 位,产生起始条件,然后等待 EV5 事件,下面解释和刚才一样,EV5 事件就代表起始条件已发送,之后是寻址,接收应答,结束后产生 EV6 事件,下面的解释也和刚才一样,EV6 事件代表,寻址已完成,之后数据 1 这一块,代表数据正在通过移位寄存器进行输入,EV6_1 事件,下面解释是没有对应的事件标志,只适于接收 1 个字节的情况,这个 EV6_1,可以看到,数据 1 其实还正在移位,还没收到呢,所以这个事件就没有标志位。之后当这个时序单元完成时,硬件会自动根据我们的配置,把应答位发送出去,如何配置是否要给应答呢?也是看手册,控制寄存器 CR1 里,这里有一位 ACK,应答使能,如果写 1,在接收到一个字节后就返回一个应答,写 0,就是不给应答,这就是应答位的配置。

之后继续,当这个时序单元结束后,就说明移位寄存器就已经成功移入一个字节的数据 1 了,这时,移入的一个字节就整体转移到数据寄存器,同时置 RxNE 标志位,表示数据寄存器非空,也就是收到了一个字节的数据,这个状态就是 EV7 事件。下面解释是,RxNE = 1,数据寄存器非空,读 DR 寄存器清除该事件,也就是,收到数据了,当我们把数据读走之后,这个事件就没有了,上面这里,EV7 事件没有了,说明此时数据 1 被读走了,当然数据 1 还没读走的时候,数据 2 就可以字节移入移位寄存器了,之后数据 2 移位完成,收到数据 2,产生 EV7 事件,读走数据 2,EV7 事件没有了。

然后按照这个流程,就可以一直接收数据了,最后,当我们不需要继续接收时,需要在最后一个时序单元发生时,提前把刚才说的应答位控制寄存器 ACK 置 0,并且设置终止条件请求,这就是 EV7_1 事件,下面解释,和 EV7 一样,后面加了一句,设置 ACK = 0 和 STOP 请求,也就是我们想要结束了。之后,在这个时序完成后,由于设置了 ACK = 0,所以这里就会给出非应答,最后由于设置 STOP 位,所以产生终止条件。

SCL占空比

i2c的输出模式规定开漏输出,所以输出低电平时是强下拉,一瞬间就降到了低电平。输出高电平时是通过弱上拉电阻拉上去的,会需要一点点时间才能升到高电平

在i2c协议下,设备是在低电平期间变化数据,高电平读取数据。而数据变化需要一定时间来翻转SDA的波形,尤其在数据上升沿期间变化比较慢,所以在SCL频率较高的情况下需要给SCL的低电平更多的时间来保证SDA的波形变化完成

i2c规定SCL的频率超过100KHz就算快速传输,需要调节占空比了

i2c库函数

I2C_InitTypeDef

typedef struct
{
  uint32_t I2C_ClockSpeed;          /*!< 时钟频率/通信速度 */
  uint16_t I2C_Mode;                /*!< i2c的模式 */
  uint16_t I2C_DutyCycle;           /*!< i2c快速传输时的占空比 */
  uint16_t I2C_OwnAddress1;         /*!< 作为从机时的地址 */
  uint16_t I2C_Ack;                 /*!< 开启还是关闭 */
  uint16_t I2C_AcknowledgedAddress; /*!< 作为从机时的地址位数,是7位还是10位 */
}I2C_InitTypeDef;

/* I2C_ClockSpeed */
标准速度:0~100KHz
快速传输:100~400KHz
直接写想要的传输速度即可,最大400KHz。例如50KHz的传输速度,参数就是50000

/* I2C_Mode */
I2C_Mode_I2C			// i2c模式
I2C_Mode_SMBusDevice	// SMBus总线的设备
I2C_Mode_SMBusHost		// SMBus总线的主机

/* I2C_DutyCycle */
快速传输的情况下该参数才有效
I2C_DutyCycle_16_9	// SCL低电平和高电平时间比例是16:9
I2C_DutyCycle_2		// SCL低电平和高电平时间比例是2:1

/* I2C_OwnAddress1 */
从模式下,根据地址位数设置地址

/* I2C_Ack */
I2C_Ack_Enable	// 开启i2c
I2C_Ack_Disable	// 禁用i2c

/* I2C_AcknowledgedAddress */
I2C_AcknowledgedAddress_7bit	// 7位的地址
I2C_AcknowledgedAddress_10bit	// 10位的地址

库函数

// 复位i2c的设置
void I2C_DeInit(I2C_TypeDef* I2Cx);

// 通过I2C_InitTypeDef结构体中的参数配置i2c
void I2C_Init(I2C_TypeDef* I2Cx, I2C_InitTypeDef* I2C_InitStruct);

// 使用默认值配置i2c
void I2C_StructInit(I2C_InitTypeDef* I2C_InitStruct);

// 启动或禁用i2c
void I2C_Cmd(I2C_TypeDef* I2Cx, FunctionalState NewState);

// 使用i2c的DMA
void I2C_DMACmd(I2C_TypeDef* I2Cx, FunctionalState NewState);
void I2C_DMALastTransferCmd(I2C_TypeDef* I2Cx, FunctionalState NewState);

// 生成起始条件
void I2C_GenerateSTART(I2C_TypeDef* I2Cx, FunctionalState NewState);

// 生成结束条件
void I2C_GenerateSTOP(I2C_TypeDef* I2Cx, FunctionalState NewState);

// STM32作为主机时是否给从机应答
void I2C_AcknowledgeConfig(I2C_TypeDef* I2Cx, FunctionalState NewState);
void I2C_OwnAddress2Config(I2C_TypeDef* I2Cx, uint8_t Address);
void I2C_DualAddressCmd(I2C_TypeDef* I2Cx, FunctionalState NewState);
void I2C_GeneralCallCmd(I2C_TypeDef* I2Cx, FunctionalState NewState);
void I2C_ITConfig(I2C_TypeDef* I2Cx, uint16_t I2C_IT, FunctionalState NewState);

// 发送一个字节的数据,实际就是写入DR寄存器
void I2C_SendData(I2C_TypeDef* I2Cx, uint8_t Data);

// 接收一个字节的数据,实际就是读取DR寄存器
uint8_t I2C_ReceiveData(I2C_TypeDef* I2Cx);

// 对7位地址的从机寻址。读写位要算在里面,加上地址共8位数据
void I2C_Send7bitAddress(I2C_TypeDef* I2Cx, uint8_t Address, uint8_t I2C_Direction);
uint16_t I2C_ReadRegister(I2C_TypeDef* I2Cx, uint8_t I2C_Register);
void I2C_SoftwareResetCmd(I2C_TypeDef* I2Cx, FunctionalState NewState);
void I2C_NACKPositionConfig(I2C_TypeDef* I2Cx, uint16_t I2C_NACKPosition);
void I2C_SMBusAlertConfig(I2C_TypeDef* I2Cx, uint16_t I2C_SMBusAlert);
void I2C_TransmitPEC(I2C_TypeDef* I2Cx, FunctionalState NewState);
void I2C_PECPositionConfig(I2C_TypeDef* I2Cx, uint16_t I2C_PECPosition);
void I2C_CalculatePEC(I2C_TypeDef* I2Cx, FunctionalState NewState);
uint8_t I2C_GetPEC(I2C_TypeDef* I2Cx);
void I2C_ARPCmd(I2C_TypeDef* I2Cx, FunctionalState NewState);
void I2C_StretchClockCmd(I2C_TypeDef* I2Cx, FunctionalState NewState);
void I2C_FastModeDutyCycleConfig(I2C_TypeDef* I2Cx, uint16_t I2C_DutyCycle);

// 基础状态监测
ErrorStatus I2C_CheckEvent(I2C_TypeDef* I2Cx, uint32_t I2C_EVENT);

// 高级状态监测
uint32_t I2C_GetLastEvent(I2C_TypeDef* I2Cx);

// 获取指定标志位的状态。基于标志位的状态监测
FlagStatus I2C_GetFlagStatus(I2C_TypeDef* I2Cx, uint32_t I2C_FLAG);

// 清除指定标志位
void I2C_ClearFlag(I2C_TypeDef* I2Cx, uint32_t I2C_FLAG);

// 获取指定的中断标志位
ITStatus I2C_GetITStatus(I2C_TypeDef* I2Cx, uint32_t I2C_IT);

// 清除指定的中断标志位
void I2C_ClearITPendingBit(I2C_TypeDef* I2Cx, uint32_t I2C_IT);

i2c状态监测函数

也就是监控前面的EVx事件,例如EV5表示起始条件已发送,EV6表示寻址完成

这个I2C驱动程序根据应用需求和约束提供了三种不同的I2C状态监测方式:

  1. 基础状态监测:

    • 使用I2C_CheckEvent()函数:

      /**
        * @作用  检查最后一个I2Cx事件是否等于作为参数传递的事件
        * @参数  I2Cx: 其中x可以是1或2来选择I2C外设。
        * @参数  I2C_EVENT: 指定要检查的事件
        *   该参数可以是以下值之一:
        *     @arg I2C_EVENT_SLAVE_TRANSMITTER_ADDRESS_MATCHED           : EV1
        *     @arg I2C_EVENT_SLAVE_RECEIVER_ADDRESS_MATCHED              : EV1
        *     @arg I2C_EVENT_SLAVE_TRANSMITTER_SECONDADDRESS_MATCHED     : EV1
        *     @arg I2C_EVENT_SLAVE_RECEIVER_SECONDADDRESS_MATCHED        : EV1
        *     @arg I2C_EVENT_SLAVE_GENERALCALLADDRESS_MATCHED            : EV1
        *     @arg I2C_EVENT_SLAVE_BYTE_RECEIVED                         : EV2
        *     @arg (I2C_EVENT_SLAVE_BYTE_RECEIVED | I2C_FLAG_DUALF)      : EV2
        *     @arg (I2C_EVENT_SLAVE_BYTE_RECEIVED | I2C_FLAG_GENCALL)    : EV2
        *     @arg I2C_EVENT_SLAVE_BYTE_TRANSMITTED                      : EV3
        *     @arg (I2C_EVENT_SLAVE_BYTE_TRANSMITTED | I2C_FLAG_DUALF)   : EV3
        *     @arg (I2C_EVENT_SLAVE_BYTE_TRANSMITTED | I2C_FLAG_GENCALL) : EV3
        *     @arg I2C_EVENT_SLAVE_ACK_FAILURE                           : EV3_2
        *     @arg I2C_EVENT_SLAVE_STOP_DETECTED                         : EV4
        *     @arg I2C_EVENT_MASTER_MODE_SELECT                          : EV5
        *     @arg I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED            : EV6 发送数据的模式
        *     @arg I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED               : EV6 接收数据的模式
        *     @arg I2C_EVENT_MASTER_BYTE_RECEIVED                        : EV7
        *     @arg I2C_EVENT_MASTER_BYTE_TRANSMITTING                    : EV8
        *     @arg I2C_EVENT_MASTER_BYTE_TRANSMITTED                     : EV8_2
        *     @arg I2C_EVENT_MASTER_MODE_ADDRESS10                       : EV9
        *     
        * @提示: 有关事件的详细描述,请参见stm32f10x_i2c.h文件中的I2C_Events部分。
        *    
        * @返回值 ErrorStatus枚举值:
        * - SUCCESS: Last event is equal to the I2C_EVENT
        * - ERROR: Last event is different from the I2C_EVENT
        */
      ErrorStatus I2C_CheckEvent(I2C_TypeDef* I2Cx, uint32_t I2C_EVENT)
      
    • 将状态寄存器(SR1和SR2)的内容与给定的事件进行比较(可以是一个或多个标志的组合)。

    • 如果当前状态包含给定标志,则返回SUCCESS
      如果当前状态中缺少一个或多个标志,则返回ERROR。

    • 何时使用

      此功能适用于大多数应用程序以及启动活动,因为这些事件在产品参考手册(RM0008)中有详细描述。

      它也适合需要定义自己的事件的用户

    • 局限性

      如果发生错误(例如:除了被监视的标志之外,还设置了错误标志),I2C_CheckEvent()函数可能会返回SUCCESS,尽管存在通信保持或损坏的实际状态。
      在这种情况下,建议使用错误中断来监视错误事件,并在中断IRQ处理程序中处理它们。

    • 对于错误管理,建议使用以下功能:

    • I2C_ITConfig()配置并启用错误中断(I2C_IT_ERR)。

    • I2Cx_ER_IRQHandler(),在发生错误中断时调用。

      其中x为外围设备实例(I2C1, I2C2…)

    • I2C_GetFlagStatus()或I2C_GetITStatus()被调用到I2Cx_ER_IRQHandler()中,以确定发生了哪个错误。

    • I2C_ClearFlag()或I2C_ClearITPendingBit()和/或i2c_softwareesetcmd()和/或I2C_GenerateStop(),以清除错误标志和源,并返回到正确的通信状态。

  2. 高级状态监测:

使用函数I2C_GetLastEvent(),把SR1和SR2两个状态寄存器的值拼接成16位的数据并返回

  1. 基于标志位的状态监控:

使用I2C_GetFlagStatus()函数返回单个标志位的状态

案例:硬件i2c读写

接线图

与软件i2c一致

示例代码

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

#define MPU6050_ADDR 			0xD0	// MPU6050设备地址

#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	// X轴的加速度高8位数据
#define	MPU6050_ACCEL_XOUT_L	0x3C	// X轴的加速度低8位数据
#define	MPU6050_ACCEL_YOUT_H	0x3D	// Y轴的加速度高8位数据
#define	MPU6050_ACCEL_YOUT_L	0x3E	// Y轴的加速度低8位数据
#define	MPU6050_ACCEL_ZOUT_H	0x3F	// Z轴的加速度高8位数据
#define	MPU6050_ACCEL_ZOUT_L	0x40	// Z轴的加速度低8位数据
#define	MPU6050_TEMP_OUT_H		0x41	// 温度数据的高8位数据
#define	MPU6050_TEMP_OUT_L		0x42	// 温度数据的低8位数据
#define	MPU6050_GYRO_XOUT_H		0x43	// X轴的角速度高8位数据
#define	MPU6050_GYRO_XOUT_L		0x44	// X轴的角速度低8位数据
#define	MPU6050_GYRO_YOUT_H		0x45	// Y轴的角速度高8位数据
#define	MPU6050_GYRO_YOUT_L		0x46	// Y轴的角速度低8位数据
#define	MPU6050_GYRO_ZOUT_H		0x47	// Z轴的角速度高8位数据
#define	MPU6050_GYRO_ZOUT_L		0x48	// Z轴的角速度低8位数据

#define	MPU6050_PWR_MGMT_1		0x6B	// 电源管理寄存器1
#define	MPU6050_PWR_MGMT_2		0x6C	// 电源管理寄存器2
#define	MPU6050_WHO_AM_I		0x75	// ID

/**
  * 函    数:一个事件的响应时间过长时,结束对该事件的等待
  * 参    数:与I2C_CheckEvent()函数保持一致
  * 返 回 值:无
  */
void MPU6050_WaitEvent(I2C_TypeDef* I2Cx, uint32_t I2C_EVENT)
{
	// 用于计次,当超过一定次数时该事件等待事件过长,直接结束对该事件的等待
	uint32_t i;
	
	// I2C_CheckEvent()函数:监测一个事件的状态
	while(I2C_CheckEvent(I2Cx, I2C_EVENT) != SUCCESS)
	{
		i++;
		if(i >= 10000){return;}
	}
}

/**
  * 函    数:发送一个字节的数据
  * 参    数:Reg_Addr MPU6050寄存器的地址
  * 参    数:Byte 要写入MPU6050寄存器的数据
  * 返 回 值:无
  */
void MPU6050_Write_Reg(uint8_t Reg_Addr, uint8_t Byte)
{
	// 起始条件
	I2C_GenerateSTART(I2C2, ENABLE);
	// 等待EV5事件
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);
	
	// 寻址。MPU6050的地址,写指令
	I2C_Send7bitAddress(I2C2, MPU6050_ADDR, I2C_Direction_Transmitter);
	// 等待EV6事件
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);
	
	// 不需要等待EV8_1事件
	
	// 发送MPU6050的寄存器地址
	I2C_SendData(I2C2, Reg_Addr);
	// 等待EV8事件,数据寄存器空,移位寄存非空
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTING);
	
	// 写入一个字节的数据
	I2C_SendData(I2C2, Byte);
	// 全部数据发送完毕,等待EV8_2事件。移位寄存器将数据全部发送完毕,且数据寄存器中没有数据需要发送了
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED);
	
	// 停止条件
	I2C_GenerateSTOP(I2C2, ENABLE);
}

/**
  * 函    数:读取一个MPU6050指定的寄存器中的数据
  * 参    数:Reg_Addr 指定的寄存器地址
  * 返 回 值:读取到的数据
  */
uint8_t MPU6050_Read_Reg(uint8_t Reg_Addr)
{
	/* 设置当前地址指针的指向 */
	// 起始条件
	I2C_GenerateSTART(I2C2, ENABLE);
	// 等待EV5事件
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);
	
	// 寻址。MPU6050的地址,写指令
	I2C_Send7bitAddress(I2C2, MPU6050_ADDR, I2C_Direction_Transmitter);
	// 等待EV6事件
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);
	
	// 发送寄存器地址,让当前地址指针的指向Reg_Addr的地方
	I2C_SendData(I2C2, Reg_Addr);
	// 等待EV8_2事件。让移位寄存器中的数据为空,准备读数据
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED);
	
	/* 当前地址读 */
	// 起始条件
	I2C_GenerateSTART(I2C2, ENABLE);
	// 等待EV5事件
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);
	
	// 寻址。读指令
	I2C_Send7bitAddress(I2C2, MPU6050_ADDR, I2C_Direction_Receiver);
	// 等待EV6事件
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED);
	
	// 因为只读取一个字节的数据,所以要在主机发送应答前将ACK置0,不给从机应答
	I2C_AcknowledgeConfig(I2C2, DISABLE);
	// 同时将停止位置1。在数据移位寄存器接收数据的途中设置停止位不会立刻生效,会在数据接收完毕后再发送停止条件的波形
	I2C_GenerateSTOP(I2C2, ENABLE);
	
	// 等待EV7事件,数据寄存器不为空
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_RECEIVED);
	// 读取数据寄存器中的数据并返回
	uint8_t Data = I2C_ReceiveData(I2C2);
	
	// 为了方便连续接收多个字节的时序,ACK默认置1,也就是主机在接收数据时默认给从机应答
	I2C_AcknowledgeConfig(I2C2, ENABLE);
	
	return Data;
}

/**
  * 函    数:初始化MPU6050、I2C
  * 参    数:无
  * 返 回 值:无
  */
void MPU6050_Init()
{
	// 开启GPIOB和I2C2的时钟
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2, ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
	
	GPIO_InitTypeDef GPIO_InitStruct;
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_OD;			//  复用开漏输出
	GPIO_InitStruct.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11;
	GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &GPIO_InitStruct);
	
	I2C_InitTypeDef I2C_InitStruct;
	I2C_InitStruct.I2C_Ack = I2C_Ack_Enable;								// 开启I2C
	I2C_InitStruct.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;	// 作为从机时的地址位数。7位
	I2C_InitStruct.I2C_ClockSpeed = 50000;									// SCL的时钟频率,也就是传输的速度。50KHz
	I2C_InitStruct.I2C_DutyCycle = I2C_DutyCycle_2;							// SCL的占空比。低电平与高低平的比例为2:1
	I2C_InitStruct.I2C_Mode = I2C_Mode_I2C;									// I2C模式
	I2C_InitStruct.I2C_OwnAddress1 = 0x00;									// 作为从机时的地址
	I2C_Init(I2C2, &I2C_InitStruct);
	
	// I2C2启动
	I2C_Cmd(I2C2, ENABLE);
	
	/*MPU6050寄存器初始化,需要对照MPU6050手册的寄存器描述配置,此处仅配置了部分重要的寄存器*/
	MPU6050_Write_Reg(MPU6050_PWR_MGMT_1, 0x01);		//电源管理寄存器1,取消睡眠模式,选择时钟源为X轴陀螺仪
	MPU6050_Write_Reg(MPU6050_PWR_MGMT_2, 0x00);		//电源管理寄存器2,保持默认值0,所有轴均不待机
	MPU6050_Write_Reg(MPU6050_SMPLRT_DIV, 0x09);		//采样率分频寄存器,配置采样率
	MPU6050_Write_Reg(MPU6050_CONFIG, 0x06);			//配置寄存器,配置DLPF
	MPU6050_Write_Reg(MPU6050_GYRO_CONFIG, 0x18);		//陀螺仪配置寄存器,选择满量程为±2000°/s
	MPU6050_Write_Reg(MPU6050_ACCEL_CONFIG, 0x18);		//加速度计配置寄存器,选择满量程为±16g
}

int main()
{
	MPU6050_Init();
	OLED_Init();
	
	// 对MPU6050可写的寄存器写入一个数
	MPU6050_Write_Reg(MPU6050_SMPLRT_DIV, 0x99);
	// 读出刚刚写入数据的寄存器中的值
	uint8_t Data = MPU6050_Read_Reg(MPU6050_SMPLRT_DIV);
	// 打印在OLED上检验代码是否正常
	OLED_ShowHexNum(1,1,Data,2);
	
	/* 关于MPU6050功能的实现在软件i2c中写过,这里就不过多赘述 */
	while(1)
	{
		
	}
}

标签:十一,i2c,通信,MPU6050,地址,寄存器,I2C,数据
From: https://www.cnblogs.com/liuhousheng/p/18026367

相关文章

  • 《Effective Java》阅读笔记-第十一章
    EffectiveJava阅读笔记第十一章并发第78条同步访问共享的可变数据多线程访问变量时,需要进行同步,否则就会产生并发问题。同步代码块、加锁等或者直接不共享变量,也就是将可变数据限制在单个线程内。ThreadLocal这种第79条避免过度同步为了避免活性失败和安全性失败......
  • 读程序是怎样跑起来的第十一章有感
    书中首先介绍了CPU的内部结构,如寄存器、程序计数器等,它们是CPU的核心部分,确保指令能够被正确执行。接着,详细解释了CPU如何与内存进行数据交换,这一过程看似简单,实则包含了大量的细节与原理。值得一提的是,书中对于硬件控制方法的讲解,特别是对输入输出指令IN和OUT的描述,让我感受到了......
  • 第十一章:硬件控制方法
    在阅读《程序是怎样跑起来》这本书的第十一章后,我对硬件控制方法有了更深入的了解,同时也对计算机硬件与软件之间的交互有了更清晰的认识。这一章主要围绕硬件控制方法展开,介绍了计算机硬件的基本组成,以及如何通过软件来控制硬件。首先,本章指出在高级编程语言如C语言中,开发者很少......
  • 第十一章——硬件控制方法
    本章对I/O端口的输入输出以及中断处理等用程序控制硬件的方法说明。控制输入输出的IN指令和OUT指令,IN指令应用于输入数据,并将其存储在CPU内部的寄存器中,而OUT指令就是把数据输出到指定端口号的端口。中断处理也就是IRQ,用来暂停当前正在运行的程序,并跳转到其他程序运行的必要机制......
  • CAN通信
    KvaserCANKing:模拟仿真CAN总线通讯  模拟器发送 ......
  • 《程序是怎样跑起来的》第十一、十二章读后感
    第十一章主要展示了程序的实际应用及其功能,并探讨了计算机硬件与软件的密切关系。在这一章中,作者提到了人工智能这一热门话题,指出其发展离不开计算机的支持。通过学习本章内容,我们可以了解到计算机在现代科技中的重要地位以及如何通过编程实现对硬件设备的控制。在计算机硬件方面......
  • Qt 使用Http协议通信
    介绍使用QT进行应用开发时,有时候需要进行客户端和服务端的网络通信,网络通信常用的一种协议就是http协议。QT对http协议进行了封装,下面将介绍两种http通信方式的使用。在使用http时需要在pro文件中添加对应的模块。QT+=networkhttp主要两种通信方式为get和post......
  • DSACTF 十一月挑战赛 IceTea
    DSACTF十一月挑战赛IceTea非常感谢C26H52大佬的博文,否则我个狒狒连官方wp都看不懂。拿到第一个http流,很长串Hex密文,丢进cyberchef发现是ELF文件,注意这里不要搞多了,然后丢进IDA发现需要upx脱壳,注意这里是要用4.2版本的3.95的貌似不行。然后发现多了一串字符串base字母表。然......
  • ASCII编码的诞生:解决字符标准化与跨平台通信的需求
    在计算机的发展过程中,字符的表示和传输一直是一个重要的问题。为了实现字符的标准化和跨平台通信,ASCII(AmericanStandardCodeforInformationInterchange)编码应运而生。Ascii编码解码|一个覆盖广泛主题工具的高效在线平台(amd794.com)https://amd794.com/asciiencordec......
  • 关于thrift python接口和java通信出现问题解决
    真的无语,搞了一个下午。使用thrift出现错误,先说一下遇到第一个错误,如下图:那时候代码是这叼样```if__name__=='__main__':handler=MessageServiceHandler()processor=MessageService.Processor(handler)transport=TSocket.TServerSocket(None,"9090"......