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;						//数据拼接,通过输出参数返回

	
}
相关推荐
果粒橙_LGC10 分钟前
论文阅读系列(一)Qwen-Image Technical Report
论文阅读·人工智能·学习
code bean2 小时前
【halcon】Halcon 开发笔记: gray_histo_abs 报错陷阱
笔记
Warren983 小时前
软件测试-Selenium学习笔记
java·javascript·笔记·学习·selenium·测试工具·安全
猫猫的小茶馆3 小时前
【STM32】HAL库中的实现(五):ADC (模数转换)
stm32·单片机·嵌入式硬件·mcu·51单片机·智能硬件·pcb工艺
在路上`4 小时前
前端学习之后端小白java的一些理论知识(框架)
java·学习
练习时长两年半的Java练习生(升级中)5 小时前
从0开始学习Java+AI知识点总结-18.web基础知识(Java操作数据库)
java·学习·web
Jayyih5 小时前
嵌入式系统学习Day19(数据结构)
数据结构·学习
紫阡星影5 小时前
【模块系列】STM32&W25Q64
stm32·单片机·嵌入式硬件
xy_recording6 小时前
Day08 Go语言学习
开发语言·学习·golang