STM32-I2C

I2C通信协议

  • I2C总线是由飞利浦公司开发的一种通用数据总线;
  • 两根通信线:SCL(Serial Clock)、SDA(Serial Data);
  • 同步时钟,半双工;
  • 带数据应答;
  • 支持在总线上挂载多个设备(一主多从、多主多从)

I2C的硬件电路

硬件电路最重要的特征是"弱上拉+开漏输出",当SCL、SDA释放总线时,弱上拉将电平拉高;当SCL、SDA控制总线时,强下拉将电平拉低。

I2C的六大时序要素

分别是:起始条件、终止条件、发送一个字节,接收一个字节,发送应答、接收应答。

起始条件

当SCL为高电平时,SDA从高电平变为低电平。

终止条件

当SCL为高电平时,SDA从低电平变为高电平。

注:除了产生起始条件和终止条件时,其余情况下,SCL为高电平时,SDA的电平不允许有变化。

发送一个字节

SCL为低电平期间,主机将数据依次放到SDA上(高位先行,且一次只能放一个bit位的数据),当SCL释放变为高电平时,从机到SDA读取数据(一次读取一个bit位数据),上述过程循环8次后,可完整发送一个字节数据。

接收一个字节

主机先释放SDA交出数据总线控制权,当SCL低电平期间,从机将数据位放到SDA总线上(高位先行),当SCL高电平时,主机到SDA总线上读取数据位,重复上述步骤8次,即可完整接收一个字节数据。

发送应答

主机在接收到一个字节数据后,在下一个时钟给从机发送一位数据,数据0为应答,数据1为非应答。(发送数据的应答位的过程和发送一个bit位数据的过程一致)

接收应答

主机在发送一个字节后,先释放SDA,在下一个时钟接收一位数据,数据0应答,数据1非应答。

以上六个时序要素构成了I2C协议的完整时序。

指定地址写

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

首先触发起始条件,再对从机地址和I2C模式选择写入,进行接收应答,再对要写入的寄存器地址进行写入,进行接收应答,再对要发送到指定地址的具体数据进行发送,进行接收应答(要发送多个字节数据就重复这个步骤),最后触发终止条件。

指定地址读

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

指定地址读 = 指定地址写的前两步 + 当前地址读

当前地址读的时序如下图:

指定地址读的时序图如下:

首先触发起始条件,再对从机地址和I2C模式选择写入,进行接收应答,再对要写入的寄存器地址进行写入,进行接收应答,再次触发起始条件,再对从机地址和I2C模式选择读取,进行接收应答,对指定地址的数据进行读取,进行发送应答(需要接收多个数据时,重复此步骤),最后触发终止条件。

MPU6050

简介

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

参数

  • 16位ADC采集传感器的模拟信号,量化范围:-32768~32767;
  • 加速度计满量程选择:±2、±4、±8、±16(g);
  • 陀螺仪满量程选择:±250、±500、±1000、±2000(°/sec);
  • 可配置的数字低通滤波器;
  • 可配置的时钟源;
  • 可配置的时钟源;
  • 12C从机地:1101000 (AD0=0)
  • 1101001 (AD0=1)

以上参数均在对MPU6050芯片进行初始化时,对相应寄存器进行写入来配置,最后一个参数AD0的选择可以直接在模块AD0引脚上接上高低电平来控制。

硬件电路

如上图所示为MPU6050的硬件电路图。

上图为MPU6050的一个整体结构框图。

一、软件I2C

本实验将MPU6050的SDA、SCL引脚连接到单片机的PB10、PB11引脚,用软件模拟I2C通信,将MPU6050的加速值和角速度值传输至单片机。

程序

先对I2C模块进行编写,MyI2C.c文件:

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

//函数:将对SCL总线的写操作进行封装
void MyI2C_W_SCL(uint16_t Value)
{
	GPIO_WriteBit (GPIOB ,GPIO_Pin_10,(BitAction)Value);//对连接SCL的PB10进行电平的写入
	Delay_us (10);//延时一段时间,生成波形,稳定时序
}

