I2C通信协议详解和通信流程分析

一.I2C简介
I2C(Inter IC Bus)是由Philips公司开发的一种通用数据总线,I2C 总线由两根线组成:串行数据线(SDA)和串行时钟线(SCL),具有同步、半双工、带数据应答、支持总线挂载多设备(一主多从、多主多从)的特点。其硬件电路图如下所示,所有I2C设备的SCL连在一起,SDA连在一起,设备的SCL和SDA均要配置成开漏输出模式,SCL和SDA各添加一个上拉电阻,阻值一般为4.7KΩ左右。

二.I2C总线的时序基本单元
(1)开始信号、结束信号

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

终止条件:SCL高电平期间,SDA从低电平切换到高电平。
【腾讯文档】I2C通信协议详解 1.pdf
https://docs.qq.com/pdf/DRXhaekhGWnhqUmpC
在这里插入图片描述在这里插入图片描述
在这里插入图片描述
在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述
在这里插入图片描述在这里插入图片描述在这里插入图片描述

(2)数据发送与接收

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

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

(3)应答信号

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

三.I2C总线的时序
(1)指定地址写:对于指定设备(Slave Address),在指定地址(Reg Address)下,写入指定数据(Data),对应波形如下(参考自江科协资料)。

(2)当前地址读:对于指定设备(Slave Address),在当前地址指针指示的地址下,读取从机数据(Data),对应波形如下(参考自江科协资料)。

(3)指定地址读:对于指定设备(Slave Address),在指定地址(Reg Address)下,读取从机数据(Data) ,对应波形如下(参考自江科协资料)。

四.STM32 I2C 外设简介
STM32内部集成了硬件I2C收发电路,可以由硬件自动执行时钟生成、起始终止条件生成、应答位收发、数据收发等功能,减轻CPU的负担,支持多主机模型,支持7位/10位地址模式,支持不同的通讯速度,标准速度(高达100 kHz),快速(高达400 kHz),支持DMA,兼容SMBus协议,其功能框图如下。 (注:STM32F103C8T6 硬件I2C资源:I2C1、I2C2)

五. 硬件I2C读写MPU6050
硬件I2C是指直接利用 STM32 芯片中的硬件 I2C 外设,配置好对应的寄存器,外设就会产生标准串口协议的时序,使用I2C 外设则可以方便地通过外设寄存器来控制硬件I2C外设产生 I2C 协议方式的通讯,而不需要内核直接控制引脚的电平。

采用stm32f103c8t6,面包板,MPU6050,OLED,stlink进行实验,接线图如下所示(参考自江科协资料)。

MPU6050简介
MPU6050是一个6轴姿态传感器,可以测量芯片自身X、Y、Z轴的加速度、角速度参数,通过数据融合,可进一步得到姿态角,常应用于平衡车、飞行器等需要检测自身姿态的场景,其中3轴加速度计(Accelerometer):测量X、Y、Z轴的加速度,3轴陀螺仪传感器(Gyroscope):测量X、Y、Z轴的角速度。其电路图、框图如下(参考自江科协资料)。

MPU6050参数、特点如下:

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

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

3.陀螺仪满量程选择: ±250、±500、±1000、±2000(°/sec)

4.可配置的数字低通滤波器、时钟源、采样分频

5.I2C从机地址:1101000(AD0=0)或 1101001(AD0=1)

实现程序
首先对所需的GPIO和I2C进行配置,根据主发送器传送序列图可得主机向从机发送数据逻辑,根据主接受器传送序列图可得主机从从机读取数据逻辑,最后编写从MPU6050获取数据的函数。(相应寄存器介绍可参考STM32F10XXX参考手册24章I2C接口中24.6 I 2 C寄存器描述)

主发送函数

void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data)
{
I2C_GenerateSTART(I2C2, ENABLE); //硬件I2C生成起始条件
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT); //等待EV5

I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter);	//硬件I2C发送从机地址,方向为发送
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);	//等待EV6

I2C_SendData(I2C2, RegAddress);											//硬件I2C发送寄存器地址
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTING);			//等待EV8

I2C_SendData(I2C2, Data);												//硬件I2C发送数据
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED);				//等待EV8_2

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

}
AI写代码

