[STM32]Day10-Part1软件I2C读写MPU6050

I2C通信

设计一种通信协议:

  • 通过通信线,实现单片机读写外挂模块寄存器的功能
  • 节省资源,半双工通信
  • 降低对硬件的要求,同步通信
  • 带数据应答
  • 多设备通信

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

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

通信协议 = 硬件电路 + 软件规定

硬件电路

所有I2C设备的SCL连在一起,SDA连在一起。

设备的SCL和SDA均要配置成开漏输出模式。

SCL和SDA各添加一个上拉电阻,阻值一般为4.7k欧左右。

主机永远掌握SCL的控制权,大部分时间掌握SDA的控制权,只有当主机需要从从机读取数据时,从机才控制SDA。从机不能主动发起对SDA的控制,只能被动响应。

关于各引脚输入/输出模式配置的分析:

对于SCL,主机时刻控制,可设置为推挽输出;从机永远只能被动接收,可设置为浮空/上拉输入

对于SDA,由于主机需要完成对从机的读写操作,因此主机和从机SDA在不同时刻输入输出状态不同。如果出现时序出错,导致主机和从机同时处于输出状态,且输出一个高电平和一个低电平,此时会导致短路,严重损坏硬件。为了避免这一问题,I2C规定:禁止所有设备输出强上拉的高电平,采用外置弱上拉电阻加开漏输出的电路结构,SDA和SCL均采用这种"开漏+弱上拉模式"。所有设备引脚电流如右图所示,对于输入信号,都可以通过一个数据缓冲器或施密特触发器进行输入;输出时设置为开漏输出,只有内部电路输出低电平时引脚输出低电平,内部电路输出高电平时引脚应当浮空,此时由于外部弱上拉电阻的作用使得SDA恢复高电平。也就是说,SDA默认由上拉电阻上拉到高电平,所有设备只允许输出低电平,避免了短路。这样设计的好处:

  • 杜绝短路现象
  • 避免引脚模式频繁切换。开漏加弱上拉模式兼具了输入和输出的功能
  • 可以利用"线与"现象进行主机模式下的时钟同步和总线仲裁(线与:只有所有设备都输出高电平,SDA/SCL才为高电平,否则SDA/SCL为低电平)

I2C时序基本单元

起始条件:SCL高电平期间,SDA下降沿

终止条件:SCL高电平期间,SDA上升沿

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

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

发送应答:主机在接收完一个字节后,在下一个时钟发送一位数据,数据0表示应答,数据1表示非应答

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

I2C中每个从机都要有一个地址,供主机选择,地址可以为7位或10位,7位使用广泛。每个外设出厂时都设置了地址,如果有多个同种外设共同连在I2C总线,可以根据外设上某些引脚的值指定该外设的地址地位,从而进行区分。

I2C时序

指定地址写

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

当前地址读

对于指定设备(Slave Address),在当前地址指针指示的地址下,读取从机数据(Data)

从机中,所有的寄存器都被分配到一个线性区间,并且存在一个单独的指针变量指示着其中一个寄存器,即当前地址指针,每对从机进行一次写入/读出操作后,当前地址指针减1/加1。

指定地址读

对于指定设备(Slave Address),在指定地址(Reg Address)下,读取从机数据(Data)

指定地址读 = 指定地址写 + 当前地址读

MPU6050

MPU6050是一个6轴姿态传感器,可以测量芯片自身X、Y、Z轴的加速度,角加速度参数,通过数据融合,可进一步得到姿态角,常应用于平衡车、飞行器等需要检测自身姿态的场景。

3轴加速度计(Accelerometer):测量X、Y、Z轴的加速度

3轴陀螺仪传感器(Gyroscope):测量X、Y、Z轴的角速度

MPU6050参数:

16位ADC采集传感器的模拟信号,量化范围:-32768 - 32767

加速度计满量程选择:+/-2、+/-4、+/-8、+/-16(g)

陀螺仪满量程选择:+/-250、+/-500、+/-1000、+/-2000(°/s)

可配置的数字低通滤波器

可配置的时钟源

可配置的采样分频

I2C从机地址:1101 000(AD0 = 0),1101 001(AD0 = 1)

表示为16进制:0x68(I2C通信时要先左移)或者0xD0(无需左移)

MPU6050硬件电路

XCL和XDA通常用于外接磁力计或者气压计,实现传感器扩展

MPU6050框图

CLKIN和CLKOUT为时钟系统,可以选择内部时钟或者外部时钟,一般选择内部时钟。

Self test为芯片自测单元,用来测试传感器是否正常。

Interrupt Status Register中断状态寄存器,可以控制内部的哪些事件都中断引脚输出。