//函数:对SDA总线的电平进行写入
void MyI2C_W_SDA(uint16_t Value)
{
	GPIO_WriteBit (GPIOB ,GPIO_Pin_11,(BitAction)Value);//对连接SDA的PB11进行电平的写入
	Delay_us (10);//延时一段时间,生成波形,稳定时序
}

//函数:对SDA总线的电平进行读取
uint8_t MyI2C_R_SDA(void)
{
	uint16_t data;//创建变量,用于存储读取的变量
	data = GPIO_ReadInputDataBit (GPIOB,GPIO_Pin_11);//读取电平
	Delay_us (10);//延时一段时间,生成波形,稳定时序
	return data;//返回读取值
}

//函数:对I2C的软件初始化
void MyI2C_Init(void)
{
	//打开GPIOB的时钟
	RCC_APB2PeriphClockCmd (RCC_APB2Periph_GPIOB ,ENABLE);
	
	//对PB10、PB11进行初始化配置,这里配置为开漏输出模式
	GPIO_InitTypeDef GPIO_InitStruct;
	GPIO_InitStruct.GPIO_Mode =GPIO_Mode_Out_OD ;
	GPIO_InitStruct.GPIO_Pin =GPIO_Pin_10 | GPIO_Pin_11 ;
	GPIO_InitStruct.GPIO_Speed=GPIO_Speed_50MHz;
	GPIO_Init (GPIOB ,&GPIO_InitStruct );
	
	//设置两个总线的初始状态:释放状态
	GPIO_SetBits (GPIOB ,GPIO_Pin_10 | GPIO_Pin_11 );
}

//函数:触发起始条件
void MyI2C_Start(void)
{
	//确保SCL为高电平
	MyI2C_W_SCL (1);
	//确保SDA为高电平
	MyI2C_W_SDA (1);
	//当SCL为高电平时,将SDA由高电平切换为低电平
	MyI2C_W_SDA (0);
	//初始条件触发完成后,将SCL释放为高电平
	MyI2C_W_SCL (0);
}

//函数:触发终止条件
void MyI2C_Stop(void)
{
	//提前控制将SDA电平拉低
	MyI2C_W_SDA(0);
	//确保SCL为高电平
	MyI2C_W_SCL(1);
	//当SCL为高电平时,将SDA由低电平切换为高电平
	MyI2C_W_SDA(1);
}

//函数:发送一个字节数据
void MyI2C_SendData(uint8_t Data)
{
	//创建一个变量,用于存储循环次数
	uint8_t i;
	//用for循环将一个字节数据一个bit一个bit的发送
	for(i=0;i<8;i++)
	{
		//按位与逻辑实现对每一位数据的发送
		MyI2C_W_SDA(Data & (0x80 >> i));
		//发送完主机将SCL释放拉高
		MyI2C_W_SCL(1);
		//在SCL高电平期间从机到SDA上进行数据的读取
		//读取后主机将SCL电平拉低
		MyI2C_W_SCL(0);
	}
}

//函数:主机接收一个字节的数据
uint8_t MyI2C_ReceiveData(void)
{
	//创建一个变量,用于存储接收到的数据
	uint8_t Data =0x00;
	//创建一个变量,用于存储循环次数
	uint8_t i;
	//在接收数据之前主机将SDA释放交出控制权
	MyI2C_W_SDA(1);
	//用for循环将一个字节数据一个bit一个bit的接收
	for(i=0;i<8;i++)
	{
		//在SCL低电平期间从机会把数据写入SDA中
		//将SCL变为高电平
		MyI2C_W_SCL(1);
		//当SCL高电平期间,主机到SDA中读取数据
		Data |= MyI2C_R_SDA() << (7 - i);
		//读取完之后,将SCL电平拉低
		MyI2C_W_SCL(0);
	}
	//返回读取到的数据
	return Data;
}

