STM32 I2C(二)软件I2C读写MPU6050

STM32 I2C(二)软件I2C读写MPU6050

程序总体架构

由于这个代码使用的是软件I2C,就是用普通的GPIO口,手动翻转电平实现的协议,它并不需要STM32内部的外设资源支持,所以SCL和SDA的端口其实可以任意指定,接在任意的两个普通的GPIO口就可以,然后我们只需要在程序中,配置和操作SCL和SDA对应的端口就行了,这算是软件I2C相比硬件I2C的一大优势,就是端口不受限,可以任意指定。

根据I2C协议的硬件规定,SCL和SDA都应该外挂一个上拉电阻,但是MPU6050内部自带了上拉电阻,所以外部的上拉电阻就不需要接了,目前STM32是主机,MPU6050是从机。

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

程序示例

MyI2C.c

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
//直接定义函数,对操作端口的库函数进行封装
//1.方便后期加延时
//2.提高可读性,方便代码移植
void MyI2C_W_SCL(uint8_t BitValue)//SCL写
{
    GPIO_WriteBit(GPIOB,GPIO_Pin_10,(BitAction)BitValue);//这里要强转为BitAction(枚举类型)
    Delay_us(10);
}
void MyI2C_W_SDA(uint8_t BitValue)//SDA写
{
    GPIO_WriteBit(GPIOB,GPIO_Pin_11,(BitAction)BitValue);//设置引脚输出
    Delay_us(10);
}
uint8_t MyI2C_R_SDA(void)//SDA读
{
    uint8_t BitValue;
    BitValue=GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_11);//读取GPIO口的状态
    Delay_us(10);
    return BitValue;
}

void MyI2C_Init(void)
{
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);//开启GPIOB时钟
    
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_OD;//开漏输出
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10|GPIO_Pin_11;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    
    GPIO_Init(GPIOB,&GPIO_InitStructure);
    GPIO_SetBits(GPIOB,GPIO_Pin_10|GPIO_Pin_11);//释放总线 SCL和SDA处于高电平 此时I2C总线处于空闲状态
}

void MyI2C_Start(void)//起始条件
{
    MyI2C_W_SDA(1);//先释放SDA再释放SCL 防止误判为终止条件(如果SCL一开始是低电平,那么先释放SCL再释放SDA就是终止条件)    
    MyI2C_W_SCL(1);
    
    MyI2C_W_SDA(0);//先拉低SDA再拉低SCL
    MyI2C_W_SCL(0);
}
void MyI2C_Stop(void)//终止条件
{
    MyI2C_W_SDA(0);//先拉低SDA 确保之后释放SDA能产生下降沿(由于除了终止条件,SCL以高电平结束,所有的时序单元都会以SCL低电平结束,以方便各单元的拼接)所以直接拉低SDA即可
    MyI2C_W_SCL(1);//先拉高SCL再拉高SDA
    MyI2C_W_SDA(1);   
}
void MyI2C_SendByte(uint8_t Byte)//发送一个字节
{
    uint8_t i;
    for(i=0;i<8;i++)//一共操作8次 写入一个字节
    {
        MyI2C_W_SDA(Byte & (0x80>>i));//右移+按位与 取出对应位置的值
        MyI2C_W_SCL(1);//先释放SCL再拉低SCL 驱动时钟走一个脉冲 释放SCL之后,从机就会立刻把刚才放在SDA的数据读走
        MyI2C_W_SCL(0);
    }
}
uint8_t MyI2C_ReceiveByte()//接收一个字节
{
    uint8_t Byte = 0x00;
    uint8_t i ;
    MyI2C_W_SDA(1);//为了防止主机干扰从机写入数据,主机需要先释放SDA,,释放SDA也相当于切换为输入模式,在SCL低电平时,从机会把数据放到SDA,如果从机想发1,就释放SDA,想发0,就拉低SDA
    for(i=0;i<8;i++)//重复八次,主机就能读到一个字节了
    {        
        MyI2C_W_SCL(1);//主机释放SCL,在SCL高电平期间,读取SDA
        if (MyI2C_R_SDA()==1)//当主机不断驱动SCL时钟时,从机会去改变SDA的电平,这个读取到的数据是从机控制的(想要给我们发送的数据)
        {
            Byte |=(0x80>>i);
        }
        MyI2C_W_SCL(0);//再拉低SCL,低电平期间,从机就会把下一位数据放到SDA上       
    }
    return Byte;
}
void MyI2C_SendAck(uint8_t AckBit)//发送应答位
{
    MyI2C_W_SDA(AckBit);
    MyI2C_W_SCL(1);
    MyI2C_W_SCL(0);
}
uint8_t MyI2C_ReceiveAck()//接收应答位
{
    uint8_t AckBit;
    MyI2C_W_SDA(1);
    MyI2C_W_SCL(1);
    AckBit = MyI2C_R_SDA();
    MyI2C_W_SCL(0);       
    return AckBit;
} 

