STM32学习笔记13-通信协议I2C&MPU6050&I2C软件控制

I2C通信协议

I2C通信

  • I2C(Inter IC Bus)是由Philips公司开发的一种通用数据总线
  • 两根通信线:SCL(Serial Clock)、SDA(Serial Data)

SCL:串口时钟线:同步的时序,降低对硬件的依赖;

SDA:串口数据线:半双工,一根线兼具发送和接收;


  • 同步,半双工
  • 带数据应答
  • 支持总线挂载多设备(一主多从、多主多从)

一主多从:单片机作为主机,主导I2C总线的运行,挂载在I2C总线的所有外部模块都是从机,从机只有被主机点名之后才能控制I2C总线------常用模型

在多主多从的情况下,需要额外调用SCL来达成同步,且复杂的协议

硬件电路(一主多从)

  • 所有I2C设备的SCL连在一起,SDA连在一起
  • 设备的SCL和SDA均要配置成开漏上拉输出模式
  • SCL和SDA各添加一个上拉电阻,阻值一般为4.7KΩ左右

禁止所有设备输出强上拉的高电平,采用外置弱上拉电阻加开漏输出的电路结构

开漏输出:原本的推挽输出,是上下都接入高低电平,如果导通则强上拉导通高电平,反之;现在不需要上面电路,当电流导通,会进入浮空状态。

所以 这就是对应的开漏输出,当导通后属于浮空状态,这时就需要电阻 ,把此电路改成弱上拉;

好处:

一、完全杜绝了电源短路的现象,保证电路安全------无论电流高或低,都不会存在同时高电平和低电平的情况;

二、避免了引脚模式的频繁切换,开漏加弱上拉的模式,同时兼具了输入和输出的功能------想换高电平可直接导通即可,低电平就连GND即可;

三、存在"线与"的现象,指只要有任意一个或多个设备输出了低电平,总线就处于低电平,反之;因此可以利用这特性,实现执行多主机模式下的时钟同步和总线仲裁

I2C时序基本单元

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

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

低电平主机放数据,高电平从机读数据


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

释放SDA:就证明切换为了输入模式,主机一般默认是输入模式,当需要发送的时候,会将SDA拉低;在被动接收的时候,就必须先释放SDA,避免有数据重叠

低电平从机放数据,高电平主机读数据。

此时从机获得控制权,实现表示主机控制部分,虚线表从机控制的部分;


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

地址:当主机发送指令,去访问对应设备,这些设备就要有一个独立的地址:

在I2C协议标准里分为7位和10位地址:7位为例,且7位使用范围最广:

厂商会为它分配一个7位的地址,以MPU6050芯片为例,通过芯片手册可以得知是:

1101 000,一般器件地址的最后几位是可以在电路中改变,这里是1101 000x;用来区分同器件的不同地址,当对应引脚接低电平,地址为1101 0000,反之

I2C时序:

都是一个字节的操作

  • 指定地址写
  • 对于指定设备(Slave Address),在指定地址(Reg Address)下,写入指定数据(Data)

示波器:

  • 当前地址读
  • 对于指定设备(Slave Address),在当前地址指针指示的地址下,读取从机数据(Data)
  • 但是对应读地址,并不能确定当前的读入的地址,所以需要使用当前地址指针,去指向指定地址读
  • 对于指定设备(Slave Address),在指定地址(Reg Address)下,读取从机数据(Data)

若要多个字节的操作,就需要把读和写的操作重复执行,这样时许就变为,在指定的位置开始按顺序连续读写入多个字节

MPU6050简介

  • MPU6050是一个6轴姿态传感器,可以测量芯片自身X、Y、Z轴的加速度、角速度参数,通过数据融合,可进一步得到姿态角,常应用于平衡车、飞行器等需要检测自身姿态的场景
  • 3轴加速度计(Accelerometer):测量X、Y、Z轴的加速度,能确定静态稳定性
  • 3轴陀螺仪传感器(Gyroscope):测量X、Y、Z轴的角速度(绕X、Y、Z轴旋转的速度),能确定动态稳定性

我们通常都是把芯片内部的器件,想象为机器设备,至于怎么放进芯片的,就是公司的秘籍。

