目录
声明:本专栏是本人跟着B站江科大的视频的学习过程中记录下来的笔记,我之所以记录下来是为了方便自己日后复习。如果你也是跟着江科大的视频学习的,可以配套本专栏食用,如有问题可以QQ交流群:963138186
本节我们来用软件I2C读写MPU6050。
接线图:
由于我们这个代码使用的是软件I2C,就是用普通的GPIO口,手动翻转电平实现的协议。它并不需要STM32内部的外设资源支持。所以这里的端口其实可以任意指定,不局限于这两个端口,接在任意的两个普通的GPIO口就可以。
然后我们只需要在程序中配置并操作SCL和SDA对应的端口就行了。这算是软件I2C相比硬件I2C的一大优势,就是端口不受限,可以任意指定。
根据I2C协议的硬件规定,SCL和SDA都应该外挂一个上拉电阻,但是我们的接线这里并没有外挂上拉电阻。是因为上一节我们分析模块电路的时候提到过这个模块内部自带了上拉电阻,所以外部的上拉电阻就不需要接了。
目前这里STM32是主机,MPU6050是从机,是一主一从的模型,当然主机和从机的执行逻辑是完全不同的,我们程序中一般只关注主机端的程序。
这里由于模块内置了下拉电阻,所以引脚悬空的话就相当于接地。
复制之前OLED显示屏的工程并改名:
MyI2C.c
由于我们本代码要使用软件I2C,所以I2C的库函数我们就不用看了。软件I2C只需要用GPIO的读写函数就行了。
初始化函数
然后初始化函数中,我们要做两个任务。第一个任务把SCL和SDA都初始化为开漏输出模式(开漏输出低电平+浮空输入也就是高阻态)。第二个任务把SCL和SDA置高电平。
注意:开漏输出并不只能输出,开漏输出模式仍然可以输入。输入时先输出1,再直接读取输入数据寄存器就行了。
然后接下来我们就来完成I2C的六个时序基本单元。
起始条件
第一个基本单元是起始条件,这里对应写一个函数。
起始条件:SCL高电平期间,SDA从高电平切换到低电平
我们首先把SCL和SDA都确保释放,然后先拉低SDA,再拉低SCL,这样就能产生起始条件了。
在这里我们可以不断的调用SetBits和RetsetBits手动翻转高低电平。但是这样做的话,会在后面的程序中出现非常多的地方来指定这个GPIO端口号。一方面这样做语义并不是很明显,另一方面,如果我们之后需要换一个端口,就需要改动非常多的地方。所以这时我们就需要在上面做个定义,把这个端口号统一替换一个名字,这样无论是语义,还是端口的修改,都会非常方便。给端口号换一个名字,有很多方法都能实现功能。在51单片机中,我们一般使用sbit来定义端口的名称,但是sbit并不是标准C语言的语法,STM32也不支持这样做。这里一种简单的替换方法就是宏定义define。
修改引脚的时候,直接在上面修改一下宏定义,这是一种简单可行的方法,在STM32程序中也是挺常见的一个操作。
进一步的,如果觉得每次都需要定义port和pin比较麻烦,还可以把这整个函数用宏定义进行替换,并且用宏定义替换的函数还可以有参数,叫有参宏。
以我们之前讲过的OLED的程序为例:
在宏定义后面加一个括号,里面写入形参,在实际引用的时候,传入实参。
这样实际上OLED_W_SCL(1)就等价于GPIO_WriteBit(GPIOB, GPIO_Pin_8, (BitAction)(1));
补充:BitAction是什么意思?
在STM32中,用于强制将特定的操作数转换为一个位值,将一个非零值转换为逻辑高电平(1),将零值转换为逻辑低电平(0)。
在GPI0操作中,可以使用"BitAction"宏定义来设置引脚的状态,例如通过调用GPI0 writeBit()函数来设置引脚的输出状态。
GPI0x表示GPIO端口,GPI0 Pin表示具体的引脚位,而BitAction表示要设置的引脚状态。
但是这种方法在移植到其他库或者其他种类单片机时,很多人都不知道怎么修改。另外还有这种宏定义的方法,如果换到一个主频很高的单片机中,需要对软件的时序进行延时操作的时候也不太方便进一步修改。
所以综合以上缺点,在这里我们就直接一点干脆再套个函数。如果单片机主频比较快,也非常方便加一些延时,比如每次操作引脚之后,都要延时10us。
后面再调用这个W_SCL,参数给1或0就可以释放或拉低SCL了。
对于STM32F1系列,这里即使不加任何延时,这个引脚翻转速度,MPU6050也能跟得上。但是保险起见,还是延时个十微秒。
如果要把这个程序移植到别的单片机,就可以把这个函数里的操作替换为其他单片机对应的操作。比如SCL是51单片机的P10口,就可以把GPIO_WriteBit(GPIOB, GPIO_Pin_10, (BitAction)BitValue);这句替换为P10=BitValue。
操作SDA的函数
接下来封装一下操作SDA的函数:
读和写不是同一个寄存器,再定义一个函数
有了这三个函数的分装,我们就实现了函数名称、端口号的替换。同时也可以很方便的修改时序的延时。当我们需要替换端口,或者把这个程序移植到别的单片机中时,就只需要对这前四个函数里的操作对应更改。
我们回到这个函数,开始调用以上四个函数。
我们需要先把SCL和SDA都释放,也就是都输出1,然后先拉低SDA。再拉低SCL,这就是起始条件的执行逻辑。
注意:我们最好把释放SDA的放在前面。
如果起始条件之前,SCL和SDA已经是高电平了,先释放哪一个是一样的效果。
但是后面start还要兼容这里的重复起始条件sr。sr最开始SCL是低电平,SDA电平不敢确定,所以保险起见,趁SCL是低电平时,先确保释放SDA再释放SCL,这时SDA和SCL都是高电平。然后再拉低SDA拉低SCL,这样start就可以兼容起始条件和重复起始条件了。
接下来继续终止条件
终止条件
终止条件:SCL高电平期间,SDA从低电平切换到高电平
如果stop开始时SCL和SDA都已经是低电平了,就先释放SCL,再释放SDA就行了。但是在这个时序单元开始时,SDA并不一定是低电平。
所以为了确保之后释放SDA,能产生上升沿,我们要在时序单元开始时先拉低SDA,然后再释放SCL,释放SDA。
然后是发送一个字节
发送一个字节
发送一个字节:SCL低电平期间,主机将数据位依次放到SDA线上(高位先行),然后释放SCL,从机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可发送一个字节
实际上除了终止条件SCL以高电平结束,所有的单元我们都会保证SCL以低电平结束,这样方便各个单元的拼接。
SCL低电平变换数据,高电平保持数据稳定,由于是高位先行,所以变换数据的时候,按照先放最高位再放次高位,依次把一个字节的每一位放在SDA线上,每放完一位后执行释放SCL拉低SCL的操作,驱动时钟运转。
Byte & 0x80 就是保留字节的高位,对其他位清0,假设Byte是xxxx xxxxx
由于调用的这个函数中的参数最后会被强制转换成bitAction类型,所以非0即1,所以最终MyI2C_W_SDA(Byte & (0x80 >> i))也相当于传了一个1
接着继续写接收一个字节
接收一个字节
接收一个字节:SCL低电平期间,从机将数据位依次放到SDA线上(高位先行),然后释放SCL,主机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可接收一个字节(主机在接收之前,需要释放SDA)
主机需要先释放SDA,释放SDA也相当于切换为输入模式。
SCL低电平变换数据,高电平读取数据,实际上就是一种读写分离的设计,低电平时间定义为写的时间,高电平时间定义为读的时间。
SCL高电平时,SDA下降沿为起始条件,SDA上升沿为终止条件。这个设计也保证了起始和终止的特异性,能够让我们在连续不断的波形中快速的定位起始和终止。因为起始终止和数据传输的波形有本质区别。数据传输时SCL高电平不许动SDA,起始终止条件下是SCL高电平必须动SDA。
注意:I2C是在进行通信,通信是有从机的,当主机不断驱动SCL时钟时,从机就有义务去改变SDA的电平。所以主机每次循环读取SDA的时候,这个读取到的数据是从机控制的,这个数据也正是从机想要给我们发送的数据。
发送应答
然后发送应答和接收应答只要复制发送一个字节和接收一个字节的函数修改一下就可以了。
发送应答:主机在接收完一个字节之后,在下一个时钟发送一位数据,数据0表示应答,数据1表示非应答
接收应答
接收应答:主机在发送完一个字节之后,在下一个时钟接收一位数据,判断从机是否应答,数据0表示应答,数据1表示非应答(主机在接收之前,需要释放SDA)
注意:I2C的引脚都是开漏输出+弱上拉的配置。主机输出1并不是强制SDA为高电平,而是释放SDA;I2C通信时,主机释放了SDA,从机在此时把SDA再拉低的。所以这里即使之前主机把SDA置1再读取SDA,读到的值也可能是0,读到0代表从机给了应答,读到1代表从机没给应答。
测试应答功能
想要测试应答功能时主函数可以这样调用
这样就可以测试从机给不给应答的时序
1101 000是从机的地址,可以理解为是从机的名字,最低位的0是表示“写入操作”
这样运行后显示从机可以给我们应答
我们接下来讲一下通过AD0引脚改名的功能。
通过AD0引脚改名的功能
我们可以把一根飞线连接AD0引脚和VCC, 这时MPU6050的从机地址就是1101 001了。
这个时候运行就发现从机没有给我们应答了,因为它刚刚改名成1101 001了。
这个时候把飞线拔掉,再次运行发现它又可以应答了。
这就是改名的实验现象。目前我们这个芯片只有AD0一个引脚,它就只能拥有两个名字。如果有AD0和AD1两个引脚,就可以拥有总共四个名字。如果有更多的可配置引脚,就有更多的改名机会。当我们需要一条总件挂载多个相同型号的设备时,就可以利用这个改名的功能,避免名字也就是从机地址的重复。
MyI2C.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
/*引脚配置层*/
/**
* 函 数:I2C写SCL引脚电平
* 参 数:BitValue 协议层传入的当前需要写入SCL的电平,范围0~1
* 返 回 值:无
* 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置SCL为低电平,当BitValue为1时,需要置SCL为高电平
*/
void MyI2C_W_SCL(uint8_t BitValue)//W代表写的意思
{
GPIO_WriteBit(GPIOB, GPIO_Pin_10, (BitAction)BitValue); //根据BitValue,设置SCL引脚的电平
Delay_us(10); //延时10us,防止时序频率超过要求
}
/**
* 函 数:I2C写SDA引脚电平
* 参 数:BitValue 协议层传入的当前需要写入SDA的电平,范围0~0xFF
* 返 回 值:无
* 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置SDA为低电平,当BitValue非0时,需要置SDA为高电平
*/
void MyI2C_W_SDA(uint8_t BitValue)
{
GPIO_WriteBit(GPIOB, GPIO_Pin_11, (BitAction)BitValue); //根据BitValue,设置SDA引脚的电平,BitValue要实现非0即1的特性
Delay_us(10); //延时10us,防止时序频率超过要求
}
/**
* 函 数:I2C读SDA引脚电平
* 参 数:无
* 返 回 值:协议层需要得到的当前SDA的电平,范围0~1
* 注意事项:此函数需要用户实现内容,当前SDA为低电平时,返回0,当前SDA为高电平时,返回1
*/
uint8_t MyI2C_R_SDA(void)//R表示读
{
uint8_t BitValue;
BitValue = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11); //读取SDA电平
Delay_us(10); //延时10us,防止时序频率超过要求
return BitValue; //返回SDA电平
}
/**
* 函 数:I2C初始化
* 参 数:无
* 返 回 值:无
* 注意事项:此函数需要用户实现内容,实现SCL和SDA引脚的初始化
*/
void MyI2C_Init(void)
{
/*开启时钟*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); //开启GPIOB的时钟
/*GPIO初始化*/
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); //将PB10和PB11引脚初始化为开漏输出
/*设置默认电平*/
GPIO_SetBits(GPIOB, GPIO_Pin_10 | GPIO_Pin_11); //设置PB10和PB11引脚初始化后默认为高电平(释放总线状态)
}
/*协议层*/
/**
* 函 数:I2C起始
* 参 数:无
* 返 回 值:无
*/
void MyI2C_Start(void)
{
MyI2C_W_SDA(1); //释放SDA,确保SDA为高电平
MyI2C_W_SCL(1); //释放SCL,确保SCL为高电平
MyI2C_W_SDA(0); //在SCL高电平期间,拉低SDA,产生起始信号
MyI2C_W_SCL(0); //起始后把SCL也拉低,即为了占用总线,也为了方便总线时序的拼接
}
/**
* 函 数:I2C终止
* 参 数:无
* 返 回 值:无
*/
void MyI2C_Stop(void)
{
MyI2C_W_SDA(0); //拉低SDA,确保SDA为低电平
MyI2C_W_SCL(1); //释放SCL,使SCL呈现高电平
MyI2C_W_SDA(1); //在SCL高电平期间,释放SDA,产生终止信号
}
/**
* 函 数:I2C发送一个字节
* 参 数:Byte 要发送的一个字节数据,范围:0x00~0xFF
* 返 回 值:无
*/
void MyI2C_SendByte(uint8_t Byte)
{
uint8_t i;
for (i = 0; i < 8; i ++) //循环8次,主机依次发送数据的每一位
{
MyI2C_W_SDA(Byte & (0x80 >> i)); //使用掩码的方式取出Byte的指定一位数据并写入到SDA线
MyI2C_W_SCL(1); //释放SCL,从机在SCL高电平期间读取SDA
MyI2C_W_SCL(0); //拉低SCL,主机开始发送下一位数据
}
}
/**
* 函 数:I2C接收一个字节
* 参 数:无
* 返 回 值:接收到的一个字节数据,范围:0x00~0xFF
*/
uint8_t MyI2C_ReceiveByte(void)
{
uint8_t i, Byte = 0x00; //定义接收的数据,并赋初值0x00,此处必须赋初值0x00,后面会用到
MyI2C_W_SDA(1); //接收前,主机先确保释放SDA,避免干扰从机的数据发送
for (i = 0; i < 8; i ++) //循环8次,主机依次接收数据的每一位
{
MyI2C_W_SCL(1); //释放SCL,主机机在SCL高电平期间读取SDA
if (MyI2C_R_SDA() == 1){Byte |= (0x80 >> i);} //读取SDA数据,并存储到Byte变量
//当SDA为1时,置变量指定位为1,当SDA为0时,不做处理,指定位为默认的初值0
MyI2C_W_SCL(0); //拉低SCL,从机在SCL低电平期间写入SDA
}
return Byte; //返回接收到的一个字节数据
}
/**
* 函 数:I2C发送应答位
* 参 数:Byte 要发送的应答位,范围:0~1,0表示应答,1表示非应答
* 返 回 值:无
*/
void MyI2C_SendAck(uint8_t AckBit)//函数进来时,SCL低电平,主机把AckBit放在SDA上
{
MyI2C_W_SDA(AckBit); //主机把应答位数据放到SDA线
MyI2C_W_SCL(1); //释放SCL,从机在SCL高电平期间,读取应答位
MyI2C_W_SCL(0); //拉低SCL,开始下一个时序模块
}
/**
* 函 数:I2C接收应答位
* 参 数:无
* 返 回 值:接收到的应答位,范围:0~1,0表示应答,1表示非应答
*/
uint8_t MyI2C_ReceiveAck(void)
{
uint8_t AckBit; //定义应答位变量
MyI2C_W_SDA(1); //接收前,主机先确保释放SDA,避免干扰从机的数据发送
MyI2C_W_SCL(1); //释放SCL,主机机在SCL高电平期间读取SDA
AckBit = MyI2C_R_SDA(); //将应答位存储到变量里
MyI2C_W_SCL(0); //拉低SCL,开始下一个时序模块
return AckBit; //返回定义应答位变量
}
MyI2C.h
#ifndef __MYI2C_H
#define __MYI2C_H
void MyI2C_Init(void);
void MyI2C_Start(void);
void MyI2C_Stop(void);
void MyI2C_SendByte(uint8_t Byte);
uint8_t MyI2C_ReceiveByte(void);
void MyI2C_SendAck(uint8_t AckBit);
uint8_t MyI2C_ReceiveAck(void);
#endif
接下来我们就继续来写建立在myI2C这一模块之上的MPU6050模块
MPU6050.c
先初始化MPU6050
模拟指定地址写和指定地址读的时序
然后封装指定地址写和指定地址读的时序。
以上代码跟我们上节讲的指定地址写的这个时序是一样的,可以对照一下每一句代码:
补充:如果想要指定地址写多个字节就用一个for循环将这两句代码框起来多执行几遍
同理,我们按照指定地址读一个字节的时序来完成读的函数
同理,如果想要指定读取多个字节就可以将这两句代码用for循环框起来多执行几遍:
但是要注意,读取最后一个字节给非应答,这之前都要给应答。
在这里指定句子读一个字节的时序就完成了,我们就可以进一步来进行测试一下。
以这个MPU6050的这个寄存器为例,这个寄存器是只读寄存器,它的地址是0x75,内容是ID号,默认是0x68
头文件大家都会自己声明了,这个就不用说了,以后都略过
在主函数里可以这样调用:
可以看到读出的ID号是0x68,这说明我们指定地址读一个字节的时序没问题。
注:有的芯片ID号是0x69,这个也是正常的,可能是批次或者其他什么原因ID号有点不一样。
接下来验证一下写寄存器的功能。
验证写寄存器的功能
要想写寄存器,首先需要解除芯片的睡眠模式,否则写入是无效的。
睡眠模式是电源管理寄存器1的sleep控制的。我们可以直接把这个寄存器写入0x00,这样就能解除睡眠模式了。
这个寄存器的地址是0x6B
解除了睡眠模式后,我们找一个寄存器写试一下,比如采样率分频寄存器,它的地址是0x19,内容是采样分频,这个值可以先随便给一个,比如0xAA。
到底有没有写入成功?我们在读寄存器把0x19地址下的数据读出来,存到一个变量里
结果显示AA
说明读写程序都没有问题。
目前我们是把MPU6050当成一个存储器来使用的,写某个存储器,读某个存储器。
其实读写真正的存储器芯片也是完全一样的逻辑。
寄存器也是一种存储器,只不过普通的存储器只能写和读里面的数据,并没有赋予什么实际意义,而寄存器的每一位数据都对应着硬件电路的状态,寄存器和外设的硬件电路是可以进行互动的。
完善一下MPU6050初始化函数
接下来我们再完善一下MPU6050初始化函数
初始化之后,我们还要再写入一些寄存器,对MPU6050硬件电路进行初始化配置。
在这里我们一般会用宏定义,先把寄存器的地址都用一个字符串来表示。
要不然每次都查手册比较麻烦,而且光写一个数据的地址也不容易理解。寄存器如果比较少的话,可以直接在这上面进宏定义。如果比较多的话,我们可以再新建一个单独的头文件存,要不然比较占地方。
所以我们在hardware右键再添加一个头文件。
以上宏定义跟手册上是对应的。
有了这个头文件之后,我们要记得在相关的.c文件中包含这个头文件。
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
接下来开始配置寄存器
配置寄存器
第一步,配置电源管理寄存器1
设备复位给0不复位,睡眠模式给0解除睡眠,循环模式给0不需要循环,无关位给0即可,温度传感器失能给0。最后三位选择时钟给000选择内部时钟。但是上节说了它非常建议我们选择陀螺仪时钟,所以我们可以给个001,选择x轴的陀螺仪时钟。当然测试看的话,选择哪个始终影响并不大。所以这个寄存器写入的数据就是0x01。
第二步,配置电源管理寄存器2
前两位是循环模式唤醒频率,给00,即不需要。后六位是每个轴的待机位,全都给0,不需要待机。所以这个寄存器写入的值就是0x00
第三步,配置采样率分频
这八位决定了数据输出的快慢,值越小越快,这个可以根据实际需求来,我们给个0x09,也就是十分频。(因为分频系数=1+SMPLRT_DIV(十进制))
第四步,配置寄存器
外部同步全都给0,不需要。最后三位数字低通滤波器,这个也是根据需求来,我们可以给个110,这就是最平滑的滤波。所以这整个计算机的值就是0x06。
第五步,陀螺仪配置寄存器
前面三位是自测使能,这里手册写漏了,我们就不自测了,都给0。满量程选择这个也是根据实际需求来,我们就给11选择最大量程,后面三位无关位,所以这个基准器就是0x18。
第六步,加速度计配置寄存器
在这里自测给000,满量程暂时也给最大量程11,最后高通滤波器我们用不到,给零零。所以这个计算机的值也是0x18。
这样初始化配置就完成了。
目前的配置主要是解除睡眠、选择陀螺仪时钟、六个轴均不待机、采样分频为10、滤波参数给最大、陀螺仪和加速度计都选择最大量程。
这就是目前给的配置,大家根据实际项目的需求,可以对应更改。
配置完之后,陀螺仪内部就在连续不断的进行数据转换了,输出的数据就存放在这里的数据寄存器里。
获取数据寄存器的值的函数
接下来我们想获取数据的话,只需要再写一个获取数据寄存器的函数即可。
并且这个函数需要返回六个uint16_t的数据,分别表示xyz的加速度值和陀螺仪值。
但是C语言中函数的返回值只能有一个,所以这里就需要一些特殊操作来实现返回六个值的任务,多返回值函数的设计方法有很多。
函数返回返回六个值的方法
第一种方法
第一种最简单的方法就是在函数外面定义六个全局变量,子函数读到的数据直接写到全局变量里。然后六个全局变量在主函数里进行共享,这样就相当于返回来六个值。这是一种比较简单且直接的方法,比较适合用在规模比较小的项目中。
但这种方法不太利于分装。
第二种方法
第二种进阶一点的方法是用指针进行变量的地址传递来实现多返回值。
第三种方法
然后第三种更进一步更高阶的方法就是用结构体对多个变量进行打包,然后再统一进行传递。这种方法就是STM32的库函数里,这里使用到的,类似于我们初始化GPIO口这里使用 的结构体
这里是结构体打包,输入参数。但是输出参数或者返回值,也可以这样进行打包。
总之,参数的传递方法有很多,一般项目越大,就越要考虑使用这些高级语法,这样更有利于工程的管理。
在这里,我们就使用第二种方法,用指针的地址传递。
所以在函数参数这里写上六个输出参数
我们会在主函数里定义变量,通过指针把主函数变量的地址传递到子函数来。子函数中通过传递过来的地址,操作主函数的变量。
这样子函数结束之后,主函数变量的值就是子函数想要返回的值,这就是使用指针实现函数多返回值的设计。
然后子函数中想要获取数据,我们就要通过MPU6050_ReadReg函数读取数据寄存器。
首先读取加速度静器x轴的高8位,然后再读取加速度寄存器的低8位。
前面定义两个变量,读取高8位的值放在datah中,读取低八位的值,放在dataL中。
之后,高8位左移8位再或上低八位,这就是加速度计x轴的十六位数据,得到十六位数据之后,用指针引用传递进来的地址,把读到的数据通过指针返回回去。这样accx的值就完成了。
然后这里可能有人疑问,这个datah是八位的数据,它再左移八位会不会出问题?这个经过测试是没问题的,因为最终赋值的变量是十六位的。所以八位数据左移之后,会自动进行类型转换,移出去的位并不会丢失。当然如果不放心的话,可以把这两个数据改为十六位的,这样就肯定没问题了。
另外因为手册里说过,这个十六位数据是一个用补码表示的有符号数,所以最终直接赋制给int16_t也是没问题的。
接下来读取后续的数据,同样的操作。
这样这个读取函数就完成了。程序逻辑是分别读取六个轴数据寄存器的高位和低位拼接成十六位的数据,再通过指针变量返回。这里我们是用读取一个寄存器的函数,连续调用了十二次才读取完十二个寄存器。但实际上还有一种更高效的方法,就是使用我们之前提到的I2C读取多个字节的时序,从一个基地址开始,连续读取一片的寄存器。
因为我们这个寄存器的地址是连续的,所以可以从第一个寄存器的地址0x3b开始,连续读取十四个字节,这样就可以一次性的把加速度值、陀螺仪值,当然还包括两个字节的温度值都读出来了。这样在时序上读取效率就会大大提升。有兴趣的话可以自己写程序试一下,这里我们就不写了。
接下来我们来测试一下看看。
测试
主函数这里我们先定义六个变量,这六个值分别用来接收xyz轴的加速度值和陀螺移值
之后,在主循环里调用刚刚写的读取函数,把这6个变量的地址传过去操作。这样就能读取六个轴的数据了,然后用OLED显示一下
结果:
可以看到目前显示的六个数据,并且不断在刷新,晃动传感器数据也都有变化。
然后按照之前我们讲的加速度计和陀螺仪的模型,大概验证一下这些数的含义。
首先这里左边三个数是xyz轴的加速度计,我们按照之前说的一个正方体里面放置一个小球的模型来理解一下。小球压在哪个面上,就产生对应轴的输出。
目前这个芯片是水平放置,对应正方体四个侧面应该不受力,所以这里显示的xy轴数据基本为零。
小球压在底面上产生一个g的重力加速度。这里显示的数据是1943,这个数据对应的重力加速度值,可以算一下:目前初始化配置里我们选择的满量程是最大的16g,所以按比例算下1943/32768=x/16g,解的x就是测量值,结果是0.95g
这里标准的答案应该是一个g,所以测量基本没问题。
然后再看一下这个板子上标的有x轴和y轴的示意图,可能比较小,大家可以自己看一下,画的是纵向为x轴,横向为y轴,剩下一个z轴是垂直于芯片的个轴
我们这样倾斜就应该是加速度的x轴两个面受力。
第一个数据,上倾,x轴正值
下倾,x轴负值。
而这样倾斜就是外轴的两个面受力
无论怎么倾斜,z轴加速度都是正值,如何让z轴出现负值?
在个正方体和小球的模型中,Z轴代表上下两个面的受力。下面受力是正值,上面受力就是负值
所以要想让z轴输负值得让上面一个面受力。显然我们把这个芯片翻过来,这样z轴就是负值了。
这些就是加速度大小的体现。
然后我们看一下陀螺仪,这里右边三个数据为三个轴的角速度。
我们让面包板平行于桌面转,即是绕z轴旋转陀螺仪,z轴会输出对应的角速度。
以面包板横向的中间那条槽为轴,前后翻转,即是绕y轴的转动,陀螺仪y轴数据变化。
以纵向中心轴转,即绕x轴的转动陀螺仪x轴数据变化
具体每个轴旋转的角速度是多少?
也是按照我们刚才说的比例公式计算。
读取的数据/32768=x/满量程,解得x就是具体的角速度值。
这些就是这个传感器测量各轴姿态数据的实验现象。
目前这个代码的任务也基本完成了。
获取ID号的代码函数
我们最终把获取ID号的代码函数也加上:
然后在主函数里调用
MPU6050.c
#include "stm32f10x.h" // Device header
#include "MyI2C.h"
#include "MPU6050_Reg.h"
#define MPU6050_ADDRESS 0xD0 //MPU6050的I2C从机地址
/**
* 函 数:MPU6050写寄存器
* 参 数:RegAddress 寄存器地址,范围:参考MPU6050手册的寄存器描述
* 参 数:Data 要写入寄存器的数据,范围:0x00~0xFF
* 返 回 值:无
*/
void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data)
{
MyI2C_Start(); //I2C起始
MyI2C_SendByte(MPU6050_ADDRESS); //发送从机地址,读写位为0,表示即将写入
MyI2C_ReceiveAck(); //接收应答
MyI2C_SendByte(RegAddress); //发送寄存器地址,即指定要写入哪个寄存器
MyI2C_ReceiveAck(); //接收应答
MyI2C_SendByte(Data); //发送要写入寄存器的数据
MyI2C_ReceiveAck(); //接收应答
MyI2C_Stop(); //I2C终止
}
/**
* 函 数:MPU6050读寄存器
* 参 数:RegAddress 寄存器地址,范围:参考MPU6050手册的寄存器描述
* 返 回 值:读取寄存器的数据,范围:0x00~0xFF
*/
uint8_t MPU6050_ReadReg(uint8_t RegAddress)
{
uint8_t Data;
MyI2C_Start(); //I2C起始
MyI2C_SendByte(MPU6050_ADDRESS); //发送从机地址,读写位为0,表示即将写入
MyI2C_ReceiveAck(); //接收应答
MyI2C_SendByte(RegAddress); //发送寄存器地址
MyI2C_ReceiveAck(); //接收应答
MyI2C_Start(); //I2C重复起始
MyI2C_SendByte(MPU6050_ADDRESS | 0x01); //发送从机地址,读写位为1,表示即将读取
MyI2C_ReceiveAck(); //接收应答
Data = MyI2C_ReceiveByte(); //接收指定寄存器的数据
MyI2C_SendAck(1); //发送应答,1给从机非应答,终止从机的数据输出
MyI2C_Stop(); //I2C终止
return Data;
}
/**
* 函 数:MPU6050初始化
* 参 数:无
* 返 回 值:无
*/
void MPU6050_Init(void)
{
MyI2C_Init(); //先初始化底层的I2C
/*MPU6050寄存器初始化,需要对照MPU6050手册的寄存器描述配置,此处仅配置了部分重要的寄存器*/
MPU6050_WriteReg(MPU6050_PWR_MGMT_1, 0x01); //电源管理寄存器1,取消睡眠模式,选择时钟源为X轴陀螺仪
MPU6050_WriteReg(MPU6050_PWR_MGMT_2, 0x00); //电源管理寄存器2,保持默认值0,所有轴均不待机
MPU6050_WriteReg(MPU6050_SMPLRT_DIV, 0x09); //采样率分频寄存器,配置采样率
MPU6050_WriteReg(MPU6050_CONFIG, 0x06); //配置寄存器,配置DLPF
MPU6050_WriteReg(MPU6050_GYRO_CONFIG, 0x18); //陀螺仪配置寄存器,选择满量程为±2000°/s
MPU6050_WriteReg(MPU6050_ACCEL_CONFIG, 0x18); //加速度计配置寄存器,选择满量程为±16g
}
/**
* 函 数:MPU6050获取ID号
* 参 数:无
* 返 回 值:MPU6050的ID号
*/
uint8_t MPU6050_GetID(void)
{
return MPU6050_ReadReg(MPU6050_WHO_AM_I); //返回WHO_AM_I寄存器的值
}
/**
* 函 数:MPU6050获取数据
* 参 数:AccX AccY AccZ 加速度计X、Y、Z轴的数据,使用输出参数的形式返回,范围:-32768~32767
* 参 数:GyroX GyroY GyroZ 陀螺仪X、Y、Z轴的数据,使用输出参数的形式返回,范围:-32768~32767
* 返 回 值:无
*/
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; //定义数据高8位和低8位的变量
DataH = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_H); //读取加速度计X轴的高8位数据
DataL = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L); //读取加速度计X轴的低8位数据
*AccX = (DataH << 8) | DataL; //数据拼接,通过输出参数返回
DataH = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_H); //读取加速度计Y轴的高8位数据
DataL = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_L); //读取加速度计Y轴的低8位数据
*AccY = (DataH << 8) | DataL; //数据拼接,通过输出参数返回
DataH = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_H); //读取加速度计Z轴的高8位数据
DataL = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_L); //读取加速度计Z轴的低8位数据
*AccZ = (DataH << 8) | DataL; //数据拼接,通过输出参数返回
DataH = MPU6050_ReadReg(MPU6050_GYRO_XOUT_H); //读取陀螺仪X轴的高8位数据
DataL = MPU6050_ReadReg(MPU6050_GYRO_XOUT_L); //读取陀螺仪X轴的低8位数据
*GyroX = (DataH << 8) | DataL; //数据拼接,通过输出参数返回
DataH = MPU6050_ReadReg(MPU6050_GYRO_YOUT_H); //读取陀螺仪Y轴的高8位数据
DataL = MPU6050_ReadReg(MPU6050_GYRO_YOUT_L); //读取陀螺仪Y轴的低8位数据
*GyroY = (DataH << 8) | DataL; //数据拼接,通过输出参数返回
DataH = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_H); //读取陀螺仪Z轴的高8位数据
DataL = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_L); //读取陀螺仪Z轴的低8位数据
*GyroZ = (DataH << 8) | DataL; //数据拼接,通过输出参数返回
}
MPU6050.h
#ifndef __MPU6050_H
#define __MPU6050_H
void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data);
uint8_t MPU6050_ReadReg(uint8_t RegAddress);
void MPU6050_Init(void);
uint8_t MPU6050_GetID(void);
void MPU6050_GetData(int16_t *AccX, int16_t *AccY, int16_t *AccZ,
int16_t *GyroX, int16_t *GyroY, int16_t *GyroZ);
#endif
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "MPU6050.h"
uint8_t ID; //定义用于存放ID号的变量
int16_t AX, AY, AZ, GX, GY, GZ; //定义用于存放各个数据的变量
int main(void)
{
/*模块初始化*/
OLED_Init(); //OLED初始化
MPU6050_Init(); //MPU6050初始化
/*显示ID号*/
OLED_ShowString(1, 1, "ID:"); //显示静态字符串
ID = MPU6050_GetID(); //获取MPU6050的ID号
OLED_ShowHexNum(1, 4, ID, 2); //OLED显示ID号
while (1)
{
MPU6050_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);
}
}
运行结果:
本节的程序部分到这里就结束了,下节继续。
博主留言:今天百忙之中终于将这篇博客发出来了!由于前段时间去实习了,最近在备战电赛所以这个栏目停更两周,比完赛再继续接着更!感谢各位的关注。
我知道最近暑假很多人在学单片机,甚至有人再学32备战电赛,这里给个小小的建议,就是其实备战电赛的话32只要把本节和本节之前的内容学完就基本足以应对比赛中用到32的环节,现在是抓紧学习比赛中涉及到的别的模块,等比赛完之后再接着学习32剩下的部分,祝大家学习、实习或者比赛顺利!
QQ交流群:963138186
本篇就到这里,下篇继续!欢迎点击下方订阅本专栏↓↓↓