I2C通信
学习资料:
前言
线与:连接在总线上的设备只要有一个输出低电平(0)总线就为低电平(0),只有全部设备都为高阻态时总线才是高电平(1)
I2C简介
I2C(Inter IC Bus)是由Philips公司开发的一种通用数据总线
- 两根通信线:SCL(Serial Clock)、SDA(Serial Data)
- 同步,半双工
- 带数据应答
- 支持总线挂载多设备(一主多从、多主多从(可利用“线与”特性来执行时钟同步和总线仲裁))
I2C的硬件电路设计
所有I2C设备的SCL连在一起,SDA连在一起
设备的SCL和SDA均要配置成开漏输出模式
SCL和SDA各添加一个上拉电阻,阻值一般为4.7KΩ左右
我主要学习的是I2C一主多从的模式,所以下面以一主多从的模式(一主 即 MCU(STM32)来当作主机 ,其它设备当作从机)来讲解I2C的硬件电路是如何设计出来的
解释:
- 对于 SCL 时钟线 来说 它是由主机控制的 所以它可以是 开漏输出 亦可以是 推挽输出 都可以 ,但为什么选择了 开漏输出模式 是因为 :多主多从模式下可利用“线与”特性来执行 时钟同步和总线仲裁
- 对于 **SDA 时钟线 **来说 它是通信数据的线 不仅主机需要它进行 输出数据 还需要 接收从机发送过来的 数据 ,即在在此期间需要频繁的切换GPIO的输入输出模式 还要兼顾 不能在一个设备输出高电平 另一个设备输出低电平 导致的电源短路现象 所以为了防止总线没有协调好而导致此现象的产生 I2C设计是禁止所有设备输出强上拉的高电平 采用 外置弱上拉电阻加开漏输出模式
- 那此时 你可能有所疑惑: 选择开漏输出模式 那岂不是不能输入了 其实然也 不知道你是否还记得 学习GPIO时,我们的STM32输出配置框图中 无论我们选择什么输出模式 都是可以进行输入的 不信?请看下图 STM32数据手册上写到 当I/O端口被配置为输出时 施密特触发输入被激活 如果你对GPIO有所遗忘 可以去看我的另一篇关于STM32之GPIO外设 - Sakura_Ji - 博客园 的笔记
- 开漏输出模式: 设备输出1时是高阻态,在硬件电路设计图中即SCLKN1OUT/DATAN1OUT断开使引脚浮空,为了避免引脚浮空,通过外置的上拉电阻呈现弱上拉的高电平 但不影响数据的传输;设备输出0时是强下拉低电平
- 所有任何设备在任何时候 是都可以进行输入的 都可以通过一个数据缓冲器或者是施密特触发器,进行输入
通过上文可知:
- 设备在进行输出时: 低电平:强下拉的低电平 高电平: 弱上拉的高电平
- 设备在进行输入时: 可直接输出高电平(相当于高阻态 断开引脚) 然后观察总线的高低电平即可
I2C的软件设计
- 主机可以访问总线上的任何一个设备
- 要与那个设备进行通信 主机在起始条件后 需要先发送 该设备的地址
- 所有设备都会对这个地址进行判断,如果和自己的不一样会认为没有访问自己,之后的时序就不管了,如果一样会向主机发送应答,并准备响应之后主机的读写操作
- 同一条的I2C总线上的从机的设备地址要求不能相同
- 从机设备地址在I2C协议标准里分为7位地址和10位地址,7位地址应用最为广泛
- 以7位作为示例:厂商一般规定高4位是固定死的,但低3位是可以通过电路进行改变的,这样地址就可以不同,所以I2C总线可以搭载相同的设备
I2C最大的一个特点就是有完善的应答机制,从机(主机)接收到主机(从机)的数据时,会回复一个应答信号来通知主机表示“我收到了”。
应答信号: 出现在1个字节传输完成之后,即第9个SCL时钟周期内,此时主机需要释放SDA总线,把总线控制权交给从机,由于上拉电阻的作用,此时总线为高电平,如果从机正确的收到了主机发来的数据,会把SDA拉低,表示应答响应。
非应答信号:当第9个SCL时钟周期时,SDA保持高电平,表示非应答信号。
非应答信号可能是主机产生也可能是从机产生,产生非应答信号的情况主要有以下几种:
- I2C总线上没有主机所指定地址的从机设备;
- 从机正在执行一些操作,处于忙状态,还没有准备好与主机通讯;
- 主机发送的一些控制命令,从机不支持;
- 主机接收从机数据时,主机产生非应答信号,通知从机数据传输结束,不要再发数据了;
I2C的时序机制
-
总线空闲状态: SCL和SDK同时处于高电平
-
起始条件: SCL高电平期间,SDA从 高电平 切换到 低电平
-
终止条件: SCL高电平期间,SDA从 低电平 切换到 高电平
-
发送一个字节: SCL低电平期间,主机将数据位依次放到SDA线上(高位先行),然后释放SCL,从机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可发送一个字节
-
接收一个字节: SCL低电平期间,从机将数据位依次放到SDA线上(高位先行),然后释放SCL,主机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可接收一个字节(主机在接收之前,需要释放SDA)
-
发送应答:主机在接收完一个字节之后,在下一个 时钟发送一位数据,数据0表示应答,数据1表示非应答
-
接收应答:主机在发送完一个字节之后,在下一个时钟接收一位数据,判断从机是否应答,数据0表示应答,数据1表示非应答(主机在接收之前,需要释放SDA)
I2C的数据帧格式
I2C的数据帧格式有:指定地址写,当前地址读,指定地址读
指定地址写
对于指定设备(Slave Address),在指定地址(Reg Address)下,写入指定数据(Data)
-
总线由空闲状态转为起始位:在SCL高电平的期间,SDA下降沿触发
-
主机控制SDA 发送从机设备地址(7位地址) + 发送 读(1)/写(0)指令(1位):
-
数据变化:在SCL低电平期间,SDA进行数据变化
-
数据稳定:在SCL高电平期间,SDA保持不动
-
-
主机释放SDA 接收从机的应答位 从机控制SDA :
- 从机回复 0是应答 1为非应答
- 由于线与机制,当从机给予应答,从总线的现象上看 当主机释放SDA后 立刻被从机拉下并控制 所以在RA(Receive Ack)处显示的像未被释放过一样
-
主机得到从机的应答 从机释放SDA 主机控制SDA 发送寄存器地址(8位)
-
因为从机要在低电平尽快变化数据(释放SDA),所以SCL的下降沿和SDA的上升沿几乎是同时发生的
-
数据变化:在SCL低电平期间,SDA进行数据变化
-
数据稳定:在SCL高电平期间,SDA保持不动
-
-
主机释放SDA 接收从机的应答位 从机控制SDA :
- 从机回复 0是应答 1为非应答
- 由于线与机制,当从机给予应答,从总线的现象上看 当主机释放SDA后 立刻被从机拉下并控制 所以在RA(Receive Ack)处显示的像未被释放过一样
-
主机得到从机的应答 从机释放SDA 主机控制SDA 发送数据(8位)
-
因为从机要在低电平尽快变化数据(释放SDA),所以SCL的下降沿和SDA的上升沿几乎是同时发生的
-
数据变化:在SCL低电平期间,SDA进行数据变化
-
数据稳定:在SCL高电平期间,SDA保持不动
-
-
主机释放SDA 接收从机的应答位 从机控制SDA :
- 从机回复 0是应答 1为非应答
- 由于线与机制,当从机给予应答,从总线的现象上看 当主机释放SDA后 立刻被从机拉下并控制 所以在RA(Receive Ack)处显示的像未被释放过一样
-
主机得到从机的应答 从机释放SDA 主机控制SDA 产生停止条件
- 因为从机要在低电平尽快变化数据(释放SDA),所以SCL的下降沿和SDA的上升沿几乎是同时发生的
- 数据变化:在SCL低电平期间,SDA进行数据变化
- 如果主机想结束通讯 就可以产生停止条件 在停止条件之前 ,先拉低SDA,为后续SDA的上升沿做准备
如果主机想要继续传输 就可以继续发送数据 它的数据会自动写入下一个寄存器地址的位置(单独的记录地址的指针变量会自增 在下文当前地址读中有解释为什么)
数据稳定:在SCL高电平期间,SDA保持不动
-
在SCL高电平的期间,SDA上升沿触发 产生停止位 转向 总线空闲状态
当前地址读
对于指定设备(Slave Address),在当前地址指针指示的地址下,读取从机数据(Data)
-
总线由空闲状态转为起始位:在SCL高电平的期间,SDA下降沿触发
-
主机控制SDA 发送从机设备地址(7位地址) + 发送 读(1)/写(0)指令(1位):
-
数据变化:在SCL低电平期间,SDA进行数据变化
-
数据稳定:在SCL高电平期间,SDA保持不动
-
-
主机释放SDA 接收从机的应答位 从机控制SDA :
- 从机回复 0是应答 1为非应答
- 由于线与机制,当从机给予应答,从总线的现象上看 当主机释放SDA后 立刻被从机拉下并控制 所以在RA(Receive Ack)处显示的像未被释放过一样
-
主机得到从机的应答 从机继续控制SDA 从机发送数据(8位) -- 数据传输方向 变换
-
数据变化:在SCL低电平期间,SDA进行数据变化
-
数据稳定:在SCL高电平期间,SDA保持不动
-
那么问题来了 -- 此时从机是从将哪个寄存器的数据 传给主机的呢? 在I2C协议的规定中,在主机进行寻址时,一旦读写标志位给了1,下一个字节要立马转为读的时序,所以主机还来不及指定,我要读那个寄存器,就要开始接收数据了,所以这里没有指定地址这个环节,那从机该发哪一个寄存器的数据呢?
在从机中,所有的寄存器都被分配到了一个线性区域中,并且会有一个单独的记录地址的指针变量,指示着其中的一个寄存器,这个指针上电默认指向0地址,并且每写入一个字节和读出一个字节后,这个指针会自动自增一次,移动到下一个位置
那么在调用当前地址读的时序时,主机没有指定要读那个地址,从机就会返回当前 记录地址的那个指针变量的 指针指向的寄存器的值,举例:
- 上一步刚刚调用了指定地址写的时序,在0X19的位置写入了0XAA,那么 记录地址的那个指针变量 就会自动加一 移动到 0X1A的位置
- 之后再调用当前地制读的时序,读取的就是0X1A这个地址的寄存器中的值
- 再继续读数据,读取的就是0X1B这个地址的寄存器中的值
- ···
- 以此类推
-
-
从机释放SDA 接收主机的应答位 主机控制SDA 产生停止条件
- 从机回复 0是应答 1为非应答
- 由于线与机制,当从机给予应答,从总线的现象上看 当从机释放SDA后 由于主机是非应答 所以总线依旧处于上拉状态 所以在SA(Send Ack)处显示的像未被释放过一样
- 当从机未得到主机的应答时,从机将不会再继续发送给主机数据,由主机控制SDA
- 数据变化:在SCL低电平期间,SDA进行数据变化
- 如果主机想要结束通讯 就可以产生停止条件 在停止条件之前 ,先拉低SDA,为后续SDA的上升沿做准备
如果主机想要继续读取 上面就得应答从机 然后可以继续读取下一个寄存器地址中的数据
数据稳定:在SCL高电平期间,SDA保持不动
-
在SCL高电平的期间,SDA上升沿触发 产生停止位 转向 总线空闲状态
指定地址读
对于指定设备(Slave Address),在指定地址(Reg Address)下,读取从机数据(Data)
-
总线由空闲状态转为起始位:在SCL高电平的期间,SDA下降沿触发
-
主机控制SDA 发送从机设备地址(7位地址) + 发送 读(1)/写(0)指令(1位):
-
数据变化:在SCL低电平期间,SDA进行数据变化
-
数据稳定:在SCL高电平期间,SDA保持不动
-
-
主机释放SDA 接收从机的应答位 从机控制SDA :
- 从机回复 0是应答 1为非应答
- 由于线与机制,当从机给予应答,从总线的现象上看 当主机释放SDA后 立刻被从机拉下并控制 所以在RA(Receive Ack)处显示的像未被释放过一样
-
主机得到从机的应答 从机释放SDA 主机控制SDA 发送寄存器地址(8位)
-
因为从机要在低电平尽快变化数据(释放SDA),所以SCL的下降沿和SDA的上升沿几乎是同时发生的
-
数据变化:在SCL低电平期间,SDA进行数据变化
-
数据稳定:在SCL高电平期间,SDA保持不动
-
-
主机释放SDA 接收从机的应答位 从机控制SDA :
- 从机回复 0是应答 1为非应答
- 由于线与机制,当从机给予应答,从总线的现象上看 当主机释放SDA后 立刻被从机拉下并控制 所以在RA(Receive Ack)处显示的像未被释放过一样
-
主机得到从机的应答 从机释放SDA 主机控制SDA 重新起始SR(Start Repeat)
- 因为从机要在低电平尽快变化数据(释放SDA),所以SCL的下降沿和SDA的上升沿几乎是同时发生的
- 数据变化:在SCL低电平期间,SDA进行数据变化
- 产生重新起始条件,从机释放SDA后,主机依旧保持SDA上拉状态,为后续SDA的下降沿做准备
- 那么问题来了 -- 重新起始是什么鬼?为什么这么操作,上文说过 有一个单独的记录地址的指针变量 我们先像那个指定的寄存器地址进行写入操作(但不真正的写入 所以指针不会自增),重新起始,然后进行当前地址读操作,这样不就完美的实现了 指定地址读操作嘛 看不懂去看当前地址读中的举例
- 重新起始相当于另起一个时序,因为读写标志位只能是跟着起始条件的第一个字节,因此想要切换读写操作,只能再启动一次时序即重新起始
- 起始位:在SCL高电平的期间,SDA下降沿触发
-
主机控制SDA 发送从机设备地址(7位地址) + 发送 读(1)/写(0)指令(1位):
-
数据变化:在SCL低电平期间,SDA进行数据变化
-
数据稳定:在SCL高电平期间,SDA保持不动
-
-
主机释放SDA 接收从机的应答位 从机控制SDA :
- 从机回复 0是应答 1为非应答
- 由于线与机制,当从机给予应答,从总线的现象上看 当主机释放SDA后 立刻被从机拉下并控制 所以在RA(Receive Ack)处显示的像未被释放过一样
-
主机得到从机的应答 从机继续控制SDA 从机发送数据(8位) -- 数据传输方向 变换
-
数据变化:在SCL低电平期间,SDA进行数据变化
-
数据稳定:在SCL高电平期间,SDA保持不动
-
-
从机释放SDA 接收主机的应答位 主机控制SDA 产生停止条件
- 从机回复 0是应答 1为非应答
- 由于线与机制,当从机给予应答,从总线的现象上看 当从机释放SDA后 由于主机是非应答 所以总线依旧处于上拉状态 所以在SA(Send Ack)处显示的像未被释放过一样
- 当从机未得到主机的应答时,从机将不会再继续发送给主机数据,由主机控制SDA
- 数据变化:在SCL低电平期间,SDA进行数据变化
- 如果主机想结束通讯 就可以产生停止条件 在停止条件之前 ,先拉低SDA,为后续SDA的上升沿做准备
如果主机想要继续读取 上面就得应答从机 然后可以继续读取下一个寄存器地址中的数据
数据稳定:在SCL高电平期间,SDA保持不动
-
在SCL高电平的期间,SDA上升沿触发 产生停止位 转向 总线空闲状态
STM32之I2C外设
I2C的外设应用
MPU6050的介绍
MPU6050是一个6轴姿态传感器,可以测量芯片自身X、Y、Z轴的加速度、角速度参数,通过数据融合,可进一步得到姿态角,常应用于平衡车、飞行器等需要检测自身姿态的场景
-
3轴加速度计(Accelerometer):测量X、Y、Z轴的加速度
-
3轴陀螺仪传感器(Gyroscope):测量X、Y、Z轴的角速度
MPU6050的参数:
-
16位ADC采集传感器的模拟信号,量化范围:-32768~32767
-
加速度计满量程选择:±2、±4、±8、±16(g)
-
陀螺仪满量程选择: ±250、±500、±1000、±2000(°/sec)
-
可配置的数字低通滤波器
-
可配置的时钟源
-
可配置的采样分频
-
I2C的地址
- 1101000(AD0=0)
- 1101001(AD0=1)
未待完续之更详细的MPU6050的笔记
I2C的实战演习
为什么分 软件读写 和 硬件读写 ?
- 软件读写 是完全利用I2C的基本原理 时序来写的 ,也就是说无论使用那个 GPIO口都可以实现本操作,甚至你可以使用其它型号的MCU都可以的,只要逻辑和软件读写的一样,当然还要看一下双方支持最大的引脚接收翻转的频率
- 硬件读写 因为在这里学习的是STM32单片机,而且它内部已经拥有I2C外设,这样很方便快捷我们的操作,所以在了解I2C是什么的基础上我们学习STM32的I2C外设会更加快速方便助我们使用 -- 库函数
软件模拟I2C之MPU6050
MyI2C.h
#ifndef __MYI2C_H__//如果没有定义了则参加以下编译
#define __MYI2C_H__//一旦定义就有了定义 所以 其目的就是防止模块重复编译
#include "stm32f10x.h"
#include "Delay.h"
void MyI2C_W_SCL(uint8_t BitValue);
void MyI2C_W_SDA(uint8_t BitValue);
uint8_t MyI2C_R_SDA(void);
void MyI2C_Init(void);
void MyI2C_Start(void);
void MyI2C_Stop(void);
void MyI2C_SendByte(uint8_t Byte);
uint8_t MyI2C_ReceiveByte(void);
void MyI2C_SendAck(uint8_t AckBit);
uint8_t MyI2C_ReceiveAck(void);
#endif //结束编译
MyI2C.c
#include "MyI2C.h"
/*
PB10 -- SCL
PB11 -- SDA
*/
//以下这些引脚操作 可使用带参宏定义
/**
* @brief 主机发送时钟SCL
* @param
* @retval
*/
void MyI2C_W_SCL(uint8_t BitValue)
{
GPIO_WriteBit(GPIOB, GPIO_Pin_10, (BitAction)BitValue);
Delay_us(10);//如果单片机主频比较快 可增加引脚延时
}
/**
* @brief 主机发送数据SDA -- 按位 每次都是改变GPIO的高低电平
BitValue 即使传入的是0X80 -- (BitAction)转成1 传入0X00 -- (BitAction)转成0
* @param
* @retval
*/
void MyI2C_W_SDA(uint8_t BitValue)
{
GPIO_WriteBit(GPIOB, GPIO_Pin_11, (BitAction)BitValue);
Delay_us(10);
}
/**
* @brief 主机接收数据SDA -- 按位 每次都是GPIO接收外部电平的变化
* @param
* @retval
*/
uint8_t MyI2C_R_SDA(void)
{
uint8_t BitValue;
BitValue = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11);
Delay_us(10);
return BitValue;
}
/**
* @brief I2C引脚初始化
* @param
* @retval
*/
void MyI2C_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
//为初始化函数做准备
GPIO_InitTypeDef GPIO_InitStructure;//定义结构体
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10|GPIO_Pin_11;//设置PB10,PB11引脚
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD ;//设置输出模式为开漏输出(也是可以输入的 先输出1 再读取输入数据寄存器就可)
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz ;//设置输出速度为50MHZ
//初始化函数↓
GPIO_Init(GPIOB,&GPIO_InitStructure);//初始化
GPIO_SetBits(GPIOB,GPIO_Pin_10|GPIO_Pin_11);//初始化为高电平 即总线空闲状态
}
/**
* @brief I2C起始条件 -- SCL高电平期间,SDA从 高电平 切换到 低电平
* @param
* @retval
*/
void MyI2C_Start(void)
{
MyI2C_W_SDA(1);//SDA在前 为了确保SDA上升沿
MyI2C_W_SCL(1);
MyI2C_W_SDA(0);
MyI2C_W_SCL(0);//起始条件后 将SCL拉低 拼接发送发送数据格式
}
/**
* @brief I2C停止条件 -- SCL高电平期间,SDA从 低电平 切换到 高电平
* @param
* @retval
*/
void MyI2C_Stop(void)
{
MyI2C_W_SDA(0);//SDA在前 为了确保SDA下降沿
MyI2C_W_SCL(1);
MyI2C_W_SDA(1);
}
/**
* @brief I2C发送字节 -- SCL低电平期间,主机将数据位依次放到SDA线上(高位先行),然后释放SCL,
从机将在SCL高电平期间读取数据位
* @param
* @retval
*/
void MyI2C_SendByte(uint8_t Byte)
{
uint8_t i;
for (i = 0; i < 8; i ++)
{
MyI2C_W_SDA(Byte & (0x80 >> i));//按位操作 依次取最高位 -- 第一个对应上 起始条件的SCL低电平
MyI2C_W_SCL(1);//高电平期间发送数据
MyI2C_W_SCL(0);//低电平期间数据变换
}
}
/**
* @brief I2C接收字节 -- SCL低电平期间,从机将数据位依次放到SDA线上(高位先行),然后释放SCL,
主机将在SCL高电平期间读取数据位
* @param
* @retval
*/
uint8_t MyI2C_ReceiveByte(void)
{
uint8_t i, Byte = 0x00;
MyI2C_W_SDA(1);//为了防止主机干扰从机的数据发送 主机将开启并保持高阻态 总线SDA只能由从机控制 也就相当于开启了输入模式
for (i = 0; i < 8; i ++)
{
MyI2C_W_SCL(1);//①接受完应答位后 先把时钟线拉高 来读取从机发送过来的数据 -- 高电平期间接收数据
if (MyI2C_R_SDA() == 1){Byte |= (0x80 >> i);}//那个字节是 就把 那一位置1 高位先行
MyI2C_W_SCL(0);//低电平期间数据变换
}
return Byte;
}
/**
* @brief I2C主机发送应答 -- 数据0表示应答,数据1表示非应答
* @param
* @retval
*/
void MyI2C_SendAck(uint8_t AckBit)
{
MyI2C_W_SDA(AckBit);//是否应答
MyI2C_W_SCL(1);//高电平期间发送数据
MyI2C_W_SCL(0);//拉低SCL
}
/**
* @brief I2C主机接收应答 -- 数据0表示应答,数据1表示非应答
* @param
* @retval
*/
uint8_t MyI2C_ReceiveAck(void)
{
uint8_t AckBit;
MyI2C_W_SDA(1);//主机释放SDA 从机控制SDA
MyI2C_W_SCL(1);//高电平期间接收数据
AckBit = MyI2C_R_SDA();//接收从机发送过来的应答位
MyI2C_W_SCL(0);//拉低SCL
return AckBit;//返回 从机是否应答
}
可以在主函数中使用下面的代码 来检测I2C的代码逻辑是否有误 没有则 MPU6050会传给你一个0应答
OLED_Init(); MyI2C_Init(); MyI2C_Start(); MyI2C_SendByte(0XD0); uint8_t ACK = MyI2C_ReceiveAck(); OLED_ShowNum(1,1,ACK,2);
MPU6050.h
#ifndef __MPU6050_H__//如果没有定义了则参加以下编译
#define __MPU6050_H__//一旦定义就有了定义 所以 其目的就是防止模块重复编译
#include "stm32f10x.h"
#include "MyI2C.h"
#define MPU6050_ADDRESS 0xD0 //默认为写操作 1101 000 0
#define MPU6050_SMPLRT_DIV 0x19 //采样率分频寄存器地址 -- 地址内容就是采样分频
#define MPU6050_CONFIG 0x1A //配置寄存器 -- Bit5~3(外部同步 000不需要) Bit2~0(数字低通滤波器 -- 110最平滑的滤波)
#define MPU6050_GYRO_CONFIG 0x1B //陀螺仪寄存器 -- Bit7~5(自测使能 000不自测)Bit4~3(满量程选择 11最大量程)后三位为无关位
#define MPU6050_ACCEL_CONFIG 0x1C //加速度计配置寄存器 -- Bit7~5(自测使能 000不自测)Bit4~3(满量程选择 11最大量程)Bit2~0(高通滤波器 000不使用)
#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 //电源管理 --设备复位(不复位),睡眠模式(0解除睡眠),循环模式(0不循环),无关位(给0),温度传感器(0不失能),时钟(000选择内部时钟 001陀螺仪时钟)
#define MPU6050_PWR_MGMT_2 0x6C //电源管理 --(前两位)循环模式和唤醒频率(00不需要) 后6位每一个轴的待机位(全给0 不需要待机)
#define MPU6050_WHO_AM_I 0x75 //查询芯片ID号 -- 0X68 0 110 1000 其实就是7地址
void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data);
uint8_t MPU6050_ReadReg(uint8_t RegAddress);
void MPU6050_Init(void);
uint8_t MPU6050_GetID(void);
void MPU6050_GetData(int16_t *AccX, int16_t *AccY, int16_t *AccZ,
int16_t *GyroX, int16_t *GyroY, int16_t *GyroZ);
#endif //结束编译
MPU6050.c
#include "MPU6050.h"
//此代码优化处 可处理是否应答了
/**
* @brief 指定地址写 -- 对于指定设备(Slave Address),在指定地址(Reg Address)下,写入指定数据(Data)
* @param
* @retval
*/
void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data)
{
MyI2C_Start();
MyI2C_SendByte(MPU6050_ADDRESS);//指定设备·写
MyI2C_ReceiveAck();
MyI2C_SendByte(RegAddress);//指定地址
MyI2C_ReceiveAck();
//想要写入多个字节 可以 在把下面两行代码 加入一个for循环 然后参数输入一个数组即可
MyI2C_SendByte(Data);//写入数据 1字节
MyI2C_ReceiveAck();
MyI2C_Stop();
}
/**
* @brief 指定地址读 -- 对于指定设备(Slave Address),在指定地址(Reg Address)下,读取从机数据(Data)
* @param
* @retval
*/
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();
//想要写入多个字节 可以 在把下面代码 加入一个for循环 读出来的数据保存到一个数组内
//同时要改为 应答从机 MyI2C_SendAck(0); 在读完最后一个 不应答 MyI2C_SendAck(1);
Data = MyI2C_ReceiveByte();//读取数据 1字节
MyI2C_SendAck(1);
MyI2C_Stop();
return Data;
}
void MPU6050_Init(void)
{
MyI2C_Init();
MPU6050_WriteReg(MPU6050_PWR_MGMT_1, 0x01); //电源管理1
MPU6050_WriteReg(MPU6050_PWR_MGMT_2, 0x00); //电源管理2
MPU6050_WriteReg(MPU6050_SMPLRT_DIV, 0x09); //采样率分频
MPU6050_WriteReg(MPU6050_CONFIG, 0x06); //配置寄存器
MPU6050_WriteReg(MPU6050_GYRO_CONFIG, 0x18); //陀螺仪寄存器
MPU6050_WriteReg(MPU6050_ACCEL_CONFIG, 0x18); //加速度计配置寄存器
}
/**
* @brief 获取MPU6050的ID号
* @param
* @retval
*/
uint8_t MPU6050_GetID(void)
{
return MPU6050_ReadReg(MPU6050_WHO_AM_I);
}
/**
* @brief 获取加速度计,陀螺仪数据
* @param
* @retval
*/
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;//加速度计X 16位的数据 PS:虽然DataH是8位的 然后左移8位 由于运算时计算结果并不存储在data变量中 所以最后赋值给16位的也没什么影响
DataH = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_H);
DataL = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_L);
*AccY = (DataH << 8) | DataL;//加速度计Y
DataH = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_H);
DataL = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_L);
*AccZ = (DataH << 8) | DataL;//加速度计Z
DataH = MPU6050_ReadReg(MPU6050_GYRO_XOUT_H);
DataL = MPU6050_ReadReg(MPU6050_GYRO_XOUT_L);
*GyroX = (DataH << 8) | DataL;//陀螺仪X
DataH = MPU6050_ReadReg(MPU6050_GYRO_YOUT_H);
DataL = MPU6050_ReadReg(MPU6050_GYRO_YOUT_L);
*GyroY = (DataH << 8) | DataL;//陀螺仪Y
DataH = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_H);
DataL = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_L);
*GyroZ = (DataH << 8) | DataL;//陀螺仪Z
}
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "MyI2C.h"
#include "MPU6050.h"
uint8_t ID;
int16_t AX, AY, AZ, GX, GY, GZ;
int main(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
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);
}
}