主接收函数

uint8_t MPU6050_ReadReg(uint8_t RegAddress)
{
uint8_t Data;

I2C_GenerateSTART(I2C2, ENABLE);										//硬件I2C生成起始条件
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);					//等待EV5

I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter);	//硬件I2C发送从机地址,方向为发送 主机发送
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);	//等待EV6

I2C_SendData(I2C2, RegAddress);											//硬件I2C发送寄存器地址
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED);				//等待EV8_2

I2C_GenerateSTART(I2C2, ENABLE);										//硬件I2C生成重复起始条件
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);					//等待EV5

I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Receiver);		//硬件I2C发送从机地址,方向为接收 主机接收
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED);		//等待EV6

I2C_AcknowledgeConfig(I2C2, DISABLE);									//在接收最后一个字节之前提前将应答失能
I2C_GenerateSTOP(I2C2, ENABLE);											//在接收最后一个字节之前提前申请停止条件 对应时序图EV7_1

MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_RECEIVED);				//等待EV7
Data = I2C_ReceiveData(I2C2);											//接收数据寄存器

I2C_AcknowledgeConfig(I2C2, ENABLE);									//开始ACK默认是应答的,将应答恢复为使能,为了不影响后续可能产生的读取多字节操作

return Data;

}
AI写代码

所使用的MPU6050.c、MPU6050.h、MPU6050_Reg.h如下所示。

#include “stm32f10x.h”
#include “MPU6050_Reg.h”

#define MPU6050_ADDRESS 0xD0 //MPU6050的I2C从机地址

/**

  • 函 数:MPU6050等待事件
  • 参 数:同I2C_CheckEvent
  • 返 回 值:无
    /
    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) //自减到0后,等待超时
    {
    /超时的错误处理代码,可以添加到此处/
    break; //跳出等待,不等了
    }
    }
    }

/**

  • 函 数:MPU6050写寄存器

  • 参 数:RegAddress 寄存器地址,范围:参考MPU6050手册的寄存器描述

  • 参 数:Data 要写入寄存器的数据,范围:0x00~0xFF

  • 返 回 值:无
    */
    void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data)
    {
    I2C_GenerateSTART(I2C2, ENABLE); //硬件I2C生成起始条件
    MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT); //等待EV5

    I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter); //硬件I2C发送从机地址,方向为发送
    MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED); //等待EV6

    I2C_SendData(I2C2, RegAddress); //硬件I2C发送寄存器地址
    MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTING); //等待EV8

    I2C_SendData(I2C2, Data); //硬件I2C发送数据
    MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED); //等待EV8_2

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

/**

  • 函 数:MPU6050读寄存器

  • 参 数:RegAddress 寄存器地址,范围:参考MPU6050手册的寄存器描述

  • 返 回 值:读取寄存器的数据,范围:0x00~0xFF
    */
    uint8_t MPU6050_ReadReg(uint8_t RegAddress)
    {
    uint8_t Data;

    I2C_GenerateSTART(I2C2, ENABLE); //硬件I2C生成起始条件
    MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT); //等待EV5

    I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter); //硬件I2C发送从机地址,方向为发送 主机发送
    MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED); //等待EV6

    I2C_SendData(I2C2, RegAddress); //硬件I2C发送寄存器地址
    MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED); //等待EV8_2

    I2C_GenerateSTART(I2C2, ENABLE); //硬件I2C生成重复起始条件
    MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT); //等待EV5

    I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Receiver); //硬件I2C发送从机地址,方向为接收 主机接收
    MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED); //等待EV6

    I2C_AcknowledgeConfig(I2C2, DISABLE); //在接收最后一个字节之前提前将应答失能
    I2C_GenerateSTOP(I2C2, ENABLE); //在接收最后一个字节之前提前申请停止条件 对应时序图EV7_1

    MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_RECEIVED); //等待EV7
    Data = I2C_ReceiveData(I2C2); //接收数据寄存器

    I2C_AcknowledgeConfig(I2C2, ENABLE); //开始ACK默认是应答的,将应答恢复为使能,为了不影响后续可能产生的读取多字节操作

    return Data;
    }