MPU6050参数

  • 16位ADC采集传感器的模拟信号,分为2个字节存储,量化范围:-32768~32767
  • 加速度计满量程选择:±2、±4、±8、±16(g:1g=9.8m/s)
  • 陀螺仪满量程选择: ±250、±500、±1000、±2000(°/sec)
  • 可配置的数字低通滤波器
  • 可配置的时钟源
  • 可配置的采样分频
  • I2C从机地址:1101000(AD0=0)------7位转换:110 1000------0x68

1101001(AD0=1)

在之前的时序中,读写的操作可以先拆分为7位:0x68,再<<1作为从机操作地址;也可以作4分的地址:0xD0的从机地址,直接修改即可;

硬件电路

MPU6050框图

接线图

10-1 软件I2C读写MPU6050

整体架构:

I2C模块-初始化:GPIO口和6个时序基本单元;

MPU6050模块:基于I2C模块实现指定地址读写操作等

Main模块中,调用MPU6050模块

cpp 复制代码
main.c
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "MyI2C.h"
#include "MPU6050.h"
//#include "OLED_Font.h"
//软件I2C读写MPU6050

int16_t AX,AY,AZ,GX,GY,GZ;

int main(void){
	OLED_Init();
	MPU6050_Init();
//	MyI2C_Start();
//	//根据通信协议,第一个内容必须是从机地址+读写位
//	MyI2C_SendByte(0xD0); //1101 000  0  :MPU6050的地址+读写位 
//	uint8_t Ack=MyI2C_ReceiveAck();
//	MyI2C_Stop();
//	OLED_ShowNum(1,1,Ack,3);
//	//返回000,符合说明MPU应答了,对此可以对其他的从机进行扫描(扫7位),返回为0的做记录。
//	
	
//	//读MPU的寄存器
//	uint8_t ID=MPU_ReadingReg(0x75);//MPU手册中0x75:芯片的ID号
//	OLED_ShowHexNum(1,1,ID,2);
	
//	//写寄存器,需要先把寄存器的睡眠模式关了,否则无效,相关位置在电源管理器寄存器1的0x6B处
//	MPU_WriteReg(0x6B,0x00);
//	
//	MPU_WriteReg(0x19,0xAA);
//	uint8_t ID=MPU_ReadingReg(0x19);
//	OLED_ShowHexNum(1,1,ID,2);

// 	目前就是相当于存储器的读和写的操作,对于寄存器是与外部设备相关的
	

	while(1){
		MPU_Getdata(&AX, &AY, &AZ, &GX, &GY, &GZ);		//获取MPU6050的数据
		OLED_ShowSignedNum(2, 1, AX, 5);					//OLED显示数据
		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);
	}
}
MyI2C.c
#include "stm32f10x.h"                  // Device header
#include "Delay.h"

//对引脚的操作进行优化-用宏或函数都可以,只不过在某些大模块中需要去考虑延时
void MyI2C_W_SCL(uint8_t BitValue){
	GPIO_WriteBit(GPIOB,GPIO_Pin_10,(BitAction)BitValue);
	Delay_us(10);
}

void MyI2C_W_SDA(uint8_t BitValue){
	GPIO_WriteBit(GPIOB,GPIO_Pin_11,(BitAction)BitValue);
	Delay_us(10);
}

uint8_t MyI2C_R_SDA(void){
	uint8_t BitValue=GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_11);
	Delay_us(10);
	return BitValue;
}

	//初始化I2C