FIFO,先入先出寄存器,可以对数据流进行缓存。

Config Registers配置寄存器,可以对内部的各个电路进行配置。

Sensor Registers,传感器寄存器,即数据寄存器,存储了各个传感器的数据。

Digital Motion Processor(DMP),可以进行姿态解算。

软件I2C读写MPU6050

整体流程:建立I2C通信层的.c和.h模块(通信层里写好I2C底层的GPIO初始化和6个基本的时序单元,包括起始、中止,发送一个字节、接收一个字节、发送应答和接收应答) -> 建立MPU6050的.c和.h模块(基于I2C通信模块,实现指定地址读、指定地址写,再实现写寄存器对芯片进行配置、读寄存器得到传感器数据) -> 在main.c中调用MPU6050模块,初始化、读数据、显示数据

I2C通信层驱动代码

c 复制代码
#include "stm32f10x.h"                  // Device header
#include "Delay.h"

//#define SCL_Port 	GPIOB
//#define SCL_Pin		GPIO_Pin_10
//#define SDA_Port 	GPIOB
//#define SDA_Pin		GPIO_Pin_11

// 封装对SCL和SDA设置高低电平的操作
void MyI2C_W_SCL(uint8_t BitVal)
{
	GPIO_WriteBit(GPIOB, GPIO_Pin_10, (BitAction)BitVal);
	Delay_us(10);
}

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

// 从SDA读数据的函数
uint8_t MyI2C_R_SDA(void)
{
	uint8_t BitVal;
	BitVal = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11);
	Delay_us(10);
	return BitVal;
}

// 软件实现I2C初始化:调用后SCL和SDA两个引脚被设置为开漏输出并且为高电平
void MyI2C_Init()
{
	// 开启时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
	// 配置GPIO,SCL -> PB10,SDA ->PB11
	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);
	// 设置SCL和SDA为高电平
	GPIO_SetBits(GPIOB, GPIO_Pin_10 | GPIO_Pin_11);
}

// 产生开始信号
void MyI2C_Start(void)
{
	// 释放SCL,SDA,注意先释放SDA,如果顺序调换可能产生停止信号
	MyI2C_W_SDA(1);
	MyI2C_W_SCL(1);
	// 产生开始信号
	MyI2C_W_SDA(0);		// SCL高电平时,SDA下降沿
	MyI2C_W_SCL(0);		// SCL下降沿		
}

// 产生停止信号
void MyI2C_Stop(void)
{
	// 先拉低SDA,由于不知道传输的最后一个bit位是0还是1
	// 如果是1并且不提前拉低SDA,那么拉高SCL之后将无法产生SDA上升沿,无法结束
	MyI2C_W_SDA(0);
	// 再产生停止信号
	MyI2C_W_SCL(1);
	MyI2C_W_SDA(1);
}

// 发送一个字节
void MyI2C_SendByte(uint8_t Byte)
{
	// 进入函数时SCL为低电平
	uint8_t i;
	for(i = 0; i < 8; i ++) {
		// 趁SCL此时为低电平,先把Byte最高位放在SDA上
		MyI2C_W_SDA(Byte & (0x80 >> i));
		// 度过一个时钟,在这个时钟高电平时接收方读取SDA,然后拉低SCL进入下一个时钟
		MyI2C_W_SCL(1);
		MyI2C_W_SCL(0);
	}
}

// 接收一个字节
uint8_t MyI2C_ReceiveByte(void)
{
	// 存放结果
	uint8_t Byte = 0x00;
	uint8_t i;
	// 主机要先释放SDA
	MyI2C_W_SDA(1);
	
	for(i = 0; i < 8; i ++) {
		// 发送方先把数据放到SDA,然后接受方拉高SCL,读取数据
		MyI2C_W_SCL(1);
		if(MyI2C_R_SDA() == 1) {
			Byte |= (0x80 >> i);
		}
		MyI2C_W_SCL(0);		// 拉低SCL,进入下一个时钟
	}
	return Byte;
}

// 发送应答
void MyI2C_SendAck(uint8_t AckBit)
{
	// 函数进来时SCL为低电平,把AckBit放到SDA上
	MyI2C_W_SDA(AckBit);
	MyI2C_W_SCL(1);		// SCL高电平共接收方读取
	MyI2C_W_SCL(0);		// SCL拉低进入下一个时钟
}

// 接收应答
uint8_t MyI2C_ReceiveAck(void)
{
	// 存放结果
	uint8_t AckBit;
	// 主机先释放SDA
	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 "MPU6050_Reg.h"
#include "MyI2C.h"

#define MPU6050_ADDRESS			0xD0

// 指定地址写
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_Stop();
}