/**

  • 函 数:MPU6050初始化

  • 参 数:无

  • 返 回 值:无
    */
    void MPU6050_Init(void)
    {
    /开启时钟/
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2, ENABLE); //开启I2C2的时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); //开启GPIOB的时钟

    /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); //将PB10和PB11引脚初始化为复用开漏输出

    /I2C初始化/
    I2C_InitTypeDef I2C_InitStructure; //定义结构体变量
    I2C_InitStructure.I2C_Mode = I2C_Mode_I2C; //模式,选择为I2C模式
    I2C_InitStructure.I2C_ClockSpeed = 50000; //时钟速度,选择为50KHz
    I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2; //时钟占空比,选择Tlow/Thigh = 2
    //时空占空比参数只有在时钟频率大于100kHz(也就是进入到快速状态才有用,在小于100kHz的情况下,占空比(低电平时间:高电平时间)是固定的1:1)
    I2C_InitStructure.I2C_Ack = I2C_Ack_Enable; //应答,选择使能
    I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit; //指定stm32作为从机,可以响应几位地址,此处应答地址,选择7位,从机模式下才有效
    I2C_InitStructure.I2C_OwnAddress1 = 0x00; //自身地址,从机模式下才有效
    I2C_Init(I2C2, &I2C_InitStructure); //将结构体变量交给I2C_Init,配置I2C2

    /I2C使能/
    I2C_Cmd(I2C2, ENABLE); //使能I2C2,开始运行

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

/**

  • 函 数:MPU6050获取ID号
  • 参 数:无
  • 返 回 值:MPU6050的ID号
    */
    uint8_t MPU6050_GetID(void)
    {
    return MPU6050_ReadReg(MPU6050_WHO_AM_I); //返回WHO_AM_I寄存器的值
    }

/**

  • 函 数:MPU6050获取数据

  • 参 数:AccX AccY AccZ 加速度计X、Y、Z轴的数据,使用输出参数的形式返回,范围:-32768~32767

  • 参 数:GyroX GyroY GyroZ 陀螺仪X、Y、Z轴的数据,使用输出参数的形式返回,范围:-32768~32767

  • 返 回 值:无
    */
    /**函数注解
    传值 vs 传指针
    如果写成 int16_t AccX(不是指针),函数收到的是 值拷贝,你在函数里改 AccX 跟外面的变量毫无关系。
    写成 int16_t *AccX,函数收到的是 地址,通过 *AccX = … 就能直接改调用者的变量。
    这就是 C 语言里典型的 “输出参数” 用法:
    “我不需要把值传进来,而是让函数把结果填到我给的地址里。”
    */
    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; //定义数据高8位和低8位的变量

    DataH = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_H); //读取加速度计X轴的高8位数据
    DataL = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L); //读取加速度计X轴的低8位数据
    *AccX = (DataH << 8) | DataL; //数据拼接,通过输出参数返回
    //*AccX 的意思是:
    //“把 AccX 这个指针里保存的地址找到,然后把右边的 16 位结果写到那个地址对应的内存里。”换句话说,改的是调用者手里的那个变量,而不是函数内部的一个副本。

    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; //数据拼接,通过输出参数返回
    ![](https://i-blog.csdnimg.cn/direct/6e16c1b921a5427888d4892b5cdf1a73.png

    DataH = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_H); //读取陀螺仪Z轴的高8位数据
    DataL = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_L); //读取陀螺仪Z轴的低8位数据
    *GyroZ = (DataH << 8) | DataL; //数据拼接,通过输出参数返回
    }
    AI写代码

#ifndef __MPU6050_H
#define __MPU6050_H

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
AI写代码

#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
AI写代码

主函数
在这里插入图片描述

#include “stm32f10x.h”
#include “Delay.h”
#include “OLED.h”
#include “MPU6050.h”

uint8_t ID; //定义用于存放ID号的变量
int16_t AX, AY, AZ, GX, GY, GZ; //定义用于存放各个数据的变量

int main(void)
{
/模块初始化/

————————————————
版权声明:本文为CSDN博主「sirris」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/sirris/article/details/149725427

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值