树莓派之GPIO模拟i2c读取MPU6050数据

最近在做嵌入式的一个实验,要求手撕i2c,不能调用gpio库和一些把i2c封装好的库,来读取MPU6050的数据。比如说wiringPi,smbus之类的库。

1. MPU6050

树莓派之GPIO模拟i2c读取MPU6050数据_第1张图片

  • 外部引脚功能

    管脚名称 说明
    VCC 3.3V-5V
    GND 地线
    SCL 时钟线(从机)
    SDA 数据线(从机)
    XCL 时钟线(主机)
    XDA 数据线(主机)
    AD0 地址管脚,见reg117(0x75)
    INT 中断

  • 软件配置及使用
    Reg117(0x75) Who Am I
    在这里插入图片描述
    WHO_AM_I是MPU-60X0的7位I2C地址的高6位。 MPU-60X0的I2C地址的最低有效位由AD0引脚的值决定。 AD0引脚的值不会反映在该寄存器中。寄存器的默认值为0x68。第0位和第7位保留。 (硬编码为0)

    Reg 107 (0x6B) Power Management 1
    在这里插入图片描述
    DEVICE_RESET设置为1时,该位​​将所有内部寄存器复位为默认值。复位完成后,该位自动清零。

    Reg27(0x1B) Gyroscope Configuration
    在这里插入图片描述
    在FS_SEL[1:0]上写入合适的值,对应不同的量程(可以理解为灵敏度)
    在这里插入图片描述
    gx = gyr_x / 32768 * value[FS_SEL]

    同上选择量程
    Reg28(0x1C) ACCEL Configuration
    在这里插入图片描述
    ax = acc_x / 32768 * value[AFS_SEL] * g

    Reg65 and 66(0x41 and 0x42) Temperature Measurement
    Temperature in degrees C = (TEMP_OUT Register Value as a signed quantity)/340 + 36.53

2. i2c通讯

I2C接口I2C是一种双线接口,由信号串行数据(SDA)和串行时钟(SCL)组成。通常,线路是开漏和双向的。在通用I2C接口实现中,连接的设备可以是主设备或从设备。主设备将从设备地址放在总线上,具有匹配地址的从设备确认主设备。在与系统处理器通信时,MPU-60X0始终作为从设备运行,因此系统处理器充当主设备。 SDA和SCL线通常需要上拉电阻到VDD。最大总线速度为400 kHz。 MPU-60X0的从机地址为b110100X,长度为7位。 7位地址的LSB位由引脚AD0上的逻辑电平决定。这允许两个MPU-60X0连接到同一I2C总线。在此配置中使用时,其中一个器件的地址应为b1101000(引脚AD0为逻辑低电平),另一个器件的地址应为b1101001(引脚AD0为逻辑高电平)。

  • i2c起始和终止条件
    树莓派之GPIO模拟i2c读取MPU6050数据_第2张图片
  • i2c数据格式

I2C数据字节定义为8位长。 I2C数据字节定义为8位长。每次数据传输的字节数没有限制。传输的每个字节后面必须跟一个确认(ACK)信号。确认信号的时钟由主机产生,而接收机通过拉低SDA并在应答时钟脉冲的高电平部分保持低电平来产生实际的确认信号。如果从器件忙,并且在执行某个其他任务之前无法发送或接收另一个数据字节,则它可以保持SCL为低电平,从而强制主器件进入等待状态。从机准备就绪后,正常数据传输恢复,并释放时钟线。
树莓派之GPIO模拟i2c读取MPU6050数据_第3张图片

  • i2c通信

在开始与START条件(S)通信后,主机发送一个7位从机地址,然后是第8位,即读/写位。读/写位指示主设备是从从设备接收数据还是正在写入从设备。然后,主设备释放SDA线并等待来自从设备的确认信号(ACK)。传输的每个字节后面必须跟一个应答位。要确认,从器件将SDA线拉低,并在SCL线的高电平期间保持低电平。数据传输总是由主机以STOP条件(P)终止,从而释放通信线路。但是,主机可以生成重复的START条件(Sr),并在不首先生成STOP条件(P)的情况下寻址另一个从机。当SCL为高电平时,SDA线上的低电平到高电平转换定义了停止条件。所有SDA更改都应在SCL为低时进行,但启动和停止条件除外。
树莓派之GPIO模拟i2c读取MPU6050数据_第4张图片
要写入内部MPU-60X0寄存器,主器件发送启动条件(S),然后发送I2C地址和写​​入位(0)。在第9个时钟周期(当时钟为高电平时),MPU-60X0确认传输。然后主设备将寄存器地址(RA)放在总线上。在MPU-60X0确认接收到寄存器地址后,主器件将寄存器数据放入总线。接下来是ACK信号,并且可以通过停止条件(P)结束数据传输。要在最后一个ACK信号之后写入多个字节,主设备可以继续输出数据而不是发送停止信号。在这种情况下,MPU-60X0自动递增寄存器地址并将数据加载到适当的寄存器。下图显示了单字节和双字节写序列。
树莓派之GPIO模拟i2c读取MPU6050数据_第5张图片
要读取内部MPU-60X0寄存器,主器件发送一个启动条件,然后是I2C地址和一个写入位,然后是要读取的寄存器地址。在从MPU-60X0接收到ACK信号后,主机发送启动信号,然后发送从机地址和读取位。结果,MPU-60X0发送ACK信号和数据。通信以非应答(NACK)信号和来自主设备的停止位结束。定义NACK条件使得SDA线在第9个时钟周期保持高电平。下图显示了单字节和双字节读取序列。
树莓派之GPIO模拟i2c读取MPU6050数据_第6张图片

  • i2c成员信号及其描述