//函数:发出应答;0为应答,1为非应答
void MyI2C_SendACK(uint8_t Value)
{
	//在接收完一个字节数据后,SCL低电平期间,主机向SDA中写入应答位
	MyI2C_W_SDA(Value);
	//SCL释放变为高电平
	MyI2C_W_SCL(1);
	//在SCL高电平期间,从机读取SDA中的应答位
	//读取完成后,将SCL的电平拉低
	MyI2C_W_SCL(0);	
}

//函数:接收应答
uint8_t MyI2C_ReceiveACK(void)
{
	//定义变量,用于存储接收到的应答位
	uint8_t ACK;
	//在发送完一个字节数据之后,先将SDA释放为高电平交出控制权
	MyI2C_W_SDA(1);
	//在SCL低电平期间,从机会将应答位数据写入SDA总线
	//主机再将SCL电平拉高
	MyI2C_W_SCL(1);
	//在SCL高电平期间,主机到SDA总线中读取应答位数据
	ACK = MyI2C_R_SDA();
	//读取后将SCL电平拉低
	MyI2C_W_SCL(0);
	//返回读取到的应答位数据
	return ACK;
}

这八个基础时序要素均只能从主机的角度去写,不能涉及从机的操作,时序到了,从机会自动进行应答。

MyI2C.h文件:

复制代码
#ifndef _MYI2C_H
#define _MYI2C_H
void MyI2C_Init(void);
void MyI2C_Start(void);
void MyI2C_Stop(void);
void MyI2C_SendData(uint8_t Data);
uint8_t MyI2C_ReceiveData(void);
void MyI2C_SendACK(uint8_t Value);
uint8_t MyI2C_ReceiveACK(void);

#endif

再对MPU6050模块进行编写,MPU6050.c文件:

复制代码
#include "stm32f10x.h"                  // Device header
#include "MyI2C.h"
#include "MPU6050_Reg.h"

//对MPU6050的地址进行宏定义
#define MPU6050_ADD 0xD0

//函数:在指定设备、指定地址写特定数据
//参数:寄存器地址、想要写入的数据
void MPU6050_W_reg(uint8_t Reg_Add,uint8_t Data)
{
	//触发起始条件
	MyI2C_Start ();
	//发送从机地址和发送模式
	MyI2C_SendData (MPU6050_ADD);
	//接收应答
	MyI2C_ReceiveACK ();
	//发送指定寄存器的地址
	MyI2C_SendData (Reg_Add);
	//接收应答
	MyI2C_ReceiveACK ();
	//发送特定数据
	MyI2C_SendData (Data);
	//接收应答
	MyI2C_ReceiveACK ();
	//产生终止条件
	MyI2C_Stop();
}

//函数:在指定设备、指定地址读数据
//参数:寄存器地址
uint8_t MPU6050_R_reg(uint8_t Reg_Add)
{
	//前面这一段代码就是指定地址写的选择从机,在选择寄存器的代码
	uint8_t Data;
	MyI2C_Start ();
	MyI2C_SendData (MPU6050_ADD);
	MyI2C_ReceiveACK ();
	MyI2C_SendData (Reg_Add);
	MyI2C_ReceiveACK ();
	MyI2C_Stop();
	
	//再次产生起始条件
	MyI2C_Start ();
	//发送从机地址和指定I2C为接收模式
	MyI2C_SendData (MPU6050_ADD | 0x01);
	//接收应答
	MyI2C_ReceiveACK ();
	//这里只接受一个字节的数据,将应答位置为非应答
	MyI2C_SendACK (1);
	//接收数据
	Data = MyI2C_ReceiveData ();
	//产生终止条件
	MyI2C_Stop();
	//返回接收到的数据
	return Data ;
}

