STM32入门教程---I2C通信

I2C通信

简介

  • I2C总线(Inter IC BUS)是由Philips公司开发的一种通用数据总线
  • 两根通信线:SCL(串行时钟线)、SDA(串行数据线)
  • 同步、半双工
  • 带数据应答
  • 支持总线挂载多设备(一主多从、多主多从)
    在多主多从模式下,当有多个主机跳出来时,会发生总线冲突,此时I2C协议会进行仲裁,仲裁胜利方取得总线控制权,失败的一方自动变回从机;
    在这里插入图片描述

硬件电路

  • 所有I2C设备的SCL连在一起,SDA连在一起
  • 设备的SCL和SDA均要配置成开漏输出模式
  • SCL和SDA各添加一个上拉电阻,阻值一般为4.7kΩ左右
    在这里插入图片描述
    对于任意时刻,都是主机拥有对SCL线的完全控制权,从机只能对SCL线进行被动的读取;
    而在空闲状态下也是主机拥有对SDA线的控制权,只有在主机发送读取从机的命令或从机应答后,从机发送数据时,主机才会把SDA线的控制权交给从机;
    在这里插入图片描述
    配置开漏输出模式避免SDA线高低电平造成的短路,而成为弱上拉模式;并且开漏输出+弱上拉的模式,兼具输入和输出的功能;
    因为在开漏模式下,输出高电平相当于断开引脚,输出模式即断开引脚或把电平往下拉,而输入模式就是断开引脚即可;
    “线与”现象:只有有任意一个或多个设备输出了低电平,总线就处于低电平,只有所有设备都输出高电平,总线才处于高电平。I2C可以利用这个电路特性实现多主机模式下的时钟同步和总线仲裁;

I2C时序基本单元

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

  • 终止条件:SCL高电平期间,SDA从低电平切换到高电平;
    在这里插入图片描述
    起始和终止都是由主机产生的,从机不允许产生起始和终止;

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

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

在发送过程中,SCL和SDA都由主机完全掌控,从机只能被动读取

在这里插入图片描述
由于有SCL线进行同步,所以如果有主机一个字节发送一半,突然进中断,时序就会在中断的位置不断拉长,SCL和SDA电平都暂停变化,传输也完全暂停,等到主机中断结束后再继续传输,这就是同步时序的好处;

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

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

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

I2C时序

从机设备地址,在I2C协议标准里分为7位地址和10位地址,每个I2C设备出厂时,厂商都会分配一个7位的地址,在芯片手册里可以找到。一般不同型号的芯片地址都是不同的,相同型号的芯片地址都是一样的,而器件地址的最后几位是可以在电路中改变的

指定地址写

  • 对于指定设备(Slave Address),在指定地址(Reg Address)下,写入指定数据(Data)
    在这里插入图片描述
  • 写入多数据:将后面的发送字节部分和接收应答部分多重复几次,然后再是停止位,就可以发送多个字节,这里要注意后面连续写入的字节在第一次标志的地址后面依次递增,这样就进阶为在指定位置开始,按顺序连续写入多个字节;

当前地址读

  • 对于指定设备(Slave Address),在当前地址指针指示的地址下,读取从机数据(Data)
    在这里插入图片描述
    在从机中,所有寄存器被分配到一个线性区域中,并且有一个单独的指针变量指示着其中一个寄存器,指针上电默认指向0地址,并且每写入和读出一个字节后指针自动自增一次,移动到下一个位置;

例如在上一次的写操作中写入的是0x19的地址,在下一次读操作中读的就是0x1A的寄存器,再下一次就是0x1B的寄存器

  • 读出多数据:和写入多数据类似,将后面部分时序重复,就可以连续读出按顺序的多个寄存器的值;
  • 如果只想读一个字节就停止,在读完一个字节之后,要给从机发送一个非应答SA;非应答就是该主机应答的时候,主机不把SDA拉低,从机读到SDA为1,代表主机没有应答,从机受到非应答之后,就知道主机不想要继续了,从机释放总线,把SDA控制权交给主机;如果主机读完仍然给应答,从机就会认为主机还想要数据,就会继续发送下一个数据;

指定地址读

  • 对于指定设备(Slave Address),在指定地址(Reg Address)下。读取从机数据(Data)
    在这里插入图片描述
    该时序是复合格式,即把指定地址写的前面一部分(指定地址而没有写入数据)和后面当前地址读的时序结合起来就得到指定地址读的时序,在输入地址后的Sr就是重复起始条件,相当于另起一个时序,然后就可以进行重新寻址并且指定读写标志位,从而实现指定地址读;