// 指定地址读
uint8_t MPU_6050_ReadReg(uint8_t RegAddress)
{
	uint8_t Data;
	
	// 先修改MPU6050中当前地址指针
	MyI2C_Start();
	MyI2C_SendByte(MPU6050_ADDRESS);
	MyI2C_ReceiveAck();
	MyI2C_SendByte(RegAddress);
	MyI2C_ReceiveAck();
	// 读寄存器
	MyI2C_Start();
	MyI2C_SendByte(MPU6050_ADDRESS | 0x01);		// 读操作
	MyI2C_ReceiveAck();
	Data = MyI2C_ReceiveByte();
	MyI2C_SendAck(0);
	MyI2C_Stop();
	return Data;
}

void MPU6050_Init(void)
{
	MyI2C_Init();
	
	// 配置电源管理寄存器1,解除睡眠模式
	MPU6050_WriteReg(MPU6050_PWR_MGMT_1, 0x01);
	
	// 配置电源管理寄存器2
	MPU6050_WriteReg(MPU6050_PWR_MGMT_2, 0x00);
	
	// 配置采样率分频
	MPU6050_WriteReg(MPU6050_SMPLRT_DIV, 0x09);
	
	// 配置寄存器
	MPU6050_WriteReg(MPU6050_CONFIG, 0x06);
	
	// 陀螺仪配置寄存器
	MPU6050_WriteReg(MPU6050_GYRO_CONFIG, 0x18);
	
	// 加速度计配置寄存器
	MPU6050_WriteReg(MPU6050_ACCEL_CONFIG, 0x18);
}

uint8_t MPU6050_GetID(void)
{
	return MPU_6050_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)
{
	uint8_t DataH, DataL;
	
	DataH = MPU_6050_ReadReg(MPU6050_ACCEL_XOUT_H);
	DataL = MPU_6050_ReadReg(MPU6050_ACCEL_XOUT_L);
	*AccX = (DataH << 8) | DataL;
	
	DataH = MPU_6050_ReadReg(MPU6050_ACCEL_YOUT_H);
	DataL = MPU_6050_ReadReg(MPU6050_ACCEL_YOUT_L);
	*AccY = (DataH << 8) | DataL;
	
	DataH = MPU_6050_ReadReg(MPU6050_ACCEL_ZOUT_H);
	DataL = MPU_6050_ReadReg(MPU6050_ACCEL_ZOUT_L);
	*AccZ = (DataH << 8) | DataL;
	
	DataH = MPU_6050_ReadReg(MPU6050_GYRO_XOUT_H);
	DataL = MPU_6050_ReadReg(MPU6050_GYRO_XOUT_L);
	*GyroX = (DataH << 8) | DataL;
	
	DataH = MPU_6050_ReadReg(MPU6050_GYRO_YOUT_H);
	DataL = MPU_6050_ReadReg(MPU6050_GYRO_YOUT_L);
	*GyroY = (DataH << 8) | DataL;
	
	DataH = MPU_6050_ReadReg(MPU6050_GYRO_ZOUT_H);
	DataL = MPU_6050_ReadReg(MPU6050_GYRO_ZOUT_L);
	*GyroZ = (DataH << 8) | DataL;
}
相关推荐
西城微科方案开发1 小时前
华润微CS98P171国产8位通用MCU轻量化控制高性价比优选
单片机·嵌入式硬件
Szime1 小时前
ADI高速ADC国产替代:AD9253、AD9653、AD9694深智微科技选型思路
科技·单片机·嵌入式硬件
Zyed1 小时前
[STM32]Day9-Part2串口收发数据包
stm32·单片机·嵌入式硬件
Zyed1 小时前
[STM32]Day6-Part4编码器接口测速
stm32·单片机·嵌入式硬件
开发笔记-阿牛2 小时前
CK6159A 性能测评:多外设并发交互下的运行表现与方案参考
stm32·单片机·音频
XINVRY-FPGA2 小时前
XC7A100T-2CSG324I AMD Xilinx Artix-7 FPGA
arm开发·人工智能·嵌入式硬件·神经网络·fpga开发·硬件工程·fpga
Szime2 小时前
四通道高速ADC国产替代:14位500MSPS深智微科技选型参考
科技·单片机·嵌入式硬件·fpga开发
结城明日奈是我老婆2 小时前
stm32的TIM和PWM学习笔记
笔记·stm32·学习
嵌入式小站2 小时前
STM32 可移植教程 01:VSCode 环境搭建 + 点亮 LED(实战篇)
vscode·stm32·嵌入式硬件