void MyI2C_Init(void){
	//软件调用:只需要调用GPIO的读写函数即可
	//1.把SCL和SDA都初始化为开漏输出模式(既可以输出也可输入)
	//2.把SCL和SDA置为高电平
	

	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
	
	GPIO_InitTypeDef GPIO_InitStructure;
 	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10|GPIO_Pin_11;
 	GPIO_Init(GPIOB, &GPIO_InitStructure);
	
	GPIO_SetBits(GPIOB,GPIO_Pin_10|GPIO_Pin_11);
}
//在通讯中,随着主机SCL的变化,从机也会使SDA的电平进行对应的更改
//时序单元-开始
void MyI2C_Start(void){
	//根据I2C通信的高低电平变化得知:先把SCL和SDA都释放,都输出1
	//之后先拉低SDA,再拉低SCL
	MyI2C_W_SDA(1);
	MyI2C_W_SCL(1);//优先把SDA上去,兼容后续的重复的判断条件
	
	MyI2C_W_SDA(0);
	MyI2C_W_SCL(0);
}
//终止条件
void MyI2C_Stop(void){
	MyI2C_W_SDA(0);//需要帮SDA先拉于低电平
	//而SCL,在接收应答后默认就是低电平,所以不用拉低
	//
	MyI2C_W_SCL(1);
	MyI2C_W_SDA(1);
}
//除了开始和结束,其他的操作都是让SCL以低电平的方式结束,方便单元的拼接
//发送一个字节
void MyI2C_SendByte(uint8_t Byte){
	uint8_t i;
	for(i=0;i<8;i++){
		MyI2C_W_SDA(Byte&(0x80>>i)); //根据SDA传入的最高位来确定电平
		
		MyI2C_W_SCL(1);
		MyI2C_W_SCL(0);//先释放再读取,是给从机读取之前的在SDA的数据,一段脉冲时间
	}
}

//接收一个字节
//从机在SDA上,需要先把SDA释放,也就是设置为输入模式。当SCL低电平时,从机把数据放到SDA;SCL为高电平时,读取SDA;
//如果此时SDA出现乱动,则会被认为结束或开始的单元
uint8_t MyI2C_ReceiveByte(void){
	uint8_t i, Byte=0x00;
	MyI2C_W_SDA(1);
	//读
	for(i=0;i<8;i++){
		MyI2C_W_SCL(1);
		if(MyI2C_R_SDA()==1){
			Byte|=(0x80>>i);
		}
		MyI2C_W_SCL(0);
	}
	return Byte;
}

//发送应答和接收应答
//针对一个比特
void MyI2C_SendAck(uint8_t AckBit){
	MyI2C_W_SDA(AckBit);
	MyI2C_W_SCL(1);
	MyI2C_W_SCL(0);

}

uint8_t MyI2C_ReceiveAck(void){
	uint8_t AckBit=0x00;
	MyI2C_W_SDA(1);
	MyI2C_W_SCL(1);
	AckBit=MyI2C_R_SDA();
	MyI2C_W_SCL(0);
	return AckBit;
}
MPU6050.c
#include "stm32f10x.h"                  // Device header
#include "MyI2C.h"
//在I2C的模块上运行
//宏定义对于的寄存器地址
#define MPU_ADD   0xD0
#include "MPU_Reg.h"
//封装指定地址写和指定地址读的时序
void MPU_WriteReg(uint8_t RegAddress,uint8_t Data){
	MyI2C_Start();
	//发送从机地址,做应答
	MyI2C_SendByte(MPU_ADD);
	MyI2C_ReceiveAck();
	//发送对应的寄存器位置,做应答
	MyI2C_SendByte(RegAddress);
	MyI2C_ReceiveAck();
	//发送数据进对应寄存器,做应答
	MyI2C_SendByte(Data);
	MyI2C_ReceiveAck();
	
	MyI2C_Stop();
}

uint8_t MPU_ReadingReg(uint8_t RegAddress){
	uint8_t Data;
	
	MyI2C_Start();
	//发送从机地址,做应答
	MyI2C_SendByte(MPU_ADD);
	MyI2C_ReceiveAck();
	//发送对应的寄存器位置,做应答
	MyI2C_SendByte(RegAddress);
	MyI2C_ReceiveAck();
	
	//读的时序,要先重新指定的读写位,所以要重新起始
	MyI2C_Start();
	//传入MPU写的时序
	MyI2C_SendByte(MPU_ADD|0x01);
	MyI2C_ReceiveAck();
	//此时控制权给到从机,由从机进行发送,读后,需要把应答发给从机
	Data=MyI2C_ReceiveByte();
	MyI2C_SendAck(1);//最后一次应答发送1
	
	MyI2C_Stop();
	
	return Data;
}