如果主机想连续读取多个字节,就需要在最后一个字节给非应答,在之前的所有字节都要给应答

MPU6050

简介

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

在这里插入图片描述

参数

  • 16位ADC采集传感器的模拟信号:量化范围:-32768~32767,数据是16位的,分两个字节存储;
  • 加速度计满量程选择:±2、±4、±8、±16(g。即重力加速度)
  • 陀螺仪满量程选择:±250、±500、±1000、±2000(°/sec,即每秒选择多少度)

满量程的意思是16位AD值达到最大时,对应的物理参量具体是多少,物体运动越剧烈选择的满量程越大;满量程越大,测量的范围越广,满量程越小,测量分辨率越高;

  • 可配置的数字低通滤波器
  • 可配置的时钟源

时钟源经过分频器的分频,就可以为AD转换和内部其他电路提供时钟,控制分频系数就可以控制AD转换的快慢;

  • 可配置的采样分频
  • I2C从机地址:1101000(AD0=0) 1101001(AD0=1)
    如果从机地址是0x68的话,在I2C通信中发送第一个字节时需要把0x68左移一位,再按位或上读写位;或者直接把读写位融入从机地址得到0xD0也可以

硬件电路

在这里插入图片描述
在这里插入图片描述

  • 由原理图可见,SCL和SDA已经接了上拉电阻了,所以实际操作时直接把SCL和SDA连接GPIO口就可以
  • XCL和XDA用于外接磁力计或气压计,MPU6050的主机接口就可以对这些拓展芯片的数据进行解算
  • INT中断输出引脚可以配置芯片内部的一些事件来触发中断引脚的输出,以及自由落体检测、运动检测、零运动检测等等触发INT中断引脚的电平跳变

框图

在这里插入图片描述

  • 灰色部分是芯片内部的传感器,包括XYZ轴的加速度计,XYZ轴的陀螺仪,还有最下面的温度传感器;传感器本质上相当于可变电阻,分压后输出模拟电压通过ADC进行模数转换,存放在数据寄存器中;
  • 最左边的一排是自测传感器,可以先使能自测,读取一次数据,再失能自测,读取一次数据,两次数据相减得到的就是自测响应,在使用手册的范围内就说明芯片没问题;

代码

软件I2C读写MPU6050

接线图

在这里插入图片描述

  • I2C初始化需要把SCL和SDA配置为开漏输出模式并且置高电平,此时I2C总线处于空闲状态;

代码

  • 可以先进行引脚操作的函数定义和封装,方便后续修改;
  • MyI2C.h
#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;
	BitValue = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11);
	Delay_us(10);
	return BitValue;
}

void MyI2C_Init(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
	
	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);
	
	GPIO_SetBits(GPIOB, GPIO_Pin_10 | GPIO_Pin_11);
}

void MyI2C_Start(void)
{
	MyI2C_W_SDA(1);
	MyI2C_W_SCL(1);
	MyI2C_W_SDA(0);
	MyI2C_W_SCL(0);
}

void MyI2C_Stop(void)
{
	MyI2C_W_SDA(0);
	MyI2C_W_SCL(1);
	MyI2C_W_SDA(1);
}

void MyI2C_SendByte(uint8_t Byte)
{
	uint8_t i;
	for (i = 0; i < 8; i ++)
	{
		MyI2C_W_SDA(Byte & (0x80 >> i));
		MyI2C_W_SCL(1);
		MyI2C_W_SCL(0);
	}
}

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;
	MyI2C_W_SDA(1);
	MyI2C_W_SCL(1);
	AckBit = MyI2C_R_SDA();
	MyI2C_W_SCL(0);
	return AckBit;
}

  • MPU6050.h
#include "stm32f10x.h"                  // Device header
#include "MyI2C.h"
#include "MPU6050_Reg.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_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();
	Data = MyI2C_ReceiveByte();
	MyI2C_SendAck(1);
	MyI2C_Stop();
	
	return Data;
}

void MPU6050_Init(void)
{
	MyI2C_Init();
	MPU6050_WriteReg(MPU6050_PWR_MGMT_1, 0x01);
	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 MPU6050_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 = 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;
}

  • 宏定义
#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

  • 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;

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);
	}
}

  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值