1、硬件电路
- SCL引到了STM32的PB10号引脚,SDA引到了PB11号引脚
- 软件I2C协议: 用普通GPIO口,手动反转电平实现协议,不需要STM32内部的外设资源支持,故端口是可以任意指定
- MPU605在SCL和SDA自带了两个上拉电阻,故不需要额外接上拉电阻
- AD0引脚:修改从机地址的最低位,其内置了下拉电阻,故引脚悬空时,相当于接地
- INT:中断信号输出引脚,没用到,不接
2、I2C部分代码解释
(1)发送字节
void MyI2C_SendByte(uint8_t Byte)
{
//先把数据放到SDA上面,然后SCL先置1,再置零,将SDA上面的数据送出去
uint8_t i;
for (i = 0; i < 8; i ++)
{
MyI2C_W_SDA(Byte & (0x80 >> i));//先去最高位
MyI2C_W_SCL(1);//驱动时钟走一个脉冲
MyI2C_W_SCL(0);
}
}
- 除了终止条件,SCL以高电平结束,所有单元以低电平结束,方便各个单元的拼接
- 趁着SCL是低电平,先把数据放在SDA上,再MyI2C_W_SCL(1);MyI2C_W_SCL(0);
- SCL是原本是低电平,此时先高电平再低电平,使得SDA走一个时钟,读取SCL的数据
(2)读取字节
SCL低电平期间,从机将数据位依次放到SDA线上(高位先行)
uint8_t MyI2C_ReceiveByte(void)
{
//SDA先置1,这个时候从机把第一个数据放到SDA上,然后SCL置1,读取从机的数据
uint8_t i, Byte = 0x00;
MyI2C_W_SDA(1);//主机释放SDA,从机把数据放到SDA,这时,主机释放SCL,SCL高电平,主机就能读取数据(高位先行)
for (i = 0; i < 8; i ++)
{
MyI2C_W_SCL(1);//SCL高电平,主机就可能读取数据了
//置1之后读取SDA的数据
if (MyI2C_R_SDA() == 1){Byte |= (0x80 >> i);}//如果不是高电平,就默认是写进0
MyI2C_W_SCL(0);
}
return Byte;//把接收的字节放回过去
}
- 接收一个字节,开始时,SCL为低电平,从机把数据放在SDA,为了防止主机干扰从机写入数据
- 主机先释放SDA,释放SDA相当于切换为输入模式,故在SCL低电平时,从机会把数据放到SDA
- 如果从机想发1,就释放SDA,发0,拉低SDA,然后主机释放SCL,在SCL高电平期间,读取SDA
- 即在SCL为低电平的时候,SDA写入数据,等SCL释放时,读SDA数据,即为读写分离的方式
- 故在读写数据的时候,SCL是在SDA低电平的时候变化,在高电平的时候不变,
在起始和终止的时候,SCL是在SDA高电平的时候变化
(3)接收应答
- 函数进来时,SCL为低电平,主机释放SDA,防止干扰从机,同时从机把应答位放在SDA上,
- SCL高电平,主机读取应答位,SCL低电平,进入下一个时序单元
uint8_t MyI2C_ReceiveAck(void)
{
//将SDA置1后,这个时候从机把应答位放在SDA上,这个时候只需要SCL置1,后置零,读取数据即可,记得是
//在SCL为高电平的时候读取数据,然后读取完之后SCL再置零
uint8_t AckBit;
MyI2C_W_SDA(1);
MyI2C_W_SCL(1);
AckBit = MyI2C_R_SDA();//此处不一定是1,
MyI2C_W_SCL(0);
return AckBit;
}
- AckBit = MyI2C_R_SDA();//此处不一定是1,
- 原因:I2C的引脚都是开漏输出+弱上拉配置,主机输出1,并不是强制SDA为高电平而是释放SDA
- I2C是在进行通信,主机释放SDA,从机在的情况下,有义务将SDA拉低,故读到0,代表从机给了应答,1则从机应答
3、MyI2C.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
//写SCL的函数
void MyI2C_W_SCL(uint8_t BitValue)
{
GPIO_WriteBit(GPIOB, GPIO_Pin_10, (BitAction)BitValue);
Delay_us(10);
}
//写SDA的函数
void MyI2C_W_SDA(uint8_t BitValue)
{
GPIO_WriteBit(GPIOB, GPIO_Pin_11, (BitAction)BitValue);
Delay_us(10);
}
//读SDA的函数
uint8_t MyI2C_R_SDA(void)//读
{
uint8_t BitValue;
BitValue = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11);//读取SDA的线
Delay_us(10);
return BitValue;
}
//MyI2C初始化
void MyI2C_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;//开漏输出模式(仍然可以输入,输入时,先输出1,再直接读取输入数据寄存器)
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);//都为高电平,处于空闲状态
}
//兼容起始条件和重复条件
void MyI2C_Start(void)
{
MyI2C_W_SDA(1);//先确保释放数据线,再释放SCL
MyI2C_W_SCL(1);
MyI2C_W_SDA(0);//拉低数据线,触发通讯
MyI2C_W_SCL(0);//拉低时钟线,方便数据线上的数据变化
}
//终止条件
//确保释放的时候,能产生上升沿,需要先拉底数据线,后面在SCL是高电平的时候,再拉高SDA
void MyI2C_Stop(void)
{
MyI2C_W_SDA(0);
MyI2C_W_SCL(1);
MyI2C_W_SDA(1);
}
//除了终止条件,SCL以高电平结束,所有单元以低电平结束,方便各个单元的拼接
//发送一个字节的逻辑(以stm32为视角)
void MyI2C_SendByte(uint8_t Byte)
{
uint8_t i;
for (i = 0; i < 8; i ++)
{
//除了终止条件,SCL以高电平结束,所有单元以低电平结束,方便各个单元的拼接
//趁着SCL是低电平,先把数据放在SDA上,再让SDA走一个时钟
MyI2C_W_SDA(Byte & (0x80 >> i));//先去最高位
MyI2C_W_SCL(1);//驱动时钟走一个脉冲
MyI2C_W_SCL(0);
}
}
//接收一个字节,开始时,SCL为低电平,从机把数据放在SDA,为了防止主机干扰从机写入数据
//主机先释放SDA,释放SDA相当于切换为输入模式,故在SCL低电平时,从机会把数据放到SDA
//如果从机想发1,就释放SDA,发0,拉低SDA,然后主机释放SCL,在SCL高电平期间,读取SDA
//即在SCL为低电平的时候,SDA写入数据,等SCL释放时,读SDA数据,即为读写分离的方式
//故在读写数据的时候,SCL是在SDA低电平的时候变化,在高电平的时候不变,
//在起始和终止的时候,SCL是在SDA高电平的时候变化
uint8_t MyI2C_ReceiveByte(void)
{
uint8_t i, Byte = 0x00;
MyI2C_W_SDA(1);//主机释放SDA,从机把数据放到SDA,这时,主机释放SCL,SCL高电平,主机就能读取数据(高位先行)
for (i = 0; i < 8; i ++)
{
MyI2C_W_SCL(1);//SCL高电平,主机就可能读取数据了
if (MyI2C_R_SDA() == 1){Byte |= (0x80 >> i);}//如果不是高电平,就默认是写进0
MyI2C_W_SCL(0);
}
return Byte;//把接收的字节放回过去
}
//发送应答
//函数进来时,SCL为低电平,
void MyI2C_SendAck(uint8_t AckBit)
{
MyI2C_W_SDA(AckBit);
MyI2C_W_SCL(1);
MyI2C_W_SCL(0);//进入下一个时序单元
}
//接收应答
//函数进来时,SCL为低电平,主机释放SDA,防止干扰从机,同时,从机把应答位放在SDA上,
//SCL高电平,主机读取应答位,SCL低电平,进入下一个时序单元
uint8_t MyI2C_ReceiveAck(void)
{
uint8_t AckBit;
MyI2C_W_SDA(1);
MyI2C_W_SCL(1);
AckBit = MyI2C_R_SDA();//此处不一定是1,
//原因:I2C的引脚都是开漏输出+弱上拉配置,主机输出1,并不是强制SDA为高电平而是释放SDA
//I2C是在进行通信,主机释放SDA,从机在的情况下,有义务将SDA拉低,故读到0,代表从机给了应答,1则从机应答
MyI2C_W_SCL(0);
return AckBit;
}
4、MPU6050.c 在这个代码当中写到了MPU读写寄存器等函数
#include "stm32f10x.h" // Device header
#include "MyI2C.h"
#include "MPU6050_Reg.h"
#define MPU6050_ADDRESS 0xD0
//基于I2C通信的模块,实现指定地址读、指定地址写,再实现写寄存器对芯片进行配置,读寄存器得到传感器数据
//指定地址写
void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data)
{
MyI2C_Start();
MyI2C_SendByte(MPU6050_ADDRESS);
MyI2C_ReceiveAck();//没有对应答位进行判断
MyI2C_SendByte(RegAddress);
MyI2C_ReceiveAck();//没有对应答位进行判断
MyI2C_SendByte(Data);
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);
MyI2C_ReceiveAck();
MyI2C_Start();
MyI2C_SendByte(MPU6050_ADDRESS | 0x01);//读
MyI2C_ReceiveAck();
//若想要给多个数据,则用for循环接收,然后MyI2C_SendAck(0),最后再写1
Data = MyI2C_ReceiveByte();
MyI2C_SendAck(1);//1不给从机应答,0给从机应答(想读多个字节,给应答),如果在这里给了应答,那么从机就会源源不断发送数据
MyI2C_Stop();
return Data;//地址
}
//目前的配置:解除睡眠、选择陀螺仪时钟,6个轴均不待机,采样分频为10
//滤波参数给最大,陀螺仪个加速度计都选择最大量程
void MPU6050_Init(void)
{
MyI2C_Init();
MPU6050_WriteReg(MPU6050_PWR_MGMT_1, 0x01);
//电源管理寄存器1:翻手册:一位一位赋值,不复位,解除睡眠,不需要循环,温度传感器失能,001:选择x轴的螺旋仪时钟
MPU6050_WriteReg(MPU6050_PWR_MGMT_2, 0x00);
//电源管理寄存器2:00:不需要循环模式唤醒频率,后6位,每个轴的待机位,全部给0,不需要待机
MPU6050_WriteReg(MPU6050_SMPLRT_DIV, 0x09);//10分频
//采样率分频,这8位决定了数据的快慢,值越小越快,根据实际的需求来,
MPU6050_WriteReg(MPU6050_CONFIG, 0x06);
//配置寄存器:前两位:没用,第3位到5位:000不需要外部同步,最后三位:110,最平滑的数字低通滤波器
MPU6050_WriteReg(MPU6050_GYRO_CONFIG, 0x18);
//陀螺仪配置寄存器:前面三位:自测使能,不自测;4、5位:满量程选择,11,选择最大量程,后面三位无关位
MPU6050_WriteReg(MPU6050_ACCEL_CONFIG, 0x18);
//加速度计配置寄存器:自测给000,满量程给最大量程11,用不到高通滤波器00
}
uint8_t MPU6050_GetID(void)
{
return MPU6050_ReadReg(MPU6050_WHO_AM_I);
}
/*
获取数据
加速度传感器的输出数据(x轴y轴和z轴的加速度)
陀螺仪传感器的输出数据(x轴y轴和z轴的角速度)
改变MPU6050传感器的姿态,6个数据就会对应变化
*/
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;
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;
}
5、MPU6050_Reg.c
#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
#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//陀螺仪的x轴
#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//电源管理寄存器1,地址是0x6B
#define MPU6050_PWR_MGMT_2 0x6C//电源管理寄存器2,地址是0x6B
#define MPU6050_WHO_AM_I 0x75
#endif
6、main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "MPU6050.h"
uint8_t ID;
int16_t AX, AY, AZ, GX, GY, GZ;
//写好I2C底层的GPIO初始化和6各时序基本单元
//起始、终止、发送一个字节、接受一个字节、发送应答和接收应答
int main(void)
{
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);
}
}
7、从机地址
该设备中只有AD0一个引脚,故只有两个名字,若有AD0和AD1两个引脚,则有4个名字
最后一位为0,否则就是把控制权交出去
I2C从机地址:1101000(AD0=0)——>1101 0000 0xD0
1101001(AD0=1)——>1101 0010 0xD2
8、验证结果
将陀螺仪 放在水平位置上
显示屏读出的六个数据是
+00130 -00018
-00017 -00003
+01943 -00007
验证数据
配置的是量程最大的18g,故1943/32768=x/16g x=0.949g,接近1
把设备上倾,x轴正值,下倾,x轴负值