void MPU6050_Init(void){
	MyI2C_Init();
	MPU_WriteReg(MPU6050_PWR_MGMT_1,0x01);  //解除睡眠,选择陀螺仪时钟
	MPU_WriteReg(MPU6050_PWR_MGMT_2,0x00);	//6个轴均不待机
	MPU_WriteReg(MPU6050_SMPLRT_DIV,0x09);	//采样分频为10
	MPU_WriteReg(MPU6050_CONFIG,0x06);	//滤波参数最大
	MPU_WriteReg(MPU6050_GYRO_CONFIG,0x18);	//陀螺仪和加速度选择最大
	MPU_WriteReg(MPU6050_ACCEL_CONFIG,0x18);
	//此时的MPU就在进行大量的数据转换,数据存放在其他的寄存器里
}
//获取寄存器的数据
//6个返回值:XYZ的加速度值和陀螺仪值:多返回值的设计:1.外部定义6个全局变量;2.指针,进行变量的地址传递(实参传递);3.用结构体,成员实现参数打包
void MPU_Getdata(int16_t *AccX, int16_t *AccY, int16_t *AccZ, 
      						int16_t *GyroX, int16_t *GyroY, int16_t *GyroZ)  
{
	//读取加速度寄存器XYZ轴的高8位和低8位
	uint8_t DataH, DataL;								//定义数据高8位和低8位的变量
	
	DataH = MPU_ReadingReg(MPU6050_ACCEL_XOUT_H);		//读取加速度计X轴的高8位数据
	DataL = MPU_ReadingReg(MPU6050_ACCEL_XOUT_L);		//读取加速度计X轴的低8位数据
	*AccX = (DataH << 8) | DataL;						//数据拼接,通过输出参数返回
	
	DataH = MPU_ReadingReg(MPU6050_ACCEL_YOUT_H);		//读取加速度计Y轴的高8位数据
	DataL = MPU_ReadingReg(MPU6050_ACCEL_YOUT_L);		//读取加速度计Y轴的低8位数据
	*AccY = (DataH << 8) | DataL;						//数据拼接,通过输出参数返回
	
	DataH = MPU_ReadingReg(MPU6050_ACCEL_ZOUT_H);		//读取加速度计Z轴的高8位数据
	DataL = MPU_ReadingReg(MPU6050_ACCEL_ZOUT_L);		//读取加速度计Z轴的低8位数据
	*AccZ = (DataH << 8) | DataL;						//数据拼接,通过输出参数返回
	
	DataH = MPU_ReadingReg(MPU6050_GYRO_XOUT_H);		//读取陀螺仪X轴的高8位数据
	DataL = MPU_ReadingReg(MPU6050_GYRO_XOUT_L);		//读取陀螺仪X轴的低8位数据
	*GyroX = (DataH << 8) | DataL;						//数据拼接,通过输出参数返回
	
	DataH = MPU_ReadingReg(MPU6050_GYRO_YOUT_H);		//读取陀螺仪Y轴的高8位数据
	DataL = MPU_ReadingReg(MPU6050_GYRO_YOUT_L);		//读取陀螺仪Y轴的低8位数据
	*GyroY = (DataH << 8) | DataL;						//数据拼接,通过输出参数返回
	
	DataH = MPU_ReadingReg(MPU6050_GYRO_ZOUT_H);		//读取陀螺仪Z轴的高8位数据
	DataL = MPU_ReadingReg(MPU6050_GYRO_ZOUT_L);		//读取陀螺仪Z轴的低8位数据
	*GyroZ = (DataH << 8) | DataL;						//数据拼接,通过输出参数返回

	
}
相关推荐
yb0os116 小时前
RPC实战和核心原理学习(一)----基础
java·开发语言·网络·数据结构·学习·计算机·rpc
乱飞的秋天16 小时前
网络编程学习
网络·学习·php
2202_7557443016 小时前
开学季技术指南:构建高效知识管理系统与学习工作流
学习
一支闲人18 小时前
STM32新建工程
stm32·基础知识·适用于新手小白·新建工程
hazy1k18 小时前
STM32H750 I2C介绍及应用
stm32·单片机·嵌入式硬件
不会聊天真君64718 小时前
ES(springcloud笔记第五期)
笔记·elasticsearch·spring cloud
时空自由民.18 小时前
repo 学习教程
大数据·学习·elasticsearch
汇能感知19 小时前
光谱相机在AI眼镜领域中的应用
经验分享·笔记·科技
汇能感知19 小时前
光谱相机的图像模式
经验分享·笔记·科技
路弥行至19 小时前
从0°到180°,STM32玩转MG996R舵机
c语言·数据库·stm32·单片机·嵌入式硬件·mcu·mongodb