//函数:MPU6050的初始化函数
void MPU6050_Init(void)
{
	//对I2C软件初始化
	MyI2C_Init ();
	
	//对MPU6050的电源进行配置,这里选择陀螺仪时钟作为基准时钟
	MPU6050_W_reg(MPU6050_PWR_MGMT_1,0x01);
	MPU6050_W_reg(MPU6050_PWR_MGMT_2,0x00);
	//对采样频率进行分频,这里选择10分频
	MPU6050_W_reg(MPU6050_SMPLRT_DIV,0x09);
	//对低通滤波器进行配置,为了数据变化更顺滑,这里给得最大
	MPU6050_W_reg(MPU6050_CONFIG,0x06);
	//对陀螺仪的最大量程进行选择,选择最大量程且无自测
	MPU6050_W_reg(MPU6050_GYRO_CONFIG,0x18);
	//对加速度计的最大量程进行选择,选择最大量程且无自测,不适用高通滤波
	MPU6050_W_reg(MPU6050_ACCEL_CONFIG,0x18);
}

//函数:对MPU6050所测得的参数进行读取
//参数:加速度计的X、Y、Z轴数据,陀螺仪的X、Y、Z轴数据
void MPU6050_Getdata(int16_t *AX,int16_t *AY,int16_t *AZ,
						int16_t *GX,int16_t *GY,int16_t *GZ)
{
//	I2C协议传输数据只能一个字节一个字节(8个bit位)的传输,但是MPU6050
//	所测得的数据都是16位的,一次需要将MPU6050的数据分成高八位和第八位两次传输
	uint16_t AX_L,AX_H;
	//主机接收高八位数据
	AX_H = MPU6050_R_reg(MPU6050_ACCEL_XOUT_H);
	//主机接收低八位数据
	AX_L = MPU6050_R_reg(MPU6050_ACCEL_XOUT_L);
	//将数据高八位和低八位拼成一个完整的16位数据
	*AX = (AX_H << 8) | AX_L ;
	
	//下面这五个参数同上
	uint16_t AY_L,AY_H;
	AY_H = MPU6050_R_reg(MPU6050_ACCEL_YOUT_H);
	AY_L = MPU6050_R_reg(MPU6050_ACCEL_YOUT_L);
	*AY = (AY_H << 8) | AY_L ;
	
	uint16_t AZ_L,AZ_H;
	AZ_H = MPU6050_R_reg(MPU6050_ACCEL_XOUT_H);
	AZ_L = MPU6050_R_reg(MPU6050_ACCEL_XOUT_L);
	*AZ = (AZ_H << 8) | AZ_L ;
	
	uint16_t GX_L,GX_H;
	GX_H = MPU6050_R_reg(MPU6050_GYRO_XOUT_H);
	GX_L = MPU6050_R_reg(MPU6050_GYRO_XOUT_L);
	*GX = (GX_H << 8) | GX_L ;
	
	uint16_t GY_L,GY_H;
	GY_H = MPU6050_R_reg(MPU6050_GYRO_YOUT_H);
	GY_L = MPU6050_R_reg(MPU6050_GYRO_YOUT_L);
	*GY = (GY_H << 8) | GY_L ;
	
	uint16_t GZ_L,GZ_H;
	GZ_H = MPU6050_R_reg(MPU6050_GYRO_ZOUT_H);
	GZ_L = MPU6050_R_reg(MPU6050_GYRO_ZOUT_L);
	*GZ = (GZ_H << 8) | GZ_L ;
	
}

最后这个读取MPU6050参数的函数是用指针作为函数参数的形式来将参数值进行返回,这样的形式,能够同时返回多个参数值。

将MPU6050要使用到的寄存器地址打包在一个头文件中,便于编程时使用,MPU6050_Reg.h文件:

复制代码
#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
#define	MPU6050_ACCEL_XOUT_L	0x3C
#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
#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
#define	MPU6050_PWR_MGMT_2		0x6C
#define	MPU6050_WHO_AM_I		0x75

#endif

MPU6050.h文件:

复制代码
#ifndef _MPU6050_H
#define _MPU6050_H
#include "MPU6050_Reg.h"