MyI2C.h

#ifndef __MYI2C_H
#define __MYI2C_H

void MyI2C_Init(void);
void MyI2C_Start(void);
void MyI2C_Stop(void);
void MyI2C_SendByte(uint8_t Byte);
uint8_t MyI2C_ReceiveByte();
void MyI2C_SendAck(uint8_t AckBit);
uint8_t MyI2C_ReceiveAck(); 

#endif

MPU6050.c

#include "stm32f10x.h"                  // Device header
#include "MPU6050_Reg.h"
#include "MyI2C.h"
#define MPU6050_ADDRESS 0xD0
void MPU6050_WriteReg(uint8_t RegAddress,uint8_t Data)//指定地址写 8位从机地址 8位数据
{
    MyI2C_Start();//起始条件
    MyI2C_SendByte(MPU6050_ADDRESS);//主机寻址 0xD0 (从机地址7位+起始位1位)8位
    MyI2C_ReceiveAck();//主机接收应答
    MyI2C_SendByte(RegAddress);//指定寄存器地址
    MyI2C_ReceiveAck();//接收应答
    MyI2C_SendByte(Data);//写入指定寄存器地址下的数据  可以for循环多次 发送多个字节
    MyI2C_ReceiveAck();//接收应答
    MyI2C_Stop();//终止条件
}
uint8_t MPU6050_ReadReg(uint8_t RegAddress)//指定地址读
{
    uint8_t Data;
    
    MyI2C_Start();
    MyI2C_SendByte(MPU6050_ADDRESS);
    MyI2C_ReceiveAck();
    MyI2C_SendByte(RegAddress);//指定地址 就是设置了MPU6050的当前地址指针
    MyI2C_ReceiveAck();
    
    MyI2C_Start();//重复起始条件
    MyI2C_SendByte(MPU6050_ADDRESS|0x01);//或上0x01,读写位为1
    MyI2C_ReceiveAck();
    Data = MyI2C_ReceiveByte();//可以for循环多次 接收多个字节 但是读完最后一次要给非应答
    MyI2C_SendAck(1);//给从机应答 1不给应答 0给应答 从机如果接收到应答就会继续发送数据 
    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 6个轴均不待机
    MPU6050_WriteReg(MPU6050_SMPLRT_DIV,0x09);//采样率分频 采样分频为10
    MPU6050_WriteReg(MPU6050_CONFIG,0x06);//配置寄存器 滤波参数最大
    MPU6050_WriteReg(MPU6050_GYRO_CONFIG,0x18);//陀螺仪配置寄存器 选择最大量程
    MPU6050_WriteReg(MPU6050_ACCEL_CONFIG,0x18);//加速度计配置寄存器 选择最大量程
    //配置完之后,MPU6050内部就在连续不断地进行数据转换了,输出的数据就存放在数据寄存器中
}
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)
//通过指针,把主函数变量的地址传递到子函数来。子函数中,通过传递过来的地址,操作主函数的变量,这样,子函数结束后,主函数变量的值,就是子函数想要返回的值
{
    uint16_t DataH,DataL;
    //可以用I2C读取多个字节优化
    DataH = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_H);//高8位数据
    DataL = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L);//低8位数据
    *AccX = (DataH<<8) | DataL;
    //高8位左移8位再或上低8位,得到加速度计X轴的16位数据,再把读到的数据通过指针返回回去
    //这个16位数据是一个用补码表示的有符号数
    
    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;
}

