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的地址统计下来。