树莓派之GPIO模拟i2c读取MPU6050数据_第7张图片

3.手撕i2c

  • 准备工作

首先是宏定义,也可以不写,直接用地址,但是读上去不直观也不方便检查

#define	SMPLRT_DIV	0x19	
#define	CONFIG	0x1A	
#define	GYRO_CONFIG	0x1B	
#define	ACCEL_CONFIG	0x1C
#define	ACCEL_XOUT_H	0x3B	//加速度寄存器
#define	ACCEL_XOUT_L	0x3C
#define	ACCEL_YOUT_H	0x3D
#define	ACCEL_YOUT_L	0x3E
#define	ACCEL_ZOUT_H	0x3F
#define	ACCEL_ZOUT_L	0x40
#define	TEMP_OUT_H	0x41		//温度寄存器
#define	TEMP_OUT_L	0x42
#define	GYRO_XOUT_H	0x43		//角速度寄存器
#define	GYRO_XOUT_L	0x44
#define	GYRO_YOUT_H	0x45
#define	GYRO_YOUT_L	0x46
#define	GYRO_ZOUT_H	0x47
#define	GYRO_ZOUT_L	0x48
#define	PWR_MGMT_1	0x6B
#define	WHO_AM_I	0x75	
#define	SlaveAddress	0xD0	
#define Address 0x68             //MPU6050地址
#define Time 20					//i2c延时常数 

函数声明(每个函数的作用应该很明确)

static uint8 MPU6050_Init(void);	
void i2c_start(void);
void i2c_stop(void);
unsigned char i2c_read_ack(void);
void i2c_send_ack(void);
void i2c_send_noack(void);
void i2c_write_byte(unsigned char b);
unsigned char i2c_read_byte(void);
void i2c_read(unsigned char addr, unsigned char* buf, int len);
void i2c_write (unsigned char addr, unsigned char buff);
short GetData(unsigned char REG_Address);

GPIO传参(要和用户函数中的结构体成员相对应)

static struct gpio_config{
	int SDA;
	int SCL;
	short ax;
	short ay;
	short az;
	short gx;
	short gy;
	short gz;
	short tem;
	short name;
}config;

要初始化MPU及设置量程

			MPU6050_Init();
			config.ax = GetData(ACCEL_XOUT_H);
			config.ay = GetData(ACCEL_YOUT_H);
			config.az = GetData(ACCEL_ZOUT_H);
			config.gx = GetData(GYRO_XOUT_H);
			config.gy = GetData(GYRO_YOUT_H);
			config.gz = GetData(GYRO_ZOUT_H);
			config.tem = GetData(TEMP_OUT_H);
			config.name = GetData(WHO_AM_I);
  • i2c模块及MPU初始化模块
void i2c_start(void)  
{  
	//初始化GPIO口  
	gpio_direction_output(config.SDA, 1);          //设置config.SDA方向为输出  
	gpio_direction_output(config.SCL, 1);         //设置config.SCL方向为输出  
	gpio_set_value(config.SDA, 1);                //设置config.SDA为高电平  
	gpio_set_value(config.SCL, 1);                 //设置config.SCL为高电平  
	udelay(Time);                         		//延时  
	//起始条件  
	gpio_set_value(config.SDA, 0);                 //config.SCL为高电平时,config.SDA由高变低  
	udelay(Time);
}  