MPU6050.h

#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);
void MPU6050_GetData(int16_t * AccX,int16_t * AccY,int16_t * AccZ,
                     int16_t * GyroX,int16_t * GyroY,int16_t * GyroZ);
uint8_t MPU6050_GetID(void);

#endif

mian.c

#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);
void MPU6050_GetData(int16_t * AccX,int16_t * AccY,int16_t * AccZ,
                     int16_t * GyroX,int16_t * GyroY,int16_t * GyroZ);
uint8_t MPU6050_GetID(void);

#endif

总结

多层的模块架构

  • 最底层I2C协议层,主要关注点是,引脚是哪两个,什么配置,时序什么时候高电平,什么时候低电平,这些协议相关的内容。
  • 之后协议之上,是MPU6050的驱动层,主要关注点是,如何读写寄存器,怎么配置寄存器,怎么读取数据,这些驱动相关的内容。
  • 最后就是主函数的应用层,调用MPU6050_GetData函数,得到数据,主要关注点,如何使用这些数据,来完成我们程序的功能设计。
  • 这就是分层的框架,有了良好的分层结构,我们在写每一层的时候,就可以专注每一层的任务,其他层的事情不需要管,完成一层之后,就尽量对这一层进行测试,测试通过了,再进行后续的代码。

其他一些注意点

开漏输出

  • 开漏输出,虽然名字上带了输出,但这并不代表它只能输出,开漏输出模式仍然可以输入,输入时,先输出1(主机放开SDA,从机获得SDA的控制权,从机发送数据,主机读取数据),再直接读取输入数据寄存器就行了。

时序单元的拼接

  • 除了终止条件,SCL以高电平结束,所有的时序单元都会以SCL低电平结束,来方便各单元的拼接。

读写分离

  • SCL低电平时变换数据,SCL高电平时读取数据,实际上就是一种的设计,低电平时间定义为写时间,高电平时间定义为读时间。
  • 在SCL高电平期间,如果非要动SDA,那这个信号就是起始条件和终止条件。
    • SCL高电平时,SDA下降沿为起始条件,SDA上升沿为终止条件。这个设计也保证了起始和终止的特异性,能够让我们在连续不断的波形中,快速地定位起始和终止,因为,起始终止和数据传输的波形有本质区别,数据传输,SCL高电平不许动SDA,起始终止,SCL高电平必须动SDA。

对下面代码的解释(为什么给SDA写了1,还要去读SDA,不是一定是1嘛?)

uint8_t MyI2C_ReceiveAck()//接收应答位
{
    uint8_t AckBit;
    MyI2C_W_SDA(1);
    MyI2C_W_SCL(1);
    AckBit = MyI2C_R_SDA();
    MyI2C_W_SCL(0);       
    return AckBit;
} 
  • I2C的引脚都是开漏输出+弱上拉的配置,主机SDA输出1,并不是强置SDA为高电平,而是释放SDA。
  • I2C是在进行通信,主机释放了SDA,从机如果在的话,会把SDA再拉低,所以即使主机把SDA置1了,之后再读取SDA,读到的值也可能时0。

AD0(从机地址最低位)

  • 通过AD0引脚改地址,默认低电平(7位从机地址 1101 000),接高电平(7位从机地址 1101 001)可以避免从机地址的重复。

寄存器

  • 寄存器也是一种存储器,只不过普通的存储器只能写和读,里面的数据并没有赋予什么实际意义,但是寄存器的每一位数据,都对应了硬件电路的状态,寄存器和外设的硬件电路,是可以进行互动的,所以就可以通过寄存器来控制硬件电路。

多返回值函数的设计:

1.在函数外面定义6个全局变量,子函数读到的数据直接写道全局变量中, 6个全局变量在主函数里进行共享。

2.指针,进行变量的地址传递。

3.结构体对多个变量进行打包,然后再统一进行传递。

  • MPU6050上电之后默认是睡眠模式,睡眠模式写入寄存器是无效的。

  • 点名时序可以用来扫描所有从机地址(遍历所有从机地址),然后把应答位为0的地址统计下来。

  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

YRr YRr

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值