void MPU6050_W_reg(uint8_t Reg_Add,uint8_t Data);
uint8_t MPU6050_R_reg(uint8_t Reg_Add);
void MPU6050_Init(void);
void MPU6050_Getdata(int16_t *AX,int16_t *AY,int16_t *AZ,
						int16_t *GX,int16_t *GY,int16_t *GZ);
#endif

最后在主函数中,对各模块函数进行调用,main.c文件:

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

//创建六个变量用于存储从MPU6050中接收到的数据
int16_t AX,AY,AZ,GX,GY,GZ;
//创建变量用于存储MPU6050的ID号
uint16_t ID;

int main(void)
{	
	OLED_Init();
	MPU6050_Init();///各模块初始化
	
	//读取MPU6050的ID号
	ID = MPU6050_R_reg (MPU6050_WHO_AM_I);
	
	OLED_ShowString (1,1,"ID");//显示提示字符串
	OLED_ShowHexNum (1,3,ID,2);//显示MPU6050ID号
	
	OLED_ShowString (2,1,"AX");//显示提示字符串
	OLED_ShowString (3,1,"AY");
	OLED_ShowString (4,1,"AZ");
	OLED_ShowString (2,8,"GX");
	OLED_ShowString (3,8,"GY");
	OLED_ShowString (4,8,"GZ");
	while(1)
	{
		//读取MPU6050数据
		MPU6050_Getdata(&AX,&AY,&AZ,&GX,&GY,&GZ);
		//实时显示MPU6050的六个数据
		OLED_ShowSignedNum (2,3,AX,5);
		OLED_ShowSignedNum (3,3,AY,5);
		OLED_ShowSignedNum (4,3,AZ,5);
		OLED_ShowSignedNum (2,10,GX,5);
		OLED_ShowSignedNum (3,10,GY,5);
		OLED_ShowSignedNum (4,10,GZ,5);
	}
}

最终效果:改变面包板姿态,OLED实时显示MPU6050各数据的变化。

注意事项

  1. 软件I2C需要手动跳转电平去模拟实现基本时序中的六个要素;
  2. 对时序进行编写时,只能写主机进行的操作,从机的操作不需要在代码中写,时序到了它会自动执行;
  3. 清楚用I2C来连接片外外设和STM32的整个流程,片外外设换了,只需要查看该外设的数据手册,找到写入哪些寄存器去初始化它,并将读取数据寄存器的地址找到,再进行一个完整的操作来连接。(对其他片外外设的移植)

二、硬件I2C

相关推荐
LCG元1 小时前
STM32L431 低功耗实战:STOP2 模式 + LPUART 唤醒 + 实测电流对比
stm32·单片机·嵌入式硬件
国科安芯1 小时前
空间数据总线的新范式:ASM1042S2S型抗辐射CANFD收发器在低轨遥感卫星星座中的应用研究
网络·嵌入式硬件·架构·canfd·数据总线·星载数据处理
Messy create1 小时前
【BMS-Pack-电流检测】
单片机·嵌入式硬件·能源
路上有只喵1 小时前
STM32 UI Designer:一个开源的嵌入式 LCD 界面可视化设计工具(自动生成 C 代码)
c语言·驱动开发·stm32
深圳市恒锐丰科技杨生2 小时前
EG1164 高压同步升压 DC‑DC 控制器|外置 MOS 升压电源方案
嵌入式硬件·硬件工程
智购科技无人售货机工厂2 小时前
2026自动售货机云端API设计规范:从RESTful到GraphQL的接口演进~YH
android·人工智能·驱动开发·单片机·云原生·pandas·设计规范
天空'之城2 小时前
单片机基础核心知识点汇总(二十八)
单片机·嵌入式硬件
糖糖单片机设计3 小时前
STM32 智能奶茶冲泡系统
stm32·单片机·嵌入式硬件
jianqiang.xue5 小时前
ESP-IDF保姆级入门17|GPIO外部中断全解与工程化设计:分层架构/防抖策略/长按双击/优先级配置,掌握工业级可靠输入处理
stm32·单片机·物联网·架构·esp32