/* I2C终止条件 */  
void i2c_stop(void)  
{  
	gpio_set_value(config.SCL, 1);  
	gpio_direction_output(config.SDA, 0);  
	gpio_set_value(config.SDA, 0);  
	udelay(Time);
	gpio_set_value(config.SDA, 1);             //config.SCL高电平时,config.SDA由低变高  
}
/*   
I2C读取ACK信号(写数据时使用)  
返回值 :0表示ACK信号有效;非0表示ACK信号无效  
*/  
unsigned char i2c_read_ack(void)  
{  
	unsigned char r;  
	gpio_direction_input(config.SDA);           //设置config.SDA方向为输入  
	gpio_set_value(config.SCL,1);              // config.SCL变高 
	udelay(Time);
	r = gpio_get_value(config.SDA);                //读取ACK信号  
	udelay(Time);
	gpio_set_value(config.SCL,0);              // config.SCL变低  
	udelay(Time);
	return r;
}  
/* I2C发出ACK信号(读数据时使用) */  
void i2c_send_ack(void)  
{
	gpio_set_value(config.SCL,0);
	gpio_direction_output(config.SDA, 0);       //设置config.SDA方向为输出,并发送ack 
	udelay(Time);
	gpio_set_value(config.SCL,1);              // config.SCL变高  
	udelay(Time);
	gpio_set_value(config.SCL,0);
}  
/* I2C发出noACK信号(读数据时使用) */ 
void i2c_send_noack(void)
{
	gpio_set_value(config.SCL,0);
	gpio_direction_output(config.SDA, 1);    //设置config.SDA方向为输出,并发送noack     
	udelay(Time);
	gpio_set_value(config.SCL,1);              // config.SCL变高  
	udelay(Time);
	gpio_set_value(config.SCL,0);
}
/* I2C字节写 */  
void i2c_write_byte(unsigned char b)  
{  
	int i;  
	gpio_direction_output(config.SDA, 0);          //设置config.SDA方向为输出
	udelay(Time);
	for (i=7; i>=0; i--) {  
		gpio_set_value(config.SCL, 0);             // config.SCL变低 (SCL为低时可以改变SDA数据,开始写) 
		udelay(Time);
		gpio_set_value(config.SDA, b & (1<=0; i--) {  
		gpio_set_value(config.SCL, 1);         // config.SCL变高(SCL为高时不能改变SDA数据,开始读)  
		udelay(Time);
		r = (r <<1) | gpio_get_value(config.SDA);      //从高位到低位依次准备数据进行读取  
		gpio_set_value(config.SCL, 0);         // config.SCL变低 
		udelay(Time); 
	}  
	i2c_send_noack();                 //向目标设备发送onack信号,表示一个字节读取完毕,请求终止  
	return r;  
}  
/*  
I2C读操作  
addr:目标设备地址  
buf:读缓冲区  
len:读入字节的长度  
*/  
void i2c_read(unsigned char addr, unsigned char* buf, int len)  
{  
	int i;  
	unsigned char t;  
	i2c_start();
	i2c_write_byte(0xD0);     //起始条件,开始数据通信,低位为1,表示开始写数据  
	t = addr;                    
	i2c_write_byte(t);		//发送地址和数据读写方向  
	i2c_start();
	i2c_write_byte(0xD1); //低位为1,表示读数据  
	for (i=0; i
  • 整体逻辑:

写数据:先写从机地址加一位读/写位(写/0),得到从机应答后,再写寄存器地址,得到应答后,开始写数据,写完发送ack告诉从机写完了,i2c stop。

读数据:先写从机地址加一位读/写位(写/0),得到ack后,写寄存器地址,得到ack后,重新i2c start ,写从机地址加一位读/写位(读/1),得到ack后,开始读数据存入缓存区,由于博主都是一个字节一个字节读的,所以这里的len为1;读完之后发送noack,然后i2c stop终止通信;

  • 寄存器高低位:

由于有些寄存器有高位和低位,所以读取时要读2个字节,及i2c_read(reg_addr,&H)+i2c_read(reg_addr+1,&L);

  • SCL控制线的提示:

博主在这里也是困惑了很久,最后在小伙伴的帮助下找出问题,实在感谢。
顺便把SDA也讲了吧

无数据:SCL=1,SDA=1;
开始位(Start):当SCL=1时,SDA由1向0跳变;
停止位(Stop):当SCL=1时,SDA由0向1跳变;
数据位:当SCL由0向1跳变时,由发送方控制SDA,此时SDA为有效数据,不可随意改变SDA;当SCL保持为0时,SDA上的数据可随意改变;

总结:
读数据时,把SCL从0置1,读完置0;
写数据时,把SCL置0,写完一位置1,再写下一位;最后把SCL置0;
在每一步处理的时候,要保证SCL状态为:初始为0和结束为0;

树莓派之GPIO模拟i2c读取MPU6050数据_第8张图片
附一张效果图:
WHO AM I应该是68的,博主偷懒直接用了GET_Data,就读了2个字节的值。

你可能感兴趣的:(树莓派之GPIO模拟i2c读取MPU6050数据)